AWS DevOps

CI/CD on AWS: CodePipeline + CodeBuild Hands-On (Build, Test, Deploy)

You have an application in a Git repository and a place to run it — an ECS service, a Lambda function, an S3 bucket, a CloudFormation stack. Between the two sits the least glamorous, most load-bearing part of the whole system: the thing that turns git push into “the new version is live,” reliably, on every commit, without a human copying artifacts around at 2 a.m. On AWS the native pair that does this is AWS CodePipeline — the orchestrator that models your release as stages of actions — and AWS CodeBuild — the managed build service that runs your tests, compiles, and (for a container app) does the docker build and docker push. Wire them together and a push to main runs your tests, builds an immutable image, waits for someone to click Approve, and does a rolling deploy — hands-off, auditable, repeatable.

CodePipeline looks like a row of boxes in the console and hides a stack of decisions that separate a pipeline you trust from one that silently ships the wrong thing. How does a Source action even see your GitHub repo without you pasting a personal access token — a CodeConnections connection. How does the artifact from Build reach Deploy — a zipped input/output artifact in a per-pipeline S3 bucket, matched by name. How does CodeBuild run docker build at all — privileged mode and a big enough compute type. Where do secrets come from — SSM Parameter Store and Secrets Manager, referenced in buildspec.yml, never hard-coded. Which of the two IAM roles grants what — the pipeline role assumes actions and passes roles; the CodeBuild role talks to ECR, logs, SSM. Get one of those wrong and the failure is specific and infuriating: connection is not available, denied: not authorized, artifact not found, or a Deploy stage that sits InProgress forever.

This article builds the whole mental model by building the pipeline. You will connect GitHub via CodeConnections, create a CodeBuild project with a real buildspec.yml that runs tests, docker builds and pushes an immutable :sha tag to Amazon ECR and emits imagedefinitions.json, add a manual-approval gate, and finish with a rolling deploy to an ECS service — first with the aws CLI, then the identical stack in Terraform, then a clean teardown. Around that sits the reference you will actually return to: option-by-option tables for stages, actions, sources, buildspec phases, compute types, deploy providers, the two service roles, and a symptom → confirm → fix troubleshooting playbook for every stage that breaks. Read the prose once; keep the tables open when your own pipeline goes red.

What problem this solves

Without a pipeline, “deploying” is a person following a runbook: pull the latest code, run the tests locally (or skip them because it’s late), build the artifact on a laptop with whoever’s toolchain versions, upload it somewhere, and poke the running service into picking it up. Every one of those steps is a place for drift and mistakes. The build isn’t reproducible because it ran on a machine you can’t reconstruct. The tests are advisory because a human decides whether to run them. The deploy is manual, so it’s rare, so each one is big and scary, so rollbacks are worse. And there is no audit trail: six weeks later nobody can say which commit is in production or who approved it.

CI/CD replaces the runbook with a program. Continuous integration means every push is automatically built and tested in a clean, defined environment — CodeBuild spins up a fresh container, runs your buildspec, and fails the commit if a test fails, so main stays releasable. Continuous delivery means that green build flows automatically toward production through gated stages — build the image, push it to ECR, pause for a human approval on the risky ones, then deploy — so releases become small, frequent, and boring. The failure modes you are trying to design out are concrete: a build that “works on my machine” but not in CI; a deploy that ships an image nobody scanned; a rollback that pulls “the version that was live” and gets the wrong thing because tags were mutable; a production change with no record of who shipped it.

Who hits this: every developer who has manually deployed twice and never wants to a third time; every platform team asked for “one-click, audited deploys” and “no long-lived credentials in CI”; every security engineer who needs to prove that the artifact in production came from a specific commit through a controlled path. CodePipeline + CodeBuild is a DVA-C02 core topic (you’re expected to know buildspec phases, artifacts, and approvals cold), a SAA-C03 architecture piece (how the release path fits the workload), and a SOA-C02 operations concern (pipelines that page you when they break). Here is the whole field on one screen — the pieces you meet and the classic trap on each.

Piece What it is You configure it as The classic trap
CodePipeline The release orchestrator: stages of actions A pipeline (V1 or V2) Modeling one giant stage instead of Source→Build→Deploy
Stage An ordered group of actions with a transition stages[] in the pipeline Serial actions that should run in parallel (runOrder)
Action One unit of work (Source/Build/Deploy/Approval/Invoke) actions[] with a provider Wrong provider, or missing iam:PassRole
Artifact The zipped files handed between stages Named OutputArtifacts/InputArtifacts Output name ≠ the next action’s input name
Artifact store The per-pipeline S3 bucket + encryption artifactStore (SSE-S3 or KMS) KMS key the role can’t decrypt
CodeConnections Managed OAuth link to GitHub/GitLab/Bitbucket A connection ARN, UseConnection Left PENDING; handshake never completed
CodeBuild Managed build compute that runs buildspec.yml A build project No privileged-mode for Docker; compute too small
buildspec.yml The build recipe: phases, artifacts, reports, cache A YAML file in the repo Wrong phase for a command; secret hard-coded
Manual approval A human gate that pauses the run An Approval action + SNS No notify; approver lacks PutApprovalResult
Deploy action Ship it: ECS/CFN/Lambda/S3/EKS A Deploy action Deploy role can’t UpdateService / cross-account
Service roles Pipeline role + CodeBuild role Two IAM roles, least privilege One over-broad * role for everything

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You need an AWS account with permission to create CodePipeline, CodeBuild, IAM roles, an S3 bucket and (optionally) a KMS key — a dev or sandbox account, never straight into production. Have the AWS CLI v2 configured, Terraform ≥ 1.5 for the IaC half, a GitHub account with a small repo you control (a Dockerfile plus a trivial test), and the two things the lab deploys into: an ECR repository and a running ECS service. Those two are built in the wave siblings Amazon ECR Hands-On: Push, Pull, Scan & Lifecycle Container Images and Amazon ECS on Fargate: Deploy Your First Service Behind an ALB (Hands-On) — do those first if you haven’t; this article assumes the repo kv-web and an ECS service already exist. You should be comfortable with Docker basics, IAM roles vs policies, and reading YAML/JSON.

Where this sits: CodePipeline is the control plane of your release, sitting above the compute you already run. It doesn’t replace your infrastructure tooling — it drives it. A pipeline’s Deploy stage typically runs a CloudFormation change set, an ECS rolling update, a CodeDeploy blue/green shift, or a CDK deploy. If your deploys are described as code, the natural next steps are the wave siblings AWS CodeDeploy: Blue/Green & Canary Deployments (Hands-On) for safe traffic shifting and AWS CDK with TypeScript: Infrastructure as Code (Hands-On) for synthesising the stacks a pipeline deploys. The identity and network plumbing under all of this is covered in AWS IAM: Users, Groups, Roles & Policies (Hands-On) and AWS PrivateLink & VPC Endpoints: Interface vs Gateway. A quick map of who owns what, so when a stage fails you look in the right place first:

Layer What lives here Who “owns” it What a mistake here causes
Source connection The link to GitHub/GitLab CodeConnections connection is not available; no trigger
Pipeline definition Stages, actions, artifacts, triggers CodePipeline Artifact-name mismatch; stage never runs
Artifact store The zipped handoffs + encryption S3 + KMS AccessDenied on the CMK; artifact not found
Build recipe Phases, tests, docker build, push buildspec.yml + CodeBuild Phase error; OOM; docker daemon unreachable
Service roles What the pipeline / build may do IAM denied; PassRole refused; deploy blocked
Deploy target ECS/CFN/Lambda/S3/EKS The deploy provider Rolling deploy stuck; wrong image; cross-account

Core concepts

Start with the object model, because every error message is phrased in these nouns. A pipeline is an ordered list of stages. Each stage is a group of actions. Between stages sits a transition (which you can disable to hold a release). Within a stage, actions run in parallel by default but you can serialize them with runOrder (an action with runOrder: 2 waits for runOrder: 1 to succeed). Each action belongs to a category — Source, Build, Test, Deploy, Approval, or Invoke — and each category is fulfilled by a provider (CodeConnections, CodeBuild, ECS, CloudFormation, CodeDeploy, Lambda, and so on).

