npx skills add ...
npx skills add pulumi/agent-skills --skill cloudformation-to-pulumi
npx skills add pulumi/agent-skills --skill cloudformation-to-pulumi
Convert, migrate, or import AWS CloudFormation stacks or templates into Pulumi programs. Load this skill whenever a user wants to move from CloudFormation to Pulumi, convert a CFN template, import existing CloudFormation-managed resources into Pulumi, or asks about CloudFormation-to-Pulumi migration in any form. Also load when the user mentions cdk-importer in a migration context.
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:
List all resources in the stack:
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.
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 |
Convert CloudFormation conditions to TypeScript logic:
Convert parameters to Pulumi config:
Convert mappings to TypeScript objects:
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():
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: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:
aws cloudformation list-stack-resources \
--region <region> \
--stack-name <stack-name> \
--output json// 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", { ... });// 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;// 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
}// CloudFormation:
// "Parameters": {
// "InstanceType": { "Type": "String", "Default": "t3.micro" }
// }
// Pulumi:
const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";// 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;// WRONG
functionName: lambdaFunction.functionName!,
// CORRECT
functionName: lambdaFunction.functionName.apply(name => name || ""),pulumi import <pulumi-resource-type> <logical-id> <import-id>