Exploring CDK for Terraform for .NET
Overview
Both AWS CDK and Terraform aim to solve a similar problem: alleviating some of the infrastructure management challenges with code. CDK supports several general-purpose languages, including C#, Python, and TypeScript, while Terraform uses its configuration language called HCL. While CDK can only create AWS resources, Terraform supports virtually every cloud provider, granting the ability to write code to deploy to multiple public clouds at once. Last year, Terraform and AWS announced a project called Terraform for CDK, aiming to grant the best of both worlds (i.e., support for GPLs, multi-cloud, etc.).
Nuances and Limitations
In addition to the programming language features of AWS CDK, there's a construct library with three levels:
- L1 (level one) constructs are representations of CloudFormation resources
- L2 (level two) constructs provide defaults and boilerplate to simplify the code
- Patterns are the highest level and create many resources configured together wrapped in a single construct (e.g., Lambda RESTful API)
While CDK for Terraform utilizes the AWS construct programming model, it does not share the same construct library as CDK. It's important to distinguish that CDK for Terraform stacks only support Terraform providers.
GitHub Repository
You can find a complete working example here.
Installing the Tools and Scaffolding the .NET Solution
The following command line tools are required for getting started:
- Terraform (0.12+)
- Node.js (12.16+)
- AWS CLI (specifically the credentials)
First, install the cdktf
CLI:
npm install -g cdktf-cli
# 0.3
cdktf --version
After that, create the .NET project using the cdktf
CLI:
mkdir resources
cd resources
# the --local flag refers to local Terraform state management
cdktf init --template=csharp --local
This action creates several files, including a cdktf.json
file. Inside this configuration file, specify the AWS provider.
{
"language": "csharp",
"app": "dotnet run -p MyTerraformStack.csproj",
"terraformProviders": ["aws@~> 2.0"],
"terraformModules": [],
"context": {
"excludeStackIdFromLogicalIds": "true",
"allowSepCharsInLogicalIds": "true"
}
}
After adding the provider configuration, generate the provider objects using the following command:
cdktf get
The generated objects are stored in the newly created .gen/
folder. Add this as a reference:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HashiCorp.Cdktf" Version="0.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include=".gen\aws\aws.csproj" />
</ItemGroup>
</Project>
Lastly, initialize the AwsProvider
object in the Main.cs
file.
using System;
using Constructs;
using HashiCorp.Cdktf;
// AWS provider objects generated by cdktf get command
using aws;
namespace MyCompany.MyApp
{
class MyApp : TerraformStack
{
public MyApp(Construct scope, string id) : base(scope, id)
{
// initialize the AWS provider
// located in the .gen/ folder
new AwsProvider(this, "aws", new AwsProviderConfig {
Region = "us-east-1"
});
}
public static void Main(string[] args)
{
App app = new App();
new MyApp(app, "resources");
app.Synth();
Console.WriteLine("App synth complete");
}
}
}
Adding Resources
As noted above, the resources will be created using the Terraform AWS provider. There are corresponding C# classes for each of the AWS resources specified by the provider. While writing code, the AWS provider documentation in conjunction with your IDE's autocomplete functionality is a powerful way to navigate the available resources. For this example, the code looks up the latest AMI for Ubuntu 20.04 and uses it to create an EC2 Instance. Below the AwsProvider
constructor method call in the MyApp
constructor method, add a data source and instance like so:
// initialize the AWS provider
// located in the .gen/ folder
new AwsProvider(this, "aws", new AwsProviderConfig {
Region = "us-east-1"
});
// https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
DataAwsAmi dataAwsAmi = new DataAwsAmi(this, "aws_ami_ubuntu", new DataAwsAmiConfig()
{
MostRecent = true,
Filter = new []
{
new DataAwsAmiFilter()
{
Name = "name",
Values = new [] { "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*" }
},
new DataAwsAmiFilter()
{
Name = "virtualization-type",
Values = new [] { "hvm" }
},
},
Owners = new [] { "099720109477" }
});
// https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance
Instance ec2Instance = new Instance(this, "aws_ec2_instance", new InstanceConfig()
{
Ami = dataAwsAmi.ImageId,
InstanceType = "t3.micro",
});
Note how this functionally behaves the same as the corresponding Terraform HCL with the power of a general-purpose programming language.
Deploying and Managing State
Once finished adding the data source and resource, the project can be built and deployed assuming that the AWS credentials are available (i.e., aws configure
has been run).
dotnet build
cdktf deploy
# when ready
cdktf destroy
The state resides in terraform.resources.tfstate
.