Kubernetes Fundamentals

Project: Deploy an App to Kubernetes

18 min Lesson 10 of 32

Project: Deploy an App to Kubernetes

Theory becomes engineering only when you can ship something to a cluster, watch it roll out, verify it serves traffic, and recover gracefully when something goes wrong. This capstone lesson walks through the full lifecycle of a production-grade deployment: writing a Namespace, a Deployment manifest, and a Service — then performing a rolling update and an emergency rollback. Every step mirrors what platform engineers at large-scale companies do dozens of times a day.

Step 1 — Carve Out a Namespace

Every real application lives in its own Namespace so that resource quotas, RBAC policies, and network policies can be scoped cleanly without bleeding into other teams' workloads. Create a dedicated Namespace for your project before writing any other resource.

# namespace.yaml apiVersion: v1 kind: Namespace metadata: name: webapp labels: env: production team: platform
kubectl apply -f namespace.yaml kubectl get namespaces webapp # NAME STATUS AGE # webapp Active 3s
Label your Namespace immediately. Labels like env and team are used later by NetworkPolicy selectors, cost-allocation tools, and monitoring dashboards. Adding them retroactively causes silent policy gaps.

Step 2 — Write the Deployment Manifest

A production Deployment requires more than a container image and a replica count. It needs resource requests and limits so the scheduler can bin-pack correctly and the kernel OOM killer does not arbitrarily terminate your pod. It needs a readinessProbe so that Kubernetes does not route traffic to an instance that has started but is not yet warm. It needs a livenessProbe so that a deadlocked process is restarted automatically. And it needs a RollingUpdate strategy tuned to guarantee zero-downtime deploys.

# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: webapp namespace: webapp labels: app: webapp version: v1.0.0 spec: replicas: 3 selector: matchLabels: app: webapp strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # one extra pod during rollout maxUnavailable: 0 # never drop below desired count — zero-downtime guarantee template: metadata: labels: app: webapp version: v1.0.0 spec: containers: - name: webapp image: ghcr.io/yourorg/webapp:1.0.0 ports: - containerPort: 8080 resources: requests: cpu: "250m" memory: "256Mi" limits: cpu: "500m" memory: "512Mi" readinessProbe: httpGet: path: /healthz/ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /healthz/live port: 8080 initialDelaySeconds: 15 periodSeconds: 20 failureThreshold: 3 env: - name: APP_ENV value: "production" - name: LOG_LEVEL value: "info" terminationGracePeriodSeconds: 30
Never omit resource limits in production. A pod without limits can consume all CPU on a node and starve every neighbour. Without a memory limit, a memory leak causes the node itself to go OOM and get evicted by the cloud provider. Always set both requests and limits. As a starting point, set limits at 2x the measured requests, then tune after load testing.

Step 3 — Expose the App with a Service

A Service gives your pods a stable virtual IP and DNS name regardless of which node they land on or how many times they restart. For internal microservices use ClusterIP (the default). For an entry point that a load balancer can reach, use LoadBalancer or combine ClusterIP with an Ingress controller. Here we use ClusterIP — a real cluster would place an Ingress or an external load balancer in front.

# service.yaml apiVersion: v1 kind: Service metadata: name: webapp namespace: webapp labels: app: webapp spec: selector: app: webapp # routes to any pod with this label ports: - name: http protocol: TCP port: 80 # port the Service listens on targetPort: 8080 # port inside the container type: ClusterIP
# Apply all three manifests at once kubectl apply -f namespace.yaml -f deployment.yaml -f service.yaml # Watch the rollout converge kubectl rollout status deployment/webapp -n webapp # Waiting for deployment "webapp" rollout to finish: 0 of 3 updated replicas are available... # deployment "webapp" successfully rolled out # Verify pods are Running and Ready (3/3) kubectl get pods -n webapp -l app=webapp # Verify the Service has an Endpoints slice (healthy pods behind it) kubectl get endpoints -n webapp webapp
Deployment, Service, and Namespace layout Namespace: webapp Service ClusterIP :80 Deployment — 3 replicas (webapp:1.0.0) Pod A webapp :8080 Ready Pod B webapp :8080 Ready Pod C webapp :8080 Ready selector: app=webapp ReplicaSet owns Pods; Deployment owns ReplicaSet
A Deployment creates a ReplicaSet that owns three Pods; a Service routes traffic to all healthy Pods via label selectors — all scoped to the webapp Namespace.

Step 4 — Perform a Rolling Update

When your CI pipeline pushes a new image tag, update the Deployment. Because maxUnavailable: 0, Kubernetes will spin up the new pod first (maxSurge: 1) and only terminate an old one after the new pod passes its readiness probe. Traffic never drops to zero.

# Update the image tag — triggers a rolling rollout immediately kubectl set image deployment/webapp webapp=ghcr.io/yourorg/webapp:1.1.0 -n webapp # Watch in real time — streams progress until convergence kubectl rollout status deployment/webapp -n webapp --watch # Confirm the new ReplicaSet is active; old one scaled to 0 kubectl get replicasets -n webapp # NAME DESIRED CURRENT READY AGE # webapp-6d8f9b7c4f 3 3 3 45s <-- new (v1.1.0) # webapp-5c7b8d6a3e 0 0 0 12m <-- old (v1.0.0)
Kubernetes preserves old ReplicaSets by default (controlled by spec.revisionHistoryLimit, default 10). This is what makes rollback instant — the old ReplicaSet still exists and just needs to be scaled back up. Do not set revisionHistoryLimit: 0 in production or you will lose rollback capability entirely.

Step 5 — Emergency Rollback

A bad release gets into production. Metrics spike. The on-call engineer needs to revert in under two minutes. The following command sequence is the exact playbook used at hyperscale companies during an active incident.

# Check rollout history — lists every revision with its change-cause annotation kubectl rollout history deployment/webapp -n webapp # REVISION CHANGE-CAUSE # 1 kubectl set image ... webapp:1.0.0 # 2 kubectl set image ... webapp:1.1.0 # Roll back to the immediately previous revision (most common case) kubectl rollout undo deployment/webapp -n webapp # Or target a specific historical revision kubectl rollout undo deployment/webapp -n webapp --to-revision=1 # Confirm rollback converged — same command as a normal rollout check kubectl rollout status deployment/webapp -n webapp # Verify every pod is now running the old image kubectl get pods -n webapp -o wide
Annotate every rollout with a change cause. Before applying a new version, run: kubectl annotate deployment/webapp kubernetes.io/change-cause="deploy v1.1.0 — fix login bug" -n webapp. This fills the CHANGE-CAUSE column in rollout history, making incident timelines and postmortems much easier to reconstruct.

Production Checklist Before You Ship

Before any real team ships a Deployment to production, they verify the following:

  1. Resource requests and limits are set — both CPU and memory on every container.
  2. Readiness and liveness probes are configured — pointing at fast, dependency-aware health endpoints.
  3. The rolling update strategy guarantees zero downtimemaxUnavailable: 0 and at least maxSurge: 1.
  4. The image tag is pinned to an immutable digest or semantic version — never :latest in production.
  5. Secrets are not stored in environment variables as plaintext — use Kubernetes Secrets or an external vault.
  6. The Namespace has a ResourceQuota — so one team cannot starve the whole cluster.
  7. PodDisruptionBudget (PDB) is defined — so node drains during upgrades respect your availability SLA.

The manifests in this lesson are deliberately minimal for clarity. In real infrastructure, a Helm chart or Kustomize overlay would layer in the remaining fields automatically.