DevOps Admin

O que Todo Dev Deve Saber sobre AWS Avançado: EKS, ECS, Lambda e RDS em Ambientes Reais Já leu

Arquitetura Moderna na AWS: Da Orquestração de Contêineres ao Serverless A computação em nuvem evoluiu significativamente nos últimos anos, e a AWS oferece múltiplos caminhos para executar suas aplicações. Quando você trabalha com ambientes reais e em escala, precisar escolher entre EKS (Elastic Kubernetes Service), ECS (Elastic Container Service), Lambda e banco de dados gerenciado (RDS) não é trivial. Cada um desses serviços resolve um problema específico, e um arquiteto experiente sabe quando usar qual ferramenta. Este artigo é um guia prático baseado em anos de implementação em produção, não em teoria genérica. Vamos explorar quando e como usar cada um desses serviços, com exemplos reais que você pode adaptar para seu contexto. O Contexto Real: Por que essa combinação importa Em uma empresa que cresce, você não usa apenas um serviço. Você pode ter microserviços rodando em ECS, jobs assíncronos em Lambda, e um banco de dados RDS compartilhado. Essa abordagem híbrida é comum e necessária. A diferença entre

Arquitetura Moderna na AWS: Da Orquestração de Contêineres ao Serverless

A computação em nuvem evoluiu significativamente nos últimos anos, e a AWS oferece múltiplos caminhos para executar suas aplicações. Quando você trabalha com ambientes reais e em escala, precisar escolher entre EKS (Elastic Kubernetes Service), ECS (Elastic Container Service), Lambda e banco de dados gerenciado (RDS) não é trivial. Cada um desses serviços resolve um problema específico, e um arquiteto experiente sabe quando usar qual ferramenta. Este artigo é um guia prático baseado em anos de implementação em produção, não em teoria genérica. Vamos explorar quando e como usar cada um desses serviços, com exemplos reais que você pode adaptar para seu contexto.

O Contexto Real: Por que essa combinação importa

Em uma empresa que cresce, você não usa apenas um serviço. Você pode ter microserviços rodando em ECS, jobs assíncronos em Lambda, e um banco de dados RDS compartilhado. Essa abordagem híbrida é comum e necessária. A diferença entre um arquiteto júnior e um sênior é entender os trade-offs: custo, latência, escalabilidade, equipe e complexidade operacional.


ECS: Orquestração Simplificada para Contêineres

O que é ECS e quando usar

ECS (Elastic Container Service) é o serviço de orquestração de contêineres nativo da AWS. Diferentemente do Kubernetes (usado no EKS), ECS é mais simples, tem menos abstrações e integra-se perfeitamente com outros serviços AWS. Use ECS quando você precisa de uma solução rápida, sua equipe não tem expertise em Kubernetes, ou você quer reduzir overhead operacional.

A arquitetura do ECS é baseada em Tasks (unidades de trabalho) e Services (grupo de tasks rodando continuamente). Uma Task é essencialmente um pod do Kubernetes, mas sem a complexidade. Você define um Task Definition (parecido com um Deployment do Kubernetes), especifica recursos, variáveis de ambiente, e portas expostas. O ECS então gerencia onde essas tasks rodam—em EC2 ou Fargate (serverless).

Exemplo Prático: Deploy de uma API REST em ECS com Fargate

Imagine uma API Node.js simples que você quer rodar em ECS. Primeiro, você cria um Dockerfile:

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

O arquivo server.js:

const express = require('express');
const app = express();

app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date() });
});

app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from ECS', version: '1.0' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Agora você cria a Task Definition via AWS CLI ou Terraform. Aqui está um exemplo em JSON (você pode salvar como task-definition.json):

{
  "family": "api-service",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [
    {
      "name": "api-container",
      "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "NODE_ENV",
          "value": "production"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/api-service",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole"
}

Para fazer deploy, você usa:

aws ecs register-task-definition --cli-input-json file://task-definition.json

aws ecs create-service \
  --cluster my-cluster \
  --service-name api-service \
  --task-definition api-service:1 \
  --desired-count 2 \
  --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-xxx],assignPublicIp=ENABLED}"

