No Lab 01, construímos a fundação: uma instância PostgreSQL reproduzível, persistente e validada com Docker.
Mas subir um banco saudável é apenas o primeiro passo. Para testar backup, replicação, observabilidade ou performance, precisamos de uma base que se aproxime de um cenário real: usuários com responsabilidades diferentes, schemas bem definidos, extensões operacionais, relacionamentos, índices e dados suficientes para exercitar o ambiente.
Esse é o objetivo do Lab 02 do PostgreSQL Reliability Lab.
Objetivo
Evoluir a fundação do projeto para um banco PostgreSQL que:
- crie roles específicas para aplicação e operação
- separe objetos por responsabilidade usando schemas
- habilite extensões de diagnóstico e suporte
- modele um domínio simples de e-commerce
- carregue uma massa de dados reproduzível
- valide automaticamente todos os componentes do bootstrap
Ao final, temos uma base pronta para sustentar os próximos labs de backup, replicação, failover, observabilidade e performance.
Estrutura do lab
O Lab 02 organiza a inicialização em scripts SQL numerados:
labs/02-database-initialization/
├── .env.example
├── docker-compose.yml
├── init/
│ ├── 01_roles.sql
│ ├── 02_extensions.sql
│ ├── 03_schemas.sql
│ ├── 04_tables.sql
│ ├── 05_seed_procedures.sql
│ └── 06_load_sample_data.sql
├── scripts/
│ └── check.sh
└── README.md
O PostgreSQL executa os arquivos de init/ em ordem alfabética durante a primeira inicialização de um volume vazio. A numeração torna explícita a dependência entre cada etapa: primeiro as identidades, depois as extensões e schemas, em seguida as tabelas e, por último, os dados.
Roles separadas por responsabilidade
Em vez de concentrar todas as operações no superusuário postgres, o lab cria cinco roles:
app_owner: proprietária dos objetos da aplicaçãoapp_user: identidade de leitura e escrita usada pela aplicaçãoreadonly: acesso somente para consultasbackup_user: preparada para backup físico e replicaçãomonitor_user: recebe a role nativapg_monitor
Um trecho simplificado da criação é:
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_owner') THEN
CREATE ROLE app_owner NOLOGIN;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN
CREATE ROLE app_user LOGIN PASSWORD 'app_user_password';
END IF;
END
$$;
GRANT app_owner TO app_user;
GRANT pg_monitor TO monitor_user;
Essa separação aplica o princípio do menor privilégio e cria desde cedo as identidades que serão usadas nos cenários operacionais dos próximos labs.
As senhas presentes nos scripts são exclusivas para o laboratório local. Em um ambiente real, credenciais devem ser armazenadas e distribuídas por um gerenciador de secrets.
Schemas e privilégios
Os objetos são divididos em três schemas:
app: tabelas do domínio da aplicaçãoaudit: eventos e trilhas de auditoriaseed: procedures usadas para gerar dados
O lab também remove do público permissões que não são necessárias:
CREATE SCHEMA IF NOT EXISTS app AUTHORIZATION app_owner;
CREATE SCHEMA IF NOT EXISTS audit AUTHORIZATION app_owner;
CREATE SCHEMA IF NOT EXISTS seed AUTHORIZATION app_owner;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
Além dos grants atuais, são configurados privilégios padrão para objetos que app_owner criar no futuro. Isso evita um problema recorrente: uma tabela nova ser criada corretamente, mas ficar inacessível à aplicação ou à role de leitura.
Extensões para operação e diagnóstico
Três extensões são habilitadas:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
O docker-compose.yml inicia o PostgreSQL com pg_stat_statements em shared_preload_libraries e habilita track_io_timing. Assim, o ambiente já coleta informações úteis para os labs de observabilidade e análise de performance.
Um modelo de dados mais realista
O domínio escolhido foi um e-commerce simples, composto por:
- clientes e endereços
- categorias e produtos
- pedidos e itens
- pagamentos
- eventos de auditoria
As tabelas usam chaves estrangeiras, restrições, índices, UUID, JSONB e colunas geradas. Por exemplo, o valor total de um item é calculado pelo próprio PostgreSQL:
CREATE TABLE IF NOT EXISTS app.order_items (
order_item_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id BIGINT NOT NULL
REFERENCES app.orders(order_id) ON DELETE CASCADE,
product_id BIGINT NOT NULL
REFERENCES app.products(product_id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(12, 2) NOT NULL CHECK (unit_price > 0),
total_price NUMERIC(12, 2)
GENERATED ALWAYS AS (quantity * unit_price) STORED
);
Esse conjunto é pequeno o bastante para ser compreendido rapidamente, mas diverso o suficiente para testar joins, agregações, integridade referencial, backups, restores e comportamento de consultas.
Geração reproduzível de dados
A procedure seed.load_sample_data concentra a criação da massa de teste. Por padrão, ela gera:
- 100 clientes
- 5 categorias
- 50 produtos
- 500 pedidos
- itens, pagamentos e eventos de auditoria relacionados
CALL seed.load_sample_data(100, 5, 50, 500);
Usar PL/pgSQL mantém a carga junto do banco e elimina dependências de ferramentas ou arquivos externos. A procedure também utiliza restrições e ON CONFLICT nos cadastros principais para evitar duplicações nessas entidades.
Subindo o ambiente
Clone o repositório e entre no diretório do lab:
cd labs/02-database-initialization
cp .env.example .env
Antes de iniciar, ajuste POSTGRES_PASSWORD no arquivo .env. Depois execute:
docker compose up -d
docker compose ps
O container deve chegar ao estado healthy. O Lab 02 expõe o PostgreSQL na porta 5433, evitando conflito com a porta padrão usada pelo Lab 01.
Assim como no Lab 01, os scripts de inicialização só são executados automaticamente quando o diretório de dados está vazio.
Para reconstruir toda a base:
docker compose down -v
docker compose up -d
O comando remove também os dados persistidos no volume. Portanto, ele deve ser usado apenas quando a intenção for reinicializar o laboratório do zero.
Validação automatizada
O script scripts/check.sh não verifica apenas se o processo do PostgreSQL está em execução. Ele confirma que o bootstrap entregou o estado esperado:
- container em execução e banco acessível com
pg_isready - cinco roles criadas
- schemas
app,auditeseeddisponíveis - extensões instaladas
- quantidade mínima de registros nas tabelas principais
- pedidos acompanhados por itens, pagamentos e eventos de auditoria
Para executar:
chmod +x scripts/check.sh
./scripts/check.sh
Resultado esperado:
ok: database initialization validado com roles, schemas, extensões, tabelas e dados.
Essa validação transforma a inicialização em uma propriedade verificável: não basta o container estar de pé; o banco precisa estar pronto para cumprir seu papel.
Conectando ao banco
Via container:
docker compose exec postgres psql -U postgres -d appdb
Ou com um cliente psql instalado no host:
psql "postgresql://postgres:SUA_SENHA@localhost:5433/appdb"
Dentro do psql, alguns comandos ajudam a inspecionar o resultado:
\dn
\dt app.*
\dt audit.*
\du
\dx
Decisões de engenharia
Scripts SQL numerados
O bootstrap tem uma ordem determinística e fácil de auditar. Cada arquivo possui uma responsabilidade clara e pode evoluir sem transformar a inicialização em um único script monolítico.
Propriedade e acesso separados
Objetos pertencem a app_owner, enquanto a aplicação opera com app_user. Isso reduz o acoplamento entre execução e administração do schema.
Dados gerados dentro do PostgreSQL
A procedure de seed permite variar o volume de dados e reproduzir a carga sem instalar outra linguagem ou ferramenta.
Validação do estado, não apenas do processo
Healthcheck responde se o PostgreSQL aceita conexões. O check.sh vai além e verifica se roles, schemas, extensões, tabelas e dados realmente foram criados.
Próximo passo
Com uma base realista e reproduzível, o próximo lab poderá tratar de backup e restore: backup lógico, backup físico, WAL archiving e recuperação point-in-time.
O código completo do Lab 02 está disponível no GitHub:
👉 PostgreSQL Reliability Lab — Lab 02: Database Initialization