Data moves between actions as artifacts: a zipped bundle of files. An action declares its output artifacts (what it produces) and input artifacts (what it consumes), each by a name you choose. CodePipeline stores every artifact in a per-pipeline S3 bucket — the artifact store — encrypted with SSE-S3 or a KMS customer-managed key. The single most common structural bug in all of CodePipeline is a name mismatch: Build writes an output artifact called BuildOut, Deploy reads an input artifact called build_output, and the run fails with “artifact not found.” The names are literal strings and they must match.

The Source action is special: it turns “there is new code” into “here is a zip of that code as the first artifact.” For GitHub/GitLab/Bitbucket that plumbing is a CodeConnections connection (formerly CodeStar Connections) — a managed OAuth link you authorize once by installing the AWS Connector app on your Git provider; the connection lives as an ARN the pipeline references with the codeconnections:UseConnection permission. This is why you never paste a personal access token into a pipeline anymore. For a container-image source you can use ECR (a new image tag triggers the pipeline); for a blob you can use S3.

Everything the pipeline is allowed to do comes from two service roles. The pipeline service role (trusted by codepipeline.amazonaws.com) is assumed by CodePipeline to read/write the artifact bucket, start builds, and call deploy APIs — crucially it needs iam:PassRole to hand your task/execution roles to ECS or your stack role to CloudFormation. The CodeBuild service role (trusted by codebuild.amazonaws.com) is assumed by each build to write logs, read the source artifact, pull secrets from SSM/Secrets Manager, and — in our lab — authenticate to and push to ECR. Keeping these two roles distinct and least-privilege is the difference between a pipeline you can hand an auditor and one you can’t.

Concept Definition Why it matters
Stage An ordered group of actions with a transition in front The unit you gate, disable, and read status on
Transition The gate between two stages (enabled/disabled) Disable it to hold releases without deleting the pipeline
Action One task: a category + provider + config Where the actual work (and the failure) happens
runOrder Integer that serializes actions in a stage Run test before build; approval before deploy in a stage
Category Source · Build · Test · Deploy · Approval · Invoke Determines valid providers and artifact rules
Provider The service that fulfils a category CodeConnections, CodeBuild, ECS, CFN, CodeDeploy, Lambda
Input artifact A named bundle an action consumes Must match a prior action’s output name exactly
Output artifact A named bundle an action produces Zipped into the artifact-store bucket
Artifact store The pipeline’s S3 bucket + encryption key Cross-region actions need a bucket per region
Execution One run of the pipeline for one source change Has an ID; visible in history; mode governs concurrency
Pipeline role Role CodePipeline assumes to orchestrate Needs PassRole, StartBuild, deploy + S3/KMS
CodeBuild role Role each build assumes to do work Needs logs, S3 artifact, ECR, SSM/Secrets

CodePipeline: stages, actions, artifacts

Action categories and their providers

The category constrains what an action may do and how artifacts flow. Source actions have only outputs; Deploy actions have only inputs; Build/Test actions have both. Know the common provider per category — the exam tests it and so does real life.

Category Common providers Input artifact Output artifact Typical use
Source CodeConnections (GitHub/GitLab/Bitbucket), CodeCommit, S3, ECR none yes (the source) Fetch the code/image that triggered the run
Build CodeBuild, Jenkins yes optional Compile, test, docker build, push
Test CodeBuild, third-party yes optional Run a test suite as its own action
Deploy ECS, CloudFormation, CodeDeploy, S3, Elastic Beanstalk, Lambda, EKS, Service Catalog yes usually none Ship the artifact to the target
Approval Manual approval none none Pause for a human decision
Invoke Lambda, Step Functions optional optional Custom logic: run a script, call an API

Transitions, runOrder, and stage shape

A healthy pipeline is a handful of stages, each doing one job. Actions inside a stage default to parallel; runOrder makes them sequential when order matters (an approval must precede the deploy it guards). Transitions are your manual brake: disable the transition before Deploy to freeze production while leaving Build running.

Control What it does Values When to use
Stage Groups actions; status rolls up 1…N stages Source, Build, Approve, Deploy as separate stages
Transition Gate before a stage Enabled / Disabled Disable to hold releases (e.g. change freeze)
runOrder Serialize actions in a stage Integer ≥ 1 Approval runOrder:1, Deploy runOrder:2
Parallel actions Same runOrder = concurrent Deploy to two regions at once
Namespace Exposes an action’s variables e.g. SourceVariables Pass the commit SHA to a later action

V1 vs V2 pipelines

CodePipeline now has two pipeline types. V1 is the classic model. V2 adds git-based triggers with branch/tag/PR/file-path filters, pipeline-level variables, and execution modes — and is billed differently (per action-execution-minute rather than a flat monthly fee). New pipelines should almost always be V2; the extra trigger precision alone (build only when src/** changes on main) is worth it.

Capability V1 V2
Stages / actions Yes Yes
Git triggers with filters No (branch only) Yes — branch, tag, PR, filePaths include/exclude
Pipeline-level variables No Yes — declared, referenced as #{variables.name}
Execution modes SUPERSEDED only SUPERSEDED, QUEUED, PARALLEL
Billing ~$1 per active pipeline / month Per action-execution-minute (usage-based)
Recommended for Legacy, simple flows Everything new

Execution modes

The mode decides what happens when a new commit lands while a run is still going — the difference between deploying the latest code and deploying every commit in order.

Mode Behaviour on a new change mid-run Use when
SUPERSEDED (default) Newer execution replaces the older waiting one; only the latest proceeds You only care about deploying the newest commit
QUEUED Executions queue and run one-by-one, in order Every commit must go through (ordered releases)
PARALLEL Executions run independently and concurrently Per-branch pipelines; independent changes

Source: CodeConnections, CodeCommit, S3, ECR

Source provider comparison

The Source action is where a pipeline meets your VCS. For third-party Git the answer is CodeConnections; the older per-pipeline webhook + OAuth token and CodeStar connections are legacy. CodeCommit is AWS’s own Git host but is now in maintenance.

Source Trigger mechanism Auth Notes
CodeConnections → GitHub Webhook via the AWS Connector app Managed OAuth (a connection ARN) The default for GitHub today; no PAT stored
CodeConnections → GitLab Webhook Managed OAuth GitLab.com and self-managed
CodeConnections → Bitbucket Webhook Managed OAuth Bitbucket Cloud
CodeCommit EventBridge rule (recommended) or polling IAM (it’s an AWS service) ⚠️ Closed to new customers; prefer GitHub/GitLab
Amazon S3 EventBridge on object create, or polling IAM Source is a zip/object; good for artifacts, not code
Amazon ECR EventBridge on new image push IAM A new image tag drives the pipeline

CodeCommit’s maintenance status — read this before you pick it

As of mid-2024 AWS CodeCommit is closed to new customers: if your account never used it, you cannot create new CodeCommit repositories, and AWS steers all new work to GitHub, GitLab, or Bitbucket via CodeConnections. Existing CodeCommit customers keep working. Practical takeaway: do not architect a new pipeline around CodeCommit. Use a third-party Git provider through a CodeConnections connection — which is exactly what the lab does.

V2 triggers and filters

V2 lets a Source trigger fire selectively. This is how you stop a docs-only commit from rebuilding and redeploying your service, or run a separate pipeline for tags.

Filter What it matches Example Effect
Push · branches Branch name (include/exclude glob) main, release/* Build only on those branches
Push · tags Git tag (include/exclude) v* A release pipeline on version tags
Push · filePaths Changed file paths (include/exclude) include src/**, exclude docs/** Skip docs-only commits
Pull request PR events (opened/updated/closed) on main CI checks on PRs
PR · filePaths Paths changed in the PR include app/** Gate only code-relevant PRs

CodeBuild and the buildspec.yml

buildspec phases — where each command belongs

CodeBuild runs your build inside a container from a managed (or custom) image, executing the phases of buildspec.yml in order. Put each command in the right phase: dependency installs in install, registry login in pre_build, compile/test/build in build, push and artifact emission in post_build. If a phase command exits non-zero, the build fails and later phases are skipped — except finally blocks, which run even on failure (use them for cleanup or diagnostics).

Phase Runs Typical commands If it fails
install First; sets runtime versions runtime-versions, npm ci, install tools Build fails; nothing else runs
pre_build Before the build ECR docker login, compute the image tag, lint Build fails; build/post_build skipped
build The main work npm test, docker build, compile post_build still runs? No — only finally
post_build After a successful build docker push, write imagedefinitions.json Build marked failed
finally (per phase) Always, after that phase’s commands Upload logs, tear down a test DB Runs even when the phase failed

buildspec top-level keys

buildspec.yml is more than phases. These keys control secrets, outputs, test reports, and caching.

Key Purpose Example
version Buildspec schema 0.2 (current)
env Build-time variables + secret sources variables, parameter-store, secrets-manager
phases The ordered commands install/pre_build/build/post_build
artifacts Files to zip as the output artifact files: [imagedefinitions.json]
reports Test/coverage reports to ingest file-format: JUNITXML
cache Paths to cache across builds paths: [node_modules/**/*]
batch Batch/matrix build configuration build-graph, build-matrix
proxy Behaviour behind an explicit proxy upload-artifacts, logs