Isso cria um serviço com 2 replicas rodando em Fargate. A AWS gerencia automaticamente a escalabilidade horizontal, health checks e reinicializações.

Diferenças entre EC2 e Fargate no ECS

Quando você cria um cluster ECS, pode escolher EC2 ou Fargate. EC2: você gerencia instâncias, patches, segurança. Mais barato em workloads estáveis, mas você tem overhead operacional. Fargate: você paga por CPU e memória consumidos, sem gerenciar infraestrutura. Ideal para aplicações com padrões impredizíveis ou para reduzir toil operacional. Em minha experiência, Fargate é a escolha padrão para novas implementações, a menos que haja restrição orçamentária severa.


EKS: Kubernetes para Quem Precisa de Potência e Portabilidade

Por que Kubernetes? O dilema arquitetural

EKS (Elastic Kubernetes Service) é Kubernetes gerenciado na AWS. Use quando você precisa de: portabilidade (rodar em múltiplas clouds), controle granular (network policies, RBAC complexo), equipe experiente em K8s, ou ecossistema Kubernetes (Helm, ArgoCD, operadores). Se nenhum desses se aplica, ECS é mais simples. Kubernetes tem curva de aprendizado íngreme, e overhead operacional significativo.

Kubernetes organiza recursos de forma diferente do ECS. Você tem Pods (menor unidade), ReplicaSets (garante N replicas), Deployments (estratégia de atualização), Services (rede interna), e muito mais. Para um projeto novo em uma equipe sem K8s, use ECS. Para integração com ferramentas open-source ou múltiplas clouds, use EKS.

Exemplo Prático: Deploy em EKS com Kubernetes Manifests

Você já tem a imagem Docker. Agora cria um manifesto Kubernetes:

# api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
  namespace: default
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
      - name: api
        image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:latest
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: url
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "512Mi"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector:
    app: api
  type: LoadBalancer
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000

Deploy com kubectl:

# Primeiro, crie o cluster EKS (via AWS Console ou CLI)
aws eks create-cluster \
  --name my-cluster \
  --version 1.28 \
  --roleArn arn:aws:iam::123456789:role/eks-service-role \
  --resourcesVpcConfig subnetIds=subnet-xxx,subnet-yyy

# Aguarde o cluster ficar pronto (pode levar 10-15 minutos)

# Configure kubectl
aws eks update-kubeconfig --region us-east-1 --name my-cluster

# Crie um node group (máquinas que rodam os pods)
aws eks create-nodegroup \
  --cluster-name my-cluster \
  --nodegroup-name my-nodegroup \
  --scaling-config minSize=2,maxSize=10,desiredSize=3 \
  --subnets subnet-xxx subnet-yyy \
  --nodeRole arn:aws:iam::123456789:role/eks-nodegroup-role

# Deploy a aplicação
kubectl apply -f api-deployment.yaml

# Verifique o status
kubectl get deployments
kubectl get pods
kubectl get svc

Escalabilidade em EKS com Horizontal Pod Autoscaler

Em ambientes reais, você quer que o Kubernetes escale automaticamente. Para isso, instale o Metrics Server e use HPA:

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-deployment
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Aplique com: kubectl apply -f hpa.yaml. Agora seu deployment escala automaticamente entre 2 e 10 replicas conforme a carga.


Lambda: Computação Serverless para Workloads Específicos

Quando Lambda faz sentido

Lambda é função-como-serviço (FaaS). Você escreve código, faz upload, e paga apenas pelo tempo de execução. Use Lambda para: jobs assíncronos (processar imagens, enviar emails), APIs de baixa latência com tráfego esporádico, webhooks e integrações, processamento de eventos (S3, DynamoDB streams). Não use para aplicações monolíticas, workloads de longa duração (máximo 15 minutos), ou que precisem estado persistente complexo.

