Gerenciar servidores EC2 diretamente — aplicar patches, monitorar uso de disco, escalar manualmente — é uma responsabilidade operacional que consome tempo e atenção que poderiam ser direcionados ao produto. Os serviços gerenciados de computação da AWS existem para transferir essa responsabilidade operacional para a nuvem.
O Amazon ECS gerencia clusters de containers, eliminando a necessidade de operar o plano de controle do orquestrador. O AWS Lambda vai além: abstrai completamente a infraestrutura, deixando o engenheiro responsável apenas pelo código da função e por quanto tempo ela pode executar.
A escolha entre EC2, ECS e Lambda não é uma progressão linear em que cada opção é "melhor" que a anterior. São ferramentas com trade-offs distintos, adequadas para tipos diferentes de workload. Entender esses trade-offs é o que permite fazer escolhas de arquitetura conscientes.
Amazon ECS: Containers sem Gerenciar o Orquestrador
O ECS é o serviço de orquestração de containers da AWS. Ele gerencia onde os containers rodam, garante que o número desejado de instâncias está em execução, integra com o Application Load Balancer para distribuição de tráfego e com o IAM para controle de acesso.
Conceitos Fundamentais do ECS
Cluster — o agrupamento lógico de recursos de computação onde as tasks são executadas. Um cluster pode conter instâncias EC2 ou usar o Fargate.
Task Definition — o blueprint de um container ou grupo de containers. Define a imagem Docker, quantidade de CPU e memória, variáveis de ambiente, volumes, configurações de rede e a IAM role da task.
Task — uma instância em execução de uma task definition. Equivalente a um pod no Kubernetes.
Service — um controlador que garante que um número determinado de tasks está sempre em execução. Gerencia atualizações com zero downtime, integra com load balancers e escala automaticamente.
ECS com Fargate: Serverless para Containers
O Fargate é o modo de execução do ECS que elimina completamente a necessidade de gerenciar instâncias EC2. Ao usar Fargate, o time define quanto de CPU e memória cada container precisa — a AWS aloca a infraestrutura necessária de forma invisível.
# ecs.tf — Infraestrutura completa do ECS com Fargate
# Cluster ECS
resource "aws_ecs_cluster" "principal" {
name = "${var.project_name}-${var.environment}"
configuration {
execute_command_configuration {
logging = "OVERRIDE"
log_configuration {
cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_exec.name
}
}
}
setting {
name = "containerInsights"
value = "enabled"
}
tags = local.tags_comuns
}
# Log group para os logs dos containers
resource "aws_cloudwatch_log_group" "aplicacao" {
name = "/ecs/${var.project_name}-${var.environment}"
retention_in_days = 30
tags = local.tags_comuns
}
resource "aws_cloudwatch_log_group" "ecs_exec" {
name = "/ecs/${var.project_name}-${var.environment}/exec"
retention_in_days = 7
tags = local.tags_comuns
}
# IAM Role para as tasks ECS — permissões da aplicação
resource "aws_iam_role" "ecs_task" {
name = "${var.project_name}-${var.environment}-ecs-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy" "ecs_task" {
name = "permissoes-aplicacao"
role = aws_iam_role.ecs_task.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "LerSecretos"
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [
"arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.atual.account_id}:secret:${var.project_name}/*"
]
},
{
Sid = "AcessoS3"
Effect = "Allow"
Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
Resource = ["${aws_s3_bucket.assets.arn}/*"]
},
{
Sid = "LogsCloudWatch"
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = ["${aws_cloudwatch_log_group.aplicacao.arn}:*"]
}
]
})
}
# IAM Role de execução — permissões do ECS para gerenciar a task
resource "aws_iam_role" "ecs_execution" {
name = "${var.project_name}-${var.environment}-ecs-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy_attachment" "ecs_execution" {
role = aws_iam_role.ecs_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# Permissão adicional para ler segredos durante o pull da task
resource "aws_iam_role_policy" "ecs_execution_secrets" {
name = "ler-segredos-na-inicializacao"
role = aws_iam_role.ecs_execution.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [
"arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.atual.account_id}:secret:${var.project_name}/*"
]
}]
})
}
# Task Definition — define o container da aplicação
resource "aws_ecs_task_definition" "aplicacao" {
family = "${var.project_name}-${var.environment}"
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 = "minha-api"
image = var.app_image
essential = true
portMappings = [{
containerPort = 3000
protocol = "tcp"
}]
# Variáveis de ambiente não sensíveis
environment = [
{ name = "NODE_ENV", value = var.environment },
{ name = "PORT", value = "3000" },
{ name = "LOG_LEVEL", value = "info" },
{ name = "APP_VERSION", value = var.app_version }
]
# Segredos injetados a partir do Secrets Manager
# O ECS busca o valor em tempo de execução e injeta como variável de ambiente
secrets = [
{
name = "DATABASE_URL"
valueFrom = "arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.atual.account_id}:secret:${var.project_name}/${var.environment}:DATABASE_URL::"
},
{
name = "JWT_SECRET"
valueFrom = "arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.atual.account_id}:secret:${var.project_name}/${var.environment}:JWT_SECRET::"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.aplicacao.name
awslogs-region = var.aws_region
awslogs-stream-prefix = "ecs"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
tags = local.tags_comuns
}
# Application Load Balancer
resource "aws_lb" "aplicacao" {
name = "${var.project_name}-${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.load_balancer.id]
subnets = module.vpc.ids_subnets_publicas
enable_deletion_protection = var.environment == "production"
tags = local.tags_comuns
}
resource "aws_lb_target_group" "aplicacao" {
name = "${var.project_name}-${var.environment}-tg"
port = 3000
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
target_type = "ip" # Fargate usa IP, não instância
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 3
interval = 30
path = "/health"
matcher = "200"
timeout = 5
}
deregistration_delay = 30
tags = local.tags_comuns
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.aplicacao.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate.principal.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.aplicacao.arn
}
}
# ECS Service — mantém tasks em execução e integra com o ALB
resource "aws_ecs_service" "aplicacao" {
name = "${var.project_name}-${var.environment}"
cluster = aws_ecs_cluster.principal.id
task_definition = aws_ecs_task_definition.aplicacao.arn
desired_count = var.service_desired_count
launch_type = "FARGATE"
# Configuração de deploy com zero downtime
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
deployment_circuit_breaker {
enable = true
rollback = true # Rollback automático se o deploy falhar
}
network_configuration {
subnets = module.vpc.ids_subnets_privadas
security_groups = [aws_security_group.aplicacao.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.aplicacao.arn
container_name = "minha-api"
container_port = 3000
}
# Permite que o Terraform gerencie o task definition
# sem interferir em deploys feitos via CLI ou pipeline
lifecycle {
ignore_changes = [task_definition, desired_count]
}
tags = local.tags_comuns
}
# Auto Scaling do serviço ECS
resource "aws_appautoscaling_target" "ecs" {
max_capacity = var.service_max_count
min_capacity = var.service_min_count
resource_id = "service/${aws_ecs_cluster.principal.name}/${aws_ecs_service.aplicacao.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
# Escala com base em CPU
resource "aws_appautoscaling_policy" "cpu" {
name = "escala-por-cpu"
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 {
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
}
}
Deploying no ECS via GitHub Actions
# .github/workflows/deploy-ecs.yml
name: Deploy no ECS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configura credenciais AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-1
- name: Login no ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Constrói e publica imagem no ECR
id: build
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/minha-api:$IMAGE_TAG .
docker push $ECR_REGISTRY/minha-api:$IMAGE_TAG
echo "image=$ECR_REGISTRY/minha-api:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Atualiza task definition com a nova imagem
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: infrastructure/task-definition.json
container-name: minha-api
image: ${{ steps.build.outputs.image }}
- name: Deploy no ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: minha-api-production
cluster: minha-api-production
wait-for-service-stability: true
wait-for-minutes: 10
codedeploy-appspec: infrastructure/appspec.json
ECS Exec: Acesso ao Container sem SSH
O ECS Exec permite abrir uma sessão interativa em um container em execução usando o AWS Systems Manager — sem expor portas SSH, sem chaves de acesso:
# Abre um shell interativo em um container Fargate
aws ecs execute-command \
--cluster minha-api-production \
--task TASK_ID \
--container minha-api \
--interactive \
--command "/bin/sh"
# Executa um comando específico
aws ecs execute-command \
--cluster minha-api-production \
--task TASK_ID \
--container minha-api \
--interactive \
--command "node -e 'console.log(process.env.NODE_ENV)'"
# Lista as tasks em execução para obter o TASK_ID
aws ecs list-tasks \
--cluster minha-api-production \
--service-name minha-api-production \
--query 'taskArns[*]' \
--output text
AWS Lambda: Computação Orientada a Eventos
O Lambda executa código em resposta a eventos — uma requisição HTTP via API Gateway, um arquivo novo no S3, uma mensagem em uma fila SQS, um agendamento via EventBridge. A infraestrutura é completamente invisível: sem servidores para provisionar, sem capacidade para planejar, sem patches para aplicar.
O modelo de cobrança é por invocação e por duração de execução — medida em GB-segundos. Uma função que não é invocada não gera custo.
Estrutura de uma Função Lambda
// src/handlers/processar-pedido.js
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const { SQSClient, DeleteMessageCommand } = require('@aws-sdk/client-sqs');
// Clientes SDK inicializados fora do handler — reutilizados entre invocações
const secretsClient = new SecretsManagerClient({ region: process.env.AWS_REGION });
const sqsClient = new SQSClient({ region: process.env.AWS_REGION });
// Cache de configuração — evita buscar segredos a cada invocação
let config = null;
async function carregarConfig() {
if (config) return config;
const resposta = await secretsClient.send(
new GetSecretValueCommand({
SecretId: process.env.SECRET_ARN,
})
);
config = JSON.parse(resposta.SecretString);
return config;
}
// Handler principal — invocado pelo Lambda para cada evento
exports.handler = async (event, context) => {
// context.callbackWaitsForEmptyEventLoop = false evita que o Lambda
// aguarde conexões de banco abertas antes de encerrar
context.callbackWaitsForEmptyEventLoop = false;
const cfg = await carregarConfig();
// O evento pode vir de diferentes fontes — este exemplo processa SQS
const resultados = await Promise.allSettled(
event.Records.map(record => processarMensagem(record, cfg))
);
// Identifica mensagens que falharam para que o SQS as reenvie
const falhas = resultados
.map((resultado, idx) => ({ resultado, record: event.Records[idx] }))
.filter(({ resultado }) => resultado.status === 'rejected')
.map(({ record }) => ({
itemIdentifier: record.messageId,
}));
// Retorno com batchItemFailures permite reprocessar apenas as mensagens que falharam
return { batchItemFailures: falhas };
};
async function processarMensagem(record, cfg) {
const pedido = JSON.parse(record.body);
console.log(JSON.stringify({
level: 'info',
msg: 'Processando pedido',
pedidoId: pedido.id,
requestId: record.messageId,
}));
// Lógica de processamento
await validarPedido(pedido, cfg);
await reservarEstoque(pedido, cfg);
await confirmarPagamento(pedido, cfg);
await notificarCliente(pedido, cfg);
console.log(JSON.stringify({
level: 'info',
msg: 'Pedido processado com sucesso',
pedidoId: pedido.id,
}));
}
Infraestrutura do Lambda com Terraform
# lambda.tf
# Empacota o código da função
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "${path.module}/../src"
output_path = "${path.module}/../dist/funcao.zip"
excludes = ["**/*.test.js", "**/node_modules/.cache/**"]
}
# IAM Role da função Lambda
resource "aws_iam_role" "lambda" {
name = "${var.project_name}-${var.environment}-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basico" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
resource "aws_iam_role_policy" "lambda" {
name = "permissoes-funcao"
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [aws_secretsmanager_secret.config.arn]
},
{
Effect = "Allow"
Action = [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes"
]
Resource = [aws_sqs_queue.pedidos.arn]
}
]
})
}
# A função Lambda
resource "aws_lambda_function" "processar_pedido" {
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
function_name = "${var.project_name}-${var.environment}-processar-pedido"
role = aws_iam_role.lambda.arn
handler = "handlers/processar-pedido.handler"
runtime = "nodejs20.x"
timeout = 30
memory_size = 512
vpc_config {
subnet_ids = module.vpc.ids_subnets_privadas
security_group_ids = [aws_security_group.lambda.id]
}
environment {
variables = {
NODE_ENV = var.environment
SECRET_ARN = aws_secretsmanager_secret.config.arn
REGION = var.aws_region
}
}
# Lambda Layers — dependências compartilhadas entre funções
layers = [aws_lambda_layer_version.dependencias.arn]
tracing_config {
mode = "Active" # Habilita rastreamento com AWS X-Ray
}
tags = local.tags_comuns
}
# Layer com as dependências npm
resource "aws_lambda_layer_version" "dependencias" {
filename = "${path.module}/../dist/camada-dependencias.zip"
layer_name = "${var.project_name}-dependencias"
compatible_runtimes = ["nodejs20.x"]
description = "Dependências npm compartilhadas"
}
# Trigger SQS — invoca a função para cada mensagem na fila
resource "aws_lambda_event_source_mapping" "sqs" {
event_source_arn = aws_sqs_queue.pedidos.arn
function_name = aws_lambda_function.processar_pedido.arn
batch_size = 10
maximum_batching_window_in_seconds = 5
function_response_types = ["ReportBatchItemFailures"]
scaling_config {
maximum_concurrency = 50 # Limita concorrência para proteger o banco de dados
}
}
# Dead Letter Queue — mensagens que falharam múltiplas vezes
resource "aws_sqs_queue" "pedidos_dlq" {
name = "${var.project_name}-${var.environment}-pedidos-dlq"
message_retention_seconds = 1209600 # 14 dias
tags = local.tags_comuns
}
resource "aws_sqs_queue" "pedidos" {
name = "${var.project_name}-${var.environment}-pedidos"
visibility_timeout_seconds = 60 # Deve ser >= timeout do Lambda
message_retention_seconds = 86400
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.pedidos_dlq.arn
maxReceiveCount = 3 # Tenta 3 vezes antes de mover para DLQ
})
tags = local.tags_comuns
}
Quando Usar ECS, Lambda ou EC2
A decisão entre os três modelos depende das características do workload:
EC2 direto — quando o time precisa de controle total sobre o sistema operacional, quando as aplicações têm requisitos específicos de kernel ou hardware, quando os workloads são altamente previsíveis e de longa duração, ou quando o custo de instâncias reservadas supera o overhead operacional. É o modelo com maior responsabilidade operacional e maior controle.
ECS com Fargate — quando o workload é baseado em containers e tem tráfego relativamente estável ou previsível. O tempo de inicialização do Fargate — alguns segundos — é adequado para serviços web que precisam de escalabilidade mas não de resposta em milissegundos. O modelo ideal para APIs REST, workers de background e microsserviços.
Lambda — quando o workload é orientado a eventos, tem picos imprevisíveis de tráfego, executa em menos de 15 minutos, ou quando o custo por invocação é mais econômico que manter containers em execução. O modelo ideal para processamento de filas, webhooks, automações agendadas, transformação de dados e APIs de baixo tráfego.
Critério EC2 ECS/Fargate Lambda
─────────────────────────────────────────────────────────
Controle do SO Total Nenhum Nenhum
Tempo de startup Minutos Segundos Milissegundos*
Duração máxima Ilimitada Ilimitada 15 minutos
Custo ocioso Alto Médio Zero
Escala a zero Não Não Sim
Cold start Não Baixo Sim
Complexidade operat. Alta Média Baixa
Workload ideal Stateful APIs REST Eventos
*Lambda tem cold start — a primeira invocação após um período ocioso inicializa o ambiente de execução, levando de centenas de milissegundos a alguns segundos dependendo do runtime e do tamanho do pacote.
Mitigando Cold Starts no Lambda
O cold start é o principal problema de latência do Lambda. Três estratégias eficazes para mitigá-lo:
Provisioned Concurrency — mantém instâncias pré-inicializadas prontas para responder sem cold start. Tem custo por hora de concorrência provisionada:
resource "aws_lambda_provisioned_concurrency_config" "aplicacao" {
function_name = aws_lambda_function.api.function_name
qualifier = aws_lambda_alias.producao.name
provisioned_concurrent_executions = 5
}
Reduzir o tamanho do pacote — pacotes menores inicializam mais rápido. Lambda Layers separam dependências do código da aplicação, permitindo que o runtime carregue as dependências em paralelo.
Manter o runtime aquecido — para funções de menor criticidade, um EventBridge agendado pode invocar a função a cada minuto com um evento de "ping", evitando que o ambiente de execução seja desalocado:
resource "aws_cloudwatch_event_rule" "manter_aquecido" {
name = "manter-lambda-aquecido"
schedule_expression = "rate(5 minutes)"
}
resource "aws_cloudwatch_event_target" "manter_aquecido" {
rule = aws_cloudwatch_event_rule.manter_aquecido.name
arn = aws_lambda_function.api.arn
input = jsonencode({ source = "warmup" })
}
O Que Vem a Seguir
O próximo artigo aprofunda os serviços de banco de dados gerenciados da AWS — RDS em alta disponibilidade com Multi-AZ, ElastiCache para caching com Redis, e as estratégias de backup e recuperação de desastre que garantem durabilidade dos dados.
Referências para Aprofundamento
Documentação oficial AWS - Amazon ECS Documentation — docs.aws.amazon.com — Documentação completa do ECS, incluindo guias de Fargate, task definitions, serviços e auto scaling. - AWS Lambda Documentation — docs.aws.amazon.com — Referência completa do Lambda, cobrindo runtimes, triggers, limites e boas práticas de performance. - AWS Lambda Power Tuning — github.com — Ferramenta open source que encontra automaticamente a configuração de memória que otimiza custo ou performance para uma função Lambda específica.
Boas práticas - ECS Best Practices Guide — docs.aws.amazon.com — Guia oficial de boas práticas do ECS, cobrindo networking, segurança, storage e auto scaling. - Lambda Operator Guide — docs.aws.amazon.com — Guia de operações do Lambda para produção, cobrindo monitoramento, troubleshooting e otimização de custos.
Comparações e arquitetura - Serverless Land — serverlessland.com — Portal da AWS com padrões de arquitetura serverless, exemplos de integração entre serviços Lambda, SQS, SNS e EventBridge e workshops práticos.