Secrets in a build — never hard-code

env pulls values from three places. Plaintext variables are fine for non-secret config; secrets must come from SSM Parameter Store (parameter-store:) or Secrets Manager (secrets-manager:), which resolve at build start using the CodeBuild role’s permissions. Reference a specific JSON key in a secret with secret-id:json-key.

Source buildspec syntax Resolved from Grant on the CodeBuild role
Plaintext variables: { KEY: value } The buildspec / project env none (visible in logs — non-secret only)
SSM Parameter Store parameter-store: { API: /kv/web/api } SSM at build start ssm:GetParameters on the parameter ARN
Secrets Manager secrets-manager: { DB: kv/web/db:password } Secrets Manager at build start secretsmanager:GetSecretValue (+ kms:Decrypt)
Project env override Set in the project, not the buildspec Project config Same as above; keep secrets out of plaintext

Build environments: compute types

The compute type sets vCPU and memory; the environment type sets the OS/architecture (Linux, Linux GPU, ARM, Windows). Too small and a JVM/webpack build OOMs; too big and you pay for idle cores. Sizes for the Linux general-purpose fleet:

Compute type vCPU Memory Good for
BUILD_GENERAL1_SMALL 2 3 GB Small apps, quick tests, the free tier
BUILD_GENERAL1_MEDIUM 4 7 GB Typical app builds + docker build
BUILD_GENERAL1_LARGE 8 15 GB Big compiles, multi-arch images
BUILD_GENERAL1_XLARGE 36 72 GB Heavy monorepos, parallel test shards
BUILD_GENERAL1_2XLARGE 72 145 GB Very large builds
BUILD_LAMBDA_* 1–10 GB tiers Fast-start, short builds on Lambda compute

Managed images and privileged mode

The managed image ships a toolchain; aws/codebuild/standard:7.0 (Ubuntu) carries current Node, Python, Go, Java, and the Docker CLI. To run docker build you must set privileged-mode: true on the project so the build container can run the Docker daemon — without it, docker commands fail with “Cannot connect to the Docker daemon.”

Setting Values Default When to change Gotcha
image aws/codebuild/standard:7.0, AL2023, ARM, Windows, custom ECR standard Match your runtime/arch Custom images need the CodeBuild role to pull
type LINUX_CONTAINER, LINUX_GPU_CONTAINER, ARM_CONTAINER, WINDOWS_SERVER_2022_CONTAINER Linux ARM/GPU/Windows builds Wrong type ≠ your image’s arch → won’t start
privileged-mode true / false false Any Docker build/run Off → “Cannot connect to the Docker daemon”
imagePullCredentialsType CODEBUILD / SERVICE_ROLE CODEBUILD Custom image in your ECR SERVICE_ROLE needs ECR pull perms

Caching, reports, batch, and local builds

Three features earn their keep on real projects. Cache (S3 or local Docker-layer/source/custom) skips re-downloading dependencies. Reports ingest JUnit/coverage output so pass/fail and coverage show in the console. Batch builds fan out a matrix (multiple runtimes/arch) from one project. And you can run the whole buildspec locally with the CodeBuild agent Docker image to debug without pushing.

Feature What it does How Payoff
S3 cache Persists paths (e.g. node_modules) across builds cache: { paths: [...] } + project S3 cache Faster dependency-heavy builds
Local cache DOCKER_LAYER / SOURCE / CUSTOM on the host Project cache.modes Reuse Docker layers between builds on a warm host
Reports Test/coverage visualization + history reports: group, file-format See failing tests without reading logs
Batch builds Matrix/graph of builds from one project batch: in buildspec + StartBuildBatch Test across Node 18/20/22 in one run
Local build Run the buildspec on your machine codebuild_build.sh + agent image Debug a phase failure fast, no pipeline

Useful built-in environment variables

CodeBuild injects environment variables you’ll reference constantly in a buildspec — the commit SHA for the image tag, paths, and the region/account. These are set automatically; you don’t declare them.

Variable Value Typical use
CODEBUILD_RESOLVED_SOURCE_VERSION The full commit SHA of the source Tag the image :${…:0:8} for an immutable, traceable tag
CODEBUILD_BUILD_ID project:uuid of this build Correlate logs/reports to a build
CODEBUILD_BUILD_NUMBER Monotonic build number Human-friendly build labelling
CODEBUILD_SRC_DIR Path to the primary source cd into the checkout; find files
CODEBUILD_BUILD_ARN ARN of the build Cross-reference in other AWS APIs
AWS_REGION / AWS_DEFAULT_REGION The build’s region Region-aware CLI calls
CODEBUILD_INITIATOR Who/what started it (e.g. the pipeline) Distinguish pipeline vs manual runs

Deploy actions and manual approval

Deploy provider comparison

The Deploy action’s provider decides how the artifact reaches production. For a container app you have two ECS paths: the built-in Amazon ECS provider (a rolling update driven by imagedefinitions.json) and Amazon ECS (Blue/Green) backed by CodeDeploy (traffic-shifting with appspec.yaml + taskdef.json).

Provider Deploys Input artifact needs Rollback story
Amazon ECS (rolling) New image to an ECS service imagedefinitions.json ECS circuit breaker / redeploy prior task def
ECS (Blue/Green) via CodeDeploy Shift traffic to a new task set appspec.yaml + taskdef.json + image Automatic on alarm; instant traffic re-shift
CloudFormation A stack (create/update/change set) Template + params Stack rollback on failure
CodeDeploy (EC2/on-prem) App to an instance fleet appspec.yml + bundle Redeploy last-known-good
AWS Lambda (via CFN/SAM or CodeDeploy) New function version + alias shift Package / template Alias weight rollback
Amazon S3 Files to a bucket (static site) The built site artifact Re-deploy prior artifact
Amazon EKS Manifests/Helm to a cluster Kube manifests kubectl rollout undo

ECS rolling vs blue/green — the two artifacts

The difference is what the Deploy action reads. Rolling wants a tiny imagedefinitions.json mapping container name → image URI; CodePipeline registers a new task-definition revision and calls UpdateService. Blue/green hands CodeDeploy an appspec.yaml and taskdef.json and shifts an ALB’s traffic from the old task set to the new one.

Aspect ECS rolling (Amazon ECS provider) ECS blue/green (CodeDeploy)
Artifact imagedefinitions.json appspec.yaml + taskdef.json + image detail
Mechanism New task-def revision + UpdateService New task set; ALB traffic shift
Traffic shift Gradual by min-healthy/max-percent Canary / Linear / AllAtOnce
Rollback Circuit breaker or manual redeploy Auto on CloudWatch alarm; instant
Extra cost None Second target group; brief double capacity

Manual approval gates

An Approval action pauses the run until a human approves or rejects — the human control point in an otherwise automated flow. Wire an SNS topic so approvers are notified, add a review URL (a dashboard, the change ticket) and optional comments. The action waits up to 7 days; if nobody acts, it fails and the release stops.

Field Purpose Notes
SNS topic Notify approvers Approver identity/email via the subscription
Review URL Link to what to review e.g. a staging URL or the CloudWatch dashboard
Comments Context for the approver Shows in the console and the approval record
Timeout Max wait before it fails ~7 days, then the action is rejected
Permission Who may approve codepipeline:PutApprovalResult on the pipeline

Service roles, least privilege, and cross-account

The two roles side by side

Two roles do all the work. The pipeline role orchestrates; the CodeBuild role builds. Keep them separate — CodeBuild should never be able to call UpdateService, and the pipeline should never need ECR push.

