AI EngineeringZero to ProductionHome·About·Contact
Kubernetes Orchestration · Part 2

Pods, Deployments & Services

Now the workhorses. This lesson is the core object model you'll use every day: Pods wrapped by ReplicaSets wrapped by Deployments; Services (ClusterIP / NodePort / LoadBalancer) that give a stable address to a moving set of Pods; ConfigMaps and Secrets for configuration; and labels & selectors — the glue that connects them all. You'll write your first real Deployment + Service and drive it with kubectl apply / get / describe / logs.

⏱️ ~90 min☸️ Core objects🎯 Beginner→Advanced
🌱 What you're buildingA single deployable web app: a Deployment running 3 replicas, fronted by a Service so clients hit one stable address, configured by a ConfigMap and a Secret. Every YAML block here is copy-paste correct but needs a cluster — minikube/kind locally, or EKS (K5). Terminal output is illustrative. Field names occasionally drift between API versions — verify against the current Kubernetes docs.

Learning objectives

  • Explain the Pod → ReplicaSet → Deployment ownership chain and why you deploy via Deployments.
  • Write a Deployment and expose it with a Service, connected by labels & selectors.
  • Choose between ClusterIP, NodePort, and LoadBalancer Service types.
  • Inject configuration with a ConfigMap and sensitive data with a Secret.
  • Use namespaces to isolate environments, and drive it all with core kubectl verbs.

1 · The ownership chain: Pod ← ReplicaSet ← Deployment

You rarely touch the lower two directly. A Deployment is the object you write; it creates and manages a ReplicaSet, which in turn keeps the right number of Pods alive. The Deployment adds what a bare ReplicaSet lacks: rollouts and rollbacks. When you change the image, the Deployment spins up a new ReplicaSet and shifts Pods over gradually.

Deployment ReplicaSet Pods (×N)
Why three layers, not oneSeparation of concerns: the Pod is one running instance, the ReplicaSet owns "keep N identical copies alive," and the Deployment owns "change from version A to B safely." Each rollout is really "scale up a new ReplicaSet, scale down the old one" — which is why you can roll back by just re-activating the previous ReplicaSet.

2 · Your first Deployment

Here is a real Deployment: 3 replicas of a web image, with a selector that must match the Pod template's labels. That label match is not decoration — it's how the Deployment knows which Pods it owns.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
YAML · a Deployment (needs a cluster)
deployment.yamlapiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web            # which Pods this Deployment manages
  template:               # the Pod template it stamps out
    metadata:
      labels:
        app: web          # MUST match the selector above
    spec:
      containers:
        - name: app
          image: ghcr.io/example/web:1.2.0   # pin a version
          ports:
            - containerPort: 8000
shell · apply and watch the rollout (needs a cluster)
apply.shkubectl apply -f deployment.yaml
kubectl get deploy web              # READY 3/3 when settled
kubectl get pods -l app=web         # list the 3 Pods by label
kubectl rollout status deploy/web   # block until the rollout is done
kubectl describe deploy web         # events, replica counts, strategy
NAME   READY   UP-TO-DATE   AVAILABLE   AGE
web    3/3     3            3           40s
Selector must match template labelsIf selector.matchLabels doesn't match the Pod template's labels, the Deployment either errors or manages zero Pods. This is the #1 first-Deployment bug. And the selector is immutable after creation — verify the current rules against the Kubernetes docs.

3 · Services — a stable address for moving Pods

Pods are cattle: they're created, killed, and rescheduled with new IPs constantly. Clients can't chase those IPs. A Service gives a single stable virtual IP and DNS name that load-balances across whichever Pods currently match its selector. Three types, from internal to external:

TypeReachable fromTypical use
ClusterIP (default)Inside the cluster onlyService-to-service calls; the default
NodePortAny node's IP on a high portDev/testing, or behind an external LB
LoadBalancerThe public internet (cloud LB)Exposing a service externally on a cloud
YAML · a ClusterIP Service in front of the Deployment (needs a cluster)
service.yamlapiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web            # sends traffic to Pods with label app=web
  ports:
    - port: 80          # the Service's port
      targetPort: 8000  # the container's port
  # type: ClusterIP is the default (internal). Use LoadBalancer to expose externally.
shell · reach the Service by DNS from inside the cluster (needs a cluster)
svc.shkubectl apply -f service.yaml
kubectl get svc web                 # note the CLUSTER-IP
# Every Service gets in-cluster DNS: <name>.<namespace>.svc.cluster.local
kubectl run tmp --rm -it --image=curlimages/curl -- \
  curl -s http://web.default.svc.cluster.local/healthz
