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
- Why an orchestrator is needed
- Kubernetes architecture: control plane and nodes
- The basic objects
- Local environment with kind and essential
kubectl Namespace,ConfigMapandSecretfororders-service- A
Jobfor the migrations - The
orders-serviceDeployment, line by line - The
Serviceand the DNS nameorders-service:3002 Ingress: exposing the gateway atapi.techcorp.example- Manual scaling and deployment status
- RabbitMQ and PostgreSQL: inside the cluster or managed
- Helm and Kustomize to avoid duplicating YAML across six services
- 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.
- 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
Deploymentcontroller createsReplicaSets; theReplicaSetcontroller creates or deletes pods untilreplicasmatches; theEndpointscontroller maintains the list of ready pods for eachService(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).
- 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.example → gateway: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 |
- Local environment with kind and essential
kubectl
kubectlkind (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: workerkind 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.ioThe 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
Namespace, ConfigMap and Secret for orders-service
Namespace, ConfigMap and Secret for orders-serviceThe 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.
- A
Job for the migrations
Job for the migrationsscripts/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 errorThe 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.
- The
orders-service Deployment, line by line
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 diskThe points that raise the most questions:
selectorandlabels: theDeployment(through itsReplicaSet) "owns" the pods whoseapp: orders-servicelabel matches; theServiceof section 8 uses the same selector. Changingselectoron an existingDeploymentis not allowed: choose well from the start.readinessProbeversuslivenessProbe(03-05): readiness looks at the service's own dependencies (PostgreSQL and RabbitMQ in the/health/readyof 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: 30closes the loop of the graceful shutdown: when a pod is deleted, Kubernetes removes it from the Endpoints and sends SIGTERM to PID 1 (thenodeof 05-01); the service sets/health/readyto 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:runAsNonRootverifies what the image already does (USER node);readOnlyRootFilesystemforces any write to go to an explicitemptyDir. The rest of the hardening (capabilities, seccomp, admission policies) belongs to 07-04.
- The
Service and the DNS name orders-service:3002
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.
Ingress: exposing the gateway at api.techcorp.example
Ingress: exposing the gateway at api.techcorp.exampleThe 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-02kubectl 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.
- 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.
- 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=ordersHelm 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.
- 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 baseThere 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. Withoutrequeststhe scheduler piles pods onto one node; withoutlimitsthe first one whose memory spikes takes the others down. The starting values are tuned by measuring (06-04). selectorlabels that don't matchtemplate.metadata.labels. Theapplyfails with a clear message; in theService, on the other hand, a misspelled selector simply leavesEndpointsempty and the service "doesn't respond".kubectl get endpointsis the first check.- Secrets in YAML inside the repository "because they are base64". It is not encryption.
kubectl create secretor the tools of 07-04 (Sealed Secrets, External Secrets). - Reapplying a
Jobwith the same name and changes: Kubernetes rejects it ("field is immutable"). Version in the name, orkubectl delete jobfirst. - Tip:
kubectl describe podand itsEventssection answer 90% of the "it doesn't start" cases;kubectl get events --sort-by=.lastTimestampgives 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
- Basic Concepts of Microservices
- Advantages and Disadvantages of Microservices
- Comparison with the Monolithic Architecture
- When to Adopt Microservices: Decision Criteria
- The Course Case Study: TechCorp's Online Store
Module 2: Microservice Design
- Microservice Design Principles
- Decomposing Monolithic Applications
- Defining Bounded Contexts
- Data Management: One Database per Service
- Distributed Consistency: Sagas, CQRS and Event Sourcing
Module 3: Communication between Microservices
- RESTful APIs
- Asynchronous Messaging
- Communication Protocols: gRPC, GraphQL
- API Gateway and Backend for Frontend
- Service Discovery and Load Balancing
- API Contracts and Versioning
Module 4: Implementing Microservices
- Choosing Technologies and Tools
- Building a Simple Microservice
- Configuration Management
- Hands-On Integration: Consuming APIs and Publishing Events
- Testing Microservices: Unit, Integration and Contract Tests
Module 5: Deployment and Orchestration
- Containers and Docker
- Orchestration with Kubernetes
- CI/CD for Microservices
- Deployment Strategies: Rolling, Blue-Green and Canary
- Service Mesh: Istio and Linkerd
Module 6: Monitoring and Maintenance
- Monitoring and Logging
- Distributed Tracing with OpenTelemetry
- Error Handling and Recovery
- Scalability and Performance
- SLOs, Alerts and Incident Management
Module 7: Security in Microservices
- Authentication and Authorization
- Communication Security
- Security Practices
- Container and Kubernetes Security