Aspect Pipeline service role CodeBuild service role
Trusted by codepipeline.amazonaws.com codebuild.amazonaws.com
Assumed by CodePipeline, to orchestrate Each build, to do work
Core permissions S3 artifact RW, KMS, StartBuild, deploy APIs, iam:PassRole Logs, S3 artifact RW, ECR, SSM/Secrets
Must NOT have ECR push, app secrets ecs:UpdateService, PassRole for deploy
Classic failure PassRole denied → deploy blocked No GetAuthorizationToken → push denied

Least-privilege permission matrix

Grant per action. The pipeline role needs to read the source, start the build, and drive the deploy — plus PassRole scoped to exactly the roles it hands off. The CodeBuild role needs logs, the artifact bucket, and ECR.

Action / need Role Key IAM actions
Use the GitHub connection Pipeline codeconnections:UseConnection on the connection ARN
Read/write artifacts Both s3:GetObject, s3:PutObject, s3:GetBucketVersioning on the artifact bucket
Use the artifact CMK Both kms:Decrypt, kms:GenerateDataKey on the key
Start the build Pipeline codebuild:StartBuild, BatchGetBuilds
Write build logs CodeBuild logs:CreateLogGroup/CreateLogStream/PutLogEvents
Auth to ECR CodeBuild ecr:GetAuthorizationToken (Resource *)
Push the image CodeBuild ecr:BatchCheckLayerAvailability, InitiateLayerUpload, UploadLayerPart, CompleteLayerUpload, PutImage on the repo ARN
Read a secret CodeBuild ssm:GetParameters / secretsmanager:GetSecretValue (+ kms:Decrypt)
Deploy to ECS Pipeline ecs:DescribeServices/DescribeTaskDefinition/RegisterTaskDefinition/UpdateService, iam:PassRole on the task+exec roles
Deploy via CFN Pipeline cloudformation:* on the stack + iam:PassRole on the stack role

Cross-account deploy

A common shape is one tools/CI account running the pipeline that deploys into dev/stage/prod accounts. Each target account holds a deploy role that the pipeline role can sts:AssumeRole; the artifact KMS key must be shareable so the target account can decrypt artifacts; the artifact bucket policy must allow the target account to read.

Piece Where it lives What it must allow
Deploy role Target account Trust the tools account; permission to do the deploy (ECS/CFN)
Pipeline role Tools account sts:AssumeRole on each deploy role
Artifact bucket policy Tools account s3:Get* for the target account principals
Artifact KMS key policy Tools account kms:Decrypt for the target account
Action roleArn Pipeline action Set to the target account’s deploy role

CodeArtifact — the package registry alongside

Not part of the release path but adjacent: AWS CodeArtifact is a managed package registry (npm, PyPI, Maven, NuGet, generic) with a domain → repository → upstream model. A build authenticates with a 12-hour token (aws codeartifact get-authorization-token) and pulls dependencies from your repo, which can proxy public npmjs/PyPI so you cache and vet third-party packages instead of pulling from the public internet on every build. Point your CodeBuild install phase at CodeArtifact when you want reproducible, audited dependencies.

Architecture at a glance

The diagram below is the exact pipeline you build in the lab, read left to right along the release path. A push to main fires a CodeConnections webhook; CodePipeline pulls the repo into its encrypted S3 artifact store as the source artifact and hands it to CodeBuild, which runs buildspec.ymlnpm test, then docker build, then docker push of an immutable :sha tag to Amazon ECR, and finally writes imagedefinitions.json as its output artifact. The run then waits at a manual-approval gate (with an SNS notification); on approve, the Deploy action registers a new task-definition revision and does a rolling update of the ECS service to the new image. The numbered badges mark the six stages where pipelines most often break — the source connection, the artifact/KMS handoff, the build phase/IAM/privileged mode, the ECR push, the approval gate, and the ECS deploy role — each narrated in the legend as symptom · confirm · fix.

CodePipeline release path: a GitHub push through a CodeConnections webhook into CodePipeline with an encrypted S3 artifact store, to a CodeBuild project that tests, docker-builds and pushes an immutable sha tag to Amazon ECR and emits imagedefinitions.json, then a manual-approval gate, then a rolling deploy to an ECS service, with numbered failure badges on the source connection, artifact and KMS handoff, build IAM and privileged mode, ECR push, approval, and ECS deploy role

Real-world scenario

LedgerLoop, a Pune fintech, shipped its statements API by hand: a senior engineer pulled main, ran docker build on a laptop, docker pushed to ECR, and edited the ECS service in the console to point at the new tag. It worked until it didn’t. One Friday a build made on a laptop with a newer base image passed locally, deployed, and crash-looped in production because a native dependency mismatched the runtime — and because the same engineer was the only one who knew the runbook and had gone home, the service was degraded for forty minutes. The retro produced one demand: no more laptop builds, no more console deploys, and an audit trail of who shipped what.

The platform engineer built a CodePipeline V2 pipeline. Source was GitHub through a CodeConnections connection, with a trigger filtered to main and filePaths including src/** and excluding docs/** so documentation edits stopped triggering redeploys. Build was a CodeBuild project on BUILD_GENERAL1_MEDIUM with privileged-mode on; the buildspec ran npm ci, npm test, built the image tagged with the commit SHA (CODEBUILD_RESOLVED_SOURCE_VERSION), pushed it to ECR, and wrote imagedefinitions.json. Between Build and Deploy sat a manual-approval action wired to an SNS topic the on-call rotation subscribed to. Deploy used the Amazon ECS provider for a rolling update.

The first pipeline run failed at Source: the connection was PENDING because nobody had finished installing the AWS Connector GitHub App — a two-minute fix in the console, and the single most common first-run failure. The second run failed in Build with denied: not authorized on docker push: the CodeBuild role had ECR push actions on the repo but not ecr:GetAuthorizationToken, which must be granted on Resource: *. Once that was fixed the build was green but the pipeline sat at the approval gate — because the SNS topic had no confirmed subscription, nobody got the email, and the run would have timed out after seven days. They subscribed the on-call address, confirmed it, and the gate started paging correctly.

The payoff came six weeks later. A change that would have crash-looped in production got its image built and pushed, then paused at the approval gate; the approver opened the review URL (a staging deployment of that exact image), saw it failing there, and rejected the approval. Production never saw it. Because the pipeline registered a new task-definition revision per deploy and the SHA tag was immutable, rolling back was UpdateService to the previous revision — one command, thirty seconds. The forty-minute Friday outage did not recur; deploys went from a scary weekly event to a boring, audited, several-times-a-day non-event, and the audit question (“who shipped build 4c1f9a and when?”) became a line in the pipeline’s execution history.

Advantages and disadvantages

Advantages Disadvantages
Fully managed — no CI servers to run, patch, or scale Less flexible than Jenkins/GitHub Actions for exotic workflows
Deep IAM + AWS-service integration (ECS, CFN, Lambda, ECR) AWS-native; awkward if your world is multi-cloud
No long-lived Git tokens — CodeConnections manages OAuth Connection handshake is a manual first-run step
Per-pipeline artifact store with encryption + audit history Artifact-name matching is finicky and error-prone
CodeBuild scales build compute per run, per-minute billing Cold build start adds latency vs a warm self-hosted runner
Manual approvals + cross-account roles built in Two-role least-privilege takes real IAM effort to get right
V2 triggers/filters/variables + execution modes V2 billing is usage-based and less predictable than V1’s flat fee

Where the AWS-native pair sits against the common alternatives:

Tool Strength Weakness Best when
CodePipeline + CodeBuild IAM-native, managed, cross-account, no servers AWS-only; less flexible; artifact-name fiddliness Your targets are AWS (ECS/EKS/Lambda/CFN)
GitHub Actions Huge marketplace, PR-native UX, multi-cloud You manage secrets/OIDC to AWS; runner ops if self-hosted Source lives in GitHub; broad ecosystem needed
GitLab CI Integrated with GitLab, powerful .gitlab-ci.yml Tied to GitLab; runner management You’re a GitLab shop
Jenkins Infinitely flexible, plugin for everything You run, patch, scale, and secure it Complex legacy/on-prem pipelines

CodePipeline + CodeBuild wins decisively when your workloads are AWS (ECS/EKS/Lambda/CloudFormation) and you want managed, IAM-native, auditable delivery without operating build infrastructure. It’s the natural choice for a team already deep in AWS that wants “one-click, least-privilege, cross-account” without standing up and securing Jenkins. Reach for GitHub Actions or GitLab CI instead when your source and issues already live there and you want tight PR-level UX, a huge marketplace of pre-built steps, or genuinely multi-cloud targets — many teams even run GitHub Actions for CI and CodePipeline for the AWS deploy. The disadvantages that bite first pipelines are almost always the fiddly ones — the connection handshake and artifact-name matching — which is exactly what the lab and playbook below dwell on.

Hands-on lab

You will build a full pipeline: Source (GitHub via CodeConnections) → Build (CodeBuild: test + docker build + push to ECR) → Manual approval → Deploy (rolling update to an ECS service) — first with the aws CLI, then the same stack in Terraform, then teardown. ⚠️ Cost note: CodeBuild bills per build-minute (a small build is a few paise), the V2 pipeline bills per action-execution-minute, and the artifact S3 bucket + KMS key cost almost nothing at this scale. Tear down when done. This lab assumes you already have an ECR repo kv-web and an ECS cluster kv-demo with a service kv-web-svc from the wave siblings.

Lab variables

Variable Example Notes
Region ap-south-1 Mumbai
Account ID 111122223333 Yours: aws sts get-caller-identity
GitHub repo your-org/kv-web Has a Dockerfile, buildspec.yml, a test
Branch main The trigger branch
ECR repo kv-web Built in the ECR sibling lab
ECS cluster / service kv-demo / kv-web-svc Built in the ECS sibling lab
Container name kv-web Must match the task def + imagedefinitions.json
Artifact bucket kv-pipeline-artifacts-111122223333 Created below
Pipeline name kv-web-pipeline V2

Step 1 — Put a buildspec.yml in the repo

Commit this buildspec.yml at the repo root. It sets the Node runtime, logs in to ECR, tags the image with the commit SHA, runs tests, builds and pushes, and writes the deploy artifact.

version: 0.2
env:
  variables:
    AWS_REGION: ap-south-1
    ECR_REPO: kv-web
    CONTAINER_NAME: kv-web
  parameter-store:
    API_BASE: /kv/web/api-base
phases:
  install:
    runtime-versions:
      nodejs: 20
    commands:
      - npm ci
  pre_build:
    commands:
      - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
      - REGISTRY=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
      - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REGISTRY
      - IMAGE_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION:0:8}
      - IMAGE_URI=$REGISTRY/$ECR_REPO:$IMAGE_TAG
  build:
    commands:
      - npm test
      - docker build -t $IMAGE_URI .
  post_build:
    commands:
      - docker push $IMAGE_URI
      - printf '[{"name":"%s","imageUri":"%s"}]' "$CONTAINER_NAME" "$IMAGE_URI" > imagedefinitions.json
reports:
  unit:
    files:
      - 'junit.xml'
    file-format: JUNITXML
artifacts:
  files:
    - imagedefinitions.json
cache:
  paths:
    - 'node_modules/**/*'

