Deploying software should be boring. Not “boring because nothing works and nobody cares” boring. Boring like a commercial airline flight. Hundreds of safety checks happen automatically, the process is the same every time, and everyone lands safely without thinking about it. When deployment is exciting, it means something isn’t automated enough.
At Kids Learn, pushing code to production takes exactly 37 minutes: 2 minutes to merge a PR, 10 minutes for CodeBuild to run tests and build artifacts, 5 minutes for CDK to synthesize CloudFormation, 15 minutes for deployment to staging and integration tests, and 5 minutes for me to click “approve” and watch the production deployment complete. The entire process is automated except for that one approval click. And the person clicking isn’t making judgement calls about the deployment — they’re confirming that the staging integration tests passed and the monitoring dashboard looks clean.
This is Part 7 of the AWS Full-Stack Mastery series. We’re automating the deployment of everything we’ve built in Parts 2-6.
The Pipeline Architecture
GitHub (Source)
│
▼
CodeBuild (Build + Test)
│ npm ci, lint, unit tests, build
│ CDK synth → CloudFormation templates
│ Docker build → ECR push
│
▼
Deploy → Dev Stage
│ CloudFormation deployment
│ Lambda functions updated
│ Fargate service updated (blue/green)
│
▼
Deploy → Staging Stage
│ Same as Dev, different account
│
▼
Integration Tests (CodeBuild)
│ API endpoint tests
│ Database connectivity
│ AI service health checks
│
▼
Manual Approval
│ Slack notification sent
│ Tech Lead reviews staging metrics
│
▼
Deploy → Production
│ Blue/green deployment
│ CloudWatch alarms monitored
│ Automatic rollback on error
CDK Pipelines — The Self-Mutating Pipeline
CDK Pipelines is a remarkable construct. The pipeline deploys itself. When you change the pipeline definition in your CDK code, the next run updates the pipeline before deploying your application. No chicken-and-egg problem.
Complete Pipeline Implementation
// lib/stacks/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as pipelines from 'aws-cdk-lib/pipelines';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as snsSubscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import * as chatbot from 'aws-cdk-lib/aws-chatbot';
import { Construct } from 'constructs';
import { ApplicationStage } from '../stages/application-stage';
import { environments } from '../config/environments';
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// =========================================
// Notification Topic
// =========================================
const pipelineTopic = new sns.Topic(this, 'PipelineTopic', {
topicName: 'kidslearn-pipeline-notifications',
});
pipelineTopic.addSubscription(
new snsSubscriptions.EmailSubscription('devops@kidslearn.app')
);
// =========================================
// Pipeline Definition
// =========================================
const pipeline = new pipelines.CodePipeline(this, 'Pipeline', {
pipelineName: 'KidsLearn-Infrastructure',
crossAccountKeys: true, // Enable cross-account deployments
synth: new pipelines.ShellStep('Synth', {
input: pipelines.CodePipelineSource.gitHub(
'kidslearn/infrastructure',
'main',
{
authentication: cdk.SecretValue.secretsManager('github-token'),
trigger: pipelines.GitHubTrigger.WEBHOOK,
}
),
commands: [
'npm ci',
'npm run lint',
'npm run test -- --ci',
'npx cdk synth',
],
primaryOutputDirectory: 'cdk.out',
// Build environment
buildEnvironment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
privileged: true, // Required for Docker builds
computeType: codebuild.ComputeType.MEDIUM,
environmentVariables: {
CDK_DEV_ACCOUNT: { value: environments.dev.account },
CDK_STAGING_ACCOUNT: { value: environments.staging.account },
CDK_PROD_ACCOUNT: { value: environments.production.account },
},
},
}),
// Docker support for Fargate images
dockerEnabledForSynth: true,
dockerEnabledForSelfMutation: true,
// CodeBuild options
codeBuildDefaults: {
buildEnvironment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
computeType: codebuild.ComputeType.MEDIUM,
},
timeout: cdk.Duration.minutes(30),
},
});
// =========================================
// Dev Stage — Auto-deploy
// =========================================
pipeline.addStage(new ApplicationStage(this, 'Dev', {
config: environments.dev,
env: { account: environments.dev.account, region: environments.dev.region },
}));
// =========================================
// Staging Stage — Deploy + Integration Tests
// =========================================
const stagingStage = pipeline.addStage(new ApplicationStage(this, 'Staging', {
config: environments.staging,
env: { account: environments.staging.account, region: environments.staging.region },
}));
stagingStage.addPost(
new pipelines.ShellStep('IntegrationTests', {
commands: [
'npm ci',
'npm run test:integration',
],
envFromCfnOutputs: {
// API URL from the compute stack output
API_URL: stagingStage.apiUrlOutput,
},
})
);
// =========================================
// Production Stage — Manual Approval + Deploy
// =========================================
pipeline.addStage(new ApplicationStage(this, 'Production', {
config: environments.production,
env: { account: environments.production.account, region: environments.production.region },
}), {
pre: [
new pipelines.ManualApprovalStep('PromoteToProduction', {
comment: 'Staging integration tests passed. Review CloudWatch dashboards before approving.',
}),
],
});
}
}
CodeBuild — Build and Test Configuration
The buildspec for Application Code
# deployments/buildspec-app.yml
version: 0.2
phases:
install:
runtime-versions:
nodejs: 20
commands:
- npm ci --cache .npm --prefer-offline
pre_build:
commands:
- echo "Running linting..."
- npm run lint
- echo "Running unit tests..."
- npm run test -- --ci --coverage
build:
commands:
- echo "Building application..."
- npm run build
# Build Docker image for adaptive engine
- echo "Building Docker image..."
- cd src/fargate/adaptive-engine
- docker build -t $ECR_REPO_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION .
- docker tag $ECR_REPO_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION $ECR_REPO_URI:latest
post_build:
commands:
# Push Docker image to ECR
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REPO_URI
- docker push $ECR_REPO_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION
- docker push $ECR_REPO_URI:latest
# Generate image definitions for ECS
- printf '[{"name":"adaptive-engine","imageUri":"%s"}]' $ECR_REPO_URI:$CODEBUILD_RESOLVED_SOURCE_VERSION > imagedefinitions.json
reports:
unit-test-reports:
files:
- 'coverage/clover.xml'
file-format: CLOVERXML
artifacts:
files:
- imagedefinitions.json
- 'cdk.out/**/*'
cache:
paths:
- '.npm/**/*'
- 'node_modules/**/*'
ECR — Container Registry
ECR Repository with CDK
// lib/constructs/ecr-repository.ts
import * as cdk from 'aws-cdk-lib';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import { Construct } from 'constructs';
export class KidsLearnECR extends Construct {
public readonly repository: ecr.Repository;
constructor(scope: Construct, id: string) {
super(scope, id);
this.repository = new ecr.Repository(this, 'AdaptiveEngineRepo', {
repositoryName: 'kidslearn/adaptive-engine',
// Image scanning on push
imageScanOnPush: true,
// Lifecycle rules to control costs
lifecycleRules: [
{
description: 'Keep only 10 tagged images',
tagStatus: ecr.TagStatus.TAGGED,
tagPrefixList: ['v'],
maxImageCount: 10,
},
{
description: 'Remove untagged images after 7 days',
tagStatus: ecr.TagStatus.UNTAGGED,
maxImageAge: cdk.Duration.days(7),
},
],
// Encryption
encryption: ecr.RepositoryEncryption.AES_256,
});
}
}
Blue/Green Deployments for Fargate
When we deploy a new version of the adaptive engine, we don’t just replace the running containers. We use blue/green deployment:
- New tasks launch with the updated container image
- Health checks pass on the new tasks
- Load balancer shifts traffic from old tasks to new tasks
- Old tasks drain existing connections and terminate
- If anything fails, traffic shifts back to the old tasks automatically
// In the compute stack — ECS deployment configuration
import * as ecs from 'aws-cdk-lib/aws-ecs';
// Configure rolling deployment with circuit breaker
const service = new ecs.FargateService(this, 'AdaptiveService', {
cluster,
taskDefinition,
desiredCount: 2,
deploymentController: {
type: ecs.DeploymentControllerType.ECS,
},
// Rolling update configuration
minHealthyPercent: 100, // Keep all old tasks running until new ones are healthy
maxHealthyPercent: 200, // Allow double the tasks during deployment
// Circuit breaker — automatic rollback on failure
circuitBreaker: {
rollback: true,
},
// Deployment alarms — roll back if errors spike
deploymentAlarms: {
alarmNames: ['HighErrorRate', 'HighLatency'],
behavior: ecs.AlarmBehavior.ROLLBACK_ON_ALARM,
},
});
Database Migrations in the Pipeline
We run database migrations as a CodeBuild step after infrastructure deployment:
# deployments/buildspec-migrate.yml
version: 0.2
phases:
install:
runtime-versions:
nodejs: 20
commands:
- npm ci
build:
commands:
- echo "Running database migrations..."
- npx ts-node scripts/run-migrations.ts
- echo "Migrations completed successfully"
post_build:
commands:
- echo "Verifying migration status..."
- npx ts-node scripts/verify-migrations.ts
The Bottom Line
The CI/CD pipeline transforms deployment from a manual, error-prone process into an automated, auditable workflow:
- CodePipeline orchestrates the entire flow from Git push to production
- CodeBuild handles builds, tests, and Docker image creation
- ECR stores container images with vulnerability scanning
- CDK Pipelines self-mutates — the pipeline updates itself
- Blue/green deployments ensure zero-downtime releases
- Circuit breakers automatically roll back failed deployments
In Part 8, we implement security — IAM least privilege, Cognito authentication with COPPA compliance, WAF, and secrets management.
See you in Part 8.
This is Part 7 of a 10-part series: AWS Full-Stack Mastery for Technical Leads.
Series outline:
- Why AWS & Getting Started (Part 1)
- Infrastructure as Code (CDK) (Part 2)
- Frontend (Amplify + CloudFront) (Part 3)
- Backend (API Gateway + Lambda + Fargate) (Part 4)
- Database (Aurora + DynamoDB + ElastiCache) (Part 5)
- AI/ML (Bedrock + SageMaker) (Part 6)
- DevOps (CodePipeline + CodeBuild) (this post)
- Security (IAM + Cognito + WAF) (Part 8)
- Observability (CloudWatch + X-Ray) (Part 9)
- Production (Multi-Region + DR) (Part 10)
References
- AWS CodePipeline User Guide — Pipeline orchestration and configuration.
- CDK Pipelines — Self-mutating CI/CD pipelines with CDK.
- CodeBuild User Guide — Build project configuration and buildspec reference.
- ECR User Guide — Container image registry management.
- ECS Blue/Green Deployments — Zero-downtime deployment strategies.
- ECS Deployment Circuit Breaker — Automatic rollback on deployment failures.
- CodePipeline Cross-Account — Multi-account pipeline configuration.
- ECR Image Scanning — Automated vulnerability scanning.
- CodeBuild Caching — Build acceleration with caching.
- GitHub Actions vs. CodePipeline — GitHub integration options.