Introducción a Pulumi: Infraestructura como Código

Pulumi es una plataforma moderna de infraestructura como código que te permite definir recursos en la nube usando lenguajes de programación de propósito general como Python, Go, TypeScript y C#. A diferencia de los lenguajes declarativos de configuración, Pulumi aprovecha las capacidades de lenguaje de programación completo para la definición de infraestructura, permitiendo código más expresivo, reutilizable y mantenible. Esta guía cubre instalación, soporte de lenguaje, gestión de stack, configuración, manejo de secretos, gestión de estado y comparación con Terraform.

Tabla de Contenidos

  1. Visión General y Filosofía de Pulumi
  2. Instalación y Configuración
  3. Infraestructura como Código en Python
  4. Infraestructura como Código en TypeScript
  5. Gestión de Stack y Proyecto
  6. Configuración y Secretos
  7. Gestión de Estado
  8. Comparación con Terraform
  9. Mejores Prácticas de Pulumi
  10. Conclusión

Visión General y Filosofía de Pulumi

Pulumi cambia el paradigma de infraestructura como código usando lenguajes de programación reales en lugar de lenguajes específicos del dominio. Este enfoque proporciona varias ventajas: acceso a características de lenguaje completas, reutilización más fácil de código mediante funciones y clases, seguridad de tipo, soporte de IDE y patrones de abstracción poderosos.

Beneficios clave:

  • Lenguajes Reales: Usar Python, Go, TypeScript, C# con todas las características del lenguaje
  • Seguridad de Tipo: Verificación de tipo estático atrapa errores temprano
  • Reutilización: Funciones, clases y librerías para patrones de infraestructura
  • Soporte de IDE: Características IDE completas, autocompletado, refactorización
  • Pruebas: Pruebas unitarias e integración con marcos de prueba estándar
  • Patrones Familiares: Aprovechar conocimiento de programación existente

Arquitectura:

┌─────────────────────┐
│   Programa Pulumi   │
│  (Python/Go/TS/C#)  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Runtime de Pulumi  │
│  (Language Runtime) │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│   Motor de Pulumi   │
│  (State Mgmt, etc)  │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│  Proveedores Cloud  │
│  (AWS, Azure, GCP)  │
└─────────────────────┘

Instalación y Configuración

Instalar Pulumi y configurar credenciales de proveedor de nube.

Instalar CLI de Pulumi:

# macOS
brew install pulumi

# Linux
curl -fsSL https://get.pulumi.com | sh

# Windows
choco install pulumi

# Verificar instalación
pulumi version
# Salida: v3.80.0

Configurar credenciales de proveedor de nube:

# AWS
export AWS_REGION=us-east-1
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...

# O usar configuración de AWS CLI
aws configure

# Azure
az login

# GCP
gcloud auth login
gcloud config set project my-project

Crear cuenta de Pulumi:

# Registrarse en https://app.pulumi.com

# Iniciar sesión en Pulumi
pulumi login

# O usar backend auto-gestionado
pulumi login s3://my-pulumi-state-bucket
pulumi login file://~/.pulumi-state

Infraestructura como Código en Python

Definir infraestructura usando Python para capacidades de lenguaje completo.

Crear proyecto de Python:

# Crear nuevo proyecto
mkdir my-infra && cd my-infra
pulumi new aws-python

# Esto crea:
# __main__.py      - Código principal de infraestructura
# Pulumi.yaml      - Configuración de proyecto
# Pulumi.dev.yaml  - Configuración de stack
# requirements.txt - Dependencias de Python
# venv/            - Entorno virtual

Infraestructura básica de Python:

# __main__.py
import pulumi
import pulumi_aws as aws

# Obtener referencia de stack para configuración
config = pulumi.Config()
environment = config.require("environment")

# Crear VPC
vpc = aws.ec2.Vpc("my-vpc",
    cidr_block="10.0.0.0/16",
    enable_dns_hostnames=True,
    tags={
        "Name": f"vpc-{environment}",
        "Environment": environment,
    }
)

# Crear subnet
subnet = aws.ec2.Subnet("public-subnet",
    vpc_id=vpc.id,
    cidr_block="10.0.1.0/24",
    availability_zone=f"{config.require('region')}a",
    map_public_ip_on_launch=True
)

# Crear grupo de seguridad
security_group = aws.ec2.SecurityGroup("web-sg",
    vpc_id=vpc.id,
    description="Security group for web servers",
    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"],
        ),
    ]
)

