Docker Compose brings TechCorp's system up on a laptop, but it falls short as soon as there is more than one machine: if the server running orders-service goes down, nobody starts it somewhere else; if Black Friday demands ten replicas of catalog-service, someone has to decide where to put them; if version 1.0.1 has to be deployed without interrupting the service, the startup of the new one and the shutdown of the old one have to be coordinated by hand. An orchestrator does all of that declaratively: you tell it what you want ("two replicas of this image, healthy, reachable under this name") and it takes care of the how, continuously. Kubernetes is the orchestrator on the shortlist of 04-01. In this lesson we understand its architecture and its objects, set up a local cluster, and write and apply the complete manifests of orders-service with the names that 03-05 (Service, /health/*) and 04-03 (orders-service-config, orders-db, orders-rabbitmq) left settled. The pipeline that will apply these manifests automatically (05-03) and the strategies for changing version without downtime (05-04) come afterwards.

Contents

  1. Why an orchestrator is needed
  2. Kubernetes architecture: control plane and nodes
  3. The basic objects
  4. Local environment with kind and essential kubectl
  5. Namespace, ConfigMap and Secret for orders-service
  6. A Job for the migrations
  7. The orders-service Deployment, line by line
  8. The Service and the DNS name orders-service:3002
  9. Ingress: exposing the gateway at api.techcorp.example
  10. Manual scaling and deployment status
  11. RabbitMQ and PostgreSQL: inside the cluster or managed
  12. Helm and Kustomize to avoid duplicating YAML across six services

  1. Why an orchestrator is needed

What an orchestrator solves, compared with "containers on machines":

Need Without an orchestrator With Kubernetes
Replicas Scripts that run docker run on N machines replicas: 2 in a Deployment; Kubernetes always keeps two
Restarts restart: always per machine; if the machine dies, nothing If a pod or a node goes down, it is recreated on another node
Placement Someone decides which machine each service goes on The scheduler picks a node according to requested CPU/memory and rules
Networking and discovery Ports and IPs by hand, or Consul (03-05) Service + internal DNS: http://orders-service:3002
Configuration and secrets .env files copied to every machine ConfigMap/Secret injected as variables (04-03)
Deployments Stop the old one, start the new one, cross your fingers RollingUpdate with readiness: no downtime (05-04)
Health Docker HEALTHCHECK, with no consequences Probes that restart pods and take them out of load balancing

Everything is declared in YAML and stored in git; the cluster continuously reconciles the actual state with the desired one. That is the central idea: you do not run imperative commands, you describe the outcome.

  1. Kubernetes architecture: control plane and nodes

flowchart TB
    subgraph CP["Control plane"]
        API[API server<br/>single door: kubectl, controllers, kubelets]
        ETCD[(etcd<br/>desired and actual state)]
        SCH[Scheduler<br/>assigns pods to nodes]
        CM[Controller manager<br/>Deployment, ReplicaSet, Job, Endpoints...]
        API <--> ETCD
        SCH --> API
        CM --> API
    end
    subgraph N1["Node 1"]
        K1[kubelet] --> R1[containerd]
        R1 --> P1[pod orders-service-7d9f-abc]
        R1 --> P2[pod catalog-service-5c1b-xyz]
        KP1[kube-proxy]
    end
    subgraph N2["Node 2"]
        K2[kubelet] --> R2[containerd]
        R2 --> P3[pod orders-service-7d9f-def]
        KP2[kube-proxy]
    end
    API --> K1
    API --> K2
    kubectl -->|kubectl apply -f| API
  • API server: receives every request (from kubectl, from the controllers, from the kubelets), validates them and persists them in etcd, the cluster's key-value database.
  • Scheduler: watches pods with no node assigned and picks one for them according to requested resources, affinities and constraints.
  • Controller manager: runs the control loops. The Deployment controller creates ReplicaSets; the ReplicaSet controller creates or deletes pods until replicas matches; the Endpoints controller maintains the list of ready pods for each Service (the "registry" of 03-05).
  • kubelet (on each node): agent that talks to the API server, starts the containers of the pods assigned to its node through the runtime (containerd, the same container technology as 05-01) and runs the probes.
  • kube-proxy: programs the network rules so that a Service's ClusterIP spreads traffic across its pods.

