2025-09-04
AWS Fargate 104: Deploying with CDK, Terraform, and SAM
How to deploy Fargate effectively with different IaC tools. Practical patterns, common gotchas, and what works best for each approach.
After three posts about Fargate (101, 102, 103), you might be thinking “cool, but how do I deploy this stuff without clicking through the AWS Console like it’s 2015?”
Deploying Fargate services requires choosing the right Infrastructure as Code (IaC) tool for your team and requirements. Each approach offers different trade-offs in complexity, maintainability, and developer experience.
IaC Tool Comparison for Fargate
CloudFormation - The Foundation
# Verbose but comprehensive
Resources:
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: my-app
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: '256'
Memory: '512'
# Requires detailed configuration
Terraform - The Industry Standard
# Declarative and explicit
resource "aws_ecs_task_definition" "app" {
family = "my-app"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
# Good balance of readability and control
}
CDK - The Programming Approach
// High-level abstractions with programming constructs
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
memoryLimitMiB: 512,
cpu: 256,
});
Let’s explore what works well with each approach.
Deploying Fargate with CDK
AWS CDK shines for Fargate deployments when you want programmatic control and high-level abstractions. Here’s how to use it effectively:
The CDK Advantage for Fargate
import * as cdk from 'aws-cdk-lib';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
export class FargateStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// This single construct creates:
// - VPC, Subnets, NAT Gateways
// - ECS Cluster
// - Fargate Service
// - Application Load Balancer
// - Task Definition
// - Security Groups
// - CloudWatch Logs
const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('nginx'),
containerPort: 80,
environment: {
NODE_ENV: 'production',
API_URL: 'https://api.example.com'
}
},
desiredCount: 3,
domainName: 'app.example.com',
domainZone: hostedZone,
certificate: certificate,
});
// Add auto-scaling
const scaling = fargateService.service.autoScaleTaskCount({
maxCapacity: 10,
minCapacity: 2,
});
scaling.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 50,
});
// Add CloudWatch alarms
new cloudwatch.Alarm(this, 'HighMemory', {
metric: fargateService.service.metricMemoryUtilization(),
threshold: 80,
evaluationPeriods: 2,
});
}
}
What this CDK construct creates:
- ~300 lines of CloudFormation
- 15+ AWS resources
- All the IAM roles and policies
- Proper security group rules
- CloudWatch log groups
Fargate-Specific CDK Patterns
1. Service Templates with Environment Variations
interface FargateServiceProps {
serviceName: string;
image: string;
environment: 'dev' | 'staging' | 'prod';
port?: number;
}
class FargateService extends Construct {
constructor(scope: Construct, id: string, props: FargateServiceProps) {
super(scope, id);
// Environment-specific sizing
const configs = {
dev: { cpu: 256, memory: 512, desiredCount: 1 },
staging: { cpu: 512, memory: 1024, desiredCount: 2 },
prod: { cpu: 1024, memory: 2048, desiredCount: 5 }
};
const config = configs[props.environment];
const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry(props.image),
containerPort: props.port || 80,
},
cpu: config.cpu,
memoryLimitMiB: config.memory,
desiredCount: config.desiredCount,
// Auto-configure ALB, VPC, subnets, security groups
});
// Add Fargate-specific monitoring
this.addFargateMonitoring(service);
}
private addFargateMonitoring(service: ecsPatterns.ApplicationLoadBalancedFargateService) {
// Memory utilization alarm
new cloudwatch.Alarm(this, 'MemoryAlarm', {
metric: service.service.metricMemoryUtilization(),
threshold: 80,
evaluationPeriods: 2,
});
// Task count alarm
new cloudwatch.Alarm(this, 'TaskCountAlarm', {
metric: service.service.metricDesiredCount(),
threshold: 1,
comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN,
});
}
}
2. Handling Fargate Spot with CDK
// CDK doesn't directly support Fargate Spot in high-level constructs
// You need to use escape hatches
const service = new ecs.FargateService(this, 'Service', {
cluster,
taskDefinition,
capacityProviderStrategies: [
{
capacityProvider: 'FARGATE_SPOT',
weight: 4,
base: 0,
},
{
capacityProvider: 'FARGATE',
weight: 1,
base: 2, // Always keep 2 on regular Fargate
}
],
});
CDK Gotchas for Fargate
Issue: ENI Limits
// CDK creates many resources that consume ENIs
// Monitor and set up alerts
const eniUsageMetric = new cloudwatch.Metric({
namespace: 'Custom/VPC',
metricName: 'ENIsInUse',
});
new cloudwatch.Alarm(this, 'ENIUsage', {
metric: eniUsageMetric,
threshold: 4500, // 90% of default 5000 limit
});
Deploying Fargate with Terraform
Terraform provides explicit, predictable Fargate deployments with excellent state management. Here’s how to structure your Fargate infrastructure effectively:
Terraform Fargate Foundations
resource "aws_ecs_cluster" "main" {
name = "production"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_task_definition" "app" {
family = "my-app"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "512"
memory = "1024"
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = jsonencode([{
name = "app"
image = "nginx:latest"
portMappings = [{
containerPort = 80
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.app.name
awslogs-region = var.aws_region
awslogs-stream-prefix = "ecs"
}
}
environment = [
{
name = "NODE_ENV"
value = "production"
}
]
}])
}
resource "aws_ecs_service" "app" {
name = "my-app-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.app_count
launch_type = "FARGATE"
enable_execute_command = true
# Use track_latest for automatic task definition updates
task_definition_track_latest = true
network_configuration {
security_groups = [aws_security_group.ecs_tasks.id]
subnets = aws_subnet.private[*].id
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_alb_target_group.app.arn
container_name = "app"
container_port = 80
}
depends_on = [aws_alb_listener.front_end]
}
The Module Pattern That Saved Our Sanity
# modules/fargate-service/main.tf
variable "service_name" {}
variable "image" {}
variable "cpu" { default = "256" }
variable "memory" { default = "512" }
variable "desired_count" { default = 2 }
# ... 200 lines of reusable Terraform ...
output "service_url" {
value = aws_alb.main.dns_name
}
# In your main configuration
module "api_service" {
source = "./modules/fargate-service"
service_name = "api"
image = "myapp/api:latest"
cpu = "512"
memory = "1024"
desired_count = 3
}
module "worker_service" {
source = "./modules/fargate-service"
service_name = "worker"
image = "myapp/worker:latest"
cpu = "256"
memory = "512"
desired_count = 5
}
Essential State Management
Proper state management is critical for Terraform deployments. Outdated state files can lead to unintended resource destruction.
# Always review plan output carefully
$ terraform plan
Terraform will perform the following actions:
# aws_ecs_service.app will be destroyed
- resource "aws_ecs_service" "app" {
- name = "production-api" -> null
# ... 50 resources to be destroyed
}
Plan: 0 to add, 0 to change, 52 to destroy.
# Never use auto-approve in production
$ terraform apply # Review and confirm manually
Required: Always use remote state for team environments.
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "fargate/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
SAM: The Lambda-First Approach
AWS SAM (Serverless Application Model) is great for Lambda, but for Fargate? It’s like using a screwdriver to hammer nails.
# template.yaml
Transform: AWS::Serverless-2016-10-31
Resources:
FargateCluster:
Type: AWS::ECS::Cluster
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
RequiresCompatibilities:
- FARGATE
NetworkMode: awsvpc
Cpu: '256'
Memory: '512'
# Back to CloudFormation verbosity
# SAM shines when you mix Lambda with Fargate
ProcessorFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: python3.12
Events:
ECSTask:
Type: CloudWatchEvent
Properties:
Pattern:
source:
- aws.ecs
detail-type:
- ECS Task State Change
When SAM makes sense for Fargate:
- You’re primarily Lambda-based with some Fargate
- You need Step Functions orchestration
- You’re already invested in SAM for other services
When it doesn’t:
- Fargate is your primary compute
- You need complex networking
- You want programming language features
Migration Strategies
CloudFormation to Terraform Migration
Migrating existing infrastructure requires careful planning. Consider these challenges:
Migration Process:
- Export existing resources
- Write equivalent Terraform
- Import resources carefully
- Validate before removing CloudFormation
Common Issues:
$ terraform import aws_ecs_service.app production-app-service
Import successful!
$ terraform plan
~ 147 resources to modify # Resource drift detection
! 23 resources will be recreated # Breaking changes
Error: Resource already exists # Import conflicts
Best Practices:
- Start with non-critical resources
- Use targeted applies:
terraform apply -target=resource - Maintain parallel stacks during transition
- Script resource discovery and import
Terraform to CDK Migration
CDK migrations face import limitations:
class MigrationStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// Limited import support
const cluster = ecs.Cluster.fromClusterArn(
this,
'ImportedCluster',
'arn:aws:ecs:us-east-1:123456789:cluster/production'
);
// CDK import limitations:
// - Task definitions require recreation
// - Complex service configurations
// - Service discovery integration
}
}
Migration Strategy: Consider running both tools temporarily for complex transitions.
The Decision Matrix
Here’s guidance for choosing the right IaC tool:
Choose CDK if:
- Your team knows TypeScript/Python well
- You’re starting fresh (no legacy)
- You want high-level abstractions
- You’re all-in on AWS
- You like living on the edge
Choose Terraform if:
- You need multi-cloud potential
- Your team prefers declarative syntax
- You have existing Terraform modules
- Stability > Latest features
- You value huge community support
Choose SAM if:
- You’re Lambda-first architecture
- You need Step Functions
- You want minimal tooling
- Your Fargate usage is minimal
Still Use CloudFormation if:
- You enjoy pain (kidding!)
- You need AWS Support to debug
- You’re using AWS Service Catalog
- Corporate mandate (my condolences)
The Patterns That Work Everywhere
Regardless of tool, these patterns saved us:
1. The Environment Abstraction
// CDK
interface EnvironmentConfig {
cpu: number;
memory: number;
desiredCount: number;
environment: Record<string, string>;
}
const configs: Record<string, EnvironmentConfig> = {
dev: { cpu: 256, memory: 512, desiredCount: 1 },
staging: { cpu: 512, memory: 1024, desiredCount: 2 },
prod: { cpu: 1024, memory: 2048, desiredCount: 5 }
};
# Terraform
locals {
env_config = {
dev = { cpu = 256, memory = 512, count = 1 }
staging = { cpu = 512, memory = 1024, count = 2 }
prod = { cpu = 1024, memory = 2048, count = 5 }
}
config = local.env_config[var.environment]
}
2. The Service Template Pattern
Instead of copying code, create templates:
// CDK: Base service construct
export class BaseEcsService extends Construct {
public readonly service: ecs.FargateService;
constructor(scope: Construct, id: string, props: BaseEcsServiceProps) {
super(scope, id);
// 100 lines of boilerplate
this.service = new ecs.FargateService(this, 'Service', {
// Common configuration
});
// Standard alarms
this.setupAlarms();
// Standard dashboard
this.setupDashboard();
}
}
// Usage
new BaseEcsService(this, 'ApiService', {
image: 'api:latest',
port: 3000,
cpu: 512
});
3. The GitOps Pipeline
# .github/workflows/deploy.yml
name: Deploy Infrastructure
on:
push:
branches: [main]
paths:
- 'infrastructure/**'
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Terraform Plan
run: |
cd infrastructure
terraform init
terraform plan -out=tfplan
- name: Post Plan to PR
uses: actions/github-script@v6
with:
script: |
// Post plan output as PR comment
apply:
needs: plan
if: github.ref == 'refs/heads/main'
steps:
- name: Terraform Apply
run: |
terraform apply tfplan
The Cost of Each Approach
Let’s talk money, because cloud bills don’t lie:
| Tool | Learning Curve | Maintenance Cost | Flexibility | AWS Feature Lag |
|---|---|---|---|---|
| CDK | 2 weeks | Medium | High | 0-2 weeks |
| Terraform | 1 week | Low | High | 2-4 weeks |
| SAM | 3 days | Low | Low | 0 weeks |
| CloudFormation | 1 week | High | Medium | 0 weeks |
But the bigger cost? Developer happiness.
The impact on development flow:
- CloudFormation: Slower iterations, more debugging
- Terraform: Predictable but verbose workflows
- CDK: Faster development once team is comfortable
The Verdict
Here’s what works well for different scenarios:
- New projects: CDK with TypeScript
- Existing projects: Whatever’s already there (don’t migrate unless you must)
- Multi-cloud potential: Terraform
- Quick prototypes: SAM
- Never again: Raw CloudFormation
The dirty secret? They all generate CloudFormation anyway. Pick the abstraction level that makes your team productive.
Remember: The best IaC tool is the one your team will use. Don’t let perfect be the enemy of deployed.
AWS Fargate Deep Dive Series
Complete guide to AWS Fargate from basics to production. Learn serverless containers, cost optimization, debugging techniques, and Infrastructure-as-Code deployment patterns through real-world experience.
All posts in this series
Related posts
Exploring proven strategies to overcome CloudFormation's 500 resource limit using nested stacks, cross-stack references, SSM Parameter Store, and microstack architecture with real TypeScript CDK examples and decision frameworks.
A practical guide to AWS Fargate from someone who's managed too many EC2 instances. Learn when serverless containers make sense and when they don't.
Advanced Fargate patterns learned from running production workloads. From cost optimization to stateful containers, here's what the docs won't tell you.
A CDK guide for deploying a minimal Strands agent on AgentCore Runtime — parameterized stack, arm64 build, deploy and invoke, and the IAM and Marketplace prerequisites you need before the first call.
A practical guide to designing and implementing AWS Control Tower multi-account strategy covering OU structure, SCPs, RCPs, Account Factory for Terraform, IAM Identity Center, and centralized security architecture.