| name | deployment-aws-canary-setup |
| description | Step-by-step guide for setting up AWS Lambda canary deployments with CDK, CloudWatch alarms, and automated rollback. |
Skill: AWS Canary Deployment Setup
This skill teaches you how to set up progressive canary deployments for AWS Lambda using CDK TypeScript. You'll configure Lambda versions and aliases, CloudWatch alarms for automated rollback, and CodeDeploy deployment groups for traffic shifting.
Canary deployments reduce deployment risk by routing a small percentage of traffic to new code first. If the new version performs well (low errors, acceptable latency), traffic gradually shifts until 100% reaches the new version. If metrics fail, traffic automatically rolls back to the stable version.
This approach is essential for production workloads where zero-downtime deployments and automatic failure recovery are critical requirements.
Prerequisites
- AWS CDK v2 installed (
npm install -g aws-cdk)
- TypeScript project with CDK configured
- Lambda function deployed with proper IAM roles
- CloudWatch configured for Lambda metrics
- Understanding of Lambda versions and aliases
Overview
In this skill, you will:
- Create Lambda function with versioning and alias
- Configure CloudWatch alarms for error rate and latency
- Set up CodeDeploy deployment group with canary config
- Create CDK stack with all deployment infrastructure
- Implement rollback procedures
- Add monitoring dashboard for canary metrics
Step 1: Create Lambda with Versioning and Alias
Lambda versions are immutable snapshots of your function code and configuration. Aliases are mutable pointers that can route traffic between versions.
CDK Lambda Construct with Versioning
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
import { Construct } from 'constructs';
export interface VersionedLambdaProps {
functionName: string;
description: string;
codePath: string;
handler: string;
runtime: lambda.Runtime;
memorySize?: number;
timeout?: cdk.Duration;
environment?: { [key: string]: string };
reservedConcurrentExecutions?: number;
}
export class VersionedLambda extends Construct {
public readonly function: lambda.Function;
public readonly alias: lambda.Alias;
public readonly currentVersion: lambda.Version;
constructor(scope: Construct, id: string, props: VersionedLambdaProps) {
super(scope, id);
this.function = new lambda.Function(this, 'Function', {
functionName: props.functionName,
description: props.description,
runtime: props.runtime,
handler: props.handler,
code: lambda.Code.fromAsset(props.codePath),
memorySize: props.memorySize ?? 256,
timeout: props.timeout ?? cdk.Duration.seconds(30),
environment: props.environment,
tracing: lambda.Tracing.ACTIVE,
reservedConcurrentExecutions: props.reservedConcurrentExecutions,
currentVersionOptions: {
removalPolicy: cdk.RemovalPolicy.RETAIN,
description: `Version deployed at ${new Date().toISOString()}`,
},
});
this.currentVersion = this.function.currentVersion;
this.alias = new lambda.Alias(this, 'LiveAlias', {
aliasName: 'live',
version: this.currentVersion,
description: 'Production traffic alias',
});
}
}
The key concepts here:
- Versions: Each deployment creates an immutable version snapshot
- Alias: The "live" alias is what clients invoke, not a specific version
- Traffic routing: The alias can split traffic between two versions
Using the Construct
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import { VersionedLambda } from '../constructs/versioned-lambda';
export class ApiStack extends cdk.Stack {
public readonly apiHandler: VersionedLambda;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const stage = this.node.tryGetContext('stage') || 'dev';
this.apiHandler = new VersionedLambda(this, 'ApiHandler', {
functionName: `my-service-api-${stage}`,
description: 'API handler for my-service',
: ,
: ,
: lambda..,
: ,
: cdk..(),
: {
: stage,
: stage === ? : ,
},
});
cdk.(, , {
: ...,
: ,
});
}
}
Step 2: Configure CloudWatch Alarms
CloudWatch alarms monitor your Lambda's health during deployment. If metrics breach thresholds, CodeDeploy triggers automatic rollback.
Alarm Configuration Construct
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export interface DeploymentAlarmsProps {
lambdaFunction: lambda.Function;
alias: lambda.Alias;
alarmNamePrefix: string;
errorRateThreshold?: number;
latencyP99Threshold?: number;
throttleThreshold?: number;
evaluationPeriods?: number;
}
export class DeploymentAlarms extends Construct {
public readonly errorAlarm: cloudwatch.Alarm;
public readonly latencyAlarm: cloudwatch.;
: cloudwatch.;
: cloudwatch.[];
() {
(scope, id);
evaluationPeriods = props. ?? ;
. = cloudwatch.(, , {
: ,
: ,
: props..({
: cdk..(),
: ,
}),
: props. ?? ,
: evaluationPeriods,
: cloudwatch..,
: cloudwatch..,
});
. = cloudwatch.(, , {
: ,
: ,
: props..({
: cdk..(),
: ,
}),
: props. ?? ,
: evaluationPeriods,
: cloudwatch..,
: cloudwatch..,
});
. = cloudwatch.(, , {
: ,
: ,
: props..({
: cdk..(),
: ,
}),
: props. ?? ,
: evaluationPeriods,
: cloudwatch..,
: cloudwatch..,
});
. = [., ., .];
}
}
These alarms monitor the alias (which receives production traffic), not the function directly. This ensures we're measuring the actual user experience during canary deployment.
Step 3: Configure CodeDeploy Deployment Group
CodeDeploy manages the traffic shifting between Lambda versions. It supports multiple deployment strategies.
Deployment Configuration Options
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { Construct } from 'constructs';
export enum CanaryDeploymentConfig {
CANARY_10_PERCENT_5_MINUTES = 'CANARY_10PERCENT_5MINUTES',
CANARY_10_PERCENT_10_MINUTES = 'CANARY_10PERCENT_10MINUTES',
CANARY_10_PERCENT_15_MINUTES = 'CANARY_10PERCENT_15MINUTES',
LINEAR_10_PERCENT_EVERY_1_MINUTE = 'LINEAR_10PERCENT_EVERY_1MINUTE',
LINEAR_10_PERCENT_EVERY_2_MINUTES = 'LINEAR_10PERCENT_EVERY_2MINUTES',
LINEAR_10_PERCENT_EVERY_3_MINUTES = 'LINEAR_10PERCENT_EVERY_3MINUTES',
LINEAR_10_PERCENT_EVERY_10_MINUTES = 'LINEAR_10PERCENT_EVERY_10MINUTES',
ALL_AT_ONCE = ,
}
{
: lambda.;
: ;
: cloudwatch.[];
?: ;
?: ;
}
{
: codedeploy.;
: codedeploy.;
() {
(scope, id);
. = codedeploy.(, , {
: props.,
});
deploymentConfig = .(props.);
. = codedeploy.(, , {
: .,
: props.,
: props.,
: deploymentConfig,
: props.,
: {
: ,
: ,
: ,
},
});
}
(
:
): codedeploy. {
(config) {
.:
codedeploy..;
.:
codedeploy..;
.:
codedeploy..;
.:
codedeploy..;
.:
codedeploy..;
.:
codedeploy..;
.:
codedeploy..;
.:
codedeploy..;
:
codedeploy..;
}
}
}
Deployment Config Comparison
| Config | Traffic Shift | Total Time | Use Case |
|---|
| CANARY_10PERCENT_5MINUTES | 10% → 100% | 5 min | Quick validation |
| CANARY_10PERCENT_10MINUTES | 10% → 100% | 10 min | Standard production |
| LINEAR_10PERCENT_EVERY_1MINUTE | 10% increments | 10 min | Gradual rollout |
| LINEAR_10PERCENT_EVERY_10MINUTES | 10% increments | 100 min | High-risk changes |
Step 4: Complete CDK Stack
Combine all components into a production-ready deployment stack.
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { Construct } from 'constructs';
import { VersionedLambda } from '../constructs/versioned-lambda';
import { DeploymentAlarms } from '../constructs/deployment-alarms';
import { CanaryDeployment, CanaryDeploymentConfig } from '../constructs/canary-deployment';
export interface CanaryDeploymentStackProps extends cdk.StackProps {
stage: string;
serviceName: string;
codePath: string;
deploymentConfig?: CanaryDeploymentConfig;
errorRateThreshold?: number;
latencyP99Threshold?: ;
}
{
: lambda.;
: lambda.;
: codedeploy.;
() {
(scope, id, props);
alarmPrefix = ;
versionedLambda = (, , {
: ,
: ,
: props.,
: ,
: lambda..,
: ,
: cdk..(),
: {
: props.,
: props.,
},
});
. = versionedLambda.;
. = versionedLambda.;
alarms = (, , {
: .,
: .,
: alarmPrefix,
: props.,
: props.,
});
deploymentConfig = props. ??
(props. ===
? .
: .);
canaryDeployment = (, , {
: .,
: deploymentConfig,
: alarms.,
: ,
: ,
});
. = canaryDeployment.;
cdk.(, , {
: ..,
});
cdk.(, , {
: ..,
});
cdk.(, , {
: versionedLambda..,
});
cdk.(, , {
: ..,
});
}
}
CDK App Entry Point
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CanaryDeploymentStack } from '../lib/stacks/canary-deployment-stack';
import { CanaryDeploymentConfig } from '../lib/constructs/canary-deployment';
const app = new cdk.App();
const stage = app.node.tryGetContext('stage') || 'dev';
const serviceName = 'my-service';
new CanaryDeploymentStack(app, `${serviceName}-${stage}`, {
stage: stage,
serviceName: serviceName,
codePath: '../dist/api',
deploymentConfig: stage === 'prod'
? CanaryDeploymentConfig.CANARY_10_PERCENT_10_MINUTES
: CanaryDeploymentConfig.ALL_AT_ONCE,
errorRateThreshold: 10,
latencyP99Threshold: 5000,
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.. || ,
},
});
Step 5: Test Rollback Scenarios
Implement scripts and procedures for testing rollback behavior.
Metrics Check Script
#!/bin/bash
set -euo pipefail
SERVICE=$1
STAGE=$2
FUNCTION_NAME="${SERVICE}-api-${STAGE}"
ALIAS_NAME="live"
echo "Checking metrics for ${FUNCTION_NAME}:${ALIAS_NAME}..."
END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
START_TIME=$(date -u -v-5M +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
date -u -d "5 minutes ago" +"%Y-%m-%dT%H:%M:%SZ")
ERRORS=$(aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Errors \
--dimensions Name=FunctionName,Value=$FUNCTION_NAME Name=Resource,Value="${FUNCTION_NAME}:${ALIAS_NAME}" \
--start-time "$START_TIME" \
--end-time "$END_TIME" \
--period 300 \
--statistics Sum \
--query 'Datapoints[0].Sum' \
--output text 2>/dev/null || echo "0")
INVOCATIONS=$(aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value=$FUNCTION_NAME Name=Resource,Value="${FUNCTION_NAME}:${ALIAS_NAME}" \
--start-time "$START_TIME" \
--end-time "$END_TIME" \
--period 300 \
--statistics Sum \
--query \
--output text 2>/dev/null || )
DURATION_P99=$(aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Duration \
--dimensions Name=FunctionName,Value= Name=Resource,Value= \
--start-time \
--end-time \
--period 300 \
--extended-statistics p99 \
--query \
--output text 2>/dev/null || )
ERRORS=
INVOCATIONS=
DURATION_P99=
[[ != ]];
ERROR_RATE=$( | bc)
ERROR_RATE=
FAILED=0
(( $(echo " > " | bc -l) ));
FAILED=1
(( $(echo " > " | bc -l) ));
FAILED=1
[[ -eq 1 ]];
1
0
Manual Rollback Commands
#!/bin/bash
set -euo pipefail
SERVICE=$1
STAGE=$2
FUNCTION_NAME="${SERVICE}-api-${STAGE}"
ALIAS_NAME="live"
echo "Rolling back ${FUNCTION_NAME}..."
PREVIOUS_VERSION=$(aws lambda list-versions-by-function \
--function-name "$FUNCTION_NAME" \
--query 'Versions[-2].Version' \
--output text)
if [[ "$PREVIOUS_VERSION" == "None" || "$PREVIOUS_VERSION" == "\$LATEST" ]]; then
echo "Error: No previous version found to rollback to"
exit 1
fi
echo "Rolling back to version: $PREVIOUS_VERSION"
aws lambda update-alias \
--function-name "$FUNCTION_NAME" \
--name "$ALIAS_NAME" \
--function-version "$PREVIOUS_VERSION" \
--routing-config 'AdditionalVersionWeights={}'
echo "✅ Rollback complete. Alias now points to version $PREVIOUS_VERSION"
CURRENT=$(aws lambda get-alias \
--function-name "$FUNCTION_NAME" \
--name \
--query \
--output text)
Stop In-Progress Deployment
#!/bin/bash
set -euo pipefail
SERVICE=$1
STAGE=$2
APPLICATION_NAME="${SERVICE}-${STAGE}"
DEPLOYMENT_GROUP="${SERVICE}-${STAGE}-dg"
DEPLOYMENT_ID=$(aws deploy list-deployments \
--application-name "$APPLICATION_NAME" \
--deployment-group-name "$DEPLOYMENT_GROUP" \
--include-only-statuses "InProgress" \
--query 'deployments[0]' \
--output text)
if [[ "$DEPLOYMENT_ID" == "None" ]]; then
echo "No in-progress deployment found"
exit 0
fi
echo "Stopping deployment: $DEPLOYMENT_ID"
aws deploy stop-deployment \
--deployment-id "$DEPLOYMENT_ID" \
--auto-rollback-enabled
echo "✅ Deployment stopped and rollback initiated"
Step 6: Monitor Canary Metrics
Create a CloudWatch dashboard for monitoring canary deployments in real-time.
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export interface CanaryDashboardProps {
dashboardName: string;
lambdaFunction: lambda.Function;
alias: lambda.Alias;
}
export class CanaryDashboard extends Construct {
public readonly dashboard: cloudwatch.Dashboard;
constructor(scope: Construct, id: string, props: CanaryDashboardProps) {
super(scope, id);
this.dashboard = new cloudwatch.Dashboard(this, 'Dashboard', {
dashboardName: props.dashboardName,
});
..(
cloudwatch.({
: ,
: [
props..({
: cdk..(),
: ,
: ,
}),
],
: [
props..({
: cdk..(),
: ,
: ,
}),
],
: ,
}),
cloudwatch.({
: ,
: [
props..({
: cdk..(),
: ,
: ,
}),
props..({
: cdk..(),
: ,
: ,
}),
props..({
: cdk..(),
: ,
: ,
}),
],
: ,
})
);
..(
cloudwatch.({
: ,
: [
props..({
: cdk..(),
: ,
}),
],
: ,
}),
cloudwatch.({
: ,
: [
props..(, {
: cdk..(),
: ,
}),
],
: ,
}),
cloudwatch.({
: ,
: [
props..({
: cdk..(),
: ,
}),
],
: ,
})
);
}
}
Makefile Targets
Add deployment targets to your Makefile for easy canary operations.
SERVICE_NAME := my-service
STAGE ?= dev
CDK_DIR := deploy/cdk
VERSION ?= $(shell git describe --tags --always)
.PHONY: deploy deploy-canary promote rollback check-metrics stop-deployment
deploy: build
@echo "Deploying $(SERVICE_NAME) to $(STAGE)..."
cd $(CDK_DIR) && cdk deploy --context stage=$(STAGE) --require-approval never
deploy-canary: build
ifndef WEIGHT
$(error WEIGHT is required, e.g., make deploy-canary WEIGHT=0.1)
endif
@echo "Deploying canary at $(WEIGHT) weight..."
cd $(CDK_DIR) && CANARY_WEIGHT=$(WEIGHT) cdk deploy --context stage=$(STAGE)
promote:
@echo "Promoting $(SERVICE_NAME) to 100% in $(STAGE)..."
./scripts/promote.sh $(SERVICE_NAME) $(STAGE)
rollback:
@echo "Rolling back $(SERVICE_NAME) in $(STAGE)..."
./scripts/rollback.sh $(SERVICE_NAME) $(STAGE)
check-metrics:
@./scripts/check-metrics.sh $(SERVICE_NAME) $(STAGE)
stop-deployment:
@./scripts/stop-deployment.sh
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o bootstrap ./cmd/api
zip -j dist/api.zip bootstrap
rm bootstrap
Verification Checklist
After setting up canary deployments, verify: