An internal developer platform should reduce cognitive load without hiding essential details. Pulumi enables this balance by expressing infrastructure as real code, packaging opinionated components, and enforcing policies close to the developer workflow. The result is a layered system where foundations are stable, platform capabilities are reusable, and application teams have safe self-service. The following approach focuses on composable layers, reproducible guardrails, and practical delivery paths across cloud providers.

A layered model starts with a foundations layer that establishes organizations, accounts or subscriptions, logging, and baseline networking. A platform layer builds shared capabilities such as identity, secrets, container registries, Kubernetes, and internal DNS. A product layer offers curated components and “golden paths” for common workloads, expressed as Pulumi packages. Application stacks consume those components with minimal inputs and clear outputs. Each layer is versioned, testable, and promotes through environments using immutable releases.

State and configuration should be explicit and portable. Teams often choose a cloud-native object store with server-side locking for Pulumi state and a provider-backed secrets manager for encryption. Configuration is committed as code per stack, with naming and tagging schemes enforced by policies rather than conventions. The example below shows a durable backend on AWS using S3 and DynamoDB; an equivalent approach on Azure uses Blob Storage and Table Storage with Key Vault encryption.

# Setup Pulumi backend with S3 state storage and DynamoDB locking for platform infrastructure
aws s3api create-bucket --bucket idp-pulumi-state-123 --region eu-central-1
aws dynamodb create-table \
  --table-name pulumi-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST
pulumi login s3://idp-pulumi-state-123
pulumi stack init platform-prod
pulumi stack select platform-prod
pulumi config set env production
pulumi config set region eu-central-1
pulumi config set dbPassword 'REDACTED' --secret \
  --secret-provider 'awskms://arn:aws:kms:eu-central-1:111122223333:key/abcd-1234'

Reusable capabilities belong in component resources. A component should accept a minimal contract, apply defaults, and emit stable outputs for downstream stacks. The point is not to wrap every resource but to encode platform choices once, including naming, tags, and secure defaults. The component below illustrates a network baseline that validates inputs, enforces tags, and publishes outputs needed by higher layers.

// Pulumi component resource for standardized network baseline with validation and tagging
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

export interface NetArgs {
  cidr: string;
  tags?: Record<string, string>;
}

export class NetworkBaseline extends pulumi.ComponentResource {
  public readonly vpcId: pulumi.Output<string>;
  public readonly privateSubnetIds: pulumi.Output<string[]>;

  constructor(name: string, args: NetArgs, opts?: pulumi.ComponentResourceOptions) {
    super("pkg:platform:NetworkBaseline", name, {}, opts);

    if (!/^(\d{1,3}\.){3}\d{1,3}\/\d+$/.test(args.cidr)) {
      throw new Error("cidr must be a valid CIDR block");
    }
    const tags = { "platform:owner": "platform-team", "platform:layer": "network", ...(args.tags || {}) };

    const vpc = new aws.ec2.Vpc(`${name}-vpc`, { cidrBlock: args.cidr, enableDnsHostnames: true, enableDnsSupport: true, tags }, { parent: this });

    const azs = ["a", "b"];
    const priv = azs.map((z, i) =>
      new aws.ec2.Subnet(`${name}-priv-${z}`, {
        vpcId: vpc.id,
        cidrBlock: pulumi.output(aws.ec2.getSubnetIds()).apply(() => `${args.cidr.split("/")[0].slice(0, -1)}${i * 16}/28`), // simple placeholder
        mapPublicIpOnLaunch: false,
        availabilityZone: pulumi.output(aws.getAvailabilityZones()).apply(r => r.names[i]),
        tags,
      }, { parent: this })
    );

    this.vpcId = vpc.id;
    this.privateSubnetIds = pulumi.output(priv.map(s => s.id));
    this.registerOutputs({ vpcId: this.vpcId, privateSubnetIds: this.privateSubnetIds });
  }
}

Guardrails should exist at multiple points: organization policies, infrastructure policy packs, and runtime admission where applicable. Pulumi’s policy-as-code model evaluates rules during preview and apply, giving developers immediate feedback. Start with a small, enforceable set: mandatory tags, encryption requirements, no public storage buckets, and bounded CIDRs. The following policy pack demonstrates two practical rules.

// Pulumi policy pack enforcing mandatory tags and preventing public storage buckets
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

new PolicyPack("platform-guardrails", {
  policies: [
    {
      name: "required-tags",
      description: "Ensure required owner and cost tags are present.",
      enforcementLevel: "mandatory",
      validateResource: (args, reportViolation) => {
        const t = (args.props as any).tags || {};
        if (!t["owner"] || !t["cost-center"]) reportViolation("Resources must include 'owner' and 'cost-center' tags.");
      },
    },
    {
      name: "no-public-buckets",
      description: "Prevent public S3 buckets.",
      enforcementLevel: "mandatory",
      validateResource: validateResourceOfType(aws.s3.BucketV2, (b, reportViolation) => {
        if (b.acl && (b.acl === "public-read" || b.acl === "public-read-write")) {
          reportViolation("S3 buckets must not be public.");
        }
      }),
    },
  ],
});

Organization-level policies complement infrastructure checks. Cloud account guardrails catch drift and out-of-band changes. For example, a service control policy can deny public access control lists on storage regardless of IaC intent, creating a defense in depth. The minimal example below illustrates the idea with a deny-by-condition approach and masked IDs.

# AWS Service Control Policy denying public S3 bucket ACLs for security enforcement
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyPublicS3ACL",
      "Effect": "Deny",
      "Action": ["s3:PutBucketAcl", "s3:PutObjectAcl"],
      "Resource": ["arn:aws:s3:::*"],
      "Condition": { "StringEquals": { "s3:x-amz-acl": ["public-read", "public-read-write"] } }
    }
  ]
}

Self-service depends on stable contracts between layers. Application teams should not discover network IDs or certificate ARNs by hand; they should consume them as typed outputs. Stack references are the simplest mechanism to share these values without hardcoding. A small service stack can then assemble compute, registry, and permissions from the platform package with a handful of inputs.

// Application stack consuming platform components via stack references for self-service deployment
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import { NetworkBaseline } from "@org/platform-pkg";

const cfg = new pulumi.Config();
const app = cfg.require("app");
const platform = new pulumi.StackReference("org/platform/platform-prod");

const vpcId = platform.requireOutput("vpcId");
const subnets = platform.requireOutput("privateSubnetIds");

const role = new aws.iam.Role(`${app}-task-role`, {
  assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: "ecs-tasks.amazonaws.com" }),
});

const net = new NetworkBaseline("noop", { cidr: "10.0.0.0/24" }, { dependsOn: [] }); // referenced for typing; real network comes from platform
export const networkId = vpcId;
export const subnetIds = subnets;
export const taskRoleArn = role.arn;

Delivery should be gated through previews, policy evaluation, and environment approvals. A thin pipeline can authenticate to the target account, run a Pulumi preview that comments on the pull request, and promote to apply once checks pass. Keeping environment configuration immutable ensures the same artifact deploys through dev, test, and prod with only stack and role changes.

# GitHub Actions workflow for Pulumi platform deployment with preview and apply stages
name: platform-deploy
on:
  pull_request:
    paths: ["infra/**"]
  push:
    branches: ["main"]
jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pulumi/actions@v5
        with:
          command: preview
          cloud-url: s3://idp-pulumi-state-123
          stack-name: platform-prod
        env:
          AWS_REGION: eu-central-1
          AWS_ROLE_TO_ASSUME: arn:aws:iam::111122223333:role/ci-pulumi
  apply:
    if: github.ref == 'refs/heads/main'
    needs: preview
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pulumi/actions@v5
        with:
          command: up
          stack-name: platform-prod
        env:
          AWS_REGION: eu-central-1
          AWS_ROLE_TO_ASSUME: arn:aws:iam::111122223333:role/ci-pulumi

Runtime controls round out the story for container platforms. Even with infrastructure policies, misconfigured workloads can stress clusters or breach tenancy boundaries. Admission policies in Kubernetes keep limits, security contexts, and network policies consistent. The following Gatekeeper constraint requires resource requests and limits for every pod; equivalent checks can be expressed with native admission policies.

# Gatekeeper constraint requiring resource limits and requests for all pods
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
  name: require-limits
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    limits:
      - containerName: "*"
        requests: ["cpu", "memory"]
        limits: ["cpu", "memory"]

Incremental adoption is straightforward. Existing Terraform or raw cloud resources can be brought under management without rewriting everything at once. A practical path is to convert simple modules to components, import critical resources to establish state, then expand coverage as confidence grows. The following snippet shows common commands used during migration.

# Pulumi commands for incremental migration from existing infrastructure
pulumi convert --from terraform --yes
pulumi import aws:ec2/vpc:Vpc platform-vpc vpc-0abc123456789def0
pulumi import azure-native:network/virtualNetwork:VirtualNetwork core-vnet /subscriptions/0000.../resourceGroups/rg-core/providers/Microsoft.Network/virtualNetworks/vnet-core
pulumi preview
pulumi up

Cost and operations benefit from the same discipline. Mandatory tags unlock budget alerts and reporting by team or environment. Drift detection via scheduled previews surfaces unmanaged changes. Backups of the state backend and key rotations for the secrets provider prevent unpleasant surprises. Error budgets and SLOs for the platform itself keep focus on reliability rather than feature count.

The approach trades some initial engineering effort for repeatable outcomes. Teams inherit secure defaults, but can still read and modify code when needed. Policy packs catch mistakes early, while organization policies and runtime admission provide defense in depth. With layered components, reproducible state, and a narrow set of self-service contracts, platform engineering becomes a product with clear boundaries and predictable behavior.

References

  1. Pulumi Documentation https://www.pulumi.com/docs/

  2. Pulumi CrossGuard Policy as Code https://www.pulumi.com/docs/using-pulumi/crossguard/

  3. Platform Engineering Principles https://platformengineering.org/

  4. AWS Service Control Policies https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html

  5. OPA Gatekeeper for Kubernetes https://open-policy-agent.github.io/gatekeeper/