Ir para o conteúdo
Ferramenta para Plataforma1681 palavras

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:

TYPESCRIPT
// 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:

Bash
# 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_production

Configurando parâmetros essenciais do servidor

Alguns parâmetros do PostgreSQL precisam de ajuste imediato após a criação:

Bash
# 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_statements

Configurando regras de firewall

Para ambientes que não utilizam VNET, configure o acesso por IP:

Bash
# 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.0

Exemplo 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:

TYPESCRIPT
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:

TYPESCRIPT
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