Seu ambiente de staging funciona. Seu ambiente de production não. A diferença entre os arquivos .env deles tem 400 linhas, e metade dessas linhas são comentários em que ninguém confia mais. Alguém adicionou FEATURE_X_ENABLED=true no staging no mês passado. Ninguém adicionou em production. A aplicação iniciou mesmo assim, usou um padrão hardcoded, e agora suas feature flags estão fora de sincronia.
Esse é o estado padrão da configuração entre ambientes. Não é um problema de gerenciamento de segredos. Não é falta de Terraform. É um problema de linguagem. Você está expressando configuração estruturada e dependente de ambiente em um formato que não tem conceito de estrutura, ambientes ou dependências.
Arquivos planos de chave-valor apodrecem em escala
Variáveis de ambiente e arquivos YAML planos compartilham o mesmo defeito: eles tratam a configuração como um saco indiferenciado de strings. Não há distinção entre um valor que deveria ser idêntico em todos os ambientes, um valor que difere intencionalmente e um valor que está faltando por acidente.
Em uma configuração típica, staging.env e production.env começam como cópias um do outro. Com o tempo, eles divergem. Um colega adiciona uma variável no staging para testar uma feature. Outro colega renomeia uma chave em production, mas não no staging, porque o deploy era urgente. Um terceiro colega assume um padrão no código porque a variável está faltando em um dos arquivos, e esse padrão está errado para o outro ambiente.
A podridão é invisível até causar um incidente. Nesse ponto, seus arquivos .env se tornaram artefatos write-only. O medo de quebrar algo impede a limpeza. A bagunça se acumula.
Por que contextos delimitados precisam de sua própria linguagem de configuração
O Domain-Driven Design divide sistemas em contextos delimitados (bounded contexts), cada um com seu próprio modelo e linguagem. A configuração deve seguir a mesma fronteira. Um serviço de pagamentos se importa com STRIPE_WEBHOOK_SECRET e INVOICE_GRACE_PERIOD_DAYS. Um serviço de notificações se importa com SNS_TOPIC_ARN e RATE_LIMIT_PER_MINUTE. Os formatos de configuração deles não têm nada em comum, então eles não deveriam compartilhar um arquivo de configuração.
A ideia que a maioria das equipes ignora: configuração não são apenas valores. É um schema, um conjunto de padrões e um conjunto de sobrescritas específicas por ambiente. Quando você trata os três como um único arquivo plano, perde a capacidade de raciocinar sobre qualquer um deles.
Uma DSL de configuração por contexto delimitado torna a estrutura explícita. Ela separa padrões compartilhados de sobreposições por ambiente e valida o resultado mesclado antes da aplicação iniciar.
Uma DSL de configuração funcional em TypeScript
Aqui está uma DSL pequena que define configuração por contexto. Ela usa Zod para validação e objetos simples para sobreposições por ambiente. O padrão é o mesmo em Python, Rust ou Go. Só muda a biblioteca de validação.
Primeiro, a maquinaria da DSL:
import { z } from "zod";
type ConfigDef<S extends z.ZodTypeAny> = {
schema: S;
base: z.infer<S>;
environments: Record<string, Partial<z.infer<S>>>;
};
function defineConfig<S extends z.ZodTypeAny>(def: ConfigDef<S>) {
return (env: string): z.infer<S> => {
const overlay = def.environments[env] ?? {};
const merged = { ...def.base, ...overlay };
return def.schema.parse(merged);
};
}
defineConfig recebe um schema, um objeto de configuração base e um mapa de sobrescritas por ambiente. Ela retorna uma função que recebe um nome de ambiente, mescla a base com a sobreposição e valida o resultado. Se um campo obrigatório estiver faltando ou um tipo estiver errado, o parse lança um erro antes do seu servidor iniciar.
Agora um contexto delimitado a utiliza:
// contexts/payment/config.ts
const loadPaymentConfig = defineConfig({
schema: z.object({
stripeSecretKey: z.string().startsWith("sk_"),
webhookEndpoint: z.string().url(),
retryAttempts: z.number().min(1).max(10),
logLevel: z.enum(["debug", "info", "warn", "error"]),
}),
base: {
retryAttempts: 3,
logLevel: "info",
},
environments: {
development: {
stripeSecretKey: "sk_test_dummy",
webhookEndpoint: "http://localhost:3000/webhook",
logLevel: "debug",
},
staging: {
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
webhookEndpoint: "https://staging.example.com/webhook",
},
production: {
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
webhookEndpoint: "https://api.example.com/webhook",
retryAttempts: 5,
},
},
});
const config = loadPaymentConfig(process.env.APP_ENV ?? "development");
Observe o que acontece aqui. retryAttempts tem o padrão 3 em todos os ambientes, exceto em production, onde é explicitamente sobrescrito para 5. logLevel é "debug" em development e "info" em todos os outros ambientes porque o valor base é "info" e apenas development o sobrescreve. Não há duplicação de valores compartilhados. Não há adivinhação sobre qual arquivo tem o padrão canônico.
Se alguém esquecer de definir STRIPE_SECRET_KEY no staging, a aplicação encerra na inicialização com um erro claro. A falha acontece no CI, não em production.
Como isso impede o drift de configuração
Drift acontece quando dois ambientes divergem acidentalmente. A DSL impede isso de três formas.
Primeiro, valores compartilhados vivem em um único lugar: o objeto base. Se você alterar a política de retry padrão, altera uma linha. Todo ambiente a recebe automaticamente, a menos que tenha uma sobreposição explícita.
Segundo, as sobrescritas por ambiente são objetos completos, não diffs no nível de linha entre arquivos. Você pode ver todos os valores que diferem da base olhando para um único objeto. Não há necessidade de comparar dois arquivos .env e adivinhar quais linhas importam.
Terceiro, o schema impõe completude. Se você adicionar um novo campo obrigatório ao schema, todo objeto de ambiente que não o fornecer falhará na validação. Você não pode adicionar um campo em production e esquecer o staging. O compilador e o validador vão te lembrar.
Compromissos: estrutura explícita custa esforço explícito
Esse padrão não é de graça. Ele exige design prévio. Alguém precisa definir o schema, decidir quais valores são base e quais são sobrescritas, e manter a maquinaria da DSL.
Se sua equipe é pequena e sua configuração tem dez variáveis, um arquivo .env está ótimo. A sobrecarga de um schema e uma camada de mesclagem não vale a pena até que você sinta a dor do drift.
Outro custo é a tentação de super-abstrair. Uma DSL pode crescer até se tornar um framework de configuração de propósito geral que abrange todos os serviços da sua organização. Resista a isso. Todo o ponto do padrão é manter a configuração local a cada contexto delimitado. Se sua DSL de pagamentos e sua DSL de notificações forem forçadas ao mesmo schema, você recriou o arquivo .env monolithic com passos extras.
Segredos são outra consideração. O exemplo lê STRIPE_SECRET_KEY de process.env, mas você poderia obtê-lo de um vault. A DSL não se importa de onde os valores vêm. Ela só se importa que eles correspondam ao schema antes da aplicação usá-los.
O que as equipes erram ao adotar isso
O erro mais comum é tratar o schema como documentação opcional. Equipes definem uma interface TypeScript e nunca validam em runtime. Uma interface é uma promessa, não uma verificação. Se uma variável de ambiente do staging estiver faltando, TypeScript não vai detectar porque process.env é populado em runtime.
A validação deve acontecer na inicialização, antes que qualquer requisição seja atendida. O schema é a fonte da verdade tanto para tipos quanto para valores.
O segundo erro é mesclar ambientes em uma única definição de configuração muito cedo. Comece com um contexto delimitado. Extraia sua configuração em um schema e sobreposições. Deixe o padrão se provar.
Juntando tudo: uma migração em três passos
Se você está começando a partir de uma pilha de arquivos .env, aqui está um caminho que não exige um big-bang rewrite.
Passo um: escolha o contexto delimitado com mais incidentes relacionados a configuração. É onde a dor é maior e a equipe sentirá o benefício mais rápido.
Passo dois: faça um inventário de todos os valores de configuração que aquele contexto lê. Separe-os em segredos, não-segredos específicos por ambiente e padrões compartilhados. Escreva um schema Zod que capture os formatos e restrições.
Passo três: substitua as leituras diretas de process.env naquele contexto por uma única chamada loadConfig() que usa o schema e as sobreposições. Faça deploy em um ambiente, observe-o detectar um valor faltante no CI e corrija. A validação provará imediatamente seu valor.
Repita para o próximo contexto. Depois de três ou quatro contextos, o padrão é auto-sustentável. As equipes o adotam sem serem obrigadas porque já viram ele prevenir incidentes.
FAQ
O que é uma DSL de configuração?
Uma DSL de configuração é uma linguagem pequena e específica de domínio para definir configuração dentro de um contexto delimitado. Ela tipicamente inclui um schema, valores base e sobreposições por ambiente. É estrutura suficiente para tornar a configuração explícita e validá-la antes do runtime.
Por que não usar apenas um serviço de configuração centralizado?
Um serviço centralizado é útil para valores dinâmicos como feature flags. Configuração estática, como endpoints de API e políticas de retry, raramente muda em runtime. Obtê-la de um serviço remoto adiciona uma dependência de rede à inicialização. O padrão DSL lida com configuração estática localmente e deixa valores dinâmicos para o serviço.
Como eu lido com segredos?
Segredos devem entrar no sistema através de variáveis de ambiente ou um vault, mas ainda devem passar pelo validador do schema. Defina-os como strings obrigatórias no schema. Leia-os de process.env ou de um cliente de vault dentro do objeto de sobreposição por ambiente. Se o segredo estiver faltando, a validação falha e a aplicação encerra de forma limpa.
Posso usar isso sem TypeScript?
Sim. O padrão funciona em qualquer linguagem com uma biblioteca de validação. Python tem Pydantic. Rust tem serde com validator. Go tem go-playground/validator. A camada DSL é apenas uma função que mescla um objeto base com uma sobreposição e valida o resultado.
Quando isso é exagero?
Se sua aplicação tem menos de uma dúzia de valores de configuração e um único ambiente, um arquivo .env é mais simples. Adote o padrão DSL quando você tem múltiplos ambientes, múltiplas equipes ou um histórico de incidentes relacionados a configuração.
O que fazer em seguida
Audite seus arquivos .env atuais ou configurações YAML para valores que são idênticos entre ambientes. Esses são seus padrões base. Qualquer coisa que difere é uma sobreposição. Escolha um contexto delimitado, defina seu schema e mova esses valores para uma estrutura que torna as diferenças explícitas. Na primeira vez que seu CI detectar uma variável faltante no staging antes que ela chegue a production, a fronteira está funcionando.