DevOps & CI/CD

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

18 min de leitura

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

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

<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(&quot;ambiente&quot;)

tamanho_instancia = config.get(&quot;tamanho_instancia&quot;) or &quot;t2.micro&quot;

pulumi.export(&quot;ambiente&quot;, ambiente)

pulumi.export(&quot;tamanho&quot;, tamanho_instancia)</code></pre>

<p>Aqui, <code>pulumi.Config()</code> lê do arquivo <code>Pulumi.&lt;stack&gt;.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(&quot;ambiente&quot;)

cidr_block = config.get(&quot;cidr_block&quot;) or &quot;10.0.0.0/16&quot;

instancia_count = int(config.get(&quot;instancia_count&quot;) or &quot;2&quot;)

tags_padrao = {

&quot;Ambiente&quot;: ambiente,

&quot;ManagedBy&quot;: &quot;Pulumi&quot;

}

Criar VPC

vpc = aws.ec2.Vpc(&quot;vpc-app&quot;,

cidr_block=cidr_block,

tags={**tags_padrao, &quot;Name&quot;: f&quot;vpc-{ambiente}&quot;}

)

Criar subnet

subnet = aws.ec2.Subnet(&quot;subnet-app&quot;,

vpc_id=vpc.id,

cidr_block=&quot;10.0.1.0/24&quot;,

availability_zone=&quot;us-east-1a&quot;,

tags={**tags_padrao, &quot;Name&quot;: f&quot;subnet-{ambiente}&quot;}

)

Criar Security Group com regras

sg = aws.ec2.SecurityGroup(&quot;sg-app&quot;,

vpc_id=vpc.id,

ingress=[

aws.ec2.SecurityGroupIngressArgs(

protocol=&quot;tcp&quot;,

from_port=80,

to_port=80,

cidr_blocks=[&quot;0.0.0.0/0&quot;]

),

aws.ec2.SecurityGroupIngressArgs(

protocol=&quot;tcp&quot;,

from_port=443,

to_port=443,

cidr_blocks=[&quot;0.0.0.0/0&quot;]

),

aws.ec2.SecurityGroupIngressArgs(

protocol=&quot;tcp&quot;,

from_port=22,

to_port=22,

cidr_blocks=[&quot;203.0.113.0/32&quot;] # Seu IP

)

],

egress=[

aws.ec2.SecurityGroupEgressArgs(

protocol=&quot;-1&quot;,

from_port=0,

to_port=0,

cidr_blocks=[&quot;0.0.0.0/0&quot;]

)

],

tags={**tags_padrao, &quot;Name&quot;: f&quot;sg-{ambiente}&quot;}

)

User data para inicializar a instância

user_data_script = &quot;&quot;&quot;#!/bin/bash

apt-get update

apt-get install -y nginx

systemctl start nginx

systemctl enable nginx

&quot;&quot;&quot;

Buscar a AMI mais recente do Ubuntu

ubuntu_ami = aws.ec2.get_ami(

most_recent=True,

owners=[&quot;099720109477&quot;], # Canonical

filters=[

aws.ec2.GetAmiFilterArgs(name=&quot;name&quot;, values=[&quot;ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*&quot;]),

aws.ec2.GetAmiFilterArgs(name=&quot;virtualization-type&quot;, values=[&quot;hvm&quot;])

]

)

Criar múltiplas instâncias com loop

instancias = []

for i in range(instancia_count):

instancia = aws.ec2.Instance(f&quot;web-server-{i}&quot;,

ami=ubuntu_ami.id,

instance_type=&quot;t2.micro&quot;,

subnet_id=subnet.id,

vpc_security_group_ids=[sg.id],

user_data=user_data_script,

associate_public_ip_address=True,

tags={**tags_padrao, &quot;Name&quot;: f&quot;web-server-{i}-{ambiente}&quot;}

)

instancias.append(instancia)

Exportar valores importantes

pulumi.export(&quot;vpc_id&quot;, vpc.id)

pulumi.export(&quot;subnet_id&quot;, subnet.id)

