Crash Courses · PhD
DevOps on AWS — CloudFormation, CDK & CloudWatch
Infrastructure as code, CI/CD pipelines, and production observability
19 min read+250 XP on completionCert: AWS Crash Course
Tap any word in the text below to start reading from there.
DevOps on AWS — CloudFormation, CDK & CloudWatch
Why Infrastructure as Code
Clicking through the AWS console creates infrastructure nobody can reproduce. Infrastructure as Code (IaC) means:
- Reproducibility — run the same template in dev, staging, prod
- Version control — infra changes go through pull requests
- Auditability — what changed and when is in git history
- Automation — pipelines deploy infra changes automatically
CDK — Infrastructure in TypeScript
CDK lets you write AWS infrastructure in TypeScript (or Python, Java, etc.) with full IDE support, type safety, and reuse.
import * as cdk from 'aws-cdk-lib'
import * as s3 from 'aws-cdk-lib/aws-s3'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
export class MyAppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// S3 bucket for uploads
const bucket = new s3.Bucket(this, 'UploadsBucket', {
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
})
// Lambda function
const handler = new lambda.Function(this, 'ApiHandler', {
runtime: lambda.Runtime.NODEJS_20_X,
code: lambda.Code.fromAsset('lambda'),
handler: 'index.handler',
environment: {
BUCKET_NAME: bucket.bucketName,
},
})
// Grant Lambda read/write access to the bucket
bucket.grantReadWrite(handler)
// API Gateway in front of Lambda
new apigateway.RestApi(this, 'Api', {
defaultIntegration: new apigateway.LambdaIntegration(handler),
})
}
}cdk synth # Generate CloudFormation template (inspect what will be created)
cdk diff # Show changes vs. deployed stack
cdk deploy # Deploy the stackCI/CD with GitHub Actions → AWS
The most common pipeline pattern in 2024: GitHub Actions triggers on push, builds and pushes a Docker image to ECR, then deploys to ECS Fargate.
# .github/workflows/deploy.yml
name: Deploy to ECS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-role
aws-region: us-east-1
- name: Login to ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push image
run: |
IMAGE=${{ steps.ecr-login.outputs.registry }}/my-app:${{ github.sha }}
docker build -t $IMAGE .
docker push $IMAGE
echo "IMAGE=$IMAGE" >> $GITHUB_ENV
- name: Deploy to ECS
run: |
aws ecs update-service \
--cluster my-cluster \
--service my-service \
--force-new-deploymentOIDC for GitHub Actions — use IAM roles with OIDC instead of long-lived access keys. GitHub Actions requests a short-lived token from AWS; no secrets stored in GitHub.
CloudWatch Observability
The three pillars of observability: metrics, logs, traces.
Structured logging from Lambda/Node.js:
export const handler = async (event) => {
console.log(JSON.stringify({
level: 'INFO',
requestId: event.requestContext?.requestId,
path: event.rawPath,
message: 'Request received',
}))
try {
const result = await processRequest(event)
return { statusCode: 200, body: JSON.stringify(result) }
} catch (err) {
console.error(JSON.stringify({
level: 'ERROR',
message: err.message,
stack: err.stack,
}))
return { statusCode: 500, body: 'Internal Server Error' }
}
}CloudWatch Alarms — essential alarms for production:
Lambda Error Rate > 1% → PagerDuty/SNS alert
Lambda P99 Duration > 3s → Slack alert
RDS CPU > 80% → Slack alert
ALB 5xx Rate > 0.1% → PagerDuty alert
SQS Queue Depth > 1000 → Slack alert (processing backed up)Log Insights query for error analysis:
fields @timestamp, level, message, requestId
| filter level = "ERROR"
| sort @timestamp desc
| limit 20Reading progress
0% read
In this module
CloudFormation
CDK (Cloud Development Kit)
Stack
CloudWatch
0%