Skip to main content

CI/CD on AWS — CodePipeline, CodeBuild, and CodeDeploy

· 7 min read
Goel Academy
DevOps & Cloud Learning Hub

A developer pushes to main. Twenty minutes later, the changes are live in production — tested, built, deployed, and verified. No one SSH'd into a server. No one clicked a button in the console. No one held their breath. That's what CI/CD should feel like. AWS provides a full suite of developer tools to build this pipeline natively, and understanding how they fit together saves you from the "it works on my machine" disaster.

The AWS Developer Tools Suite

AWS splits CI/CD into specialized services that snap together:

ServiceRoleAnalogy
CodeCommitGit repository hostingGitHub/GitLab (repo only)
CodeBuildBuild and testGitHub Actions runner / Jenkins agent
CodeDeployApplication deploymentAnsible / deployment scripts
CodePipelineOrchestration (glue)GitHub Actions workflow / Jenkins pipeline
CodeArtifactPackage repositorynpm registry / PyPI / Maven Central

You don't have to use all of them. Most teams use GitHub for source, CodeBuild for builds, and CodePipeline for orchestration. Mix and match based on what works.

CodeBuild — Build and Test

CodeBuild runs your build in a managed container. No servers to manage, no Jenkins to maintain. You define the build steps in a buildspec.yml file in your repository root:

# buildspec.yml
version: 0.2

env:
variables:
NODE_ENV: "production"
parameter-store:
DB_PASSWORD: "/myapp/prod/db-password"

phases:
install:
runtime-versions:
nodejs: 20
commands:
- echo "Installing dependencies..."
- npm ci

pre_build:
commands:
- echo "Running linter..."
- npm run lint
- echo "Running unit tests..."
- npm test -- --coverage

build:
commands:
- echo "Building application..."
- npm run build
- echo "Building Docker image..."
- docker build -t myapp:$CODEBUILD_RESOLVED_SOURCE_VERSION .
- docker tag myapp:$CODEBUILD_RESOLVED_SOURCE_VERSION 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
- docker tag myapp:$CODEBUILD_RESOLVED_SOURCE_VERSION 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:$CODEBUILD_RESOLVED_SOURCE_VERSION

post_build:
commands:
- echo "Pushing Docker image to ECR..."
- aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
- docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
- docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:$CODEBUILD_RESOLVED_SOURCE_VERSION
- echo "Writing image definitions for CodeDeploy..."
- printf '[{"name":"myapp","imageUri":"123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:%s"}]' $CODEBUILD_RESOLVED_SOURCE_VERSION > imagedefinitions.json

artifacts:
files:
- imagedefinitions.json
- appspec.yml
- scripts/**/*

reports:
jest-reports:
files:
- "coverage/clover.xml"
file-format: CLOVERXML

cache:
paths:
- "node_modules/**/*"
- "/root/.npm/**/*"

Create the build project:

# Create a CodeBuild project
aws codebuild create-project \
--name myapp-build \
--source '{
"type": "GITHUB",
"location": "https://github.com/myorg/myapp.git",
"buildspec": "buildspec.yml"
}' \
--environment '{
"type": "LINUX_CONTAINER",
"image": "aws/codebuild/amazonlinux2-x86_64-standard:5.0",
"computeType": "BUILD_GENERAL1_MEDIUM",
"privilegedMode": true,
"environmentVariables": [
{"name": "AWS_DEFAULT_REGION", "value": "us-east-1"},
{"name": "AWS_ACCOUNT_ID", "value": "123456789012"}
]
}' \
--artifacts '{"type": "S3", "location": "myapp-build-artifacts"}' \
--service-role "arn:aws:iam::123456789012:role/codebuild-role" \
--cache '{"type": "S3", "location": "myapp-build-cache/cache"}'

Build caching is critical for performance. Without it, npm ci downloads everything from scratch on every build. With S3 caching, subsequent builds reuse node_modules and cut build times by 50-70%.

CodeDeploy — Deployment Strategies

