03 - Pulumi (Usando linguagens de programação)
video: https://youtu.be/... no frontmatter da nota.Pulumi (Usando linguagens de programação)
Pulumi é a ferramenta de IaC que permite descrever infraestrutura em linguagens de programação de propósito geral, TypeScript, Python, Go, C# e Java, em vez de linguagem de configuração dedicada como HCL ou YAML. O programa é compilado e executado pela CLI do Pulumi, que registra os recursos desejados em uma engine declarativa responsável pelo plan, pela ordem de execução e pela convergência, com providers que falam com as mesmas nuvens que Terraform fala. O valor central é real: loops, funções, classes, tipos, testes unitários e todo o ferramental de engenharia de software passam a valer para infraestrutura, o que encurta o caminho para times de desenvolvimento que já dominam essas linguagens. O preço também é real: abstrações ficam fáceis demais de construir, o código pode esconder decisões críticas de infraestrutura dentro de lógica imperativa, e a equipe de plataforma precisa ler programas, não apenas arquivos de configuração. Este arquivo cobre como a engine funciona, onde a expressividade da linguagem paga o preço e quando HCL continua sendo a melhor resposta.
Conceito: programas que declaram, engine que converge
Um programa Pulumi é um programa normal: você escreve funções, importa bibliotecas e constrói objetos de recursos, new aws.s3.Bucket(...). A execução não cria nada diretamente: o programa roda sob controle da CLI e apenas declara os recursos que existiriam naquele momento, em uma execução de registro. A engine compara essa declaração com o estado anterior, calcula o delta e executa as operações na ordem do grafo de dependências.
Essa separação é o ponto que desarma a objeção clássica ao IaC imperativo. O código-fonte usa imperatividade de linguagem, laços e condicionais, mas a semântica de execução é declarativa: a engine garante idempotência, diff e ordenação. Escrever um for que cria dez filas não é um script que cria filas; é uma declaração de que dez filas devem existir, e rodar de novo não cria nada duplicado.
O modelo de estado segue as convenções do domínio: state remoto por projeto e por stack, armazenado no serviço Pulumi Cloud ou em backends self-hosted, como S3, com lock de execução. O conceito de stack, dev, staging, produção, é nativo e separado do código, o que facilita composição multi-ambiente com o mesmo programa.
Em produção, o programa Pulumi não é executado por desenvolvedores a partir da máquina local: é a pipeline quem roda preview e up, com credenciais efêmeras e com o state travado. O diff do preview é anexado ao pull request, e a revisão segue a mesma disciplina de qualquer IaC, com olhar especial para operações de substituição e destruição, que a CLI marca explicitamente no diff.
O problema que resolve: os limites da linguagem de configuração
Linguagens de configuração dedicadas, HCL e YAML, escalam bem até um ponto e depois começam a ranger. Lógica repetida vira cópia com pequenas variações; abstração vira módulo com dezenas de variáveis opcionais; lógica condicional complexa fica truncada pela expressividade da linguagem; e testes unitários do desenho, no sentido de asserções sobre o que será criado, dependem de ferramentas externas que simulam o diff.
Pulumi resolve essas quatro dores com a linguagem. A repetição vira função; a abstração vira classe com interface tipada; a condicional é um if comum; e o teste unitário roda o programa com mocks e asserções sobre os recursos criados, sem nuvem e sem plano. Para times de desenvolvimento fortes, a curva de aprendizado cai de forma mensurável: a pessoa já sabe programar, e o que ela precisa aprender é o schema dos providers, não uma nova linguagem.
Há um segundo ganho menos citado: versionamento e distribuição de abstrações. Uma biblioteca interna de componentes Pulumi é um pacote npm ou pip, versionado, testado e publicado como qualquer biblioteca de software. Isso transforma o time de plataforma em produtor de pacotes de infraestrutura, com ciclo de release conhecido, algo que os registries de módulos HCL aproximam, mas com menos poder de composição.
Funcionamento interno: a engine declarativa por baixo
A arquitetura tem três camadas. O programa, na linguagem escolhida, roda na máquina do executor; a SDK da linguagem serializa as declarações de recursos para a engine via gRPC, inclusive com execução em múltiplas linguagens no mesmo projeto; a engine, escrita em Go, mantém o state, calcula o diff e orquestra os providers, que são os mesmos binários que Terraform usa, o que explica a cobertura de recursos praticamente idêntica.
O conceito de resource em Pulumi guarda uma propriedade importante: as saídas dos recursos são valores assíncronos, outputs, que a SDK propaga como promessas. Isso é o que permite encadear dependências em código imperativo sem perder a propriedade declarativa; o apply de um output exprime uma dependência, e a engine resolve a ordem real de criação. Entender outputs e apply é a principal curva de aprendizado de quem chega de programação convencional.
O fluxo de trabalho espelha o ciclo do domínio: pulumi preview é o plan, pulumi up é o apply, pulumi destroy é o destroy, e a CLI pergunta confirmação com diff colorido. Em pipeline, o fluxo é o mesmo com flags não interativas, e o preview pode ser anexado ao pull request por integração nativa do Pulumi Cloud, que comenta o PR com o diff calculado.
O ciclo de vida de recursos oferece os mesmos controles do ecossistema HCL: proteção contra destruição por recurso, timeouts por operação, dependências explícitas com dependsOn quando a referência de output não basta, e estratégias de substituição para recursos que não aceitam edição in-place. Quem já opera Terraform encontra paridade funcional, e a diferença fica mesmo na linguagem do programa e no backend de state.
// Programa Pulumi em TypeScript: fila com DLQ como função reutilizável
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Função: a linguagem permite abstração com assinatura tipada
function criarFilaComDlq(nome: string, maxReceive: number) {
const dlq = new aws.sqs.Queue(`${nome}-dlq`, {
messageRetentionSeconds: 1209600, // 14 dias de retenção
});
const fila = new aws.sqs.Queue(nome, {
redrivePolicy: dlq.arn.apply(arn =>
JSON.stringify({
deadLetterTargetArn: arn, // dependência declarada
maxReceiveCount: maxReceive // por parâmetro, não por cópia
})
),
tags: { ManagedBy: "pulumi" },
});
return { fila, dlq };
}
// Loop: cada domínio de negócio ganha fila própria com a mesma garantia
for (const dominio of ["pedidos", "pagamentos", "entregas"]) {
const { fila } = criarFilaComDlq(`fila-${dominio}`, 5);
pulumi.export(`url-${dominio}`, fila.url); // output tipado
}
Testes unitários de infraestrutura
O argumento mais forte de Pulumi é o teste. Com o framework de mocks da SDK, o programa roda em teste unitário comum, mocha, pytest ou go test, e as asserções interrogam os recursos declarados: o bucket tem criptografia habilitada? A fila aponta para a DLQ certa? O security group abre porta que não devia? Nenhuma nuvem é chamada, e o teste roda em segundos no CI de qualquer pull request.
A camada seguinte são os testes de política, policy as code: regras escritas na mesma linguagem que valem para qualquer programa do time, como proibir buckets sem criptografia ou instâncias sem tag de custo. As políticas rodam no preview e no up, e há pacotes prontos de políticas de referência por nuvem. É o equivalente ao Sentinel e ao OPA do ecossistema HCL, com a vantagem de viver no mesmo ferramental de teste.
// Teste unitário com mocks: asserção sobre o que foi declarado
import * as pulumi from "@pulumi/pulumi";
pulumi.runtime.setMocks({
newResource: (args) => ({ id: `${args.name}_id`, state: args.inputs }),
});
it("fila de pedidos aponta para DLQ com maxReceiveCount 5", async () => {
const recursos = await Promise.all([
import("./index") // executa o programa
]);
const filas = recursos[0].criarFilaComDlq("fila-pedidos", 5);
const politica = await new Promise<string>(res =>
filas.fila.redrivePolicy.apply(res)
);
const parsed = JSON.parse(politica);
if (parsed.maxReceiveCount !== 5) {
throw new Error("maxReceiveCount incorreto");
}
});
Comparação com Terraform e HCL
| Dimensão | Pulumi | Terraform ou OpenTofu |
|---|---|---|
| Linguagem | TypeScript, Python, Go, C#, Java | HCL |
| Abstração | Funções, classes e pacotes da linguagem | Módulos com variáveis e outputs |
| Testes unitários | Nativos, com mocks da SDK | Ferramentas externas de simulação |
| Reuso de lógica | Bibliotecas npm, pip, go mod | Registry de módulos, sem lógica geral |
| Curva para devs | Baixa, linguagem conhecida | Média, nova linguagem de configuração |
| Curva para ops | Média-alta, exige programação | Baixa-média, formato declarativo |
| State | Pulumi Cloud ou backend próprio | Backend do ecossistema HCL |
| Legibilidade do diff | Equivalente, via preview | Equivalente, via plan |
| Risco de ofuscação | Alto se sem disciplina de abstração | Baixo, formato puxa para o explícito |
| Licença | Apache 2.0 | BSL 1.1 e MPL 2.0, no OpenTofu |
A leitura de decisão não é sobre qual ferramenta é melhor no abstrato: é sobre qual linguagem o time domina e qual risco o time prefere gerenciar. O código Terraform de mil linhas é verbose e explícito; o código Pulumi de duzentas linhas é compacto e pode esconder decisões dentro de funções. Os dois conseguem resultados excelentes, com disciplinas diferentes de revisão.
Quando o poder da linguagem paga o preço
O poder da linguagem paga quando a infraestrutura tem lógica real e repetida. Décadas de padrões, múltiplos produtos com desenho idêntico parametrizado, topologias condicionais por ambiente, integração com sistemas internos via APIs, e times de desenvolvimento fortes que vão tocar o código de infraestrutura. Nesses cenários, as abstrações de Pulumi transformam-se em plataformas internas com custo de manutenção menor que módulos gigantes de HCL.
O poder não paga quando a infraestrutura é configuracional e estável, desenhada por time de operações que não programa no dia a dia, e revisada por gente que precisa ler cada decisão. Nesse perfil, HCL ganha porque o formato puxa para o explícito: cada recurso está à vista, o diff é direto, e a barreira de entrada baixa permite auditoria por mais pessoas. A escolha errada aqui não é técnica, é de audiência.
O modelo híbrido é legítimo e comum: plataforma central em Pulumi com bibliotecas versionadas, e times consumidores escrevendo programas curtos que só chamam componentes. O ponto de atenção é o contrato: componentes internos precisam de versionamento semântico, changelog e testes, porque, na prática, viram produto, e produto sem release notes vira incidente de compatibilidade no consumidor.
Há um custo adicional de governança que vale explicitar: em HCL, uma varredura de conformidade lê arquivos de configuração; em Pulumi, ela precisa executar programas ou confiar nas políticas da engine. Times com requisitos de auditoria estática pesados devem pesar esse detalhe na decisão, pois parte do ferramental de análise da comunidade HCL não se aplica diretamente.
Erros comuns e boas práticas
- Escrever lógica com efeito colateral no caminho de declaração, chamadas de API externa e leitura de dados não determinísticos no programa; o programa precisa ser determinístico por execução.
- Construir abstrações profundas sem testes; abstração de infraestrutura sem teste unitário e sem changelog é dívida com juros altos.
- Deixar segredos em texto claro no código; use secret do Pulumi ou gerenciador externo, e trate o state como sensível, pois ele guarda valores em claro por padrão no backend.
- Misturar stack de dev e de produção no mesmo projeto sem parâmetros; stack existe exatamente para isolar estados com o mesmo código.
- Rodar
pulumi upfora de pipeline em produção; o preview precisa estar no PR, e o up precisa ser executado pela CI com autenticação efêmera. - Ignorar outputs e encadear dependências por convenção de nome; dependência implícita quebra a ordenação da engine e produz falhas de criação intermitentes.
- Adotar Pulumi só porque a sintaxe parece familiar e terminar com time de operações incapaz de ler o código; a audiência decide, não a novidade.
- Esquecer que os providers são os mesmos de Terraform; schemas, ciclos de vida e armadilhas por recurso são compartilhados, e o aprendizado transfere.