Containerization Fundamentals

kubectl First Steps: Your First Local Cluster & Deployment

You have learned what containers and Kubernetes are, and you know the core objects — Pods, Deployments, Services. Now you actually drive the thing. kubectl (say it “cube-control” or “cube-cuttle”; nobody agrees) is the command-line tool you use to talk to any Kubernetes cluster, anywhere. In this lesson you will install it, spin up a real cluster on your own laptop for free, write a Deployment and a Service as YAML, apply them, reach the running app from your browser, read its logs, and clean everything up.

By the end you will understand the single most important workflow in Kubernetes — the declarative kubectl apply loop — and you will be able to picture exactly what the cluster does in the half-second after you press Enter. That mental model is what separates someone who copies commands from someone who can debug a cluster.

Learning objectives

Prerequisites & where this fits

You need basic comfort with a terminal and Docker (or Podman) already installed and running — see Containers & Docker Basics if that part is new. You should also know what a Pod, Deployment, and Service are, because we deploy all three here. This is Lesson 4 of the Kubernetes Zero-to-Hero course (Fundamentals module) and the first one where you operate a cluster end to end with your own hands. Everything afterward — Helm, GitOps, autoscaling — is built on the apply loop you learn today.

kubectl and the cluster: two separate things

A common beginner trap is thinking kubectl is Kubernetes. It is not. kubectl is a small client program on your machine. The cluster is a separate set of machines (or, today, containers pretending to be machines) running the Kubernetes control plane and your workloads. kubectl turns your commands into HTTPS requests to the cluster’s API server and prints the responses. That is the whole relationship.

your laptop                          the cluster
┌─────────────┐   HTTPS (REST)   ┌──────────────────────┐
│  kubectl    │ ───────────────► │  kube-apiserver       │
│  (+kubeconfig)│ ◄─────────────── │  (the only front door)│
└─────────────┘    JSON           └──────────────────────┘

Because it is just a client, the same kubectl binary works against a tiny local cluster, a managed cloud cluster (EKS, AKS, GKE), or a customer’s air-gapped cluster. What changes is which cluster it points at — and that is decided entirely by the kubeconfig.

Install kubectl

Pick your platform. Verify with kubectl version --client.

# macOS (Homebrew)
brew install kubectl

# Linux (official binary)
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

# Windows (winget) — run in PowerShell
winget install -e --id Kubernetes.kubectl

# Verify (works even with no cluster yet)
kubectl version --client
# Client Version: v1.30.x

Keep kubectl within one minor version of your cluster (e.g. a v1.30 client against a v1.29–v1.31 server). A big skew can make commands behave oddly.

kubeconfig and contexts: which cluster am I about to change?

Every kubectl command answers one question first: which cluster, as which user, in which namespace? That answer lives in a YAML file called the kubeconfig, by default at ~/.kube/config. It has three lists stitched together by contexts:

Section What it holds
clusters The API server URL + its CA certificate (how to find and trust the cluster)
users Your credentials (a client cert, token, or an exec plugin that fetches one)
contexts A named triple: which cluster + which user + which default namespace

The current context is the one kubectl uses unless you say otherwise. These are the commands you will run constantly:

kubectl config get-contexts          # list all; the * marks the current one
kubectl config current-context       # just the name
kubectl config use-context kind-demo # switch clusters
kubectl config set-context --current --namespace=web   # stop typing -n every time

This sounds dull until the day you run kubectl delete against production thinking you were on staging. Before any change, glance at kubectl config current-context. Senior engineers bake the context name into their shell prompt for exactly this reason. Treat it as a seatbelt.

Imperative vs. declarative: two ways to tell Kubernetes what you want

There are two styles for creating things, and knowing which is which is a genuine interview topic.

Imperative = you issue a command that performs an action now. You describe the steps.

kubectl run web --image=nginx:1.27 --port=80   # create one Pod
kubectl create deployment web --image=nginx:1.27 --replicas=3
kubectl scale deployment web --replicas=5
kubectl delete pod web