A TechCorp developer only talks to the API server (kubectl), and not even that in production: the pipeline or Argo CD will (05-03).

  1. The basic objects

Object What it is Use at TechCorp
Pod Smallest unit: one or more containers sharing network (same IP) and storage; ephemeral One orders-service container per pod (plus a sidecar if there were a mesh, 05-05)
ReplicaSet Keeps N identical pods alive Never written by hand: the Deployment creates it
Deployment Describes the pod template, the replicas and how to update them One per stateless service: the six services and the gateway
Service Stable name and IP in front of a set of pods (03-05). Types: ClusterIP (internal, default), NodePort (a port on every node), LoadBalancer (cloud load balancer) ClusterIP for all internal services; the gateway is exposed through an Ingress
Ingress HTTP rule (host/path → Service) executed by an ingress controller (NGINX, Traefik) api.techcorp.examplegateway:8080
ConfigMap Non-sensitive key-value pairs orders-service-config (04-03)
Secret Sensitive key-value pairs (base64, not encrypted by default) orders-db, orders-rabbitmq (04-03)
Namespace Logical partition of the cluster for names, quotas and permissions techcorp for all the services; platform for ingress, observability
StatefulSet Pods with stable identity and disk PostgreSQL/RabbitMQ in dev (section 11); no TechCorp service
Job / CronJob Task that finishes / scheduled task Migrations (Job); a nightly cleanup of idempotency_keys would be a CronJob

  1. Local environment with kind and essential kubectl

kind (Kubernetes in Docker) creates a complete cluster inside containers; minikube is the equivalent alternative. TechCorp uses kind because it is the same one that runs in CI. The configuration file publishes the node's ports 80/443 on the laptop so the Ingress works:

# techcorp/platform/local/kind.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:                       # node's 80/443 → laptop, so the Ingress (section 9) answers on localhost
      - { containerPort: 80, hostPort: 80 }
      - { containerPort: 443, hostPort: 443 }
  - role: worker
  - role: worker
kind create cluster --name techcorp --config platform/local/kind.yaml
kubectl cluster-info                       # check that kubectl points at the kind-techcorp cluster
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kind load docker-image ghcr.io/techcorp/orders-service:1.0.0 --name techcorp   # local image (05-01) without going through ghcr.io

The kubectl commands used every day (kubectl config set-context --current --namespace=techcorp avoids repeating -n techcorp; from here on we omit it):

kubectl apply -f file.yaml               # create or update (declarative, idempotent); -k for a Kustomize directory
kubectl get pods -n techcorp -w          # list (and -w: watch changes); also get deploy/svc/cm/secret/ingress/job
kubectl describe pod orders-service-7d9f-abc -n techcorp   # details and, above all, the Events section (why it doesn't start)
kubectl logs -f deploy/orders-service -n techcorp          # logs (of one pod of the Deployment); --previous if it restarted
kubectl port-forward svc/orders-service 3002:3002 -n techcorp   # local tunnel to test with curl without an Ingress
kubectl rollout status deploy/orders-service -n techcorp    # waits for the deployment to finish (or fail)
kubectl exec -it deploy/orders-service -n techcorp -- sh    # shell in a pod; kubectl delete -f: delete what was declared

  1. Namespace, ConfigMap and Secret for orders-service

The manifests live in techcorp/platform/k8s/orders-service/base/ (section 12 explains the structure). First the namespace and the non-sensitive configuration, which is exactly the "production" column of the 04-03 table:

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: techcorp
---
# k8s/orders-service/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: orders-service-config            # the name agreed in 04-03
  namespace: techcorp