# Crear instancia EC2
instance = aws.ec2.Instance("web-server",
    ami="ami-0c55b159cbfafe1f0",  # Ubuntu 20.04
    instance_type="t3.micro",
    subnet_id=subnet.id,
    vpc_security_group_ids=[security_group.id],
    tags={
        "Name": f"web-{environment}",
    }
)

# Exportar salidas
pulumi.export("vpc_id", vpc.id)
pulumi.export("instance_id", instance.id)
pulumi.export("instance_public_ip", instance.public_ip)

Python con funciones para reutilización:

# __main__.py
import pulumi
import pulumi_aws as aws

def create_vpc(name: str, cidr: str, environment: str) -> aws.ec2.Vpc:
    """Crear VPC con configuración común"""
    return aws.ec2.Vpc(name,
        cidr_block=cidr,
        enable_dns_hostnames=True,
        enable_dns_support=True,
        tags={
            "Name": name,
            "Environment": environment,
        }
    )

def create_subnet(name: str, vpc_id: pulumi.Input[str], 
                 cidr: str, az: str) -> aws.ec2.Subnet:
    """Crear subnet con configuración"""
    return aws.ec2.Subnet(name,
        vpc_id=vpc_id,
        cidr_block=cidr,
        availability_zone=az,
        map_public_ip_on_launch=True,
        tags={"Name": name}
    )

def create_security_group(name: str, vpc_id: pulumi.Input[str],
                         ingress_rules: list) -> aws.ec2.SecurityGroup:
    """Crear grupo de seguridad con reglas"""
    return aws.ec2.SecurityGroup(name,
        vpc_id=vpc_id,
        description=f"Security group for {name}",
        ingress=ingress_rules,
        tags={"Name": name}
    )

# Usar funciones
config = pulumi.Config()
environment = config.require("environment")

vpc = create_vpc("my-vpc", "10.0.0.0/16", environment)
subnet = create_subnet("public-subnet", vpc.id, "10.0.1.0/24", "us-east-1a")
sg = create_security_group("web-sg", vpc.id, [
    aws.ec2.SecurityGroupIngressArgs(
        protocol="tcp", from_port=80, to_port=80, cidr_blocks=["0.0.0.0/0"]
    )
])

Clases de Python para componentes de infraestructura:

# vpc.py
import pulumi
import pulumi_aws as aws
from typing import Optional

class VpcConfig:
    def __init__(self, 
                 name: str,
                 cidr_block: str,
                 public_subnets: dict,
                 private_subnets: dict):
        self.name = name
        self.cidr_block = cidr_block
        self.public_subnets = public_subnets
        self.private_subnets = private_subnets

class VpcComponent(pulumi.ComponentResource):
    """Componente VPC encapsulando VPC, subnets y gateways"""
    
    def __init__(self, name: str, config: VpcConfig, opts=None):
        super().__init__("custom:network:Vpc", name, None, opts)
        
        # Crear VPC
        self.vpc = aws.ec2.Vpc(f"{name}-vpc",
            cidr_block=config.cidr_block,
            tags={"Name": name},
            opts=pulumi.ResourceOptions(parent=self)
        )
        
        # Crear subnets públicas
        self.public_subnets = {}
        for subnet_name, subnet_config in config.public_subnets.items():
            self.public_subnets[subnet_name] = aws.ec2.Subnet(
                f"{name}-{subnet_name}",
                vpc_id=self.vpc.id,
                cidr_block=subnet_config["cidr"],
                availability_zone=subnet_config["az"],
                map_public_ip_on_launch=True,
                tags={"Name": f"{name}-{subnet_name}", "Type": "public"},
                opts=pulumi.ResourceOptions(parent=self)
            )
        
        # Crear internet gateway
        self.igw = aws.ec2.InternetGateway(f"{name}-igw",
            vpc_id=self.vpc.id,
            tags={"Name": f"{name}-igw"},
            opts=pulumi.ResourceOptions(parent=self)
        )
        
        # Exportar salidas
        self.register_outputs({
            "vpc_id": self.vpc.id,
            "public_subnet_ids": {k: v.id for k, v in self.public_subnets.items()},
            "igw_id": self.igw.id,
        })

# Uso en main
# __main__.py
import pulumi
from vpc import VpcComponent, VpcConfig

config = pulumi.Config()

