TL;DR — Resumo Rápido
Pulumi permite definir infraestrutura cloud com TypeScript, Python, Go, C# e Java. Guia completo de stacks, estado, testes e integração com CI/CD.
Pulumi é uma plataforma de infraestrutura como código que permite definir, implantar e gerenciar recursos cloud usando linguagens de programação reais — TypeScript, Python, Go, C#, Java — em vez de linguagens de domínio específico como HCL ou YAML. Se você já se deparou com os limites dos loops no Terraform, lutou com a verbosidade do JSON no CloudFormation, ou desejou poder testar sua infraestrutura com o mesmo framework de testes usado para código de aplicação, Pulumi resolve exatamente esses problemas. Este guia cobre desde a instalação e conceitos fundamentais até testes, gerenciamento de estado, integração com CI/CD e um exemplo prático multi-nuvem.
Pré-requisitos
- Familiaridade com ao menos um de: TypeScript/JavaScript, Python, Go, C# ou Java
- Uma conta no AWS, Azure ou GCP (o nível gratuito é suficiente para os exemplos)
- Node.js 18+ instalado (para os exemplos de TypeScript/JavaScript)
- Entendimento básico de conceitos cloud (VPCs, buckets de armazenamento, VMs)
- Conta no Pulumi Cloud (nível gratuito) ou um backend de estado alternativo
Por Que Pulumi: Linguagens Reais vs. HCL e YAML
A diferença fundamental entre Pulumi e ferramentas como Terraform ou CloudFormation é que os programas Pulumi são escritos em linguagens de programação de propósito geral. Isso desbloqueia capacidades que são complicadas ou impossíveis em ferramentas baseadas em DSL.
Suporte completo de IDE — Seu editor fornece autocomplete para cada propriedade de recurso, documentação inline e erros de tipo antes de executar qualquer implantação. Digitar errado o nome de uma propriedade de bucket S3 é um erro em tempo de compilação, não uma falha em tempo de execução após uma chamada de API de 30 segundos.
Fluxo de controle padrão — Loops, condicionais, funções e classes funcionam como esperado. Criar 10 subnets é um loop for, não o meta-argumento count do Terraform nem a complexidade do for_each.
Componentes reutilizáveis como pacotes — Você pode publicar componentes de infraestrutura no npm, PyPI ou Maven. Um time que constrói um módulo de VPC compatível publica como pacote; outros times o adicionam como dependência e importam como qualquer biblioteca.
Testes com frameworks padrão — Testes unitários usam Jest, pytest ou o pacote testing do Go. Testes de propriedade usam Policy as Code. Testes de integração implantam infraestrutura real e a validam de ponta a ponta.
| Recurso | Pulumi | Terraform/OpenTofu | AWS CDK | Crossplane | CloudFormation |
|---|---|---|---|---|---|
| Linguagem | TS/Python/Go/C#/Java | HCL | TS/Python/Java/Go | YAML/CRDs | JSON/YAML |
| Suporte IDE | Completo (tipos + autocomplete) | Parcial (plugin HCL) | Completo | Mínimo | Mínimo |
| Testes | Frameworks padrão | Terratest (Go) | Frameworks padrão | Sem suporte nativo | Sem suporte nativo |
| Gerenciamento de estado | Cloud/S3/GCS/Azure/local | Cloud/S3/GCS/Azure/local | Stacks CloudFormation | Kubernetes etcd | CloudFormation |
| Multi-nuvem | Sim (programa único) | Sim (múltiplos provedores) | Focado em AWS | Sim | Apenas AWS |
| Pacotes reutilizáveis | npm/PyPI/Maven/NuGet | Terraform Registry | npm/PyPI/Maven/NuGet | Helm/OCI | Stacks aninhados |
| Curva de aprendizado | Baixa (usa linguagem existente) | Média (aprender HCL) | Baixa (usa linguagem existente) | Alta (CRDs Kubernetes) | Alta (JSON/YAML verboso) |
Arquitetura: Como o Pulumi Funciona
Language host — Um processo executando o runtime da linguagem escolhida (Node.js, Python, JVM, etc.) que executa seu programa e chama o SDK do Pulumi para registrar recursos.
Motor do Pulumi — O processo de orquestração central que recebe solicitações de registro de recursos, calcula o grafo de dependências, compara o estado desejado com o último estado conhecido e gera um plano.
Provedores de recursos — Plugins que traduzem declarações de recursos Pulumi em chamadas de API (AWS, Azure, GCP, Kubernetes, etc.).
Backend de estado — Armazena o último estado conhecido do seu stack. O motor faz diff da nova saída do programa contra este estado para determinar o que criar, atualizar ou excluir.
Motor de implantação — Executa o plano respeitando o grafo de dependências, realizando operações de recursos independentes em paralelo.
Instalação
# Linux/macOS via script de instalação
curl -fsSL https://get.pulumi.com | sh
# macOS via Homebrew
brew install pulumi
# Windows via Chocolatey
choco install pulumi
# Verificar
pulumi version
Login no backend de estado:
# Pulumi Cloud (padrão, nível gratuito disponível)
pulumi login
# Backend S3
pulumi login s3://seu-bucket-de-estado/pulumi
# Sistema de arquivos local (não recomendado para times)
pulumi login --local
Estrutura do Projeto
pulumi new aws-typescript # TypeScript + AWS
pulumi new aws-python # Python + AWS
pulumi new azure-go # Go + Azure
Um projeto TypeScript tem esta estrutura:
minha-infra/
Pulumi.yaml # Metadados do projeto
Pulumi.dev.yaml # Configuração do stack "dev"
package.json # Dependências npm
index.ts # Ponto de entrada
Conceitos Fundamentais
Recursos
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("meu-bucket", {
acl: "private",
tags: { Ambiente: "producao" },
versioning: { enabled: true },
});
export const nomeBucket = bucket.bucket;
export const arnBucket = bucket.arn;
Entradas e Saídas
Propriedades de recursos são Output<T> — valores assíncronos que se resolvem após a criação do recurso:
// pulumi.interpolate — para templates de string
const urlBucket = pulumi.interpolate`https://${bucket.bucket}.s3.amazonaws.com`;
// Output.apply — para transformações
const nomeMaiusculo = bucket.bucket.apply(name => name.toUpperCase());
// Output.all — quando precisar de múltiplas saídas juntas
const combinado = pulumi.all([bucket.bucket, bucket.arn]).apply(([nome, arn]) => ({
nome, arn, url: `https://${nome}.s3.amazonaws.com`,
}));
Referências de Stack
const stackRede = new pulumi.StackReference("org/network/prod");
const vpcId = stackRede.getOutput("vpcId");
const subnetPrivadasIds = stackRede.getOutput("privateSubnetIds");
ComponentResource
class BucketS3Seguro extends pulumi.ComponentResource {
public readonly bucket: aws.s3.Bucket;
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("minhaorg:storage:BucketS3Seguro", name, {}, opts);
this.bucket = new aws.s3.Bucket(`${name}-bucket`, {
acl: "private",
versioning: { enabled: true },
}, { parent: this });
new aws.s3.BucketPublicAccessBlock(`${name}-block`, {
bucket: this.bucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
}, { parent: this });
this.registerOutputs({ nomeBucket: this.bucket.bucket });
}
}
Infraestrutura em Python
import pulumi
import pulumi_aws as aws
vpc = aws.ec2.Vpc("vpc-principal",
cidr_block="10.0.0.0/16",
enable_dns_hostnames=True,
tags={"Ambiente": pulumi.get_stack()},
)
zonas_disponibilidade = ["us-east-1a", "us-east-1b", "us-east-1c"]
subnets = [
aws.ec2.Subnet(f"subnet-{i}",
vpc_id=vpc.id,
cidr_block=f"10.0.{i}.0/24",
availability_zone=az,
)
for i, az in enumerate(zonas_disponibilidade)
]
pulumi.export("vpc_id", vpc.id)
Backends de Estado
| Backend | Comando | Ideal para |
|---|---|---|
| Pulumi Cloud | pulumi login | Times, criptografia de segredos, log de auditoria integrado |
| AWS S3 | pulumi login s3://bucket/caminho | Times nativos AWS |
| Azure Blob | pulumi login azblob://container/caminho | Times nativos Azure |
| Google Cloud Storage | pulumi login gs://bucket/caminho | Times nativos GCP |
| Arquivo local | pulumi login --local | Apenas testes individuais |
Stacks e Ambientes
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod
pulumi stack select prod
pulumi config set aws:region sa-east-1
pulumi config set --secret senhaDatabase "valor-super-secreto"
Testes
Testes Unitários com Mocks
pulumi.runtime.setMocks({
newResource: (args: pulumi.runtime.MockResourceArgs) => {
return { id: `${args.name}-id`, state: args.inputs };
},
call: (args: pulumi.runtime.MockCallArgs) => args.inputs,
});
Policy as Code com CrossGuard
new PolicyPack("conformidade-aws", {
policies: [
{
name: "s3-sem-leitura-publica",
description: "Buckets S3 não devem permitir leitura pública",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (bucket.acl === "public-read") {
reportViolation("Bucket S3 não deve ser legível publicamente.");
}
}),
},
],
});
Integração com CI/CD
GitHub Actions
name: Deploy com Pulumi
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
work-dir: ./infra
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Automation API
import { LocalWorkspace } from "@pulumi/pulumi/automation";
async function provisionarAmbiente(tenantId: string) {
const stack = await LocalWorkspace.createOrSelectStack({
stackName: `tenant-${tenantId}`,
projectName: "saas-infra",
program: async () => {
const bucket = new aws.s3.Bucket(`${tenantId}-dados`);
return { nomeBucket: bucket.bucket };
},
});
await stack.setConfig("aws:region", { value: "sa-east-1" });
const resultado = await stack.up({ onOutput: console.log });
return resultado.outputs;
}
Exemplo Multi-Nuvem Prático
import * as aws from "@pulumi/aws";
import * as cloudflare from "@pulumi/cloudflare";
const config = new pulumi.Config();
const dominio = config.require("domainName");
const cfZoneId = config.requireSecret("cloudflareZoneId");
const lb = new aws.lb.LoadBalancer("lb-web", {
internal: false,
loadBalancerType: "application",
subnets: subnetsPublicasIds,
});
const registroDns = new cloudflare.Record("dns-web", {
zoneId: cfZoneId,
name: dominio,
type: "CNAME",
value: lb.dnsName,
proxied: true,
});
export const urlApp = pulumi.interpolate`https://${dominio}`;
Armadilhas e Casos Especiais
Saídas em contexto de string — Nunca use const url = "https://" + bucket.bucket. Use pulumi.interpolate. O operador + converte uma Output em [object Object].
Excluir saídas do stack — Remover um export não exclui o recurso; apenas remove o valor de saída. Para excluir o recurso, remova-o do código do programa.
pulumi refresh vs pulumi up — Use pulumi refresh para sincronizar o estado com o estado real da nuvem após alterações manuais. Use pulumi up para aplicar mudanças do programa.
Nomenclatura de recursos — Pulumi adiciona um sufixo aleatório aos nomes de recursos por padrão (ex. meu-bucket-a3f8c2b). Use a propriedade name explicitamente se precisar de um nome fixo.
Resumo
- Pulumi usa linguagens de programação reais (TypeScript, Python, Go, C#, Java) em vez de HCL ou YAML, oferecendo suporte completo de IDE e testes padrão
- O motor calcula um grafo de dependências e aplica mudanças em paralelo, comparando contra o estado armazenado no Pulumi Cloud, S3, Azure Blob ou GCS
- Stacks isolam ambientes dev/staging/prod; cada stack tem sua própria configuração e estado
ComponentResourcepermite abstrações de infraestrutura reutilizáveis publicáveis como pacotes npm ou PyPI- Testes unitários usam mocks; testes de integração usam a Automation API; conformidade usa políticas CrossGuard
- A Automation API embute Pulumi em código de aplicação para portais de autoatendimento e ambientes efêmeros
- Integração CI/CD é um único passo
pulumi/actionsno GitHub Actions ou equivalente no GitLab CI