Aurora Libros scales by itself and takes the peak. What is left is the most delicate moment of all: releasing a new version. Between the old one and the new one there is always a period in which they coexist, and deciding how that period is managed is the difference between a release nobody notices and fifteen minutes during which nobody can buy a book.

Contents

  1. The problem: the coexistence window
  2. The five strategies, compared
  3. Rolling updates in Kubernetes: maxSurge and maxUnavailable
  4. update_config in Swarm and the mapping table
  5. Readiness and minReadySeconds
  6. Watching a deployment in progress
  7. Rollback: undo, revisions and revisionHistoryLimit
  8. Rollback in Swarm
  9. Rehearsing a failed deployment: aurora-api:2.1.0
  10. Blue-green with two Deployments
  11. Canary by replicas and by Ingress
  12. What you watch during a canary
  13. Argo Rollouts, Flagger and GitOps
  14. Database migrations: expand/contract
  15. Feature flags

  1. The problem: the coexistence window

During a rolling deployment, traffic is split between different versions. That forces version 2.1.0 to be compatible with 2.0.0 on three fronts:

Front What it requires
The database schema That both versions can read and write the same table
The API contract That a client which started on the old one can finish on the new one
The shared cache That an entry written by one version is understood by the other

The third is the most surprising and the easiest to trigger: if 2.1.0 changes the format of what it stores in aurora-cache under the same key, the old replicas will read objects they do not understand and return intermittent errors that come and go depending on which Pod each request lands on. The fix is as simple as versioning the key: books:v2:*.

  1. The five strategies, compared

Strategy How Extra resources Downtime Rollback Risk When
Recreate Kills everything and starts the new one None Yes, total Redeploy (slow) High Incompatible changes, internal environments
Rolling update Replaces N at a time +maxSurge No By rolling backwards Medium The sensible default
Blue-green Two complete environments, you switch over ×2 No Instant Low Big changes, when you can pay double
Canary A % of the traffic goes to the new one +1 replica No Fast The lowest Sensitive changes with reliable metrics
Shadow A copy of real traffic with no response returned ×2 in compute No Not applicable None for the user Validating performance before exposing it
flowchart TB
    subgraph R[Rolling update]
        R1["v1 v1 v1"] --> R2["v2 v1 v1"] --> R3["v2 v2 v1"] --> R4["v2 v2 v2"]
    end
    subgraph B[Blue-green]
        B1["blue v1 ← 100%"] --> B2["blue v1 + green v2 (0%)"] --> B3["green v2 ← 100%"]
    end
    subgraph C[Canary]
        C1["v1 100%"] --> C2["v1 90% / v2 10%"] --> C3["v1 50% / v2 50%"] --> C4["v2 100%"]
    end

Shadow deserves a note: it duplicates real traffic towards the new version but discards its response, so the user never sees it. It is magnificent for validating performance with authentic load, and it has a dangerous trap: if the duplicated request writes to the database or charges a card, the operation happens twice. It only works with read traffic or with isolated dependencies.

  1. Rolling updates in Kubernetes: maxSurge and maxUnavailable

spec:
  replicas: 6
  minReadySeconds: 10               # 10 s healthy before it counts as available
  progressDeadlineSeconds: 300      # if it has not progressed in 5 min, it is marked as failed
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2                   # up to 8 Pods at once (6 + 2)
      maxUnavailable: 0             # never fewer than 6 available
maxSurge maxUnavailable Capacity during the deployment Speed Extra resources
0 1 5 of 6 (83 %) Slow None
1 1 6 of 6 Medium +1 Pod
2 0 6 of 6 at all times Medium +2 Pods
100% 0 Double during the transition Maximum ×2 (blue-green in practice)
25% 25% 75 % minimum Medium +25 %

The combination maxSurge: 2 / maxUnavailable: 0 is the right one for Aurora Libros: there are never fewer than six Pods serving, so capacity does not drop for a single instant during the release. It costs two extra Pods for a few minutes, which is a ridiculous price. maxUnavailable: 0 is also an insurance policy: if the new Pods never become ready, the deployment cannot advance, because advancing would require removing old Pods.

Watch out for one specific case: maxSurge: 0 and maxUnavailable: 0 together is an invalid configuration —the deployment could not take a single step— and Kubernetes rejects it.

  1. update_config in Swarm and the mapping table

    deploy:
      replicas: 6
      update_config:
        parallelism: 2          # 2 tasks at a time
        delay: 15s              # wait between batches
        order: start-first      # start the new one before stopping the old one
        failure_action: rollback
        monitor: 30s            # observation window after each task
        max_failure_ratio: 0.2
      rollback_config:
        parallelism: 3
        order: stop-first
Concept Kubernetes Swarm
How many at a time maxSurge / maxUnavailable parallelism
Pause between batches minReadySeconds delay
Order maxSurge > 0 = start-first order: start-first / stop-first
What to do on failure It stops (with maxUnavailable: 0) failure_action: rollback
Observation window progressDeadlineSeconds monitor
Failure threshold Implicit in the probes max_failure_ratio
Undo kubectl rollout undo docker service rollback
History revisionHistoryLimit Only the previous version

Two important differences. Swarm can undo by itself with failure_action: rollback, whereas Kubernetes limits itself to stopping and waiting for you to decide —which is defensible: stopping leaves the system in a known state and does not hide the problem. In exchange, Swarm keeps only the immediately previous version, while Kubernetes preserves as many revisions as you tell it to.

  1. Readiness and minReadySeconds

This is where what you did in 06-01 really pays off. Without a readiness probe, a rolling update is a game of roulette: Kubernetes considers a Pod "ready" the moment its container starts, sends it traffic, and your application is still establishing the PostgreSQL pool. Every replaced replica produces a few seconds of errors.

Without readiness:  container starts → gets traffic → still connecting → 502
With readiness:     container starts → /health/ready 503 → connects → 200 → gets traffic

minReadySeconds: 10 adds a second safeguard: it requires the Pod to have been ready for ten consecutive seconds before counting it as available and moving on to the next. Without it, a Pod that passes readiness and falls over two seconds later would still advance the deployment, and you would end up with six broken replicas. With it, the deployment stops at the first unstable Pod.

And the preStop with sleep 5 from 06-05 closes the other end: Pods on their way out stop receiving traffic before they close their connections. Readiness on the way in, preStop on the way out: between the two, zero errors during a release.

  1. Watching a deployment in progress

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
# or, with Kustomize: edit the overlay and kubectl apply -k

kubectl rollout status deploy/aurora-api -n aurora --timeout=300s
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api -w
kubectl get rs -n aurora -l app.kubernetes.io/name=aurora-api
# Waiting for deployment "aurora-api" rollout to finish: 4 of 6 updated replicas are available...
# deployment "aurora-api" successfully rolled out
# NAME                  DESIRED  CURRENT  READY  AGE
# aurora-api-6d4f8b7c9  0        0        0      3d   ← v2.0.0, preserved
# aurora-api-8f2a1c5e7  6        6        6      2m   ← v2.1.0, active

The two ReplicaSets are the key to everything that follows. The old one stays at zero replicas, it is not deleted: it is the history that makes undo possible. The rollout status returns code 0 if it finishes and non-zero if it fails, which makes it the natural gate for the last step of the pipeline from 06-02.

  1. Rollback: undo, revisions and revisionHistoryLimit

kubectl rollout history deploy/aurora-api -n aurora --revision=4   # view one revision
kubectl rollout undo deploy/aurora-api -n aurora                   # to the previous one
kubectl rollout undo deploy/aurora-api -n aurora --to-revision=3   # to a specific one
kubectl rollout pause deploy/aurora-api -n aurora                  # freeze it halfway
kubectl rollout resume deploy/aurora-api -n aurora
# REVISION  CHANGE-CAUSE
# 3         kubectl apply -k k8s/overlays/production (2.0.0)
# 4         kubectl set image ... aurora-api:2.1.0
# 5         kubectl rollout undo (back to 2.0.0)

Look at revision 5: the rollback does not delete number 4, it creates a new revision with the contents of number 3. History always moves forward, so you can move ahead again once you fix the problem.

revisionHistoryLimit: 5 in your manifest controls how many old ReplicaSets are kept. A value of 0 makes any undo impossible; an enormous value fills the namespace with empty objects. Somewhere between 5 and 10 is sensible.

And the golden rule, which is not a command: the rollback has to be tested before you need it. An undo nobody has ever run is an assumption, not a plan. Rehearse it in staging every time you change something relevant about the deployment, because the day you need it will be three in the morning with customers waiting.

  1. Rollback in Swarm