data:                                    # everything is a string: numbers go in quotes
  NODE_ENV: production
  PORT: "3002"
  LOG_LEVEL: info
  CATALOG_URL: http://catalog-service:3001        # DNS names of the Services (03-05)
  CUSTOMERS_URL: http://customers-service:3004
  HTTP_TIMEOUT_MS: "2000"
  OUTBOX_INTERVAL_MS: "250"
  REMOTE_CATALOG: "true"

Secrets do not go in YAML inside the repository. They are created with kubectl (or injected by an external manager, 07-04):

kubectl create secret generic orders-db \
  --from-literal=ORDERS_DB_URL='postgres://svc_orders:[email protected]:5432/orders'
kubectl create secret generic orders-rabbitmq \
  --from-literal=RABBITMQ_URL='amqp://orders:Pr0d-Rq7t...@rabbitmq:5672'

kubectl get secret orders-db -o yaml
# data:
#   ORDERS_DB_URL: cG9zdGdyZXM6Ly9zdmNfb3JkZXJzOlByMGQtWGszdi4uLkBwZy1vcmRlcnMuLi4=

That value is base64, not encryption: echo cG9z... | base64 -d gives back the password. A Secret is only "secret" because access to it is restricted with permissions (RBAC) and because etcd can be encrypted at rest; both belong to 07-04. That is why secrets are not versioned in plain text and why the .dockerignore of 05-01 excluded .env. The Secret keys are named like the environment variables (ORDERS_DB_URL) so they can be injected with envFrom.

  1. A Job for the migrations

scripts/migrate.js (04-04) must run before the new version of Orders starts, once, and fail loudly if it cannot. It is a Job:

# k8s/orders-service/base/job-migrations.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: orders-service-migrations-1-0-0        # the name carries the version: a Job is immutable, each version creates its own
  namespace: techcorp
spec:
  backoffLimit: 3                              # retries if the pod fails (e.g. PostgreSQL not yet accepting connections)
  ttlSecondsAfterFinished: 3600                # deletes itself one hour after finishing
  template:
    spec:
      restartPolicy: Never                     # a Job doesn't restart the container: it creates another pod if needed
      securityContext:
        runAsNonRoot: true
      containers:
        - name: migrations
          image: ghcr.io/techcorp/orders-service:1.0.0       # the SAME image as the service: migrations/ and scripts/ are inside (05-01)
          command: ["node", "scripts/migrate.js"]
          envFrom:
            - secretRef: { name: orders-db }   # it only needs ORDERS_DB_URL
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { memory: 256Mi }
kubectl apply -f k8s/orders-service/base/job-migrations.yaml
kubectl wait --for=condition=complete job/orders-service-migrations-1-0-0 --timeout=120s
kubectl logs job/orders-service-migrations-1-0-0     # "applied 001-initial-schema.sql" ... or the SQL error

The alternative is an init container inside the service's pod; it works, but it runs the migrations on every replica at startup (with two replicas, twice, and migrate.js has to cope with the concurrency). The Job runs them once per version and shows up in kubectl get jobs. In 05-03 the pipeline launches it and waits before applying the Deployment.

  1. The orders-service Deployment, line by line

# k8s/orders-service/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders-service
  namespace: techcorp
  labels:
    app: orders-service