Lambda é gerenciado, logo sem preocupação com infraestrutura. Você escreve código, o AWS gerencia escala, patching, rede. O modelo de custo é por execução: você paga pela quantidade de invocações e pelo tempo de CPU consumido (GB-segundo). Para aplicações que ficam ociosas boa parte do tempo, Lambda é muito mais barato.

Exemplo Prático: Processamento de Imagens com Lambda e S3

Imagine que você quer processar imagens quando elas são uploadadas no S3. Cria uma função Lambda (em Python):

# lambda_function.py
import json
import boto3
import urllib.parse
from PIL import Image
from io import BytesIO

s3_client = boto3.client('s3')

def lambda_handler(event, context):
    """
    Processada acionada quando imagem é enviada para S3.
    Redimensiona a imagem e salva thumbnail em outro bucket.
    """

    # Extrai informação do evento S3
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])

    try:
        # Download da imagem original
        response = s3_client.get_object(Bucket=bucket, Key=key)
        image_data = response['Body'].read()

        # Processa com Pillow
        image = Image.open(BytesIO(image_data))
        image.thumbnail((200, 200))

        # Salva thumbnail em outro bucket
        thumb_buffer = BytesIO()
        image.save(thumb_buffer, format='JPEG')
        thumb_buffer.seek(0)

        thumb_key = f"thumbnails/{key}"
        s3_client.put_object(
            Bucket='my-thumbnails-bucket',
            Key=thumb_key,
            Body=thumb_buffer.getvalue(),
            ContentType='image/jpeg'
        )

        return {
            'statusCode': 200,
            'body': json.dumps(f'Thumbnail criado: {thumb_key}')
        }

    except Exception as e:
        print(f'Erro: {str(e)}')
        return {
            'statusCode': 500,
            'body': json.dumps(f'Erro ao processar: {str(e)}')
        }

O arquivo requirements.txt para deploy:

Pillow==10.0.0
boto3==1.28.0

Deploy via AWS CLI:

# Instale dependências localmente
pip install -r requirements.txt -t lambda_package/

# Copie seu código
cp lambda_function.py lambda_package/

# Crie ZIP
cd lambda_package && zip -r ../function.zip . && cd ..

# Crie a função (primeira vez)
aws lambda create-function \
  --function-name image-processor \
  --runtime python3.11 \
  --role arn:aws:iam::123456789:role/lambda-role \
  --handler lambda_function.lambda_handler \
  --zip-file fileb://function.zip \
  --timeout 60 \
  --memory-size 512 \
  --environment Variables={THUMBNAIL_BUCKET=my-thumbnails-bucket}

# Atualize a função (em desenvolvimentos futuros)
aws lambda update-function-code \
  --function-name image-processor \
  --zip-file fileb://function.zip

Agora crie um trigger para que Lambda seja acionada quando uma imagem chegar no S3:

aws s3api put-bucket-notification-configuration \
  --bucket my-images-bucket \
  --notification-configuration '{
    "LambdaFunctionConfigurations": [
      {
        "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789:function:image-processor",
        "Events": ["s3:ObjectCreated:*"],
        "Filter": {
          "Key": {
            "FilterRules": [
              {
                "Name": "suffix",
                "Value": ".jpg"
              }
            ]
          }
        }
      }
    ]
  }'

Lambda com RDS: Conexão e Boas Práticas

Quando sua função Lambda precisa acessar um banco RDS, há desafios: cold starts (inicialização lenta da função), conexões de longa vida (Lambda por padrão termina conexões rapidamente). Aqui está uma abordagem robusta em Python:

# lambda_with_rds.py
import json
import psycopg2
import os
from psycopg2 import pool

# Crie um connection pool (reutiliza conexões entre invocações)
connection_pool = None

def get_db_connection():
    global connection_pool

    if connection_pool is None:
        connection_pool = psycopg2.pool.SimpleConnectionPool(
            1, 5,  # min 1, max 5 conexões
            host=os.getenv('DB_HOST'),
            database=os.getenv('DB_NAME'),
            user=os.getenv('DB_USER'),
            password=os.getenv('DB_PASSWORD')
        )

    return connection_pool.getconn()

def lambda_handler(event, context):
    """
    Consulta banco de dados e retorna resultado.
    """
    conn = None
    try:
        conn = get_db_connection()
        cursor = conn.cursor()

        # Exemplo: busca usuários
        cursor.execute("SELECT id, email FROM users LIMIT 10")
        rows = cursor.fetchall()

        cursor.close()
        connection_pool.putconn(conn)

        return {
            'statusCode': 200,
            'body': json.dumps({
                'users': [{'id': row[0], 'email': row[1]} for row in rows]
            })
        }

    except Exception as e:
        if conn:
            connection_pool.putconn(conn)
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

Deploy com variáveis de ambiente:

aws lambda update-function-configuration \
  --function-name my-db-function \
  --environment Variables='{
    DB_HOST=mydb.c9akciq32.us-east-1.rds.amazonaws.com,
    DB_NAME=production,
    DB_USER=admin,
    DB_PASSWORD=SecurePassword123
  }'

RDS: Banco de Dados Gerenciado para Persistência

Arquitetura e Configuração de RDS

RDS (Relational Database Service) gerencia bancos SQL (PostgreSQL, MySQL, MariaDB, Oracle, SQL Server). Você não se preocupa com patches, backups automáticos, replicação—tudo é automático. RDS é essencial quando suas aplicações precisam persistência robusta e ACID compliance.

RDS oferece várias opções de deployment: Single-AZ (simples, menos resiliente), Multi-AZ (replica automática em outra zona, failover instantâneo), e Read Replicas (cópias apenas-leitura para distribuir carga de leitura). Em produção, use Multi-AZ. O custo é dobrado, mas a confiabilidade é incomparável.

Exemplo Prático: RDS Multi-AZ com PostgreSQL e Terraform

Aqui está como provisionar um RDS robusto usando Terraform:

# rds.tf
provider "aws" {
  region = "us-east-1"
}

resource "aws_db_subnet_group" "main" {
  name       = "main-subnet-group"
  subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id]

  tags = {
    Name = "Main subnet group"
  }
}

resource "aws_security_group" "rds_sg" {
  name   = "rds-security-group"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 5432
    to_port     = 5432
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]  # Permite de dentro da VPC
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "RDS SG"
  }
}

resource "aws_db_instance" "production" {
  identifier            = "production-db"
  engine                = "postgres"
  engine_version        = "15.3"
  instance_class        = "db.t3.small"  # 2 vCPU, 2GB RAM
  allocated_storage     = 100
  storage_type          = "gp3"
  storage_encrypted     = true

  db_name               = "production"
  username              = "postgres"
  password              = var.db_password  # Use AWS Secrets Manager

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds_sg.id]

  # Multi-AZ para alta disponibilidade
  multi_az               = true

  # Backups automáticos
  backup_retention_period = 30
  backup_window           = "03:00-04:00"

  # Janela de manutenção
  maintenance_window      = "mon:04:00-mon:05:00"

  # Enable enhanced monitoring
  enable_cloudwatch_logs_exports = ["postgresql"]

  # Snapshots automáticos antes de deletar
  skip_final_snapshot = false
  final_snapshot_identifier = "production-db-final-snapshot"

  tags = {
    Name = "Production PostgreSQL"
  }
}

variable "db_password" {
  type      = string
  sensitive = true
}

output "rds_endpoint" {
  value = aws_db_instance.production.endpoint
}

Deploy com:

terraform plan
terraform apply

Exemplo Prático: Migração de Dados para RDS

Quando você tem um banco local ou em outro servidor, precisa migrar. Aqui está uma abordagem com pg_dump:

# 1. Dump do banco antigo (local ou outro servidor)
pg_dump -h old-server.com -U admin -d production > dump.sql

# 2. Restaure no RDS
psql -h production-db.c9akciq32.us-east-1.rds.amazonaws.com \
     -U postgres \
     -d production \
     -f dump.sql

Para garantir consistência em tabelas grandes, use AWS DMS (Database Migration Service), que sincroniza em tempo real. Mas para dados iniciais menores, o pg_dump é suficiente.

