MercadoFresco already has the three pieces: a governed repository, a build that verifies and produces
artefacts, and a deployment that knows how to roll itself back. And yet, when Luis merges a PR he still has
to remember to launch the build, copy the path of the ZIP, write create-deployment with the right bucket
and key, wait, look at whether it went well, and repeat it for the next environment. Eight commands, four
waits and a sequence that only lives in his head.
AWS CodePipeline is the orchestration that is missing. It defines the path a change travels from commit to production as a series of stages that run in order, each with its actions, with artefacts flowing from one to the next and with the guarantee that a stage does not start if the previous one did not finish properly. What today is one person's memory becomes a versioned definition that runs the same way every time.
Cost warning. CodePipeline V1 costs 1 USD per active pipeline per month — a pipeline is active if it had any execution — with the first one free. V2 charges 0.002 USD per action minute, with no fixed fee, and 100 free minutes a month. For MercadoFresco, with around 20 daily executions, V2 works out at about 4 USD a month. On top of that come the CodeBuild minutes of 08-02 and the storage of the artefact bucket, which grows fast if nobody gives it a lifecycle rule. Fictitious data.
Contents
- Continuous delivery versus continuous deployment
- Structure: pipeline, stages, actions, executions and transitions
- The artefacts and the bucket that moves them
- MercadoFresco's pipeline
- V1 and V2 types, triggers and variables
- The source action with CodeConnections
- Build, deployment and parallel actions with
runOrder - Manual approval: what Marta must read in thirty seconds
- Environments: development, pre-production and production
- Smoke tests and quality gates with a Lambda action
- Retries, queued executions and superseded mode
- Permissions: pipeline role, per-action roles and cross-account
- Notifications to email and Slack
- The pipeline as code
- Cost and cleanup
- Common mistakes and tips
- Exercises
- Conclusion
Continuous delivery versus continuous deployment
The two terms get confused constantly and the difference is one single thing: whether or not there is a person between the validation and production.
| Aspect | Continuous delivery | Continuous deployment |
|---|---|---|
| What it automates | Everything up to the production gate | Everything, production included |
| Who decides it goes out | A person approves | Nobody: if it passes the tests, it goes out |
| Prerequisite | A reliable pipeline | A reliable pipeline and tests you trust |
| Time to production | Minutes + human wait | Minutes |
| Risk per deployment | The same | The same, but more deployments and smaller ones |
| When to choose it | At the start; sensitive changes | When automatic rollback has been tested |
There is a common trap: thinking that manual approval reduces risk. On its own it does not. It only reduces it if whoever approves has enough information to decide; if Marta presses "approve" without looking at anything, the approval is friction that delays the deployment without filtering anything. And there is a perverse side effect: approvals accumulate changes — "since I am approving, let all three go together" — and a deployment of three changes is harder to diagnose than three deployments of one.
MercadoFresco's decision: continuous delivery, with manual approval before production, and with a review date. Marta puts it explicitly as a transitional stage: the approval stays until three conditions are met — that the smoke tests cover the full purchase flow, that automatic rollback has been tested on purpose at least three times, and that two months go by without a deployment having to be rolled back by hand. Once all three are met, the approval disappears. Putting exit conditions on a manual control is what stops it staying forever "just in case".
Structure: pipeline, stages, actions, executions and transitions
| Concept | What it is | Rule to remember |
|---|---|---|
| Pipeline | The complete flow | One pipeline per deployable application |
| Stage | A named group of actions | It only starts if the previous one succeeded |
| Action | A unit of work | Six types: source, build, test, deploy, approval, invoke |
| Execution | One specific run | Identified, with its commit and its artefacts |
| Transition | The step between two stages | It can be disabled to hold changes back |
| Artefact | What flows between actions | It travels through S3, not in memory |
Two nuances that save you surprises. Within a stage, actions with the same runOrder run in parallel,
and those with a higher runOrder wait for the earlier ones; it is the mechanism for parallelising without
creating new stages. And a disabled transition is a little-known and very useful operational tool:
during the Friday peak, Marta disables the transition to production and the changes pile up validated in
pre-production, ready to go out on Monday with one click.
The artefacts and the bucket that moves them
Each action can declare input and output artefacts. CodePipeline stores them compressed in an S3 bucket and each action receives its own uncompressed. Three practical consequences. The bucket must have versioning enabled: without it the pipeline does not work, and it is a hard requirement. The artefacts pile up: with 20 daily executions and 45 MB per artefact that is 27 GB a year, so without a lifecycle rule the S3 bill grows on its own. And it is the point where immutability is guaranteed: the ZIP Marta approves is literally the same object that gets deployed in production, without being rebuilt between environments, so that "what was validated in pre-production" and "what went out" are the same file byte for byte. That guarantee is the reason this lesson exists.
aws s3api put-bucket-lifecycle-configuration --bucket mercadofresco-artefactos \
--lifecycle-configuration '{"Rules":[{
"ID":"caducar-artefactos-pipeline","Status":"Enabled",
"Filter":{"Prefix":"pipeline/"},
"Expiration":{"Days":60},
"NoncurrentVersionExpiration":{"NoncurrentDays":15},
"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":7}}]}' \
--profile mercadofresco-dev --region eu-west-1MercadoFresco's pipeline
flowchart TB
A[Source<br/>GitHub main] --> B[Build<br/>build-mercadofresco-tienda]
B --> C{Quality stage}
C --> C1[Static analysis]
C --> C2[Contract tests]
C1 --> D[Schema migration<br/>EXPAND phase]
C2 --> D
D --> E[Deploy to development<br/>dg-...-desarrollo]
E --> F[Smoke tests<br/>build-mercadofresco-humo]
F --> G[Deploy to pre-production]
G --> H[Pre-prod smoke tests]
H --> I[MANUAL APPROVAL<br/>alertas-mercadofresco]
I --> J[Deploy to production<br/>blue/green + canary]
J --> K[Quality gate Lambda<br/>MercadoFresco/Tienda metrics]
K -->|Metrics OK| L[Successful execution]
K -->|Degraded| M[Stop and roll back]
Eight stages, and each one answers a specific question: what has changed? does it compile and pass the tests? does it meet the quality bar? is the schema ready? does it work in a real environment? does the purchase flow pass? does somebody authorise it? is it still healthy afterwards?
V1 and V2 types, triggers and variables
| Aspect | V1 | V2 |
|---|---|---|
| Price | 1 USD/month per active pipeline | 0.002 USD per action minute |
| Triggers with filters | No | Yes: branch, tag, file path |
| Pipeline variables | No | Yes, with values on each execution |
| Parallel stages | Limited | Yes |
| When it pays off | Pipelines with enormous numbers of executions | Almost always |
Choose V2 unless you have a clear reason not to. The triggers with filters justify the change on their
own: without them, any commit to main launches the entire pipeline, including a correction to the
README. With them:
{
"triggers": [{
"providerType": "CodeStarSourceConnection",
"gitConfiguration": {
"sourceActionName": "Origen",
"push": [{
"branches": { "includes": ["main"] },
"filePaths": {
"includes": ["app/**", "scripts/**", "requirements.txt", "appspec.yml"],
"excludes": ["**/*.md", "docs/**", ".github/**"]
}
}],
"pullRequest": [{
"events": ["OPEN", "UPDATED"],
"branches": { "includes": ["desarrollo", "main"] }
}]
}
}]
}This trigger does two different things. On push to main, it runs the complete pipeline, but only if
real code changed: a change under docs/ spends no minutes and bothers nobody. And on pull request, it
runs — with the deployment stages omitted — the build and the tests, which is what closes the protection
of main that we left open in 08-01: the PR cannot be merged unless the check is
green.
Pipeline variables let you parameterise an execution without touching the definition. They are declared in
a variables block with name, default value and description, referenced as #{variables.nivelLog}, and they
can be set when launching an execution by hand. A warning: if you are ever tempted to declare something like
omitirHumo, remember that a variable that lets you skip a gate is a gate that will be skipped. If you
add it, make its use visible in CloudTrail and make the runbook demand a written justification.
The source action with CodeConnections
{
"name": "Origen",
"actionTypeId": {
"category": "Source", "owner": "AWS",
"provider": "CodeStarSourceConnection", "version": "1"
},
"configuration": {
"ConnectionArn": "arn:aws:codeconnections:eu-west-1:111122223333:connection/a1b2c3d4-EXAMPLE",
"FullRepositoryId": "mercadofresco/mercadofresco-tienda",
"BranchName": "main",
"DetectChanges": "false",
"OutputArtifactFormat": "CODEBUILD_CLONE_REF"
},
"outputArtifacts": [{ "name": "CodigoFuente" }],
"runOrder": 1
}Three fields deserve explanation. DetectChanges: false switches off the implicit webhook because in V2
the triggers are governed by the triggers block; leaving it at true with triggers defined produces
duplicate executions, which is a classic source of bewilderment. OutputArtifactFormat: CODEBUILD_CLONE_REF hands CodeBuild a reference to the repository instead of a ZIP, so that the build has
the Git history available — needed for git describe or git-secrets --scan; the price is that the
CodeBuild role needs codeconnections:UseConnection. And the ConnectionArn must point at a connection in
AVAILABLE state: if it is still PENDING, as we warned in 08-01, the action fails with a permissions error
that does not mention the connection.
Build, deployment and parallel actions with runOrder
The build stage consumes CodigoFuente and produces the deployable artefact:
{
"name": "Construccion",
"actions": [{
"name": "ConstruirYProbar",
"actionTypeId": { "category": "Build", "owner": "AWS",
"provider": "CodeBuild", "version": "1" },
"configuration": { "ProjectName": "build-mercadofresco-tienda" },
"inputArtifacts": [{ "name": "CodigoFuente" }],
"outputArtifacts": [{ "name": "PaqueteTienda" }],
"namespace": "construccion",
"runOrder": 1
}]
}The namespace is the piece that connects this lesson with 08-02: it exposes the variables the
buildspec declared in exported-variables, so that later stages can use #{construccion.VERSION_APP}
without recalculating anything. That is how the approval message will know which version Marta is approving.
The quality stage shows the parallelism:
{
"name": "Calidad",
"actions": [
{ "name": "AnalisisEstatico", "runOrder": 1,
"actionTypeId": { "category": "Test", "owner": "AWS",
"provider": "CodeBuild", "version": "1" },
"configuration": { "ProjectName": "build-mercadofresco-analisis" },
"inputArtifacts": [{ "name": "CodigoFuente" }] },
{ "name": "PruebasContrato", "runOrder": 1,
"actionTypeId": { "category": "Test", "owner": "AWS",
"provider": "CodeBuild", "version": "1" },
"configuration": { "ProjectName": "build-mercadofresco-contrato" },
"inputArtifacts": [{ "name": "CodigoFuente" }] }
]
}Both have runOrder: 1 and run at the same time; if either fails, the stage fails and the pipeline
stops. A third one with runOrder: 2 would wait for both. And the deployment consumes the artefact:
{
"name": "DesplegarProduccion",
"actions": [{
"name": "CodeDeployProduccion",
"actionTypeId": { "category": "Deploy", "owner": "AWS",
"provider": "CodeDeploy", "version": "1" },
"configuration": {
"ApplicationName": "app-mercadofresco-tienda",
"DeploymentGroupName": "dg-mercadofresco-tienda-produccion"
},
"inputArtifacts": [{ "name": "PaqueteTienda" }],
"runOrder": 1
}]
}Notice that the inputArtifacts is the same PaqueteTienda that was deployed in development and in
pre-production. It is not rebuilt. It is the immutability guarantee that makes the earlier tests mean
something.
Manual approval: what Marta must read in thirty seconds
A badly designed approval is a button that gets pressed without looking. A well designed one gives exactly the information needed to decide.
{
"name": "AprobacionProduccion",
"actions": [{
"name": "AprobarMarta",
"actionTypeId": { "category": "Approval", "owner": "AWS",
"provider": "Manual", "version": "1" },
"configuration": {
"NotificationArn": "arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco",
"CustomData": "Version #{construccion.VERSION_APP} | Commit #{construccion.COMMIT_CORTO} | Author #{construccion.AUTOR} | Preprod smoke: OK | Migration: EXPAND applied | Window: do NOT deploy Friday 16-22h",
"ExternalEntityLink": "https://github.com/mercadofresco/mercadofresco-tienda/compare/#{construccion.VERSION_ANTERIOR}...#{construccion.VERSION_APP}"
},
"timeoutInMinutes": 1440,
"runOrder": 1
}]
}What makes this approval useful are the two text fields. CustomData carries the five things Marta
needs: which version, which commit, who did it, whether the smoke tests passed in pre-production and whether
there is a schema migration in play, plus the reminder of the forbidden window. And
ExternalEntityLink is a link to the diff between the deployed version and the candidate: in one
click she sees exactly what changes, which is the question that really matters.
The timeoutInMinutes: 1440 is a decision, not a detail: the approval expires after 24 hours and the
execution fails. That is correct — a change that has been waiting three days is no longer the change that was
validated, because main has moved on — and it stops a queue of zombie approvals building up.
Three rules to make this work: whoever approves is not whoever wrote the code — or the approval filters
nothing; the codepipeline:PutApprovalResult permission is granted only to whoever should approve; and if
the approval always gets pressed without looking, it has to go, because a control that filters nothing only
adds delay and a false sense of security.
Environments: development, pre-production and production
The three deployment stages of the pipeline use different deployment groups:
| Environment | Deployment group | Strategy | Who triggers it | Data |
|---|---|---|---|---|
| Development | dg-mercadofresco-tienda-desarrollo |
In place, AllAtOnce |
Automatic | Synthetic |
| Pre-production | dg-mercadofresco-tienda-preproduccion |
Blue/green | Automatic | Anonymised |
| Production | dg-mercadofresco-tienda-produccion |
Blue/green + canary | After approval | Real |
Here we have to be honest about what MercadoFresco has today: the three environments live in the same
account 111122223333, separated by tags, subnets and IAM policies. It works, but it has three limits
worth naming without decoration. The blast radius is not bounded: a mistake in a policy or a script
with the wrong profile can touch production from a development task. Service limits are shared: the
load tests in pre-production consume the same Lambda concurrent invocation quota as the real shop. And
the bill is not really separated: tags help — we will see that in 11-02 — but they are not a
boundary.
Serious separation is one AWS account per environment, with the pipeline in a tooling account that assumes roles in the others. That is AWS Organizations and it is 09-04; here it is enough to know that separation by tags is a starting point and not the destination, and that the pipeline is already ready for the change because each stage points to an independent deployment group.
Smoke tests and quality gates with a Lambda action
Smoke tests verify that what has been deployed really works, against the freshly updated environment and through the front door:
# tests/humo/test_flujo_compra.py -> run by build-mercadofresco-humo
import os, requests, pytest
BASE = os.environ["URL_ENTORNO"] # e.g. https://preprod.mercadofresco.example
def test_health_responds_and_reports_the_version():
r = requests.get(f"{BASE}/salud", timeout=5)
assert r.status_code == 200
assert r.json()["version"] == os.environ["VERSION_ESPERADA"]
def test_full_purchase_flow():
s = requests.Session()
assert s.get(f"{BASE}/productos?categoria=fruta", timeout=5).status_code == 200
s.post(f"{BASE}/carrito", json={"sku": "FRUT-0012", "uds": 2}, timeout=5)
r = s.post(f"{BASE}/pedidos", timeout=10, json={
"franja_entrega": "tarde", "metodo_pago": "tarjeta_prueba",
"clave_idempotencia": f"humo-{os.environ['ID_EJECUCION']}"})
assert r.status_code == 201
assert r.json()["estado"] == "confirmado"
assert r.elapsed.total_seconds() < 2.0 # the 07-05 targetThe third assertion is the one that is usually missing and the one that prevents the most incidents: not only that the order is confirmed, but that it is confirmed within the latency budget. An order that takes 6 seconds is broken even if it returns a 201.
The quality gate goes one step further: after deploying to production, an invoke action queries the real metrics and decides whether the pipeline carries on.
# mercadofresco-puerta-calidad
import boto3
from datetime import datetime, timedelta, timezone
cp = boto3.client("codepipeline")
cw = boto3.client("cloudwatch")
THRESHOLDS = {
"TiempoConfirmacionPedido": {"stat": "p95", "max": 1500},
"PedidosConfirmados": {"stat": "Sum", "min": 5},
}
def _metric(name, stat, minutes=10):
"""Returns the aggregated value, or None if there is no data."""
end = datetime.now(timezone.utc)
is_percentile = stat.startswith("p")
r = cw.get_metric_statistics(
Namespace="MercadoFresco/Tienda", MetricName=name,
StartTime=end - timedelta(minutes=minutes), EndTime=end, Period=60,
ExtendedStatistics=[stat] if is_percentile else None,
Statistics=None if is_percentile else [stat])
points = r["Datapoints"]
if not points:
return None
if is_percentile:
return max(p["ExtendedStatistics"][stat] for p in points)
return sum(p[stat] for p in points)
def handler(event, context):
job_id = event["CodePipeline.job"]["id"]
failures = []
try:
for name, rule in THRESHOLDS.items():
value = _metric(name, rule["stat"])
if value is None:
# No data is NOT approval: it is not knowing. And not knowing, in production, is failing.
failures.append(f"{name}: no data in 10 min")
elif "max" in rule and value > rule["max"]:
failures.append(f"{name} {rule['stat']}={value:.0f} > {rule['max']}")
elif "min" in rule and value < rule["min"]:
failures.append(f"{name}={value:.0f} < {rule['min']}")
if failures:
cp.put_job_failure_result(jobId=job_id,
failureDetails={"type": "JobFailed", "message": "; ".join(failures)[:265]})
else:
cp.put_job_success_result(jobId=job_id)
except Exception as e:
# Faced with an unexpected error, fail. Never approve by default.
cp.put_job_failure_result(jobId=job_id,
failureDetails={"type": "JobFailed", "message": str(e)[:265]})Three design decisions in this code, and all three are deliberate. No data means failure, because
PedidosConfirmados at zero for ten minutes during business hours does not mean "everything is fine", it
means nobody is buying — which is exactly what you want to detect. An exception means failure, because a
gate that opens when it breaks is not a gate. And calling put_job_success_result or
put_job_failure_result is mandatory: if the Lambda finishes without doing so, the action stays in
InProgress until the one-hour timeout, just like the CodeDeploy hook in 08-03.
Retries, queued executions and superseded mode
# Retry only what failed, without building again
aws codepipeline retry-stage-execution \
--pipeline-name pipeline-mercadofresco-tienda \
--stage-name DesplegarPreproduccion \
--pipeline-execution-id 7a1f2c33-EXAMPLE \
--retry-mode FAILED_ACTIONS \
--profile mercadofresco-dev --region eu-west-1FAILED_ACTIONS retries only the failed actions and ALL_ACTIONS the whole stage: the first when the
failure was transient, the second if the stage has interdependent actions. The three execution modes are
a decision you take once and that affects every single day:
| Mode | Behaviour | When |
|---|---|---|
SUPERSEDED (default) |
The new change replaces the waiting one | Fast continuous delivery |
QUEUED |
They queue up and run in order | When every change must be deployed |
PARALLEL |
Independent simultaneous executions | Pipelines per feature branch |
SUPERSEDED is the right one for MercadoFresco and it is worth understanding why, because it sounds
like changes get lost and they do not. If Luis pushes three commits in ten minutes, there is no point in
deploying three times: the third contains the two before it, so deploying only the last is both faster
and equivalent. What gets replaced is the waiting execution, not the code. The exception is when each
execution has an effect of its own that is not cumulative — for example a pipeline that publishes tagged
releases; there QUEUED is the right choice.
Permissions: pipeline role, per-action roles and cross-account
The pipeline role is the one CodePipeline assumes in order to orchestrate, and it deserves the same discipline as 04-01: it does not do the work, it only launches it.
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "Artefactos", "Effect": "Allow",
"Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", "s3:GetBucketVersioning"],
"Resource": ["arn:aws:s3:::mercadofresco-artefactos",
"arn:aws:s3:::mercadofresco-artefactos/*"] },
{ "Sid": "LanzarConstrucciones", "Effect": "Allow",
"Action": ["codebuild:StartBuild", "codebuild:BatchGetBuilds"],
"Resource": "arn:aws:codebuild:eu-west-1:111122223333:project/build-mercadofresco-*" },
{ "Sid": "LanzarDespliegues", "Effect": "Allow",
"Action": ["codedeploy:CreateDeployment", "codedeploy:GetDeployment",
"codedeploy:GetDeploymentConfig", "codedeploy:RegisterApplicationRevision"],
"Resource": "arn:aws:codedeploy:eu-west-1:111122223333:*:app-mercadofresco-*" },
{ "Sid": "PuertaDeCalidad", "Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-puerta-calidad" },
{ "Sid": "Origen", "Effect": "Allow",
"Action": "codeconnections:UseConnection",
"Resource": "arn:aws:codeconnections:eu-west-1:111122223333:connection/a1b2c3d4-EXAMPLE" },
{ "Sid": "CifradoDeArtefactos", "Effect": "Allow",
"Action": ["kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "arn:aws:kms:eu-west-1:111122223333:key/*",
"Condition": {"StringEquals": {"kms:ViaService": "s3.eu-west-1.amazonaws.com"}} }
]
}Notice that the pipeline role has no permissions over EC2, Aurora or SQS: it can only launch CodeBuild
and CodeDeploy, which in turn act with their own roles. That separation into three roles — the
pipeline's, the build's and the instance's — is what stops compromising one giving access to everything. And
kms:GenerateDataKey is essential: without it, the pipeline can read artefacts but cannot write them to an
encrypted bucket, with an access denied error that does not mention KMS.
Cross-account access, which will be the model of 09-04, works with an assumable role: the production
account publishes a role that trusts the tooling account, and the deployment action carries a roleArn. The
essential condition: the KMS key encrypting the artefact bucket must be a customer key shared with the
target accounts, because the AWS managed key cannot be shared and the deployment would fail as it could not
decrypt the artefact.
Notifications to email and Slack
Two mechanisms, as in 08-01, and it is worth choosing well:
aws codestar-notifications create-notification-rule \
--name notif-pipeline-mercadofresco \
--resource arn:aws:codepipeline:eu-west-1:111122223333:pipeline-mercadofresco-tienda \
--detail-type FULL \
--event-type-ids codepipeline-pipeline-pipeline-execution-failed \
codepipeline-pipeline-manual-approval-needed \
codepipeline-pipeline-stage-execution-failed \
--targets TargetType=SNS,TargetAddress=arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Notice what is NOT on the list: codepipeline-pipeline-pipeline-execution-succeeded. With 20 daily
executions, notifying the successes means 400 messages a month that teach the team to ignore the channel, and
then the warning that matters — a Friday failure — goes unnoticed. Notify only what is actionable:
failures and pending approvals. It is the same discipline as 05-01 with alarms.
For Slack, AWS Chatbot connects the SNS topic to a channel and adds the important part: buttons to
approve from the chat itself, which brings Marta's approval time down from hours to minutes. And for your
own logic, EventBridge receives every event of the pipeline with source: ["aws.codepipeline"], as we saw
in 07-03.
The pipeline as code
Everything in this lesson has been created with aws codepipeline create-pipeline and a JSON file on Luis's
laptop. In other words: we have automated the application deployment with a tool that is itself configured
by hand. If somebody deletes the pipeline, there is no reliable way to recreate it; if Marta changes a
quality gate threshold, there is no record of who did it or why.
The provisional way out is to export the definition and version it in mercadofresco-infra:
aws codepipeline get-pipeline --name pipeline-mercadofresco-tienda \
--query 'pipeline' --output json > pipeline-mercadofresco-tienda.json
aws codepipeline update-pipeline --cli-input-json file://pipeline-mercadofresco-tienda.json \
--profile mercadofresco-dev --region eu-west-1It works, but it is a patch: the exported JSON brings metadata you have to clean up, it does not parameterise by environment and it does not manage dependencies between resources. The real solution is to define the pipeline — and all the infrastructure — as code, and that is module 9: CloudFormation in 09-01 and CDK in 09-02.
Cost and cleanup
| Item | Calculation | Cost |
|---|---|---|
| CodePipeline V2, ~20 executions/day | ~1,700 action min/month × 0.002 | 3.40 USD |
| Quality gate Lambda | 600 invocations of 3 s | ~0.01 USD |
| Artefacts in S3, expiring after 60 days | ~3 GB | 0.07 USD |
| CodePipeline | ≈ 3.50 USD/month | |
| Complete module 8 (with CodeBuild and CodeDeploy) | ≈ 28 USD/month |
Twenty-eight dollars a month for eliminating the manual deployments of a three-person team. The honest comparison is not against zero: it is against the cost of a thirty-minute outage on a Friday at 19:00, with 900 orders/hour, plus the hours Luis spent deploying by hand.
# Holding changes back without deleting anything: the disabled transition
aws codepipeline disable-stage-transition --pipeline-name pipeline-mercadofresco-tienda \
--stage-name DesplegarProduccion --transition-type Inbound \
--reason "Friday peak: no deployments until Monday" \
--profile mercadofresco-dev --region eu-west-1
# Delete a test pipeline (it does not delete the bucket or its artefacts)
aws codepipeline delete-pipeline --name pipeline-pruebas-borrar \
--profile mercadofresco-dev --region eu-west-1Deleting the pipeline does not delete the artefacts, which go on taking up space and billing in S3. It is the most common leftover of this module: check the bucket and its lifecycle rule.
Common Mistakes and Tips
Leaving DetectChanges: true with V2 triggers defined. Every push launches two executions, they tread
on each other and nobody understands why.
An artefact bucket without versioning. The pipeline does not work and the message is not obvious. It is a hard requirement.
No lifecycle rule on the bucket. Twenty daily executions of 45 MB are 27 GB a year of ZIP files nobody is ever going to deploy.
Rebuilding the artefact at each stage. It breaks the immutability guarantee: what Marta approves stops being what goes out to production. Build once, deploy the same object.
A manual approval with no useful CustomData. It turns into a button that gets pressed without looking,
and then it is only delay.
Letting the same person who wrote the change approve it. The approval stops filtering anything.
A quality gate that approves when there is no data or when it throws an exception. A gate that opens when it breaks is not a gate.
Forgetting put_job_success_result / put_job_failure_result in the invoke Lambda. The action stays in
InProgress until the one-hour timeout.
Notifying the successes. Four hundred messages a month teach the team to ignore the channel, and the warning that matters gets lost.
A pipeline role with PowerUserAccess. The pipeline should only launch actions; the work is done by the
CodeBuild and CodeDeploy roles. Three separate roles, three bounded blast radii.
Forgetting kms:GenerateDataKey in the pipeline role, or using the AWS managed key in a cross-account
pipeline: the deployment fails because it cannot decrypt the artefact.
Tip: use V2 and filter by file path. A change to the README should not spend minutes or bother
anybody.
Tip: connect the pipeline to the PRs, not only to main. It is what closes the branch protection we
left open in 08-01.
Tip: put exit conditions on the manual approval. A temporary control with no criterion for removing it stays forever.
Tip: learn to use the disabled transition. It is the clean way of saying "no deploying to production today" without dismantling anything.
Exercises
Exercise 1: the pipeline that deployed the wrong thing
Friday, 18:50. Marta approves version 1.7.0 after seeing that the smoke tests passed in pre-production. The
pipeline deploys to production and four minutes later mercadofresco-alb-latencia-alta fires. CodeDeploy
rolls back. Investigating, they discover that the production stage rebuilt the artefact with a CodeBuild
action of its own instead of reusing PaqueteTienda, and that between the pre-production build (18:20) and
the production one (18:52) somebody had merged another PR into main.
Answer: (a) what exactly was deployed to production; (b) why the pre-production smoke tests were worth nothing in this design; (c) how the pipeline is fixed; (d) which other two safeguards from the module would have limited the damage; (e) what process measure you would propose for the timing.
Exercise 2: designing the quality gate
MercadoFresco wants to move from continuous delivery to continuous deployment: remove Marta's approval
and let the quality gate decide on its own. Data: during business hours between 120 and 900 orders an hour
are confirmed; from 02:00 to 07:00 there are between 0 and 5; the normal TiempoConfirmacionPedido p95 is
400 ms and the target is not to go over 1,500 ms; the canary deployment of the charging Lambda lasts 5
minutes and the blue/green of the shop takes about 12 to complete.
Design the gate: which metrics, which thresholds, which observation window, what it does when there is no data and at which point of the pipeline it sits. Justify every decision and say which three conditions should be met before removing the manual approval.
Exercise 3: the pipeline of the three emergencies
On Monday three things happen. (a) At 09:15, Luis pushes five commits in a row to main fixing a style
error; the pipeline starts five times and the fourth one is left waiting. (b) At 11:40, the
DesplegarPreproduccion stage fails because the CodeDeploy agent on one instance did not respond; the rest
of the pipeline had gone well and the build took 9 minutes. (c) At 17:20 an urgent security fix has to be
deployed, but Marta is on a plane and cannot approve until 21:00.
For each situation state what happens with the configuration described in the lesson, what specific command or action you would use, and what you would change so that it stops being a problem.
Solutions
Solution 1
(a) What was deployed was 1.7.0 plus the change from the PR merged at 18:45, that is, code that never went through pre-production or the approval. Marta approved one thing and another went out. And the serious part is that nothing in the interface said so: the execution was still called 1.7.0.
(b) Because they validated a different artefact from the one that was deployed. A test only says something
about the specific binary it tested; if you rebuild afterwards, you have thrown that information away.
Here the rebuild took main in the state it was in at that moment, not the commit of the execution, so it
dragged in a new change. It is the difference between "we tested this" and "we tested something like this".
(c) By removing the build action from the production stage and making the deployment action consume the
PaqueteTienda produced in the build stage, which is the same S3 object that was already deployed in
development and pre-production:
Build once, deploy many times is the rule, and it admits no exceptions "because production needs a
different configuration": per-environment configuration is resolved at run time with Parameter Store, not by
rebuilding the package. If you needed to check it, it is enough to verify that the commit in the artefact
metadata matches the one of the pipeline execution.
(d) The quality gate would have detected the degradation from the metrics even if the CodeDeploy rollback had not fired, stopping the pipeline and leaving a record. And the transition to production disabled during the Friday peak would have prevented the 18:50 deployment, which is when getting it wrong costs the most. It is worth noting that the automatic rollback worked: four minutes of degradation instead of thirty, and without anybody having to do anything. The failure was upstream, in the design of the pipeline, not in the safety mechanism.
(e) An explicit deployment window: nothing to production on Fridays from 16:00 to 22:00, nor on public holidays, nor after 18:00 on any day. It is not distrust of the pipeline, it is arithmetic: the cost of an incident at the peak with 900 orders/hour is several times that of a Monday morning, and at 19:00 on a Friday there are fewer people available to respond. Implement it with the disabled transition on a schedule, not by trusting that somebody remembers.
Solution 2
Metrics and thresholds. Three complementary signals, because a single one is easy to fool:
| Metric | Threshold | Why |
|---|---|---|
TiempoConfirmacionPedido p95 |
> 1,500 ms → fail | Business target from 07-05; the normal one is 400 ms |
PedidosConfirmados (Sum) |
Drop > 40 % against the same slot over the previous 7 days | Detects that nobody is buying, the silent failure |
| ALB 5xx errors (ratio) | > 1 % of requests | Detects the obvious, fast failure |
The relative threshold on PedidosConfirmados is the key decision of the exercise. An absolute
threshold cannot work when the normal volume swings between 0 and 900 depending on the hour: if you set "at
least 5 orders in 10 minutes", the gate will fail every legitimate night-time deployment; if you set 0, it
will never detect anything. Comparing with the same time slot over the previous seven days adapts itself to
the real pattern, the Friday peak included.
Observation window: 15 minutes, and the reason is arithmetic. The blue/green takes about 12 minutes to complete, so a shorter window would measure partly the blue environment and partly the green, mixing the two versions and diluting any degradation. Fifteen minutes guarantee at least three of traffic entirely on the new version.
When there is no data it depends on the time of day, and this is what makes the gate usable at night.
During business hours (07:00-02:00), no data for PedidosConfirmados means fail: it means nobody is
buying. From 02:00 to 07:00 that is normal, so in that slot PedidosConfirmados is ignored and the gate
leans on TiempoConfirmacionPedido measured with synthetic traffic — a smoke test every minute from
CloudWatch Synthetics — without which there would be nothing to measure in the small hours. The general rule
still holds: no data is not approval; it is not knowing, and not knowing is only acceptable if you have
decided in advance that in that slot there is nothing to know.
Where it sits: two gates, not one. One immediately after the canary deployment of the charging
Lambda, with a 5-minute window matched to the duration of the canary, to cut in before the canary is
promoted to 100 %. And another after the blue/green of the shop, with the 15-minute window, inside the
terminationWaitTimeInMinutes of 30 so that the rollback is still 90 seconds. A gate that evaluates once the
blue environment has already been terminated arrives too late.
The three conditions for removing the manual approval, which are the ones Marta already wrote down: that the smoke tests cover the full purchase flow — including the charge with a test card — that automatic rollback has been tested on purpose at least three times with measured times, and that two months go by without any deployment having had to be rolled back by hand. To those it is worth adding a fourth that the exercise suggests: that the quality gate has run in observation mode for a month — recording what it would have done without actually blocking — and has produced no false positives. Removing the human approval and debuting the automatic gate on the same day is swapping a control for a hypothesis.
Solution 3
(a) The five commits. With the default SUPERSEDED mode, five complete pipelines do not run: the first
execution carries on and the following ones replace each other, so that two are left — the one that was
running and the last one. Nothing is lost, because the fifth commit contains the four before it. So
technically the behaviour is correct. What is a waste is having started at all: they were style fixes. The
fix is the V2 file path filter, with excludes for **/*.md and docs/**, and if the change affects
app/ but is trivial, the right flow is to group it into a single commit before merging. Nothing to do at
the time except let it finish.
(b) The agent failure. It is a transient infrastructure failure, not a code one, so there is no need to rebuild: rebuilding would throw away nine minutes and, worse, would produce a new artefact with the risk from exercise 1.
aws codepipeline retry-stage-execution \
--pipeline-name pipeline-mercadofresco-tienda \
--stage-name DesplegarPreproduccion \
--pipeline-execution-id <id> --retry-mode FAILED_ACTIONS \
--profile mercadofresco-dev --region eu-west-1FAILED_ACTIONS retries only the deployment action, reusing the same PaqueteTienda. So that it stops
being a problem: the agent installed as a Systems Manager association with automatic updates, as we saw in
08-03, and an alarm on the state of the agent on the instances. And if it is recurring, check whether the
instance is missing network egress or whether the ASG created it from an AMI with no agent.
(c) The urgent fix without Marta. The design already has the answer and the important thing is not to
improvise. The approval accepts several approvers: anybody with codepipeline:PutApprovalResult can
authorise, so there must be a designated deputy agreed in advance — not Luis, because he is the one who
wrote the change and the rule that the author does not approve is precisely what protects here; the deputy
would be a second technical lead, or Sara if the organisation accepts that for documented cases. If there
really is nobody, the way out is AWS Chatbot in Slack, which lets Marta approve from her phone as soon as
she has a signal, including the plane's.
What you must not do is skip the pipeline and deploy by hand with create-deployment: you lose the
smoke tests, the quality gate and the traceability, precisely on a security change, which is the kind you
most want to have on record. And nor should you use a variable like omitirHumo, for the reason given in the
lesson: a gate that can be skipped will be skipped.
As a permanent measure, two things. A written approvers policy with a principal and a deputy, reviewed every quarter. And considering a documented fast path for security fixes: a pipeline with the same automatic gates but with the human approval replaced by an immediate notification to the whole team and a mandatory post-mortem within 24 hours. That a change is urgent does not justify skipping the automatic controls; at most it justifies replacing the human control with accountability after the fact.
Conclusion
The sequence that lived in Luis's head is now pipeline-mercadofresco-tienda, a versioned definition that
runs the same way every time. A commit on main touching app/ starts the flow, builds, tests, checks the
quality, applies the expansion phase of the schema, deploys to development, passes the smoke tests, deploys
to pre-production, passes them again, notifies Marta with the information to decide in thirty seconds,
deploys to production with blue/green and canary, and checks with the real MercadoFresco/Tienda metrics
that the shop is still healthy. Nobody writes a command.
You can tell continuous delivery from continuous deployment — the difference is a person between the
validation and production — and why MercadoFresco chooses the first with written exit conditions,
because a temporary control with no criterion for removing it stays forever. You know the structure of the
pipeline and the two nuances that give the most leverage: that actions with the same runOrder run in
parallel and that a disabled transition is the clean way of saying "no deploying today". And above all
you have the guarantee that gives everything else meaning: the artefact is built once and the same S3
object travels through the three environments, so that what Marta approves and what goes out to production
are the same file byte for byte — breaking that, as in exercise 1, invalidates every earlier test.
You know why V2 is the default choice: the triggers filtered by branch, tag and file path stop a change
to the README spending minutes, and the executions on pull requests are what closes the protection of
main that was left open in 08-01. You know the source action with CodeConnections and its two traps —
DetectChanges: true alongside triggers produces duplicate executions, and a connection in PENDING fails
with an error that does not mention it — the namespace that exposes the variables exported by the
buildspec of 08-02, and the manual approval that genuinely helps: CustomData with version, commit,
author, smoke state and pending migration, plus an ExternalEntityLink to the diff.
You have the quality gates with their three deliberate decisions — no data means failure, an exception
means failure, and you must always call put_job_success_result or put_job_failure_result — the three
execution modes with SUPERSEDED as the right one because the new commit contains the earlier ones, and
the retry with FAILED_ACTIONS that does not rebuild. You know how to separate permissions into three
roles — the pipeline's, which only launches; the build's; and the instance's — and that
kms:GenerateDataKey is what is missing when a pipeline cannot write to an encrypted bucket. And you know to
notify only what is actionable: failures and approvals, never successes, or the channel becomes noise.
All for some 28 dollars a month for the complete module, with two kinds of leftovers to watch: the artefacts that outlive the deleted pipeline and the bucket with no lifecycle rule.
Two loose ends remain that the lesson has deliberately left in plain sight. The first is that the three
environments share account 111122223333: the blast radius is not bounded, service limits are shared and
the bill is not really separated. The second is more uncomfortable: this pipeline, which exists so that
nobody deploys by hand, was created by hand, with a JSON file on Luis's laptop and a
create-pipeline.
In 08-05, "An end-to-end pipeline", we close the module by following a real change from Luis's laptop
to production: the "delivery time slot" field, which touches the shop, a consumer of
cola-mercadofresco-pedidos, the Aurora schema and the contract of the PedidoConfirmado event. We will see
step by step what gets checked and what happens if it fails; how expand-contract is applied in Aurora and
an event is versioned without breaking existing consumers, with the right order between producer and consumer
that closes the module 7 incident; the testing pyramid and which tests block; MercadoFresco's DORA
metrics before and after; the rollback runbook when the failure is spotted half an hour late; and feature
flags with AppConfig that turn "deploying on a Friday" into a business decision rather than an act of
faith.
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
