Migrating from Terraform or CDKTF to Pulumi
If your infrastructure was provisioned with Terraform or the CDK for Terraform (CDKTF), there are a number of options that will help you adopt Pulumi.
- Coexist with resources provisioned by Terraform or CDKTF by referencing a
.tfstatefile. - Import existing resources into Pulumi in the usual way or using
pulumi convert --from terraformalong withpulumi import --from terraformto adopt all resources from an existing.tfstatefile. - Convert any Terraform HCL to Pulumi code using
pulumi convert --from terraform. - Use Terraform Modules directly within your Pulumi programs through the Terraform Module feature.
This range of techniques helps to either temporarily or permanently use Pulumi alongside Terraform, in addition to fully migrating existing infrastructure to Pulumi.
Referencing Terraform State
Pulumi allows you to reference output values from existing Terraform state files, enabling you to build new infrastructure that depends on resources provisioned with Terraform. This capability is particularly useful for:
- Organizations with existing Terraform infrastructure where the cost of migration isn’t justified
- Teams transitioning gradually from Terraform or CDKTF to Pulumi
- Scenarios where some infrastructure must remain under management by Terraform due to organizational constraints
- Accessing shared infrastructure (like VPCs, networks, or databases) managed by other teams
You can use the Terraform provider functions to reference output values from a Terraform state source:
- For local state files, use
terraform.state.getLocalReference - For state files stored in Terraform Cloud or Terraform Enterprise, use
terraform.state.getRemoteReference
The following code reads VPC and subnet IDs from a local terraform.tfstate file and provisions an EKS cluster that uses the read IDs:
import * as pulumi from "@pulumi/pulumi";
import * as terraform from "@pulumi/terraform";
import * as eks from "@pulumi/eks";
const tfState = terraform.state.getLocalReferenceOutput({
path: "../terraform/terraform.tfstate",
});
const vpcId = tfState.outputs["vpc_id"] as pulumi.Output<string>;
const publicSubnetIds = tfState.outputs["public_subnet_ids"] as pulumi.Output<string[]>;
const privateSubnetIds = tfState.outputs["private_subnet_ids"] as pulumi.Output<string[]>;
const cluster = new eks.Cluster("my-cluster", {
vpcId: vpcId,
publicSubnetIds: publicSubnetIds,
privateSubnetIds: privateSubnetIds,
});
import pulumi
import pulumi_terraform as terraform
import pulumi_eks as eks
tf_state = terraform.state.get_local_reference_output(
path="../terraform/terraform.tfstate"
)
vpc_id = tf_state.outputs["vpc_id"]
public_subnet_ids = tf_state.outputs["public_subnet_ids"]
private_subnet_ids = tf_state.outputs["private_subnet_ids"]
cluster = eks.Cluster("my-cluster",
vpc_id=vpc_id,
public_subnet_ids=public_subnet_ids,
private_subnet_ids=private_subnet_ids
)
package main
import (
"github.com/pulumi/pulumi-eks/sdk/v2/go/eks"
"github.com/pulumi/pulumi-terraform/sdk/v6/go/terraform/state"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
tfState := state.GetLocalReferenceOutput(ctx, state.GetLocalReferenceOutputArgs{
Path: pulumi.String("../terraform/terraform.tfstate"),
})
outputs := tfState.Outputs()
vpcId := outputs.ApplyT(func(outputs map[string]interface{}) string {
return outputs["vpc_id"].(string)
}).(pulumi.StringOutput)
publicSubnetIds := outputs.ApplyT(func(outputs map[string]interface{}) []string {
ids := outputs["public_subnet_ids"].([]interface{})
result := make([]string, len(ids))
for i, id := range ids {
result[i] = id.(string)
}
return result
}).(pulumi.StringArrayOutput)
privateSubnetIds := outputs.ApplyT(func(outputs map[string]interface{}) []string {
ids := outputs["private_subnet_ids"].([]interface{})
result := make([]string, len(ids))
for i, id := range ids {
result[i] = id.(string)
}
return result
}).(pulumi.StringArrayOutput)
_, err := eks.NewCluster(ctx, "my-cluster", &eks.ClusterArgs{
VpcId: vpcId,
PublicSubnetIds: publicSubnetIds,
PrivateSubnetIds: privateSubnetIds,
})
if err != nil {
return err
}
return nil
})
}
using System.Linq;
using System.Collections.Immutable;
using Pulumi;
using Pulumi.Terraform.State;
using Pulumi.Eks;
return await Deployment.RunAsync(() =>
{
var tfState = GetLocalReference.Invoke(new GetLocalReferenceInvokeArgs
{
Path = "../terraform/terraform.tfstate"
});
var vpcId = tfState.Apply(state => (string)state.Outputs["vpc_id"]);
var publicSubnetIds = tfState.Apply(state =>
((ImmutableArray<object>)state.Outputs["public_subnet_ids"])
.Select(id => (string)id)
.ToArray());
var privateSubnetIds = tfState.Apply(state =>
((ImmutableArray<object>)state.Outputs["private_subnet_ids"])
.Select(id => (string)id)
.ToArray());
var cluster = new Cluster("my-cluster", new ClusterArgs
{
VpcId = vpcId,
PublicSubnetIds = publicSubnetIds,
PrivateSubnetIds = privateSubnetIds
});
});
import com.pulumi.Pulumi;
import com.pulumi.terraform.state.inputs.GetLocalReferenceArgs;
import com.pulumi.terraform.state.StateFunctions;
import com.pulumi.eks.Cluster;
import com.pulumi.eks.ClusterArgs;
import java.util.List;
import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
Pulumi.run(ctx -> {
var tfState = StateFunctions.getLocalReference(GetLocalReferenceArgs.builder()
.path("../terraform/terraform.tfstate")
.build());
var vpcId = tfState.applyValue(state -> (String) state.outputs().get("vpc_id"));
var publicSubnetIds = tfState.applyValue(state -> {
@SuppressWarnings("unchecked")
List<Object> ids = (List<Object>) state.outputs().get("public_subnet_ids");
return ids.stream()
.map(id -> (String) id)
.collect(Collectors.toList());
});
var privateSubnetIds = tfState.applyValue(state -> {
@SuppressWarnings("unchecked")
List<Object> ids = (List<Object>) state.outputs().get("private_subnet_ids");
return ids.stream()
.map(id -> (String) id)
.collect(Collectors.toList());
});
var cluster = new Cluster("my-cluster", ClusterArgs.builder()
.vpcId(vpcId)
.publicSubnetIds(publicSubnetIds)
.privateSubnetIds(privateSubnetIds)
.build());
});
}
}
name: my-terraform-state-example
runtime: yaml
variables:
tfState:
fn::invoke:
function: terraform:state:getLocalReference
arguments:
path: "../terraform/terraform.tfstate"
resources:
my-cluster:
type: eks:Cluster
properties:
vpcId: ${tfState.outputs["vpc_id"]}
publicSubnetIds: ${tfState.outputs["public_subnet_ids"]}
privateSubnetIds: ${tfState.outputs["private_subnet_ids"]}
Converting Terraform HCL to Pulumi
The Pulumi CLI can convert existing Terraform source code written in the HashiCorp Configuration Language (HCL) into Pulumi source code using the pulumi convert command.
If you’re coming to Pulumi from CDKTF, you can generate the HCL for the stacks in your project with cdktf synth:
cdktf synth --hcl
This produces a single HCL file for each stack at ./cdktf.out/stacks/<stack-name>/cdk.tf.
Using the Converter
To use the converter, first install Pulumi, then change to a folder containing the HCL source files you’d like to convert. Next, run pulumi convert --from terraform from within that folder:
pulumi convert --from terraform --language typescript
pulumi convert --from terraform --language python
pulumi convert --from terraform --language go
pulumi convert --from terraform --language csharp
This will generate a Pulumi program that when run with pulumi up will deploy the infrastructure originally described by the Terraform project. Note that if your infrastructure references files or directories with paths relative to the location of the Terraform project, you will most likely need to update these paths such that they are relative to the generated index.jsindex.ts__main__.pymain.goProgram.csProgram.fsProgram.vbApp.javaPulumi.yaml
Supported Terraform Features
The following major features are supported:
- Variables, outputs, resources, and data sources
- Terraform modules, which are converted to Pulumi components
- Almost all HCL2 expression syntax
In cases where the converter does not yet support a certain feature, the pulumi convert command succeeds, but generates a TODO in the form of a call to a notImplementednot_implementednotImplementedNotImplemented
If you notice a feature that’s not yet implemented or you encounter a bug, please consider filing an issue.
Importing Resources
The convert command translates static HCL source code into Pulumi program code. Often, however, you’ll also need to import existing resource state from your Terraform or CDKTF project in order to begin managing those resources with Pulumi.
To do so, you can use pulumi import --from terraform:
pulumi import --from terraform ./terraform.tfstate
Given a path to a valid .tfstate file and a target Pulumi stack, Pulumi will import the resources defined in that file into the stack and mark them protected to allow you to make follow-up changes to their source code safely. You can also import resources individually using the import resource option.
To learn more about importing resources with Pulumi, see Importing Resources.
Conversion Examples
To help make migration from Terraform and CDKTF more approachable, we’ve prepared the following examples for reference:
- Converting Full Terraform Programs to Pulumi: A blog post that covers the process of converting a real-world Terraform codebase
- Migrating from CDKTF to Pulumi: An end-to-end example that covers converting and importing a multi-stack CDKTF project
Using Terraform Modules Directly
Pulumi allows you to use existing Terraform modules directly in your Pulumi programs without converting or rewriting them. This feature is particularly useful for:
- Organizations with significant investment in custom Terraform modules
- Teams that want to leverage the vast ecosystem of modules in the Terraform Registry
- Gradual migration scenarios where some teams continue using Terraform while others adopt Pulumi
- Maintaining consistency across infrastructure while transitioning between tools
Adding a Terraform Module to Your Pulumi Project
To use a Terraform module in Pulumi, you can add it to your project using the pulumi package add command:
pulumi package add terraform-module <module-source> [<version>] <pulumi-package-name>
For example, to add the AWS VPC module from the Terraform Registry:
pulumi package add terraform-module terraform-aws-modules/vpc/aws 5.19.0 vpc
This will generate a local SDK in your programming language that you can import into your Pulumi program. You can then use this module like any other Pulumi package:
import * as vpc from "@pulumi/vpc";
// Create a VPC using the terraform-aws-modules/vpc module
const myVpc = new vpc.Module("my-vpc", {
name: "pulumi-vpc",
cidr: "10.0.0.0/16",
azs: ["us-west-2a", "us-west-2b", "us-west-2c"],
private_subnets: ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"],
public_subnets: ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"],
enable_nat_gateway: true
});
// Access outputs from the module
export const vpcId = myVpc.vpc_id;
import pulumi
import pulumi_vpc as vpc
# Create a VPC using the terraform-aws-modules/vpc module
my_vpc = vpc.Module("my-vpc",
name="pulumi-vpc",
cidr="10.0.0.0/16",
azs=["us-west-2a", "us-west-2b", "us-west-2c"],
private_subnets=["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"],
public_subnets=["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"],
enable_nat_gateway=True
)
# Access outputs from the module
pulumi.export("vpc_id", my_vpc.vpc_id)
package main
import (
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
vpc "github.com/pulumi/pulumi-vpc/sdk/go/vpc"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// Create a VPC using the terraform-aws-modules/vpc module
myVpc, err := vpc.NewModule(ctx, "my-vpc", &vpc.ModuleArgs{
Name: pulumi.String("pulumi-vpc"),
Cidr: pulumi.String("10.0.0.0/16"),
Azs: pulumi.StringArray{
pulumi.String("us-west-2a"),
pulumi.String("us-west-2b"),
pulumi.String("us-west-2c"),
},
PrivateSubnets: pulumi.StringArray{
pulumi.String("10.0.1.0/24"),
pulumi.String("10.0.2.0/24"),
pulumi.String("10.0.3.0/24"),
},
PublicSubnets: pulumi.StringArray{
pulumi.String("10.0.101.0/24"),
pulumi.String("10.0.102.0/24"),
pulumi.String("10.0.103.0/24"),
},
EnableNatGateway: pulumi.Bool(true),
})
if err != nil {
return err
}
// Access outputs from the module
ctx.Export("vpc_id", myVpc.VpcId)
return nil
})
}
using Pulumi;
using Vpc = Pulumi.Vpc;
class MyStack : Stack
{
public MyStack()
{
// Create a VPC using the terraform-aws-modules/vpc module
var myVpc = new Vpc.Module("my-vpc", new Vpc.ModuleArgs
{
Name = "pulumi-vpc",
Cidr = "10.0.0.0/16",
Azs = new[] { "us-west-2a", "us-west-2b", "us-west-2c" },
PrivateSubnets = new[] { "10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24" },
PublicSubnets = new[] { "10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24" },
EnableNatGateway = true
});
// Access outputs from the module
this.VpcId = myVpc.VpcId;
}
[Output]
public Output<string> VpcId { get; set; }
}
This feature also works seamlessly with local Terraform modules:
pulumi package add terraform-module ./path/to/module mylocalmod
For more information about using Terraform modules directly in Pulumi, see the Use a Terraform Module in Pulumi guide.
Thank you for your feedback!
If you have a question about how to use Pulumi, reach out in Community Slack.
Open an issue on GitHub to report a problem or suggest an improvement.