spec:
  replicas: 2                                  # always two pods; the scheduler spreads them across nodes
  selector:
    matchLabels:
      app: orders-service                      # which pods this Deployment manages (must match template.metadata.labels)
  # strategy: RollingUpdate is the default; its parameters are tuned in 05-04
  template:                                    # pod template
    metadata:
      labels:
        app: orders-service
        version: v1                            # extra label that 05-04 and 05-05 will use to route between versions
    spec:
      terminationGracePeriodSeconds: 30        # after SIGTERM, how long the kubelet waits before SIGKILL (the 04-02 shutdown takes < 10 s)
      securityContext:
        runAsNonRoot: true                     # the kubelet rejects the pod if the image tried to run as root (USER node in 05-01)
        runAsUser: 1000                        # uid of the base image's 'node' user
      containers:
        - name: orders-service
          image: ghcr.io/techcorp/orders-service:1.0.0       # specific tag, never latest (05-01)
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 3002              # the PORT from the ConfigMap; documents and names the port
          envFrom:                             # every key of these objects becomes an environment variable (04-03)
            - configMapRef: { name: orders-service-config }
            - secretRef: { name: orders-db }
            - secretRef: { name: orders-rabbitmq }
          resources:
            requests:                          # what the scheduler reserves to place the pod
              cpu: 100m                        # 0.1 cores
              memory: 128Mi
            limits:                            # ceiling: if memory is exceeded, the container dies (OOMKilled)
              cpu: 500m
              memory: 256Mi
          readinessProbe:                      # can it serve? If it fails, the pod leaves the Service's Endpoints (03-05)
            httpGet: { path: /health/ready, port: http }
            initialDelaySeconds: 5             # the service takes ~2 s to start and connect; 5 s of margin
            periodSeconds: 5
            failureThreshold: 3                # three consecutive failures (15 s) → not ready; one success → ready again
          livenessProbe:                       # is it alive? If it fails, the kubelet RESTARTS the container
            httpGet: { path: /health/live, port: http }
            initialDelaySeconds: 15
            periodSeconds: 10
            failureThreshold: 3
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true       # the image is read-only; Node doesn't write to disk

The points that raise the most questions:

  • selector and labels: the Deployment (through its ReplicaSet) "owns" the pods whose app: orders-service label matches; the Service of section 8 uses the same selector. Changing selector on an existing Deployment is not allowed: choose well from the start.
  • readinessProbe versus livenessProbe (03-05): readiness looks at the service's own dependencies (PostgreSQL and RabbitMQ in the /health/ready of 04-04) and its effect is to stop receiving traffic; liveness only checks that the process responds and its effect is to restart. Putting the database check in liveness is the classic mistake: if PostgreSQL goes down, Kubernetes restarts every Orders pod in a loop without fixing anything.
  • terminationGracePeriodSeconds: 30 closes the loop of the graceful shutdown: when a pod is deleted, Kubernetes removes it from the Endpoints and sends SIGTERM to PID 1 (the node of 05-01); the service sets /health/ready to 503, finishes in-flight requests and exits in under 10 s; if it did not, SIGKILL would arrive at 30 s. With two replicas and this contract, a deployment does not lose a single request (05-04 shows it step by step).
  • securityContext: runAsNonRoot verifies what the image already does (USER node); readOnlyRootFilesystem forces any write to go to an explicit emptyDir. The rest of the hardening (capabilities, seccomp, admission policies) belongs to 07-04.

  1. The Service and the DNS name orders-service:3002

# k8s/orders-service/base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: orders-service                         # → DNS orders-service.techcorp.svc.cluster.local, or simply orders-service
  namespace: techcorp
spec:
  type: ClusterIP                              # only reachable inside the cluster (the gateway and other services)
  selector:
    app: orders-service                        # the ready pods with this label are the Endpoints
  ports:
    - name: http
      port: 3002                               # Service port (the one callers use: ORDERS_URL=http://orders-service:3002)
      targetPort: http                         # container port (by name, defined in the Deployment)

With this, the gateway's ORDERS_URL=http://orders-service:3002 and the CATALOG_URL=http://catalog-service:3001 in the Orders ConfigMap resolve exactly as in Compose (05-01) and as 03-05 promised, without any service knowing how many replicas there are or which node they are on.

Apply and check all of the above:

kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/orders-service/base/       # configmap, job, deployment, service (the secrets already exist)
kubectl rollout status deploy/orders-service    # deployment "orders-service" successfully rolled out
kubectl get pods -l app=orders-service          # orders-service-7d9f6c4b8-abcde 1/1 Running, ...-fghij 1/1 Running
kubectl get endpoints orders-service            # 10.244.1.7:3002,10.244.2.4:3002 → the two ready pods
kubectl port-forward svc/orders-service 3002:3002 &
curl -s localhost:3002/health/ready             # {"status":"ok","dependencies":{"postgres":"ok","rabbitmq":"ok"}}

