The previous four lessons have assembled the pieces separately: where the code lives, how it is built and tested, how it is deployed without taking the service down, and how it is all chained together. This lesson adds no new services. It takes a real change of Luis's and follows it from his laptop to production, step by step, seeing at each point what gets checked and what exactly happens if it fails.
The change was deliberately chosen not to be trivial. Sara has asked for a delivery time slot to be
added to the order, and that touches four things at once: the shop's code, a consumer of
cola-mercadofresco-pedidos, the Aurora schema and the contract of the PedidoConfirmado event. It is
exactly the kind of change that caused an incident in module 7 — a producer deployed before its consumer —
and that we are going to make here without anybody typing a command or crossing their fingers.
Cost warning. The full journey of this change — four pipeline executions across development, pre-production and production, plus the PR builds — costs around 0.60 USD. AppConfig, which turns up at the end, charges 0.0000002 USD per configuration request with a 60 s cache: for MercadoFresco, pennies a month. Fictitious data.
Contents
- The change: delivery time slot
- The full journey at a glance
- Branch, commits and pull request
- Build, tests and artefact
- Schema compatibility: expand and contract in Aurora
- Contract compatibility: versioning
PedidoConfirmado - The right order between producer and consumer
- Deployment to development, smoke tests and Marta's approval
- Canary in production, monitoring and promotion
- Configuration and secrets per environment
- The test pyramid applied
- DORA metrics: MercadoFresco before and after
- Rollback runbook and the failure detected late
- Small deployments and feature flags
- Friday afternoon as a business decision
- Module wrap-up and cost
- Common mistakes and tips
- Exercises
- Conclusion
The change: delivery time slot
Sara backs it up with a figure: 31 % of delivery incidents are down to the customer not being in. Each
one costs a second attempt, a phone call and, with fresh produce, sometimes the goods themselves. The
request is to add three slots — manana, tarde, 24h — to the checkout flow.
What looks like a form field touches five pieces:
| Piece | What changes | Risk |
|---|---|---|
Web shop (asg-mercadofresco-tienda) |
Selector in the checkout flow | Low |
| Orders API | Accepts and persists franja_entrega |
Medium |
| Aurora schema | New column in pedidos |
High: does not roll back with the code |
PedidoConfirmado event |
New attribute in the detail |
High: there are third-party consumers |
| Warehouse consumer | Reads the slot to order the routes | Medium |
The two high risks are the heart of the lesson, and they share one property: they are the two things that CodeDeploy's automatic rollback cannot undo. Everything else goes back in ninety seconds.
The full journey at a glance
flowchart TB
A[Luis: branch funcionalidad/franja-horaria] --> B[Push: pipeline on PR<br/>builds and tests]
B -->|Red| B1[The PR cannot be merged]
B -->|Green| C[PR 47: Marta reviews<br/>1 mandatory approval]
C --> D[Squash into desarrollo]
D --> E[Build: artefact<br/>1.6.0-a3f9c21 immutable]
E --> F[Quality: static,<br/>contract, secrets]
F --> G[EXPAND migration<br/>NULLABLE column]
G --> H[Deploy consumer<br/>FIRST]
H --> I[Deploy shop to dev]
I --> J[Dev smoke: purchase flow]
J --> K[Pre-production + smoke]
K --> L[MARTA APPROVAL<br/>version, diff, migration]
L --> M[Production: blue/green<br/>+ canary on payments]
M --> N[Quality gate<br/>15 min of metrics]
N -->|OK| O[Promotion: tag v1.6.0]
N -->|Degraded| P[Automatic ROLLBACK<br/>90 s]
O --> Q[Weeks later:<br/>CONTRACT]
Branch, commits and pull request
Luis branches off desarrollo, following the model from 08-01, and holds himself to the three-day limit:
The commits, in the conventional format the pipeline will read later on:
feat(pedidos): add franja_entrega to the model and to the API feat(eventos): include franja_entrega in PedidoConfirmado as version 2 feat(almacen): order delivery routes by slot test(pedidos): cover valid slots, invalid and missing docs(catalogo-eventos): document PedidoConfirmado v2
The first feat determines that the version goes up to 1.6.0 — MINOR, compatible functionality. If any
of them carried BREAKING CHANGE: in the body it would be 2.0.0, and that is precisely what we will avoid.
When he pushes the branch, the pull request trigger from 08-04 starts the pipeline in reduced mode:
build, tests and quality, with no deployment stages. The result is published on the PR as a status check,
and the PR cannot be merged while it is red. This is where the main protection left open in 08-01 is
finally closed: it is not enough for somebody to approve the change, the build also has to be green before
the merge button does anything.
PR #47 uses the template from 08-01, with one section that in this change is the most important of all:
## Risks
- Aurora schema: NULLABLE column, no DEFAULT, no index. EXPAND phase.
Does NOT roll back with the code. Contraction planned for 3 weeks time.
- PedidoConfirmado event moves to version 2: OPTIONAL attribute added.
v1 consumers ignore it. Verified with contract tests.
- Deployment order: warehouse consumer BEFORE the shop.Marta reviews and approves; Luis merges with squash, and the resulting commit lands on desarrollo.
Build, tests and artefact
On merge, the full pipeline starts. The build stage does what 08-02 described and produces
mercadofresco-tienda-1.6.0-a3f9c21.zip, with the commit in its metadata.
This is the only thing that gets built in the whole journey. The same S3 object will be deployed to development, to pre-production and to production. If the pipeline rebuilt before production, Marta's approval would stop meaning anything, as we saw in exercise 1 of 08-04.
| Check | Blocks | What happens if it fails |
|---|---|---|
ruff and formatting |
Yes | Fails in 2 s; Luis fixes it and pushes again |
git-secrets |
Yes | Fails, and rotation is needed if the secret was real |
| Unit tests (218) | Yes | The PR does not get merged |
| Coverage ≥ 75 % | Yes | Write the missing tests |
pip-audit |
Yes, with a warning | Update the dependency or document it |
| Contract tests | Yes | The key to this change; see below |
bandit |
No, report | Reviewed on Mondays |
Schema compatibility: expand and contract in Aurora
Applying the rule from 08-03, the schema change does not go in the same step as the code and is split into three phases separated in time.
Phase 1, EXPAND, in a pipeline stage of its own before any application deployment:
-- migraciones/2026-08-02-001-expandir-franja-entrega.sql
-- Compatible with version 1.5.2, which does not know about this column.
ALTER TABLE pedidos ADD COLUMN franja_entrega VARCHAR(10) NULL;
-- No NOT NULL: 1.5.2 inserts without the column and does not fail.
-- No DEFAULT: it does not rewrite 4.2 million rows or lock the table.
-- No index: that is added in the contraction phase, with CONCURRENTLY.Those three comments are the whole exercise. With NOT NULL, the previous version would fail on every
insert during the gradual deployment. With DEFAULT, PostgreSQL 11 and later do not rewrite the table, but
earlier engines do, and over 4.2 million rows that is a lock lasting minutes. And the index is postponed
because CREATE INDEX blocks writes: it will be done with CONCURRENTLY and outside the deployment.
Phase 2, MIGRATE, is the 1.6.0 code: it writes franja_entrega when present and tolerates NULL when not.
Phase 3, CONTRACT, three weeks later and as a change of its own, when no instance is serving 1.5.2 any more and it has been confirmed that every new order comes with a slot:
CREATE INDEX CONCURRENTLY idx_pedidos_franja ON pedidos(franja_entrega);
UPDATE pedidos SET franja_entrega = '24h' WHERE franja_entrega IS NULL;
ALTER TABLE pedidos ALTER COLUMN franja_entrega SET NOT NULL;The automatic control that stops anyone skipping this is the migration analysis in CodeBuild that we
designed in exercise 3 of 08-03: it fails the build if it finds DROP COLUMN, RENAME COLUMN,
SET NOT NULL or DROP TABLE in a migration file without the -- CONTRACCION-APROBADA tag. Phase 3
carries it and phase 1 does not need it.
Contract compatibility: versioning PedidoConfirmado
The other high risk. PedidoConfirmado is consumed by three targets of bus-mercadofresco: the warehouse
queue, the analytics one and the partner's ERP API. Luis controls neither of the last two, and that is
the problem: publishing a different event can break somebody you did not even know was listening.
| Change | Compatible? | What to do |
|---|---|---|
| Adding an optional field | Yes | Bump the minor version; go ahead |
| Adding a mandatory field | No | Treat it as optional with a default value |
| Removing a field | No | Deprecate, warn, wait, remove |
| Renaming a field | No | Add the new one, publish both, remove the old one |
| Changing the type of a field | No | New field with a different name |
| Restricting the allowed values | No | Widen, never restrict |
Adding franja_entrega as an optional field is the easy case, and the event moves up to version 2:
{
"version": "2",
"source": "mercadofresco.tienda",
"detail-type": "PedidoConfirmado",
"detail": {
"id_pedido": "PED-2026-00841",
"id_cliente": "CLI-4471",
"importe_eur": 48.20,
"lineas": [{ "sku": "FRUT-0012", "uds": 3 }],
"franja_entrega": "tarde",
"confirmado_en": "2026-08-02T18:14:22Z"
}
}A v1 consumer receives an object with a field it does not know about and ignores it, as long as it was
written following the golden rule: be strict in what you publish and tolerant in what you receive. A
consumer that validates the schema with additionalProperties: false would break, and that is why the
contract test is blocking:
# tests/contrato/test_pedido_confirmado.py
import pytest
from app.eventos import construir_evento_pedido_confirmado
from consumidores.almacen_v1 import procesar as procesar_v1 # the OLD consumer
from consumidores.almacen_v2 import procesar as procesar_v2
ORDER = {"id_pedido": "PED-2026-00841", "id_cliente": "CLI-4471",
"importe_eur": 48.20, "lineas": [{"sku": "FRUT-0012", "uds": 3}],
"franja_entrega": "tarde"}
def test_OLD_consumer_does_not_break_with_the_NEW_event():
"""Forward compatibility: v1 must ignore the field it does not know."""
r = procesar_v1(construir_evento_pedido_confirmado(ORDER))
assert r["estado"] == "procesado" # does not raise, does not reject
def test_NEW_consumer_tolerates_the_OLD_event():
"""Backwards compatibility: during the deployment there will be v1 events in flight."""
without_slot = {k: v for k, v in ORDER.items() if k != "franja_entrega"}
ev = construir_evento_pedido_confirmado(without_slot)
r = procesar_v2(ev)
assert r["estado"] == "procesado"
assert r["franja_asignada"] == "24h" # explicit default value
def test_invalid_slot_is_rejected_before_publishing(fake_bus):
with pytest.raises(ValueError, match="franja_entrega"):
construir_evento_pedido_confirmado({**ORDER, "franja_entrega": "noche"})
assert fake_bus.published_events == 0The two central tests are the second and the third, and it is worth understanding why both are needed. The second checks forward compatibility: the old consumer with the new event, which is what happens as soon as the shop starts publishing v2 and some consumer has still not been updated. The third checks backwards compatibility: the new consumer with the old event, which is what happens with the messages that were already in the queue when the consumer was deployed. During a gradual deployment both situations exist at the same time, and one test on its own only covers half of it.
For the partner's ERP, which cannot be tested, the safeguard is a different one: the EventBridge archive we turned on in 07-03. If the partner breaks, there are 90 days of events to replay.
The right order between producer and consumer
This is where the incident left hanging in module 7 gets closed. The rule is short and holds for any contract change:
Always deploy the reader first, the writer second.
The reasoning is set theory. A new consumer understands the old events and the new ones — because we
have tested it — so deploying it first is safe: during the window there are only old events, which it knows
how to process. The other way round does not work: if the producer goes out first, there are v2 events
travelling towards v1 consumers for as long as the deployment lasts, and it only takes one of them
validating strictly for messages to appear in mercadofresco-pedidos-fallidos.
In the pipeline this is a stage with two actions and a different runOrder, which is exactly the mechanism
from 08-04:
{
"name": "DesplegarProduccion",
"actions": [
{ "name": "ConsumidorAlmacen", "runOrder": 1,
"configuration": { "ApplicationName": "app-mercadofresco-trabajadores",
"DeploymentGroupName": "dg-mercadofresco-trabajadores-produccion" },
"inputArtifacts": [{ "name": "PaqueteTienda" }] },
{ "name": "TiendaWeb", "runOrder": 2,
"configuration": { "ApplicationName": "app-mercadofresco-tienda",
"DeploymentGroupName": "dg-mercadofresco-tienda-produccion" },
"inputArtifacts": [{ "name": "PaqueteTienda" }] }
]
}runOrder: 1 for the consumer and 2 for the shop. The order stops depending on somebody remembering
it: it is declared, versioned and executed the same way every single time. That is, in one sentence, the
value of the whole of module 8.
Deployment to development and smoke tests
In development the pipeline uses AllAtOnce, because speed matters more than availability. The smoke tests
from 08-04 run against the freshly deployed environment and add two cases from this change:
def test_order_with_slot_is_confirmed_and_persisted():
r = _crear_pedido({"franja_entrega": "tarde"})
assert r.status_code == 201
assert r.json()["franja_entrega"] == "tarde"
assert r.elapsed.total_seconds() < 2.0
def test_order_WITHOUT_slot_still_works():
"""Backwards compatibility against the real environment, not a simulated one."""
r = _crear_pedido({})
assert r.status_code == 201
assert r.json()["franja_entrega"] in (None, "24h")The second test is the one that really matters: it verifies against a real environment that a customer with the old app on their phone can still buy. If it fails, the pipeline stops in development and nobody outside the team has noticed a thing.
Marta's approval
At 10:42 the notification reaches alertas-mercadofresco and Slack:
Pipeline: pipeline-mercadofresco-tienda | Execution: 7a1f2c33 Version 1.6.0-a3f9c21 | Author: luis | PR #47 Pre-production smoke: OK (12/12) | Preprod p95 latency: 380 ms MIGRATION: EXPAND phase applied (NULLABLE column, does not roll back with the code) EVENT: PedidoConfirmado moves to v2 (optional attribute) Window: OK (Tuesday 10:42, outside the forbidden slot) Diff: github.com/mercadofresco/mercadofresco-tienda/compare/v1.5.2...1.6.0-a3f9c21
Marta opens the diff, sees 214 lines in four files, checks that the migration is an expansion and approves. Forty seconds. Not out of blind trust, but because the message answers in advance the questions she would ask: what is going in, who did it, if it was tested, what does not revert and if the timing is right.
Canary in production and monitoring
The production stage performs three coordinated deployments:
| Order | What | Strategy | Duration |
|---|---|---|---|
| 1 | Warehouse consumer | In place, AllAtOnce |
~2 min |
| 2 | mercadofresco-cobrar-pago Lambda |
Canary10Percent5Minutes |
~5 min |
| 3 | Web shop | Blue/green, 30 min wait | ~12 min |
Throughout the process three alarms wired to the automatic rollback are watching:
mercadofresco-alb-latencia-alta, mercadofresco-pedidos-fallidos and
mercadofresco-cobrar-pago-errores-canario. And when it finishes, the quality gate from 08-04 watches
the MercadoFresco/Tienda metrics for 15 minutes, with the window sized to fit inside the 30 minutes of
terminationWaitTimeInMinutes — if the gate took longer, the blue environment would already have been
terminated and the ninety-second rollback would have ceased to exist.
Promotion or automatic rollback
If all goes well, the pipeline finishes, CodeDeploy terminates the blue environment after 30 minutes and a final action tags the version:
What gets tagged is what reached production and survived the quality gate, not what was built. It is an important difference: the tag certifies a fact, not an intention.
If something fails, each mechanism acts within its own timeframe:
| Failure | Who detects it | Reaction | Time |
|---|---|---|---|
| Errors in the canary | Alias alarm | CodeDeploy shifts the weight back | Seconds |
| High latency after the traffic switch | mercadofresco-alb-latencia-alta |
Listener goes back to blue | ~90 s |
| Messages in the DLQ | mercadofresco-pedidos-fallidos |
Rollback + notification | ~2 min |
| Subtle degradation with no alarm | Quality gate | Stops the pipeline | ~15 min |
| None of the above | A person | Runbook | Minutes or hours |
And the part that no mechanism rolls back: the franja_entrega column is still there. That is fine,
because it is NULLABLE and 1.5.2 ignores it. That is exactly why the expansion phase is harmless: it
turns an irreversible change into one that does not get in the way.
Configuration and secrets per environment
The artefact is the same in all three environments, so the difference between them cannot live in the package: it lives in the configuration read at start-up, with what we set up in 04-03.
aws ssm put-parameter --name /mercadofresco/produccion/franjas_entrega_activas \
--value "manana,tarde,24h" --type String --overwrite \
--profile mercadofresco-dev --region eu-west-1| Type of value | Where it lives | Example |
|---|---|---|
| Non-sensitive configuration | Parameter Store /mercadofresco/<entorno>/ |
Active slots, thresholds |
| Secrets | Secrets Manager, with rotation | mercadofresco/produccion/rds/mfadmin |
| Code constants | In the repository | Date format, error codes |
| "By hand" values | Nowhere | — |
The last row is the rule: if a value differs between environments, it has to be in Parameter Store. A value set by hand on an instance disappears with the next blue/green deployment — the instances are new — and comes back as an intermittent failure that is impossible to diagnose.
The test pyramid applied
| Level | How many | Where they run | Duration | Blocks? |
|---|---|---|---|---|
| Unit | 218 | CodeBuild, no network | 45 s | Yes |
| Integration | 34 | CodeBuild with moto |
1 min | Yes |
| Contract | 12 | CodeBuild | 15 s | Yes |
| Smoke | 12 | Against dev, preprod and prod | 40 s | Yes |
| Load | 1 | Pre-production, nightly | 20 min | No: report |
The shape of the pyramid is not decoration: the base is wide because the tests at the bottom are cheap, fast and precise, and the top is narrow because the ones up there are slow, expensive and brittle. A team with 200 end-to-end tests and 20 unit tests has a suite that takes an hour, fails intermittently and does not tell you where the problem is.
Contract tests are the level almost nobody has and the one this change proves necessary: they cost 15 seconds and they are the only thing that detects that a third-party consumer is about to break.
DORA metrics: MercadoFresco before and after
The four DORA metrics measure a team's delivery capability. MercadoFresco's, measured over the three months before module 8 and the two after it:
| Metric | Before (May-July) | After (August-September) | Category |
|---|---|---|---|
| Deployment frequency | 1.2 per week | 4.8 per week | From medium to high |
| Lead time (commit → production) | 6 days and 4 h | 3 h 20 min | From medium to high |
| Change failure rate | 23 % | 7 % | From low to high |
| Time to restore | 47 min (median) | 4 min | From medium to elite |
Three readings worth taking from these numbers, because figures on their own are misleading:
Frequency goes up because the marginal cost of deploying goes down. Before, deploying cost Luis an hour of work and a dose of nerves, so he piled changes up: "since I am deploying, let all four go out". Now it costs nothing, so he deploys as soon as something is ready. And that feeds back into the failure rate: a deployment of one change is far easier to diagnose than one of four.
Time to restore is the biggest improvement and the most important one. Going from 47 minutes to 4 is not a process improvement, it is a change of nature: 47 minutes of degraded shop on a Friday at 19:00 is around 700 affected orders; 4 minutes is around 60. And most of those 4 minutes is detection, not reaction, because the rollback itself takes ninety seconds.
The 7 % failure rate is not zero and should not be. A team with a 0 % failure rate is almost always deploying too little and too late. The goal is not to never fail, it is for failing to be cheap: for a failure to cost 4 minutes and not 47.
And a warning. DORA metrics are useful as a thermometer for the team and dangerous as an individual target: as soon as "deployment frequency" becomes a goal, empty deployments appear to push the number up. Measure them, watch the trend, and do not hang them on anybody's personal performance board.
Rollback runbook and the failure detected late
The automatic mechanisms cover the first fifteen minutes. The hard case is the failure that shows up at 19:30 from a deployment at 11:00, when the blue environment no longer exists and the quality gate gave its approval hours ago.
This is the runbook, written to be followed at three in the morning:
1. Contain (0-5 min). Can it be switched off without deploying? If the change is behind a feature flag,
turning it off is a matter of seconds and is always the first option. If not, disable the pipeline
transition into production with disable-stage-transition so that nobody makes the situation worse while it
is being diagnosed.
2. Decide the direction (5-10 min). It is the decision most often got wrong under pressure:
| Situation | Direction | Why |
|---|---|---|
| Only the code changed | Back: deploy the previous version | Fast and safe |
| There was an expansion migration | Back: the column is redundant but harmless | Harmless by design |
| There was a contraction migration | Forward: fix and deploy | Going back would break more |
| The event moved up a version | Depends on whether consumers are updated | See below |
3. Execute (10-20 min). Backwards is a normal deployment of the previous version through the pipeline,
using the artefact that is still in S3 with its predictable path — tienda/1.5.2-8b2e440/paquete.zip —
which is what makes the rollback possible in a minute. It is the reason the bucket's lifecycle rule is 60
days and not 3.
4. Verify (20-30 min). /salud returns the expected version, TiempoConfirmacionPedido goes back to
its normal band, PedidosConfirmados picks up its rhythm again, and the DLQ stops growing.
5. Recover the data, the step that gets forgotten and the one that leaves the most damage. The messages
piled up in mercadofresco-pedidos-fallidos are processed with the DLQ runbook from 07-05 — contain,
classify, fix, reprocess — never with a redrive before fixing the cause. And if the event had moved up a
version, whatever was published during the incident is replayed from the EventBridge archive, always with
FilterArns so as not to repeat effects that already happened.
6. Blameless post-mortem, within 48 hours. What happened, why the gates did not catch it and what new test would have caught it. That last point is the only mandatory deliverable: a post-mortem that does not produce a new test is a meeting.
Small deployments and feature flags
Two practices that multiply the value of everything above.
Small, frequent deployments. This is not an aesthetic preference: it is the arithmetic of diagnosis. A deployment with one change has one suspect; one with six has fifteen possible pairs of interaction. MercadoFresco's rule is that a branch lives three days at most — from 08-01 — and that if a change does not fit, it gets split into backwards-compatible increments, exactly as this one has been split.
Feature flags with AWS AppConfig. They separate deploying from releasing: the code goes out to production switched off, and it gets switched on when and for whom it is decided.
# app/config.py
import boto3, json, time
client = boto3.client("appconfigdata")
_cache, _expires, _token = {}, 0, None
def flag(name, default=False):
"""Reads AppConfig with a 60 s cache. On error, returns the default value."""
global _cache, _expires, _token
try:
if time.time() > _expires:
if _token is None:
_token = client.start_configuration_session(
ApplicationIdentifier="mercadofresco",
EnvironmentIdentifier="produccion",
ConfigurationProfileIdentifier="banderas",
RequiredMinimumPollIntervalInSeconds=60)["InitialConfigurationToken"]
r = client.get_latest_configuration(ConfigurationToken=_token)
_token = r["NextPollConfigurationToken"]
content = r["Configuration"].read()
if content:
_cache = json.loads(content)
_expires = time.time() + 60
return _cache.get(name, {}).get("enabled", default)
except Exception:
return default # a broken flag must NEVER take the shop downWith this in place, the checkout flow does if flag("franja_entrega_activa"): and Luis deploys on Tuesday
with the flag off, switches it on for 10 % of customers on Wednesday, watches the metrics and reaches 100 %
on Thursday. Deploying stops being the moment of risk.
| Aspect | Canary deployment | Feature flag |
|---|---|---|
| What it controls | Which version of the code runs | Which behaviour is active |
| Granularity | % of requests | Per customer, region or segment |
| Time to switch off | 90 s (rollback) | Seconds, with no deployment |
| Reasonable duration | Minutes | Days or weeks |
| Cost if abused | None | Technical debt: dead branches in the code |
The last row is the warning. A flag is temporary code with an expiry date: it has to go on the backlog the same day it is created and be deleted as soon as it is at 100 % and stable. A system with forty old flags has a state space nobody can reason about or test.
Friday afternoon as a business decision
"No deploying on Fridays" is one of the most repeated rules in the industry, and almost always for the wrong reason. The usual reason is fear: we do not know what will happen and we do not want to be here to see it. That is not a policy, it is a symptom — that the pipeline does not inspire confidence, that the rollback has not been tested, that nobody knows how long going back takes.
With what MercadoFresco has now, the question can be answered with numbers:
| Factor | Monday 10:00 | Friday 19:00 |
|---|---|---|
| Orders/hour | ~180 | ~900 |
| Orders affected by 4 min of degradation | ~12 | ~60 |
| People available to respond | 3 | 1 |
| Time until the next window | 1 h | 60 h |
MercadoFresco's rule is not "no deploying on Fridays", it is "no deploying to production between 16:00 and 22:00 on Fridays". And the reason is not fear: it is that the same incident costs five times more in that slot and there is a third of the team around to handle it. It is a business decision, taken with data, open to review if the data changes. Outside that slot Friday is deployed to like any other day, Thursday afternoon and Friday morning included.
And it is implemented as a control, not as a reminder: a transition disabled on a schedule, so that it does not depend on somebody remembering at 18:50.
Module wrap-up and cost
The fourth problem of the course is solved. The module began with MercadoFresco's code sitting on a
laptop, an scp over SSH on a Friday afternoon and no way of going back other than hunting down the
previous file. It ends with a change that touches four systems travelling the complete path without anybody
having to type a single command.
| Course problem | Module | Status |
|---|---|---|
| 1. Single server that cannot take the peak | 2-3 | Solved: ASG, ALB, CloudFront |
| 2. Security and secrets | 4 | Solved: IAM, KMS, Secrets Manager, WAF |
| 3. Order confirmation of 2,893 ms | 6-7 | Solved: 400 ms with queues and events |
| 4. Risky deployments | 8 | Solved |
| 5. Infrastructure created by hand | 9 | Pending |
The complete chain, one sentence per link: 08-01 put the code in a repository with short branches, pull
requests and protection on main. 08-02 turned every push into a clean build that tests, scans and
produces an immutable artefact. 08-03 turned the deployment into a governed operation with hooks,
gradual strategies and automatic rollback on alarm. 08-04 chained it all together into a pipeline with
stages, informed approval and quality gates. And 08-05 has shown that the chain holds up under a real
change, schema and contract included.
| Item | Monthly cost |
|---|---|
| CodeCommit / GitHub / CodeConnections / AppConfig | 0.10 USD |
| CodeBuild (~1,850 min) | 20.90 USD |
| CodeDeploy (blue/green capacity) | 2.00 USD |
| CodePipeline V2 | 3.50 USD |
| Artefacts in S3 and logs | 1.30 USD |
| Module 8 total | ≈ 27.80 USD/month |
Twenty-eight dollars a month. The right comparison is not against zero: it is against a 47-minute median time to restore, 23 % of deployments failing and the hours Luis spent deploying by hand.
Common Mistakes and Tips
Deploying the producer before the consumer. It is the module 7 incident and the rule is a single sentence: the reader first, the writer second.
Putting the schema change in the same deployment as the code. The rollback rolls back artefacts, not databases.
An expansion migration with NOT NULL or with an index. It stops being harmless: it breaks the
previous version or locks the table.
Testing only backwards compatibility. During a gradual deployment both directions exist at the same time; both tests are needed.
Removing or renaming a field of an event. It is not compatible even if the consumer "looks" tolerant. Add, publish both, wait, remove.
Rebuilding the artefact before production. It invalidates all the earlier tests and the approval.
Different configuration set by hand on an instance. It disappears with the next blue/green and comes back as an undiagnosable intermittent failure.
Feature flags that never get retired. Forty old flags are a state space nobody can test. Put it on the backlog the day you create it.
A flag that takes the shop down if AppConfig fails. The default value on error must always be the safe one.
Using DORA metrics as an individual target. Empty deployments appear and the metric stops measuring anything.
Redriving the DLQ before fixing the cause. The messages fail again and information is lost.
Tip: write the PR's "Risks" section before writing the code. If you do not know what risks your change carries, you have not designed it yet. And rehearse the rollback deliberately once a quarter, timing it: it is the only way for the number in the runbook to be true.
Tip: in the post-mortem, the mandatory deliverable is a new test. Without that, it is a meeting.
Tip: turn "we do not deploy on Fridays" into a window backed by data. A rule justified with numbers gets respected; one justified with fear gets skipped the day there is a rush.
Exercises
Exercise 1: the change that does not fit in three days
Sara is now asking for something bigger: scheduled deliveries up to 7 days in advance, with an
availability calendar per postcode, different prices depending on the day and a limit on orders per slot and
zone. It touches the shop, the API, three new tables in Aurora, the warehouse consumer, the
PedidoConfirmado event (which needs fecha_entrega as well as franja_entrega) and a new price
calculation. Luis estimates three weeks.
Break it down into increments that respect the three-day-per-branch limit. For each one, say what gets deployed, what changes in the schema and in which phase, whether it needs a feature flag and what can be verified in production when it is done. Say explicitly which one comes first and why.
Exercise 2: the 19:30 failure
Tuesday, 11:00: 1.6.0 is deployed with everything green. At 19:30, Sara reports that on the
mercadofresco-negocio dashboard orders with the tarde slot have dropped to zero since 17:00, while
manana and 24h are normal. Total PedidosConfirmados is only 8 % below normal, no alarm has fired and
the quality gate gave its approval at 11:20. The blue environment no longer exists.
Answer: (a) why no automatic mechanism detected it; (b) the first 15 minutes, in order; (c) in which direction to roll back and why, knowing the migration was an expansion; (d) what to do with the orders from those two and a half hours; (e) which three new controls would come out of the post-mortem.
Exercise 3: measuring and improving DORA
MercadoFresco has had the pipeline for two months. Data from the last month: 19 deployments to production; a median lead time of 3 h 20 min, of which 2 h 40 min is waiting for Marta's approval; 2 deployments rolled back out of 19; a median time to restore of 4 min, but with one case of 95 minutes — the failure detected late. Marta wants to reach the elite category in all four metrics.
For each one: say where MercadoFresco is, what the elite target is, what concrete change you would propose and what risk that change carries. Say which one you would tackle first and justify it.
Solutions
Solution 1
Six increments, each deployable and verifiable separately. The principle that orders them is that each increment must be useful or harmless on its own, never "half a feature halfway there".
| # | What gets deployed | Schema | Flag | Verifiable when done |
|---|---|---|---|---|
| 1 | Calendar and availability tables, empty; nobody reads them | EXPAND: 3 new tables | No | The tables exist and affect nothing |
| 2 | Calendar loading and internal lookup API | None | No | /interno/disponibilidad?cp=08001 responds |
| 3 | fecha_entrega in the order and the event (v3), optional |
EXPAND: NULLABLE column |
Yes, off | Orders keep working without a date |
| 4 | Warehouse consumer reads fecha_entrega with a fallback |
None | No | Processes events with and without a date |
| 5 | Date picker in the checkout flow | None | Yes, at 10 % | 10 % of customers see the calendar |
| 6 | Price per day and limit per slot and zone | EXPAND: price column | Yes | Correct prices with the flag active |
The first is increment 1, and the reason is this lesson's rule: the schema change is the only thing that does not roll back with the code, so it goes on its own, in its own deployment, with nothing else that could fail and force a rollback. Three empty tables nobody queries are the safest change there is: if something goes wrong, there is nothing to undo. Putting them in the same deployment as the new code would be mixing irreversible risk with reversible risk, which is exactly what 08-03 warns against.
Two further observations. Increment 4 goes before 5 by the ordering rule: the consumer understands before the producer starts publishing. And increment 3 deploys the v3 event with the flag off, so that the field exists in the code but is not published until it is switched on: that gives a window to verify the consumers in production without anything really having changed.
Solution 2
(a) Because no alarm was measuring the dimension that failed. PedidosConfirmados is an aggregate,
and an 8 % drop in the total is within the normal noise of a Tuesday; the latency alarm does not fire
because the system is not slow, it is rejecting; the DLQ does not grow because there is probably no
exception, just a validation returning a clean 400. And the quality gate did its job correctly at 11:20,
six hours before the problem appeared: it was a time of day with little tarde traffic.
The underlying lesson is uncomfortable and holds for any system: automatic gates detect what they have
been told to look at, in the window in which they look. A failure that only affects one segment and only
shows up six hours later is invisible to them by construction. Sara detected it looking at a business
dashboard, and that is not a failure of the system: it is the reason mercadofresco-negocio exists.
(b) The first 15 minutes. Contain: if franja_entrega is behind an AppConfig flag, switch it off —
customers go back to the previous behaviour in seconds and the incident ends there. If not, disable the
transition into production so that nobody deploys on top of it. Confirm: check in CloudWatch that the
drop in tarde is real and not a dashboard problem, and look at the API logs filtering by requests with
franja_entrega=tarde — most likely a systematic 400 or 422. Narrow it down: do all tarde orders fail
or only some? From exactly 17:00? That it starts at 17:00 and not at 11:00 is the main clue: it suggests a
time-dependent condition, for example a validation that rejects the tarde slot once its cut-off time
has passed, with the time zone calculated wrongly. Notify: the team and Sara, with what is known and
what is not.
(c) Backwards, to 1.5.2, if the flag does not exist or does not fix it. And it can be done without fear
precisely because the migration was an expansion: the franja_entrega column is NULLABLE, 1.5.2 does
not know about it and ignores it, so rolling the code back leaves the system exactly as it was before
Tuesday. The orders from the last few hours keep their slot in the column, which will be recovered when the
corrected version is deployed. If the migration had been a contraction, the answer would be the opposite —
forwards, fixing — and with the shop degraded in the meantime: the expansion phase is what makes this
decision an easy one.
(d) The orders from those two and a half hours. They are not in the DLQ, because they were probably
never even created: they are customers who tried to choose tarde, saw an error and left. It is lost
sales, not lost data, and it has to be treated as such: estimate the volume by comparing with the same
slot on previous Tuesdays, and decide with Sara whether any commercial action is called for. If there were
also orders created with the wrong slot assigned, they are identified by time range and corrected with a
reviewed script, never with an improvised UPDATE in production. And if there are events that were not
published, they are recovered from the EventBridge archive with FilterArns.
(e) Three controls from the post-mortem. One alarm per dimension, not just per aggregate: a
PedidosConfirmados metric with a FranjaEntrega dimension and an alarm that fires if any slot drops to
zero for 15 minutes during business hours. It is cheap and it would have warned at 17:15 instead of 19:30.
A smoke test with a clock: cases that depend on the time of day have to be tested at different times, so
a synthetic execution every hour with the three slots, or unit tests with a simulated clock covering the
edges — and very much including the time zone, which is the suspect here. And extending the quality gate
with a second, deferred evaluation: an automatic check 4 and 12 hours after the deployment that compares
the metrics by dimension with the same period the week before and warns if anything has moved. It does not
block the pipeline — that finished long ago — but it turns "Sara spotted it by chance" into "the system
raised it".
Solution 3
Deployment frequency. MercadoFresco is at 19 a month, around 4.4 per week: category high. Elite is on demand, several times a day. The change: remove the manual approval — which is what imposes a daily rhythm on the whole thing — and batch fewer changes together. The risk: removing it before the quality gate is mature swaps a control for a hypothesis, and the exit conditions Marta wrote in 08-04 exist precisely for that.
Lead time. 3 h 20 min is high; elite is under an hour. And the diagnosis is served up on a plate: 2 h 40 min of the 3 h 20 is human waiting, so the technical pipeline already takes 40 minutes and is in the elite range. The change is not optimising the build: it is the approval. Two gradual measures before removing it altogether: approval from Slack with AWS Chatbot, which brings the wait down from hours to minutes because Marta does not need to be sitting at her computer, and a designated stand-in. The risk of the first is practically nil, and that is why it is the one to do right now.
Change failure rate. 2 out of 19 is 10.5 %: category medium-high; elite is below 15 %, so technically it is already there. But the number misleads if you do not look at the cases: one of the two failures was the 19:30 one, which no gate detected. The useful change is not lowering the percentage, it is improving detection with the three controls from exercise 2. The risk of chasing the number for its own sake is the usual one: small changes stop being deployed so as not to put the statistic at risk.
Time to restore. A median of 4 minutes, which is elite, but with one case of 95 minutes. Here the median lies: what hurts is the tail. The change: feature flags on every change of visible behaviour, which turn restoration into seconds with no deployment, and the per-dimension alarm that cuts the detection time, which is where 90 of those 95 minutes were.
First, approval from Slack. It is the one with the most impact — 2 h 40 min out of 3 h 20 — the cheapest — an afternoon's integration — the lowest risk — it removes no control, it just makes it more accessible — and the one that also improves frequency indirectly, because waiting for the approval is what pushes people to batch changes. Starting by removing the approval would be attacking the same metric with far more risk and without having met the conditions the team set for itself.
Conclusion
A change of Luis's has travelled the complete path. It started as a request from Sara about the 31 % of
delivery incidents and ended in production three hours later, touching the shop's code, a consumer of
cola-mercadofresco-pedidos, the Aurora schema and the PedidoConfirmado contract, without anybody
typing a command. The branch lasted two days, the PR was reviewed with the build green, the artefact was
built once and that same object travelled through the three environments, Marta approved in forty seconds
with the information in front of her, and the quality gate confirmed with real metrics that the shop was
still healthy.
What makes this work is not the services, it is the two disciplines this lesson has put into practice. The
first is compatibility: the migration in expansion phase with the NULLABLE column, with no NOT NULL,
no DEFAULT and no index, which turns an irreversible change into a harmless one; and the versioned event
with an optional attribute, with the two contract tests almost nobody writes — the old consumer with the
new event and the new consumer with the old event — because during a gradual deployment both situations
exist at the same time. The second is order: the reader first, the writer second, declared as runOrder
in the pipeline and not as something somebody has to remember. That is where the incident module 7 left
open gets closed.
You have the test pyramid with its blocking levels and the one almost nobody has — contract tests, fifteen seconds that are the only thing detecting that a third-party consumer is about to break; the per-environment configuration in Parameter Store and the secrets in Secrets Manager, with the rule that a value set by hand on an instance disappears with the next blue/green and comes back as an undiagnosable failure; and the rollback runbook with its hardest decision, the direction: back if only code changed or there was an expansion, forward if there was a contraction.
And you have the numbers. Deployment frequency from 1.2 to 4.8 per week. Lead time from 6 days to 3 hours. Change failure rate from 23 % to 7 %. Time to restore from 47 minutes to 4. With the three readings that go with them: that frequency goes up because the marginal cost of deploying goes down, that time to restore is the metric that really changes the nature of the risk, and that a 0 % failure rate would be a sign of deploying too little — the goal is not to avoid failing, it is for failing to cost four minutes and not forty-seven. Plus the feature flags with AppConfig, which separate deploying from releasing and carry their own debt if nobody retires them, and the conversion of "we do not deploy on Fridays" into a window justified with data: five times more orders at stake and a third of the team available. A business decision, not an article of faith.
The fourth problem of the course is solved, and for around 28 dollars a month. MercadoFresco has governed code, a build that verifies, a deployment that rolls itself back and a pipeline that chains it all together.
But there is an asymmetry that is hard to justify and that this lesson has laid bare twice. The
application code is governed; the infrastructure is not. The VPC was created with create-vpc in a
terminal. The queues with create-queue. The EventBridge rules by uploading a JSON. The alarms, the
security groups, the ALB target groups, the DynamoDB tables: everything exists because somebody typed a
command one day, and nowhere is there a record of why. Nobody can recreate MercadoFresco's environment from
scratch. Nobody can guarantee that pre-production has the same configuration as production — in fact it does
not, and the difference is discovered during every incident. And the very pipeline that exists so that
nobody deploys by hand was created by hand, with a JSON on Luis's laptop. The three environments also share
account 111122223333, so the blast radius is not bounded, quotas are shared and the bill is not really
split.
In module 9, "Infrastructure as code and account governance", we attack the fifth and last problem of the course. 09-01, "AWS CloudFormation", brings declarative templates, stacks, the change sets that say what is going to happen before it happens, and infrastructure rollback. 09-02, "AWS CDK", lets you define that same infrastructure with real code, with abstractions and tests. 09-03, "Elastic Beanstalk", shows the managed alternative and when it pays off. And 09-04, "AWS Organizations", solves the serious separation of environments with separate accounts, service control policies and consolidated billing. By the end of that module, everything we have built across eight modules will be recreatable from scratch with one command.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
