Terragrunt — Inter-module dependency management
By: Patrick Picard
Tags: Terragrunt, Terraform

Sample Terraform Graph

Terraform provides a graph engine that is rather smart. As you use attributes from one resource in another, it generates implied dependencies in the deployment graph. This is probably one of the best features of the language. If you have code ARM templates (Azure’s JSON DSL), you will remember vividly where you have to define all the dependencies by hand.

If you didn’t know about Terraform’s graphing approach, take your favorite module and run the following command. It will generate a graph you can open in the browser to see the dependencies.

terraform graph | dot -Tsvg > graph.svg

As you deploy components of a large infrastructure, you often need to pass the outputs from a deployment to another. This can be accomplished using remote state configurations which has its own challenges.

What if you could could pass the information more cleanly and not pollute your code with remote state references?

Terragrunt to the rescue again!

Terragrunt’s Approach to Dependencies

Terragrunt allows you to define one to many dependencies for a deployment. Dependency management is heavily predicated on a folder hierarchy for the various components of a large infrastructure

Once the dependencies are defined, you now have access to the outputs of the dependency and can use them as inputs. Let’s have a look at an example. First you define the dependency using the dependency resource. This resource requires the path to where this dependency is configured (where the terragrunt.hcl lives). This will allow the runtime to retrieve the Terraform state for the deployment and export the outputs of the module.

Then, in the inputs section, you can access any of the attributes from the dependency as follows:

dependency.<dependency_name>.outputs.<attribute_name>

Example:

# Snippet 1 - The dependency definition

dependency "core-routing" {

config_path = "../core-routing"

} inputs = {

department_code = local.config.global.department_code

environment = local.config.dns.nonp.environment location = local.config.dns.nonp.location tags = local.config.global.tags # Configure DNS only once in the primary region folder.

public_dns_zone = local.config.dns.nonp.public # Configure DNS only once in the primary region folder.

private_dns_zone = local.config.dns.nonp.private vnet_id_to_link_to_private_dns_zone = {

# Snippet 2 - referencing a dependency's output associate_with_vnet_id = dependency.core-routing.outputs.vnet.id

registration_enabled = true

}

}

Prerequisites

For Terragrunt to retrieve the outputs of a preceding deployment, that deployment needs to have been deployed before (otherwise the outputs will be non-existent)

If it has not been deployed before, it is possible to add mocks into the dependency configuration to help with the planning efforts. This is not something I have explored a whole lot. If you do use mocks, make sure that the mocked output matches the structure of what the module would output (the attributes and content).

For more information on mocking outputs, see the Terragrunt documentation. https://terragrunt.gruntwork.io/docs/features/execute-terraform-commands-on-multiple-modules-at-once/#unapplied-dependency-and-mock-outputs

Graphing

Since Terragrunt is an extension to Terraform, it can also graph dependencies between components.

terragrunt graph-dependencies | dot -Tsvg > graph.svg

The output file can be opened in a browser. Let’s have a look at a sample. The graph below is pretty simple. As your infrastructure grows, the dependency matrix will grow. Having tools to graph the hierarchy can go a long way in understanding the order of deployment.

Conclusion

Terragrunt provides a native approach of connecting outputs from one module to another while maintaining each deployment’s independence. It takes Terraform’s dependency management to the next level and allows the orchestration of large infrastructures.