<h2>O Que é Pulumi e Por Que Abandona a Sintaxe Declarativa</h2>
<p>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.</p>
<p>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.</p>
<h2>Arquitetura e Conceitos Fundamentais</h2>
<h3>Pilares da Plataforma Pulumi</h3>
<p>Pulumi funciona sobre três pilares principais: o <strong>programa</strong>, o <strong>estado</strong> e o <strong>serviço</strong>. 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.</p>
<p>Quando você executa <code>pulumi up</code>, 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.</p>
<h3>Stack e Configuração</h3>
<p>Um conceito essencial é a <strong>Stack</strong>, 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.</p>
<pre><code class="language-python">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)</code></pre>
<p>Aqui, <code>pulumi.Config()</code> lê do arquivo <code>Pulumi.<stack>.yaml</code> onde você define valores específicos por ambiente. Isso permite que o mesmo código provisione tudo diferente conforme a Stack selecionada.</p>
<h2>Exemplo Prático: Provisionando AWS com Python</h2>
<h3>Estrutura de Projeto</h3>
<p>Um projeto Pulumi começa com <code>pulumi new</code>. Vamos criar um exemplo real que provisiona uma aplicação web na AWS com VPC, segurança e instâncias EC2.</p>
<pre><code class="language-bash">pulumi new aws-python
cd seu-projeto</code></pre>
<p>Isso cria uma estrutura básica. Vamos expandir <code>__main__.py</code>:</p>
<pre><code class="language-python">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])</code></pre>
<h3>Arquivo de Configuração</h3>
<p>Crie <code>Pulumi.dev.yaml</code>:</p>
<pre><code class="language-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"</code></pre>
<p>E <code>Pulumi.prod.yaml</code>:</p>
<pre><code class="language-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"</code></pre>
<h3>Executar e Gerenciar</h3>
<pre><code class="language-bash"># 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</code></pre>
<h2>Componentes Reutilizáveis: Abstraindo Complexidade</h2>
<h3>Por Que Abstrair?</h3>
<p>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 <strong>componentes</strong> — classes que encapsulam essa lógica e podem ser reutilizadas em projetos diferentes.</p>
<h3>Criando um Componente</h3>
<p>Vamos criar um componente que provisiona uma aplicação web completa:</p>
<pre><code class="language-python">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
})</code></pre>
<h3>Usando o Componente</h3>
<p>Agora em <code>__main__.py</code>, em vez de repetir código:</p>
<pre><code class="language-python">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)</code></pre>
<p>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.</p>
<h2>Secrets, Outputs e Gestão de Estado</h2>
<h3>Lidando com Dados Sensíveis</h3>
<p>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.</p>
<pre><code class="language-python">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</code></pre>
<p>Configure o secret via CLI:</p>
<pre><code class="language-bash">pulumi config set --secret db_password "sua_senha_super_secreta"</code></pre>
<h3>Outputs e Referências Entre Recursos</h3>
<p>Um conceito crucial é que você pode referenciar outputs de um recurso em outro. Pulumi rastreia essas dependências automaticamente.</p>
<pre><code class="language-python">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)</code></pre>
<p>Pulumi entende que <code>listener</code> depende de <code>alb</code> e <code>target_group</code>, e provisionará tudo na ordem correta.</p>
<h3>Gerenciamento de Estado</h3>
<p>O estado é mantido em um backend. Você pode usar:</p>
<ul>
<li><strong>Pulumi Service</strong> (padrão, recomendado) — armazena remotamente, com acesso controlado</li>
<li><strong>Local</strong> — arquivo <code>Pulumi.<stack>.yaml</code> (apenas desenvolvimento)</li>
<li><strong>S3, Azure Blob, etc.</strong> — autogerenciado</li>
</ul>
<p>Inicializar com um backend local:</p>
<pre><code class="language-bash">pulumi login --local</code></pre>
<p>Com Pulumi Service (padrão):</p>
<pre><code class="language-bash">pulumi login
Será pedido seu token (crie em app.pulumi.com)</code></pre>
<h2>Boas Práticas e Padrões Avançados</h2>
<h3>Organização de Código</h3>
<p>Para projetos maiores, organize em módulos:</p>
<pre><code>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</code></pre>
<p><code>config.py</code>:</p>
<pre><code class="language-python">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"
}
}</code></pre>
<p><code>__main__.py</code>:</p>
<pre><code class="language-python">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)</code></pre>
<h3>Testing e Validação</h3>
<p>Pulumi suporta testes via frameworks padrão:</p>
<pre><code class="language-python"># 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)</code></pre>
<p>Execute com pytest:</p>
<pre><code class="language-bash">pip install pulumi[testing]
pytest test_infra.py -v</code></pre>
<h3>Policy as Code</h3>
<p>Pulumi permite definir políticas que executam antes de aplicar mudanças:</p>
<pre><code class="language-python"># 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
)</code></pre>
<h2>Conclusão</h2>
<p>Pulumi revoluciona a forma como pensamos sobre Infrastructure as Code ao permitir linguagens reais em vez de DSLs restritivas. Aprendemos três pontos fundamentais: <strong>primeiro</strong>, 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; <strong>segundo</strong>, componentes reutilizáveis criam abstrações poderosas que escalam conforme a complexidade cresce, permitindo que times compartilhem padrões de infraestrutura como bibliotecas; <strong>terceiro</strong>, 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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.pulumi.com/docs/" target="_blank" rel="noopener noreferrer">Documentação Oficial Pulumi</a></li>
<li><a href="https://www.pulumi.com/docs/reference/pkg/python/" target="_blank" rel="noopener noreferrer">Pulumi SDK Python Reference</a></li>
<li><a href="https://www.pulumi.com/registry/packages/aws/" target="_blank" rel="noopener noreferrer">AWS Provider para Pulumi</a></li>
<li><a href="https://github.com/pulumi/examples" target="_blank" rel="noopener noreferrer">Pulumi Examples Repository</a></li>
<li><a href="https://www.oreilly.com/library/view/infrastructure-as-code/9781491924334/" target="_blank" rel="noopener noreferrer">Infrastructure as Code: Managing Servers in the Cloud</a> — Kief Morris</li>
</ul>
<p><!-- FIM --></p>