Setting up AWS ECS (Elastic Container Service) with Terraform
Learn how to provision and manage AWS ECS clusters, services, and tasks using Terraform, including load balancing and auto scaling
Setting up AWS ECS (Elastic Container Service) with Terraform
AWS ECS is a fully managed container orchestration service. This guide demonstrates how to set up and manage ECS clusters, services, and tasks using Terraform.
Video Tutorial
Learn more about managing AWS ECS with Terraform in this comprehensive video tutorial:
Prerequisites
- AWS CLI configured with appropriate permissions
- Terraform installed (version 1.0.0 or later)
- Docker installed locally
- Basic understanding of containerization
Project Structure
terraform-ecs/
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│ └── ecs/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── templates/
└── container-definition.json
ECS Cluster Configuration
Create modules/ecs/main.tf:
# ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
configuration {
execute_command_configuration {
logging = "OVERRIDE"
log_configuration {
cloud_watch_encryption_enabled = true
cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs.name
}
}
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-cluster"
}
)
}
# ECS Capacity Providers
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
default_capacity_provider_strategy {
base = 1
weight = 100
capacity_provider = "FARGATE"
}
}
# Task Definition
resource "aws_ecs_task_definition" "main" {
family = "${var.project_name}-task"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.task_cpu
memory = var.task_memory
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = "${var.project_name}-container"
image = "${var.container_image}:${var.container_tag}"
essential = true
portMappings = [
{
containerPort = var.container_port
hostPort = var.container_port
protocol = "tcp"
}
]
environment = [
{
name = "ENVIRONMENT"
value = var.environment
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.ecs.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:${var.container_port}/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-task"
}
)
}
# ECS Service
resource "aws_ecs_service" "main" {
name = "${var.project_name}-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count = var.desired_count
deployment_minimum_healthy_percent = 50
deployment_maximum_percent = 200
health_check_grace_period_seconds = 60
launch_type = "FARGATE"
platform_version = "LATEST"
network_configuration {
security_groups = [aws_security_group.ecs_tasks.id]
subnets = var.private_subnet_ids
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.main.arn
container_name = "${var.project_name}-container"
container_port = var.container_port
}
deployment_controller {
type = "ECS"
}
deployment_circuit_breaker {
enable = true
rollback = true
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-service"
}
)
lifecycle {
ignore_changes = [desired_count]
}
}
# Application Load Balancer
resource "aws_lb" "main" {
name = "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
enable_deletion_protection = true
access_logs {
bucket = aws_s3_bucket.logs.id
prefix = "alb-logs"
enabled = true
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-alb"
}
)
}
# Target Group
resource "aws_lb_target_group" "main" {
name = "${var.project_name}-tg"
port = var.container_port
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 3
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-tg"
}
)
}
# Auto Scaling
resource "aws_appautoscaling_target" "ecs" {
max_capacity = var.max_capacity
min_capacity = var.min_capacity
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "${var.project_name}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70
}
}
resource "aws_appautoscaling_policy" "memory" {
name = "${var.project_name}-memory-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
target_value = 70
}
}
# CloudWatch Logs
resource "aws_cloudwatch_log_group" "ecs" {
name = "/aws/ecs/${var.project_name}"
retention_in_days = 30
tags = merge(
var.tags,
{
Name = "${var.project_name}-logs"
}
)
}
# Security Groups
resource "aws_security_group" "ecs_tasks" {
name = "${var.project_name}-ecs-tasks"
description = "Security group for ECS tasks"
vpc_id = var.vpc_id
ingress {
description = "Allow inbound traffic from ALB"
from_port = var.container_port
to_port = var.container_port
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-ecs-tasks"
}
)
}
resource "aws_security_group" "alb" {
name = "${var.project_name}-alb"
description = "Security group for ALB"
vpc_id = var.vpc_id
ingress {
description = "Allow inbound HTTP traffic"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Allow inbound HTTPS traffic"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(
var.tags,
{
Name = "${var.project_name}-alb"
}
)
}
## Service Discovery
```hcl
resource "aws_service_discovery_private_dns_namespace" "main" {
name = "${var.project_name}.local"
description = "Service discovery namespace for ECS services"
vpc = var.vpc_id
}
resource "aws_service_discovery_service" "main" {
name = var.project_name
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.main.id
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
}
Container Registry
resource "aws_ecr_repository" "main" {
name = var.project_name
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
}
tags = merge(
var.tags,
{
Name = var.project_name
}
)
}
resource "aws_ecr_lifecycle_policy" "main" {
repository = aws_ecr_repository.main.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 30 images"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 30
}
action = {
type = "expire"
}
}
]
})
}
Best Practices
-
Service Configuration
- Use Fargate for serverless containers
- Implement proper health checks
- Configure service discovery
- Enable auto scaling
-
Security
- Use IAM roles
- Implement security groups
- Enable encryption
- Scan container images
-
Monitoring
- Enable Container Insights
- Configure CloudWatch logs
- Set up alarms
- Monitor performance
-
Cost Optimization
- Use appropriate instance types
- Implement auto-scaling
Conclusion
You’ve learned how to set up and manage AWS ECS using Terraform. This setup provides:
- Containerized application deployment
- Auto scaling capabilities
- Load balancing
- Monitoring and logging
Remember to:
- Monitor container performance
- Implement proper security
- Optimize costs
- Maintain container images
Advanced Features
- Blue/Green Deployments
resource "aws_ecs_service" "blue_green" {
# ... other configuration ...
deployment_controller {
type = "CODE_DEPLOY"
}
load_balancer {
target_group_arn = aws_lb_target_group.blue.arn
container_name = "${var.project_name}-container"
container_port = var.container_port
}
}
- Capacity Providers
resource "aws_ecs_capacity_provider" "main" {
name = "${var.project_name}-capacity-provider"
auto_scaling_group_provider {
auto_scaling_group_arn = aws_autoscaling_group.main.arn
managed_termination_protection = "ENABLED"
managed_scaling {
maximum_scaling_step_size = 1000
minimum_scaling_step_size = 1
status = "ENABLED"
target_capacity = 100
}
}
}
Monitoring and Alerts
- CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "service_cpu" {
alarm_name = "${var.project_name}-cpu-utilization"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = "300"
statistic = "Average"
threshold = "80"
alarm_description = "CPU utilization is too high"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
ClusterName = aws_ecs_cluster.main.name
ServiceName = aws_ecs_service.main.name
}
}