Project — Multi-Tier CI/CD Pipeline¶
This project combines modules 2 (EKS/ECS), 4 (IAM), and 6 (CodePipeline)
into one working release pipeline: a container-based app deployed to
ECS Fargate, built and shipped automatically on every push to main,
with a staging environment gating production.
Architecture¶
GitHub (main branch)
│ (CodeStar connection webhook)
▼
CodePipeline
├─ Source stage → pulls commit
├─ Build stage → CodeBuild: test, docker build, push to ECR
├─ Deploy-Staging → ECS service in `staging` cluster
├─ Manual Approval → human clicks "Approve" in console/CLI
└─ Deploy-Prod → ECS service in `prod` cluster (blue/green via CodeDeploy)
Two ECS clusters (staging, prod) share one ECR repository, keyed by
commit SHA tags — the exact image tested in staging is what gets
promoted to prod, never a rebuild.
Step 1 — ECR repository and ECS clusters¶
aws ecr create-repository --repository-name training-app
aws ecs create-cluster --cluster-name staging
aws ecs create-cluster --cluster-name prod
Step 2 — IAM roles (least privilege per module 4)¶
# CodeBuild role: push to ECR, write logs, read source — nothing else
aws iam create-role --role-name CodeBuildServiceRole \
--assume-role-policy-document file://trust-codebuild.json
aws iam put-role-policy --role-name CodeBuildServiceRole \
--policy-name ecr-push --policy-document file://codebuild-ecr-policy.json
# CodePipeline role: start builds, deploy to ECS, read/write the artifact bucket
aws iam create-role --role-name CodePipelineServiceRole \
--assume-role-policy-document file://trust-codepipeline.json
# ECS task execution role: pull from ECR, write CloudWatch Logs
aws iam create-role --role-name ecsTaskExecutionRole \
--assume-role-policy-document file://trust-ecs-tasks.json
aws iam attach-role-policy --role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Each role gets only the actions its stage needs — CodeBuild cannot call
ecs:UpdateService, and CodePipeline's role cannot push images
directly, matching the boundary discussed in module 4.
Step 3 — buildspec.yml¶
version: 0.2
phases:
pre_build:
commands:
- aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URI
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c1-8)
build:
commands:
- npm ci && npm test
- docker build -t $ECR_URI:$IMAGE_TAG .
post_build:
commands:
- docker push $ECR_URI:$IMAGE_TAG
- printf '[{"name":"training-app","imageUri":"%s"}]' "$ECR_URI:$IMAGE_TAG" > imagedefinitions.json
artifacts:
files: [imagedefinitions.json]
Step 4 — pipeline with a manual approval gate¶
The pipeline JSON follows module 6's structure with one addition — an
Approval action type between staging and prod:
{
"name": "PromoteToProd",
"actions": [{
"name": "ManualApproval",
"actionTypeId": { "category": "Approval", "owner": "AWS", "provider": "Manual", "version": "1" },
"configuration": { "CustomData": "Verify staging looks healthy before promoting." }
}]
}
# Approving from the CLI once staging is verified:
aws codepipeline put-approval-result \
--pipeline-name training-app-pipeline \
--stage-name PromoteToProd \
--action-name ManualApproval \
--result summary="Staging verified",status=Approved \
--token <token-from-get-pipeline-state>
Step 5 — blue/green production deploy¶
aws deploy create-deployment-group \
--application-name training-app \
--deployment-group-name prod \
--service-role-arn arn:aws:iam::123456789012:role/CodeDeployECSRole \
--deployment-config-name CodeDeployDefault.ECSLinear10PercentEvery1Minutes \
--ecs-services clusterName=prod,serviceName=training-app \
--auto-rollback-configuration enabled=true,events=DEPLOYMENT_FAILURE,DEPLOYMENT_STOP_ON_ALARM
Auto-rollback on a CloudWatch alarm means a spike in 5xx errors during the 10-minute traffic shift reverts to the prior task set automatically — combining module 8's monitoring with the deployment itself.
Verifying the pipeline¶
aws codepipeline start-pipeline-execution --name training-app-pipeline
aws codepipeline get-pipeline-state --name training-app-pipeline \
--query 'stageStates[*].{stage:stageName,status:latestExecution.status}'
Gotchas¶
- The staging and prod ECS services must use distinct task definitions even though they share an image — different environment variables (DB endpoints, feature flags) baked into separate task-def revisions, not the container image.
- A manual approval action blocks the pipeline indefinitely — set
up an EventBridge rule on
CodePipeline Stage Execution State ChangeforApprovalactions to notify a Slack/SNS channel, or approvals get forgotten and staging drifts from prod for days. - Blue/green rollback reverts ECS task sets, not any database migration run in the same pipeline — sequence schema migrations to be backward-compatible with both old and new code during the shift.
- The ECR image tag scheme must be deterministic (commit SHA, not
latest) or you cannot prove which exact build is running in prod during an incident.
How It Actually Works¶
The manual approval gate in this pipeline isn't a UI trick — it's
implemented as CodePipeline pausing its own state machine at that stage
transition, holding the pipeline execution in a durable InProgress state
with the approval action specifically waiting on either an SNS-notified
human decision (via the Console or API) or an expiration timeout you
configure; nothing is polling or consuming compute while it waits, exactly
like the artifact-passing mechanism between stages generally — pipeline
state lives in CodePipeline's own store, not in a running process.
The blue/green production deploy stage works by CodeDeploy provisioning an
entirely separate, parallel set of compute (new ASG instances or a new ECS
task set) and running it through your defined lifecycle hooks
(BeforeInstall, AfterInstall, ValidateService, etc.) while it receives
zero production traffic — only after ValidateService passes does
CodeDeploy issue the actual cutover, which for an ALB target group is
atomic re-registration of targets, and for Lambda is an alias weight shift
that can even be gradual (canary/linear) rather than instant. This is why
rollback in this design is nearly free: the previous version's compute is
left running (for a configurable window) rather than torn down immediately,
so "rollback" is simply re-pointing the same traffic-shift mechanism
backward instead of rebuilding anything.
Notice that every stage boundary in this pipeline is a genuine
service-to-service API handoff — CodePipeline calling CodeBuild's
StartBuild, then CodeDeploy's CreateDeployment — each polled
asynchronously for completion rather than any single long-running process
owning the whole pipeline; a stuck pipeline almost always means one of these
downstream services' own execution is stuck or throttled, not CodePipeline
itself.
Stretch goals¶
- Add a second manual approval before staging too, gated on automated integration test results rather than a human always clicking through.
- Fan out the Build stage to also run
terraform planagainst a staging Terraform stack (module 5) and post the plan output as a pipeline artifact for review. - Add an EventBridge rule that triggers a Step Functions workflow (module 3) on pipeline failure to page an on-call engineer.
- Extend the deployment group to multiple regions using the DR patterns from module 7, so a prod promotion updates a warm-standby region too.