Saltar al contenido

Pulumi y TypeScript para infraestructura en la nube

Guía práctica para gestionar infraestructura con Pulumi y TypeScript: composición de recursos, estado, secretos y pruebas del código de infraestructura.

4 min de lectura
Arquitectura de un stack de Pulumi que muestra definiciones de recursos en TypeScript compiladas en llamadas a la API del proveedor de nube

Por qué TypeScript para infraestructura

YAML y HCL se diseñaron como lenguajes de configuración declarativos. Funcionan bien hasta que necesitas condicionales, bucles, abstracciones o seguridad de tipos; a partir de ahí terminas luchando contra el lenguaje en lugar de resolver el problema de infraestructura. Pulumi utiliza lenguajes de programación reales para definir la infraestructura, lo que significa que el sistema de tipos de TypeScript, el soporte del IDE, los frameworks de pruebas y el ecosistema de paquetes se aplican directamente a los recursos en la nube.

Este cambio no es cosmético. Transforma la forma en que piensas la infraestructura: ya no como configuración estática, sino como código componible y verificable mediante pruebas.

Definición de recursos con seguridad de tipos

tstypescript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
 
// ❌ YAML/HCL — no type checking, no autocomplete, string-based references
// resource "aws_s3_bucket" "data" {
//   bucket = "my-data-bucket"
//   acl    = "privte"  ← typo undetected until apply
// }
 
// ✅ TypeScript — type errors caught at compile time
const dataBucket = new aws.s3.Bucket("data-bucket", {
  bucket: "my-data-bucket",
  acl: "private", // Autocomplete shows valid values
  versioning: { enabled: true },
  serverSideEncryptionConfiguration: {
    rule: {
      applyServerSideEncryptionByDefault: {
        sseAlgorithm: "aws:kms",
      },
    },
  },
  lifecycleRules: [
    {
      enabled: true,
      transitions: [
        { days: 30, storageClass: "STANDARD_IA" },
        { days: 90, storageClass: "GLACIER" },
      ],
      expiration: { days: 365 },
    },
  ],
});
 
// Output the bucket ARN — typed as pulumi.Output<string>
export const bucketArn = dataBucket.arn;

Composición de componentes reutilizables

Los lenguajes de programación reales te permiten construir abstracciones. Un ComponentResource encapsula recursos relacionados en un módulo reutilizable con una interfaz tipada, algo imposible en herramientas puramente declarativas sin recurrir a plantillas de terceros.

tstypescript
interface WebAppArgs {
  domain: string;
  environment: "staging" | "production";
  containerImage: string;
  cpu: number;
  memory: number;
  desiredCount: number;
  healthCheckPath: string;
}
 
class WebApp extends pulumi.ComponentResource {
  public readonly url: pulumi.Output<string>;
  public readonly serviceName: pulumi.Output<string>;
 
  constructor(
    name: string,
    args: WebAppArgs,
    opts?: pulumi.ComponentResourceOptions
  ) {
    super("custom:WebApp", name, {}, opts);
 
    const cluster = new aws.ecs.Cluster(`${name}-cluster`, {}, { parent: this });
 
    const taskDef = new aws.ecs.TaskDefinition(
      `${name}-task`,
      {
        family: name,
        cpu: String(args.cpu),
        memory: String(args.memory),
        networkMode: "awsvpc",
        requiresCompatibilities: ["FARGATE"],
        containerDefinitions: JSON.stringify([
          {
            name: name,
            image: args.containerImage,
            portMappings: [{ containerPort: 3000 }],
            healthCheck: {
              command: ["CMD-SHELL", `curl -f http://localhost:3000${args.healthCheckPath} || exit 1`],
              interval: 30,
              timeout: 5,
              retries: 3,
            },
          },
        ]),
      },
      { parent: this }
    );
 
    const service = new aws.ecs.Service(
      `${name}-service`,
      {
        cluster: cluster.arn,
        taskDefinition: taskDef.arn,
        desiredCount: args.desiredCount,
        launchType: "FARGATE",
      },
      { parent: this }
    );
 
    this.url = pulumi.interpolate`https://${args.domain}`;
    this.serviceName = service.name;
 
    this.registerOutputs({
      url: this.url,
      serviceName: this.serviceName,
    });
  }
}
 
// Usage — deploy two environments with one component
const staging = new WebApp("api-staging", {
  domain: "staging.api.example.com",
  environment: "staging",
  containerImage: "registry.example.com/api:latest",
  cpu: 256,
  memory: 512,
  desiredCount: 1,
  healthCheckPath: "/health",
});
 
const production = new WebApp("api-production", {
  domain: "api.example.com",
  environment: "production",
  containerImage: "registry.example.com/api:v2.3.1",
  cpu: 1024,
  memory: 2048,
  desiredCount: 3,
  healthCheckPath: "/health",
});

Gestión de secretos y configuración