docker service update --image ghcr.io/auroralibros/aurora-api:2.1.0 aurora_aurora-api
docker service rollback aurora_aurora-api
docker service inspect aurora_aurora-api --format '{{.UpdateStatus.State}} {{.UpdateStatus.Message}}'
# rollback_completed  rollback: service rolled back to previous specification

With failure_action: rollback, Swarm does it on its own: if a new task fails during the monitor window and max_failure_ratio is exceeded, it reverts with nobody intervening. It is convenient and it has a clear limit: it only keeps the previous specification, so a rollback on top of another rollback puts you back where you started.

  1. Rehearsing a failed deployment: aurora-api:2.1.0

Let's simulate the real case: 2.1.0 has a typo in the name of the cache variable, so readiness never passes.

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
kubectl rollout status deploy/aurora-api -n aurora --timeout=120s
# Waiting for deployment "aurora-api" rollout to finish: 2 out of 6 new replicas updated...
# error: deployment "aurora-api" exceeded its progress deadline
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api
# NAME                        READY  STATUS   RESTARTS  AGE
# aurora-api-6d4f8b7c9-2xkpq  1/1    Running  0         3d   ← v2.0.0 serving (×6)
# aurora-api-8f2a1c5e7-b7kmd  0/1    Running  0         2m   ← v2.1.0 never ready
# aurora-api-8f2a1c5e7-n9xwp  0/1    Running  0         2m

There is the whole mechanism: six old Pods serving and two new ones that never reach 1/1. The deployment stopped by itself at the second Pod because maxUnavailable: 0 forbids removing any of the old ones while the new ones are not ready. No client has noticed anything; the two broken Pods receive no traffic because their readiness returns 503 and kube-proxy does not have them in the endpoints.

kubectl logs -n aurora aurora-api-8f2a1c5e7-b7kmd --tail=2
kubectl rollout undo deploy/aurora-api -n aurora
kubectl rollout status deploy/aurora-api -n aurora

This is the scenario that justifies every decision in the module: the readiness probe detected the problem, maxUnavailable: 0 stopped it from spreading and the revision history let you undo it in one command.

  1. Blue-green with two Deployments

The idea is to have two complete Deployments and a Service whose selector decides which one receives the traffic.

# Two identical Deployments apart from the version label and the image
metadata: { name: aurora-api-blue }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-api, version: "2.0.0" } }
---
metadata: { name: aurora-api-green }
  template:
    metadata: { labels: { app.kubernetes.io/name: aurora-api, version: "2.1.0" } }
---
apiVersion: v1
kind: Service
metadata: { name: aurora-api }
spec:
  selector:
    app.kubernetes.io/name: aurora-api
    version: "2.0.0"            # ← the switch
  ports: [{ port: 3000, targetPort: http }]
# 1. Deploy green and validate it without exposing it, through a secondary Service
kubectl apply -f k8s/blue-green/green.yaml
kubectl port-forward -n aurora deploy/aurora-api-green 8081:3000 &
curl -s localhost:8081/health/ready && curl -s localhost:8081/books | jq '.books|length'

# 2. Switch over: a single command, immediate effect
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.1.0"}}}'

# 3. If something goes wrong, go back: the same command in reverse
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.0.0"}}}'

This is what the label-based coupling from 06-04 enabled. The rollback is instant —a selector change, with nothing to start— and that is its great virtue. The cost is equally obvious: during the transition you pay twice the resources, and with twelve API replicas that is not trivial. On top of that, the connections already open against the blue Pods stay there until they finish; the switch is not as atomic as it looks.

A practical rule: keep the blue environment alive for at least one full business cycle —a peak hour, a day— before removing it. A cheap rollback only exists while the other environment is still standing.

  1. Canary by replicas and by Ingress

By replicas, with no tooling: two Deployments with the same selector label and the Service distributing by Pod count.

aurora-api-stable aurora-api-canary Traffic to the canary
9 1 ~10 %
8 2 ~20 %
5 5 ~50 %
0 10 100 % (promoted)

It is approximate —kube-proxy's distribution is random— and coarse-grained: serving 1 % would take 99 stable replicas. But it requires installing nothing.

By Ingress, with real precision:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: aurora-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"        # exactly 10 %
    nginx.ingress.kubernetes.io/canary-by-header: "x-aurora-canary"
spec:
  ingressClassName: nginx
  rules:
    - host: libros.aurora.example
      http:
        paths:
          - { path: /books, pathType: Prefix, backend: { service: { name: aurora-api-canary, port: { number: 3000 } } } }

