1. Docs
  2. Pulumi IaC
  3. Adopting Pulumi
  4. Migrate from...
  5. AWS CloudFormation

Migrating from AWS CloudFormation to Pulumi

    If your team has already provisioned infrastructure using AWS CloudFormation, and you’d like to adopt Pulumi, you have three primary strategies you can take:

    • Coexist with resources provisioned by CloudFormation by referencing stack outputs.
    • Import existing resources into Pulumi in the usual way.
    • Convert your deployments to use Pulumi and then incrementally migrate resources.

    Referencing Stack Outputs

    It is possible to reference existing AWS CloudFormation stacks from your program. It doesn’t matter how these stacks were created. This lets you read properties of that CloudFormation stack for use within your Pulumi program. This includes output values computed from resources provisioned that stack.

    For instance, let’s say your infrastructure team has provisioned your network infrastructure using CloudFormation and you need to use the Subnet ID to provision something new from your Pulumi program. One approach is to hardcode that ID but this is brittle and, if it ever changes, you’d need to go and manually update the hardcoded value.

    Instead, you can look up that stack by name and use one of its output values. The following example reads an AWS CloudFormation stack named my-network-stack and then uses the exported SubnetId value to provision a brand new EC2 instance that runs in that subnet:

    let aws = require("@pulumi/aws");
    
    let network = aws.cloudformation.getStackOutput({
        name: "my-network-stack",
    });
    
    let subnetId = network.outputs["SubnetId"];
    
    let web = new aws.ec2.Instance("web", {
        ami: "ami-0adc0e3ef2558cb1f", // us-west-2 AMI
        instanceType: "t3.micro",
        subnetId: subnetId,
    });
    
    import * as aws from "@pulumi/aws";
    
    const network = aws.cloudformation.getStackOutput({
        name: "my-network-stack",
    });
    
    const subnetId = network.outputs["SubnetId"];
    
    const web = new aws.ec2.Instance("web", {
        ami: "ami-0adc0e3ef2558cb1f", // us-west-2 AMI
        instanceType: "t3.micro",
        subnetId: subnetId,
    });
    
    import pulumi_aws as aws
    
    network = aws.cloudformation.get_stack(
        name='my-network-stack'
    )
    
    subnet_id = network.outputs['SubnetId']
    
    web = aws.ec2.Instance('web',
        ami='ami-0adc0e3ef2558cb1f', # us-west-2 AMI
        instance_type='t2.micro',
        subnet_id=subnet_id
    )
    
    package main
    
    import (
    	"github.com/pulumi/pulumi-aws/sdk/v4/go/aws/cloudformation"
    	"github.com/pulumi/pulumi-aws/sdk/v4/go/aws/ec2"
    	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
    	pulumi.Run(func(ctx *pulumi.Context) error {
    		network := cloudformation.LookupStackOutput(ctx, cloudformation.LookupStackOutputArgs{
    			Name: pulumi.String("my-network-stack"),
    		})
    
    		subnetID := network.Outputs().MapIndex(pulumi.String("SubnetId"))
    
    		web, err := ec2.NewInstance(ctx, "web", &ec2.InstanceArgs{
    			Ami:          pulumi.String("ami-0adc0e3ef2558cb1f"), // us-west-2 AMI
    			InstanceType: pulumi.String("t2.micro"),
    			SubnetId:     subnetID,
    		})
    		if err != nil {
    			return err
    		}
    
    		ctx.Export("publicIp", web.PublicIp)
    		return nil
    	})
    }
    
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    using Pulumi;
    using CloudFormation = Pulumi.Aws.CloudFormation;
    using Ec2 = Pulumi.Aws.Ec2;
    
    class Program
    {
        static Task<int> Main()
        {
            return Deployment.RunAsync(async () =>
            {
                var network = CloudFormation.GetStack.InvokeAsync(new CloudFormation.GetStackArgs
                {
                    Name = "my-network-stack",
                });
    
                var subnetId = (string)network.Outputs["SubnetId"];
    
                var web = new Ec2.Instance("web", new Ec2.InstanceArgs
                {
                    Ami = "ami-0adc0e3ef2558cb1f", // us-west-2 AMI
                    InstanceType = "t3.micro",
                    SubnetId = subnetId,
                });
    
                return new Dictionary<string, object?>();
            });
        }
    }
    

    All we need to do is run pulumi up and the Pulumi runtime will know how to query the CloudFormation stack and retrieve its output values. In this case, the CloudFormation stack is treated entirely as read-only, and Pulumi will never attempt to modify it or any resources managed by it.

    Although we’ve hard-coded the AWS CloudFormation stack name here — "my-network-stack" — it’s common to dynamically compute a name using unique per-stack information, like the stack name, AWS region, or other configuration variables.

    Converting Stacks and Resources

    Let’s say you want to migrate from AWS CloudFormation to Pulumi and that simply co-existing side-by-side as shown above isn’t sufficient. There are two approaches:

    1. Deploy your CloudFormation stack using Pulumi.
    2. Migrate resources from your CloudFormation stack to Pulumi code.

    Depending on what you’re trying to accomplish, you may prefer to start with (1) and move to (2) incrementally, or simply jump straight to (2) directly.

    Deploy Stacks Using Pulumi

    The Pulumi AWS package provides a CloudFormation Stack resource type. Using this type, you can deploy an existing AWS CloudFormation template written in YAML or JSON.

    For instance, this code deploys a simple CloudFormation template using the given parameters, and exports the resulting VPC ID:

    let aws = require("@pulumi/aws");
    
    let template = `{
        "Parameters" : {
            "VPCCidr" : {
                "Type" : "String",
                "Default" : "10.0.0.0/16",
                "Description" : "Enter the CIDR block for the VPC. Default is 10.0.0.0/16."
            }
        },
        "Resources": {
            "myVpc": {
                "Type" : "AWS::EC2::VPC",
                "Properties" : {
                    "CidrBlock" : { "Ref" : "VPCCidr" },
                    "Tags" : [
                        {"Key": "Name", "Value": "Primary_CF_VPC"}
                    ]
                }
            }
        },
        "Outputs": {
            "VpcId": {
                "Value": { "Ref": "myVpc" }
            }
        }
    }
    `;
    
    let network = new aws.cloudformation.Stack("network", {
        templateBody: template,
        parameters: {
            VPCCidr: "10.0.0.0/16",
        },
    });
    
    module.exports = {
        vpcId: network.outputs["VpcId"],
    };
    
    import * as aws from "@pulumi/aws";
    
    const template = `{
        "Parameters" : {
            "VPCCidr" : {
                "Type" : "String",
                "Default" : "10.0.0.0/16",
                "Description" : "Enter the CIDR block for the VPC. Default is 10.0.0.0/16."
            }
        },
        "Resources": {
            "myVpc": {
                "Type" : "AWS::EC2::VPC",
                "Properties" : {
                    "CidrBlock" : { "Ref" : "VPCCidr" },
                    "Tags" : [
                        {"Key": "Name", "Value": "Primary_CF_VPC"}
                    ]
                }
            }
        },
        "Outputs": {
            "VpcId": {
                "Value": { "Ref": "myVpc" }
            }
        }
    }
    `;
    
    const network = new aws.cloudformation.Stack("network", {
        templateBody: template,
        parameters: {
            VPCCidr: "10.0.0.0/16",
        },
    });
    
    export const vpcId = network.outputs["VpcId"];
    
    import pulumi
    import pulumi_aws as aws
    
    template = """{
        "Parameters" : {
            "VPCCidr" : {
                "Type" : "String",
                "Default" : "10.0.0.0/16",
                "Description" : "Enter the CIDR block for the VPC. Default is 10.0.0.0/16."
            }
        },
        "Resources": {
            "myVpc": {
                "Type" : "AWS::EC2::VPC",
                "Properties" : {
                    "CidrBlock" : { "Ref" : "VPCCidr" },
                    "Tags" : [
                        {"Key": "Name", "Value": "Primary_CF_VPC"}
                    ]
                }
            }
        },
        "Outputs": {
            "VpcId": {
                "Value": { "Ref": "myVpc" }
            }
        }
    }
    """
    
    network = aws.cloudformation.Stack('network',
        template_body=template,
        parameters={
            'VPCCidr': '10.0.0.0/16',
        },
    )
    
    pulumi.export('vpc_id', network.outputs["VpcId"])
    
    package main
    
    import (
        "github.com/pulumi/pulumi-aws/sdk/v4/go/aws/cloudformation"
        "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    const (
        template = `{
        "Parameters" : {
            "VPCCidr" : {
                "Type" : "String",
                "Default" : "10.0.0.0/16",
                "Description" : "Enter the CIDR block for the VPC. Default is 10.0.0.0/16."
            }
        },
        "Resources": {
            "myVpc": {
                "Type" : "AWS::EC2::VPC",
                "Properties" : {
                    "CidrBlock" : { "Ref" : "VPCCidr" },
                    "Tags" : [
                        {"Key": "Name", "Value": "Primary_CF_VPC"}
                    ]
                }
            }
        },
        "Outputs": {
            "VpcId": {
                "Value": { "Ref": "myVpc" }
            }
        }
    }
    )
    
    func main() {
        pulumi.Run(func(ctx *pulumi.Context) error {
            network, err := cloudformation.NewStack(ctx, "network", &cloudformation.StackArgs{
                TemplateBody: pulumi.String(template),
                Parameters: pulumi.Map{
                    "VPCCidr": pulumi.String("10.0.0.0/16"),
                },
            })
            if err != nil {
                return err
            }
    
            ctx.Export("vpcId", network.Outputs.MapIndex(pulumi.String("VpcId")))
            return nil
        })
    }
    
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    using Pulumi;
    using CloudFormation = Pulumi.Aws.CloudFormation;
    
    class Program
    {
        static Task<int> Main()
        {
            return Deployment.RunAsync(() => {
                var template = @"{
        ""Parameters"" : {
            ""VPCCidr"" : {
                ""Type"" : ""String"",
                ""Default"" : ""10.0.0.0/16"",
                ""Description"" : ""Enter the CIDR block for the VPC. Default is 10.0.0.0/16.""
            }
        },
        ""Resources"": {
            ""myVpc"": {
                ""Type"" : ""AWS::EC2::VPC"",
                ""Properties"" : {
                    ""CidrBlock"" : { ""Ref"" : ""VPCCidr"" },
                    ""Tags"" : [
                        {""Key"": ""Name"", ""Value"": ""Primary_CF_VPC""}
                    ]
                }
            }
        },
        ""Outputs"": {
            ""VpcId"": {
                ""Value"": { ""Ref"": ""myVpc"" }
            }
        }
    }";
    
                var network = new CloudFormation.Stack("network", new CloudFormation.StackArgs
                {
                    TemplateBody = template,
                    Parameters = new Dictionary<string, object?>
                    {
                        { "VPCCidr", "10.0.0.0/16" },
                    },
                });
    
                return new Dictionary<string, object?>
                {
                    { "vpcId", network.Outputs.Apply(nw => nw["VpcId"]) },
                };
            });
        }
    }
    

    We could have just as well read the template off disk, instead of putting it right in the source code.

    After deploying this, Pulumi will see the CloudFormation stack as an opaque single resource. That is, it won’t assume control of individual resources inside of the stack. For that, you will need to migrate and/or import individual resources, per the following section:

    $ pulumi up
    Updating (dev):
         Type                         Name            Status
     +   pulumi:pulumi:Stack          aws-cfn-dev
     +   └─ aws:cloudformation:Stack  network         created
    
    Outputs:
     + vpcId: "vpc-0e1a74859af1da17f"
    
    Resources:
     + 2 created
    

    From here, you can change the template body and/or surrounding code, rerun pulumi up, and the right incremental changes will take place.

    Migrate Resources into Code

    Now let’s see how to actually migrate your CloudFormation resources fully to Pulumi. This requires rewriting the CloudFormation JSON or YAML as real code, either entirely, or one resource at a time. Because you can query stack outputs and provide stack parameters in code, you can more easily intermingle CloudFormation-managed resources alongside Pulumi ones. Cyclic dependencies, of course, cannot be expressed, since the CloudFormation stack is seen as one opaque resource to Pulumi.

    Because Pulumi’s AWS resource model doesn’t match CloudFormation’s resource projections exactly, there is no tool currently available to automate this translation. A good approach is to copy the CloudFormation template definition into your code and then rewrite it to your language of choice, translating resource and property names as appropriate.

    Note that you can always skip the intermediate step of deploying your CloudFormation stack using Pulumi and go straight to migrating your resources. For large stacks, however, doing this in multiple incremental steps can help minimize disruption and allow you to do this migration more slowly over time.

    Our example below will result in a Pulumi program that creates a VPC definition identical to the above CloudFormation stack example. The example will also use import in conjunction with CloudFormation’s “retain” deletion policy to adopt resources on-the-fly from CloudFormation to Pulumi rather than recreating them.

    Before replacing the CloudFormation stack, we must first update the stack definition so that the VPC’s set to “retain” upon deletion. Since we’ll be replacing its definition with our Pulumi equivalent, we need to set this to ensure CloudFormation doesn’t delete the VPC during the adoption process:

    {
        "Parameters" : {
            "VPCCidr" : {
                "Type" : "String",
                "Default" : "10.0.0.0/16",
                "Description" : "Enter the CIDR block for the VPC. Default is 10.0.0.0/16."
            }
        },
        "Resources": {
            "myVpc": {
                "Type" : "AWS::EC2::VPC",
    +           "DeletionPolicy": "Retain",
                "Properties" : {
                    "CidrBlock" : { "Ref" : "VPCCidr" },
                    "Tags" : [
                        {"Key": "Name", "Value": "Primary_CF_VPC"}
                    ]
                }
            }
        },
        "Outputs": {
            "VpcId": {
                "Value": { "Ref": "myVpc" }
            }
        }
    }
    

    Remember to run pulumi up so that your changes are applied before moving on.

    Next, we can adopt the resources under Pulumi’s control, by using import IDs. For this example, recall that our VPC ID from above was "vpc-0e1a74859af1da17f", which is what we will use for illustration purposes. Also, in this example, there is just one resource, so we can simply delete the CloudFormation stack in its entirety and replace it with a Pulumi definition of the VPC. In cases where multiple resources exist, you can delete them one by one, until the stack is eventually empty.

    let aws = require("@pulumi/aws");
    
    let vpc = new aws.ec2.Vpc("myVpc", {
        cidrBlock: "10.0.0.0/16",
        tags: { Name: "Primary_CF_VPC" },
    }, { import: "vpc-0e1a74859af1da17f" });
    
    module.exports = {
        vpcId: vpc.id,
    };
    
    import * as aws from "@pulumi/aws";
    
    const vpc = new aws.ec2.Vpc("myVpc", {
        cidrBlock: "10.0.0.0/16",
        tags: { Name: "Primary_CF_VPC" },
    }, { import: "vpc-0e1a74859af1da17f" });
    
    export const vpcId = vpc.id;
    
    import pulumi
    import pulumi_aws as aws
    
    vpc = aws.ec2.Vpc('myVpc',
        cidr_block='10.0.0.0/16',
        tags={ 'Name': 'Primary_CF_VPC' },
        opts=pulumi.ResourceOptions(import_='vpc-0e1a74859af1da17f')
    )
    
    pulumi.export('vpc_id', vpc.id)
    
    package main
    
    import (
        "github.com/pulumi/pulumi-aws/sdk/v4/go/aws/ec2"
        "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
        pulumi.Run(func(ctx *pulumi.Context) error {
            vpc, err := ec2.NewVpc(ctx, "myVpc",
                &ec2.VpcArgs{
                    CidrBlock: pulumi.String("10.0.0.0/16"),
                    Tags: pulumi.Map{
                        "Name": pulumi.String("Primary_CF_VPC"),
                    },
                },
                pulumi.Import(pulumi.ID("vpc-0e1a74859af1da17f")),
            })
            if err != nil {
                return err
            }
    
            ctx.Export("vpcId", vpc.ID())
            return nil
        })
    }
    
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    using Pulumi;
    using Ec2 = Pulumi.Aws.Ec2;
    
    class Program
    {
        static Task<int> Main()
        {
            return Deployment.RunAsync(() => {
                var vpc = new Ec2.Vpc("myVpc",
                    new Ec2.VpcArgs
                    {
                        CidrBlock = "10.0.0.0/16",
                        Tags = new Dictionary<string, object?>
                        {
                            { "Name", "Primary_CF_VPC" },
                        },
                    },
                    new CustomResourceOptions {
                        ImportId = "vpc-0e1a74859af1da17f",
                    },
                );
    
                return new Dictionary<string, object?>
                {
                    { "vpcId", vpc.Id },
                };
            });
        }
    }
    

    After running pulumi up, your VPC will become under the control of Pulumi without any disruption, and you can then delete the import directives from your code. All subsequent infrastructure changes you’d like to be made can happen within Pulumi instead of AWS CloudFormation.

      PulumiUP 2024. Watch On Demand.