Pulumi cifra los secretos en el estado de forma predeterminada. Los valores de configuración están tipados y delimitados por stack, de modo que el mismo código se despliega en distintos entornos sin necesidad de condicionales dispersos por las definiciones.

tstypescript
const config = new pulumi.Config();
 
// Plaintext config
const region = config.require("region");
const environment = config.require("environment");
 
// Encrypted secrets — never stored in plaintext in state
const dbPassword = config.requireSecret("dbPassword");
const apiKey = config.requireSecret("apiKey");
 
// Type-safe configuration objects
interface AppConfig {
  replicas: number;
  logLevel: string;
  features: string[];
}
 
const appConfig = config.requireObject<AppConfig>("app");
 
// Secrets are pulumi.Output<string> — can only be used in resource args
const database = new aws.rds.Instance("main-db", {
  instanceClass: "db.t3.medium",
  allocatedStorage: 20,
  engine: "postgres",
  engineVersion: "16",
  masterUsername: "admin",
  masterPassword: dbPassword, // Encrypted in state
  skipFinalSnapshot: environment !== "production",
});

Pruebas del código de infraestructura

Como la infraestructura es TypeScript, puedes escribir pruebas unitarias que verifiquen las configuraciones de los recursos sin desplegar nada. El framework de mocks de Pulumi sustituye las llamadas a la API de la nube por aserciones.

tstypescript
import * as pulumi from "@pulumi/pulumi";
import { describe, it, expect, beforeAll } from "vitest";
 
// Mock Pulumi runtime for testing
pulumi.runtime.setMocks({
  newResource: (args) => ({
    id: `${args.name}-id`,
    state: args.inputs,
  }),
  call: (args) => args.inputs,
});
 
describe("WebApp component", () => {
  let app: typeof import("./index");
 
  beforeAll(async () => {
    app = await import("./index");
  });
 
  it("creates ECS service with correct desired count", (done) => {
    pulumi.all([app.production.serviceName]).apply(([name]) => {
      expect(name).toBeDefined();
      done();
    });
  });
 
  it("uses FARGATE launch type", (done) => {
    // Verify resource properties match expectations
    const resources = pulumi.runtime.listResourceOutputs();
    // Assert against collected resources
    done();
  });
});
 
// Policy tests — enforce organizational rules
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
 
new PolicyPack("security-policies", {
  policies: [
    {
      name: "s3-no-public-read",
      description: "S3 buckets must not have public read access",
      enforcementLevel: "mandatory",
      validateResource: validateResourceOfType(
        aws.s3.Bucket,
        (bucket, args, reportViolation) => {
          if (bucket.acl === "public-read" || bucket.acl === "public-read-write") {
            reportViolation("S3 buckets must not be publicly readable");
          }
        }
      ),
    },
    {
      name: "rds-encryption-required",
      description: "RDS instances must have storage encryption enabled",
      enforcementLevel: "mandatory",
      validateResource: validateResourceOfType(
        aws.rds.Instance,
        (instance, args, reportViolation) => {
          if (!instance.storageEncrypted) {
            reportViolation("RDS instances must enable storage encryption");
          }
        }
      ),
    },
  ],
});

Referencias entre stacks para arquitecturas multi-stack

Las infraestructuras grandes se dividen en varios stacks (red, cómputo, bases de datos), cada uno gestionado de forma independiente. Las referencias entre stacks permiten que un stack consuma los outputs de otro sin recurrir a valores fijos en el código.

tstypescript
// Networking stack exports VPC and subnet IDs
export const vpcId = vpc.id;
export const privateSubnetIds = privateSubnets.map((s) => s.id);
 
// Application stack references networking outputs
const networkStack = new pulumi.StackReference("org/networking/production");
const vpcId = networkStack.getOutput("vpcId");
const subnetIds = networkStack.getOutput("privateSubnetIds");
 
const service = new aws.ecs.Service("app", {
  networkConfiguration: {
    subnets: subnetIds as pulumi.Output<string[]>,
    securityGroups: [appSecurityGroup.id],
  },
});

Conclusiones clave

Usar TypeScript para infraestructura te da seguridad de tipos, autocompletado en el IDE, herramientas de refactorización y abstracciones reales, nada de lo cual está disponible en YAML o HCL. Los errores de tipos en las configuraciones de recursos se detectan en tiempo de compilación, no durante un ciclo de apply de diez minutos.

Crea clases ComponentResource reutilizables que encapsulen recursos relacionados detrás de interfaces tipadas. Esto elimina el copia y pega entre entornos y garantiza configuraciones consistentes. Usa el sistema de configuración de Pulumi para los valores específicos de cada entorno y su cifrado de secretos para las credenciales.

Prueba el código de infraestructura igual que pruebas el código de aplicación: pruebas unitarias para la configuración de recursos, pruebas de políticas para las reglas de la organización y pruebas de integración para el despliegue del stack de extremo a extremo. La inversión en infraestructura verificable con pruebas se recupera cada vez que se detecta un error de configuración antes de que llegue a producción.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX