AWS CodePipeline
Building CI/CD pipelines with AWS CodePipeline. Stages and actions, source providers, approval gates, deployment strategies, and how it compares to GitHub Actions.
CodePipeline orchestrates a release process as a sequence of stages. It does not build or deploy anything itself. It coordinates other services that do, passing artifacts between them.
Understanding that division is what makes the rest of it make sense.
Structure
| Concept | What it is |
|---|---|
| Pipeline | The whole release workflow |
| Stage | A named group (Source, Build, Deploy) |
| Action | One unit of work within a stage |
| Artifact | Files passed between stages, stored in S3 |
| Transition | The link between stages, which can be disabled |
Stages run in order. Actions within a stage run in parallel unless you give them runOrder values, which sequence them.
Every stage must have at least one action, and the first must be a source action.
Source ──► Build ──► Deploy-Staging ──► Approve ──► Deploy-Prod
(GitHub) (CodeBuild) (ECS) (manual) (ECS)
Source
For GitHub, GitLab or Bitbucket, use a CodeStar connection rather than an OAuth token or a webhook you manage:
aws codeconnections create-connection \
--provider-type GitHub \
--connection-name acme-github
The connection is created in PENDING and must be authorised in the console. A one-time browser step that cannot be scripted. Until then, pipelines using it fail with a permissions error that does not mention the connection state.
aws codeconnections list-connections \
--query 'Connections[].{Name:ConnectionName,Status:ConnectionStatus}'
Source actions can also be S3, ECR, or CodeCommit, though CodeCommit is closed to new customers, so new pipelines should assume an external Git provider.
Artifacts
Every artifact passes through an S3 bucket that CodePipeline manages. Actions declare input and output artifacts by name, and CodePipeline handles the upload and download.
Two consequences:
The bucket is a security boundary. Build output lives there, sometimes including configuration. Encrypt it, block public access, and restrict who can read it.
Artifacts accumulate. Every pipeline execution stores its outputs, and nothing removes them. Add a lifecycle rule or the bucket grows forever:
{
"Rules": [{
"ID": "expire-pipeline-artifacts",
"Status": "Enabled",
"Filter": { "Prefix": "" },
"Expiration": { "Days": 30 },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}]
}
Manual approval
The simplest and most valuable gate:
{
"name": "ApproveProduction",
"actionTypeId": {
"category": "Approval", "owner": "AWS",
"provider": "Manual", "version": "1"
},
"configuration": {
"NotificationArn": "arn:aws:sns:eu-west-1:111122223333:deploy-approvals",
"CustomData": "Review the staging deployment before promoting",
"ExternalEntityLink": "https://staging.acme.com"
}
}
Approval actions time out after 7 days and fail the pipeline, which surprises teams who leave one pending over a holiday.
Control who can approve with the codepipeline:PutApprovalResult permission scoped to that pipeline's ARN. Otherwise anyone with pipeline access can approve their own change.
Deployment actions
| Target | Provider |
|---|---|
| ECS | ECS for rolling, CodeDeployToECS for blue/green |
| Lambda | CloudFormation or SAM |
| EC2 / on-premises | CodeDeploy |
| S3 static site | S3 |
| Infrastructure | CloudFormation |
The ECS rolling provider is the simplest: point it at a cluster and service with an imagedefinitions.json artifact from the build, and it updates the service.
CodeDeploy blue/green for ECS is more work and genuinely better for anything user-facing. It stands up a replacement task set alongside the existing one, shifts traffic between two target groups gradually, and can roll back automatically on a CloudWatch alarm. A bad deploy becomes a reverted deploy rather than an incident.
Variables between stages
Actions can emit namespaced variables that later stages consume:
{
"name": "Build",
"namespace": "BuildVars",
"configuration": { "ProjectName": "payments-build" }
}
Downstream: #{BuildVars.IMAGE_TAG}. CodeBuild exports these via exported-variables in its buildspec, which is how a generated image tag reaches the deploy stage without being written to a file.
Should you use it?
Worth being direct, because CodePipeline is frequently chosen by default on AWS and is not always the right answer.
| CodePipeline | GitHub Actions / GitLab CI | |
|---|---|---|
| AWS integration | Native, IAM-based | Via OIDC, which works well |
| Configuration | JSON or IaC, verbose | YAML in the repository |
| Local reproduction | Difficult | Easier |
| Ecosystem | AWS services | Thousands of community actions |
| Cost | Per active pipeline per month, plus CodeBuild minutes | Included minutes, then per minute |
| Code and CI together | Separate | Same platform |
If your code is on GitHub or GitLab, their native CI is usually the better choice. The configuration lives beside the code, the ecosystem is far larger, and OIDC gives keyless AWS access that removes CodePipeline's main historical advantage.
CodePipeline makes sense when you are deeply AWS-native, need approval workflows and deployment orchestration tied into IAM, are deploying with CodeDeploy's blue/green capabilities, or have compliance requirements that keep the pipeline inside your AWS account boundary.
A common and sensible hybrid: build and test in GitHub Actions, then use CodeDeploy for the deployment itself.
Debugging
aws codepipeline get-pipeline-state --name payments-pipeline \
--query 'stageStates[].{Stage:stageName,Status:latestExecution.status,Error:actionStates[?latestExecution.errorDetails].latestExecution.errorDetails.message}'
| Symptom | Cause |
|---|---|
| Source fails immediately | CodeStar connection still PENDING (authorise it in the console) |
| Build fails with access denied | The CodeBuild service role lacks a permission, not the pipeline role |
| Deploy cannot find artifacts | Output artifact name does not match the downstream input name |
| Pipeline does not trigger | Webhook missing, or the branch filter does not match |
| Approval expired | Seven-day timeout elapsed |
That second row catches people repeatedly: CodePipeline and CodeBuild have separate service roles, and a permissions error inside a build is almost always the CodeBuild role rather than the pipeline's.
Quick reference
| Task | Command |
|---|---|
| Pipeline state | aws codepipeline get-pipeline-state --name <n> |
| Start manually | aws codepipeline start-pipeline-execution --name <n> |
| Execution history | aws codepipeline list-pipeline-executions --pipeline-name <n> |
| Approve | aws codepipeline put-approval-result --pipeline-name <n> --stage-name <s> --action-name <a> --result summary=ok,status=Approved --token <t> |
| Export definition | aws codepipeline get-pipeline --name <n> |
| List connections | aws codeconnections list-connections |
| Stop an execution | aws codepipeline stop-pipeline-execution --pipeline-name <n> --pipeline-execution-id <id> |
Key takeaways
- CodePipeline orchestrates; it does not build or deploy. Other services do the work.
- CodeStar connections must be authorised in the console: a pipeline using a
PENDINGconnection fails confusingly. - Artifacts live in S3 and accumulate. Add a lifecycle rule and encrypt the bucket.
- Approval actions expire after 7 days and fail the pipeline.
- Restrict
codepipeline:PutApprovalResult, or people can approve their own changes. - CodePipeline and CodeBuild have separate service roles. Build permission errors are usually the CodeBuild role.
- If your code is on GitHub or GitLab, their CI is usually the better choice: OIDC removes the old integration advantage.
Check your understanding
A source stage that fails immediately
A new pipeline fails at the source stage with a permissions error. The CodeStar connection exists and the IAM role looks correct. What should you check?
Access denied inside a build
A build step fails with AccessDenied reading an S3 bucket. The CodePipeline service role has full S3 access. Why does it still fail?
Frequently Asked Questions
What is the difference between CodePipeline, CodeBuild and CodeDeploy?
CodePipeline orchestrates. It defines the stages and passes artifacts between them, but performs no build or deployment itself. CodeBuild runs build and test commands in a managed container. CodeDeploy handles the deployment mechanics for EC2, ECS and Lambda, including blue/green traffic shifting and automatic rollback. A typical pipeline uses all three: CodePipeline coordinating, CodeBuild producing the artifact, and CodeDeploy releasing it.
Should I use CodePipeline or GitHub Actions?
If your code is on GitHub or GitLab, their native CI is usually the better choice. The configuration lives beside the code, the community ecosystem is vastly larger, and OIDC federation gives keyless access to AWS, which removes the integration advantage CodePipeline used to have. CodePipeline still makes sense for deeply AWS-native teams, for CodeDeploy's blue/green capabilities, and where compliance requires the pipeline to stay inside the AWS account boundary. A common hybrid is building in GitHub Actions and deploying with CodeDeploy.
Why does my CodeStar connection not work?
It is almost certainly still in PENDING status. Creating a connection via the API or infrastructure-as-code only creates a placeholder. Completing it requires a one-time browser authorisation with the Git provider that cannot be scripted. Until then, pipelines using it fail with a permissions error that gives no hint about the connection state. Check with aws codeconnections list-connections and finish the handshake in the console.
How do I add a manual approval step?
Add an action with category Approval and provider Manual in its own stage before production. It can send an SNS notification, include custom text for the reviewer, and link to a staging URL for verification. Two details matter: approval actions expire after seven days and fail the pipeline, and you should scope the codepipeline:PutApprovalResult permission to specific pipelines. Otherwise anyone with access can approve their own changes, which defeats the gate.
Where do CodePipeline artifacts go?
Into an S3 bucket CodePipeline creates and manages, with each execution storing its output artifacts there. Two things follow. The bucket is a security boundary, since build output may contain configuration. Encrypt it, block public access, and restrict who can read it. And nothing cleans it up, so add a lifecycle rule expiring objects after a few weeks or it grows indefinitely and becomes a quiet line on your bill.
Why does my build fail with AccessDenied when the pipeline role has permissions?
Because CodeBuild has its own service role, and that is the identity your build commands run as. The CodePipeline role only orchestrates the pipeline and moves artifacts; it is not used inside a build at all. Permissions your buildspec needs, pulling from ECR, reading a parameter, deploying with Terraform, belong on the CodeBuild project's role. This separation accounts for a large share of the permission problems people hit with CodePipeline.