By David Mattia
December 15, 2021ā¢3 min read
When you useĀ Infrastructure as CodeĀ tools like Terraform, Pulumi, or the AWS CDK, youāll doubtlessly need to have dependencies between your stacks/modules.
This can be illustrated by a small example:
In this example, we have a piece of code that creates a Virtual Private Cloud (VPC). The code that creates our backend application and database needs to depend on the VPCs private subnet identifiers, so that we can place our backend and database inside the VPC. Likewise, our backend code will need to depend on our database code so that it can create a connection and talk to the database.
Each tool handles this scenario in a slightly different way: Terraform handles module dependencies viaĀ data source lookupsĀ orĀ Terragrunt dependencies, Pulumi handles stack dependencies viaĀ StackReferences, and the AWS CDK handles stack dependencies viaĀ referencing CloudFormation exports.
Each of these tools makes it easy to have multiple stacks that all reference each other. This blog post will focus on how to use Terraform modules and Pulumi stacks interchangeably, with each having the ability to consume the others outputs.
This opens up two potential outcomes for your company:
Letās explore how each tool can natively read the otherās outputs.
If you are replacing a Terraform module that depends on other Terraform modules, your new Pulumi stack will need to be able to reference the dependencyās terraform outputs. Pulumi already wrote aĀ really nice blogĀ on how to do this, so weāll keep this section nice and short.
When you made your Pulumi module, you had to write your statefile to a specificĀ backend state. In Pulumi, using Typescript, you can use theĀ @pulumi/terraformĀ package on NPM to reference the Terraform moduleās statefile to consume its outputs. If you are using a different language in Pulumi, there should be a similar tool to grab Terraform state.
As some quick examples, hereās a way to grab a Terraform state output value if you chose the S3 backend:
import * as tf from "@pulumi/terraform"; const remoteState = new tf.state.RemoteStateReference("s3state", { backendType: "s3", bucket: "pulumi-terraform-state-test", key: "test/terraform.tfstate", region: "us-west-2" }); // Use the getOutput function on the resource to access root outputs 11const vpcId= remoteState.getOutput("vpc_id");
And hereās a way to grab a Terraform state output value from a Terraform Enterprise backend:
import * as pulumi from "@pulumi/pulumi"; import * as tf from "@pulumi/terraform"; const config = new pulumi.Config(); const ref = new tf.state.RemoteStateReference("remote", { backendType: "remote", organization: "pulumi", token: config.requireSecret("terraformEnterpriseToken"), workspaces: { name: "test-state-file" } }); // Use the getOutput function on the resource to access root outputs const vpcId= remoteState.getOutput("vpc_id");
Note:Ā These examples are borrowed from theĀ @pulumi/terraformĀ documentation as of the time of this writing, but you should check the official docs yourself in case there are any API changes.
And just like that, your Pulumi stacks can depend on your Terraform modules.
If you are replacing a Terraform module that other Terraform modules depend on, youāll need to update those to consume your Pulumi stackās outputs.
This direction was a bit tougher for us, as Terraform has no built-in way to consume Pulumi outputs. We started out by trying some paths that worked, but we werenāt thrilled about, like having Pulumi write outĀ SSM parametersĀ orĀ Vault secretsĀ that Terraform could read in via data sources.
But those solutions were unideal because:
Our solution was to createĀ an open source Terraform providerĀ that can fetch and parse Pulumi statefiles. This provider offersĀ a data sourceĀ namedĀ pulumi_stack_outputsĀ that will fetch a Pulumi statefile, parse the outputs, and then supply them in Terraform as a nativeĀ map(string)Ā object.
In practice, it will often look something like this:
terraform { required_providers { pulumi = { version = "0.0.2" source = "hashicorp.com/transcend-io/pulumi" } } } provider "pulumi" {} data "pulumi_stack_outputs" "stack_outputs" { organization = "transcend-io" project = "airgap-telemetry-backend" stack = "dev" } output "stack_outputs" { value = data.pulumi_stack_outputs.stack_outputs.stack_outputs }
This code block tells Terraform to use theĀ 0.0.2Ā version of theĀ pulumiĀ Terraform provider fromĀ transcend-io. It then declares theĀ provider, where you can either supply a Pulumi cloud token directly, or via theĀ PULUMI_ACCESS_TOKENĀ environment variable.
Lastly, it looks up theĀ airgap-telemetry-backend/devĀ stack under theĀ transcend-ioĀ organization and allows Terraform to consume that stackās outputs. Youāll want to use your own organization, project, and stack names here, of course.
And with that, your Terraform modules can now depend on your Pulumi Cloud stacks.
One of Pulumiās biggest strengths is that it has support for multiple programming languages that can be used together. If you have one Pulumi stack written in golang and another written in Typescript, one module could easily depend on the other via aĀ StackReference.
With the tooling described in this post, you can essentially make Terraform one more language in your Pulumi toolchain if youād like to. Or if you want to think of it as having many more languages in your Terraform toolchain, thatās cool too. Either way, the two tools can seamlessly work together in a way where it almost feels like they are all a part of one ecosystem.
At Transcend, this meant that we started by taking some of our more commonly updated Terraform modules that were painful to maintain and converted them over to Pulumi. This way, we could more easily manage things like multi-region deployments where Pulumi is easier to work with than Terraform.
As we did this though, we left the other Terraform files alone, as they can coexist and pose no real problems to us as a business. Just for ease-of-onboarding and consistency, we hope to eventually use one tool, though if some particular team ever decides they want to use golang, python, or HCL, we know itāll be easy to support them.
By David Mattia