Note CODEBUILD_RESOLVED_SOURCE_VERSION (the real commit SHA), parameter-store for non-secret config, and imagedefinitions.json as the single artifact the ECS Deploy action consumes. The container name in that file must match the container name in your ECS task definition.

Step 2 — Create the CodeConnections connection (and authorize it)

aws codeconnections create-connection \
  --provider-type GitHub \
  --connection-name kv-github \
  --region ap-south-1

Expected: a ConnectionArn and ConnectionStatus: PENDING. ⚠️ A PENDING connection cannot be used yet. Open the Developer Tools → Settings → Connections console, select kv-github, click Update pending connection, and install/authorize the AWS Connector for GitHub app on your repo. Verify:

aws codeconnections get-connection --connection-arn <arn> \
  --query 'Connection.ConnectionStatus' --output text
# Expected: AVAILABLE

Step 3 — Artifact bucket and a KMS key

aws s3 mb s3://kv-pipeline-artifacts-111122223333 --region ap-south-1
aws s3api put-bucket-versioning \
  --bucket kv-pipeline-artifacts-111122223333 \
  --versioning-configuration Status=Enabled

(SSE-S3 is the default; a KMS CMK is optional for this single-account lab but required for cross-account.)

Step 4 — The two IAM roles

Create the CodeBuild role with a trust policy for codebuild.amazonaws.com, then attach a policy granting logs, artifact S3, ECR, and SSM:

aws iam create-role --role-name kv-codebuild-role \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"codebuild.amazonaws.com"},"Action":"sts:AssumeRole"}]}'

aws iam put-role-policy --role-name kv-codebuild-role --policy-name kv-build-perms \
  --policy-document '{
    "Version":"2012-10-17",
    "Statement":[
      {"Effect":"Allow","Action":["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],"Resource":"*"},
      {"Effect":"Allow","Action":["s3:GetObject","s3:PutObject","s3:GetBucketLocation"],"Resource":["arn:aws:s3:::kv-pipeline-artifacts-111122223333","arn:aws:s3:::kv-pipeline-artifacts-111122223333/*"]},
      {"Effect":"Allow","Action":"ecr:GetAuthorizationToken","Resource":"*"},
      {"Effect":"Allow","Action":["ecr:BatchCheckLayerAvailability","ecr:InitiateLayerUpload","ecr:UploadLayerPart","ecr:CompleteLayerUpload","ecr:PutImage","ecr:BatchGetImage"],"Resource":"arn:aws:ecr:ap-south-1:111122223333:repository/kv-web"},
      {"Effect":"Allow","Action":"ssm:GetParameters","Resource":"arn:aws:ssm:ap-south-1:111122223333:parameter/kv/web/*"}
    ]}'

Then the pipeline role, trusted by codepipeline.amazonaws.com, with connection use, artifact S3, StartBuild, ECS deploy, and PassRole:

aws iam create-role --role-name kv-pipeline-role \
  --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"codepipeline.amazonaws.com"},"Action":"sts:AssumeRole"}]}'

aws iam put-role-policy --role-name kv-pipeline-role --policy-name kv-pipeline-perms \
  --policy-document '{
    "Version":"2012-10-17",
    "Statement":[
      {"Effect":"Allow","Action":"codeconnections:UseConnection","Resource":"<connection-arn>"},
      {"Effect":"Allow","Action":["s3:GetObject","s3:PutObject","s3:GetBucketVersioning"],"Resource":["arn:aws:s3:::kv-pipeline-artifacts-111122223333","arn:aws:s3:::kv-pipeline-artifacts-111122223333/*"]},
      {"Effect":"Allow","Action":["codebuild:StartBuild","codebuild:BatchGetBuilds"],"Resource":"*"},
      {"Effect":"Allow","Action":["ecs:DescribeServices","ecs:DescribeTaskDefinition","ecs:DescribeTasks","ecs:ListTasks","ecs:RegisterTaskDefinition","ecs:UpdateService"],"Resource":"*"},
      {"Effect":"Allow","Action":"iam:PassRole","Resource":["arn:aws:iam::111122223333:role/kv-web-task-role","arn:aws:iam::111122223333:role/kv-web-exec-role"],"Condition":{"StringLike":{"iam:PassedToService":"ecs-tasks.amazonaws.com"}}}
    ]}'

Step 5 — The CodeBuild project

aws codebuild create-project --region ap-south-1 \
  --name kv-web-build \
  --source '{"type":"CODEPIPELINE"}' \
  --artifacts '{"type":"CODEPIPELINE"}' \
  --service-role arn:aws:iam::111122223333:role/kv-codebuild-role \
  --environment '{
    "type":"LINUX_CONTAINER",
    "image":"aws/codebuild/standard:7.0",
    "computeType":"BUILD_GENERAL1_MEDIUM",
    "privilegedMode":true
  }'

privilegedMode:true is what lets docker build run. source/artifacts of type CODEPIPELINE mean CodePipeline provides the source and collects the artifact.

Step 6 — Create the pipeline (V2)

Write pipeline.json describing four stages, then create it. Abbreviated but complete in shape:

