DevOps Admin

Pulumi: Infraestrutura como Código com Linguagens Reais: Do Básico ao Avançado Já leu

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

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.

Referências


Artigos relacionados