The canary-by-header annotation is more useful than it looks: it lets the team push its own traffic to the canary with a header, and validate the new version with known real users before opening the percentage to the public.

  1. What you watch during a canary

A canary with no metrics is just a slow deployment. What turns it into a strategy is comparing the two versions using the same indicators over the same window.

Metric Typical abort threshold Where it comes from (05-06)
5xx error rate > 1 %, or double the stable one Prometheus over the logs with req_id
p95 latency > 1.2 × the stable one The API's histogram
p99 latency > 1.5 × the stable one The API's histogram
Pod restarts Any kube_pod_container_status_restarts
Memory usage > 1.3 × the stable one cAdvisor
Application errors Any new kind Structured logs
Typical progression:  10 % → 25 % → 50 % → 100 %
Wait at each step: at least 10 min or 1,000 requests (whichever comes later)

The waiting criterion matters as much as the thresholds. At three in the morning, 10 % of the traffic for five minutes is thirty requests: they tell you absolutely nothing. That is why the condition has to be double, on time and on volume. And the promotion, as soon as it is feasible, should be automatic against those thresholds: if it depends on somebody watching a dashboard, it will end up being promoted without anybody looking.

  1. Argo Rollouts, Flagger and GitOps

Tool What it brings
Argo Rollouts Replaces the Deployment with a Rollout carrying declarative canary and blue-green steps, plus AnalysisTemplate to promote or abort based on Prometheus
Flagger Automates the canary over your existing Deployment, integrated with service meshes and Ingress
Argo CD / Flux GitOps: the cluster syncs with whatever is in Git; deploying is a merge
# Argo Rollouts: the canary progression, declared
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: { duration: 10m }
        - analysis: { templates: [{ templateName: success-rate }] }
        - setWeight: 50
        - pause: { duration: 10m }

The GitOps model reverses the direction of the deployment and is worth understanding: instead of the pipeline running kubectl apply against the cluster (push), an agent inside the cluster watches the repository and applies what it finds (pull). The advantages are concrete: CI does not need cluster credentials —the risk of that last step in 06-02 disappears—, the desired state lives in Git with its history and its revisions, and any manual change is detected as drift and corrected. The rollback becomes a git revert.

  1. Database migrations: expand/contract

No deployment strategy solves this on its own, because the schema is shared by every version at once.

Imagine 2.1.0 adds language to the books table. The naive approach —deploying code and schema together— fails in both directions: if you migrate first, the old replicas do not know about the column; if you deploy first, the new ones fail because the column does not exist. And if you roll back the code, what do you do with the schema?

The answer is the expand/contract pattern, in three separate deployments:

-- PHASE 1 — EXPAND: additive and backwards-compatible
ALTER TABLE books ADD COLUMN language VARCHAR(8) DEFAULT 'es';

2.0.0 keeps working: it ignores a column it does not know about. Only the schema is deployed, without touching the code.

-- PHASE 2 — MIGRATE: 2.1.0 writes to both places and reads from the new one
UPDATE books SET language = 'es' WHERE language IS NULL;

Now 2.1.0 does get deployed, already using language. During the rolling update both versions coexist without any problem: the old one does not touch the column, the new one fills it in. And the rollback is safe, because going back to 2.0.0 breaks nothing.

-- PHASE 3 — CONTRACT: only when nobody uses the old thing any more
ALTER TABLE books ALTER COLUMN language SET NOT NULL;
ALTER TABLE books DROP COLUMN old_language;
Change Backwards-compatible? How to do it
Adding a column with a DEFAULT Yes Straight away
Adding a NOT NULL column with no default No Expand with a default → backfill → SET NOT NULL
Renaming a column No Add the new one → write to both → migrate → drop
Dropping a column No Stop using it → wait a cycle → drop
Changing the type No New column, dual writes, migrate, drop
Adding an index Yes CREATE INDEX CONCURRENTLY (does not block)

The rule that follows: schema and code are never deployed coupled together. The schema always goes first and additively; the removal of the old thing always goes last and one cycle behind. That way, at every moment, the database is compatible with the previous version and with the next one, and any code rollback is safe.

Warning. A migration over real data can lock tables, exhaust the disk through the WAL or make a deployment irreversible. An ALTER TABLE that rewrites a large table leaves the service blocked for as long as it runs. Always validate the migration plan, the window and the fallback procedure with the data officer in your organization, and rehearse it first against a copy of the production volume.

  1. Feature flags