Great for quick experiments, debugging, and one-off tasks. The downside: the desired state lives only in your shell history. There is no file to review, diff, or commit.

Declarative = you write a file describing the desired end state, and kubectl apply makes reality match it. You describe the destination, not the steps.

kubectl apply -f deployment.yaml     # create or update to match the file
kubectl diff  -f deployment.yaml     # preview the change first (do this!)
kubectl apply -f k8s/ --recursive    # a whole directory of manifests

Declarative is how real systems are run, because the YAML can live in Git, be reviewed in a pull request, and be re-applied identically a hundred times. apply is idempotent: apply the same file twice and the second run is a no-op. This is the foundation of GitOps, which you will meet later in the course.

Imperative Declarative
You specify The action / steps The desired end state
Source of truth Your shell history A YAML file (in Git)
Repeatable? No (you re-type) Yes (apply is idempotent)
Best for Debugging, learning, one-offs Production, teams, GitOps

A pro tip that bridges the two: generate YAML from an imperative command, then save and edit it. --dry-run=client builds the object without sending it to the cluster:

kubectl create deployment web --image=nginx:1.27 --dry-run=client -o yaml > deployment.yaml

That single trick — imperative to scaffold, declarative to keep — is also the fastest way to write correct YAML in the CKAD exam.

What actually happens when you run kubectl apply

This is the heart of the lesson. When you apply a Deployment, a precise relay race runs inside the cluster. Understanding it means you can debug any “my pod isn’t running” problem by asking which baton got dropped.

How a kubectl apply flows through the Kubernetes control plane to a running pod

Walking the diagram, step by step:

  1. kubectl → API server. kubectl reads your YAML, sends it as an HTTPS request to the kube-apiserver — the cluster’s only front door. The server authenticates you (who are you?), authorizes you (are you allowed? — that is RBAC), and validates the object (is this valid YAML for a Deployment?).
  2. API server → etcd. The validated object is written to etcd, the cluster’s database — its single source of truth for desired state. At this instant the API call returns success and your prompt comes back. Note: the Pod does not exist yet. You have only recorded an intention.
  3. Controllers notice. The Deployment controller is watching the API server. It sees a new Deployment wanting 2 replicas, so it creates a ReplicaSet; the ReplicaSet controller sees that and creates 2 Pod objects — still just records in etcd, marked Pending, with no node assigned.
  4. Scheduler → assigns a node. The kube-scheduler watches for unscheduled Pods, picks a suitable node (enough CPU/memory, no blocking taints), and writes that choice back. The Pod now has a home but is still not running.
  5. kubelet → starts the container. The kubelet on the chosen node sees a Pod assigned to it, tells the container runtime (containerd) to pull the image and start the container, then reports the Pod’s status — ContainerCreating, then Running — back to the API server, which records it in etcd.
  6. Reconciliation never stops. This is the magic. Kubernetes constantly compares desired state (etcd: “2 replicas”) with actual state (“how many are running?”). Kill a Pod and the ReplicaSet controller notices the gap and creates a replacement within seconds. You declared what you want; the cluster’s job is to keep making it true. This control loop is the single idea that makes Kubernetes self-healing.

The practical payoff: when something is wrong, you debug by stage. Pod stuck Pending? The scheduler could not place it (step 4) — check node capacity and taints. ImagePullBackOff? The kubelet’s runtime cannot fetch the image (step 5) — check the image name and registry auth. CrashLoopBackOff? The container starts then dies (step 5) — read its logs. You are not guessing; you are asking which baton was dropped.

Hands-on lab: your first cluster and deployment (free, local)

We will create a local cluster with kind (Kubernetes IN Docker), apply a Deployment + Service, reach it from your browser, read its logs, and clean up. kind is free and runs entirely on your machine. If you prefer minikube or k3d, the only command that changes is cluster creation; everything from kubectl apply onward is identical.

