Claude Code for Infrastructure as Code: Pulumi, CDK, and Cloud Resource Management — Claude Skills 360 Blog
Blog / DevOps / Claude Code for Infrastructure as Code: Pulumi, CDK, and Cloud Resource Management
DevOps

Claude Code for Infrastructure as Code: Pulumi, CDK, and Cloud Resource Management

Published: July 28, 2026
Read time: 9 min read
By: Claude Skills 360

Infrastructure as code means treating cloud resources — VPCs, databases, queues — with the same rigor as application code: version controlled, reviewed, tested, and reproducible. Claude Code generates Pulumi programs and AWS CDK constructs that define both the resources and their relationships, with proper dependency ordering and parameterization for multiple environments.

Pulumi TypeScript

Define the infrastructure for our Node.js API:
- VPC with public/private subnets
- RDS PostgreSQL in private subnets
- ECS Fargate service in private subnet, load balanced
- Secrets Manager for DB credentials
Parameters for dev vs production (different sizes, single vs multi-AZ)
// infra/index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";

const config = new pulumi.Config();
const env = pulumi.getStack(); // "dev" or "production"

const isProd = env === "production";

// VPC with public/private subnets
const vpc = new awsx.ec2.Vpc("app-vpc", {
  numberOfAvailabilityZones: isProd ? 3 : 2,
  natGateways: { strategy: isProd ? "OnePerAz" : "Single" }, // Cost vs HA
  subnetSpecs: [
    { type: awsx.ec2.SubnetType.Public, name: "public" },
    { type: awsx.ec2.SubnetType.Private, name: "private" },
  ],
  tags: { Environment: env, ManagedBy: "pulumi" },
});

// DB password in Secrets Manager
const dbPassword = new aws.secretsmanager.Secret("db-password");
const dbPasswordValue = new aws.secretsmanager.SecretVersion("db-password-value", {
  secretId: dbPassword.id,
  secretString: config.requireSecret("dbPassword"),
});

// RDS PostgreSQL
const dbSubnetGroup = new aws.rds.SubnetGroup("db-subnets", {
  subnetIds: vpc.privateSubnetIds,
});

const dbSecurityGroup = new aws.ec2.SecurityGroup("db-sg", {
  vpcId: vpc.vpcId,
  ingress: [{
    protocol: "tcp",
    fromPort: 5432,
    toPort: 5432,
    // Only allow from ECS tasks — set after creating ECS security group
    securityGroups: [], // Filled below
  }],
  tags: { Name: "db-sg", Environment: env },
});

const db = new aws.rds.Instance("app-db", {
  engine: "postgres",
  engineVersion: "16.1",
  instanceClass: isProd ? "db.t4g.medium" : "db.t4g.micro",
  allocatedStorage: isProd ? 100 : 20,
  storageEncrypted: true,
  dbName: "appdb",
  username: "appuser",
  password: config.requireSecret("dbPassword"),
  multiAz: isProd,
  dbSubnetGroupName: dbSubnetGroup.name,
  vpcSecurityGroupIds: [dbSecurityGroup.id],
  skipFinalSnapshot: !isProd,
  backupRetentionPeriod: isProd ? 7 : 0,
  deletionProtection: isProd,
  tags: { Environment: env },
});

// ECS Cluster
const cluster = new aws.ecs.Cluster("app-cluster", {
  settings: [{ name: "containerInsights", value: "enabled" }],
  tags: { Environment: env },
});

// Application Load Balancer
const alb = new awsx.lb.ApplicationLoadBalancer("app-alb", {
  subnetIds: vpc.publicSubnetIds,
  tags: { Environment: env },
});

// ECS Fargate Service
const appService = new awsx.ecs.FargateService("app-service", {
  cluster: cluster.arn,
  taskDefinitionArgs: {
    containers: {
      app: {
        image: config.require("imageUri"),
        cpu: isProd ? 512 : 256,
        memory: isProd ? 1024 : 512,
        portMappings: [{ containerPort: 3000, targetGroup: alb.defaultTargetGroup }],
        environment: [
          { name: "NODE_ENV", value: env },
          { name: "DB_HOST", value: db.address },
          { name: "DB_NAME", value: "appdb" },
        ],
        secrets: [
          {
            name: "DB_PASSWORD",
            valueFrom: dbPasswordValue.arn,
          },
        ],
        logConfiguration: {
          logDriver: "awslogs",
          options: {
            "awslogs-group": `/ecs/app-${env}`,
            "awslogs-region": aws.config.region!,
            "awslogs-stream-prefix": "app",
          },
        },
      },
    },
    taskRole: { args: { managedPolicyArns: [aws.iam.ManagedPolicy.AmazonSSMManagedInstanceCore] } },
  },
  desiredCount: isProd ? 3 : 1,
  networkConfiguration: {
    subnets: vpc.privateSubnetIds,
    securityGroups: [/* ECS security group */],
  },
  tags: { Environment: env },
});

