Kubernetes Lesson 15 – First Kubernetes Deployment | Dataplexa
Kubernetes Fundamentals · Lesson 15

First Kubernetes Deployment

This is where the last 14 lessons snap together — you're going to deploy a real multi-replica application, expose it to traffic, verify it's healthy, and understand exactly what Kubernetes did behind the scenes to make it happen.

Why You Almost Never Create Raw Pods

Back in Lesson 8, you learned what a Pod is. And in Lesson 14, you created one directly with a YAML manifest. But here's the thing — in real production environments, you almost never create Pods directly. Raw Pods have one fatal flaw: if the node they're running on dies, the Pod dies with it and nothing brings it back. There's no supervisor. No resurrection. Just gone.

A Deployment fixes this. A Deployment wraps your Pods in a management layer that continuously watches them. If a Pod crashes, the Deployment creates a new one. If you need 5 replicas, the Deployment ensures exactly 5 are always running. If you push a new image version, the Deployment handles the rollout without downtime. This is why Deployments are the default way to run workloads in Kubernetes — and why this lesson is the most important one in the fundamentals section.

🎯 The mental model: A Deployment is a manager. It creates a ReplicaSet (from Lesson 9), which in turn creates and owns the Pods. You talk to the Deployment — the Deployment talks to the ReplicaSet — the ReplicaSet talks to the Pods. You never need to touch ReplicaSets or Pods directly. The Deployment handles everything.

The Deployment Ownership Chain

Understanding the three-tier ownership structure is the key to understanding everything that happens when you run kubectl apply on a Deployment manifest.

YOU (kubectl apply)
Deployment
Manages rollouts, rollbacks, scaling strategy
ReplicaSet
Maintains exact replica count — auto-created by Deployment
Pod 1
Running
Pod 2
Running
Pod 3
Running
☠️ Pod 2 crashes → ReplicaSet notices → creates Pod 4 automatically

Writing Your First Deployment Manifest

The scenario: You're a DevOps engineer at a fast-growing fintech startup. The backend team just finished version 1.0 of a new payment API and it needs to go live. The engineering lead wants three replicas minimum for high availability — if one Pod goes down, the other two keep handling payments. She also needs it accessible from within the cluster so the frontend service can reach it. You need to write the Deployment manifest, the Service, and get it live today.

apiVersion: apps/v1         # Deployments live in the apps API group, version 1
kind: Deployment            # We're creating a Deployment — the production-standard way to run Pods
metadata:
  name: payment-api         # Name of this Deployment object — unique within the namespace
  namespace: default        # Which namespace to deploy into
  labels:
    app: payment-api        # Label on the Deployment itself — useful for kubectl filtering
    team: backend           # Team label — great for multi-team clusters
spec:
  replicas: 3               # Run exactly 3 Pods at all times — Kubernetes enforces this continuously
  selector:                 # selector: how this Deployment finds and owns its Pods
    matchLabels:
      app: payment-api      # Own any Pod that has the label app=payment-api
  template:                 # template: the Pod blueprint — everything below is applied to each Pod
    metadata:
      labels:
        app: payment-api    # CRITICAL: these labels must match the selector above — if they don't, kubectl apply fails
        version: "1.0"      # Extra label on the Pod — useful for canary deployments later
    spec:
      containers:
        - name: payment-api           # Name of the container within the Pod
          image: nginx:1.25           # Using nginx as a stand-in for the payment API image
          ports:
            - containerPort: 8080     # The port the app listens on inside the container
          resources:
            requests:
              cpu: "100m"             # 0.1 CPU guaranteed — used by the scheduler for placement
              memory: "128Mi"         # 128MB RAM guaranteed
            limits:
              cpu: "300m"             # 0.3 CPU ceiling — container throttled if it exceeds this
              memory: "256Mi"         # 256MB RAM ceiling — container OOM-killed if exceeded
          env:
            - name: APP_ENV           # Inject an environment variable into the container
              value: "production"     # Value accessible as $APP_ENV inside the container
            - name: PORT
              value: "8080"           # Tell the app which port to bind to
$ kubectl apply -f payment-deployment.yaml
deployment.apps/payment-api created

$ kubectl get deployments
NAME          READY   UP-TO-DATE   AVAILABLE   AGE
payment-api   3/3     3            3           18s

$ kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
payment-api-7d6b9c8f4d-2xkpj   1/1     Running   0          18s
payment-api-7d6b9c8f4d-8rvnq   1/1     Running   0          18s
payment-api-7d6b9c8f4d-m4czl   1/1     Running   0          18s

What just happened?

spec.replicas: 3 — You declared 3 replicas. The Deployment controller received this, created a ReplicaSet, and the ReplicaSet launched 3 Pods. This number is now a contract — if you manually delete one of those Pods, the ReplicaSet immediately creates a replacement. Try it: kubectl delete pod payment-api-7d6b9c8f4d-2xkpj and then instantly run kubectl get pods — you'll see a brand new Pod spinning up.

spec.selector.matchLabels — This is the ownership declaration. The Deployment says "I own any Pod with label app=payment-api." The template labels must match this selector or Kubernetes will reject the manifest. This is a hard validation error — not a warning.

Pod names — Notice the pattern: payment-api-7d6b9c8f4d-2xkpj. That's [deployment-name]-[replicaset-hash]-[pod-hash]. The middle hash (7d6b9c8f4d) identifies which ReplicaSet owns this Pod. When you roll out a new image, the ReplicaSet hash changes — that's how you can tell which generation of Pods are from which rollout.

Deployment READY 3/3UP-TO-DATE: 3 means 3 Pods are running the desired spec. AVAILABLE: 3 means 3 are passing readiness checks and can receive traffic. READY: 3/3 combines both. Any number lower than your replica count here means something is wrong.

Exposing the Deployment with a Service

The Pods are running, but nothing can reach them yet. Each Pod has its own IP address — but those IPs are temporary. Every time a Pod restarts, it gets a new IP. You need a stable address that other services can depend on. That's what a Service provides.

The scenario: Your payment API Pods are live. Now the frontend team's checkout service needs to call it. They can't be hardcoding Pod IPs — those change. They need a stable DNS name and a load-balanced endpoint that automatically distributes their requests across all three healthy Pods. Here's the Service that makes that happen.

apiVersion: v1              # Services are in the core v1 API group
kind: Service               # Creating a Service — stable network endpoint for our Pods
metadata:
  name: payment-api-svc     # Service name — this becomes the DNS hostname inside the cluster
  namespace: default        # Must be in the same namespace as the Pods it targets
spec:
  selector:
    app: payment-api        # Match all Pods with label app=payment-api — our 3 Deployment Pods
  ports:
    - protocol: TCP         # TCP is default but explicit here for clarity
      port: 80              # Port the Service exposes to other services in the cluster
      targetPort: 8080      # Port to forward traffic to on each matched Pod (our containerPort)
  type: ClusterIP           # ClusterIP = internal-only — reachable only within the cluster
                            # Other types: NodePort (external via node IP), LoadBalancer (cloud LB)
$ kubectl apply -f payment-service.yaml
service/payment-api-svc created

$ kubectl get services
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes        ClusterIP   10.96.0.1       <none>        443/TCP   5d
payment-api-svc   ClusterIP   10.96.214.88    <none>        80/TCP    6s

$ kubectl describe service payment-api-svc
Name:              payment-api-svc
Namespace:         default
Selector:          app=payment-api
Type:              ClusterIP
IP:                10.96.214.88
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         10.244.0.5:8080,10.244.1.3:8080,10.244.2.7:8080
Session Affinity:  None

What just happened?

CLUSTER-IP: 10.96.214.88 — Kubernetes assigned the Service a stable virtual IP. This IP never changes for the lifetime of the Service. Any Pod in the cluster can now call http://10.96.214.88 or the DNS name http://payment-api-svc to reach the payment API.

Endpoints: 10.244.0.5:8080, 10.244.1.3:8080, 10.244.2.7:8080 — These are the actual Pod IPs. The Service's job is to keep this list up-to-date. When a Pod crashes and a new one appears with a different IP, kube-proxy updates the endpoints list automatically. Callers keep hitting the same Service address — they never notice the Pod churn underneath.

port: 80 vs targetPort: 8080 — The Service listens on port 80 (easy to remember, no port in the URL). But your app container actually listens on 8080. The Service does the translation. This decoupling means you can change the internal port your app uses without forcing every caller to update their URL.

Inspecting a Running Deployment

Deploying is one thing. Knowing how to inspect what you deployed is equally important — especially when something isn't working at 3am.