1. Install kind and create a cluster

# Install kind (macOS). Linux/Windows: https://kind.sigs.k8s.io/docs/user/quick-start/
brew install kind

# Create a one-node cluster named "demo" (Docker must be running)
kind create cluster --name demo

Creating the cluster automatically adds a context named kind-demo to your kubeconfig and switches to it. Confirm the cluster is up and you are pointed at it:

kubectl config current-context
# kind-demo

kubectl get nodes
# NAME                 STATUS   ROLES           AGE   VERSION
# demo-control-plane   Ready    control-plane   40s   v1.30.x

Alternative: minikube start (creates context minikube) or k3d cluster create demo (context k3d-demo). Pick one; the rest is the same.

2. Write the Deployment + Service YAML

Create a file app.yaml. It declares two objects separated by ---: a Deployment running 2 nginx replicas, and a Service that gives them one stable in-cluster address. The Service finds its Pods by label selector (app: web) — the same label set on the Pod template.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web          # the Deployment manages Pods with this label
  template:
    metadata:
      labels:
        app: web        # Pods are stamped with this label
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web            # routes to any Pod carrying app: web
  ports:
    - port: 80
      targetPort: 80

3. Apply it and watch the relay race happen

kubectl apply -f app.yaml
# deployment.apps/web created
# service/web created

# Watch the Pods march from Pending -> ContainerCreating -> Running
kubectl get pods -l app=web --watch
# NAME                   READY   STATUS    RESTARTS   AGE
# web-6f8c9d4b7-2xk9p    1/1     Running   0          8s
# web-6f8c9d4b7-q4m2t    1/1     Running   0          8s
# (press Ctrl+C to stop watching)

You just witnessed steps 2–5 from the diagram in real time. Inspect the whole set and read the Events — the bottom of describe is where Kubernetes tells you what it did and why:

kubectl get deploy,rs,pods,svc -l app=web
kubectl describe deployment web        # scroll to "Events:" at the bottom

4. Reach the app with port-forward and curl (the validation step)

A ClusterIP Service is only reachable inside the cluster. To hit it from your laptop without exposing anything publicly, use port-forwardkubectl opens a tunnel from a local port to the Service.

# Tunnel localhost:8080 -> the Service's port 80 (leave this running)
kubectl port-forward svc/web 8080:80
# Forwarding from 127.0.0.1:8080 -> 80

In a second terminal, validate that the app responds:

curl -s http://localhost:8080 | head -n 4
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>

