Pulumi es una herramienta de infraestructura como código que describe recursos con lenguajes de programación generales (Python, TypeScript, Go, C# o Java) en lugar de un lenguaje propio como HCL. Detrás hay el mismo modelo que en Terraform: un estado que registra qué existe, un plan que muestra los cambios y proveedores que hablan con cada API. En este tutorial instalarás Pulumi en Ubuntu 24.04, crearás un proyecto en Python que despliega un contenedor Nginx con el proveedor de Docker, usarás configuración y secretos por stack, y verás en qué se diferencia de Terraform.

Se usa Docker como destino para que puedas practicar sin cuenta en ninguna nube; el flujo de trabajo es idéntico con AWS, Azure, Kubernetes o cualquier otro proveedor.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios sudo.
  • Python 3.12 (incluido en Ubuntu 24.04).
  • Al menos 1 GB de RAM libre para Docker y los plugins de Pulumi.
  • El puerto 8080 libre, que se usará para publicar el contenedor de prueba.

Paso 1: Instalar Docker y las dependencias de Python

Pulumi crea un entorno virtual de Python para cada proyecto, así que necesitas el módulo venv. Instala también Docker desde los repositorios de Ubuntu:

sudo apt update
sudo apt install python3-venv docker.io

Añade tu usuario al grupo docker para que Pulumi pueda usar el socket de Docker sin sudo:

sudo usermod -aG docker "$USER"
newgrp docker

Comprueba que Docker responde:

docker info --format '{{.ServerVersion}}'
27.5.1

Paso 2: Instalar la CLI de Pulumi

Pulumi distribuye la CLI con un script de instalación oficial. Descárgalo primero para revisarlo antes de ejecutarlo:

curl -fsSL https://get.pulumi.com -o install-pulumi.sh
less install-pulumi.sh

Ejecútalo con tu usuario normal, sin sudo. Instala los binarios en ~/.pulumi/bin y añade esa ruta a tu ~/.bashrc:

sh install-pulumi.sh

Recarga la configuración del shell y comprueba la versión:

source ~/.bashrc
pulumi version
v3.197.0

La versión exacta será la última disponible. Para actualizar más adelante, vuelve a ejecutar el script.

Paso 3: Elegir dónde se guarda el estado

Por defecto Pulumi guarda el estado en Pulumi Cloud, un servicio que requiere cuenta. Para esta guía usarás el backend local, que guarda el estado en ~/.pulumi y no depende de nada externo:

pulumi login --local
Logged in to servidor as tu_usuario (file://~)

Con el backend local, Pulumi cifra los secretos del stack con una frase de paso. Expórtala en la sesión para que no la pida en cada comando, y guárdala en tu gestor de contraseñas, porque sin ella no podrás leer los secretos:

export PULUMI_CONFIG_PASSPHRASE="tu_frase_de_paso_segura"

Sustituye tu_frase_de_paso_segura por una frase larga y única.

Para trabajar en equipo, el estado puede ir también a un bucket con pulumi login s3://nombre-del-bucket (u otros almacenamientos como Azure Blob o Google Cloud Storage). El resto de comandos de la guía no cambia.

Paso 4: Crear el proyecto

Crea un directorio vacío y genera un proyecto a partir de la plantilla de Python:

mkdir -p ~/pulumi-web && cd ~/pulumi-web
pulumi new python --name pulumi-web --stack dev --yes

--yes acepta los valores por defecto del resto de preguntas. El comando crea el stack dev, un entorno virtual e instala las dependencias básicas. El proyecto queda así:

ls -A
.gitignore  Pulumi.yaml  __main__.py  requirements.txt  venv
  • Pulumi.yaml: nombre del proyecto y runtime (Python y el entorno virtual que usa).
  • __main__.py: el programa que define la infraestructura.
  • requirements.txt: dependencias de Python, incluido el SDK pulumi.

Un stack es una instancia independiente del proyecto con su propio estado y su propia configuración, por ejemplo dev, staging y produccion.

Paso 5: Añadir el proveedor de Docker

Cada proveedor de Pulumi es un paquete de Python. Añade el de Docker a requirements.txt:

nano requirements.txt
pulumi>=3.0.0,<4.0.0
pulumi-docker>=4.0.0,<5.0.0

Instala las dependencias en el entorno virtual del proyecto:

pulumi install

pulumi install lee Pulumi.yaml, instala los paquetes de requirements.txt en el entorno virtual y descarga los plugins de proveedor necesarios.

Paso 6: Escribir el programa

Sustituye el contenido de __main__.py por un programa que descarga la imagen de Nginx y arranca un contenedor:

nano __main__.py
import pulumi
import pulumi_docker as docker

config = pulumi.Config()
puerto = config.get_int("puerto") or 8080

imagen = docker.RemoteImage(
    "nginx",
    name="nginx:stable",
    keep_locally=True,
)

contenedor = docker.Container(
    "web",
    name=f"web-{pulumi.get_stack()}",
    image=imagen.image_id,
    ports=[docker.ContainerPortArgs(internal=80, external=puerto)],
    restart="unless-stopped",
)

pulumi.export("contenedor", contenedor.name)
pulumi.export("url", f"http://localhost:{puerto}")

Fíjate en image=imagen.image_id: el contenedor recibe una salida de otro recurso. Pulumi deduce de ahí que debe crear la imagen antes que el contenedor, igual que Terraform con las referencias entre recursos.

Paso 7: Previsualizar y desplegar

Muestra qué cambios haría Pulumi sin aplicarlos:

pulumi preview
Previewing update (dev):
     Type                         Name            Plan
 +   pulumi:pulumi:Stack          pulumi-web-dev  create
 +   ├─ docker:index:RemoteImage  nginx           create
 +   └─ docker:index:Container    web             create

Resources:
    + 3 to create

Aplica los cambios:

pulumi up --yes
Updating (dev):
     Type                         Name            Status
 +   pulumi:pulumi:Stack          pulumi-web-dev  created (4s)
 +   ├─ docker:index:RemoteImage  nginx           created (3s)
 +   └─ docker:index:Container    web             created (1s)

Outputs:
    contenedor: "web-dev"
    url       : "http://localhost:8080"

Resources:
    + 3 created

Comprueba que el contenedor responde:

curl -sI http://localhost:8080 | head -n 1
HTTP/1.1 200 OK

Consulta los outputs del stack cuando quieras con:

pulumi stack output url

Paso 8: Configuración y secretos por stack

El programa lee el puerto con config.get_int("puerto"). Cambia el valor solo para el stack dev:

pulumi config set puerto 8081

El valor se guarda en Pulumi.dev.yaml, que se versiona junto al código:

cat Pulumi.dev.yaml
encryptionsalt: v1:...
config:
  pulumi-web:puerto: "8081"

Aplica el cambio. Pulumi reemplaza el contenedor porque el puerto publicado no se puede modificar en caliente:

pulumi up --yes

Para valores sensibles usa --secret. Pulumi los cifra en el archivo de configuración y en el estado:

pulumi config set --secret token_api "valor_secreto"
grep token_api Pulumi.dev.yaml
  pulumi-web:token_api:
    secure: v1:3kZ0...

En el programa, léelo con config.require_secret("token_api"). El resultado es una salida marcada como secreta: si la exportas o la pasas a un recurso, Pulumi la oculta en la consola y la mantiene cifrada en el estado. Para verla en claro:

pulumi config get token_api

Paso 9: Trabajar con varios stacks

Crea un segundo stack para simular otro entorno:

pulumi stack init staging
pulumi config set puerto 8090
pulumi up --yes

Ahora hay dos contenedores, web-dev y web-staging, cada uno con su estado. Lista los stacks y cambia entre ellos:

pulumi stack ls
pulumi stack select dev
NAME      LAST UPDATE     RESOURCE COUNT
dev*      2 minutes ago   3
staging   30 seconds ago  3

Paso 10: Destruir los recursos

Borra los recursos de cada stack y después el propio stack:

pulumi destroy --yes --stack staging
pulumi stack rm staging --yes

Repite con dev si ya no lo necesitas. pulumi stack rm se niega a borrar un stack que todavía tiene recursos, lo que evita dejar infraestructura huérfana sin estado.

Pulumi frente a Terraform

AspectoPulumiTerraform
LenguajePython, TypeScript, Go, C#, Java, YAMLHCL
LógicaBucles, funciones, clases y paquetes del lenguajecount, for_each, módulos y funciones integradas
EstadoPulumi Cloud, local o bucket (S3, Azure, GCS)Local o backend remoto (S3, HCP Terraform, pg, etc.)
SecretosCifrados en configuración y estadoEn texto plano en el estado; sensitive solo oculta la salida
PruebasTests unitarios con las herramientas del lenguaje (pytest, Jest)terraform test
LicenciaApache 2.0BSL 1.1 (OpenTofu es la bifurcación libre)

Elige Pulumi si tu equipo ya programa en uno de esos lenguajes y la infraestructura tiene lógica que en HCL resulta forzada, o si quieres reutilizar componentes como paquetes normales. Elige Terraform u OpenTofu si prefieres un lenguaje declarativo que cualquiera pueda leer sin saber programar, o si ya tienes módulos y experiencia en HCL.

Solución de problemas

error: getting secrets manager: passphrase must be set: falta la variable PULUMI_CONFIG_PASSPHRASE en la sesión. Expórtala con la misma frase que usaste al crear el stack.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock: tu usuario no está en el grupo docker en la sesión actual. Cierra sesión y vuelve a entrar, o ejecuta newgrp docker.

ModuleNotFoundError: No module named 'pulumi_docker': el paquete no está en el entorno virtual del proyecto. Comprueba requirements.txt y ejecuta pulumi install de nuevo.

Bind for 0.0.0.0:8080 failed: port is already allocated: otro proceso usa el puerto. Cambia el valor con pulumi config set puerto y vuelve a aplicar.

Conclusión

Has instalado Pulumi, creado un proyecto en Python con estado local, desplegado y modificado un contenedor, cifrado secretos en la configuración y gestionado dos stacks independientes. Como siguientes pasos, mueve el estado a un bucket compartido con pulumi login s3://..., sustituye el proveedor de Docker por el de tu nube o por pulumi-kubernetes, y agrupa recursos relacionados en un ComponentResource para reutilizarlos entre proyectos.