If a pod gets stuck in CrashLoopBackOff, kubectl logs --previous will almost always show the config.js message (04-03) saying which variable is missing: the fail-fast validation was designed for this very moment.

  1. Ingress: exposing the gateway at api.techcorp.example

The gateway (03-04) is deployed like any other service (Deployment + Service gateway:8080, with its ConfigMap of URLs). The only thing that leaves the cluster is an Ingress that the ingress controller (NGINX in kind and in production; Traefik would be equivalent with ingressClassName: traefik) turns into proxy rules:

# k8s/gateway/base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gateway
  namespace: techcorp
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: 2m
spec:
  ingressClassName: nginx
  rules:
    - host: api.techcorp.example               # locally: add "127.0.0.1 api.techcorp.example" to /etc/hosts
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: gateway
                port: { number: 8080 }
  # tls: [{ hosts: [api.techcorp.example], secretName: api-techcorp-tls }]   # certificates: 07-02
kubectl apply -f k8s/gateway/base/
curl -s http://api.techcorp.example/api/v1/products?ids=p-501 | jq .data[0].name    # "BT X200 Headphones"

The ingress controller balances at layer 7 towards the gateway's Service, and the gateway towards the internal services (03-05). The only entry point is still the gateway's 8080; no *-service has an Ingress.

  1. Manual scaling and deployment status

kubectl scale deploy/catalog-service --replicas=6        # catalog peak (×20 during campaigns, 01-05): more pods, same Service
kubectl get pods -l app=catalog-service -w               # the new ones go through ContainerCreating → Running → Ready
kubectl scale deploy/catalog-service --replicas=2        # back to normal
kubectl rollout history deploy/orders-service            # revisions (every template change creates one)

kubectl scale is manual: someone decides the number. Automatic scaling by CPU or by metrics (HorizontalPodAutoscaler) belongs to 06-04, once we have metrics to decide with. What matters here is that scaling means changing a number and that the Service spreads traffic only across the ready replicas.

  1. RabbitMQ and PostgreSQL: inside the cluster or managed

Criterion Inside the cluster (StatefulSet, usually via Helm or an operator) Managed service (RDS/Cloud SQL, Amazon MQ/CloudAMQP)
Operations (backups, patches, failover, disks) Done by the Platform team Done by the provider
Cost Only the cluster's compute/storage More expensive per unit, no operations hours
Performance and control Total; and all the responsibility too Fewer fine adjustments; availability guarantees by contract
Local/CI environment Essential (there is no cloud in kind) Not applicable
Risk Losing data through an operational mistake by a 4-person team Provider lock-in

TechCorp's decision: in dev and in the CI kind cluster, PostgreSQL and RabbitMQ inside the cluster with Helm (two commands, disposable data); in staging and production, managed, with the URL in the Secrets (orders-db, orders-rabbitmq) and the name rabbitmq resolved by a Service of type ExternalName if RABBITMQ_URL needs to stay stable. The Platform team has four people and its priority is the gateway, the cluster and the pipeline, not being DBAs.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm install rabbitmq bitnami/rabbitmq -n techcorp --set auth.username=orders --set auth.password=dev-rabbit
helm install pg-orders bitnami/postgresql -n techcorp --set auth.username=svc_orders --set auth.password=dev-orders --set auth.database=orders

Helm installs a chart (a package of parameterized manifests) that creates the StatefulSet, the Service (rabbitmq, pg-orders-postgresql) and the volumes; RABBITMQ_URL=amqp://orders:dev-rabbit@rabbitmq:5672 works in kind without touching the services' manifests.

  1. Helm and Kustomize to avoid duplicating YAML across six services

The four Orders files would be repeated almost identically for Catalog, Inventory, Payments, Notifications, Customers and the gateway, and per environment as well (dev, staging, prod). Two tools avoid copy-and-paste:

Kustomize Helm
Idea Base YAML + per-environment patches; no templates Go templates with values.yaml; versioned packages (charts)
Built into kubectl Yes (kubectl apply -k) No (helm binary)
Learning curve Low: it is plain YAML Medium: template syntax
Fit TechCorp's own manifests Installing third-party software (RabbitMQ, ingress-nginx, Prometheus in 06-01)

TechCorp uses Kustomize for its services and Helm for third parties. Structure in techcorp/platform/k8s/:

k8s/
├── namespace.yaml
├── orders-service/
│   ├── base/
│   │   ├── kustomization.yaml
│   │   ├── configmap.yaml  deployment.yaml  service.yaml  job-migrations.yaml
│   └── overlays/
│       ├── dev/kustomization.yaml
│       └── prod/kustomization.yaml
├── catalog-service/ ...   (same shape)
└── gateway/ ...
# k8s/orders-service/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: techcorp
resources: [configmap.yaml, deployment.yaml, service.yaml, job-migrations.yaml]
commonLabels:
  app.kubernetes.io/part-of: techcorp-shop
---
# k8s/orders-service/overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../../base]
replicas: [{ name: orders-service, count: 1 }]              # in dev, one replica
images:                                                     # this is where the 05-03 pipeline will change the tag
  - { name: ghcr.io/techcorp/orders-service, newTag: sha-9f3c2ab }
patches:
  - patch: |-                                               # overrides only these keys of the base ConfigMap
      apiVersion: v1
      kind: ConfigMap
      metadata: { name: orders-service-config }
      data: { LOG_LEVEL: debug, OUTBOX_INTERVAL_MS: "500" }
---
# k8s/orders-service/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../../base]
replicas: [{ name: orders-service, count: 3 }]
images:
  - { name: ghcr.io/techcorp/orders-service, newTag: 1.0.0 }
kubectl kustomize k8s/orders-service/overlays/dev        # shows the final YAML without applying it
kubectl apply -k k8s/orders-service/overlays/dev         # applies the dev overlay
kubectl apply -k k8s/orders-service/overlays/prod        # ...or the prod one, with the same base

There is one base per service, and the overlays only state how each environment differs: replicas, image tag, two configuration keys. When the Inventory team creates its service, it will copy orders-service/ and change names, port (3006) and ConfigMap: that is the "template" part of Luis's rule; the "automate" part belongs to 05-03.

Common Mistakes and Tips

  • Checking the database in the livenessProbe. PostgreSQL goes down for 30 s and Kubernetes restarts every Orders pod in a loop. Dependencies in readiness; in liveness, only the process.
  • No resources. Without requests the scheduler piles pods onto one node; without limits the first one whose memory spikes takes the others down. The starting values are tuned by measuring (06-04).
  • selector labels that don't match template.metadata.labels. The apply fails with a clear message; in the Service, on the other hand, a misspelled selector simply leaves Endpoints empty and the service "doesn't respond". kubectl get endpoints is the first check.
  • Secrets in YAML inside the repository "because they are base64". It is not encryption. kubectl create secret or the tools of 07-04 (Sealed Secrets, External Secrets).
  • Reapplying a Job with the same name and changes: Kubernetes rejects it ("field is immutable"). Version in the name, or kubectl delete job first.
  • Tip: kubectl describe pod and its Events section answer 90% of the "it doesn't start" cases; kubectl get events --sort-by=.lastTimestamp gives the namespace-wide view.

Exercises

Exercise 1. Write the Deployment and the Service for catalog-service (image ghcr.io/techcorp/catalog-service:1.4.2, port 3001, ConfigMap catalog-service-config with PORT, MONGO_DB, LOG_LEVEL, NODE_ENV; Secret catalog-mongo with MONGO_URL) stating only the lines that differ from the Orders ones. How many replicas would you set in the prod overlay and why?

Exercise 2. An orders-service pod shows READY 0/1 for minutes but STATUS Running and no restarts. kubectl logs shows "listening on 3002" and no error trace. List, in order, the three commands you would run to diagnose it and the two most likely causes.

Exercise 3. Marta asks why the migrations Job carries the version in its name and what happens if the pipeline deploys 1.0.1 without any migration having changed. Answer and propose how to prevent the Job from failing in that case.

Solutions

Solution 1. What differs: metadata.name, labels/selector (app: catalog-service), image: ghcr.io/techcorp/catalog-service:1.4.2, containerPort: 3001, envFrom with configMapRef: catalog-service-config and secretRef: catalog-mongo, and in the Service port: 3001 (everybody's CATALOG_URL=http://catalog-service:3001). The probes point at the same /health/live and /health/ready (contract of 03-05); terminationGracePeriodSeconds, securityContext and resources can be the same. There is no migrations Job (MongoDB has no schema; the seed is dev-only). Replicas in prod: more than Orders (for example 4), because Catalog takes the ×20 campaign peaks (01-05) and is read-only, cheap to replicate; the definitive number will come from the HPA of 06-04.

Solution 2. (1) kubectl describe pod <name>: Events will show "Readiness probe failed: HTTP probe failed with statuscode: 503" (or a timeout). (2) kubectl port-forward pod/<name> 3002:3002 and curl localhost:3002/health/ready: the body says which dependency is wrong (postgres or rabbitmq). (3) kubectl get secret orders-db -o jsonpath='{.data.ORDERS_DB_URL}' | base64 -d (and the same for RabbitMQ) to see where it points. Most likely causes: the URL in the Secret points at a wrong host or has bad credentials (the service starts because config.js only validates the format, but /health/ready cannot run SELECT 1), or the dependency is not reachable from the namespace (RabbitMQ not yet installed, rabbitmq Service missing). This is the desired behavior: not ready, no traffic, no restart loop.

Solution 3. A Job is immutable and its name unique: if orders-service-migrations were reapplied with another image, the API server would reject it. With the version in the name, every deployment creates a new Job, a record remains (kubectl get jobs) and ttlSecondsAfterFinished cleans it up. If 1.0.1 brings no new migrations, the Job starts, migrate.js queries applied_migrations, sees that 001-004 are already there and exits with code 0 without doing anything: it is idempotent by design (04-04), so it does not fail; it is simply a two-second Job. The alternative of skipping the Job when "there are no changes" requires someone to decide it, and Luis's rule prefers that it always be the same pipeline.

Conclusion

Kubernetes runs the images of 05-01 declaratively and continuously: a control plane (API server, etcd, scheduler, controller manager) that reconciles the desired state, and nodes with kubelet, kube-proxy and containerd that materialize it. For orders-service we have written and applied in the techcorp namespace the orders-service-config ConfigMap, the orders-db and orders-rabbitmq Secrets created with kubectl create secret (base64, not encryption), a versioned Job that runs scripts/migrate.js with the same image, a two-replica Deployment with envFrom, resources, readinessProbe on /health/ready, livenessProbe on /health/live, terminationGracePeriodSeconds: 30 and runAsNonRoot, and the Service that provides the name http://orders-service:3002; the gateway reaches the outside through an NGINX Ingress at api.techcorp.example; RabbitMQ and PostgreSQL go via Helm in dev/kind and managed in production; and Kustomize (base + dev/prod overlays, with images: as the spot the pipeline will change) avoids duplicating YAML across the six services. Everything has been applied by hand with kubectl apply, and that is precisely what the next lesson removes: a CI/CD pipeline per service that tests, verifies pacts, builds the image and updates these manifests without anyone typing a command.

Microservices Course

Module 1: Introduction to Microservices

Module 2: Microservice Design

Module 3: Communication between Microservices

Module 4: Implementing Microservices

Module 5: Deployment and Orchestration

Module 6: Monitoring and Maintenance

Module 7: Security in Microservices

Module 8: Case Studies and Practical Examples

© Copyright 2026. All rights reserved