CodePipeline: orchestrating delivery
Stages, actions, artifacts, approvals, secure roles.
A car factory does not build a car in one room. Raw steel goes in at one end, each station bolts on one more part and hands the shell down the line, and an inspected car rolls out the far end. If a station finds a cracked weld, the belt stops. AWS CodePipeline (Amazon Web Services' managed release orchestration service) is that belt for your code. You describe a release as an ordered list of stages, CodePipeline watches your source repository for changes, and every change gets walked through build, test, approval and deployment on its own. CodePipeline compiles nothing and deploys nothing itself. It is the foreman calling out the order of work, invoking CodeBuild, CodeDeploy, CloudFormation, ECS (Elastic Container Service) and Lambda at the right moment, and halting the line the second one of them reports a failure. Stopping fast, on an event rather than on a timer, is what continuous delivery on AWS is built on.
Stages, actions, and where the artifacts actually live
A pipeline is a list of stages, and each stage holds one or more actions. An action is one unit of work. It has a category (Source, Build, Test, Approval, Deploy or Invoke), an owner (AWS, ThirdParty or Custom), and a provider, which is the concrete service doing the work, such as CodeBuild. Each action names the artifacts it takes in and the artifacts it puts out, where an artifact is a named bundle of files. Each action also carries a runOrder number. Actions sharing a runOrder start together, and higher numbers wait for lower ones to finish, so you get fan-out and sequencing inside a single stage without inventing more stages. Between stages, nothing is handed over in memory. Every pipeline owns an artifact store, an S3 (Simple Storage Service) bucket sitting in the pipeline's Region. Each output artifact is zipped, written to that bucket, and read back out by the next action. A stage only hands over to the next one when every action in it has succeeded. One failed action stops the run cold and leaves later stages untouched, which is the fail-fast behaviour you want. It also means a deleted bucket, a bucket policy somebody tightened, or an artifact encrypted with the wrong key will break a pipeline that looks perfectly healthy on screen.
$ aws codepipeline get-pipeline-state --name web-prod \--query 'stageStates[].{stage:stageName,status:latestExecution.status}' \--output table------------------------------------| GetPipelineState |+-----------+----------------------+| stage | status |+-----------+----------------------+| Source | Succeeded || Build | Succeeded || Approve | Succeeded || Deploy | InProgress |+-----------+----------------------+# Deploy is the only stage still running; every earlier stage passed and# handed its output artifact to the next through the S3 artifact store.
V2 pipelines and the triggers that start them
CodePipeline comes in two versions. V1 is the original. V2 arrived in late 2023 and carries the features production teams keep asking for: git-push triggers you can filter by branch, tag and file path; pipeline-level variables you can pass into actions; stage-level conditions and rollback; and the QUEUED and PARALLEL execution modes. How a source change reaches the pipeline depends on the provider. Changes in CodeCommit and S3 arrive over EventBridge, the AWS event bus that routes events between services, and the pipeline starts within seconds. GitHub, GitLab and Bitbucket come in through AWS CodeConnections (the service formerly called CodeStar Connections) on a managed webhook, a callback URL the git host pings on every push. Old-style polling still works, but it adds latency and burns API calls, so reach for the event-driven path. A tight trigger filter pays for itself fast: one pipeline can react only to pushes on main that touch src/**, so a typo fix in the README never spends a build minute. Pipeline variables let a single definition serve dev, staging and prod without you copying the same actions three times.
$ aws codepipeline get-pipeline --name web-prod \--query 'pipeline.{type:pipelineType,mode:executionMode}'{"type": "V2","mode": "SUPERSEDED"}$ aws codepipeline get-pipeline --name web-prod --query 'pipeline.triggers'[{"providerType": "CodeStarSourceConnection","gitConfiguration": {"sourceActionName": "Source","push": [{"branches": { "includes": ["main"] },"filePaths": { "includes": ["src/**"] }}]}}]# Only a push to main that changes files under src/ starts this pipeline.
Execution modes: when commits arrive faster than you can deploy
Execution mode is the rule for what happens when changes land faster than a run can finish. It is a V2-only control, and it catches out teams who assume every commit they push reaches production. SUPERSEDED is the default. A newer execution overtakes an older one still sitting outside a stage waiting to get in, and the older run is stopped with the status Superseded, so only the newest change lands and the commits in between may never deploy at all. QUEUED behaves like a single-file queue at a bank teller: executions run one at a time in the order they arrived, and every change gets processed. PARALLEL lets executions run side by side and independently, which suits a pipeline that fans out per feature branch. The mode is a property of the pipeline, so pick it on purpose. This is a correctness decision about which of your commits reach production, not a performance knob.
Least privilege, and deploying into another account
A pipeline that can push to production is, in effect, a key to production, so its identity deserves the same care you would give a human administrator. Three kinds of role are in play. The pipeline service role, trusted by codepipeline.amazonaws.com, is what CodePipeline assumes to read the artifact bucket, invoke actions and send approval notifications. Any individual action can also carry its own roleArn, and that role can live in a different AWS account, which is how one central tooling account deploys into isolated dev and prod accounts without a single credential being copied anywhere. Then the services the pipeline calls, CodeBuild among them, run under roles of their own. Scope each one to exactly what its stage touches. A wildcard on any of the three turns one compromised build step into account-wide access. Keep long-lived keys out of this entirely: use CodeConnections for git, use OIDC federation (OpenID Connect, which mints a short-lived token per run instead of storing a secret) for outside CI systems, and pull secrets from Secrets Manager or SSM Parameter Store (the config and secret store inside AWS Systems Manager) while the action runs, rather than baking them into an artifact. For cross-account work, encrypt the artifact store with a customer-managed KMS key (Key Management Service, where AWS holds your encryption keys) and grant that key to the remote account's roles. A forgotten key grant is the most common way a cross-account deploy fails.
$ aws codepipeline get-pipeline --name web-prod \--query 'pipeline.{role:roleArn,store:artifactStore}'{"role": "arn:aws:iam::111122223333:role/service-role/AWSCodePipelineServiceRole-us-east-1-web-prod","store": {"type": "S3","location": "codepipeline-us-east-1-8f3a2c9b41d7","encryptionKey": {"id": "arn:aws:kms:us-east-1:111122223333:key/9b2c7e10-4a1d-4f83-b0c5-2d7e9f1a6b04","type": "KMS"}}}# A customer-managed KMS key on the artifact store is what lets a role in# another account decrypt artifacts during a cross-account deploy.
What it costs, and where the limits bite
Price and quotas quietly shape how you carve up pipelines. V1 charges a flat 1.00 USD per active pipeline per month, where active means it ran at least once that month; idle pipelines cost nothing, and one active pipeline per account is free. V2 drops the per-pipeline fee and bills by the minute an action spends executing, 0.002 USD per action-execution-minute, with 100 free action-execution minutes per account per month. So a busy pipeline full of long actions can cost more on V2, while a shelf of pipelines that rarely run gets cheaper. Model your real usage before you migrate everything. On quotas, the default is 1000 pipelines per Region per account and you can raise it through Service Quotas. A separate fixed cap of 300 applies only to pipelines still using source polling, and that one does not move. Inside a stage, runOrder is your unit of parallelism. Manual approvals bring one limit worth memorising: an approval nobody clicks expires after seven days by default and fails the stage, which is how a release quietly dies over a long holiday weekend.
$ aws codepipeline start-pipeline-execution --name web-prod{"pipelineExecutionId": "b3d5e1a2-4f6c-4a1e-9c2d-7e8f0a1b2c3d"}$ aws codepipeline list-pipeline-executions --name web-prod --max-items 3 \--query 'pipelineExecutionSummaries[].{id:pipelineExecutionId,status:status,trigger:trigger.triggerType}' \--output table-------------------------------------------------------------| ListPipelineExecutions |+---------------+-------------+---------------------------+| id | status | trigger |+---------------+-------------+---------------------------+| b3d5e1a2... | InProgress | StartPipelineExecution || a1c47f90... | Superseded | WebhookV2 || 90fbe233... | Succeeded | WebhookV2 |+---------------+-------------+---------------------------+# The Superseded row: a newer push overtook that run before it could deploy.# WebhookV2 is the trigger type for a CodeConnections git push.
CodePipeline orchestrates; it does not build. Stages fail closed, so a red CodeBuild action means nothing gets promoted to prod. The artifacts in S3, or the container images referenced by digest in ECR (Elastic Container Registry), are the baton passed from stage to stage. If that baton can be swapped after the tests ran, what you call prod is not what you tested.
V2 gives you sharper triggers and parallel actions, and the execution mode decides whether a burst of commits supersedes or queues. A manual approval is still a human standing at a gate. Put those gates where the risk genuinely jumps, not in front of every README typo.
A cross-account deploy role should be assumable by exactly one principal: the pipeline role in the tooling account. Hand the pipeline role AdministratorAccess instead, and a single compromised GitHub OIDC trust relationship burns three environments at once.
Try this
List your pipelines, then read the stage states of one execution. Every command here is read-only, and that is enough to watch fail-fast orchestration with your own eyes.
aws codepipeline list-pipelines --query 'pipelines[].name' --output tableaws codepipeline list-pipeline-executions --pipeline-name app-delivery --max-items 3 \--query 'pipelineExecutionSummaries[].{Id:pipelineExecutionId,Status:status,Started:startTime}' --output tableaws codepipeline get-pipeline-state --name app-delivery \--query 'stageStates[].{Stage:stageName,Status:latestExecution.status}' --output table
app-delivery---------------------------------------------| ListPipelineExecutions |+----------+----------+---------------------+| Id | Status | Started |+----------+----------+---------------------+| exe-123 | Succeeded| 2026-07-23T18:01:00Z|+----------+----------+---------------------+Source | SucceededBuild | SucceededDeploy | InProgress
Takeaway
The pipeline is the foreman, not the worker. Artifacts that cannot change after the tests pass, roles scoped to one stage each, and stages that stop dead on the first failure are what make an automated line safe to point at production.
Next thing to try on your own pipeline: put one gate on the road to prod, either a manual approval or an automatic rollback, then deliberately break a build and prove it cannot slip past.
The busiest station on this line is the build, where a git commit becomes the tested, packaged artifact everything downstream trusts. That is where the next lesson goes. CodeBuild: fast, secure builds covers buildspec phases, caching dependencies and image layers, custom build images, and how to keep a build both quick and hard for anyone to tamper with.