vpc_config = VpcConfig(
    name="prod-vpc",
    cidr_block="10.0.0.0/16",
    public_subnets={
        "us-east-1a": {"cidr": "10.0.1.0/24", "az": "us-east-1a"},
        "us-east-1b": {"cidr": "10.0.2.0/24", "az": "us-east-1b"},
    },
    private_subnets={}
)

vpc = VpcComponent("prod", vpc_config)
pulumi.export("vpc_id", vpc.vpc.id)

Infraestructura como Código en TypeScript

Definir infraestructura usando TypeScript con seguridad de tipo.

Crear proyecto de TypeScript:

pulumi new aws-typescript
cd my-project

# Instalar dependencias
npm install

Infraestructura básica de TypeScript:

// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const environment = config.require("environment");

// Crear VPC
const vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
    tags: {
        Name: `vpc-${environment}`,
        Environment: environment,
    },
});

// Crear subnet
const subnet = new aws.ec2.Subnet("public-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    availabilityZone: `${config.require("region")}a`,
    mapPublicIpOnLaunch: true,
});

// Crear grupo de seguridad
const securityGroup = new aws.ec2.SecurityGroup("web-sg", {
    vpcId: vpc.id,
    description: "Security group for web servers",
    ingress: [
        {
            protocol: "tcp",
            fromPort: 80,
            toPort: 80,
            cidrBlocks: ["0.0.0.0/0"],
        },
        {
            protocol: "tcp",
            fromPort: 443,
            toPort: 443,
            cidrBlocks: ["0.0.0.0/0"],
        },
    ],
});

// Crear instancia EC2
const instance = new aws.ec2.Instance("web-server", {
    ami: "ami-0c55b159cbfafe1f0",
    instanceType: "t3.micro",
    subnetId: subnet.id,
    vpcSecurityGroupIds: [securityGroup.id],
    tags: {
        Name: `web-${environment}`,
    },
});

// Exportar salidas
export const vpcId = vpc.id;
export const instanceId = instance.id;
export const instancePublicIp = instance.publicIp;

Gestión de Stack y Proyecto

Gestionar múltiples stacks de infraestructura y configuraciones.

Estructura de proyecto:

my-infrastructure/
├── Pulumi.yaml           # Metadatos de proyecto
├── Pulumi.dev.yaml       # Configuración de stack dev
├── Pulumi.staging.yaml   # Configuración de stack staging
├── Pulumi.prod.yaml      # Configuración de stack producción
├── __main__.py           # Código de infraestructura
├── vpc.py                # Componente VPC
├── requirements.txt
└── .pulumi/              # Estado de Pulumi

Pulumi.yaml:

name: my-infrastructure
runtime: python
description: Infrastructure for web application

config:
  aws:region:
    description: AWS region
    default: us-east-1

Configuración específica de stack:

# Pulumi.dev.yaml
config:
  environment: dev
  instance_type: t2.micro
  instance_count: 1

# Pulumi.staging.yaml
config:
  environment: staging
  instance_type: t2.small
  instance_count: 2

# Pulumi.prod.yaml
config:
  environment: production
  instance_type: m5.large
  instance_count: 5

Crear y gestionar stacks:

# Crear nuevo stack
pulumi stack init staging

# Listar stacks
pulumi stack ls

# Seleccionar stack
pulumi stack select staging

# Mostrar información de stack
pulumi stack output
pulumi stack output instance_id

# Eliminar stack (peligroso)
pulumi stack rm --yes

Desplegar a diferentes stacks:

# Desplegar a dev
pulumi stack select dev
pulumi up

# Desplegar a staging
pulumi stack select staging
pulumi up

# Desplegar a producción
pulumi stack select prod
pulumi up

Configuración y Secretos

Gestionar valores de configuración y datos sensibles de forma segura.

Valores de configuración:

# __main__.py
import pulumi

config = pulumi.Config()

# Valores requeridos
environment = config.require("environment")
region = config.require("aws:region")

# Opcionales con valores por defecto
instance_type = config.get("instance_type") or "t2.micro"
instance_count = config.get_int("instance_count") or 1
enable_monitoring = config.get_bool("enable_monitoring") or True

# Configuración anidada
db_config = config.require_object("database")
db_name = db_config["name"]
db_engine = db_config["engine"]

Gestión de secretos:

# Crear secreto
pulumi config set --secret db_password "my-secure-password"

# Ver secreto (enmascarado)
pulumi config
# db_password: [secret]

# Usar secreto en código
db_password = config.require_secret("db_password")

# Marcar salida como sensible
pulumi.export("db_password", db_password)  # Salida marcada como sensible automáticamente

Secretos en código:

# __main__.py
import pulumi
import pulumi_aws as aws

config = pulumi.Config()

# Crear instancia RDS con secreto
db_password = config.require_secret("db_password")

db = aws.rds.Instance("mydb",
    allocated_storage=20,
    engine="mysql",
    instance_class="db.t3.micro",
    db_name="myapp",
    username="admin",
    password=db_password,
    skip_final_snapshot=False,
)

# Exportar contraseña como sensible
pulumi.export("db_password", db_password)

Secretos específicos de entorno:

# Almacenar secretos por stack
pulumi config set --secret -s prod db_password "prod-password"
pulumi config set --secret -s staging db_password "staging-password"

# Los secretos se cifran y almacenan de forma segura

Gestión de Estado

Pulumi gestiona el estado de forma similar a Terraform pero con diferentes opciones de almacenamiento.

Almacenamiento de estado:

# Estado alojado en Pulumi.com (predeterminado)
pulumi login

# Estado de archivo local
pulumi login file://~/.pulumi-state

# Backend S3
pulumi login s3://my-pulumi-state-bucket

# Azure Blob Storage
pulumi login azurblob://container@storageaccount

Inspeccionar estado:

# Mostrar estado de stack
pulumi stack export

# Mostrar en JSON
pulumi stack export | jq

# Mostrar recurso específico
pulumi export | jq '.resources[] | select(.name=="my-instance")'

Operaciones de estado:

# Importar estado desde otro stack
pulumi stack export > backup.json

# Restaurar estado
pulumi stack import < backup.json

# Refrescar estado
pulumi refresh

Comparación con Terraform

Entender las diferencias ayuda a elegir la herramienta correcta.

Fortalezas de Terraform:

  • Enfoque declarativo (lo que deseas)
  • Curva de aprendizaje más pequeña para operadores
  • Ecosistema de proveedor más grande
  • Mejor uniformidad multi-nube
  • Más ampliamente adoptado

Fortalezas de Pulumi:

  • Lenguajes de programación reales
  • Características de lenguaje completo (bucles, condicionales, funciones)
  • Mejor reutilización de código y abstracción
  • Soporte de IDE y seguridad de tipo
  • Más fácil de probar con pruebas unitarias
  • Un solo lenguaje para código de infra y aplicación

Comparación lado a lado:

# Terraform (HCL)
variable "instance_count" {
  type = number
}

resource "aws_instance" "web" {
  count = var.instance_count
  ami = "ami-123"
}

# Pulumi (Python)
config = pulumi.Config()
instance_count = config.get_int("instance_count") or 3

instances = []
for i in range(instance_count):
    instances.append(aws.ec2.Instance(f"web-{i}",
        ami="ami-123"
    ))

Mejores Prácticas de Pulumi

Organizar código lógicamente:

# components/vpc.py - Componentes reutilizables
class VpcComponent(pulumi.ComponentResource):
    pass

# components/database.py
class DatabaseComponent(pulumi.ComponentResource):
    pass

# __main__.py - Orquestar componentes
vpc = VpcComponent("prod-vpc", args)
db = DatabaseComponent("prod-db", args)

Usar secretos para datos sensibles:

# Nunca codificar
password = "my-password"  # INCORRECTO

# Usar secretos de configuración
password = config.require_secret("db_password")  # CORRECTO

Implementar etiquetado apropiado:

def create_tags(environment: str, component: str) -> dict:
    return {
        "Environment": environment,
        "Component": component,
        "ManagedBy": "Pulumi",
        "CreatedAt": pulumi.get_stack(),
    }

instance = aws.ec2.Instance("web",
    tags=create_tags("prod", "web-server")
)

Conclusión

Pulumi moderniza la infraestructura como código aprovechando lenguajes de programación reales para la definición de infraestructura. La combinación de seguridad de tipo, capacidades de lenguaje completo, componentes reutilizables y soporte de prueba fuerte hace que Pulumi sea ideal para proyectos de infraestructura complejos donde los equipos tienen antecedentes sólidos de programación. Ya sea usar Python para prototipado rápido o TypeScript para despliegues de empresa grandes, Pulumi proporciona una plataforma poderosa para gestionar infraestructura en la nube como código.