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.
Install with Tessl CLI
npx tessl i github:pulumi/agent-skills --skill cloudformation-to-pulumi90
Does it follow best practices?
If you maintain this skill, you can automatically optimize it using the tessl CLI to improve its score:
npx tessl skill review --optimize ./path/to/skillValidation for skill structure
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.jsonList all resources in the stack:
aws cloudformation list-stack-resources \
--region <region> \
--stack-name <stack-name> \
--output jsonThis 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:
b6b942f
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.