NAME   TYPE        CLUSTER-IP      PORT(S)   AGE
web    ClusterIP   10.96.140.21    80/TCP    8s
Client Service (stable IP+DNS) Pod Pod
How the Service finds its PodsThe Service's selector is matched continuously against Pod labels; the set of matching Pod IPs is tracked as EndpointSlices. Add or kill a Pod and the endpoints update automatically — that's why clients hit one address while Pods churn underneath.

4 · ConfigMaps and Secrets

Never bake config or credentials into the image. A ConfigMap holds non-sensitive settings; a Secret holds sensitive ones. Both are injected at runtime as environment variables or mounted files, so the same image runs in dev and prod with different config.

YAML · ConfigMap + Secret, consumed by the Deployment (needs a cluster)
config.yamlapiVersion: v1
kind: ConfigMap
metadata:
  name: web-config
data:
  LOG_LEVEL: "info"
  FEATURE_FLAGS: "search,export"
---
apiVersion: v1
kind: Secret
metadata:
  name: web-secret
type: Opaque
stringData:                 # stringData: plain text, encoded for you on apply
  API_KEY: "replace-me-in-a-real-secret-store"
---
# ...in the Deployment's container spec:
#   envFrom:
#     - configMapRef: { name: web-config }
#     - secretRef:    { name: web-secret }
Secrets are only base64-encoded, not encrypted, by defaultA Kubernetes Secret is base64-encoded in etcd — encoding, not encryption. Anyone who can read the Secret or etcd can read the value. For real protection, enable encryption at rest for etcd and/or pull secrets from an external store (AWS Secrets Manager, Vault) via a CSI driver — covered in K5. Never commit real secret values to git. Verify current secret-handling options against the Kubernetes docs.

5 · Namespaces, labels & selectors

Namespaces partition a cluster into virtual sub-clusters — a common pattern is one per environment (dev, staging, prod) or per team. Objects in different namespaces can share names and are isolated by default. Labels are arbitrary key/value tags on objects; selectors query them. Together they're how nearly everything in k8s finds everything else.

shell · namespaces, labels, and label queries (needs a cluster)
ns.shkubectl create namespace staging
kubectl apply -f deployment.yaml -n staging   # deploy into a namespace
kubectl get pods -A                           # -A = across all namespaces

# Labels drive selection everywhere:
kubectl get pods -l 'app=web,tier!=batch'     # boolean label selectors
kubectl label pod web-abc123 canary=true      # add a label on the fly
kubectl get pods -l canary=true               # now query by it
Labels are the universal join keyServices find Pods by label. Deployments own Pods by label. Network policies, monitoring, and cost allocation all filter by label. Adopt a labeling convention early (e.g. app, tier, env, version) — it pays off across every tool.

✓ Checkpoint — you can move on when you can…

  • Draw the Deployment → ReplicaSet → Pod chain and say what each layer adds.
  • Write a Service whose selector matches a Deployment's Pod labels, mapping port 80 → 8000.
  • State when you'd use ClusterIP vs NodePort vs LoadBalancer.
  • Explain why a Secret is not encrypted by default and what to do about it.
  • Use kubectl get pods -l with a label selector to filter Pods.
✓ Knowledge check

You applied a Deployment and a Service, but curl to the Service returns nothing and kubectl get endpoints web shows no addresses. The Pods are Running. What is the most likely cause?

Show answer
The Service's selector does not match the Pods' labels. A Service populates its endpoints from Pods whose labels satisfy its selector; if they don't match, the endpoint list is empty and there's nowhere to route, so requests hang or fail even though the Pods are perfectly healthy. Fix it by making the Service selector (e.g. app: web) exactly match the labels on the Deployment's Pod template. Confirm with kubectl describe svc web and kubectl get pods --show-labels.
✓ Knowledge check

A teammate wants to expose an internal-only backend API to other services in the cluster and reaches for a LoadBalancer Service. Why is that wrong, and what should they use?

Show answer
A LoadBalancer provisions a real (usually public, billed) cloud load balancer — overkill and a security risk for a service that only other in-cluster services call. The right type is the default ClusterIP, which gives a stable in-cluster virtual IP and DNS name reachable only from inside the cluster. LoadBalancer (or an Ingress, K4) is for traffic that genuinely comes from outside; internal service-to-service traffic should stay on ClusterIP.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Write a Deployment + Service pairBeginner

Context: The Deployment+Service pair is the atom of almost every k8s app; writing it from memory is the first real milestone.