The last piece separates two things we tend to confuse: deploying code and enabling a feature.

// api/src/flags.js — the flag arrives through configuration, like everything else (06-01)
const enabled = new Set((config.flags ?? '').split(',').filter(Boolean));
const isEnabled = (name) => enabled.has(name);

app.get('/books', async (req, res) => {
  const books = await catalog.list();
  if (isEnabled('recommendations')) books.recommended = await recommender.forRequest(req);
  res.json(books);
});

With the flag off, the 2.1.0 code gets deployed all the way to production and does nothing. It is enabled afterwards, whenever you want, by changing a ConfigMap and without deploying anything; and if it goes badly, it is switched off in seconds, which is infinitely faster than any image rollback.

Aspect Deployment rollback Switching off a flag
Time 1-5 min Seconds
Scope The whole version Only that feature
Risk It recreates Pods None
Segmentation No By user, percentage or region

The price is technical debt: every flag is a live branch in the code and two paths to test. They are set with an expiry date and removed as soon as the feature is settled, or you end up with twenty flags and not one tested combination.

Common Mistakes and Tips

  • Deploying with no readiness probe. Every replaced replica produces a few seconds of errors. It is the prerequisite for everything else.
  • maxUnavailable > 0 with tight capacity. You release precisely when you have the least capacity. Use maxSurge and maxUnavailable: 0.
  • Schema and code in the same deployment. It blocks the rollback: you can go back on the code, but not on the schema. Expand/contract, always.
  • A canary with no metrics and no criteria. It is a slow deployment with more steps. Define thresholds and a window before you start.
  • revisionHistoryLimit: 0. It leaves kubectl rollout undo with nothing to go back to.
  • Blue-green and deleting blue right away. You lose the only thing that made the rollback cheap. Wait a full cycle.
  • Changing the cache format without versioning the key. Intermittent errors that are impossible to reproduce during coexistence.
  • Tip: rehearse the rollback in staging regularly. An untested procedure is not a procedure.
  • Tip: make the pipeline fail if kubectl rollout status fails. A green deployment with broken Pods is the worst of both worlds.

Exercises

Exercise 1. Rehearse a failed deployment of aurora-api:2.1.0 that never passes readiness: watch it stop by itself, check that no client receives errors throughout the whole process, and undo it while verifying the revision history.

Exercise 2. Implement a complete blue-green: deploy the green version, validate it without exposing it, switch the Service over, measure the switching time and go back.

Exercise 3. Apply expand/contract to add the language column to books with no downtime, demonstrating that a code rollback remains safe at every phase.

Solutions

Solution 1.

# Continuous load in the background throughout the exercise
kubectl run watcher --image=curlimages/curl -n aurora --restart=Never -- \
  sh -c 'while true; do curl -s -o /dev/null -w "%{http_code}\n" http://aurora-api:3000/books; sleep 0.2; done' &

kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
kubectl rollout status deploy/aurora-api -n aurora --timeout=180s; echo "code: $?"
# error: deployment "aurora-api" exceeded its progress deadline
# code: 1

kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api \
  -o custom-columns=POD:.metadata.name,READY:.status.containerStatuses[0].ready | sort -k2
kubectl get endpoints aurora-api -n aurora -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w
kubectl logs -n aurora watcher --tail=2000 | sort | uniq -c
aurora-api-8f2a1c5e7-b7kmd   false        ← v2.1.0
aurora-api-8f2a1c5e7-n9xwp   false        ← v2.1.0
aurora-api-6d4f8b7c9-2xkpq   true         ← v2.0.0 (×6)
6
   1983 200

Three numbers demonstrate everything. There are eight Pods running but only six endpoints: the two 2.1.0 Pods exist and never entered the balancing because their readiness never returned 200. And the watcher recorded 1,983 responses, every one of them a 200, during a deployment that failed completely.

The mechanism has three linked pieces, and all three were decided in earlier lessons. The readiness probe from 06-01 detected that 2.1.0 could not serve; kube-proxy excluded it from the endpoints, so it received not a single request; and maxUnavailable: 0 prevented any old Pod from being removed, because doing so would have dropped below six available. The deployment froze in a safe state.

