O Que é Pulumi e Por Que Abandona a Sintaxe Declarativa
Pulumi é uma plataforma de Infrastructure as Code (IaC) que permite descrever sua infraestrutura usando linguagens de programação reais como Python, TypeScript, Go e C#, em vez de sintaxes declarativas como YAML ou HCL. A diferença fundamental está no paradigma: enquanto Terraform força você a aprender uma linguagem específica (HCL), Pulumi permite que você use a linguagem que já domina.
A vantagem disso é imensa. Com linguagens reais, você tem acesso a loops, condicionais, funções, classes e toda a potência de um ecossistema maduro. Não precisa aprender sintaxe nova nem lidar com limitações artificiais. Se você sabe Python, sabe provisionar infraestrutura em Pulumi. Além disso, você consegue abstrair complexidade em componentes reutilizáveis, compartilhar lógica entre equipes e manter código limpo sem macetes de templating.
Arquitetura e Conceitos Fundamentais
Pilares da Plataforma Pulumi
Pulumi funciona sobre três pilares principais: o programa, o estado e o serviço. O programa é seu código que descreve a infraestrutura — é isso que você escreve. O estado registra qual infraestrutura foi criada e suas propriedades atuais, permitindo atualizações incrementais. O serviço (Pulumi Service ou um backend local) armazena esse estado e gerencia o histórico de mudanças.
Quando você executa pulumi up, o sistema executa seu programa, compara o estado desejado com o atual, calcula as diferenças e aplica as mudanças de forma segura. Esse fluxo é familiar para quem usa Terraform, mas com a diferença crítica: você está escrevendo em uma linguagem real, não em um DSL restritivo.
Stack e Configuração
Um conceito essencial é a Stack, que representa um ambiente isolado (desenvolvimento, staging, produção). Cada Stack tem seu próprio estado e configurações. Você pode ter uma única base de código que provisiona infraestrutura diferente por Stack apenas alterando arquivos de configuração YAML.
import pulumi
config = pulumi.Config()
ambiente = config.require("ambiente")
tamanho_instancia = config.get("tamanho_instancia") or "t2.micro"
pulumi.export("ambiente", ambiente)
pulumi.export("tamanho", tamanho_instancia)
Aqui, pulumi.Config() lê do arquivo Pulumi.<stack>.yaml onde você define valores específicos por ambiente. Isso permite que o mesmo código provisione tudo diferente conforme a Stack selecionada.
Exemplo Prático: Provisionando AWS com Python
Estrutura de Projeto
Um projeto Pulumi começa com pulumi new. Vamos criar um exemplo real que provisiona uma aplicação web na AWS com VPC, segurança e instâncias EC2.
pulumi new aws-python
cd seu-projeto
Isso cria uma estrutura básica. Vamos expandir __main__.py:
import pulumi
import pulumi_aws as aws
import json
# Ler configurações por ambiente
config = pulumi.Config()
ambiente = config.require("ambiente")
cidr_block = config.get("cidr_block") or "10.0.0.0/16"
instancia_count = int(config.get("instancia_count") or "2")
tags_padrao = {
"Ambiente": ambiente,
"ManagedBy": "Pulumi"
}
# Criar VPC
vpc = aws.ec2.Vpc("vpc-app",
cidr_block=cidr_block,
tags={**tags_padrao, "Name": f"vpc-{ambiente}"}
)
# Criar subnet
subnet = aws.ec2.Subnet("subnet-app",
vpc_id=vpc.id,
cidr_block="10.0.1.0/24",
availability_zone="us-east-1a",
tags={**tags_padrao, "Name": f"subnet-{ambiente}"}
)
# Criar Security Group com regras
sg = aws.ec2.SecurityGroup("sg-app",
vpc_id=vpc.id,
ingress=[
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=80,
to_port=80,
cidr_blocks=["0.0.0.0/0"]
),
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=443,
to_port=443,
cidr_blocks=["0.0.0.0/0"]
),
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=22,
to_port=22,
cidr_blocks=["203.0.113.0/32"] # Seu IP
)
],
egress=[
aws.ec2.SecurityGroupEgressArgs(
protocol="-1",
from_port=0,
to_port=0,
cidr_blocks=["0.0.0.0/0"]
)
],
tags={**tags_padrao, "Name": f"sg-{ambiente}"}
)
# User data para inicializar a instância
user_data_script = """#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl start nginx
systemctl enable nginx
"""
# Buscar a AMI mais recente do Ubuntu
ubuntu_ami = aws.ec2.get_ami(
most_recent=True,
owners=["099720109477"], # Canonical
filters=[
aws.ec2.GetAmiFilterArgs(name="name", values=["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]),
aws.ec2.GetAmiFilterArgs(name="virtualization-type", values=["hvm"])
]
)
# Criar múltiplas instâncias com loop
instancias = []
for i in range(instancia_count):
instancia = aws.ec2.Instance(f"web-server-{i}",
ami=ubuntu_ami.id,
instance_type="t2.micro",
subnet_id=subnet.id,
vpc_security_group_ids=[sg.id],
user_data=user_data_script,
associate_public_ip_address=True,
tags={**tags_padrao, "Name": f"web-server-{i}-{ambiente}"}
)
instancias.append(instancia)
# Exportar valores importantes
pulumi.export("vpc_id", vpc.id)
pulumi.export("subnet_id", subnet.id)
pulumi.export("security_group_id", sg.id)
pulumi.export("instancia_ids", [inst.id for inst in instancias])
pulumi.export("instancia_ips_publicos", [inst.public_ip for inst in instancias])
Arquivo de Configuração
Crie Pulumi.dev.yaml:
config:
aws:region: us-east-1
meu-projeto:ambiente: development
meu-projeto:cidr_block: 10.0.0.0/16
meu-projeto:instancia_count: "2"
E Pulumi.prod.yaml:
config:
aws:region: us-east-1
meu-projeto:ambiente: production
meu-projeto:cidr_block: 10.1.0.0/16
meu-projeto:instancia_count: "4"
Executar e Gerenciar
# Selecionar o ambiente
pulumi stack select dev
# ou criar: pulumi stack init dev
# Ver o que será criado
pulumi preview
# Aplicar as mudanças
pulumi up
# Destruir a infraestrutura
pulumi destroy
Componentes Reutilizáveis: Abstraindo Complexidade
Por Que Abstrair?
Conforme sua infraestrutura cresce, você percebe que cria padrões repetidos: "toda aplicação web precisa de VPC, Security Group, Load Balancer...". Pulumi permite criar componentes — classes que encapsulam essa lógica e podem ser reutilizadas em projetos diferentes.
Criando um Componente
Vamos criar um componente que provisiona uma aplicação web completa:
import pulumi
import pulumi_aws as aws
from pulumi import ResourceOptions
class AplicacaoWeb(pulumi.ComponentResource):
def __init__(self, nome, config, opts=None):
super().__init__("pkg:index:AplicacaoWeb", nome, None, opts)
ambiente = config["ambiente"]
tags = {
"Componente": "AplicacaoWeb",
"Ambiente": ambiente
}
# VPC
vpc = aws.ec2.Vpc(f"{nome}-vpc",
cidr_block=config.get("cidr_block", "10.0.0.0/16"),
tags={**tags, "Name": f"{nome}-vpc"}
)
# Subnet
subnet = aws.ec2.Subnet(f"{nome}-subnet",
vpc_id=vpc.id,
cidr_block="10.0.1.0/24",
availability_zone=f"{config['regiao']}a",
tags={**tags, "Name": f"{nome}-subnet"}
)
# Internet Gateway
igw = aws.ec2.InternetGateway(f"{nome}-igw",
vpc_id=vpc.id,
tags={**tags, "Name": f"{nome}-igw"}
)
# Route Table
route_table = aws.ec2.RouteTable(f"{nome}-rt",
vpc_id=vpc.id,
routes=[
aws.ec2.RouteTableRouteArgs(
cidr_block="0.0.0.0/0",
gateway_id=igw.id
)
],
tags={**tags, "Name": f"{nome}-rt"}
)
# Associar subnet com route table
aws.ec2.RouteTableAssociation(f"{nome}-rta",
subnet_id=subnet.id,
route_table_id=route_table.id
)
# Security Group
sg = aws.ec2.SecurityGroup(f"{nome}-sg",
vpc_id=vpc.id,
ingress=[
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=80,
to_port=80,
cidr_blocks=["0.0.0.0/0"]
),
aws.ec2.SecurityGroupIngressArgs(
protocol="tcp",
from_port=443,
to_port=443,
cidr_blocks=["0.0.0.0/0"]
)
],
egress=[
aws.ec2.SecurityGroupEgressArgs(
protocol="-1",
from_port=0,
to_port=0,
cidr_blocks=["0.0.0.0/0"]
)
],
tags={**tags, "Name": f"{nome}-sg"}
)
# Exportar recursos criados
self.vpc_id = vpc.id
self.subnet_id = subnet.id
self.security_group_id = sg.id
self.register_outputs({
"vpc_id": vpc.id,
"subnet_id": subnet.id,
"security_group_id": sg.id
})
Usando o Componente
Agora em __main__.py, em vez de repetir código:
import pulumi
from aplicacao_web import AplicacaoWeb
config = pulumi.Config()
app_config = {
"ambiente": config.require("ambiente"),
"regiao": "us-east-1",
"cidr_block": "10.0.0.0/16"
}
aplicacao = AplicacaoWeb("minha-app", app_config)
pulumi.export("vpc_id", aplicacao.vpc_id)
pulumi.export("subnet_id", aplicacao.subnet_id)
pulumi.export("sg_id", aplicacao.security_group_id)
Esse padrão permite que você mantenha lógica complexa centralizada e reutilizável. Se precisa de 10 aplicações web, cria 10 instâncias do componente com configurações diferentes.
Secrets, Outputs e Gestão de Estado
Lidando com Dados Sensíveis
Pulumi oferece suporte nativo a secrets — valores criptografados que não aparecem em logs ou histórico. Isso é crítico para senhas, chaves de API e certificados.
import pulumi
import pulumi_aws as aws
config = pulumi.Config()
# Ler um secret
db_password = config.require_secret("db_password")
# Usar o secret (será criptografado no estado)
db = aws.rds.Instance("meu-banco",
allocated_storage=20,
engine="postgres",
engine_version="13.7",
instance_class="db.t3.micro",
db_name="proddb",
username="adminuser",
password=db_password, # Automaticamente criptografado
skip_final_snapshot=True
)
pulumi.export("db_endpoint", db.endpoint)
pulumi.export("db_password", db_password) # Será exportado criptografado
Configure o secret via CLI:
pulumi config set --secret db_password "sua_senha_super_secreta"
Outputs e Referências Entre Recursos
Um conceito crucial é que você pode referenciar outputs de um recurso em outro. Pulumi rastreia essas dependências automaticamente.
import pulumi
import pulumi_aws as aws
# Load balancer
alb = aws.lb.LoadBalancer("app-lb",
internal=False,
load_balancer_type="application",
security_groups=[sg.id],
subnets=[subnet1.id, subnet2.id]
)
target_group = aws.lb.TargetGroup("app-tg",
port=80,
protocol="HTTP",
vpc_id=vpc.id
)
# Listener usa o ARN do target group
listener = aws.lb.Listener("app-listener",
load_balancer_arn=alb.arn,
port=80,
protocol="HTTP",
default_actions=[
aws.lb.ListenerDefaultActionArgs(
type="forward",
target_group_arn=target_group.arn
)
]
)
# Exportar o DNS do load balancer
pulumi.export("dns_name", alb.dns_name)
Pulumi entende que listener depende de alb e target_group, e provisionará tudo na ordem correta.
Gerenciamento de Estado
O estado é mantido em um backend. Você pode usar:
- Pulumi Service (padrão, recomendado) — armazena remotamente, com acesso controlado
- Local — arquivo
Pulumi.<stack>.yaml(apenas desenvolvimento) - S3, Azure Blob, etc. — autogerenciado
Inicializar com um backend local:
pulumi login --local
Com Pulumi Service (padrão):
pulumi login
# Será pedido seu token (crie em app.pulumi.com)
Boas Práticas e Padrões Avançados
Organização de Código
Para projetos maiores, organize em módulos:
projeto/
├── __main__.py # Ponto de entrada
├── config.py # Configurações
├── componentes/
│ ├── __init__.py
│ ├── aplicacao_web.py
│ ├── banco_dados.py
│ └── cache.py
├── policies/ # Cloud policies (custo, segurança)
│ └── security.py
└── Pulumi.yaml
config.py:
import pulumi
def get_config():
config = pulumi.Config()
return {
"ambiente": config.require("ambiente"),
"regiao": config.get("regiao") or "us-east-1",
"tags_padrao": {
"Projeto": "MeuProjeto",
"Ambiente": config.require("ambiente"),
"ManagedBy": "Pulumi"
}
}
__main__.py:
import pulumi
from config import get_config
from componentes.aplicacao_web import AplicacaoWeb
from componentes.banco_dados import BancoDados
cfg = get_config()
# Provisionar componentes
app = AplicacaoWeb("app", cfg)
db = BancoDados("db", cfg, vpc_id=app.vpc_id)
pulumi.export("app_dns", app.dns)
pulumi.export("db_endpoint", db.endpoint)
Testing e Validação
Pulumi suporta testes via frameworks padrão:
# test_infra.py
import pulumi
import pulumi_testing as pt
import json
def test_vpc_created():
def check_vpc(outputs):
assert "vpc_id" in outputs
assert outputs["vpc_id"] is not None
pt.run_test(
program=lambda: __import__("__main__"),
expected_resource_count=15, # Aproximadamente
check=check_vpc
)
def test_tags_aplicadas():
def check_tags(outputs):
assert "tags" in outputs
tags = outputs["tags"]
assert "Ambiente" in tags
pt.run_test(program=lambda: __import__("__main__"), check=check_tags)
Execute com pytest:
pip install pulumi[testing]
pytest test_infra.py -v
Policy as Code
Pulumi permite definir políticas que executam antes de aplicar mudanças:
# policies/security.py
import pulumi
from pulumi import automation as auto
def policy_sem_public_ip(stack, resource_type, resource_name, resource_config):
"""Bloqueia EC2 com IP público em produção"""
if resource_type == "aws:ec2/instance:Instance":
ambiente = pulumi.Config().get("ambiente")
if ambiente == "production":
if resource_config.get("associate_public_ip_address"):
return [
f"Recurso {resource_name} não pode ter IP público em produção"
]
return []
def registrar_policies():
pulumi.runtime.register_resource_validation_hook(
policy_sem_public_ip
)
Conclusão
Pulumi revoluciona a forma como pensamos sobre Infrastructure as Code ao permitir linguagens reais em vez de DSLs restritivas. Aprendemos três pontos fundamentais: primeiro, a capacidade de usar Python, TypeScript, Go ou C# torna a infraestrutura mais expressiva, permitindo loops, funções e orientação a objetos nativa, eliminando hacks de templating; segundo, componentes reutilizáveis criam abstrações poderosas que escalam conforme a complexidade cresce, permitindo que times compartilhem padrões de infraestrutura como bibliotecas; terceiro, o gerenciamento de estado e secrets é robusto e integrado, com suporte a múltiplos backends e criptografia automática, resolvendo problemas reais de segurança que outras ferramentas deixam soltos.