Your task: Write a Deployment of 2 replicas of a web image on container port 8080, and a ClusterIP Service that maps port 80 to it.

Requirements:

  • The Deployment's selector must match its Pod template labels
  • The Service's selector must match the same labels
  • Map Service port 80 → targetPort 8080
  • Pin the image to a version, never :latest

💡 Hint: One label (e.g. app: api) is the join key across all three objects.

Show solution

The label app: api ties the Deployment, its Pods, and the Service together:

apiVersion: apps/v1
kind: Deployment
metadata: { name: api, labels: { app: api } }
spec:
  replicas: 2
  selector: { matchLabels: { app: api } }
  template:
    metadata: { labels: { app: api } }
    spec:
      containers:
        - name: app
          image: ghcr.io/example/api:1.0.0
          ports: [ { containerPort: 8080 } ]
---
apiVersion: v1
kind: Service
metadata: { name: api }
spec:
  selector: { app: api }
  ports: [ { port: 80, targetPort: 8080 } ]

Apply with kubectl apply -f, then kubectl get deploy,svc,pods -l app=api. The Service will show endpoints once the 2 Pods are Ready.

Exercise 2 · Inject config and a secretIntermediate

Context: Externalizing config is what lets one image run in every environment.

Your task: Add a ConfigMap and a Secret to the app and consume both as environment variables via envFrom.

Requirements:

  • ConfigMap holds LOG_LEVEL and a feature flag
  • Secret holds an API_KEY using stringData
  • The container consumes both with envFrom
  • Note why the Secret value must not be committed to git

💡 Hint: envFrom pulls every key in a ConfigMap/Secret in as an env var at once.

Show solution
apiVersion: v1
kind: ConfigMap
metadata: { name: api-config }
data: { LOG_LEVEL: "info", FEATURE_SEARCH: "true" }
---
apiVersion: v1
kind: Secret
metadata: { name: api-secret }
type: Opaque
stringData: { API_KEY: "set-via-CI-or-a-secret-store" }

Consume both in the container spec:

      envFrom:
        - configMapRef: { name: api-config }
        - secretRef:    { name: api-secret }

Why not commit the value: a Secret is only base64-encoded in etcd, not encrypted, so committing the manifest with a real API_KEY leaks it in git history in effectively plaintext. Inject the real value from CI or an external secret store, and keep only a placeholder in the repo.

Exercise 3 · Debug an empty-endpoints ServiceAdvanced

Context: The most common broken-Service symptom is empty endpoints; diagnosing it fast is a core operating skill.

Your task: A Service returns nothing though its Pods are Running. Give the exact kubectl sequence to diagnose it and the two most likely root causes.

Requirements:

  • Check the Service's endpoints
  • Compare the Service selector to the Pod labels
  • Also consider a port mismatch (targetPort vs containerPort)
  • State the fix for each cause

💡 Hint: No endpoints = no Pods matched the selector; wrong port = matched but nothing listening.

Show solution

Diagnose:

kubectl get endpoints web          # empty? no Pods matched the selector
kubectl get pods --show-labels     # what labels do the Pods actually have?
kubectl describe svc web           # selector + target port
kubectl get svc web -o yaml        # confirm targetPort matches containerPort

Cause 1 — selector/label mismatch (empty endpoints). The Service selector doesn't match the Pod labels, so no endpoints are populated. Fix: align the selector with the Pod template labels.

Cause 2 — port mismatch (endpoints present, still fails). Endpoints exist but targetPort points at a port the container isn't listening on. Fix: set targetPort to the real containerPort.

Rule of thumb: no endpoints → label problem; endpoints but connection refused → port problem.

Exercise 4 · Model a rollout as ReplicaSet arithmeticExpert

Context: Understanding a rollout as two ReplicaSets scaling in opposite directions demystifies both deploys and rollbacks.

Your task: Explain what objects exist and how their replica counts move when you change a Deployment's image from v1 to v2 with the default rolling strategy, and how rollback works.

Requirements:

  • Describe the old and new ReplicaSets during the rollout
  • Explain what maxSurge and maxUnavailable control
  • Explain what kubectl rollout undo actually does

💡 Hint: A Deployment never edits Pods in place — it shifts them between ReplicaSets.

Show solution

During the rollout, the Deployment holds two ReplicaSets: the old (v1) and a new (v2). It scales the new RS up and the old RS down in steps until v2 = desired and v1 = 0. Pods are never mutated in place — old Pods are deleted, new ones created.