kubectl rollout undo deploy/aurora-api -n aurora
kubectl rollout status deploy/aurora-api -n aurora
kubectl rollout history deploy/aurora-api -n aurora
kubectl logs -n aurora watcher --tail=3000 | sort | uniq -c
# deployment "aurora-api" successfully rolled out
# REVISION  CHANGE-CAUSE
# 3         kubectl apply -k k8s/overlays/production (2.0.0)
# 4         kubectl set image ... aurora-api:2.1.0
# 5         kubectl rollout undo
#    2874 200

Zero errors in the whole exercise: failed deployment, waiting and rollback included. Notice that the undo was practically instant, and the reason is that it started nothing: the six 2.0.0 Pods never stopped existing, so undoing consisted of deleting the two broken Pods and returning the old ReplicaSet to six desired replicas. A rollback after a successful deployment does take time, because the old Pods have to be recreated.

And the code: 1 from rollout status is the piece the pipeline in 06-02 was missing: all it takes is not ignoring that exit code for a deployment like this to turn the job red and trigger the undo automatically.

Solution 2.

kubectl apply -f k8s/blue-green/green.yaml
kubectl wait --for=condition=available deploy/aurora-api-green -n aurora --timeout=120s
kubectl get endpoints aurora-api -n aurora -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w
kubectl port-forward -n aurora deploy/aurora-api-green 8081:3000 &
curl -s localhost:8081/books | jq -r '"\(.source) \(.books|length)"'
# 6
# db 9

An important detail in the first number: even though the green Deployment is already ready, the Service still has six endpoints, blue's. The new version is alive, validated through port-forward and serving the nine titles, and it receives not a single user request. That separation between "deployed" and "exposed" is the essence of blue-green.

start=$(date +%s%3N)
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.1.0"}}}'
kubectl get endpoints aurora-api -n aurora -o jsonpath='{.subsets[0].addresses[*].ip}' | wc -w
echo "switch: $(( $(date +%s%3N) - start )) ms"
kubectl logs -n aurora watcher --tail=200 | sort | uniq -c
# 6
# switch: 412 ms
#     200 200
kubectl patch svc aurora-api -n aurora -p '{"spec":{"selector":{"version":"2.0.0"}}}'   # back again
Strategy Switching time Rollback time Resources meanwhile
Rolling update (6 replicas) ~90 s ~90 s 6 + 2 Pods
Blue-green 0.4 s 0.4 s 12 Pods

The 412 milliseconds are the entire argument for blue-green, and the 12 in the last column is its bill. The switch is that fast because it starts and stops nothing: it only rewrites a selector, and the endpoints controller recalculates which Pods belong to the Service.

Two nuances the table does not show. First: the HTTP keep-alive connections already established against blue Pods keep being served by the old version until they close, so the switch is not atomic from the user's point of view. Second, more serious: while the two environments coexist, both versions are writing to the same database and the same cache, which means the compatibility requirement from section 1 still applies. Blue-green solves the routing, not the data compatibility.

Solution 3.

-- PHASE 1 (EXPAND) — deployed ON ITS OWN, without touching the code
ALTER TABLE books ADD COLUMN language VARCHAR(8) DEFAULT 'es';
kubectl exec -n aurora aurora-db-0 -- psql -U aurora -d aurora_books -f /tmp/expand.sql
curl -s localhost:8080/books | jq -r '"\(.source) \(.books|length)"'   # still on 2.0.0
# db 9

2.0.0 keeps returning the nine titles with a new column it completely ignores, because its SELECT names the columns it needs. That is the property that makes phase 1 safe: an additive change with a default value is invisible to the old code.

# PHASE 2 (MIGRATE) — now the new code, with a normal rolling update
kubectl set image deploy/aurora-api api=ghcr.io/auroralibros/aurora-api:2.1.0 -n aurora
kubectl rollout status deploy/aurora-api -n aurora
curl -s localhost:8080/books | jq -r '.books[1] | "\(.title) [\(.language)]"'
kubectl rollout undo deploy/aurora-api -n aurora     # a test rollback, halfway through
curl -s localhost:8080/books | jq -r '.books[1].title'
# Rayuela [es]      ← on 2.1.0
# Rayuela           ← after the rollback to 2.0.0

Those two lines are the demonstration that was asked for. On 2.1.0, /books includes the language; after the rollback to 2.0.0, the field disappears from the response and everything else keeps working. The database never had to change at any point, because the column was compatible with both versions.

-- PHASE 3 (CONTRACT) — days later, when no old version is left alive
ALTER TABLE books ALTER COLUMN language SET NOT NULL;
Phase What gets deployed Is a code rollback safe? Is a schema rollback needed?
1. Expand Schema only (additive) Yes (it has not changed) No
2. Migrate Code only (2.1.0) Yes No
3. Contract Schema only (restrictive) Yes, if no old versions remain No

The decisive column is the third: in none of the three phases do you need to undo the schema, and that is exactly the point of the pattern. Compared with the coupled approach, the difference is enormous: had you deployed the ALTER TABLE ... NOT NULL alongside 2.1.0, the code rollback would have left 2.0.0 inserting rows with no language against a mandatory column, and every insert would have failed.

Phase 3 demands a discipline that is easily forgotten: it cannot be run until you are sure no old version is still alive, and that includes batch jobs, reporting processes or third-party integrations that nobody may remember. That is why the prudent thing is to let a full cycle go by —days, not minutes— between phase 2 and phase 3. And it is why the migration plan, the window and the fallback procedure are agreed with the data officer before touching production.

Conclusion

You now know how to release without anybody losing the ability to buy a book. You are clear about the underlying problem —throughout every deployment two versions coexist over the same database and the same cache— and about the five strategies with their real cost: recreate with its downtime, the rolling update as the sensible default, blue-green with its 412-millisecond switch in exchange for double the resources, the canary as the lowest-risk option when you have reliable metrics, and shadow with its warning about duplicated side effects.

You have mastered the rolling update from the inside: maxSurge: 2 with maxUnavailable: 0 so capacity never drops for an instant, minReadySeconds so a Pod that falls over two seconds later is not counted as good, progressDeadlineSeconds as the limit, and the mapping table against Swarm's update_config. Above all, you have seen why the readiness probe from 06-01 is the piece that holds everything else up: the rehearsal of the failed aurora-api:2.1.0 deployment ended with eight Pods running, only six endpoints and 1,983 responses, all of them 200, with the deployment frozen in a safe state and a rollout status returning code 1 for the pipeline to catch. The undo was instant because there was nothing to start, and the revision history always moves forward, it never deletes.

You have set up blue-green by switching a selector —the label-based coupling from 06-04 paying off—, the canary by replicas and by Ingress annotations with its header for the team, and you know what to watch during a canary and by which double criterion of time and volume it gets promoted or aborted. You know about Argo Rollouts and Flagger for automating it, and about GitOps with Argo CD or Flux reversing the direction of the deployment so CI does not need cluster credentials. And you have solved what no strategy fixes on its own: the expand/contract pattern in three deployments, applied to adding language to books, with the demonstration that a code rollback is safe at every phase because the schema is never deployed coupled to it. Rounding off the picture are feature flags, which separate deploying from enabling and turn a minutes-long rollback into a seconds-long one.

And with that, module 6 closes. Aurora Libros came in as a Compose stack on your laptop and leaves as a platform: a production-grade 2.0.0 image with configuration on the outside, startup validation that fails in 0.4 seconds, a graceful shutdown that loses not a single request and three probes with separate semantics; a pipeline that on every git push tests against a real PostgreSQL and Redis, builds for two architectures in 41 seconds thanks to the remote cache, scans, signs with Cosign and publishes; a cluster —first Swarm with its overlay networks and its routing mesh, then Kubernetes with the four services in real manifests, a StatefulSet for the data, Kustomize per environment and /books returning the nine titles—; a HorizontalPodAutoscaler that took the p95 from 1,840 ms to 88 ms without anybody touching a thing, with PgBouncer solving the bottleneck that sat where you were not looking; and, now, releases with no downtime and a tested way back. Aurora Libros no longer lives on your machine: it lives in a cluster, deploys itself from a commit, grows with the load and updates without any customer noticing.

In module 7 we lift our eyes from the project to look around. You will see how the hosts that hold all of this up are provisioned, when Compose is the right call and when Kubernetes is, with an honest comparison of the two, what Docker Desktop brings and what it costs, which third-party tools and plugins deserve a place in your workflow, how Podman, containerd and the OCI standard fit in —those images of yours that you now know are not "Docker's"— and where the container ecosystem is heading.

Docker: From Beginner to Advanced

Module 1: Introduction to Docker

Module 2: Working with Docker Images

Module 3: Docker Containers

Module 4: Docker Compose

Module 5: Advanced Docker Concepts

Module 6: Docker in Production

Module 7: Docker Ecosystem and Tools

© Copyright 2026. All rights reserved