Integração com Aplicações: Connection Pooling

Aplicações conectam-se ao RDS via string de conexão. Para reduzir overhead, use connection pooling. Aqui está um exemplo com Node.js e pg:

// dbPool.js
const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST,
  port: 5432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 20,  // Máximo 20 conexões simultâneas
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

pool.on('error', (err) => {
  console.error('Pool error:', err);
});

module.exports = pool;

Usar em sua aplicação:

// api.js
const express = require('express');
const pool = require('./dbPool');

const app = express();

app.get('/users/:id', async (req, res) => {
  try {
    const result = await pool.query(
      'SELECT id, email, name FROM users WHERE id = $1',
      [req.params.id]
    );
    res.json(result.rows[0]);
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Database error' });
  }
});

app.listen(3000);

Orquestração Híbrida: Juntando Tudo

Padrão Arquitetural Recomendado

Em produção, você raramente usa apenas um serviço. Um padrão comum é: ECS (ou EKS) para aplicações síncronas, Lambda para processamento assíncronos, RDS como banco central. Aqui está um diagrama conceitual:

[Client] → [ALB] → [ECS/EKS Tasks] → [RDS]
                         ↓
                    [SQS Queue]
                         ↓
                    [Lambda Function] → [S3/DynamoDB]

A aplicação principal roda em ECS/EKS. Quando precisa processar algo longo (redimensionar imagem, enviar email), publica mensagem em SQS. Uma Lambda consome a fila e processa. RDS é o repositório central.

Exemplo Real: Integração entre ECS e Lambda via SQS

Sua aplicação ECS publica mensagens:

# ecs_application.py
import boto3
import json

sqs = boto3.client('sqs')
QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789/my-queue'

def process_order(order_id):
    """
    Quando um pedido é criado, envia para fila SQS.
    Lambda vai processar o pagamento.
    """
    message = {
        'order_id': order_id,
        'action': 'process_payment'
    }

    sqs.send_message(
        QueueUrl=QUEUE_URL,
        MessageBody=json.dumps(message)
    )

    return {'status': 'queued'}

Lambda consome:

# lambda_consumer.py
import json
import boto3

def lambda_handler(event, context):
    """
    Consome mensagens da SQS e processa pagamentos.
    """
    for record in event['Records']:
        body = json.loads(record['body'])
        order_id = body['order_id']

        # Processa pagamento (simulado)
        print(f'Processando pagamento para order {order_id}')

        # Aqui você chamaria uma API de pagamento (Stripe, etc)
        # payment_result = stripe.charge(...)

        # Atualiza banco de dados (via RDS)
        # update_order_status(order_id, 'paid')

    return {'statusCode': 200}

Configure Lambda para consumir SQS via trigger no AWS Console ou:

aws lambda create-event-source-mapping \
  --event-source-arn arn:aws:sqs:us-east-1:123456789:my-queue \
  --function-name payment-processor \
  --batch-size 10

Conclusão

Você aprendeu três coisas fundamentais sobre arquitetura AWS em escala. Primeira: ECS é mais simples que EKS e funciona bem para a maioria dos casos; use Kubernetes apenas se realmente precisar de portabilidade ou se sua equipe já domina. Segunda: Lambda não substitui servidores—é complementar. Use para jobs assíncronos, integrações, webhooks. Misture com ECS/EKS para uma arquitetura completa. Terceira: RDS é a base sólida. Multi-AZ, backups automáticos, connection pooling—esses padrões não são opcionais em produção, são fundamentais.

O sucesso em AWS avançado não é memorizar CLIs ou sintaxe. É entender os trade-offs entre simplicidade operacional (ECS) vs. flexibilidade (EKS), entre custo (EC2) vs. conveniência (Fargate), entre serverless (Lambda) e aplicações contínuas (ECS/EKS). Implemente esses conceitos em seu próximo projeto, adapte para seu contexto, e você terá arquitetura que escala.


Referências


Artigos relacionados