The two knobs bound the disruption: maxSurge = how many extra Pods above desired may exist mid-rollout (speed), maxUnavailable = how many below desired may be missing (safety). With maxUnavailable: 0 you never drop below full capacity — at the cost of needing surge headroom.

Rollback is cheap because the old ReplicaSet still exists (scaled to 0). kubectl rollout undo deploy/web just scales the previous RS back up and the current one down — reversing the arithmetic. That's why rollback is seconds, not a rebuild.

Exercise 5 · Design the namespace + labeling conventionProfessional

Context: A labeling and namespace convention adopted early is what keeps a growing cluster legible.

Your task: Design a namespace layout and a label schema for a company running several apps across dev/staging/prod, so Services, monitoring, and cost allocation all work off it.

Requirements:

  • Decide the namespace boundary (by env, by team, or both)
  • Define a minimal required label set every workload must carry
  • Show one query each label enables (ops, cost, canary)
  • Note one thing namespaces do NOT isolate by default

💡 Hint: Recommended labels cluster around app, component, version, part-of, managed-by.

Show solution

Namespaces: partition by environment × team — e.g. payments-prod, payments-staging, search-prod. This keeps prod blast radius contained and lets you apply per-namespace quotas and RBAC.

Required labels (aligned to the common recommended set): app.kubernetes.io/name, app.kubernetes.io/component, app.kubernetes.io/version, app.kubernetes.io/part-of, app.kubernetes.io/managed-by, plus a team label like owner: payments.

What each unlocks: -l app.kubernetes.io/name=web selects a service for ops; owner=payments drives cost allocation and dashboards; a track=canary label lets a Service or rollout target only canary Pods.

The gotcha: namespaces isolate names and are an RBAC/quota boundary, but they do not isolate network traffic by default — any Pod can reach a Service in another namespace unless you add NetworkPolicies (K4). Don't mistake a namespace for a security wall.

Exercise 6 · Ship a real two-service app to a namespaceIndustry scenario

Context: Representative scenario: you're handed a frontend and a backend container and asked to run them together in a staging namespace, with the frontend reachable and the backend internal-only.

Your task: Produce the full manifest set: two Deployments, two Services (one internal, one external-ish), config, and the commands to deploy and verify in a namespace.

Requirements:

  • Backend: ClusterIP Service, consumed by the frontend via in-cluster DNS
  • Frontend: a Service the outside can reach (LoadBalancer or NodePort for now)
  • Config/secret injected, image versions pinned
  • Give the deploy + verify commands scoped to the namespace

💡 Hint: The frontend calls the backend at backend.staging.svc.cluster.local.

Show solution

Backend (internal ClusterIP):

apiVersion: apps/v1
kind: Deployment
metadata: { name: backend, labels: { app: backend } }
spec:
  replicas: 2
  selector: { matchLabels: { app: backend } }
  template:
    metadata: { labels: { app: backend } }
    spec:
      containers:
        - name: app
          image: ghcr.io/example/backend:2.1.0
          ports: [ { containerPort: 8000 } ]
          envFrom: [ { configMapRef: { name: backend-config } } ]
---
apiVersion: v1
kind: Service
metadata: { name: backend }
spec:
  selector: { app: backend }
  ports: [ { port: 80, targetPort: 8000 } ]   # ClusterIP (default)

Frontend reaches the backend by DNS at http://backend.staging.svc.cluster.local and is itself exposed:

apiVersion: apps/v1
kind: Deployment
metadata: { name: frontend, labels: { app: frontend } }
spec:
  replicas: 2
  selector: { matchLabels: { app: frontend } }
  template:
    metadata: { labels: { app: frontend } }
    spec:
      containers:
        - name: app
          image: ghcr.io/example/frontend:2.1.0
          ports: [ { containerPort: 3000 } ]
          env:
            - name: BACKEND_URL
              value: "http://backend.staging.svc.cluster.local"
---
apiVersion: v1
kind: Service
metadata: { name: frontend }
spec:
  type: LoadBalancer         # externally reachable (or NodePort locally)
  selector: { app: frontend }
  ports: [ { port: 80, targetPort: 3000 } ]

Deploy & verify in the namespace:

kubectl create namespace staging
kubectl apply -n staging -f backend.yaml -f frontend.yaml
kubectl get all -n staging
kubectl get svc frontend -n staging      # note EXTERNAL-IP once assigned

The backend stays internal (ClusterIP, no external IP); the frontend gets an external address and reaches the backend over cluster DNS. In K4 you'd replace the frontend LoadBalancer with an Ingress for TLS and host-based routing.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in