TL;DR — Resumen Rápido
Pulumi permite definir infraestructura cloud con TypeScript, Python, Go, C# y Java. Guía completa de stacks, estado, pruebas e integración con CI/CD.
Pulumi es una plataforma de infraestructura como código que permite definir, desplegar y gestionar recursos cloud usando lenguajes de programación reales — TypeScript, Python, Go, C#, Java — en lugar de lenguajes de dominio específico como HCL o YAML. Si alguna vez te has topado con los límites de los bucles en Terraform, luchado con la verbosidad de JSON en CloudFormation, o deseado poder probar tu infraestructura con el mismo framework de pruebas que usas para el código de aplicación, Pulumi resuelve exactamente esos problemas. Esta guía cubre desde la instalación y los conceptos fundamentales hasta pruebas, gestión de estado, integración con CI/CD y un ejemplo práctico multi-nube.
Requisitos Previos
- Familiaridad con al menos uno de: TypeScript/JavaScript, Python, Go, C# o Java
- Una cuenta en AWS, Azure o GCP (el nivel gratuito es suficiente para los ejemplos)
- Node.js 18+ instalado (para los ejemplos de TypeScript/JavaScript)
- Comprensión básica de conceptos cloud (VPCs, buckets de almacenamiento, VMs)
- Cuenta en Pulumi Cloud (nivel gratuito) o un backend de estado alternativo
Por Qué Pulumi: Lenguajes Reales vs. HCL y YAML
La diferencia fundamental entre Pulumi y herramientas como Terraform o CloudFormation es que los programas de Pulumi se escriben en lenguajes de programación de uso general. Esto desbloquea capacidades que son complicadas o imposibles en herramientas basadas en DSL.
Soporte completo de IDE — Tu editor proporciona autocompletado para cada propiedad de recurso, documentación en línea y errores de tipo antes de ejecutar cualquier despliegue. Escribir mal el nombre de una propiedad de un bucket S3 es un error en tiempo de compilación, no un fallo en tiempo de ejecución después de una llamada a la API de 30 segundos.
Flujo de control estándar — Bucles, condicionales, funciones y clases funcionan como se espera. Crear 10 subredes es un bucle for, no el meta-argumento count de Terraform ni los ejercicios de for_each.
Componentes reutilizables como paquetes — Puedes publicar componentes de infraestructura en npm, PyPI o Maven. Un equipo que construye un módulo de VPC compatible lo publica como paquete; otros equipos lo añaden como dependencia y lo importan como cualquier librería.
Pruebas con frameworks estándar — Las pruebas unitarias usan Jest, pytest o el paquete testing de Go. Las pruebas de propiedad usan Policy as Code. Las pruebas de integración despliegan infraestructura real y la validan de extremo a extremo.
Mismo lenguaje para app e infraestructura — Un equipo de TypeScript puede mantener el código de infraestructura en el mismo repositorio, con las mismas reglas de linting, el mismo pipeline de CI y funciones de utilidad compartidas.
| Característica | Pulumi | Terraform/OpenTofu | AWS CDK | Crossplane | CloudFormation |
|---|---|---|---|---|---|
| Lenguaje | TS/Python/Go/C#/Java | HCL | TS/Python/Java/Go | YAML/CRDs | JSON/YAML |
| Soporte IDE | Completo (tipos + autocompletado) | Parcial (plugin HCL) | Completo | Mínimo | Mínimo |
| Pruebas | Frameworks estándar | Terratest (Go) | Frameworks estándar | Sin integración nativa | Sin integración nativa |
| Gestión de estado | Cloud/S3/GCS/Azure/local | Cloud/S3/GCS/Azure/local | Stacks de CloudFormation | Kubernetes etcd | CloudFormation |
| Multi-nube | Sí (programa único) | Sí (múltiples proveedores) | Centrado en AWS | Sí | Solo AWS |
| Paquetes reutilizables | npm/PyPI/Maven/NuGet | Terraform Registry | npm/PyPI/Maven/NuGet | Helm/OCI | Stacks anidados |
| Curva de aprendizaje | Baja (usa lenguaje existente) | Media (aprender HCL) | Baja (usa lenguaje existente) | Alta (CRDs de Kubernetes) | Alta (JSON/YAML verboso) |
Arquitectura: Cómo Funciona Pulumi
Comprender la arquitectura de Pulumi ayuda a razonar sobre lo que sucede cuando ejecutas pulumi up.
Language host — Un proceso que ejecuta el runtime del lenguaje elegido (Node.js, Python, JVM, etc.) y ejecuta tu programa. Llama al SDK de Pulumi para registrar recursos.
Motor de Pulumi — El proceso de orquestación central que recibe solicitudes de registro de recursos del language host, calcula el grafo de dependencias, compara el estado deseado con el último estado conocido y genera un plan.
Proveedores de recursos — Plugins que traducen las declaraciones de recursos de Pulumi en llamadas a la API (AWS, Azure, GCP, Kubernetes, etc.). Cada proveedor es un binario separado que el motor descarga y ejecuta.
Backend de estado — Almacena el último estado conocido de tu stack (qué recursos existen, sus propiedades, sus IDs). El motor compara la nueva salida del programa con este estado para determinar qué crear, actualizar o eliminar.
Motor de despliegue — Ejecuta el plan respetando el grafo de dependencias, realizando operaciones de recursos independientes en paralelo para mayor velocidad.
Instalación
Instala la CLI de Pulumi:
# Linux/macOS mediante script de instalación
curl -fsSL https://get.pulumi.com | sh
# macOS mediante Homebrew
brew install pulumi
# Windows mediante Chocolatey
choco install pulumi
# Verificar
pulumi version
Inicia sesión en tu backend de estado:
# Pulumi Cloud (por defecto, nivel gratuito disponible)
pulumi login
# Backend S3
pulumi login s3://tu-bucket-de-estado/pulumi
# Sistema de archivos local (no recomendado para equipos)
pulumi login --local
Estructura del Proyecto
Crea un nuevo proyecto desde una plantilla:
pulumi new aws-typescript # TypeScript + AWS
pulumi new aws-python # Python + AWS
pulumi new azure-go # Go + Azure
pulumi new gcp-csharp # C# + GCP
pulumi new kubernetes-typescript # TypeScript + Kubernetes
Un proyecto de TypeScript tiene esta estructura:
mi-infra/
Pulumi.yaml # Metadatos del proyecto (nombre, runtime, descripción)
Pulumi.dev.yaml # Configuración del stack para el stack "dev"
package.json # Dependencias npm
tsconfig.json # Configuración de TypeScript
index.ts # Punto de entrada — tu código de infraestructura
Conceptos Fundamentales
Recursos
Un recurso es cualquier objeto cloud que Pulumi gestiona. Lo declaras construyendo un objeto del SDK:
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("mi-bucket", {
acl: "private",
tags: { Entorno: "produccion" },
versioning: { enabled: true },
});
export const nombreBucket = bucket.bucket;
export const arnBucket = bucket.arn;
Entradas y Salidas
Las propiedades de los recursos son Output<T> — valores asíncronos que se resuelven después de crear el recurso. No puedes usar una Output directamente como string plano; debes usar pulumi.interpolate o Output.apply:
// pulumi.interpolate — para plantillas de strings
const urlBucket = pulumi.interpolate`https://${bucket.bucket}.s3.amazonaws.com`;
// Output.apply — para transformaciones
const nombreMayusculas = bucket.bucket.apply(name => name.toUpperCase());
// Output.all — cuando necesitas múltiples salidas juntas
const combinado = pulumi.all([bucket.bucket, bucket.arn]).apply(([nombre, arn]) => ({
nombre,
arn,
url: `https://${nombre}.s3.amazonaws.com`,
}));
Referencias de Stack
Las referencias de stack te permiten consumir salidas de un stack en otro stack — el patrón estándar para separar infraestructura de red, datos y aplicación:
const stackRed = new pulumi.StackReference("org/network/prod");
const vpcId = stackRed.getOutput("vpcId");
const subnetPrivadasIds = stackRed.getOutput("privateSubnetIds");
const cluster = new aws.ecs.Cluster("cluster-app", {});
const servicio = new aws.ecs.Service("servicio-app", {
cluster: cluster.arn,
networkConfiguration: {
subnets: subnetPrivadasIds,
},
});
ComponentResource
ComponentResource es el mecanismo de abstracción para construir módulos de infraestructura reutilizables:
class BucketS3Seguro extends pulumi.ComponentResource {
public readonly bucket: aws.s3.Bucket;
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("miorg:storage:BucketS3Seguro", name, {}, opts);
this.bucket = new aws.s3.Bucket(`${name}-bucket`, {
acl: "private",
serverSideEncryptionConfiguration: {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: "AES256",
},
},
},
versioning: { enabled: true },
}, { parent: this });
new aws.s3.BucketPublicAccessBlock(`${name}-block`, {
bucket: this.bucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
}, { parent: this });
this.registerOutputs({ bucketName: this.bucket.bucket });
}
}
Infraestructura en Python
Python es igualmente un ciudadano de primera clase. Los mismos conceptos aplican con sintaxis de Python:
import pulumi
import pulumi_aws as aws
vpc = aws.ec2.Vpc("vpc-principal",
cidr_block="10.0.0.0/16",
enable_dns_hostnames=True,
tags={"Entorno": pulumi.get_stack()},
)
zonas_disponibilidad = ["us-east-1a", "us-east-1b", "us-east-1c"]
subnets = [
aws.ec2.Subnet(f"subnet-{i}",
vpc_id=vpc.id,
cidr_block=f"10.0.{i}.0/24",
availability_zone=az,
)
for i, az in enumerate(zonas_disponibilidad)
]
ids_subnets = pulumi.Output.all(*[s.id for s in subnets]).apply(lambda ids: ids)
pulumi.export("vpc_id", vpc.id)
pulumi.export("subnet_ids", ids_subnets)
Backends de Estado
El backend de estado almacena el último estado conocido de los recursos de tu stack.
| Backend | Comando | Ideal para |
|---|---|---|
| Pulumi Cloud | pulumi login | Equipos, cifrado de secretos, log de auditoría integrado |
| AWS S3 | pulumi login s3://bucket/ruta | Equipos nativos de AWS |
| Azure Blob | pulumi login azblob://contenedor/ruta | Equipos nativos de Azure |
| Google Cloud Storage | pulumi login gs://bucket/ruta | Equipos nativos de GCP |
| Archivo local | pulumi login --local | Solo pruebas individuales |
| Pulumi auto-alojado | pulumi login https://pulumi.ejemplo.com | Air-gapped o requisitos de cumplimiento |
Stacks y Entornos
Un stack es un despliegue aislado del mismo programa:
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod
pulumi stack select prod
pulumi config set aws:region eu-west-1
pulumi config set tipoInstancia t3.medium
# Establecer un secreto (cifrado en el estado)
pulumi config set --secret passwordBaseDatos "valor-super-secreto"
Pruebas
Pruebas Unitarias con Mocks
El sistema de mocks de Pulumi intercepta el registro de recursos para que puedas probar la lógica de infraestructura sin desplegar nada:
pulumi.runtime.setMocks({
newResource: (args: pulumi.runtime.MockResourceArgs) => {
return { id: `${args.name}-id`, state: args.inputs };
},
call: (args: pulumi.runtime.MockCallArgs) => args.inputs,
});
Policy as Code con CrossGuard
Pulumi CrossGuard aplica reglas de cumplimiento en todos los stacks:
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";
new PolicyPack("cumplimiento-aws", {
policies: [
{
name: "s3-sin-lectura-publica",
description: "Los buckets S3 no deben permitir lectura pública",
enforcementLevel: "mandatory",
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (bucket.acl === "public-read" || bucket.acl === "public-read-write") {
reportViolation("El bucket S3 no debe ser legible públicamente.");
}
}),
},
],
});
Integración con CI/CD
GitHub Actions
name: Despliegue con Pulumi
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
work-dir: ./infra
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Automation API
La Automation API embebe el motor de Pulumi como librería, permitiendo portales de infraestructura de autoservicio y creación dinámica de entornos:
import { LocalWorkspace } from "@pulumi/pulumi/automation";
async function aprovisionarEntorno(tenantId: string) {
const stack = await LocalWorkspace.createOrSelectStack({
stackName: `tenant-${tenantId}`,
projectName: "saas-infra",
program: async () => {
const bucket = new aws.s3.Bucket(`${tenantId}-datos`);
return { nombreBucket: bucket.bucket };
},
});
await stack.setConfig("aws:region", { value: "us-east-1" });
const resultado = await stack.up({ onOutput: console.log });
return resultado.outputs;
}
Ejemplo Multi-Nube Práctico
Desplegar una aplicación web en AWS (cómputo) y Cloudflare (DNS/CDN) en un único programa de Pulumi:
import * as aws from "@pulumi/aws";
import * as cloudflare from "@pulumi/cloudflare";
const config = new pulumi.Config();
const dominio = config.require("domainName");
const cfZoneId = config.requireSecret("cloudflareZoneId");
const lb = new aws.lb.LoadBalancer("lb-web", {
internal: false,
loadBalancerType: "application",
subnets: subnetsPublicasIds,
});
const registroDns = new cloudflare.Record("dns-web", {
zoneId: cfZoneId,
name: dominio,
type: "CNAME",
value: lb.dnsName,
proxied: true,
});
export const urlApp = pulumi.interpolate`https://${dominio}`;
Errores Comunes y Casos Especiales
Outputs en contexto de string — Nunca uses const url = "https://" + bucket.bucket. Usa pulumi.interpolate en su lugar. El operador + convierte una Output en [object Object].
Eliminar salidas del stack — Eliminar un export no borra el recurso; solo elimina el valor de salida. Para borrar el recurso, elimínalo del código del programa.
pulumi refresh vs pulumi up — Usa pulumi refresh para sincronizar el estado con el estado real de la nube (tras cambios manuales). Usa pulumi up para aplicar cambios del programa.
Nomenclatura de recursos — Pulumi añade un sufijo aleatorio a los nombres de recursos por defecto (ej. mi-bucket-a3f8c2b). Usa la propiedad name explícitamente si necesitas un nombre fijo.
Versión de proveedores fijada — Fija las versiones de proveedores en package.json o requirements.txt. Los proveedores sin versión fijada pueden cambiar de comportamiento en pulumi up tras una actualización automática.
Resumen
- Pulumi usa lenguajes de programación reales (TypeScript, Python, Go, C#, Java) en lugar de HCL o YAML, dando soporte completo de IDE y pruebas estándar
- El motor calcula un grafo de dependencias y aplica cambios en paralelo, comparando contra el estado almacenado en Pulumi Cloud, S3, Azure Blob o GCS
- Los stacks aíslan los entornos dev/staging/prod; cada stack tiene su propia configuración y estado
ComponentResourcepermite abstracciones de infraestructura reutilizables publicables como paquetes npm o PyPI- Las pruebas unitarias usan mocks; las pruebas de integración usan la Automation API; el cumplimiento usa políticas CrossGuard
- La Automation API embebe Pulumi en código de aplicación para portales de autoservicio y entornos efímeros
- La integración con CI/CD es un único paso
pulumi/actionsen GitHub Actions o equivalente en GitLab CI