• Overview
    • Enforce Policy as Code
    • Infrastructure as Code
    • Inject Secrets into Terraform
    • Integrate with Existing Workflows
    • Manage Kubernetes
    • Manage Virtual Machine Images
    • Multi-Cloud Deployment
    • Network Infrastructure Automation
    • Terraform CLI
    • Terraform Cloud
    • Terraform Enterprise
  • Registry
  • Tutorials
    • About the Docs
    • Intro to Terraform
    • Configuration Language
    • Terraform CLI
    • Terraform Cloud
    • Terraform Enterprise
    • Provider Use
    • Plugin Development
    • Registry Publishing
    • Integration Program
    • Terraform Tools
    • CDK for Terraform
    • Glossary
  • Community
GitHubTerraform Cloud
Download

    CDK for Terraform

  • Overview
  • Get Started
    • Architecture
    • HCL Interoperability
    • Constructs
    • Providers and Resources
    • Modules
    • Data Sources
    • Variables and Outputs
    • Functions
    • Remote Backends
    • Aspects
    • Assets
    • Tokens
    • Stacks
  • Examples and Guides
    • Project Setup
    • Configuration File
    • Best Practices
    • Environment Variables
    • Remote Templates
    • AWS Adapter [preview]
    • Unit Tests
    • Debugging
    • CLI Configuration
    • Commands
    • Overview
    • Upgrading to Version 0.6
    • Upgrading to Version 0.7
    • Upgrading to Version 0.9
    • Upgrading to Version 0.10
    • Upgrading to Version 0.11
  • Community
  • Telemetry
  • Other Docs

  • Intro to Terraform
  • Configuration Language
  • Terraform CLI
  • Terraform Cloud
  • Terraform Enterprise
  • Provider Use
  • Plugin Development
  • Registry Publishing
  • Integration Program
  • Terraform Tools
  • CDK for Terraform
  • Glossary
Type '/' to Search

»CDK for Terraform Best Practices

There are many ways to structure your CDK for Terraform (CDKTF) application. The structure you choose depends largely on the best practices for your chosen programming language and your use case. However, we recommend using the following principles to build robust, production-ready applications.

»Read Secrets with Terraform Variables

Secrets appear in your synthesized CDKTF code when you read them directly from environment variables or from files with normal system access. This introduces risk, especially if you are checking the synthesized configuration into a version control system. To mitigate this, use the TerraformVariable construct to read secrets. Terraform uses the values in TerraformVariable directly at execution time, so CDKTF does not write them to the synthesized cdktf.json file.

The following example uses a Terraform variable to read the sensitive admin password instead of reading it directly from an environment variable.

const adminPassword = new TerraformVariable(this, "adminPassword", {
  type: "string",
  description: "Admin password for the instance",
  sensitive: true,
});

new MyResource(this, "hello", {
  adminPassword: adminPassword.value, // use this instead of process.env.ADMIN_PASSWORD
});
const adminPassword = new TerraformVariable(this, "adminPassword", {
  type: "string",
  description: "Admin password for the instance",
  sensitive: true,
});

new MyResource(this, "hello", {
  adminPassword: adminPassword.value, // use this instead of process.env.ADMIN_PASSWORD
});

To pass a Terraform variable through environment variables, name the environment variable TF_VAR_NAME. For example, set TF_VAR_adminPasword='<your password>' in the execution environment.

If you use Terraform Cloud with remote execution, you can store your secrets in Terraform Cloud. Refer to the Terraform Cloud documentation about workspace variables for more details.

»Providers

A provider is a Terraform plugin that allows users to manage an external API. Provider plugins act as a translation layer that allows Terraform to communicate with many different cloud providers, databases, and services.

Use pre-built providers when possible. It can take several minutes to generate the code bindings for providers with very large schemas, so we offer several popular providers as pre-built packages. Pre-built providers are a performance optimization that reduces the time it takes to synthesize and run your application. You can also use pre-built providers as a peer dependency if you use open-source custom constructs.

Refer to the CDKTF Provider GitHub repositories for a complete list of pre-built providers.

»Application Architecture

We recommend the following best practices when structuring your CDKTF application.

»Separate Business Units with Stacks

A stack represents a collection of infrastructure that CDKTF synthesizes as a dedicated Terraform configuration. Stacks allow you to separate the state management for multiple environments within an application. We recommend creating separate stacks for the following use cases:

  • Deployment stages (development / staging / production)
  • Business purposes (e-commerce / blog / data warehouse)
  • Regions (eu-west-1 / us-east-1) if they deploy similar infrastructure, e.g. for high availability
  • Software components (network / db / compute)
  • Deployment cycles (databases and domain names / applications)

You do not always need to create a new class for each stack. For some use cases, it is more efficient to create a single stack class to define similar infrastructure and then pass arguments into the stack that customize the deployment. For example, you might create a class that creates AWS EC2 instances and then pass different regions and other configuration details based on the team or production stage that requires the infrastructure.

The following example customizes the same base stack for different business purposes and staging environments.

const devNetworking = new NetworkingStack(this, "networking-development", {
  // ...
});

// A single region is enough to create a development environment for each product.
new Ecommerce(this, "ecommerce-development", {
  vpcId: devNetworking.vpcId,
  subnets: devNetworking.subnets,
  region: "us-west-1",
});
new Blog(this, "ecommerce-development", {
  vpcId: devNetworking.vpcId,
  subnets: devNetworking.subnets,
  region: "us-west-1",
});

// Staging environments require two ecommerce stacks in different regions
// to test the high availability features of the infrastructure.
const stageNetworking = new NetworkingStack(this, "networking-staging", {
  // ...
});
const ecommerceStaging = new Ecommerce(this, "ecommerce-staging-us", {
  vpcId: stageNetworking.vpcId,
  subnets: stageNetworking.subnets,
  region: "us-west-1",
});
new Ecommerce(this, "ecommerce-staging-eu", {
  vpcId: stageNetworking.vpcId,
  subnets: stageNetworking.subnets,
  region: "eu-central-1",
  databaseReplicationMaster: ecommerceStaging.databaseReplicationMaster,
});
new Blog(this, "ecommerce-staging", {
  vpcId: stageNetworking.vpcId,
  subnets: stageNetworking.subnets,
  region: "us-west-1",
});
const devNetworking = new NetworkingStack(this, "networking-development", {
  // ...
});

// A single region is enough to create a development environment for each product.
new Ecommerce(this, "ecommerce-development", {
  vpcId: devNetworking.vpcId,
  subnets: devNetworking.subnets,
  region: "us-west-1",
});
new Blog(this, "ecommerce-development", {
  vpcId: devNetworking.vpcId,
  subnets: devNetworking.subnets,
  region: "us-west-1",
});

// Staging environments require two ecommerce stacks in different regions
// to test the high availability features of the infrastructure.
const stageNetworking = new NetworkingStack(this, "networking-staging", {
  // ...
});
const ecommerceStaging = new Ecommerce(this, "ecommerce-staging-us", {
  vpcId: stageNetworking.vpcId,
  subnets: stageNetworking.subnets,
  region: "us-west-1",
});
new Ecommerce(this, "ecommerce-staging-eu", {
  vpcId: stageNetworking.vpcId,
  subnets: stageNetworking.subnets,
  region: "eu-central-1",
  databaseReplicationMaster: ecommerceStaging.databaseReplicationMaster,
});
new Blog(this, "ecommerce-staging", {
  vpcId: stageNetworking.vpcId,
  subnets: stageNetworking.subnets,
  region: "us-west-1",
});

»Create Extensible Constructs

Constructs let you abstract common behavior into reusable classes. If you have no reason to limit the extensibility of a construct, you should default to making it as easy as possible to overwrite custom behavior, while still providing good standard defaults.

In some cases, you can use interfaces from the generated provider bindings to allow users to customize the configuration. In very complex constructs, we recommend using methods to encapsulate behavior. For example, you can create a method that derives default values for the configuration. This lets users extend the base class and overwrite the behavior in a central location.

The following example exposes configuration options for the S3 bucket resource within the construct. It also creates a second construct class that overwrites the default naming behavior.

import { Construct } from "constructs";
import { S3Bucket, S3BucketConfig } from "@cdktf/provider-aws/lib/s3";

class MyS3Bucket extends Construct {
  constructor(
    protected scope: Construct,
    protected id: string,
    protected s3Options: S3BucketConfig
  ) {
    super(scope, id);
    new s3.Bucket(this, "MyBucket", {
      ...s3Options,
      bucketName: this.getBucketName(),
      versioned: true,
    });
  }

  public getBucketName() {
    return this.s3Options.bucketName || `${this.id}-bucket`;
  }
}

class SimpleS3Bucket extends MyS3Bucket {
  // New behaviour was patched in by overwriting
  public getBucketName() {
    return this.id;
  }
}
import { Construct } from "constructs";
import { S3Bucket, S3BucketConfig } from "@cdktf/provider-aws/lib/s3";

class MyS3Bucket extends Construct {
  constructor(
    protected scope: Construct,
    protected id: string,
    protected s3Options: S3BucketConfig
  ) {
    super(scope, id);
    new s3.Bucket(this, "MyBucket", {
      ...s3Options,
      bucketName: this.getBucketName(),
      versioned: true,
    });
  }

  public getBucketName() {
    return this.s3Options.bucketName || `${this.id}-bucket`;
  }
}

class SimpleS3Bucket extends MyS3Bucket {
  // New behaviour was patched in by overwriting
  public getBucketName() {
    return this.id;
  }
}

»Use Projen to Distribute Constructs

If your code is hosted on Github and you want to distribute it as a CDKTF construct, you can use projen to create a repository with all required tooling set up for you. You can run npx projen new cdktf-construct in a new folder, and the created project will be ready to use. Projen has built-in options to publish constructs to all registries.

If you want to deploy your CDKTF construct as a Terraform module, we recommend projen-cdktf-hybrid-construct.

github logoEdit this page
  • Overview
  • Docs
  • Extend
  • Privacy
  • Security
  • Press Kit
  • Consent Manager