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

Introdução ao Kubernetes: Orquestrando Containers em Escala
Introdução ao Kubernetes: Orquestrando Containers em Escala

A orquestração de containers além de uma única máquina: os componentes do…

Detecção de Anomalias e Análise Inteligente de Logs com IA
Detecção de Anomalias e Análise Inteligente de Logs com IA

Detecção de anomalias por Z-score sobre a API do Prometheus e por que o…

Gitea: Self-Hosted Leve para Times Menores
Gitea: Self-Hosted Leve para Times Menores

Servidor Git self-hosted em um único binário Go: instalação por systemd ou…