Ir para o conteúdo
Ferramenta para Plataforma1481 palavras

Grafana para AWS: Guia Completo de Deploy

Introdução

Monitoramento não é opcional. Se você já passou por aquela situação de receber uma ligação às três da manhã porque a aplicação caiu e ninguém sabe o motivo, entende exatamente o que quero dizer. A diferença entre uma equipe que apaga incêndios e uma equipe que previne incidentes está, quase sempre, na qualidade da observabilidade.

Grafana se consolidou como a ferramenta padrão de mercado para visualização de métricas, logs e traces. A AWS, por sua vez, é a plataforma de nuvem mais utilizada no mundo. Combinar as duas coisas parece óbvio, mas fazer isso de forma correta, escalável e segura exige conhecimento que vai além de seguir um tutorial básico.

Neste guia, vou mostrar como configurar Grafana na AWS do zero, com exemplos práticos em TypeScript usando AWS CDK, integração com CloudWatch e boas práticas que aprendi em mais de uma década operando sistemas em produção. Se você já tem pelo menos dois anos de experiência como desenvolvedor e quer subir de nível em observabilidade, este conteúdo é para você.

Por que usar Grafana com AWS

O problema que estamos resolvendo

A AWS oferece o CloudWatch como solução nativa de monitoramento. Funciona bem para casos simples, mas apresenta limitações significativas quando você precisa de dashboards avançados, correlação entre múltiplas fontes de dados ou alertas sofisticados. O custo também escala de forma imprevisível quando você começa a criar dashboards customizados no próprio CloudWatch.

Grafana resolve esses problemas ao atuar como uma camada de visualização unificada. Você pode conectar CloudWatch, Prometheus, Elasticsearch, PostgreSQL e dezenas de outras fontes de dados em um único painel. Isso significa que a equipe de desenvolvimento, SRE e produto podem consultar o mesmo lugar para entender o estado do sistema.

Casos de uso reais onde essa combinação brilha

O primeiro cenário é o de plataformas de e-commerce com microsserviços na AWS. Imagine que você tem 15 serviços rodando em ECS, cada um com suas métricas no CloudWatch, logs no CloudWatch Logs e traces no X-Ray. Sem Grafana, você ficaria alternando entre três consoles diferentes. Com Grafana, tudo aparece em um dashboard unificado que mostra a latência do API Gateway, o consumo de CPU dos containers ECS e os erros por serviço, tudo correlacionado por tempo.

O segundo cenário envolve startups SaaS que precisam monitorar custos e performance simultaneamente. Grafana permite criar painéis que mostram o custo por hora da infraestrutura (via CloudWatch Billing) ao lado das métricas de performance, facilitando decisões sobre otimização.

O terceiro caso é o de equipes que operam ambientes multi-account na AWS. Grafana suporta assumir roles em diferentes contas AWS, consolidando métricas de desenvolvimento, staging e produção em uma única interface.

Setup do Grafana na AWS com CDK

Opção 1: Amazon Managed Grafana

A AWS oferece o Amazon Managed Grafana (AMG), que elimina a necessidade de gerenciar a infraestrutura do Grafana. Essa é a opção recomendada para a maioria dos casos. No entanto, vou mostrar também o deploy self-hosted, porque existem situações onde o custo ou a necessidade de customização justificam essa abordagem.

Opção 2: Grafana Self-Hosted no ECS com AWS CDK

Vamos ao primeiro exemplo prático. O código abaixo provisiona um cluster ECS com Fargate rodando Grafana, incluindo um Application Load Balancer, armazenamento persistente com EFS e integração com o CloudWatch via IAM Role.

TYPESCRIPT
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as efs from 'aws-cdk-lib/aws-efs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { Construct } from 'constructs';
 
export class GrafanaStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
 
    const vpc = new ec2.Vpc(this, 'GrafanaVpc', {
      maxAzs: 2,
      natGateways: 1,
    });
 
    const cluster = new ecs.Cluster(this, 'GrafanaCluster', {
      vpc,
      clusterName: 'grafana-cluster',
    });
 
    const fileSystem = new efs.FileSystem(this, 'GrafanaEfs', {
      vpc,
      performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      encrypted: true,
    });
 
    const accessPoint = fileSystem.addAccessPoint('GrafanaAccessPoint', {
      path: '/grafana',
      posixUser: { uid: '472', gid: '0' },
      createAcl: { ownerUid: '472', ownerGid: '0', permissions: '755' },
    });
 
    const taskRole = new iam.Role(this, 'GrafanaTaskRole', {
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
    });
 
    taskRole.addToPolicy(new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        'cloudwatch:DescribeAlarmsForMetric',
        'cloudwatch:DescribeAlarmHistory',
        'cloudwatch:DescribeAlarms',
        'cloudwatch:ListMetrics',
        'cloudwatch:GetMetricData',
        'cloudwatch:GetInsightRuleReport',
        'logs:DescribeLogGroups',
        'logs:GetLogGroupFields',
        'logs:StartQuery',
        'logs:StopQuery',
        'logs:GetQueryResults',
        'logs:GetLogEvents',
        'ec2:DescribeTags',
        'ec2:DescribeInstances',
        'ec2:DescribeRegions',
        'tag:GetResources',
      ],
      resources: ['*'],
    }));
 
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'GrafanaTask', {
      memoryLimitMiB: 1024,
      cpu: 512,
      taskRole,
    });
 
    taskDefinition.addVolume({
      name: 'grafana-storage',
      efsVolumeConfiguration: {
        fileSystemId: fileSystem.fileSystemId,
        transitEncryption: 'ENABLED',
        authorizationConfig: {
          accessPointId: accessPoint.accessPointId,
          iam: 'ENABLED',
        },
      },
    });
 
    const container = taskDefinition.addContainer('grafana', {
      image: ecs.ContainerImage.fromRegistry('grafana/grafana-oss:11.3.0'),
      logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'grafana' }),
      environment: {
        GF_SECURITY_ADMIN_PASSWORD: 'CHANGE_ME_USE_SECRETS_MANAGER',
        GF_SERVER_ROOT_URL: 'https://grafana.seudominio.com.br',
        GF_AWS_PROFILES: 'default',
        GF_AWS_default_REGION: 'sa-east-1',
      },
      portMappings: [{ containerPort: 3000 }],
    });
 
    container.addMountPoints({
      sourceVolume: 'grafana-storage',
      containerPath: '/var/lib/grafana',
      readOnly: false,
    });
 
    const service = new ecs.FargateService(this, 'GrafanaService', {
      cluster,
      taskDefinition,
      desiredCount: 1,
      assignPublicIp: false,
    });
 
    fileSystem.connections.allowDefaultPortFrom(service);
 
    const alb = new elbv2.ApplicationLoadBalancer(this, 'GrafanaAlb', {
      vpc,
      internetFacing: true,
    });
 
    const listener = alb.addListener('HttpListener', {
      port: 80,
    });
 
    listener.addTargets('GrafanaTarget', {
      port: 3000,
      targets: [service],
      healthCheck: {
        path: '/api/health',
        interval: cdk.Duration.seconds(30),
      },
    });
 
    new cdk.CfnOutput(this, 'GrafanaUrl', {
      value: `http://${alb.loadBalancerDnsName}`,
    });
  }
}

Para fazer o deploy, execute os seguintes comandos:

Bash
npm install aws-cdk-lib constructs
npx cdk bootstrap
npx cdk deploy GrafanaStack

Uma observação importante sobre o código acima: em produção, substitua a variável de ambiente GF_SECURITY_ADMIN_PASSWORD por uma referência ao AWS Secrets Manager. Nunca deixe senhas hardcoded, nem mesmo em variáveis de ambiente definidas no CDK.

Exemplo prático: Lambda de provisionamento automático de dashboards

Um dos padrões mais úteis que já implementei em produção é o provisionamento automático de dashboards. Toda vez que um novo serviço é deployado, uma Lambda cria automaticamente um dashboard no Grafana com as métricas padrão. Isso garante que nenhum serviço entre em produção sem monitoramento.

TYPESCRIPT
import { APIGatewayProxyHandler } from 'aws-lambda';
import https from 'https';
 
interface GrafanaDashboardPanel {
  title: string;
  type: string;
  gridPos: { h: number; w: number; x: number; y: number };
  targets: Array<{
    refId: string;
    namespace: string;
    metricName: string;
    dimensions: Record<string, string[]>;
    period: string;
    stat: string;
    region: string;
  }>;
  datasource: { type: string; uid: string };
}
 
interface ServiceConfig {
  serviceName: string;
  ecsCluster: string;
  albArn: string;
  region: string;
}
 
function buildDashboardPayload(config: ServiceConfig) {
  const panels: GrafanaDashboardPanel[] = [
    {
      title: `CPU Utilization: ${config.serviceName}`,
      type: 'timeseries',
      gridPos: { h: 8, w: 12, x: 0, y: 0 },
      datasource: { type: 'cloudwatch', uid: 'cloudwatch-main' },
      targets: [
        {
          refId: 'A',
          namespace: 'AWS/ECS',
          metricName: 'CPUUtilization',
          dimensions: {
            ClusterName: [config.ecsCluster],
            ServiceName: [config.serviceName],
          },
          period: '300',
          stat: 'Average',
          region: config.region,
        },
      ],
    },
    {
      title: `Memory Utilization: ${config.serviceName}`,
      type: 'timeseries',
      gridPos: { h: 8, w: 12, x: 12, y: 0 },
      datasource: { type: 'cloudwatch', uid: 'cloudwatch-main' },
      targets: [
        {
          refId: 'A',
          namespace: 'AWS/ECS',
          metricName: 'MemoryUtilization',
          dimensions: {
            ClusterName: [config.ecsCluster],
            ServiceName: [config.serviceName],
          },
          period: '300',
          stat: 'Average',
          region: config.region,
        },
      ],
    },
    {
      title: `HTTP 5xx Errors: ${config.serviceName}`,
      type: 'stat',
      gridPos: { h: 8, w: 8, x: 0, y: 8 },
      datasource: { type: 'cloudwatch', uid: 'cloudwatch-main' },
      targets: [
        {
          refId: 'A',
          namespace: 'AWS/ApplicationELB',
          metricName: 'HTTPCode_Target_5XX_Count',
          dimensions: {
            LoadBalancer: [config.albArn],
          },
          period: '60',
          stat: 'Sum',
          region: config.region,
        },
      ],
    },
  ];
 
  return {
    dashboard: {
      title: `[Auto] ${config.serviceName} - Production`,
      tags: ['auto-provisioned', 'production', config.serviceName],
      timezone: 'browser',
      refresh: '30s',
      panels,
    },
    folderId: 0,
    overwrite: true,
  };
}
 
export const handler: APIGatewayProxyHandler = async (event) => {
  const config: ServiceConfig = JSON.parse(event.body || '{}');
 
  if (!config.serviceName || !config.ecsCluster) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'serviceName e ecsCluster sao obrigatorios' }),
    };
  }
 
  const payload = JSON.stringify(buildDashboardPayload(config));
 
  const grafanaHost = process.env.GRAFANA_HOST || 'grafana.internal';
  const grafanaToken = process.env.GRAFANA_API_TOKEN || '';
 
  const response = await new Promise<{ statusCode: number; body: string }>(
    (resolve, reject) => {
      const req = https.request(
        {
          hostname: grafanaHost,
          port: 443,
          path: '/api/dashboards/db',
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${grafanaToken}`,
          },
        },
        (res) => {
          let data = '';
          res.on('data', (chunk) => (data += chunk));
          res.on('end', () =>
            resolve({ statusCode: res.statusCode || 500, body: data })
          );
        }
      );
      req.on('error', reject);
      req.write(payload);
      req.end();
    }
  );
 
  console.log(`Dashboard criado para ${config.serviceName}: ${response.statusCode}`);
 
  return {
    statusCode: response.statusCode,
    body: response.body,
  };
};

Essa Lambda pode ser integrada ao seu pipeline de CI/CD. No final do deploy de cada serviço, basta chamar essa função com as configurações do serviço. Assim, o dashboard já estará pronto quando o primeiro request chegar. Para mais detalhes sobre pipelines de deploy, confira o post DevOps para Devs: Docker, CI/CD, Kubernetes e AWS em Produção.

Configurando o datasource CloudWatch

Após o Grafana estar rodando, você precisa configurar o datasource do CloudWatch. Se você usou a IAM Role no task definition (como no exemplo CDK acima), a autenticação acontece automaticamente via credenciais da instância. No Grafana, acesse Configuration, depois Data Sources, e adicione o CloudWatch com a seguinte configuração:

Text
Authentication Provider: AWS SDK Default
Default Region: sa-east-1

Nenhuma access key ou secret key é necessária. O Grafana vai usar a role associada

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias