Skip to main content
Back to Blog
4 October 202514 min read

AWS Fargate: Serverless Container Orchestration

AWSFargateContainersECSServerless

Running containers without managing servers using AWS Fargate. ECS vs EKS integration, task definitions, networking, and cost optimization strategies.


AWS Fargate: Serverless Container Orchestration

AWS Fargate is a serverless compute engine for containers. It removes the need to provision and manage servers, letting you focus on building applications. Fargate works with both Amazon ECS and Amazon EKS, providing flexible deployment options for containerised workloads.

Fargate Fundamentals

Fargate vs EC2 Launch Type

EC2 Launch Type:
┌─────────────────────────────────────────────────┐
│                 EC2 Instance                     │
│  ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
│  │ Container 1 │ │ Container 2 │ │ Container 3│ │
│  └─────────────┘ └─────────────┘ └────────────┘ │
│                                                  │
│  You manage: Instance sizing, patching, scaling │
└─────────────────────────────────────────────────┘

Fargate Launch Type:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Fargate Task    │ │ Fargate Task    │ │ Fargate Task    │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Container 1 │ │ │ │ Container 2 │ │ │ │ Container 3 │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
│                 │ │                 │ │                 │
│ AWS manages     │ │ AWS manages     │ │ AWS manages     │
└─────────────────┘ └─────────────────┘ └─────────────────┘

Resource Configuration

CPU (vCPU)Memory Options
0.250.5GB, 1GB, 2GB
0.51GB - 4GB
12GB - 8GB
24GB - 16GB
48GB - 30GB
816GB - 60GB
1632GB - 120GB

ECS with Fargate

Task Definition

{ "family": "api-service", "networkMode": "awsvpc", "requiresCompatibilities": ["FARGATE"], "cpu": "512", "memory": "1024", "executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole", "taskRoleArn": "arn:aws:iam::123456789:role/apiTaskRole", "containerDefinitions": [ { "name": "api", "image": "123456789.dkr.ecr.eu-west-1.amazonaws.com/api:latest", "essential": true, "portMappings": [ { "containerPort": 8080, "protocol": "tcp" } ], "environment": [ { "name": "NODE_ENV", "value": "production" } ], "secrets": [ { "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789:secret:db-url" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/api-service", "awslogs-region": "eu-west-1", "awslogs-stream-prefix": "ecs" } }, "healthCheck": { "command": ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"], "interval": 30, "timeout": 5, "retries": 3, "startPeriod": 60 } } ] }

Service Definition with Terraform

# ecs.tf resource "aws_ecs_cluster" "main" { name = "${var.project}-cluster" setting { name = "containerInsights" value = "enabled" } configuration { execute_command_configuration { logging = "OVERRIDE" log_configuration { cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_exec.name } } } } resource "aws_ecs_service" "api" { name = "api-service" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.api.arn desired_count = 3 launch_type = "FARGATE" network_configuration { subnets = var.private_subnet_ids security_groups = [aws_security_group.ecs_tasks.id] assign_public_ip = false } load_balancer { target_group_arn = aws_lb_target_group.api.arn container_name = "api" container_port = 8080 } deployment_configuration { maximum_percent = 200 minimum_healthy_percent = 100 deployment_circuit_breaker { enable = true rollback = true } } service_registries { registry_arn = aws_service_discovery_service.api.arn } enable_execute_command = true lifecycle { ignore_changes = [desired_count] } } # Auto Scaling resource "aws_appautoscaling_target" "api" { max_capacity = 10 min_capacity = 2 resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.api.name}" scalable_dimension = "ecs:service:DesiredCount" service_namespace = "ecs" } resource "aws_appautoscaling_policy" "api_cpu" { name = "api-cpu-scaling" policy_type = "TargetTrackingScaling" resource_id = aws_appautoscaling_target.api.resource_id scalable_dimension = aws_appautoscaling_target.api.scalable_dimension service_namespace = aws_appautoscaling_target.api.service_namespace target_tracking_scaling_policy_configuration { predefined_metric_specification { predefined_metric_type = "ECSServiceAverageCPUUtilization" } target_value = 70 scale_in_cooldown = 300 scale_out_cooldown = 60 } } resource "aws_appautoscaling_policy" "api_memory" { name = "api-memory-scaling" policy_type = "TargetTrackingScaling" resource_id = aws_appautoscaling_target.api.resource_id scalable_dimension = aws_appautoscaling_target.api.scalable_dimension service_namespace = aws_appautoscaling_target.api.service_namespace target_tracking_scaling_policy_configuration { predefined_metric_specification { predefined_metric_type = "ECSServiceAverageMemoryUtilization" } target_value = 80 scale_in_cooldown = 300 scale_out_cooldown = 60 } }

EKS with Fargate

Fargate Profile

# eks-fargate.tf resource "aws_eks_fargate_profile" "main" { cluster_name = aws_eks_cluster.main.name fargate_profile_name = "main-profile" pod_execution_role_arn = aws_iam_role.fargate_pod_execution.arn subnet_ids = var.private_subnet_ids selector { namespace = "default" } selector { namespace = "application" labels = { "fargate" = "true" } } } resource "aws_iam_role" "fargate_pod_execution" { name = "${var.project}-fargate-pod-execution" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "eks-fargate-pods.amazonaws.com" } }] }) } resource "aws_iam_role_policy_attachment" "fargate_pod_execution" { role = aws_iam_role.fargate_pod_execution.name policy_arn = "arn:aws:iam::aws:policy/AmazonEKSFargatePodExecutionRolePolicy" }

Kubernetes Deployment for Fargate

# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: api-service namespace: application labels: app: api-service fargate: "true" # Matches Fargate profile selector spec: replicas: 3 selector: matchLabels: app: api-service template: metadata: labels: app: api-service fargate: "true" spec: serviceAccountName: api-service containers: - name: api image: 123456789.dkr.ecr.eu-west-1.amazonaws.com/api:latest ports: - containerPort: 8080 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" env: - name: NODE_ENV value: production envFrom: - secretRef: name: api-secrets livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 securityContext: runAsNonRoot: true runAsUser: 1000 readOnlyRootFilesystem: true allowPrivilegeEscalation: false topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app: api-service

Networking

VPC Configuration

# networking.tf resource "aws_security_group" "ecs_tasks" { name = "${var.project}-ecs-tasks" description = "Security group for ECS tasks" vpc_id = var.vpc_id ingress { description = "Allow traffic from ALB" from_port = 8080 to_port = 8080 protocol = "tcp" security_groups = [aws_security_group.alb.id] } egress { description = "Allow all outbound traffic" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${var.project}-ecs-tasks" } } # VPC Endpoints for Fargate in private subnets resource "aws_vpc_endpoint" "ecr_api" { vpc_id = var.vpc_id service_name = "com.amazonaws.${var.region}.ecr.api" vpc_endpoint_type = "Interface" subnet_ids = var.private_subnet_ids security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true } resource "aws_vpc_endpoint" "ecr_dkr" { vpc_id = var.vpc_id service_name = "com.amazonaws.${var.region}.ecr.dkr" vpc_endpoint_type = "Interface" subnet_ids = var.private_subnet_ids security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true } resource "aws_vpc_endpoint" "s3" { vpc_id = var.vpc_id service_name = "com.amazonaws.${var.region}.s3" vpc_endpoint_type = "Gateway" route_table_ids = var.private_route_table_ids } resource "aws_vpc_endpoint" "logs" { vpc_id = var.vpc_id service_name = "com.amazonaws.${var.region}.logs" vpc_endpoint_type = "Interface" subnet_ids = var.private_subnet_ids security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true } resource "aws_vpc_endpoint" "secretsmanager" { vpc_id = var.vpc_id service_name = "com.amazonaws.${var.region}.secretsmanager" vpc_endpoint_type = "Interface" subnet_ids = var.private_subnet_ids security_group_ids = [aws_security_group.vpc_endpoints.id] private_dns_enabled = true }

Service Discovery

# service-discovery.tf resource "aws_service_discovery_private_dns_namespace" "main" { name = "${var.project}.local" description = "Private DNS namespace for service discovery" vpc = var.vpc_id } resource "aws_service_discovery_service" "api" { name = "api" 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 } } # Services can now discover each other at: # api.project.local

Cost Optimisation

Spot Capacity with ECS

# spot-fargate.tf resource "aws_ecs_service" "api_spot" { name = "api-service" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.api.arn desired_count = 3 capacity_provider_strategy { capacity_provider = "FARGATE_SPOT" weight = 4 base = 1 # Minimum 1 task on regular Fargate } capacity_provider_strategy { capacity_provider = "FARGATE" weight = 1 base = 0 } network_configuration { subnets = var.private_subnet_ids security_groups = [aws_security_group.ecs_tasks.id] assign_public_ip = false } } # Cluster 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 { capacity_provider = "FARGATE_SPOT" weight = 4 base = 1 } default_capacity_provider_strategy { capacity_provider = "FARGATE" weight = 1 } }

Cost Comparison

Workload TypeFargateFargate SpotEC2Recommendation
Production APIHigher~70% cheaperLowerFargate + Spot mix
Batch JobsHigher~70% cheaperLowerFargate Spot
Dev/TestHigher~70% cheaperLowerFargate Spot
StatefulHigherNot suitableLowerEC2 or Fargate

Right-Sizing

// analyse-task-utilization.ts interface TaskMetrics { taskId: string; cpuUtilization: number; memoryUtilization: number; configuredCpu: number; configuredMemory: number; } const analyseAndRecommend = (metrics: TaskMetrics[]): Recommendation[] => { const recommendations: Recommendation[] = []; const avgCpu = metrics.reduce((sum, m) => sum + m.cpuUtilization, 0) / metrics.length; const avgMemory = metrics.reduce((sum, m) => sum + m.memoryUtilization, 0) / metrics.length; const configuredCpu = metrics[0].configuredCpu; const configuredMemory = metrics[0].configuredMemory; // CPU recommendation if (avgCpu < 30) { recommendations.push({ type: 'cpu', current: configuredCpu, recommended: Math.max(256, configuredCpu / 2), reason: `Average CPU utilization is ${avgCpu.toFixed(1)}%` }); } // Memory recommendation if (avgMemory < 40) { recommendations.push({ type: 'memory', current: configuredMemory, recommended: Math.max(512, configuredMemory / 2), reason: `Average memory utilization is ${avgMemory.toFixed(1)}%` }); } return recommendations; };

Observability

CloudWatch Container Insights

# container-insights.tf resource "aws_ecs_cluster" "main" { name = "${var.project}-cluster" setting { name = "containerInsights" value = "enabled" } } # CloudWatch dashboard resource "aws_cloudwatch_dashboard" "ecs" { dashboard_name = "${var.project}-ecs" dashboard_body = jsonencode({ widgets = [ { type = "metric" x = 0 y = 0 width = 12 height = 6 properties = { metrics = [ ["ECS/ContainerInsights", "CpuUtilized", "ServiceName", "api-service", "ClusterName", aws_ecs_cluster.main.name], [".", "CpuReserved", ".", ".", ".", "."] ] title = "CPU Utilization" region = var.region } }, { type = "metric" x = 12 y = 0 width = 12 height = 6 properties = { metrics = [ ["ECS/ContainerInsights", "MemoryUtilized", "ServiceName", "api-service", "ClusterName", aws_ecs_cluster.main.name], [".", "MemoryReserved", ".", ".", ".", "."] ] title = "Memory Utilization" region = var.region } } ] }) }

Application Logging

// logging.ts import winston from 'winston'; const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), defaultMeta: { service: process.env.SERVICE_NAME, version: process.env.APP_VERSION, taskId: process.env.ECS_TASK_ID }, transports: [ new winston.transports.Console() ] }); // Request logging middleware const requestLogger = (req: Request, res: Response, next: NextFunction) => { const startTime = Date.now(); res.on('finish', () => { logger.info('HTTP Request', { method: req.method, path: req.path, statusCode: res.statusCode, duration: Date.now() - startTime, userAgent: req.get('user-agent'), requestId: req.get('x-request-id') }); }); next(); };

Key Takeaways

  1. Right launch type: Use Fargate for simplicity, EC2 for cost optimisation at scale

  2. Use Spot wisely: Mix Fargate Spot with regular Fargate for cost savings with reliability

  3. VPC endpoints: Essential for Fargate in private subnets without NAT Gateway costs

  4. Right-size tasks: Monitor CPU/memory utilisation and adjust configurations

  5. Auto-scaling: Configure both target tracking and step scaling for optimal performance

  6. Service discovery: Use Cloud Map for service-to-service communication

  7. Health checks: Configure appropriate health checks for graceful deployments

  8. Security: Use task roles, secrets manager, and security groups properly

Fargate eliminates infrastructure management overhead while providing the flexibility of containers. Choose the right orchestrator (ECS vs EKS) based on your team's Kubernetes expertise and operational requirements.

Share this article