The scenario: It's your first week as an SRE and your on-call shift just started. An alert fires — the payment API is showing elevated error rates. You need to quickly assess the state of the Deployment, check which Pods are healthy, and look at the logs from the failing Pod. Here are the commands you reach for first.

kubectl describe deployment payment-api
# describe: the most detailed view of any Kubernetes object
# Shows: replica counts, selector, pod template, conditions, and recent events
# The Events section at the bottom is often the fastest path to root cause

kubectl get pods -l app=payment-api
# -l app=payment-api: filter Pods by label — shows only pods belonging to this deployment
# Faster than scanning all pods when you have dozens of services running

kubectl get pods -l app=payment-api -o wide
# -o wide: adds NODE column — tells you which physical node each Pod landed on
# Critical when a node is degraded and all Pods on it are failing

kubectl logs payment-api-7d6b9c8f4d-2xkpj
# logs: stream the stdout/stderr from a specific Pod
# Replace the pod name with one from your kubectl get pods output

kubectl logs -l app=payment-api --all-containers
# -l: stream logs from ALL pods matching the label selector at once
# --all-containers: if a Pod has multiple containers, include all of them

kubectl logs payment-api-7d6b9c8f4d-2xkpj --previous
# --previous: get logs from the PREVIOUS container instance (the one that crashed)
# Invaluable for debugging crash-loop issues — you need the dying container's logs, not the fresh one
$ kubectl describe deployment payment-api
Name:                   payment-api
Namespace:              default
CreationTimestamp:      Mon, 10 Mar 2025 09:14:22 +0000
Labels:                 app=payment-api
Replicas:               3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType:           RollingUpdate
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  app=payment-api
           version=1.0
  Containers:
   payment-api:
    Image:      nginx:1.25
    Port:       8080/TCP
    Limits:     cpu: 300m, memory: 256Mi
    Requests:   cpu: 100m, memory: 128Mi
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
Events:
  Normal  ScalingReplicaSet  2m  deployment-controller  Scaled up replica set payment-api-7d6b9c8f4d to 3

What just happened?

StrategyType: RollingUpdate — This is the default rollout strategy. When you update the Deployment image, Kubernetes won't kill all Pods and redeploy from scratch. It rolls — bringing up new Pods while old ones are still serving traffic, then draining the old ones. The 25% max unavailable means at most 25% of Pods can be down at any point during the rollout. This is how you get zero-downtime deploys for free.

Conditions — These are truth statements about the Deployment's health. Available: True means the minimum number of Pods are ready to serve traffic. Progressing: True means a rollout is underway or just completed. If you ever see Available: False, that's your incident right there.

Events — The Events section at the bottom of any kubectl describe is often the first place to look during debugging. It shows a chronological log of what Kubernetes did to this object — scale events, image pull failures, scheduling problems. Burned into every SRE's muscle memory: when in doubt, describe and scroll to events.

Scaling and Updating the Deployment

The scenario: It's Black Friday. Traffic to the payment API is 4x normal volume and the three Pods are struggling. Your SLO is 99.9% success rate and you're watching it slip in Grafana. Your team lead pings you: "Scale it. Now." Then the load dies down and marketing wants a new feature shipped — which means a new container image needs to be rolled out without any downtime.

kubectl scale deployment payment-api --replicas=8
# scale: imperatively change the replica count — faster than editing YAML during an incident
# Kubernetes immediately creates 5 new Pods to reach the target of 8
# WARNING: imperative commands like this drift from your YAML — always update the YAML afterward

kubectl set image deployment/payment-api payment-api=nginx:1.26
# set image: update the container image — triggers a rolling update automatically
# Format: deployment/[name] [container-name]=[new-image]
# container-name must match the name field under spec.template.spec.containers

kubectl rollout status deployment/payment-api
# rollout status: watch the rolling update in real-time — blocks until complete or failed
# Shows which replica set is scaling up/down and the percentage progress
# Exit code 0 = success, 1 = failed (useful in CI/CD pipelines)

kubectl rollout history deployment/payment-api
# history: shows every revision of this Deployment — each image update creates a new revision
# You can roll back to any numbered revision if the new one breaks something
$ kubectl scale deployment payment-api --replicas=8
deployment.apps/payment-api scaled

$ kubectl get pods -l app=payment-api
NAME                           READY   STATUS              RESTARTS   AGE
payment-api-7d6b9c8f4d-2xkpj   1/1     Running             0          12m
payment-api-7d6b9c8f4d-8rvnq   1/1     Running             0          12m
payment-api-7d6b9c8f4d-m4czl   1/1     Running             0          12m
payment-api-7d6b9c8f4d-p9wxt   0/1     ContainerCreating   0          2s
payment-api-7d6b9c8f4d-r2skl   0/1     ContainerCreating   0          2s
payment-api-7d6b9c8f4d-v8njq   0/1     ContainerCreating   0          2s
payment-api-7d6b9c8f4d-xb4fm   0/1     ContainerCreating   0          2s
payment-api-7d6b9c8f4d-zq7cd   0/1     ContainerCreating   0          2s

$ kubectl rollout status deployment/payment-api
Waiting for deployment "payment-api" rollout to finish: 2 out of 8 new replicas have been updated...
Waiting for deployment "payment-api" rollout to finish: 5 out of 8 new replicas have been updated...
deployment "payment-api" successfully rolled out

$ kubectl rollout history deployment/payment-api
deployment.apps/payment-api
REVISION  CHANGE-CAUSE
1         <none>
2         <none>

What just happened?

kubectl scale — This is an imperative command, meaning you're telling Kubernetes what to do rather than declaring the desired state in YAML. It works great for emergency scaling, but because it bypasses your YAML files, your Git repo is now out of sync with the cluster. The right practice is to scale imperatively during the incident, then update replicas: in your YAML and commit the change afterward.

ContainerCreating status — The five new Pods appeared immediately in kubectl get pods but in ContainerCreating state. This means Kubernetes has scheduled them to nodes and the kubelet is pulling the container image and creating the container — it hasn't started yet. Within seconds they'll transition to Running.

rollout history — Every time you update the Deployment's Pod template (image, env vars, resources), Kubernetes saves the previous state as a revision. Revision 1 is your original nginx:1.25 deploy. Revision 2 is the nginx:1.26 update. If 1.26 turns out to be broken, you can roll back to revision 1 instantly with kubectl rollout undo deployment/payment-api --to-revision=1. We cover this in depth in Lesson 26.

The Full End-to-End Traffic Flow

Let's make the whole picture concrete — from a request entering the cluster to the response coming back:

Checkout
Service Pod
caller
HTTP to payment-api-svc:80
Service
ClusterIP
10.96.214.88:80
kube-proxy load balances
Pod 1 :8080
Pod 2 :8080
Pod 3 :8080
one gets the request
DNS Resolution:
payment-api-svc resolves to 10.96.214.88 via CoreDNS inside the cluster. No hardcoded IPs needed.
Load Balancing:
kube-proxy uses iptables rules on every node to distribute traffic round-robin across all healthy Pod endpoints.
Self-Healing:
If Pod 2 crashes, kube-proxy removes it from endpoints within seconds. Traffic flows to Pod 1 and 3 only — no manual intervention.

Teacher's Note: Declarative beats imperative every time

In this lesson you saw both styles: declarative (kubectl apply -f) and imperative (kubectl scale --replicas=8). Declarative wins in the long run because the YAML file in Git becomes the authoritative truth about what's deployed. Imperative commands are for emergencies and quick exploration. Whenever you run an imperative command in production, make it a habit to immediately update the corresponding YAML and commit it. Your future self — and your teammates — will thank you.

You've now completed the Kubernetes Fundamentals section. You understand the architecture, the objects, the YAML, and you've run a real deployment end to end. Section II starts getting into the details that separate Kubernetes beginners from engineers who can actually run production clusters.

Practice Questions

1. What is the correct apiVersion for a Kubernetes Deployment manifest?



2. What is the default Deployment update strategy that replaces Pods gradually to avoid downtime?



3. What kubectl command would you run to immediately roll back the payment-api Deployment to its previous revision?



Quiz

1. You have a Deployment with replicas: 3. You manually delete one Pod. What happens next?


2. What is a critical requirement regarding labels in a Deployment manifest that, if violated, will cause kubectl apply to fail with a validation error?


3. A Pod is crash-looping. You need to see the logs from the container instance that just crashed, not the new one that restarted. Which command do you run?


Up Next · Lesson 16

Pod Lifecycle

Pending, Running, Succeeded, Failed, Unknown — learn what every Pod phase really means and how to debug each one.