Skip to main content

Terraform

For the most part, we assume users have a solid grasp of terraform. Cloud Posse has adopted a number of conventions for how we work with terraform that we document here. Review our opinionated public “best practices” as it relates to terraform.

We use Atmos together with Stacks to call Components that provision infrastructure with terraform.

caution

Be aware of Terraform Environment Variables that can alter the behavior of terraform when run outside of what you see in atmos or geodesic. These are also helpful to change default behavior as well, such as by setting the TF_DATA_DIR.

How-to Guides

Architectural Design Records

Conventions

Mixins

Terraform does not natively support the object-oriented concepts of multiple inheritances or mixins, but we can simulate by using convention. For our purposes, we define a mixin in terraform as a controlled way of adding functionality to modules. When a mixin file is dropped into a folder of a module, the code in the mixin starts to interact with the code in the module. A module can have as many mixins as needed. Since terraform does not directly, we instead use a convention of exporting what we want to reuse.

We achieve this currently using something we call an export in our terraform modules, which publish some reusable terraform code that we copy verbatim into modules as needed. We use this pattern with our terraform-null-label using the context.tf file pattern (See below). We also use this pattern in our terraform-aws-security-group module with the https://github.com/cloudposse/terraform-aws-security-group/blob/main/exports/security-group-variables.tf.

To follow this convention, create an export/ folder with the mixin files you wish to export to other modules. Then simply copy them over (E.g. with curl). We recommend calling the installed files something .mixin.tf so it’s clear it's an external asset.

Resource Factories

Resource Factories provide a custom declarative interface for defining multiple resources using YAML and then terraform for implementing the business logic. Most of our new modules are developed using this pattern so we can decouple the architecture requirements from the implementation.

See https://medium.com/google-cloud/resource-factories-a-descriptive-approach-to-terraform-581b3ebb59c for a related discussion.

To better support this pattern, we implemented native support for deep merging in terraform using our https://github.com/cloudposse/terraform-provider-utils provider as well as implemented a module to standardize how we use YAML configurations https://github.com/cloudposse/terraform-yaml-config.

Examples of modules using Resource Factory convention:

Naming Conventions (and the terraform-null-label Module)

Naming things is hard. We’ve made it easier by defining a programmatically consistent naming convention, which we use in everything we provision. It is designed to generate consistent human-friendly names and tags for resources. We implement this using a terraform module which accepts a number of standardized inputs and produces an output with the fully disambiguate ID. This module establishes the common interface we use in all of our terraform modules in the Cloud Posse ecosystem. Use terraform-null-label to implement a strict naming convention. We use it in all of our Components and export something we call the context.tf pattern.

https://github.com/cloudposse/terraform-null-label

Here’s video from our https://cloudposse.atlassian.net/wiki/spaces/CP/pages/1170014234 where we talk about it.

There are 6 inputs considered "labels" or "ID elements" (because the labels are used to construct the ID):

  1. namespace

  2. tenant

  3. environment

  4. stage

  5. name

  6. attributes

This module generates IDs using the following convention by default: {namespace}-{environment}-{stage}-{name}-{attributes}. However, it is highly configurable. The delimiter (e.g. -) is configurable. Each label item is optional (although you must provide at least one).

Tenants

tenants are a Cloud Posse construct used to describe a collection of accounts within an Organizational Unit (OU). An OU may have multiple tenants, and each tenant may have multiple AWS accounts. For example, the platform OU might have two tenants named dev and prod. The dev tenant can contain accounts for the staging, dev, qa, and sandbox environments, while the prod tenant only has one account for the prod environment.

By separating accounts into these logical groupings, we can organize accounts at a higher level, follow AWS Well-Architected Framework recommendations, and enforce environment boundaries easily.

The context.tf Mixin Pattern

Cloud Posse Terraform modules all share a common context object that is meant to be passed from module to module. A context object is a single object that contains all the input values for terraform-null-label and every cloudposse/terraform-* module uses it to ensure a common interface to all of our modules. By convention, we install this file as context.tf which is why we call it the context.tf pattern. By default, we always provide an instance of it accessible via module.this, which makes it always easy to get your context. 🙂

Every input value can also be specified individually by name as a standard Terraform variable, and the value of those variables, when set to something other than null, will override the value in the context object. In order to allow chaining of these objects, where the context object input to one module is transformed and passed on to the next module, all the variables default to null or empty collections.

Stacks and Components

We use Stacks to define and organize configurations. We place terraform “root” modules in the components/terraform directory (e.g. components/terraform/s3-bucket). Then we define one or more catalog archetypes for using the component (e.g. catalog/s3-bucket/logs.yaml and catalog/s3-bucket/artifacts).

Atmos CLI

We predominantly call terraform from atmos, however, by design all of our infrastructure code runs without any task runners. This is in contrast to tools like terragrunt that manipulate the state of infrastructure code at run time.

See How to use Atmos

FAQ

How to upgrade Terraform?

See How to Switch Versions of Terraform for a more complete guide.

TL;DR:

  • Note the version you want to use

  • Make sure the version is available in cloudposse/packages to see if the version desired is in a merged PR for terraform

  • Make sure the version is available in Spacelift by editing an existing stack and see if the new version is available

  • Update Terraform in Dockerfile

  • Update Terraform in .github/workflows/pre-commit.yaml github action

  • Update Terraform in components/terraform/spacelift/default.auto.tfvars

How to use context.tf?

Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf and then place it in your Terraform module to automatically get Cloud Posse's standard configuration inputs suitable for passing to Cloud Posse modules.

curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf

Modules should access the whole context as module.this.context to get the input variables with nulls for defaults, for example context = module.this.context, and access individual variables as module.this.<var>, with final values filled in.

https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf

For example, when using defaults, module.this.context.delimiter will be null, and module.this.delimiter will be - (hyphen).

caution

ONLY EDIT THIS FILE IN http://github.com/cloudposse/terraform-null-label . All other instances of this file should be a copy of that one.

Learning Resources

If you’re new to terraform, here are a number of resources to check out:

Troubleshooting

Prompt: Do you want to migrate all workspaces to "s3"?

If you get this message, it means you have local state (e.g. a terraform.tfstate file) which has not been published to the S3 backend. This happens typically when the backend was not defined (e.g. backend.tf.json) prior to running terraform init.

caution

WARNING This will overwrite any state currently in S3 for this component. If you were not expecting the state to be completely new, this prompt is unexpected. Working with any existing component shouldn't involve migrating a workspace and further investigation is warranted.

As far as I know, this shouldn't involve migrating a workspace, since this state should be completely new. Should I say yes? Is this just a misleading warning? or indicative that I'm about to mess something up?

Reference