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

  1. Continuous delivery versus continuous deployment
  2. Structure: pipeline, stages, actions, executions and transitions
  3. The artefacts and the bucket that moves them
  4. MercadoFresco's pipeline
  5. V1 and V2 types, triggers and variables
  6. The source action with CodeConnections
  7. Build, deployment and parallel actions with runOrder
  8. Manual approval: what Marta must read in thirty seconds
  9. Environments: development, pre-production and production
  10. Smoke tests and quality gates with a Lambda action
  11. Retries, queued executions and superseded mode
  12. Permissions: pipeline role, per-action roles and cross-account
  13. Notifications to email and Slack
  14. The pipeline as code
  15. Cost and cleanup
  16. Common mistakes and tips
  17. Exercises
  18. 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-1

MercadoFresco'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 target

The 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-1

FAILED_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-1

Notice 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-1

It 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-1

Deleting 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:

{ "name": "CodeDeployProduccion",
  "inputArtifacts": [{ "name": "PaqueteTienda" }] }

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-1

FAILED_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.

© Copyright 2026. All rights reserved