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:
- 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
- Use multiple environments, such as dev, staging, and production
- 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
- We use the name
root.hcl
instead ofterragrunt.hcl
because the latter causes errors runningplan-all
andapply-all
from the root ordeployments/
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.