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:
# 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-accessibleAlguns 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.
# 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-immediatelyA 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):
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:
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:
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