ECS e Lambda: Containers e Serverless na AWS

[304] ECS e Lambda: Containers e Serverless na AWS

Os serviços gerenciados de computação da AWS e seus trade-offs: os conceitos de cluster, task definition, task e service no ECS, o modo Fargate que dispensa gerenciar instâncias, as funções Lambda e seus limites de execução, e o critério para escolher entre EC2, containers e serverless.
DevOps

23 min de leitura

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

Boas práticas

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.

Exercícios

Exercício 1

Explique a diferença entre Task Definition, Task e Service no ECS. Se você precisa garantir que três cópias da API estejam sempre no ar, atrás de um load balancer, qual desses objetos assume essa responsabilidade?

Ver resposta

✓ Resposta: A Task Definition é o blueprint: descreve a imagem Docker, CPU e memória, variáveis de ambiente, volumes, rede e as IAM roles. É apenas uma definição versionada — não executa nada por si só. A Task é uma instância em execução dessa definição, equivalente a um pod no Kubernetes. O Service é o controlador que mantém um número desejado de tasks rodando, integra com o load balancer, faz atualizações sem downtime e escala automaticamente.

A responsabilidade de manter as três cópias no ar é do Service. É a distinção que importa na prática: uma task iniciada avulsa que morrer simplesmente deixa de existir; uma task gerenciada por um Service que morrer é substituída, porque o Service compara continuamente o estado desejado com o real — a mesma lógica declarativa do Terraform, aplicada em tempo de execução.

Exercício 2

A task definition declara duas IAM roles: execution_role_arn e task_role_arn. Qual é a diferença entre elas, quem as usa e em que momento? Por que ambas acabaram recebendo permissão de ler o Secrets Manager?

Ver resposta

✓ Resposta: A execution role é usada pelo próprio agente do ECS, antes de o container subir: é com ela que o ECS baixa a imagem do registro, cria os log streams no CloudWatch e resolve os valores declarados no bloco secrets. A task role é usada pelo código da aplicação, durante a execução: é a identidade que o SDK da AWS assume dentro do container para acessar S3, Secrets Manager e o que mais for preciso.

São dois momentos e dois atores distintos — daí a separação. É por isso que a execution role recebe a policy gerenciada AmazonECSTaskExecutionRolePolicy, voltada à infraestrutura, enquanto a task role recebe uma policy sob medida com as permissões do negócio.

Ambas leem o Secrets Manager por razões diferentes: a execution role precisa buscar DATABASE_URL e JWT_SECRET na inicialização, para injetá-los como variáveis de ambiente do container — sem essa permissão a task nem chega a iniciar. A task role precisa porque a aplicação pode buscar outros segredos em runtime, com o SDK. Confundir as duas produz um erro clássico: a task falha ao iniciar com erro de permissão em um segredo, e o time perde tempo ajustando a policy da task role, que não é a envolvida naquele passo.

Exercício 3

Compare os três modelos de computação segundo a tabela do artigo, nestes critérios: duração máxima, escala a zero e custo ocioso. Para cada caso abaixo, escolha o modelo e justifique:

  • A — worker que processa uma fila SQS com picos imprevisíveis, cada mensagem levando ~10 s
  • B — API REST com tráfego estável durante o dia comercial
  • C — job de processamento de vídeo que roda por 40 minutos seguidos
Ver resposta

✓ Resposta: Na tabela: duração máxima é ilimitada em EC2 e ECS/Fargate, mas de 15 minutos no Lambda. Escala a zero só o Lambda faz. Custo ocioso é alto no EC2, médio no Fargate e zero no Lambda.

A — Lambda. É o caso ideal: orientado a eventos, com picos imprevisíveis e execução muito abaixo dos 15 minutos. Escala a zero significa não pagar nada nos períodos sem mensagens, e o pico é absorvido sem provisionar nada. É exatamente o exemplo do handler SQS do artigo.

B — ECS com Fargate. Tráfego estável e previsível não se beneficia da escala a zero, e a inicialização em segundos é adequada para uma API web. Manter containers rodando sai mais barato que pagar por invocação em volume contínuo, e não há cold start afetando a latência.

