Automação de Incidentes com PagerDuty e AWS Lambda: O Guia do SRE

Arquitetura de Integração: Do Evento à Ação Automatizada

Todo ciclo de automação de incidentes começa com uma decisão de arquitetura robusta. O objetivo não é apenas alertar, mas executar ações corretivas definidas em código (runbooks), com auditabilidade e fachada de segurança. A escolha da aws lambda como motor de execução é estratégica: oferece granularidade, cobre as cobranças apenas em execução, e é nativa do ecossistema AWS. No entanto, uma Lambda pura é cega. O pagerduty fornece a inteligência contextual: severidade, metadados do alerta e o gatilho humano/automático. A integração se dá via Evento PagerDuty (ou o mais robusto PagerDuty Events API v2) disparando via Webhook, ou via AWS EventBridge se o ambiente já está orquestrado com SNS/CloudWatch. Para este guia, focaremos no modelo Webhook direto para Lambda, pois minimiza latência e dependências externas.

A arquitetura de referência é simples, mas poderosa. Um alerta é criado no PagerDuty (ex: monitoramento de disco do CloudWatch). Isso dispara um webhook configurado com um URL assinado na AWS API Gateway. A API Gateway autentica a chamada, dispara a Lambda, e a Lambda interage com a AWS API (EC2, RDS, ECS) e atualiza o estado no PagerDuty via sua própria API. É crítico que a Lambda tenha permissões mínimas concedidas via IAM Role, seguindo o princípio do menor privilégio. Um runbook automatizado para “High CPU” pode, por exemplo, escalar um grupo de auto-scaling (ASG) ou iniciar uma instancia de debug.

{
  "source": "PagerDuty",
  "action": "trigger",
  "severity": "error",
  "custom_details": {
    "instance_id": "i-0123456789abcdef0",
    "cpu_percent": "95",
    "region": "us-east-1"
  }
}

Configuração do PagerDuty: Webhooks e a Estrutura do Payload

A ponte entre o SaaS do PagerDuty e sua infraestrutura AWS é definida nos serviços. Navegue até o seu serviço no PagerDuty, vá para “Integrations” e adicione uma nova integração do tipo “API v2”. O que muitos negligenciam é a configuração do Webhook no PagerDuty. É necessário especificar o endpoint URL que a AWS API Gateway irá fornecer. A chave aqui é a “Secret Key” ou “Signing Key” que o PagerDuty usa para assinar o payload. Isso não é opcional; seu Lambda precisa validar essa assinatura para garantir que a chamada é legitima e não um ataque de forgery.

O payload enviado pelo PagerDuty é extenso. Ele contém o objeto `event`, `links`, `data` e, mais importante, `custom_details`. Você deve projetar seus alertas no PagerDuty para enviar a máxima contextualização (Ex: IDs de instância, nomes de database, métricas específicas). Isso evita que sua Lambda tenha que fazer consultas adicionais desnecessárias para inferir o contexto. Um exemplo de payload vindo do PagerDuty via webhook para um incidente de memória alta seria estruturado assim. Observe o campo `images` que contém links visuais, e como `custom_details` é dicionário livre.

{
  "event": {
    "id": "abc123",
    "event_type": "incident.triggered",
    "resource_type": "incident",
    "occurred_at": "2023-10-27T18:00:00Z",
    "severity": "critical",
    "links": [],
    "images": [
      {
        "src": "https://via.placeholder.com/600x400",
        "href": "https://example.com"
      }
    ]
  },
  "data": {
    "incident": {
      "id": "PABC123",
      "type": "incident",
      "title": "High Memory Usage on DB-01",
      "service": {
        "id": "PQWERT",
        "summary": "Production Database",
        "type": "service_reference"
      },
      "assignee": null,
      "urgency": "high",
      "status": "triggered",
      "created_at": "2023-10-27T18:00:00Z",
      "custom_fields": {
        "database_id": "db-01-prod",
        "node_name": "primary-us-east-1a"
      }
    }
  }
}

Definição da Infraestrutura com Terraform (IaC)

Não existe build de automação estável sem IaC. Utilizaremos Terraform para provisionar a Lambda, a API Gateway, a Role IAM e a CloudWatch Log Group. Isso garante que todas as permissões e configurações sejam replicáveis e versionadas. Comece pelo `provider` e pela definição da Role IAM. A Lambda precisará de permissão para escrever logs e, dependendo do runbook, interagir com outros serviços (neste exemplo, concederemos `ec2:DescribeInstances` e `ec2:RebootInstances` como um placeholder para ação de remediation).

provider "aws" {
  region = "us-east-1"
}

resource "aws_iam_role" "lambda_execution_role" {
  name = "pagerduty-automation-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_policy" "lambda_policy" {
  name        = "pagerduty-lambda-policy"
  description = "IAM policy for PagerDuty automation Lambda"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:*:*:*"
      },
      {
        Action = [
          "ec2:DescribeInstances",
          "ec2:RebootInstances",
          "ec2:DescribeTags"
        ]
        Effect   = "Allow"
        Resource = "*"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_policy_attach" {
  role       = aws_iam_role.lambda_execution_role.name
  policy_arn = aws_iam_policy.lambda_policy.arn
}

Em seguida, definimos o código da função Lambda. É uma prática recomendar uma função Lambda compactada (ZIP) ou Docker, mas para simplicidade, usaremos o código inline ou um bucket S3. O exemplo abaixo assume um Dockerfile, que é a melhor prática para ambientes complexos com dependências.

resource "aws_lambda_function" "pagerduty_automation" {
  function_name = "PagerDuty-AutoRemediation"
  description   = "Handles PagerDuty alerts and performs auto-remediation"
  image_uri     = "${aws_ecr_repository.repo.repository_url}:latest"
  package_type  = "Image"
  role          = aws_iam_role.lambda_execution_role.arn
  timeout       = 30
  memory_size   = 256
  environment {
    variables = {
      PAGERDUTY_API_TOKEN = var.pagerduty_api_token
      ENFORCE_SAFETY_MODE = "true"
    }
  }
}

O Motor de Execução: A Lógica da Lambda com Python

Com a infraestrutura definida, a lógica da função precisa ser resiliente e segura. O código deve validar a assinatura do webhook, decodificar o JSON e decidir a ação baseada no payload. A validação de assinatura é o primeiro filtro de segurança. O hash é calculado usando o segredo do PagerDuty.

import os
import json
import hmac
import hashlib
import boto3
from http import HTTPStatus

PAGERDUTY_SIGNING_KEY = os.environ.get('PAGERDUTY_SIGNING_KEY')

def lambda_handler(event, context):
    # 1. Validate PagerDuty Webhook Signature
    headers = event.get('headers', {})
    body = event.get('body')
    
    if not body or not headers:
        return {'statusCode': HTTPStatus.BAD_REQUEST, 'body': 'Missing data'}

    signature = headers.get('x-pagerduty-signature')
    expected_signature = 'v1=' + hmac.new(
        PAGERDUTY_SIGNING_KEY.encode('utf-8'),
        body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_signature):
        return {'statusCode': HTTPStatus.FORBIDDEN, 'body': 'Invalid signature'}

    # 2. Parse and Analyze Payload
    payload = json.loads(body)
    event_type = payload['event']['event_type']
    
    # Process only triggered incidents, ignore acknowledgments/updates
    if event_type != 'incident.triggered':
        return {'statusCode': HTTPStatus.OK, 'body': 'No action required for event type'}

    incident = payload['data']['incident']
    service_name = incident['service']['summary']
    custom_fields = incident.get('custom_fields', {})
    
    # 3. Decision Matrix / Runbook Routing
    # Example: Auto-reboot instance for 'High CPU' on specific service
    if 'High CPU' in incident['title'] and service_name == 'Production Web Tier':
        instance_id = custom_fields.get('instance_id')
        if instance_id:
            print(f"Initiating remediation for instance {instance_id}")
            result = remediate_high_cpu(instance_id)
            update_pagerduty_status(incident['id'], result)
            
    return {'statusCode': HTTPStatus.OK, 'body': 'Processed'}

def remediate_high_cpu(instance_id):
    """Execute AWS API calls for remediation"""
    ec2 = boto3.client('ec2')
    try:
        # Verify instance exists before acting
        response = ec2.describe_instances(InstanceIds=[instance_id])
        if not response['Reservations']:
            return "Instance not found"
            
        # Action: Reboot (Replace with ASG cycle for production safety)
        ec2.reboot_instances(InstanceIds=[instance_id])
        return f"Reboot initiated for {instance_id}"
    except Exception as e:
        print(f"Remediation failed: {str(e)}")
        return f"Failed: {str(e)}"

def update_pagerduty_status(incident_id, note):
    """Add a note to PagerDuty incident via API"""
    api_token = os.environ.get('PAGERDUTY_API_TOKEN')
    headers = {
        'Authorization': f'Token token={api_token}',
        'Content-Type': 'application/json'
    }
    # Implementation of requests to PagerDuty API v2 omitted for brevity
    pass

Segurança e Tratamento de Erros: O que Acontece Quando Falha?

Automatizar incidentes traz risco. Se a Lambda falha silenciosamente, o sistema pode ficar em estado degradado. A primeira linha de defesa é a tratamento de exceções granular no código (bloco try/except). Logs detalhados são vitais. Enviar tudo para o CloudWatch Logs é padrão, mas pense além: use o CloudWatch Logs Insights para criar dashboards de falhas. Estruture seus logs no formato JSON para facilitar a query.

import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def remediate_high_cpu(instance_id):
    try:
        logger.info({"action": "start_remediation", "instance": instance_id, "trigger": "high_cpu"})
        # ... lógica ...
        logger.info({"action": "success", "instance": instance_id})
    except botocore.exceptions.ClientError as e:
        logger.error({"action": "failed", "instance": instance_id, "error": e.response['Error']['Code']})
        # Notificar falha crítica para humanos
        trigger_pagerduty_incident("Automation Failed", f"Lambda failed to reboot {instance_id}")

Outro aspecto crítico é o “circuit breaker” automático. Se a Lambda falhar N vezes consecutivas em um curto período (ex: API da AWS fora do ar), ela deve desativar a automação temporariamente. Isso pode ser implementado verificando o histórico de erros via CloudWatch Metrics ou DynamoDB. A arquitectura deve incluir uma “backdoor” manual sempre: permitir que engenheiros suplantem a automação com um comentário específico no incidente (ex: “@bypass-automation”). A Lambda deve ler o log do incidente antes de atuar.

Monitoramento do SRE: Observability da Automação

Como o SRE monitora o monitor? Precisamos de métricas sobre a própria automação. O AWS Lambda exporta métricas nativas (invocations, errors, duration) para CloudWatch. Crie alarms que disparem PagerDuty se o erro rate da Lambda ultrapassar 5% ou se a duração média estiver anormalmente alta (indicando que os runbooks estão lentos ou falhando).

Além das métricas padrão, injete métricas customizadas dentro do código da Lambda usando o CloudWatch Embedded Metric Format (EMF). Isso permite analisar, por exemplo, “quantidade de reboots automatizados por hora” ou “taxa de sucesso de remediation”. Isso transforma a observabilidade de logs discretos para dashboards acionáveis.

from aws_lambda_powertools import Logger, Metrics

metrics = Metrics()
logger = Logger()

@metrics.log_metrics
def lambda_handler(event, context):
    # ... lógica ...
    metrics.add_metric(name="RemediationAttempts", unit="Count", value=1)
    
    if remediation_successful:
        metrics.add_metric(name="RemediationSuccess", unit="Count", value=1)
    else:
        metrics.add_metric(name="RemediationFailure", unit="Count", value=1)

Testando o Pipeline: Simulação e Validação

Nunca desploye uma Lambda de remediation sem teste. A criação de ambientes de staging é essencial. Use o SAM Local ou o Docker Lambda para testar o handler localmente. Para simular eventos do PagerDuty, utilize o comando `curl` apontando para o endpoint local (via sam local start-api) ou gere payloads de teste estáticos em JSON.

# Simulação de evento local usando JSON

{
  "headers": {
    "x-pagerduty-signature": "v1=assinatura_calculada_aqui"
  },
  "body": "{"event": {"event_type": "incident.triggered"}, "data": {...}}"
}

# Comando para testar via AWS CLI (se o endpoint estiver público em staging)
aws lambda invoke 
  --function-name PagerDuty-AutoRemediation 
  --payload file://event_test.json 
  output.json

Para assinatura de teste no local, você precisa replicar o algoritmo HMAC-SHA256 do PagerDuty. Crie um script Python simples para gerar a assinatura válida com o seu segredo local e o corpo do payload de teste. Isso garante que a validação de segurança da Lambda não bloquee seu próprio teste. Integre esses testes no seu pipeline CI/CD (GitHub Actions, GitLab CI) para rodar antes de cada deploy.

Otimização e Custos: A Realidade Operacional

Automação tem custo direto e indireto. A Lambda, com sua granularidade, é economicamente viável, mas requisições frequentes de alertas críticos podem acumular custos. Monitore o número de invocações através da AWS Cost Explorer com tags específicas. Otimização de cold start é relevante se a latência na remediation for crítica (ex: remediar um overflow de tráfego). Para isso, reserve a função (provisioned concurrency) ou mantenha-a pequena e rápida ( linguagem Go/Rust vs Python ) se a lógica permitir.

Outro ponto é a limpeza de dados. Alertas podem ser redundantes. Implemente um cache em DynamoDB (TTL de 5 minutos) para evitar que a mesma instância seja reiniciada múltiplas vezes num curto espaço de tempo, ou que uma ação seja disparada enquanto outra ainda está em andamento. A infraestrutura de “runbooks” deve ter idempotência garantida. A mesma chamada de API (ex: reboot) aplicada duas vezes deve ser segura, mas evitar o workload extra é o ideal.