1. Connecting to AKS
az login # browser-based Azure sign-in
az account show --output table # which subscription am I on?
az account set --subscription "<name-or-id>"
az aks list --output table # cluster names + resource groups
az aks get-credentials --resource-group <rg> --name <cluster>
kubectl get nodes # verify
What get-credentials actually does: it calls the
Azure control plane, retrieves the cluster's API server address and certificate
authority data, and merges an entry into your kubeconfig at
%USERPROFILE%\.kube\config. It does not grant permissions — it only
tells kubectl where the cluster is and how to authenticate.
Authorization is a separate layer (see §17).
Useful variants:
az aks get-credentials -g <rg> -n <cluster> --overwrite-existing # replace a stale entry
az aks get-credentials -g <rg> -n <cluster> --admin # local admin, bypasses Entra ID
--admin uses a certificate baked into the cluster rather than your
Azure identity. It gives full cluster-admin and leaves no per-user audit trail,
which is why most organisations disable it
(--disable-local-accounts). Reach for it only when Entra auth itself
is what's broken.
Entra ID (AAD) integrated clusters
If the cluster uses Entra ID, kubectl needs the
kubelogin binary to exchange your Azure token for a Kubernetes
token. Convert the kubeconfig once so it reuses your existing
az login session instead of prompting with a device code every time:
kubelogin convert-kubeconfig -l azurecli
Other login modes exist (-l devicecode,
-l workloadidentity, -l spn); azurecli is
the right one for interactive use on your own machine.
If you don't have kubelogin, az aks install-cli
downloads both kubectl and kubelogin into
%USERPROFILE%\.azure-kubectl\ and
%USERPROFILE%\.azure-kubelogin\. Caveat: it
installs the version AKS considers current, which may be older than a
kubectl you already have, and can silently shadow it depending on PATH order.
Skip it if kubelogin already works.
Version skew
kubectl version
Kubernetes supports ±1 minor version of skew between client and server. A newer client against an older server usually works fine, but unexplained API errors are worth checking here first.
2. Mental Model: What You're Actually Talking To
Coming from Docker, this is the shift that makes the commands make sense:
| Docker | Kubernetes |
|---|---|
| You run a container | You declare a desired state; the cluster runs containers to match it |
docker run starts something | kubectl apply records an intention; a controller acts on it |
| The container is the unit | The Pod is the unit — one or more containers sharing a network namespace and volumes |
| Restart is manual | A Deployment continuously reconciles: kill a pod and a replacement appears |
This explains behaviour that otherwise looks broken:
kubectl delete pod Xand the pod comes back — correct. The Deployment's job is to maintain N replicas. To actually stop it, scale the Deployment to 0 or delete the Deployment.- You can't "restart" a pod. You delete it and let the controller create a fresh one, or use
kubectl rollout restarton its Deployment. - Changes made inside a pod with
execvanish on restart. Pods are cattle, not pets.
The rough hierarchy: Deployment → manages a ReplicaSet → manages Pods → contain Containers. A Service gives that changing set of pods one stable DNS name and IP.
3. Universal Flags
These work across most commands and are the bulk of what turns basic
kubectl get pods into something useful.
| Flag | Meaning |
|---|---|
-n <namespace> | Target a specific namespace |
-A / --all-namespaces | Every namespace (read commands only) |
-o wide | Extra columns — node, pod IP, container image |
-o yaml / -o json | The complete object as the API server stores it |
-o name | Just resource names — good for piping |
-o jsonpath="{...}" | Extract one specific field |
-w / --watch | Stream changes live instead of exiting |
-l key=value | Filter by label |
--show-labels | Add a labels column |
--sort-by=<jsonpath> | Sort output, e.g. --sort-by=.metadata.creationTimestamp |
-c <container> | Pick a container in a multi-container pod |
--dry-run=client -o yaml | Generate a manifest without touching the cluster |
On -o wide: this is worth making a habit. It shows
which node each pod landed on, which turns "some pods are failing" into "all
failing pods are on node 3" — a completely different investigation.
On labels: labels are how Kubernetes wires things together
internally. A Service finds its pods by label selector, not by name. So
kubectl get pods -l app=api shows you exactly the set of pods that
svc/api is routing to.
4. Listing and Finding Things
kubectl get pods # current namespace
kubectl get pods -A -o wide # everything, everywhere, with detail
kubectl get pods -w # watch state changes live
kubectl get svc,deploy,ingress # several resource types at once
kubectl get all # pods, services, deployments, replicasets
kubectl get pods -l app=nginx
kubectl get pods --field-selector status.phase=Running
kubectl get pods --sort-by=.status.startTime
kubectl api-resources # every resource type this cluster knows about
kubectl get all is mildly misleading — it does not
include ConfigMaps, Secrets, Ingresses, PVCs, or anything custom. It's a
shortcut for the common workload types, not a full inventory. For that,
kubectl api-resources lists what exists, including CRDs installed
by things like cert-manager or Istio.
Short names save typing and appear in most documentation:
| Full | Short |
|---|---|
pods | po |
services | svc |
deployments | deploy |
replicasets | rs |
namespaces | ns |
nodes | no |
configmaps | cm |
persistentvolumeclaims | pvc |
statefulsets | sts |
daemonsets | ds |
ingresses | ing |
5. Docker → kubectl Translation
| Docker | kubectl |
|---|---|
docker ps | kubectl get pods |
docker exec -it <c> bash | kubectl exec -it <pod> -- bash |
docker logs <c> | kubectl logs <pod> |
docker logs -f <c> | kubectl logs -f <pod> |
docker inspect <c> | kubectl describe pod <pod> or kubectl get pod <pod> -o yaml |
docker cp <c>:/f ./f | kubectl cp <pod>:/f ./f |
docker stats | kubectl top pods |
docker run --rm -it img sh | kubectl run tmp --rm -it --image=img -- sh |
docker rm -f <c> | kubectl delete pod <pod> (returns if managed) |
6. Getting a Shell Inside a Pod
kubectl exec -it <pod> -- bash # interactive shell
kubectl exec -it <pod> -- sh # fallback: alpine/busybox have no bash
kubectl exec -it <pod> -c <container> -- sh # multi-container pod
kubectl exec <pod> -- ls -la /app # one-off, no TTY needed
kubectl exec <pod> -- env # dump environment variables
kubectl exec <pod> -- cat /etc/config/app.conf
Why the --? It marks the end of kubectl's own
arguments. Everything after it is passed verbatim to the container. Omit it and
kubectl tries to interpret bash as one of its own parameters. This
matters most when your command has flags of its own:
kubectl exec pod -- ls -l works; without --, kubectl
would try to parse -l itself.
Why -it? -i keeps stdin open,
-t allocates a TTY. Same meaning as Docker. Interactive shells need
both. For a single non-interactive command, drop them.
If there's no shell at all — distroless and scratch images
genuinely contain no sh — use an ephemeral debug container, which
attaches a second container with real tooling into the running pod's namespaces:
kubectl debug -it <pod> --image=busybox --target=<container>
--target shares the process namespace so you can see the app's
processes and /proc. This is the modern replacement for the old
trick of baking debug tools into production images.
To spin up a throwaway pod for network testing from inside the cluster:
kubectl run netshoot --rm -it --image=nicolaka/netshoot -- bash
--rm deletes it on exit. Inside, you get dig,
curl, nslookup, tcpdump — everything for
diagnosing whether a Service name resolves and whether the pods behind it answer.
7. Logs
kubectl logs <pod>
kubectl logs -f <pod> # follow
kubectl logs --tail=100 <pod>
kubectl logs --since=15m <pod>
kubectl logs --since-time=2026-08-25T09:00:00Z <pod>
kubectl logs -p <pod> # PREVIOUS container instance
kubectl logs <pod> -c <container>
kubectl logs <pod> --all-containers=true
kubectl logs -l app=api --tail=50 # aggregate across all matching pods
kubectl logs deploy/<name> # picks one pod from the deployment
kubectl logs <pod> --timestamps
-p / --previous is the important one.
When a container crashes and restarts, plain kubectl logs shows the
new instance — which is often empty or still starting. The output that
explains the crash belongs to the dead instance, and -p is the only
way to see it. Any pod showing CrashLoopBackOff should get
kubectl logs -p immediately.
-l app=api streams from every pod with that label.
Invaluable when a Deployment has five replicas and only one is misbehaving — you
don't know which pod name to check yet.
Note that Kubernetes only retains logs for the current and previous container instance. Anything older is gone unless you're shipping logs somewhere. On AKS that's usually Azure Monitor / Container Insights, queryable with KQL in Log Analytics.
8. Inspecting and Debugging
kubectl describe pod <pod>
kubectl describe node <node>
kubectl describe svc <service>
kubectl get events --sort-by=.lastTimestamp
kubectl get events -A --sort-by=.lastTimestamp | Select-Object -Last 30
kubectl top pods
kubectl top pods -A --sort-by=memory
kubectl top nodes
kubectl get pod <pod> -o yaml
kubectl explain pod.spec.containers
kubectl explain deployment.spec.strategy --recursive
describe is the single most valuable debugging
command. It merges the object's spec, its current status, and —
critically — the recent Events attached to it, printed at the
bottom. Events tell you: the image couldn't be pulled, no node had enough
memory, the liveness probe failed, the volume couldn't mount. Roughly ninety
percent of "why won't this start" is answered there, and nowhere else.
Events expire. The default retention is one hour. An
event-less describe on a pod that broke yesterday isn't
evidence that nothing happened.
kubectl top requires metrics-server, which AKS
installs by default. It shows live usage, which is what you compare
against the resources.requests and resources.limits in
the pod spec to work out whether a pod is being throttled or OOM-killed.
kubectl explain reads the schema from the API
server itself, so it's always accurate for your cluster version,
including CRDs. Faster than searching documentation when you can't remember
whether it's imagePullPolicy or imagePullpolicy.
9. Contexts and Namespaces
A context is a saved bundle of {cluster, user, default
namespace}. Each az aks get-credentials adds one, so with several
AKS clusters you'll accumulate several.
kubectl config get-contexts # * marks the active one
kubectl config current-context
kubectl config use-context <name>
kubectl config set-context --current --namespace=<ns>
kubectl config delete-context <name>
kubectl config view --minify # just the active context, resolved
Set the default namespace. Typing -n production on
every command is how you eventually forget it on a delete.
set-context --current --namespace=<ns> makes it sticky for
that context.
The safety habit: run
kubectl config current-context before anything destructive. The
single most common serious mistake in Kubernetes is running the right
command against the wrong cluster. Tools like
kubectx/kubens or a PowerShell prompt that
displays the current context exist entirely because of this failure mode.
10. Deployments, Scaling, Rollouts
kubectl get deploy
kubectl rollout status deploy/<name> # blocks until rollout finishes or fails
kubectl rollout restart deploy/<name>
kubectl rollout history deploy/<name>
kubectl rollout history deploy/<name> --revision=3
kubectl rollout undo deploy/<name>
kubectl rollout undo deploy/<name> --to-revision=2
kubectl rollout pause deploy/<name>
kubectl rollout resume deploy/<name>
kubectl scale deploy/<name> --replicas=3
kubectl set image deploy/<name> <container>=<registry>/<image>:<tag>
kubectl set env deploy/<name> LOG_LEVEL=debug
rollout restart patches an annotation in the pod
template, which the Deployment sees as a spec change, triggering a normal
rolling replacement of every pod. It's the clean way to force pods to re-read a
mounted ConfigMap or re-pull a mutable :latest tag — with zero
downtime, unlike deleting pods by hand.
rollout status exits non-zero if the rollout
fails. That makes it the correct gate in a CI pipeline: deploy, then block on
rollout status, and fail the build if it doesn't converge.
rollout undo works because Kubernetes retains old
ReplicaSets (10 by default, tunable via revisionHistoryLimit).
Rolling back is just scaling the previous ReplicaSet back up.
A caution on set image and scale:
they change live cluster state directly. If your manifests live in Git and
something reapplies them — Flux, Argo CD, a pipeline — your change will be
reverted without warning. Imperative commands are for emergencies and
experiments; persistent changes belong in the manifest.
11. Applying and Deleting Manifests
kubectl apply -f manifest.yaml
kubectl apply -f ./k8s/ # every manifest in a directory
kubectl apply -f https://example.com/x.yaml
kubectl diff -f manifest.yaml # preview the change
kubectl delete -f manifest.yaml
kubectl delete pod <pod>
kubectl delete pod <pod> --grace-period=0 --force
kubectl delete pods --all -n <ns>
kubectl apply -f manifest.yaml --dry-run=server # validate against the real API
apply is declarative — it computes the difference
between your file and the live object and patches only what changed, so it works
identically for create and update. create fails if the object
exists; prefer apply almost always.
kubectl diff before apply is the
habit that prevents surprises. It shows exactly which fields will change against
the live cluster.
--force --grace-period=0 removes the object
from the API server without waiting for the kubelet to confirm the container
actually stopped. For a StatefulSet pod holding a volume, that risks two
instances briefly writing to the same storage. Use it on stuck
Terminating pods only when you understand what's holding them.
Generate a starting manifest instead of writing YAML from scratch:
kubectl create deploy web --image=nginx --dry-run=client -o yaml > web.yaml
kubectl create ns staging --dry-run=client -o yaml > ns.yaml
12. Port Forwarding
kubectl port-forward svc/<service> 8080:80
kubectl port-forward pod/<pod> 8080:80
kubectl port-forward deploy/<name> 8080:80
kubectl port-forward svc/<service> 8080:80 --address 0.0.0.0
Format is <local-port>:<remote-port>. Then browse to
http://localhost:8080.
This tunnels through the Kubernetes API server over your authenticated connection, so it works against a private AKS cluster with no public ingress and no VPN. It's the standard way to reach an internal database, a Grafana instance, or an admin UI that has no business being exposed to the internet.
Note that forwarding to svc/ picks one backing pod
and stays with it — it does not load balance. Fine for debugging, misleading if
you're trying to test balancing behaviour.
--address 0.0.0.0 exposes the forward to your local network. Only when you actually need it.
13. ConfigMaps and Secrets
kubectl get cm
kubectl get cm <name> -o yaml
kubectl describe cm <name>
kubectl get secret
kubectl get secret <name> -o jsonpath="{.data.password}" # still base64
kubectl create cm app-config --from-file=./app.conf
kubectl create secret generic db-creds --from-literal=password=s3cret
Secret values are base64-encoded, not encrypted. Anyone with read access on the Secret has the plaintext. Base64 is there to allow binary data, not to provide security. Real protection comes from RBAC restricting who can read Secrets, plus encryption at rest — on AKS, typically Azure Key Vault via the Secrets Store CSI driver.
Decode in PowerShell (there's no base64 -d on Windows):
$b = kubectl get secret db-creds -o jsonpath="{.data.password}"
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b))
As a reusable function in $PROFILE:
function Get-K8sSecret {
param($Name, $Key, $Namespace = "default")
$b = kubectl get secret $Name -n $Namespace -o jsonpath="{.data.$Key}"
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b))
}
# Get-K8sSecret db-creds password
Dump every key in a Secret at once:
kubectl get secret db-creds -o json | ConvertFrom-Json |
ForEach-Object { $_.data.PSObject.Properties } |
ForEach-Object { "$($_.Name) = $([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($_.Value)))" }
Editing a ConfigMap does not restart pods. Values injected as
environment variables are read once at container start. Volume-mounted
ConfigMaps update in place after a delay, but only if the app watches the file.
Follow a ConfigMap change with
kubectl rollout restart deploy/<name>.
14. Node Operations
kubectl get nodes -o wide
kubectl describe node <node>
kubectl top nodes
kubectl get pods -A -o wide --field-selector spec.nodeName=<node>
kubectl cordon <node> # stop scheduling new pods here
kubectl uncordon <node>
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
cordon marks a node unschedulable but leaves running pods alone.
drain cordons and evicts everything, respecting
PodDisruptionBudgets, so workloads reschedule elsewhere first. That's the
correct sequence before node maintenance.
--ignore-daemonsets is nearly always required, because DaemonSet
pods (CNI, kube-proxy, log agents) are meant to run on every node and can't be
drained meaningfully.
On AKS, node pool upgrades and scaling are managed through az aks
rather than kubectl — manually draining a node the cluster autoscaler manages
may just get it replaced.
15. AKS-Specific az Commands
az aks list --output table
az aks show -g <rg> -n <cluster> --output table
az aks nodepool list -g <rg> --cluster-name <cluster> --output table
az aks scale -g <rg> -n <cluster> --node-count 5
az aks get-upgrades -g <rg> -n <cluster> --output table
az aks browse -g <rg> -n <cluster> # dashboard (deprecated on newer clusters)
az aks command invoke -g <rg> -n <cluster> --command "kubectl get pods -A"
az aks command invoke runs a command from inside
the cluster via the Azure control plane. It's the escape hatch for
private clusters whose API server has no public endpoint — you
get kubectl access without a jumpbox or VPN.
Attach an Azure Container Registry so nodes can pull images without imagePullSecrets:
az aks update -g <rg> -n <cluster> --attach-acr <acr-name>
This grants the cluster's managed identity AcrPull on the registry.
Missing this attachment is the usual cause of ImagePullBackOff on a
fresh AKS + ACR setup.
16. PowerShell Setup: Aliases and Completion
Open your profile:
notepad $PROFILE
If it doesn't exist:
New-Item -ItemType File -Path $PROFILE -Force
Add:
Set-Alias k kubectl
# Tab completion for resource names, namespaces, contexts
kubectl completion powershell | Out-String | Invoke-Expression
# Make completion work for the `k` alias too
Register-ArgumentCompleter -CommandName k -ScriptBlock $__kubectlCompleterBlock
# Handy shortcuts
function kgp { kubectl get pods @args }
function kgpa { kubectl get pods -A -o wide @args }
function kd { kubectl describe @args }
function kl { kubectl logs @args }
function kctx { kubectl config get-contexts }
function kns { param($n) kubectl config set-context --current --namespace=$n }
@args splats arguments through, so kgp -n kube-system works as expected.
The Register-ArgumentCompleter line depends on
$__kubectlCompleterBlock, defined by the
kubectl completion powershell output — it must come after
that line. If it errors on a given kubectl version, drop it and type
kubectl in full when you want completion.
Reload without restarting the terminal:
. $PROFILE
17. Troubleshooting Playbook
A pod won't start
kubectl get pods # 1. what status?
kubectl describe pod <pod> # 2. read the Events at the bottom
kubectl logs <pod> # 3. app output, if it got far enough
kubectl logs -p <pod> # 4. if it's restarting, this is the real evidence
Work in that order. describe catches infrastructure problems
(image, scheduling, mounts, probes); logs catches application
problems. Reaching for logs first on an ImagePullBackOff wastes
time, because the container never ran and there are no logs.
A Service returns nothing
kubectl get endpoints <service>
Empty endpoints means the Service's label selector matches no ready pods. This is by far the most common Service failure, and it's usually one of two things: a typo in the selector, or the pods exist but aren't passing their readiness probe (unready pods are deliberately excluded).
kubectl describe svc <service> # check the Selector line
kubectl get pods --show-labels # compare against actual pod labels
kubectl run tmp --rm -it --image=nicolaka/netshoot -- curl http://<service>.<ns>:80
Also verify the Service's targetPort matches the container's real
listening port — port is what the Service exposes,
targetPort is where it forwards.
Permission denied / Forbidden
kubectl auth can-i list pods
kubectl auth can-i create deployments -n production
kubectl auth can-i --list # everything you're allowed to do
get-credentials succeeding only means you can reach the cluster.
Authorization is separate: Azure RBAC roles (Azure Kubernetes Service RBAC
Reader/Writer/Admin) or in-cluster RoleBindings.
auth can-i --list is the fastest way to see what your identity
actually holds.
Resource pressure
kubectl top pods -A --sort-by=memory
kubectl top nodes
kubectl describe node <node> # see the Allocated resources section
kubectl get events -A --field-selector reason=OOMKilling
describe node shows requests and limits committed versus capacity.
A node can be fully committed while barely used, because
scheduling is based on requests, not actual consumption — that's why pods go
Pending on a cluster that looks idle in top.
18. Common Pod Status Meanings
| Status | What it means | First move |
|---|---|---|
Pending | Not scheduled to a node yet | describe pod — usually insufficient CPU/memory, or an unsatisfiable node selector / taint |
ContainerCreating | Scheduled, still setting up | Normal briefly; if stuck, it's a volume mount or CNI problem |
ImagePullBackOff / ErrImagePull | Can't fetch the image | Wrong tag, wrong registry, or missing credentials — on AKS check --attach-acr |
CrashLoopBackOff | Starts, exits, restarts, repeatedly | kubectl logs -p — the app is failing at startup |
OOMKilled | Exceeded its memory limit | Raise resources.limits.memory or fix the leak |
Error | Container exited non-zero | kubectl logs -p |
Terminating (stuck) | Deletion blocked | A finalizer or an unresponsive kubelet; --force --grace-period=0 as last resort |
Completed | Exited zero | Normal for Jobs; wrong for a long-running service |
Running but not ready (0/1) | Started, failing readiness probe | describe pod for probe failures; excluded from Service endpoints until ready |
The BackOff in these names refers to exponential retry delay —
Kubernetes waits progressively longer between attempts, up to five minutes. A
pod that's been failing a while won't retry immediately after you fix the
underlying problem; kubectl delete pod to force a fresh attempt.
Quick Reference
# Where am I?
kubectl config current-context
kubectl config view --minify | Select-String namespace
# What's running?
kubectl get pods -A -o wide
# Why is it broken?
kubectl describe pod <pod>
kubectl logs -p <pod>
# Get inside
kubectl exec -it <pod> -- sh
# Reach it locally
kubectl port-forward svc/<service> 8080:80
# Restart cleanly
kubectl rollout restart deploy/<name>
kubectl rollout status deploy/<name>