pulumi.export(&quot;security_group_id&quot;, sg.id)

pulumi.export(&quot;instancia_ids&quot;, [inst.id for inst in instancias])

pulumi.export(&quot;instancia_ips_publicos&quot;, [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: &quot;2&quot;</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: &quot;4&quot;</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: &quot;toda aplicação web precisa de VPC, Security Group, Load Balancer...&quot;. 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__(&quot;pkg:index:AplicacaoWeb&quot;, nome, None, opts)

ambiente = config[&quot;ambiente&quot;]

tags = {

&quot;Componente&quot;: &quot;AplicacaoWeb&quot;,

&quot;Ambiente&quot;: ambiente

}

VPC

vpc = aws.ec2.Vpc(f&quot;{nome}-vpc&quot;,

cidr_block=config.get(&quot;cidr_block&quot;, &quot;10.0.0.0/16&quot;),

tags={**tags, &quot;Name&quot;: f&quot;{nome}-vpc&quot;}

)

Subnet

subnet = aws.ec2.Subnet(f&quot;{nome}-subnet&quot;,

vpc_id=vpc.id,

cidr_block=&quot;10.0.1.0/24&quot;,

availability_zone=f&quot;{config[&#039;regiao&#039;]}a&quot;,

tags={**tags, &quot;Name&quot;: f&quot;{nome}-subnet&quot;}

)

Internet Gateway

igw = aws.ec2.InternetGateway(f&quot;{nome}-igw&quot;,

vpc_id=vpc.id,

tags={**tags, &quot;Name&quot;: f&quot;{nome}-igw&quot;}

)

Route Table

route_table = aws.ec2.RouteTable(f&quot;{nome}-rt&quot;,

vpc_id=vpc.id,

routes=[

aws.ec2.RouteTableRouteArgs(

cidr_block=&quot;0.0.0.0/0&quot;,

gateway_id=igw.id

)

],

tags={**tags, &quot;Name&quot;: f&quot;{nome}-rt&quot;}

)

Associar subnet com route table

aws.ec2.RouteTableAssociation(f&quot;{nome}-rta&quot;,

subnet_id=subnet.id,

route_table_id=route_table.id

)

Security Group

sg = aws.ec2.SecurityGroup(f&quot;{nome}-sg&quot;,

vpc_id=vpc.id,

ingress=[

aws.ec2.SecurityGroupIngressArgs(

protocol=&quot;tcp&quot;,

from_port=80,

to_port=80,

cidr_blocks=[&quot;0.0.0.0/0&quot;]

),

aws.ec2.SecurityGroupIngressArgs(

protocol=&quot;tcp&quot;,

from_port=443,

to_port=443,

cidr_blocks=[&quot;0.0.0.0/0&quot;]

)

],

egress=[

aws.ec2.SecurityGroupEgressArgs(

protocol=&quot;-1&quot;,

from_port=0,

to_port=0,

cidr_blocks=[&quot;0.0.0.0/0&quot;]

)

],

tags={**tags, &quot;Name&quot;: f&quot;{nome}-sg&quot;}

)

Exportar recursos criados

self.vpc_id = vpc.id

self.subnet_id = subnet.id

self.security_group_id = sg.id

self.register_outputs({

&quot;vpc_id&quot;: vpc.id,

&quot;subnet_id&quot;: subnet.id,

&quot;security_group_id&quot;: 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 = {

&quot;ambiente&quot;: config.require(&quot;ambiente&quot;),

&quot;regiao&quot;: &quot;us-east-1&quot;,

&quot;cidr_block&quot;: &quot;10.0.0.0/16&quot;

}

aplicacao = AplicacaoWeb(&quot;minha-app&quot;, app_config)

pulumi.export(&quot;vpc_id&quot;, aplicacao.vpc_id)

pulumi.export(&quot;subnet_id&quot;, aplicacao.subnet_id)

pulumi.export(&quot;sg_id&quot;, 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(&quot;db_password&quot;)

Usar o secret (será criptografado no estado)

db = aws.rds.Instance(&quot;meu-banco&quot;,

allocated_storage=20,

engine=&quot;postgres&quot;,

engine_version=&quot;13.7&quot;,

instance_class=&quot;db.t3.micro&quot;,

db_name=&quot;proddb&quot;,

username=&quot;adminuser&quot;,

password=db_password, # Automaticamente criptografado

skip_final_snapshot=True

)

pulumi.export(&quot;db_endpoint&quot;, db.endpoint)

pulumi.export(&quot;db_password&quot;, 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 &quot;sua_senha_super_secreta&quot;</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(&quot;app-lb&quot;,

internal=False,

load_balancer_type=&quot;application&quot;,

security_groups=[sg.id],

subnets=[subnet1.id, subnet2.id]

)

target_group = aws.lb.TargetGroup(&quot;app-tg&quot;,

port=80,

protocol=&quot;HTTP&quot;,

vpc_id=vpc.id

)

Listener usa o ARN do target group

listener = aws.lb.Listener(&quot;app-listener&quot;,

load_balancer_arn=alb.arn,

port=80,

protocol=&quot;HTTP&quot;,

default_actions=[

aws.lb.ListenerDefaultActionArgs(

type=&quot;forward&quot;,

target_group_arn=target_group.arn

)

]

)

Exportar o DNS do load balancer

pulumi.export(&quot;dns_name&quot;, 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.&lt;stack&gt;.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 {

&quot;ambiente&quot;: config.require(&quot;ambiente&quot;),

&quot;regiao&quot;: config.get(&quot;regiao&quot;) or &quot;us-east-1&quot;,

&quot;tags_padrao&quot;: {

&quot;Projeto&quot;: &quot;MeuProjeto&quot;,

&quot;Ambiente&quot;: config.require(&quot;ambiente&quot;),

&quot;ManagedBy&quot;: &quot;Pulumi&quot;

}

}</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(&quot;app&quot;, cfg)

db = BancoDados(&quot;db&quot;, cfg, vpc_id=app.vpc_id)

pulumi.export(&quot;app_dns&quot;, app.dns)

pulumi.export(&quot;db_endpoint&quot;, 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 &quot;vpc_id&quot; in outputs

assert outputs[&quot;vpc_id&quot;] is not None

pt.run_test(

program=lambda: __import__(&quot;__main__&quot;),

expected_resource_count=15, # Aproximadamente

check=check_vpc

)

def test_tags_aplicadas():

def check_tags(outputs):

assert &quot;tags&quot; in outputs

tags = outputs[&quot;tags&quot;]

assert &quot;Ambiente&quot; in tags

pt.run_test(program=lambda: __import__(&quot;__main__&quot;), 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):

&quot;&quot;&quot;Bloqueia EC2 com IP público em produção&quot;&quot;&quot;

if resource_type == &quot;aws:ec2/instance:Instance&quot;:

ambiente = pulumi.Config().get(&quot;ambiente&quot;)

if ambiente == &quot;production&quot;:

if resource_config.get(&quot;associate_public_ip_address&quot;):

return [

f&quot;Recurso {resource_name} não pode ter IP público em produção&quot;

]

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em DevOps & CI/CD

Feature Flags: como realizar deploys mais seguros e confiáveis
Feature Flags: como realizar deploys mais seguros e confiáveis

A gestão de mudanças no código-fonte de um sistema é uma tarefa constante e d...

Incident Management: Runbooks, Post-mortems e Cultura de Confiabilidade na Prática
Incident Management: Runbooks, Post-mortems e Cultura de Confiabilidade na Prática

Incident Management: Fundamentos e Importância Incident Management é a discip...

Redes e Volumes Avançados no Docker: Bridge, Overlay e Bind Mounts: Do Básico ao Avançado
Redes e Volumes Avançados no Docker: Bridge, Overlay e Bind Mounts: Do Básico ao Avançado

Introdução: A Importância da Comunicação e Persistência de Dados em Container...