.claude/skills/cloudformation-to-pulumi/SKILL.md
Convert an AWS CloudFormation stack or template to Pulumi. This skill MUST be loaded whenever a user requests migration or conversion of CloudFormation to Pulumi.
npx skillsauth add dirien/yet-another-agent-harness cloudformation-to-pulumiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
If you have already generated a migration plan before loading this skill, you MUST:
The migration output MUST meet all of the following:
Complete Resource Coverage
CloudFormation Logical ID as Resource Name
cdk-importer tool to automatically find import IDs.Successful Deployment
pulumi preview (assuming proper config).Zero-Diff Import Validation (if importing existing resources)
pulumi preview must show NO updates, replaces, creates, or deletes.Final Migration Report
If the user has not provided a CloudFormation template, you MUST fetch it from AWS using the stack name.
Follow this workflow exactly and in this order:
Running AWS commands requires credentials loaded via Pulumi ESC.
For detailed ESC information: Use skill pulumi-esc.
You MUST confirm the AWS region with the user.
If user provided a template file: Read the template directly.
If user only provided a stack name: Fetch the template from AWS:
aws cloudformation get-template \
--region <region> \
--stack-name <stack-name> \
--query 'TemplateBody' \
--output json > template.json
List all resources in the stack:
aws cloudformation list-stack-resources \
--region <region> \
--stack-name <stack-name> \
--output json
This provides:
LogicalResourceId - Use this as the Pulumi resource namePhysicalResourceId - The actual AWS resource IDResourceType - The CloudFormation resource typeExtract from the template:
IMPORTANT: There is NO automated conversion tool for CloudFormation. You MUST convert each resource manually.
Every Pulumi resource MUST use the CloudFormation Logical ID as its name.
// CloudFormation:
// "MyAppBucketABC123": { "Type": "AWS::S3::Bucket", ... }
// Pulumi - CORRECT:
const myAppBucket = new aws.s3.Bucket("MyAppBucketABC123", { ... });
// Pulumi - WRONG (DO NOT do this - import will fail):
const myAppBucket = new aws.s3.Bucket("my-app-bucket", { ... });
This naming convention is REQUIRED because the cdk-importer tool matches resources by name.
⚠️ CRITICAL: ALWAYS USE aws-native BY DEFAULT ⚠️
aws-native for all resources unless there's a specific reason to use aws.AWS::S3::Bucket → aws-native.s3.Bucket).aws (classic) when aws-native doesn't support a required feature.This is MANDATORY for successful imports with cdk-importer. The cdk-importer works by matching CloudFormation resources to Pulumi resources, and CloudFormation maps 1:1 to aws-native. Using the classic aws provider will cause import failures.
Map CloudFormation intrinsic functions to Pulumi equivalents:
| CloudFormation | Pulumi Equivalent |
|----------------|-------------------|
| !Ref (resource) | Resource output (e.g., bucket.id) |
| !Ref (parameter) | Pulumi config |
| !GetAtt Resource.Attr | Resource property output |
| !Sub "..." | pulumi.interpolate |
| !Join [delim, [...]] | pulumi.interpolate or .apply() |
| !If [cond, true, false] | Ternary operator |
| !Equals [a, b] | === comparison |
| !Select [idx, list] | Array indexing with .apply() |
| !Split [delim, str] | .apply(v => v.split(...)) |
| Fn::ImportValue | Stack references or config |
// CloudFormation: !Sub "arn:aws:s3:::${MyBucket}/*"
// Pulumi:
const bucketArn = pulumi.interpolate`arn:aws:s3:::${myBucket.bucket}/*`;
// CloudFormation: !GetAtt MyFunction.Arn
// Pulumi:
const functionArn = myFunction.arn;
Convert CloudFormation conditions to TypeScript logic:
// CloudFormation:
// "Conditions": {
// "CreateProdResources": { "Fn::Equals": [{ "Ref": "Environment" }, "prod"] }
// }
// Pulumi:
const config = new pulumi.Config();
const environment = config.require("environment");
const createProdResources = environment === "prod";
if (createProdResources) {
// Create production-only resources
}
Convert parameters to Pulumi config:
// CloudFormation:
// "Parameters": {
// "InstanceType": { "Type": "String", "Default": "t3.micro" }
// }
// Pulumi:
const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";
Convert mappings to TypeScript objects:
// CloudFormation:
// "Mappings": {
// "RegionMap": {
// "us-east-1": { "AMI": "ami-12345" },
// "us-west-2": { "AMI": "ami-67890" }
// }
// }
// Pulumi:
const regionMap: Record<string, { ami: string }> = {
"us-east-1": { ami: "ami-12345" },
"us-west-2": { ami: "ami-67890" },
};
const ami = regionMap[aws.config.region!].ami;
CloudFormation Custom Resources (AWS::CloudFormation::CustomResource or Custom::*) require special handling:
aws-native outputs often include undefined. Avoid ! non-null assertions. Always safely unwrap with .apply():
// WRONG
functionName: lambdaFunction.functionName!,
// CORRECT
functionName: lambdaFunction.functionName.apply(name => name || ""),
After conversion, import existing resources to be managed by Pulumi.
Before proceeding with import, verify your code:
aws-nativeaws (classic) provider must be justifiedBecause you used CloudFormation Logical IDs as resource names, you can use the cdk-importer tool to automatically import resources.
Follow cfn-importer.md for detailed import procedures.
For resources that fail automatic import:
pulumi import:pulumi import <pulumi-resource-type> <logical-id> <import-id>
After import, run pulumi preview. There must be:
If there are changes, investigate and update the program until preview is clean.
When performing a migration, always produce:
| CloudFormation Logical ID | CFN Type | Pulumi Type | Provider |
|---------------------------|----------|-------------|----------|
| MyAppBucketABC123 | AWS::S3::Bucket | aws-native.s3.Bucket | aws-native |
| MyLambdaFunction456 | AWS::Lambda::Function | aws-native.lambda.Function | aws-native |
Fetch content from official Pulumi documentation:
tools
Implements advanced TypeScript type systems, creates custom type guards, utility types, and branded types, and configures tRPC for end-to-end type safety. Use when building TypeScript applications requiring advanced generics, conditional or mapped types, discriminated unions, monorepo setup, or full-stack type safety with tRPC.
development
Use when challenging ideas, plans, decisions, or proposals using structured critical reasoning. Invoke to play devil's advocate, run a pre-mortem, red team, or audit evidence and assumptions.
development
Systematic technical debt analysis across architecture, testing, documentation, and infrastructure. Investigates the codebase, scores findings by impact and effort, and generates a prioritized TECH_DEBT.md remediation plan. Delegates to specialized skills for code quality (scout) and linting (lint-fix). Use when assessing overall project health, planning cleanup sprints, or onboarding to an unfamiliar codebase.
testing
Install and configure Tailscale across platforms. Detects OS, distro, and environment (including WSL2 and containers). Verifies existing installations, performs platform-appropriate install, and guides initial connection. Use when setting up Tailscale on a new machine, onboarding a server to a tailnet, or verifying an existing install.