Seeing nginx’s welcome HTML is your proof the full chain works: Service → Pod → container, reachable through the tunnel. (Open http://localhost:8080 in a browser for the same page.) Press Ctrl+C in the first terminal to close the tunnel.

5. Logs and exec

# Logs from the Deployment's pods (-f follows; --previous shows a crashed container)
kubectl logs -l app=web --tail=20
# 127.0.0.1 - - [14/Jun/2026:...] "GET / HTTP/1.1" 200 ...   <- your curl

# Open a shell inside a running container
kubectl exec -it deploy/web -- sh
#  / # ls /usr/share/nginx/html
#  / # exit

logs, exec, and port-forward are your three everyday debugging tools — read output, look inside, and reach a service. You now have all three.

6. Cleanup

# Remove just the app
kubectl delete -f app.yaml
# deployment.apps "web" deleted
# service "web" deleted

# Delete the whole cluster (frees all Docker resources)
kind delete cluster --name demo
# (minikube: minikube delete   |   k3d: k3d cluster delete demo)

Cost note: Everything here is free and local — kind/minikube/k3d run as containers on your own machine, with no cloud account and nothing to pay for. Deleting the cluster reclaims the CPU and memory it used. Always tear down clusters you are done with so they are not idling in the background.

Common mistakes & troubleshooting

Symptom Likely cause Fix
The connection to the server localhost:8080 was refused No current context / no cluster running kind create cluster (or start it), then kubectl config current-context
Changes hit the wrong cluster You forgot which context was current Always check kubectl config current-context before a change; put it in your prompt
Pod stuck Pending Scheduler can’t place it (no capacity, a taint, an unbound PVC) kubectl describe pod <name> → read Events; check node resources
ImagePullBackOff Wrong image name/tag, or private registry needs auth Fix the image: value; for private registries add an imagePullSecret
CrashLoopBackOff Container starts then exits (bad command, missing config) kubectl logs <pod> --previous; check the command/env/probes
curl to port-forward fails Tunnel closed, or targetPort ≠ container port Keep the port-forward terminal open; confirm targetPort matches containerPort

Best practices

Security notes

Quick check

  1. What does kubectl config current-context tell you, and why glance at it before any change?
  2. In one sentence each, contrast the imperative and declarative styles.
  3. Put these in order for a kubectl apply of a Deployment: scheduler assigns a node · object written to etcd · kubelet starts the container · API server validates the request.
  4. Your Pod is stuck in Pending. Which stage failed, and which command shows you why?
  5. Why can’t you curl a ClusterIP Service directly from your laptop, and what bridges the gap in this lab?

Answers

  1. It prints the name of the cluster/user/namespace kubectl will act on. You check it so you don’t accidentally change the wrong cluster (e.g. prod instead of staging).
  2. Imperative: you run a command that performs an action now (you specify the steps). Declarative: you write a file describing the desired end state and kubectl apply makes reality match it (you specify the destination).
  3. API server validates the request → object written to etcd → scheduler assigns a node → kubelet starts the container.
  4. Scheduling failed — the scheduler couldn’t place the Pod on any node. kubectl describe pod <name> and read the Events section (usually insufficient resources or a taint).
  5. A ClusterIP Service has an address that only exists inside the cluster. kubectl port-forward svc/web 8080:80 opens a tunnel from your laptop’s port 8080 to the Service, so curl http://localhost:8080 reaches it.

Exercise

Starting from a fresh local cluster, do the following without copying the lab’s app.yaml verbatim:

  1. Scaffold a Deployment for httpd:2.4 (Apache) named site using kubectl create deployment ... --dry-run=client -o yaml, redirect it to site.yaml, and edit it to run 3 replicas on containerPort: 80.
  2. Add a Service to the same file (use ---) selecting app: site on port 80.
  3. Run kubectl diff -f site.yaml (note it shows everything as new), then kubectl apply -f site.yaml.
  4. Port-forward and curl until you see Apache’s It works! page. Confirm 3 Pods are Running.
  5. Edit the file to 5 replicas, run kubectl diff -f site.yaml (see only the replica change), then apply and watch two new Pods appear.
  6. Delete one Pod by name and watch the ReplicaSet recreate it — reconciliation in action. Clean up with kubectl delete -f site.yaml and kind delete cluster.

Stretch: break it on purpose — set the image to httpd:does-not-exist, apply, and identify the failure stage from kubectl get pods and kubectl describe pod.

Interview questions

Certification mapping

Cert How this lesson maps
KCNA “Kubernetes Fundamentals” — kubectl basics, the declarative model, and the apply/reconcile flow are core knowledge-level topics.
CKAD Directly exam-relevant. The whole exam is kubectl in a terminal: apply -f, --dry-run=client -o yaml to scaffold YAML fast, logs, exec, port-forward, and reading describe are everyday CKAD moves.
CKA Administrator focus: managing kubeconfig/contexts and diagnosing by stage (Pending → scheduler, ImagePull → kubelet/runtime) is the foundation of the troubleshooting domain.
CKS Builds on this: securing kubeconfig credentials and gating exec/access with RBAC are CKS concerns once the basics here are second nature.

Glossary

Next steps

You can now drive a cluster: write YAML, apply, observe, debug, clean up. The next leap is packaging many manifests into reusable, parameterized releases instead of hand-editing YAML per environment.

KuberneteskubectlkindDeploymentYAML
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading