PostgreSQL para Azure: Guia Completo de Deploy
Introdução
Escolher onde hospedar seu banco de dados de produção é uma das decisões de infraestrutura que mais impactam a vida útil de um projeto. Se você já trabalha com PostgreSQL e precisa de uma plataforma gerenciada com alta disponibilidade, backups automáticos e integração nativa com ferramentas de observabilidade, o Azure Database for PostgreSQL Flexible Server é uma opção que merece atenção séria.
Neste guia, vou mostrar como configurar, deployar e operar PostgreSQL no Azure de ponta a ponta. Vamos partir da criação do servidor via CLI, passar por exemplos práticos de código em TypeScript, e terminar com as boas práticas que aprendi operando esse tipo de infraestrutura em produção por anos. O conteúdo assume que você já tem experiência com desenvolvimento backend e alguma familiaridade com bancos relacionais.
Por que usar PostgreSQL no Azure
Vantagens do Azure Database for PostgreSQL Flexible Server
O Flexible Server é a geração atual do serviço gerenciado de PostgreSQL no Azure. Ele substituiu o Single Server (que está em processo de descontinuação) e trouxe melhorias significativas em controle, custo e flexibilidade.
As principais razões para considerar essa combinação:
Manutenção delegada: patching de sistema operacional, atualizações menores do PostgreSQL e backups são gerenciados automaticamente. Você configura a janela de manutenção e o Azure executa sem intervenção.
Alta disponibilidade integrada: com suporte a zone-redundant HA, o failover acontece automaticamente entre zonas de disponibilidade. Em cenários críticos, o RTO (Recovery Time Objective) fica abaixo de 120 segundos.
Custo granular: o modelo Burstable (série B) permite começar com instâncias pequenas e pagar apenas pelo que usar. Para ambientes de desenvolvimento, isso reduz custos drasticamente comparado a provisionar uma VM dedicada.
Integração com o ecossistema Azure: se sua aplicação já roda em App Service, AKS ou Azure Functions, a comunicação via VNET elimina a necessidade de expor o banco à internet pública.
Casos de uso onde essa combinação se destaca
Na prática, vi essa arquitetura brilhar em três cenários recorrentes: aplicações SaaS multi-tenant que precisam de isolamento por schema, pipelines de dados com cargas analíticas moderadas usando extensões como pg_stat_statements e pgvector, e APIs de alta concorrência onde o connection pooling nativo (PgBouncer integrado) resolve o gargalo de conexões sem infraestrutura adicional. Se você quer se aprofundar nesse último ponto, recomendo a leitura do post Como Escalar PostgreSQL com Connection Pooling e PgBouncer.
Setup completo do PostgreSQL no Azure via CLI
Pré-requisitos
Antes de começar, certifique-se de ter o Azure CLI instalado e autenticado:
// Esses comandos são executados no terminal, não em TypeScript.
// Listados aqui para referência.
// az login
// az account set --subscription "SUA_SUBSCRIPTION_ID"Criando o Resource Group e o servidor PostgreSQL
Vamos criar toda a infraestrutura usando comandos reais do Azure CLI:
# Criar o resource group
az group create \
--name rg-vivodecodigo-prod \
--location brazilsouth
# Criar o servidor PostgreSQL Flexible Server
az postgres flexible-server create \
--resource-group rg-vivodecodigo-prod \
--name pgflex-vivodecodigo \
--location brazilsouth \
--admin-user pgadmin \
--admin-password 'SuaSenhaForte@2024!' \
--sku-name Standard_B2s \
--tier Burstable \
--version 16 \
--storage-size 64 \
--backup-retention 14 \
--high-availability ZoneRedundant \
--public-access 0.0.0.0
# Criar o banco de dados da aplicação
az postgres flexible-server db create \
--resource-group rg-vivodecodigo-prod \
--server-name pgflex-vivodecodigo \
--database-name app_productionConfigurando parâmetros essenciais do servidor
Alguns parâmetros do PostgreSQL precisam de ajuste imediato após a criação:
# Habilitar PgBouncer integrado
az postgres flexible-server parameter set \
--resource-group rg-vivodecodigo-prod \
--server-name pgflex-vivodecodigo \
--name pgbouncer.enabled \
--value true
# Configurar o modo do PgBouncer para transaction pooling
az postgres flexible-server parameter set \
--resource-group rg-vivodecodigo-prod \
--server-name pgflex-vivodecodigo \
--name pgbouncer.default_pool_mode \
--value transaction
# Habilitar extensões necessárias
az postgres flexible-server parameter set \
--resource-group rg-vivodecodigo-prod \
--server-name pgflex-vivodecodigo \
--name azure.extensions \
--value "PG_STAT_STATEMENTS,UUID-OSSP,PGCRYPTO"
# Ajustar shared_preload_libraries
az postgres flexible-server parameter set \
--resource-group rg-vivodecodigo-prod \
--server-name pgflex-vivodecodigo \
--name shared_preload_libraries \
--value pg_stat_statementsConfigurando regras de firewall
Para ambientes que não utilizam VNET, configure o acesso por IP:
# Permitir acesso do IP do seu escritório
az postgres flexible-server firewall-rule create \
--resource-group rg-vivodecodigo-prod \
--name pgflex-vivodecodigo \
--rule-name allow-office \
--start-ip-address 203.0.113.50 \
--end-ip-address 203.0.113.50
# Permitir acesso de serviços Azure (para App Service, Functions, etc.)
az postgres flexible-server firewall-rule create \
--resource-group rg-vivodecodigo-prod \
--name pgflex-vivodecodigo \
--rule-name allow-azure-services \
--start-ip-address 0.0.0.0 \
--end-ip-address 0.0.0.0Exemplo prático: API TypeScript conectando ao PostgreSQL no Azure
Conexão robusta com pool de conexões e retry
O primeiro exemplo mostra como estruturar a camada de banco de dados de uma aplicação Node.js com TypeScript, incluindo tratamento de erros e connection pooling:
import { Pool, PoolConfig, PoolClient, QueryResult } from "pg";
interface DatabaseConfig {
host: string;
port: number;
database: string;
user: string;
password: string;
ssl: boolean;
maxConnections: number;
idleTimeoutMs: number;
connectionTimeoutMs: number;
}
function loadDatabaseConfig(): DatabaseConfig {
return {
host: process.env.PGHOST || "pgflex-vivodecodigo.postgres.database.azure.com",
port: parseInt(process.env.PGPORT || "6432", 10), // Porta 6432 para PgBouncer
database: process.env.PGDATABASE || "app_production",
user: process.env.PGUSER || "pgadmin",
password: process.env.PGPASSWORD || "",
ssl: process.env.PGSSLMODE !== "disable",
maxConnections: parseInt(process.env.PG_MAX_CONNECTIONS || "20", 10),
idleTimeoutMs: 30000,
connectionTimeoutMs: 10000,
};
}
class DatabaseService {
private pool: Pool;
private isConnected: boolean = false;
constructor(config: DatabaseConfig) {
const poolConfig: PoolConfig = {
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
max: config.maxConnections,
idleTimeoutMillis: config.idleTimeoutMs,
connectionTimeoutMillis: config.connectionTimeoutMs,
ssl: config.ssl ? { rejectUnauthorized: true } : false,
};
this.pool = new Pool(poolConfig);
this.pool.on("error", (err: Error) => {
console.error("[DatabaseService] Erro inesperado no pool:", err.message);
});
this.pool.on("connect", () => {
if (!this.isConnected) {
console.log("[DatabaseService] Conexão com PostgreSQL estabelecida.");
this.isConnected = true;
}
});
}
async query<T = any>(text: string, params?: any[]): Promise<QueryResult<T>> {
const start = Date.now();
try {
const result = await this.pool.query<T>(text, params);
const duration = Date.now() - start;
console.log("[DatabaseService] Query executada:", {
text: text.substring(0, 80),
duration: `${duration}ms`,
rows: result.rowCount,
});
return result;
} catch (error: any) {
console.error("[DatabaseService] Falha na query:", {
text: text.substring(0, 80),
error: error.message,
code: error.code,
});
throw error;
}
}
async withTransaction<T>(
callback: (client: PoolClient) => Promise<T>
): Promise<T> {
const client = await this.pool.connect();
try {
await client.query("BEGIN");
const result = await callback(client);
await client.query("COMMIT");
return result;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
async healthCheck(): Promise<boolean> {
try {
const result = await this.query("SELECT 1 as health");
return result.rows[0]?.health === 1;
} catch {
return false;
}
}
async shutdown(): Promise<void> {
console.log("[DatabaseService] Encerrando pool de conexões...");
await this.pool.end();
this.isConnected = false;
}
}
// Singleton para uso na aplicação
const config = loadDatabaseConfig();
export const db = new DatabaseService(config);Note que a porta utilizada é a 6432, que aponta para o PgBouncer integrado do Flexible Server. Isso é intencional: em vez de conectar diretamente na porta 5432 do PostgreSQL, passamos pelo connection pooler para reduzir overhead de conexões.
Exemplo de migração e seed com controle transacional
O segundo exemplo mostra um sistema de migrações simples que você pode usar como base para entender como operações DDL interagem com o Azure PostgreSQL:
import { db } from "./database-service";
interface Migration {
version: number;
name: string;
up: string;
down: string;
}
const migrations: Migration[] = [
{
version: 1,
name: "create_users_table",
up: `
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
password_hash TEXT NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_active ON users (is_active) WHERE is_active = true;
`,
down: `DROP TABLE IF EXISTS users;`,
},
{
version: 2,
name: "create_audit_log",
up: `
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
user_id UUID REFERENCES users(id),
action VARCHAR(100) NOT NULL,
resource VARCHAR(100) NOT NULL,
details JSONB DEFAULT '{}',
ip_address INET,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_audit_user ON audit_log (user_id);
CREATE INDEX idx_audit_created ON audit_log (created_at DESC);
CREATE INDEX idx_audit_details ON audit_log USING GIN (details);
`,
down: `DROP TABLE IF EXISTS audit_log;`,
},
];
async function ensureMigrationTable(): Promise<void> {
await db.query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
name VARCHAR(255) NOT NULL,
applied_at TIMESTAMPTZ DEFAULT NOW()
);
`);
}
async function getAppliedVersions(): Promise<number[]> {
const result = await db.query<{ version: number }>(
"SELECT version FROM schema_migrations ORDER BY version"
);
return result.rows.map((row) => row.version);
}
async function runMigrations(): Promise<void> {
console.log("[Migrations] Iniciando processo de migração...");
await ensureMigrationTable();
const applied = await getAppliedVersions();
const pending = migrations.filter((m) => !applied.includes(m.version));
if (pending.length === 0) {
console.log("[Migrations] Nenhuma migração pendente.");
return;
}
for (const migration of pending) {
console.log(`[Migrations] Aplicando v${migration.version}: ${migration.name}`);
await db.withTransaction(async (client) => {
await client.query(migration.up);
await client.query(
"INSERT INTO schema_migrations (version, name) VALUES ($1, $2)",
[migration.version, migration.name]
);
});
console.log(`[Migrations] v${migration.version} aplicada com sucesso.`);
}
console.log(`[Migrations] ${pending.length} migração(ões) aplicada(s).`);
}
// Executar
runMigrations()
.then(() => {
console.log("[Migrations] Processo finalizado.");
return db.shutdown();
})
.catch((error) => {
console.error("[Migrations] Erro fatal:", error);
process.exit(1);
});Esse padrão funciona bem com o Azure PostgreSQL porque cada migração roda dentro de uma transação, garantindo atomicidade. Se o DDL falhar por timeout ou problema de rede (algo que pode acontecer durante janelas de manutenção), a transação é revertida e o estado do banco permanece consistente.
Boas práticas para PostgreSQL em produção no Azure
Segurança e rede
Sempre prefira conectar via VNET em ambientes de produção. A combinação de Private Link com DNS Zone privada elimina completamente o tráfego pela internet pública. Configure SSL como obrigatório e nunca armazene credenciais em código: use Azure Key Vault ou variáveis de ambiente injetadas pelo App Service.
Para autenticação, considere usar Microsoft Entra ID (antigo Azure AD) em vez de usuário e senha tradicionais. O Flexible Server suporta autenticação via token do Microsoft Entra ID, usado no campo de senha da conexão. Habilite essa autenticação, associe a identidade a uma role no PostgreSQL e forneça tokens válidos ao abrir novas conexões. O pool mostrado acima ainda precisa ser adaptado para obter e renovar esses tokens.
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