// Auto-scaling for production
if (isProd) {
  const scalingTarget = new aws.appautoscaling.Target("app-scaling-target", {
    maxCapacity: 10,
    minCapacity: 2,
    resourceId: pulumi.interpolate`service/${cluster.name}/${appService.service.name}`,
    scalableDimension: "ecs:service:DesiredCount",
    serviceNamespace: "ecs",
  });

  new aws.appautoscaling.Policy("app-cpu-scaling", {
    policyType: "TargetTrackingScaling",
    resourceId: scalingTarget.resourceId,
    scalableDimension: scalingTarget.scalableDimension,
    serviceNamespace: scalingTarget.serviceNamespace,
    targetTrackingScalingPolicyConfiguration: {
      predefinedMetricSpecification: { predefinedMetricType: "ECSServiceAverageCPUUtilization" },
      targetValue: 70.0,
      scaleInCooldown: 300,
      scaleOutCooldown: 60,
    },
  });
}

// Stack outputs — used by deployment pipelines
export const albDnsName = alb.loadBalancer.dnsName;
export const dbEndpoint = db.address;
export const clusterArn = cluster.arn;
# Deploy to dev
pulumi up --stack dev

# Deploy to production (with preview first)
pulumi preview --stack production
pulumi up --stack production

AWS CDK

Convert this manually-created S3 static site + CloudFront distribution to CDK.
Include: OAI for S3 access, custom domain with ACM cert, cache policies.
// lib/StaticSiteStack.ts
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import { Construct } from 'constructs';

interface StaticSiteProps extends cdk.StackProps {
  domainName: string;
  siteSubDomain: string;
  buildDir: string;
}

export class StaticSiteStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: StaticSiteProps) {
    super(scope, id, props);

    const zone = route53.HostedZone.fromLookup(this, 'Zone', {
      domainName: props.domainName,
    });

    const siteDomain = `${props.siteSubDomain}.${props.domainName}`;

    // S3 bucket — private, no website hosting (CloudFront handles it)
    const bucket = new s3.Bucket(this, 'SiteBucket', {
      versioned: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    // ACM certificate — must be in us-east-1 for CloudFront
    const certificate = new acm.DnsValidatedCertificate(this, 'SiteCertificate', {
      domainName: siteDomain,
      hostedZone: zone,
      region: 'us-east-1',
    });

    // CloudFront distribution
    const distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
      defaultBehavior: {
        origin: origins.S3BucketOrigin.withOriginAccessControl(bucket),
        compress: true,
        allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
      domainNames: [siteDomain],
      certificate,
      defaultRootObject: 'index.html',
      errorResponses: [
        // SPA routing: 404 → serve index.html → React Router handles it
        { httpStatus: 404, responsePagePath: '/index.html', responseHttpStatus: 200 },
      ],
      minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
    });

    // Route53 alias
    new route53.ARecord(this, 'SiteAliasRecord', {
      recordName: siteDomain,
      target: route53.RecordTarget.fromAlias(
        new route53Targets.CloudFrontTarget(distribution)
      ),
      zone,
    });

    // Deploy site to S3 + invalidate CloudFront cache
    new s3deploy.BucketDeployment(this, 'DeploySite', {
      sources: [s3deploy.Source.asset(props.buildDir)],
      destinationBucket: bucket,
      distribution,
      distributionPaths: ['/*'], // Invalidate all paths on deploy
    });

    new cdk.CfnOutput(this, 'SiteUrl', { value: `https://${siteDomain}` });
    new cdk.CfnOutput(this, 'DistributionId', { value: distribution.distributionId });
  }
}

For the Terraform alternative to Pulumi/CDK, see the Terraform IaC guide. For deploying Kubernetes workloads on the infrastructure defined here, see the GitOps guide. The Claude Skills 360 bundle includes infrastructure as code skill sets for Pulumi, CDK, and multi-environment patterns. Start with the free tier to try cloud resource generation.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free