CodeDeploy handles rolling out your application to EC2 instances, ECS services, or Lambda functions. It supports two strategies:

In-Place Deployment (EC2)

Stops the application on each instance, deploys the new version, and restarts:

# appspec.yml (for EC2 deployments)
version: 0.0
os: linux
files:
- source: /
destination: /opt/myapp

hooks:
BeforeInstall:
- location: scripts/stop_server.sh
timeout: 30
AfterInstall:
- location: scripts/install_dependencies.sh
timeout: 120
ApplicationStart:
- location: scripts/start_server.sh
timeout: 30
ValidateService:
- location: scripts/health_check.sh
timeout: 60
#!/bin/bash
# scripts/health_check.sh
for i in {1..10}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health)
if [ "$STATUS" == "200" ]; then
echo "Health check passed"
exit 0
fi
echo "Attempt $i: status $STATUS, retrying in 5s..."
sleep 5
done
echo "Health check failed after 10 attempts"
exit 1

Blue/Green Deployment (ECS)

Creates a new task set (green), shifts traffic gradually, and terminates the old task set (blue) after verification:

# Create a CodeDeploy application for ECS
aws deploy create-application \
--application-name myapp-ecs \
--compute-platform ECS

# Create deployment group with blue/green config
aws deploy create-deployment-group \
--application-name myapp-ecs \
--deployment-group-name production \
--service-role-arn arn:aws:iam::123456789012:role/codedeploy-role \
--deployment-config-name CodeDeployDefault.ECSLinear10PercentEvery1Minutes \
--ecs-services '[{
"serviceName": "myapp-service",
"clusterName": "production"
}]' \
--load-balancer-info '{
"targetGroupPairInfoList": [{
"targetGroups": [
{"name": "myapp-tg-blue"},
{"name": "myapp-tg-green"}
],
"prodTrafficRoute": {
"listenerArns": ["arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/myapp-alb/abc123/def456"]
}
}]
}' \
--blue-green-deployment-configuration '{
"terminateBlueInstancesOnDeploymentSuccess": {
"action": "TERMINATE",
"terminationWaitTimeInMinutes": 5
},
"deploymentReadyOption": {
"actionOnTimeout": "CONTINUE_DEPLOYMENT",
"waitTimeInMinutes": 0
}
}'
Deployment StrategyDowntimeRollback SpeedRiskBest For
AllAtOnceYes (brief)Slow (redeploy)HighDev/test
OneAtATimeNo (rolling)MediumLowSmall fleets
HalfAtATimeNo (rolling)MediumMediumBalanced
Blue/GreenNoInstant (route switch)LowestProduction
Canary (10% then 90%)NoInstantVery LowCritical services
Linear (10% every 1min)NoInstantVery LowGradual rollout

CodePipeline — Orchestrating Everything

CodePipeline connects Source, Build, Test, and Deploy stages into an automated workflow:

# Create a complete pipeline
aws codepipeline create-pipeline --pipeline '{
"name": "myapp-pipeline",
"roleArn": "arn:aws:iam::123456789012:role/codepipeline-role",
"stages": [
{
"name": "Source",
"actions": [{
"name": "GitHub-Source",
"actionTypeId": {
"category": "Source",
"owner": "ThirdParty",
"provider": "GitHub",
"version": "1"
},
"configuration": {
"Owner": "myorg",
"Repo": "myapp",
"Branch": "main",
"OAuthToken": "{{resolve:secretsmanager:github-token}}"
},
"outputArtifacts": [{"name": "SourceOutput"}]
}]
},
{
"name": "Build",
"actions": [{
"name": "CodeBuild",
"actionTypeId": {
"category": "Build",
"owner": "AWS",
"provider": "CodeBuild",
"version": "1"
},
"configuration": {
"ProjectName": "myapp-build"
},
"inputArtifacts": [{"name": "SourceOutput"}],
"outputArtifacts": [{"name": "BuildOutput"}]
}]
},
{
"name": "Deploy-Staging",
"actions": [{
"name": "Deploy-ECS-Staging",
"actionTypeId": {
"category": "Deploy",
"owner": "AWS",
"provider": "ECS",
"version": "1"
},
"configuration": {
"ClusterName": "staging",
"ServiceName": "myapp-staging",
"FileName": "imagedefinitions.json"
},
"inputArtifacts": [{"name": "BuildOutput"}]
}]
},
{
"name": "Approval",
"actions": [{
"name": "Manual-Approval",
"actionTypeId": {
"category": "Approval",
"owner": "AWS",
"provider": "Manual",
"version": "1"
},
"configuration": {
"NotificationArn": "arn:aws:sns:us-east-1:123456789012:deploy-approvals",
"CustomData": "Review staging at https://staging.example.com"
}
}]
},
{
"name": "Deploy-Production",
"actions": [{
"name": "Deploy-ECS-Prod",
"actionTypeId": {
"category": "Deploy",
"owner": "AWS",
"provider": "CodeDeployToECS",
"version": "1"
},
"configuration": {
"ApplicationName": "myapp-ecs",
"DeploymentGroupName": "production",
"TaskDefinitionTemplateArtifact": "BuildOutput",
"AppSpecTemplateArtifact": "BuildOutput"
},
"inputArtifacts": [{"name": "BuildOutput"}]
}]
}
],
"artifactStore": {
"type": "S3",
"location": "myapp-pipeline-artifacts"
}
}'

This pipeline: pulls source from GitHub, builds and tests with CodeBuild, deploys to staging ECS, waits for manual approval, then deploys to production using blue/green via CodeDeploy.

CodeArtifact — Private Package Repository

CodeArtifact hosts your private npm, PyPI, Maven, or NuGet packages. It also proxies public registries so all packages come through a single controlled source:

# Create a CodeArtifact domain and repository
aws codeartifact create-domain --domain mycompany

aws codeartifact create-repository \
--domain mycompany \
--repository internal-packages \
--description "Internal npm packages"

# Connect npm to CodeArtifact
aws codeartifact login \
--tool npm \
--domain mycompany \
--repository internal-packages

# Now npm install pulls from CodeArtifact (which proxies to npmjs.org)
npm install

Comparison: AWS vs GitHub Actions vs Jenkins

FeatureAWS CodePipelineGitHub ActionsJenkins
HostingManagedManagedSelf-hosted
SourceCodeCommit, GitHub, S3GitHub onlyAny
BuildCodeBuildGitHub runnersAny agent
DeployCodeDeploy (blue/green, canary)Custom scriptsPlugins
Cost$1/pipeline/month + build minutesFree tier + minutesServer costs
IAM integrationNativeOIDC federationPlugin
Cross-accountNative (assume roles)Manual (OIDC)Manual
SecretsSecrets Manager, Parameter StoreGitHub SecretsCredentials plugin
Best forAWS-heavy, multi-account, ECS/EKSGitHub-centric workflowsComplex/custom pipelines

GitHub Actions is simpler for standard workflows. CodePipeline shines when you need native AWS integration, cross-account deployments, and CodeDeploy's blue/green strategies. Jenkins is for teams with complex requirements that neither managed service handles.

Cross-Account Deployments

In a multi-account setup, your pipeline runs in a CI/CD account and deploys to staging and production accounts:

# In the production account: Create a role that CodePipeline can assume
aws iam create-role \
--role-name CodePipelineCrossAccountRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "sts:AssumeRole"
}]
}'

# Attach ECS deploy permissions
aws iam attach-role-policy \
--role-name CodePipelineCrossAccountRole \
--policy-arn arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS

# In the pipeline account: Add the cross-account role to the deploy action
# The pipeline assumes this role when executing the production deploy stage

The pipeline account has no direct access to production resources. It assumes a scoped role for each deployment, and that role can be revoked instantly if compromised.

What's Next

With a complete CI/CD pipeline, your code flows from commit to production automatically. This wraps up our core AWS series covering infrastructure, networking, security, governance, and deployment. Keep building, keep automating, and keep questioning the defaults — that's where the real improvements hide.