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
- The problem: the coexistence window
- The five strategies, compared
- Rolling updates in Kubernetes:
maxSurgeandmaxUnavailable update_configin Swarm and the mapping table- Readiness and
minReadySeconds - Watching a deployment in progress
- Rollback:
undo, revisions andrevisionHistoryLimit - Rollback in Swarm
- Rehearsing a failed deployment:
aurora-api:2.1.0 - Blue-green with two Deployments
- Canary by replicas and by Ingress
- What you watch during a canary
- Argo Rollouts, Flagger and GitOps
- Database migrations: expand/contract
- Feature flags
- 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:*.
- 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.
- Rolling updates in Kubernetes:
maxSurge and maxUnavailable
maxSurge and maxUnavailablespec:
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 availablemaxSurge |
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.
update_config in Swarm and the mapping table
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.
- Readiness and
minReadySeconds
minReadySecondsThis 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 trafficminReadySeconds: 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.
- 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, activeThe 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.
- Rollback:
undo, revisions and revisionHistoryLimit
undo, revisions and revisionHistoryLimitkubectl 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.
- 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 specificationWith 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.
- Rehearsing a failed deployment:
aurora-api:2.1.0
aurora-api:2.1.0Let'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 deadlinekubectl 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 2mThere 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 auroraThis 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.
- 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.
- 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.
- 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.
- 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.
- 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 TABLEthat 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.
- 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 > 0with tight capacity. You release precisely when you have the least capacity. UsemaxSurgeandmaxUnavailable: 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 leaveskubectl rollout undowith 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
stagingregularly. An untested procedure is not a procedure. - Tip: make the pipeline fail if
kubectl rollout statusfails. 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 -caurora-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 200Three 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 200Zero 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 9An 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 92.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.0Those 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
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