{
  "pipeline": {
    "name": "kv-web-pipeline",
    "pipelineType": "V2",
    "executionMode": "SUPERSEDED",
    "roleArn": "arn:aws:iam::111122223333:role/kv-pipeline-role",
    "artifactStore": { "type": "S3", "location": "kv-pipeline-artifacts-111122223333" },
    "triggers": [{
      "providerType": "CodeStarSourceConnection",
      "gitConfiguration": {
        "sourceActionName": "Source",
        "push": [{ "branches": { "includes": ["main"] }, "filePaths": { "includes": ["src/**"], "excludes": ["docs/**"] } }]
      }
    }],
    "stages": [
      { "name": "Source", "actions": [{
        "name": "Source", "actionTypeId": {"category":"Source","owner":"AWS","provider":"CodeStarSourceConnection","version":"1"},
        "configuration": {"ConnectionArn":"<connection-arn>","FullRepositoryId":"your-org/kv-web","BranchName":"main","DetectChanges":"false"},
        "outputArtifacts": [{"name":"SourceOut"}] }] },
      { "name": "Build", "actions": [{
        "name": "Build", "actionTypeId": {"category":"Build","owner":"AWS","provider":"CodeBuild","version":"1"},
        "configuration": {"ProjectName":"kv-web-build"},
        "inputArtifacts": [{"name":"SourceOut"}], "outputArtifacts": [{"name":"BuildOut"}] }] },
      { "name": "Approve", "actions": [{
        "name": "ManualApproval", "actionTypeId": {"category":"Approval","owner":"AWS","provider":"Manual","version":"1"},
        "configuration": {"NotificationArn":"arn:aws:sns:ap-south-1:111122223333:kv-deploy-approvals","CustomData":"Approve prod deploy of kv-web"} }] },
      { "name": "Deploy", "actions": [{
        "name": "Deploy", "actionTypeId": {"category":"Deploy","owner":"AWS","provider":"ECS","version":"1"},
        "configuration": {"ClusterName":"kv-demo","ServiceName":"kv-web-svc","FileName":"imagedefinitions.json"},
        "inputArtifacts": [{"name":"BuildOut"}] }] }
    ]
  }
}
aws codepipeline create-pipeline --cli-input-json file://pipeline.json --region ap-south-1

Note the provider name is still CodeStarSourceConnection in the API even though the service is now CodeConnections. SourceOut and BuildOut are the artifact names — the Build’s input must equal the Source’s output, and the Deploy’s input must equal the Build’s output.

Step 7 — Trigger and watch