C — EC2 ou Fargate, nunca Lambda. Aqui a decisão é forçada por um limite absoluto: 40 minutos excedem o teto de 15 do Lambda, e a função seria encerrada no meio. Entre os dois restantes, EC2 se o job exigir hardware específico (GPU para codificação) ou controle do sistema operacional; Fargate se um container comum resolver.

Exercício 4

No handler Lambda, os clientes do SDK e a função carregarConfig ficam fora do handler, com let config = null como cache. Por que essa organização importa, e o que aconteceria se tudo estivesse dentro do exports.handler?

Ver resposta

✓ Resposta: Porque o Lambda reaproveita o ambiente de execução entre invocações próximas. O código fora do handler roda uma única vez, durante o cold start; o handler roda a cada evento. Deixando os clientes do SDK e o cache de configuração no escopo do módulo, eles sobrevivem entre invocações — as chamadas seguintes reutilizam a conexão já estabelecida e o carregarConfig retorna imediatamente pelo if (config) return config, sem tocar o Secrets Manager.

Se tudo estivesse dentro do handler, cada invocação criaria novos clientes do SDK e faria uma nova chamada ao Secrets Manager. O efeito é triplo: latência somada a toda requisição, custo extra de API calls do Secrets Manager, e risco de throttling sob carga. Uma função que processa milhares de mensagens faria milhares de buscas do mesmo segredo imutável.

O context.callbackWaitsForEmptyEventLoop = false é o complemento disso: sem ele, o Lambda esperaria o event loop esvaziar antes de encerrar a invocação — e conexões de banco mantidas abertas propositalmente para reuso manteriam a função pendurada até o timeout, cobrando por tempo ocioso.

Exercício 5

O que é cold start e por que ele é o principal problema de latência do Lambda? Descreva as três estratégias de mitigação do artigo com seus custos. E o que o retorno { batchItemFailures: falhas } resolve no processamento de SQS?

Ver resposta

✓ Resposta: Cold start é a inicialização do ambiente de execução na primeira invocação após um período ocioso — provisionar o runtime, carregar o pacote e rodar o código de inicialização. Custa de centenas de milissegundos a alguns segundos, conforme o runtime e o tamanho do pacote. É problemático porque atinge justamente o usuário que chega depois de um vale de tráfego, de forma imprevisível.

  • Provisioned Concurrency — mantém instâncias pré-inicializadas prontas. Elimina o cold start, mas tem custo por hora de concorrência provisionada, o que abre mão da principal vantagem do Lambda: o custo ocioso zero.
  • Reduzir o tamanho do pacote — pacotes menores carregam mais rápido; Lambda Layers separam dependências do código. Não tem custo direto, mas exige disciplina de build.
  • Manter aquecido — um EventBridge agendado invoca a função periodicamente com um ping. É barato, porém frágil: mantém uma instância viva, e um pico que exija dez concorrentes ainda sofre cold start nas outras nove.

O batchItemFailures resolve o problema do lote parcialmente falho. Sem ele, se uma única mensagem de um lote de dez falhasse, o SQS reenviaria o lote inteiro — reprocessando as nove que já tinham dado certo, com risco de efeitos duplicados. Retornando a lista de itemIdentifier das que falharam, apenas essas voltam para a fila. É também o motivo de o código usar Promise.allSettled em vez de Promise.all: é preciso deixar todas as mensagens terminarem e coletar quais rejeitaram, em vez de abortar na primeira falha.

Comentários

Mais em DevOps

Azure para Quem Já Conhece AWS
Azure para Quem Já Conhece AWS

Um mapa de tradução para quem já domina a AWS: a hierarquia de tenant…

Rastreamento Distribuído com OpenTelemetry
Rastreamento Distribuído com OpenTelemetry

O que fazer quando a latência sobe e os logs não acusam nada: a instrumentação…

Capstone: Pipeline Completo de CI/CD
Capstone: Pipeline Completo de CI/CD

O pipeline tratado como produto de engenharia, em que o desenvolvedor é o…