Kubernetes / AKS Cheat Sheet

A working reference for kubectl on Azure Kubernetes Service, from Windows Terminal (PowerShell). Every section explains why the command exists, not just the syntax.

kubectl Azure AKS PowerShell

1. Connecting to AKS

PowerShell
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:

PowerShell
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:

PowerShell
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

PowerShell
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:

DockerKubernetes
You run a containerYou declare a desired state; the cluster runs containers to match it
docker run starts somethingkubectl apply records an intention; a controller acts on it
The container is the unitThe Pod is the unit — one or more containers sharing a network namespace and volumes
Restart is manualA Deployment continuously reconciles: kill a pod and a replacement appears

This explains behaviour that otherwise looks broken:

  • kubectl delete pod X and 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 restart on its Deployment.
  • Changes made inside a pod with exec vanish 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.

FlagMeaning
-n <namespace>Target a specific namespace
-A / --all-namespacesEvery namespace (read commands only)
-o wideExtra columns — node, pod IP, container image
-o yaml / -o jsonThe complete object as the API server stores it
-o nameJust resource names — good for piping
-o jsonpath="{...}"Extract one specific field
-w / --watchStream changes live instead of exiting
-l key=valueFilter by label
--show-labelsAdd 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 yamlGenerate 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

PowerShell
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:

FullShort
podspo
servicessvc
deploymentsdeploy
replicasetsrs
namespacesns
nodesno
configmapscm
persistentvolumeclaimspvc
statefulsetssts
daemonsetsds
ingressesing

5. Docker → kubectl Translation

Dockerkubectl
docker pskubectl get pods
docker exec -it <c> bashkubectl 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 ./fkubectl cp <pod>:/f ./f
docker statskubectl top pods
docker run --rm -it img shkubectl 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

PowerShell
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:

PowerShell
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:

PowerShell
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

PowerShell
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

PowerShell
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.

PowerShell
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

PowerShell
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

PowerShell
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:

PowerShell
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

PowerShell
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

PowerShell
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):

PowerShell
$b = kubectl get secret db-creds -o jsonpath="{.data.password}"
[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b))

As a reusable function in $PROFILE:

PowerShell
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:

PowerShell
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

PowerShell
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

PowerShell
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:

PowerShell
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:

PowerShell
notepad $PROFILE

If it doesn't exist:

PowerShell
New-Item -ItemType File -Path $PROFILE -Force

Add:

PowerShell
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:

PowerShell
. $PROFILE

17. Troubleshooting Playbook

A pod won't start

PowerShell
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

PowerShell
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).

PowerShell
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

PowerShell
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

PowerShell
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

StatusWhat it meansFirst move
PendingNot scheduled to a node yetdescribe pod — usually insufficient CPU/memory, or an unsatisfiable node selector / taint
ContainerCreatingScheduled, still setting upNormal briefly; if stuck, it's a volume mount or CNI problem
ImagePullBackOff / ErrImagePullCan't fetch the imageWrong tag, wrong registry, or missing credentials — on AKS check --attach-acr
CrashLoopBackOffStarts, exits, restarts, repeatedlykubectl logs -p — the app is failing at startup
OOMKilledExceeded its memory limitRaise resources.limits.memory or fix the leak
ErrorContainer exited non-zerokubectl logs -p
Terminating (stuck)Deletion blockedA finalizer or an unresponsive kubelet; --force --grace-period=0 as last resort
CompletedExited zeroNormal for Jobs; wrong for a long-running service
Running but not ready (0/1)Started, failing readiness probedescribe 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

PowerShell
# 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>