Push a commit to main (touching src/**), or start it manually:

aws codepipeline start-pipeline-execution --name kv-web-pipeline --region ap-south-1
aws codepipeline get-pipeline-state --name kv-web-pipeline --region ap-south-1 \
  --query 'stageStates[].{stage:stageName,status:latestExecution.status}'

Expected: SourceSucceeded, BuildInProgress then Succeeded, ApproveInProgress (waiting).

Step 8 — Approve the gate

Get the approval token, then approve:

TOKEN=$(aws codepipeline get-pipeline-state --name kv-web-pipeline \
  --query "stageStates[?stageName=='Approve'].actionStates[0].latestExecution.token" --output text)

aws codepipeline put-approval-result --pipeline-name kv-web-pipeline \
  --stage-name Approve --action-name ManualApproval \
  --result summary="LGTM",status=Approved \
  --token "$TOKEN" --region ap-south-1

Step 9 — Verify the deploy

aws ecs describe-services --cluster kv-demo --services kv-web-svc \
  --query 'services[0].deployments[].{status:status,taskDef:taskDefinition,running:runningCount}'

Expected: a PRIMARY deployment referencing a new task-definition revision whose image is the :sha you just pushed, runningCount climbing to desired.

Step 10 — Teardown

aws codepipeline delete-pipeline --name kv-web-pipeline --region ap-south-1
aws codebuild delete-project --name kv-web-build --region ap-south-1
aws codeconnections delete-connection --connection-arn <arn> --region ap-south-1
aws iam delete-role-policy --role-name kv-pipeline-role --policy-name kv-pipeline-perms
aws iam delete-role --role-name kv-pipeline-role
aws iam delete-role-policy --role-name kv-codebuild-role --policy-name kv-build-perms
aws iam delete-role --role-name kv-codebuild-role
aws s3 rb s3://kv-pipeline-artifacts-111122223333 --force

(Leave the ECR repo and ECS service if you’re keeping the sibling labs; otherwise tear those down per their guides.)

The same thing in Terraform

The identical stack as code — connection, artifact bucket, both roles (policies abbreviated), the CodeBuild project, and the four-stage V2 pipeline:

terraform {
  required_providers { aws = { source = "hashicorp/aws", version = "~> 5.0" } }
}
provider "aws" { region = "ap-south-1" }

data "aws_caller_identity" "me" {}

resource "aws_codestarconnections_connection" "github" {
  name          = "kv-github"
  provider_type = "GitHub"
}

resource "aws_s3_bucket" "artifacts" {
  bucket = "kv-pipeline-artifacts-${data.aws_caller_identity.me.account_id}"
}
resource "aws_s3_bucket_versioning" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_iam_role" "codebuild" {
  name = "kv-codebuild-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{ Effect = "Allow", Principal = { Service = "codebuild.amazonaws.com" }, Action = "sts:AssumeRole" }]
  })
}
resource "aws_iam_role_policy" "codebuild" {
  role = aws_iam_role.codebuild.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      { Effect = "Allow", Action = ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"], Resource = "*" },
      { Effect = "Allow", Action = ["s3:GetObject","s3:PutObject","s3:GetBucketLocation"], Resource = [aws_s3_bucket.artifacts.arn, "${aws_s3_bucket.artifacts.arn}/*"] },
      { Effect = "Allow", Action = "ecr:GetAuthorizationToken", Resource = "*" },
      { Effect = "Allow", Action = ["ecr:BatchCheckLayerAvailability","ecr:InitiateLayerUpload","ecr:UploadLayerPart","ecr:CompleteLayerUpload","ecr:PutImage","ecr:BatchGetImage"], Resource = "arn:aws:ecr:ap-south-1:${data.aws_caller_identity.me.account_id}:repository/kv-web" },
      { Effect = "Allow", Action = "ssm:GetParameters", Resource = "arn:aws:ssm:ap-south-1:${data.aws_caller_identity.me.account_id}:parameter/kv/web/*" }
    ]
  })
}

resource "aws_codebuild_project" "build" {
  name         = "kv-web-build"
  service_role = aws_iam_role.codebuild.arn
  source    { type = "CODEPIPELINE" }
  artifacts { type = "CODEPIPELINE" }
  environment {
    type            = "LINUX_CONTAINER"
    image           = "aws/codebuild/standard:7.0"
    compute_type    = "BUILD_GENERAL1_MEDIUM"
    privileged_mode = true
  }
}

resource "aws_iam_role" "pipeline" {
  name = "kv-pipeline-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{ Effect = "Allow", Principal = { Service = "codepipeline.amazonaws.com" }, Action = "sts:AssumeRole" }]
  })
}
resource "aws_iam_role_policy" "pipeline" {
  role = aws_iam_role.pipeline.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      { Effect = "Allow", Action = "codestar-connections:UseConnection", Resource = aws_codestarconnections_connection.github.arn },
      { Effect = "Allow", Action = ["s3:GetObject","s3:PutObject","s3:GetBucketVersioning"], Resource = [aws_s3_bucket.artifacts.arn, "${aws_s3_bucket.artifacts.arn}/*"] },
      { Effect = "Allow", Action = ["codebuild:StartBuild","codebuild:BatchGetBuilds"], Resource = aws_codebuild_project.build.arn },
      { Effect = "Allow", Action = ["ecs:DescribeServices","ecs:DescribeTaskDefinition","ecs:RegisterTaskDefinition","ecs:UpdateService","ecs:DescribeTasks","ecs:ListTasks"], Resource = "*" },
      { Effect = "Allow", Action = "iam:PassRole", Resource = "*", Condition = { StringLike = { "iam:PassedToService" = "ecs-tasks.amazonaws.com" } } }
    ]
  })
}

resource "aws_codepipeline" "pipe" {
  name          = "kv-web-pipeline"
  role_arn      = aws_iam_role.pipeline.arn
  pipeline_type = "V2"
  artifact_store { location = aws_s3_bucket.artifacts.bucket, type = "S3" }

  stage {
    name = "Source"
    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceOut"]
      configuration = {
        ConnectionArn    = aws_codestarconnections_connection.github.arn
        FullRepositoryId = "your-org/kv-web"
        BranchName       = "main"
      }
    }
  }
  stage {
    name = "Build"
    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      input_artifacts  = ["SourceOut"]
      output_artifacts = ["BuildOut"]
      configuration    = { ProjectName = aws_codebuild_project.build.name }
    }
  }
  stage {
    name = "Approve"
    action {
      name     = "ManualApproval"
      category = "Approval"
      owner    = "AWS"
      provider = "Manual"
      version  = "1"
    }
  }
  stage {
    name = "Deploy"
    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "ECS"
      version         = "1"
      input_artifacts = ["BuildOut"]
      configuration = {
        ClusterName = "kv-demo"
        ServiceName = "kv-web-svc"
        FileName    = "imagedefinitions.json"
      }
    }
  }
}

terraform apply builds it all; the connection is created PENDING, so authorize it once in the console as in Step 2. terraform destroy tears it down.

Common mistakes & troubleshooting

This is the section you will come back to. The playbook maps each real failure to the exact command that confirms it and the fix. Pipeline failures are almost always one of: the connection, an artifact name, a missing IAM action, PassRole, or privilegedMode.

# Symptom Root cause Confirm (exact command) Fix
1 Source action fails: connection is not available CodeConnections connection still PENDING (handshake never completed) aws codeconnections get-connection --connection-arn <arn> --query 'Connection.ConnectionStatus' Console → Settings → Connections → Update pending connection; authorize the AWS Connector app
2 Pipeline never triggers on push V2 trigger branch/tag/filePaths filter doesn’t match; or DetectChanges off with no webhook Check triggers in get-pipeline; push a change under src/** on main Align the filter to what you push; ensure the trigger exists
3 Build fails at pre_build/build: a phase command exits non-zero Test failed, missing dependency, or wrong command for the phase Open the build log; find Phase ... State: FAILED and the command Fix the command/test; put installs in install, login in pre_build
4 docker: Cannot connect to the Docker daemon privilegedMode is false on the project aws codebuild batch-get-projects --names kv-web-build --query 'projects[0].environment.privilegedMode' Set privilegedMode:true; re-run
5 docker pushdenied: not authorized CodeBuild role lacks ecr:GetAuthorizationToken (needs Resource:*) or repo push actions aws iam simulate-principal-policy --policy-source-arn <role> --action-names ecr:GetAuthorizationToken Grant GetAuthorizationToken on * + layer/push actions on the repo ARN
6 Build OOM-killed / very slow; exit 137 Compute type too small for the build Build log shows OOM; ... computeType is SMALL Raise to BUILD_GENERAL1_MEDIUM/LARGE
7 Build can’t read a secret: value is empty / AccessDenied Role lacks ssm:GetParameters / secretsmanager:GetSecretValue (+ kms:Decrypt) Build log at start: parameter/secret resolution error Grant the parameter/secret (and KMS) to the CodeBuild role
8 Deploy fails: artifact not found / input artifact missing Build’s outputArtifacts name ≠ Deploy’s inputArtifacts name Compare names in get-pipeline; the action config Make them identical (BuildOut == BuildOut)
9 Deploy (ECS) fails: is not authorized to perform: ecs:UpdateService or iam:PassRole Pipeline role missing ECS actions or PassRole on the task/exec roles The action error; simulate-principal-policy for iam:PassRole Add ecs:UpdateService/RegisterTaskDefinition + iam:PassRole scoped to those roles
10 Approval stage sits InProgress forever, then fails after 7 days No SNS notification, or approver lacks codepipeline:PutApprovalResult get-pipeline-state → Approve action status/token Wire an SNS topic + confirmed subscription; grant PutApprovalResult; approve/reject
11 Deploy “succeeds” but ECS runs the old image imagedefinitions.json container name ≠ the task-def container name, or wrong imageUri cat imagedefinitions.json in the build log; compare to the task def Match the container name exactly; verify the pushed :sha URI
12 AccessDenied on the artifact bucket / KMS mid-pipeline Role can’t read/write the artifact S3 bucket or use its CMK The action error names s3:* or kms:Decrypt Grant S3 RW on the bucket + kms:Decrypt/GenerateDataKey on the CMK
13 CodeBuild in a VPC times out reaching the internet / public ECR / npm Build placed in a private subnet with no NAT and no endpoints Build log: connection timeout on docker pull/npm; check the project VPC config Add a NAT gateway, or VPC endpoints for the AWS services; or remove the VPC config if not needed
14 Cross-account deploy fails: not authorized to assume role Pipeline role can’t sts:AssumeRole the target deploy role, or trust/KMS/S3 policy missing The action error; the target role’s trust policy Add sts:AssumeRole; fix the deploy role trust; share the artifact KMS key + bucket policy
15 Whole stage frozen; nothing moves A transition is disabled get-pipeline-state → the stage inboundTransitionState.enabled = false Enable the transition (enable-stage-transition)

CodePipeline action/execution status reference

Status strings you’ll read in get-pipeline-state and the console — know what each means before you “fix” a non-problem.

Status Meaning What to do
InProgress Action/stage running (or an approval waiting) Wait; for an approval, act on the gate
Succeeded Completed OK Nothing
Failed Action failed Read the action error; consult the playbook
Stopped Execution stopped (manually or superseded) Expected if you stopped it or a newer run won
Superseded A newer execution replaced this one (SUPERSEDED mode) Expected; the latest commit proceeded
Cancelled Cancelled (e.g. pipeline updated mid-run) Re-run if needed

CodeBuild phase-status reference

aws codebuild batch-get-builds shows a phases[] array; each phase has a phase-status. This localizes a build failure to a phase in one call.

phase-status Meaning Usual cause
SUCCEEDED Phase completed
FAILED A command exited non-zero Failing test, bad command, denied API call
FAULT Infrastructure fault during the phase Transient; retry
TIMED_OUT Phase exceeded the build timeout Raise timeout; speed up the phase; bigger compute
CLIENT_ERROR Problem with the request/config Bad buildspec, missing source, wrong image
IN_PROGRESS Still running Wait

The two nastiest, explained

The ECR push authorization split (row 5) is the most common build failure and the least obvious. Engineers grant the CodeBuild role push actions (PutImage, UploadLayerPart) on the repository ARN, run the build, and still get denied: not authorized — because docker login first calls ecr:GetAuthorizationToken, which is an account-level action that only accepts Resource: "*". If you scope it to the repo ARN (or forget it), login fails before push is even attempted. The two must go together: GetAuthorizationToken on *, the layer/push actions on the repo ARN.

The PassRole wall on deploy (row 9) blocks the Deploy stage even when the pipeline role looks like it has ECS permissions. The Amazon ECS provider registers a new task-definition revision, and that task def references your task role and execution role — so CodePipeline must be allowed to iam:PassRole those two roles to ecs-tasks.amazonaws.com. Miss it and the deploy fails with “is not authorized to perform: iam:PassRole.” The fix is a PassRole statement scoped to exactly those role ARNs with an iam:PassedToService condition — never a blanket PassRole on *, which is a privilege-escalation footgun an auditor will flag.

Best practices

Security notes

CI/CD is a high-value target: it can build and deploy anything, so its blast radius is your whole platform. Lock it down along five axes.

Control What to do Why
Least privilege Two scoped roles; ARNs not *; PassRole conditioned A compromised build shouldn’t be able to redeploy prod
No stored credentials CodeConnections for Git; SSM/Secrets for secrets No long-lived tokens to leak from logs or config
Encrypt artifacts KMS CMK on the artifact bucket (required cross-account) Artifacts contain your source and build output
Protect the ECR push GetAuthorizationToken on *, push actions on the repo ARN only Minimal registry write; nothing broader
Guard the deploy gate Restrict PutApprovalResult; require approval to prod Only authorized humans release
Network isolation VPC-configure builds that touch private data; endpoints for AWS APIs Keep build traffic off the public internet where possible
Audit CloudTrail on Code* APIs; pipeline execution history “Who shipped what, when, and who approved it”
Log hygiene Never echo secrets; use no-echo env; scoped log group Build logs are readable by anyone with log access

Two rules matter most. First, iam:PassRole is the crown jewel — a pipeline role with PassRole on * can pass any role to a service and effectively escalate to it; always scope it to the exact roles and add an iam:PassedToService condition. Second, the approval gate is a security control, not a formality — restrict codepipeline:PutApprovalResult to the release approvers, because whoever can approve can ship.

Cost & sizing

Three line items drive the bill: CodeBuild build-minutes (by compute type), the pipeline (V1 flat vs V2 usage), and trivial S3/KMS for artifacts. The knobs are compute size, build frequency, and caching.

Cost driver Billed as Rough figure Lever
CodeBuild (Linux small) Per build-minute ~$0.005/min on-demand; ~₹0.4/min Cache deps; smaller compute; fewer builds
CodeBuild (Linux medium) Per build-minute ~$0.01/min Right-size; don’t over-provision
CodeBuild (Linux large) Per build-minute ~$0.02/min Only for builds that need it
CodePipeline V1 Per active pipeline / month ~$1/pipeline; free if no runs that month Consolidate rarely-used pipelines
CodePipeline V2 Per action-execution-minute Usage-based; there’s a monthly free allotment Fewer/faster actions; V1 for very busy simple pipelines
Artifact S3 + KMS GB-month + requests + key Paise at this scale Lifecycle-expire old artifacts
Data transfer / NAT Per GB (if build in a private subnet + NAT) NAT ~₹4/hr + per-GB Prefer VPC endpoints; avoid a VPC build unless needed

Free tier: CodeBuild includes 100 build-minutes/month on general1.small; CodePipeline V2 has a monthly free allotment of action-execution-minutes (and V1’s first pipeline is effectively free when it doesn’t run). Sizing guidance: start every project on SMALL or MEDIUM, turn on caching, and only move up a compute tier when a build genuinely OOMs (exit 137) or its wall-clock is your bottleneck — the medium tier at ~$0.01/min handles most app builds plus a docker build comfortably. For a team doing dozens of deploys a day, the build-minutes dwarf the pipeline fee, so cache aggressively and keep images small.

Interview & exam questions

1. What is the difference between a stage and an action in CodePipeline? A stage is an ordered group of actions with a transition in front of it; an action is a single unit of work (Source, Build, Test, Deploy, Approval, or Invoke) fulfilled by a provider. Actions in a stage run in parallel unless serialized with runOrder. (DVA-C02)

2. How does data pass between stages? As named artifacts — zipped bundles stored in the pipeline’s S3 artifact store. An action declares output artifacts (what it produces) and input artifacts (what it consumes); an action’s input name must exactly match a prior action’s output name, or you get “artifact not found.” (DVA-C02)

3. How do you connect a pipeline to GitHub today, and what changed from the old way? Use a CodeConnections (formerly CodeStar Connections) connection — a managed OAuth link authorized by installing the AWS Connector app — referenced by ARN with codeconnections:UseConnection. It replaces storing a personal access token or a per-pipeline webhook. (DVA-C02/SAA-C03)

4. Why can’t you create a new CodeCommit repository? AWS CodeCommit is closed to new customers as of mid-2024; only accounts already using it can create repos. New pipelines should source from GitHub/GitLab/Bitbucket via CodeConnections. (SAA-C03)

5. What does privileged-mode do in CodeBuild and when do you need it? It lets the build container run the Docker daemon, which is required to run docker build/docker run inside CodeBuild. Without it you get “Cannot connect to the Docker daemon.” (DVA-C02)

6. Where do the phases of a buildspec run, and what runs on failure? installpre_buildbuildpost_build, in order; if a phase command exits non-zero the build fails and later phases are skipped — except finally blocks within a phase, which always run (use them for cleanup). (DVA-C02)

7. How does the ECS (rolling) deploy action know what to deploy? It reads imagedefinitions.json — a JSON array mapping each container name to an image URI — from its input artifact, registers a new task-definition revision, and calls UpdateService. The container name must match the task definition. (DVA-C02)

8. Contrast ECS rolling with ECS blue/green deploys in a pipeline. Rolling uses the Amazon ECS provider with imagedefinitions.json and shifts via min-healthy/max-percent; blue/green uses CodeDeploy with appspec.yaml+taskdef.json, creates a new task set, and shifts ALB traffic (canary/linear/all-at-once) with automatic alarm-based rollback. (DVA-C02/SAA-C03)

9. What are the two service roles and the classic failure of each? The pipeline role (trusted by codepipeline.amazonaws.com) orchestrates and must iam:PassRole the deploy targets — its classic failure is PassRole denied. The CodeBuild role (trusted by codebuild.amazonaws.com) builds and pushes — its classic failure is missing ecr:GetAuthorizationToken on *. (DVA-C02/SCS-C02)

10. What do V2 pipelines add over V1? Git triggers with branch/tag/PR/filePaths filters, pipeline-level variables, and execution modes (SUPERSEDED/QUEUED/PARALLEL); V2 is billed per action-execution-minute rather than a flat monthly fee. (DVA-C02)

11. A build fails to reach the internet to docker pull a base image — what’s likely and how do you confirm? The CodeBuild project is VPC-configured into a private subnet with no NAT gateway and no VPC endpoints, so it has no egress. Confirm via the project’s vpcConfig and a connection-timeout in the log; fix with a NAT gateway or the right VPC endpoints (or remove the VPC config if the build doesn’t need private access). (SOA-C02/ANS-C01)

12. How do you do a safe, audited production deploy in a pipeline? Insert a manual-approval action before the Deploy stage, wired to an SNS topic and a review URL; restrict codepipeline:PutApprovalResult to release approvers; deploy immutable SHA-tagged images with the circuit breaker (or blue/green) on for automatic rollback. (DVA-C02/SCS-C02)

Quick check

  1. Build writes an output artifact named BuildOut; the Deploy action reads build_out. What happens, and what’s the fix?
  2. Your docker push in CodeBuild returns denied: not authorized even though the role has PutImage on the repo. What’s missing?
  3. The pipeline never runs when you push a docs change but should run for code — what V2 feature is doing this and is it a bug?
  4. The Deploy (ECS) stage fails with iam:PassRole not authorized. Which role needs what?
  5. An approval action has sat InProgress for two days. What are the two most likely reasons nobody has approved?

Answers

  1. The run fails with “artifact not found” — input and output artifact names must match exactly. Rename one so both are BuildOut (names are literal strings).
  2. ecr:GetAuthorizationToken on Resource: "*". docker login needs it before any push; scoping it to the repo ARN (or omitting it) fails login.
  3. V2 trigger filePaths filtering (include src/**, exclude docs/**). It’s working as intended — the docs-only commit didn’t match the include filter.
  4. The pipeline role needs iam:PassRole scoped to the ECS task role and execution role (with an iam:PassedToService: ecs-tasks.amazonaws.com condition), because the new task-def revision references them.
  5. No SNS notification (or an unconfirmed subscription) so nobody was told, and/or the intended approver lacks codepipeline:PutApprovalResult. It will fail after ~7 days if untouched.

Glossary

Term Definition
CodePipeline AWS’s managed release orchestrator modeling delivery as stages of actions.
CodeBuild Managed build service that runs buildspec.yml in a container and bills per build-minute.
Stage An ordered group of actions in a pipeline, fronted by a transition.
Action A single unit of work (Source/Build/Test/Deploy/Approval/Invoke) fulfilled by a provider.
Artifact A zipped bundle of files passed between actions via the S3 artifact store, matched by name.
Artifact store The per-pipeline S3 bucket (SSE-S3 or KMS) holding all artifacts.
CodeConnections Managed OAuth link (formerly CodeStar Connections) to GitHub/GitLab/Bitbucket.
buildspec.yml The YAML build recipe: phases, env, artifacts, reports, cache.
Privileged mode CodeBuild setting that enables the Docker daemon so docker build works.
Compute type CodeBuild’s vCPU/memory size (BUILD_GENERAL1_SMALL2XLARGE, Lambda tiers).
imagedefinitions.json The artifact the ECS rolling deploy reads: container name → image URI.
Manual approval An action that pauses the run for a human decision (up to ~7 days), optionally SNS-notified.
Pipeline role The IAM role CodePipeline assumes to orchestrate; needs iam:PassRole.
CodeBuild role The IAM role each build assumes to log, read artifacts, and push to ECR.
Execution mode V2 concurrency: SUPERSEDED, QUEUED, or PARALLEL.
CodeArtifact Managed package registry (npm/PyPI/Maven/NuGet) with domain→repo→upstream.

Next steps

AWSCodePipelineCodeBuildCI/CDECRECSbuildspecTerraform
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading