Ir para o conteúdo
Ferramenta para Plataforma1698 palavras

PostgreSQL para AWS: Guia Completo de Deploy

Introdução

PostgreSQL é, há anos, o banco de dados relacional open source mais robusto do mercado. A AWS, por sua vez, oferece a infraestrutura de nuvem mais utilizada no mundo. Quando você combina os dois, obtém um banco de dados de produção escalável, gerenciado e com alta disponibilidade, sem precisar se preocupar com patching de SO, backups manuais ou failover artesanal.

Neste guia, vou mostrar como fazer o deploy de PostgreSQL na AWS do zero, passando por RDS e Aurora, com exemplos práticos em TypeScript, comandos CLI reais e as boas práticas que aprendi ao longo de mais de uma década operando bancos em produção.

Se você já tem pelo menos dois anos de experiência como desenvolvedor e quer entender como levar seu PostgreSQL para a nuvem da forma correta, este artigo é para você.

Por que usar PostgreSQL na AWS

Benefícios do Amazon RDS para PostgreSQL

O Amazon RDS (Relational Database Service) elimina toda a carga operacional de manter um PostgreSQL em produção. Backups automáticos, patching, monitoramento com CloudWatch e failover com Multi-AZ vêm prontos, sem que você precise configurar nada manualmente.

Além disso, o RDS oferece réplicas de leitura com um clique, snapshots incrementais e integração nativa com IAM para controle de acesso granular. Para times pequenos ou médios, isso significa que você não precisa de um DBA dedicado apenas para manter o banco de pé.

Quando escolher Aurora PostgreSQL

O Amazon Aurora é o motor compatível com PostgreSQL da AWS, reescrito para tirar proveito da infraestrutura de nuvem. O storage é distribuído automaticamente em três zonas de disponibilidade, com replicação em seis cópias dos dados. Na prática, isso significa que o Aurora entrega até três vezes mais throughput que um PostgreSQL padrão no RDS, com latência de replicação abaixo de 10 milissegundos.

Aurora é a escolha ideal quando você precisa de alta disponibilidade real, escalabilidade de leitura com até 15 réplicas e recuperação de desastres sem downtime perceptível.

Casos de uso reais onde essa combinação brilha

Existem cenários onde PostgreSQL na AWS se destaca de forma clara:

  • SaaS multi-tenant: esquemas isolados por cliente com RDS, escalando verticalmente conforme o número de tenants cresce.
  • APIs de alta concorrência: Aurora PostgreSQL com réplicas de leitura, distribuindo queries pesadas de relatórios para réplicas enquanto a instância primária serve escritas.
  • Aplicações financeiras: Multi-AZ com failover automático garante RPO próximo de zero, atendendo requisitos de compliance.
  • Plataformas de e-commerce: snapshots automáticos antes de eventos de pico (Black Friday), com a capacidade de escalar a instância verticalmente em minutos.

Setup completo do PostgreSQL na AWS

Criando uma instância RDS PostgreSQL via CLI

Antes de tudo, certifique-se de ter o AWS CLI configurado com credenciais válidas. Vamos criar uma instância RDS PostgreSQL com Multi-AZ habilitado:

Bash
# Criar um subnet group para o RDS
aws rds create-db-subnet-group \
  --db-subnet-group-name pg-subnet-group \
  --db-subnet-group-description "Subnets privadas para PostgreSQL" \
  --subnet-ids subnet-0a1b2c3d4e5f00001 subnet-0a1b2c3d4e5f00002
 
# Criar um security group dedicado
PG_RDS_SG_ID=$(
  aws ec2 create-security-group \
    --group-name pg-rds-sg \
    --description "Security group para RDS PostgreSQL" \
    --vpc-id vpc-0a1b2c3d4e5f00000 \
    --query GroupId --output text
)
 
# Liberar acesso na porta 5432 apenas da sua VPC
aws ec2 authorize-security-group-ingress \
  --group-id "$PG_RDS_SG_ID" \
  --protocol tcp \
  --port 5432 \
  --cidr 10.0.0.0/16
 
# Criar a instância RDS PostgreSQL
aws rds create-db-instance \
  --db-instance-identifier meu-postgres-prod \
  --db-instance-class db.r6g.large \
  --engine postgres \
  --engine-version 16.4 \
  --master-username pgadmin \
  --master-user-password "SenhaSegura123!" \
  --allocated-storage 100 \
  --storage-type gp3 \
  --storage-encrypted \
  --kms-key-id alias/aws/rds \
  --multi-az \
  --db-subnet-group-name pg-subnet-group \
  --vpc-security-group-ids "$PG_RDS_SG_ID" \
  --backup-retention-period 14 \
  --preferred-backup-window "03:00-04:00" \
  --preferred-maintenance-window "sun:05:00-sun:06:00" \
  --deletion-protection \
  --no-publicly-accessible

Alguns pontos importantes sobre essa configuração: o storage gp3 oferece performance consistente e é mais barato que gp2 para a maioria dos workloads. A flag --no-publicly-accessible garante que o banco só é acessível de dentro da VPC. E o --deletion-protection evita que alguém delete a instância acidentalmente.

Configurando Parameter Groups personalizados

O parameter group padrão do RDS é read-only. Para tunar seu PostgreSQL, crie um grupo customizado:

Comece com os valores de memória do parameter group padrão da família PostgreSQL utilizada. Se houver motivo medido para personalizá-los, confira a unidade de cada parâmetro. Com blocos de 8 KiB, 25% de DBInstanceClassMemory corresponde a {DBInstanceClassMemory/32768} em shared_buffers. Para uma estimativa de 75% em effective_cache_size, a conversão é {DBInstanceClassMemory*3/32768}. Esses percentuais são exemplos de cálculo; o ajuste adequado depende da instância e da carga.

Bash
# Criar parameter group customizado
aws rds create-db-parameter-group \
  --db-parameter-group-name pg16-custom \
  --db-parameter-group-family postgres16 \
  --description "Parametros customizados para producao"
 
# Aplicar configurações de performance
aws rds modify-db-parameter-group \
  --db-parameter-group-name pg16-custom \
  --parameters \
    "ParameterName=work_mem,ParameterValue=65536,ApplyMethod=immediate" \
    "ParameterName=maintenance_work_mem,ParameterValue=524288,ApplyMethod=immediate" \
    "ParameterName=random_page_cost,ParameterValue=1.1,ApplyMethod=immediate" \
    "ParameterName=log_min_duration_statement,ParameterValue=1000,ApplyMethod=immediate"
 
# Associar o parameter group à instância
aws rds modify-db-instance \
  --db-instance-identifier meu-postgres-prod \
  --db-parameter-group-name pg16-custom \
  --apply-immediately

A configuração log_min_duration_statement=1000 registra todas as queries que levam mais de 1 segundo, o que é essencial para identificar gargalos em produção.

Exemplo prático: aplicação TypeScript conectando ao RDS PostgreSQL

Conexão segura com pool de conexões

Vamos construir um módulo de conexão robusto usando o pacote pg com pool de conexões, tratamento de erros e suporte a SSL (obrigatório para RDS):

TYPESCRIPT
import { Pool, PoolConfig, QueryResult } from "pg";
 
interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  user: string;
  password: string;
  maxConnections: number;
}
 
function createDatabaseConfig(): DatabaseConfig {
  const requiredVars = [
    "DB_HOST",
    "DB_NAME",
    "DB_USER",
    "DB_PASSWORD",
  ];
 
  for (const varName of requiredVars) {
    if (!process.env[varName]) {
      throw new Error(`Variável de ambiente ${varName} não definida`);
    }
  }
 
  return {
    host: process.env.DB_HOST!,
    port: parseInt(process.env.DB_PORT || "5432", 10),
    database: process.env.DB_NAME!,
    user: process.env.DB_USER!,
    password: process.env.DB_PASSWORD!,
    maxConnections: parseInt(process.env.DB_MAX_CONNECTIONS || "20", 10),
  };
}
 
function createPool(config: DatabaseConfig): Pool {
  const poolConfig: PoolConfig = {
    host: config.host,
    port: config.port,
    database: config.database,
    user: config.user,
    password: config.password,
    max: config.maxConnections,
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
    ssl: {
      rejectUnauthorized: true,
      ca: process.env.RDS_CA_CERT,
    },
  };
 
  const pool = new Pool(poolConfig);
 
  pool.on("error", (err: Error) => {
    console.error("Erro inesperado no pool de conexões:", err.message);
  });
 
  pool.on("connect", () => {
    console.log("Nova conexão estabelecida com o PostgreSQL");
  });
 
  return pool;
}
 
const dbConfig = createDatabaseConfig();
const pool = createPool(dbConfig);
 
export async function query<T>(
  text: string,
  params?: unknown[]
): Promise<QueryResult<T>> {
  const start = Date.now();
 
  try {
    const result = await pool.query<T>(text, params);
    const duration = Date.now() - start;
 
    if (duration > 1000) {
      console.warn(
        `Query lenta detectada (${duration}ms): ${text.substring(0, 100)}`
      );
    }
 
    return result;
  } catch (error) {
    console.error(`Erro na query: ${text.substring(0, 100)}`, error);
    throw error;
  }
}
 
export async function healthCheck(): Promise<boolean> {
  try {
    await pool.query("SELECT 1");
    return true;
  } catch {
    return false;
  }
}
 
export async function gracefulShutdown(): Promise<void> {
  console.log("Encerrando pool de conexões...");
  await pool.end();
  console.log("Pool de conexões encerrado.");
}
 
process.on("SIGTERM", gracefulShutdown);
process.on("SIGINT", gracefulShutdown);

Observe que o ssl.rejectUnauthorized está como true. Em produção com RDS, você deve baixar o certificado CA da AWS e fornecê-lo via variável de ambiente. Nunca desabilite a verificação SSL em produção.

API de health check e CRUD com Express

Agora vamos usar o módulo de conexão em uma API real:

TYPESCRIPT
import express, { Request, Response } from "express";
import { query, healthCheck } from "./database";
 
const app = express();
app.use(express.json());
 
interface Product {
  id: number;
  name: string;
  price: number;
  created_at: Date;
}
 
interface CreateProductBody {
  name: string;
  price: number;
}
 
// Health check que verifica a conexão com o banco
app.get("/health", async (_req: Request, res: Response) => {
  const dbHealthy = await healthCheck();
 
  if (dbHealthy) {
    res.status(200).json({
      status: "healthy",
      database: "connected",
      timestamp: new Date().toISOString(),
    });
  } else {
    res.status(503).json({
      status: "unhealthy",
      database: "disconnected",
      timestamp: new Date().toISOString(),
    });
  }
});
 
// Listar produtos com paginação
app.get("/products", async (req: Request, res: Response) => {
  const page = parseInt(req.query.page as string) || 1;
  const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
  const offset = (page - 1) * limit;
 
  try {
    const countResult = await query<{ total: string }>(
      "SELECT COUNT(*) as total FROM products"
    );
    const total = parseInt(countResult.rows[0].total, 10);
 
    const result = await query<Product>(
      `SELECT id, name, price, created_at
       FROM products
       ORDER BY created_at DESC
       LIMIT $1 OFFSET $2`,
      [limit, offset]
    );
 
    res.json({
      data: result.rows,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    });
  } catch (error) {
    console.error("Erro ao listar produtos:", error);
    res.status(500).json({ error: "Erro interno do servidor" });
  }
});
 
// Criar produto com validação básica
app.post("/products", async (req: Request, res: Response) => {
  const { name, price }: CreateProductBody = req.body ?? {};
 
  if (
    typeof name !== "string" ||
    name.trim().length === 0 ||
    typeof price !== "number" ||
    !Number.isFinite(price) ||
    price < 0
  ) {
    res.status(400).json({
      error: "Informe name não vazio e price numérico, finito e >= 0",
    });
    return;
  }
 
  try {
    const result = await query<Product>(
      `INSERT INTO products (name, price, created_at)
       VALUES ($1, $2, NOW())
       RETURNING id, name, price, created_at`,
      [name.trim(), price]
    );
 
    res.status(201).json({ data: result.rows[0] });
  } catch (error) {
    console.error("Erro ao criar produto:", error);
    res.status(500).json({ error: "Erro interno do servidor" });
  }
});
 
const PORT = parseInt(process.env.PORT || "3000", 10);
 
app.listen(PORT, () => {
  console.log(`Servidor rodando na porta ${PORT}`);
});

Esse código usa queries parametrizadas (com $1, $2) para prevenir SQL injection, implementa paginação com limite máximo para evitar queries abusivas e inclui um endpoint de health check que valida a conexão real com o banco. Esse health check é fundamental quando você roda a aplicação em ECS ou EKS, pois o load balancer precisa saber se a instância está saudável.

Boas práticas de infraestrutura para PostgreSQL na AWS

Segurança e controle de acesso

Nunca exponha seu RDS à internet. Mantenha o banco em subnets privadas e use bastion hosts ou AWS Systems Manager Session Manager para acesso administrativo. Para a aplicação, a conexão deve acontecer dentro da mesma VPC.

Use o AWS Secrets Manager para armazenar credenciais do banco em vez de variáveis de ambiente estáticas:

Bash
aws secretsmanager create-secret \
  --name prod/postgres/credentials \
  --description "Credenciais do PostgreSQL de producao" \
  --secret-string '{"username":"pgadmin","password":"SenhaSegura123!","host":"meu-postgres-prod.abc123.us-east-1.rds.amazonaws.com","port":5432,"dbname":"myapp"}'

Habilite a autenticação IAM para eliminar senhas estáticas nos cenários onde isso for viável. Com IAM authentication, a aplicação gera tokens temporários que expiram em 15 minutos.

Monitoramento e alertas

Configure alarmes no CloudWatch para as métricas

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