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.

RecursoPulumiTerraform/OpenTofuAWS CDKCrossplaneCloudFormation
LinguagemTS/Python/Go/C#/JavaHCLTS/Python/Java/GoYAML/CRDsJSON/YAML
Suporte IDECompleto (tipos + autocomplete)Parcial (plugin HCL)CompletoMínimoMínimo
TestesFrameworks padrãoTerratest (Go)Frameworks padrãoSem suporte nativoSem suporte nativo
Gerenciamento de estadoCloud/S3/GCS/Azure/localCloud/S3/GCS/Azure/localStacks CloudFormationKubernetes etcdCloudFormation
Multi-nuvemSim (programa único)Sim (múltiplos provedores)Focado em AWSSimApenas AWS
Pacotes reutilizáveisnpm/PyPI/Maven/NuGetTerraform Registrynpm/PyPI/Maven/NuGetHelm/OCIStacks aninhados
Curva de aprendizadoBaixa (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

BackendComandoIdeal para
Pulumi Cloudpulumi loginTimes, criptografia de segredos, log de auditoria integrado
AWS S3pulumi login s3://bucket/caminhoTimes nativos AWS
Azure Blobpulumi login azblob://container/caminhoTimes nativos Azure
Google Cloud Storagepulumi login gs://bucket/caminhoTimes nativos GCP
Arquivo localpulumi login --localApenas 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
  • ComponentResource permite 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/actions no GitHub Actions ou equivalente no GitLab CI

Artigos Relacionados