Organizing Terraform Code with Terragrunt

Chris / January 26, 2021 in 

Many teams at BTI360 use Terraform as their infrastructure definition tool. We’re also fond of Terragrunt and often use it as a foundation for our infrastructure workflow. Getting started with Terraform and Terragrunt, however, can be a challenge. One of the first pain points is figuring out how to organize the infrastructure. Common advice is:

  1. Don’t put all your infrastructure in the same stack; use multiple independent stacks of well-defined boundaries to minimize blast radius when making changes
  2. Use multiple environments, such as dev, staging, and production
  3. Stacks deployed in multiple environments should share the same infrastructure code

While this advice is sound, it isn’t always obvious how to comply. Chris, one of BTI360’s engineers, writes on Terraform, Terragrunt, and infrastructure-as-code on his blog, thirstydeveloper.io. He joins us today on the BTI360 blog to share how several of his teams organize their infrastructure to satisfy the above requirements using Terraform and Terragrunt.

Directory Structure

Our team organizes our infrastructure inside a git repo with a well-defined directory structure:

deployments/
    app/                        # app tier
        dev/                    # app/dev environment
            network/            # app/dev/network stack
                terragrunt.hcl  # terragrunt configuration for the app/dev/network deployment
        stage/                  # app/stage environment
            network/            # app/stage/network stack
                terragrunt.hcl  # terragrunt configuration for the app/stage/network deployment
        prod/                   # app/prod environment
            network/            # app/prod/network stack
                terragrunt.hcl  # terragrunt configuration for the app/prod/network deployment
    root.hcl                    # terragrunt configuration shared by all deployments
modules/
    stacks/
        app/                    # stacks for the app tier
            network/            # root terraform module for network stack
                 main.tf
                 outputs.tf
                 providers.tf

This directory structure relies on several terms:

  • tier: a large grouping of infrastructure deployed across multiple environments.
  • environment: an instantiation of a tier (e.g., dev, staging, production)
  • stack: infrastructure deployed as a unit by a Terragrunt command
  • deployment: an instantiation of a stack of infrastructure within a tier-environment

With the structure above, our deployments are organized in a directory tree by tier, environment, and stack. Terragrunt is executed within deployment directories. For instance, to apply the app/dev/network deployment, we would cd to  deployments/app/dev/network  and run terragrunt apply.

Here we have three deployments of the network stack, one each for the app/dev, app/test, and app/prod tier-environments. In keeping with the advice that multiple deployments should share the same infrastructure source code, our deployment directories do not contain/duplicate the *.tf files defining the infrastructure resources for the app/network stack. Instead, we define the app/network stack underneath a modules/stacks/ directory, and our deployments reference this common location using Terragrunt configuration files.

Terragrunt Configuration

Each deployment directory contains a terragrunt.hcl file that configures Terragrunt. There is also a root.hcl file with configuration shared by all deployments.1

The terragrunt.hcl files are usually extremely simple, only containing an include to the root.hcl file:

# deployments/app/dev/network/terragrunt.hcl
include {
  path = find_in_parent_folders("root.hcl")
}

The root.hcl file, therefore, contains all the Terragrunt configuration. A simple example is:

# deployments/root.hcl
locals {
  root_deployments_dir       = get_parent_terragrunt_dir()
  relative_deployment_path   = path_relative_to_include()
  deployment_path_components = compact(split("/", local.relative_deployment_path))

  tier  = local.deployment_path_components[0]
  stack = reverse(local.deployment_path_components)[0]
}

# Default the stack each deployment deploys based on its directory structure
# Can be overridden by redefining this block in a child terragrunt.hcl
terraform {
  source = "${local.root_deployments_dir}/../modules/stacks/${local.tier}/${local.stack}"
}

This basic root.hcl uses several of Terragrunt’s built-in functions to make each deployment, by default, deploy the stack under modules/stacks/<tier>/<stack>, with <tier> and <stack> inferred from the path of the deployment. This is how we’re able to have all three network deployments share the same source code under modules/stacks/app/network.

Running Terragrunt

As discussed above, with this directory structure we can operate on small individual deployments by running terragrunt from deployment directories containing terragrunt.hcl files. Sometimes, however, it is helpful to operate on larger sets of infrastructure. The directory structure above makes such large-scale operations just as easy.

For instance, we can run terragrunt plan-all from the repository root or from the deployments/ directory to plan every deployment in every tier-environment. This is often helpful to see if all the infrastructure has been applied.

We can similarly run plan-all or apply-all from any directory underneath deployments/ to target everything underneath that directory. For instance, running terragrunt plan-all from deployments/app/dev would run plan on all app/dev stacks.

We can also use –terragrunt-exclude-dir and –terragrunt-include-dir to target *-all commands. For example, we could apply all non-production deployments by running terragrunt apply-all --terragrunt-exclude-dir deployments/*/prod/** from the repository root.

The power to choose a blast radius that is appropriate for the change at hand is one of the highlights of Terragrunt and this directory structure for our engineers.

Conclusion

The setup above is just one way to use Terraform and Terragrunt to meet infrastructure-as-code best practices, but it is an approach that has been vetted across multiple teams at BTI360. If your team is looking to get started using Terraform for your infrastructure-as-code needs, we hope this starting point proves valuable to you. To go deeper still, a complete worked example using this approach can be found on thirstydeveloper.io.

Footnotes

  1. We use the name root.hcl instead of terragrunt.hcl because the latter causes errors running plan-all and apply-all from the root or deployments/ directory. Terragrunt seems to treat the parent hcl file as a deployment and errors out. We’ve tried Terragrunt’s skip option but to no avail, at least as of version 0.26.2

 


Interested in Solving Challenging Problems? Work Here!

Are you a software engineer, interested in joining a software company that invests in its teammates and promotes a strong engineering culture? Then you’re in the right place! Check out our current Career Opportunities. We’re always looking for like-minded engineers to join the BTI360 family.


Related Posts:

Previous

Interview: Looking Back on 4+ Decades in the Software Industry

Next

Creating a Terraform Variable Hierarchy with Terragrunt

Close Form

Enjoy our Blog?

Then stay up-to-date with our latest posts delivered right to your inbox.

  • This field is for validation purposes and should be left unchanged.

Or catch us on social media

Stay in Touch

Whether we’re honing our craft, hanging out with our team, or volunteering in the community, we invite you to keep tabs on us.

  • This field is for validation purposes and should be left unchanged.