Skip to content

Repository files navigation

Terragrunt Experiment

The goal is to create a simple project demonstrating Terragrunt deploying two Terraform modules: one module's output is used to initialize the provider in a second (dependent) module. This requires the first module to fully deploy before the provider initialization is invoked in the second module.

Key characteristics:

  1. Two pure Terraform modules: foo-module and bar-module.
  2. The foo-module takes one input, calculates a random number, writes the result to a file, reads the file and returns the result as an outout. Therefore the output cannot be consumed unless the module has been deployed (the output file exists.)
  3. The bar-module takes one input, writes it to a file verbatim, reads the file and returns the result as an output.
  4. If the output of foo-module is passed to the bar-module then both module outputs should match after apply.
  5. In addition, the bar-module has a provider (more on that below) whose config takes an argument that is initialized from a bar-module input variable. The goal is to model a common pattern where a resource (e.g. EKS cluster) is created in a one module, and then that modules's output(s) (e.g. EKS cluster endpoint, etc.) are used to initialize the Helm provider of another module to install a chart into the eks cluster.

The catch is - we want to use one single top-level command: terragrunt run --all -- apply.

Versions

This project was built using the following versions:

Component Version
OS Ubuntu 24.04.4 LTS (amd64)
Go 1.26.6 (for the custom provider - more below)
Terragrunt v1.0.5
OpenTofu v1.12.6

Quick Start - Run The Terragrunt

Follow these steps:

  1. Build the provider, edit provider-mirror.tfrc in the repo root so it's valid for your filesystem, and export TF_CLI_CONFIG_FILE to point to the edited .tfrc file. All these details are in the Provider section below.
  2. Then deploy either delaware, or maryland. E.g.:
    export TF_CLI_CONFIG_FILE=$PWD/provider-mirror.tfrc
    cd delaware
    terragrunt run --all --non-interactive -- apply
    

Observe that the foo-module is deployed first, and then the bar-module is deployed with its custom provider initialized by an output of the foo-module. This proves the requirement that the dependent module (bar-module) can be deployed by a single terragrunt run --all -- apply and Terragrunt will orchestrate the dependencies between the foo-module and the bar-module.

Provider terraform-provider-clusterconn

The project has a very simple provider providers/terraform-provider-clusterconn. This provider supports a configuration block argument. It also has a simple resource but - doesn't do much more. Its sole purpose is to be included in the bar-module to verify that the provider can be initialized using a bar-module input variable that came from the foo-module output.

The module is a tiny, hand-written, unpublished Terraform/OpenTofu provider used only within this project. It stands in for a real provider like helm, whose provider block needs a value (cluster endpoint, auth token, etc.) that only exists after another module's resources have been applied.

It is a real provider built with the terraform-plugin-framework - not a mock or a stub. It just isn't published anywhere, so it's wired up via Terraform/OpenTofu's filesystem mirror mechanism instead of hosted in a registry.

What it exposes

Provider block

provider "clusterconn" {
  input_val = <any integer>
}
Argument Type Required Description
input_val number (int64) yes Stand-in for a meaningful value. Just a number.

Resource clusterconn_testme

This exists only to make the provider used (more below) by Terraform.

resource "clusterconn_testme" "bar" {
  # empty  
}
Attribute Type Required/Computed Description
id string computed Always "testme".
input_val number (int64) computed Echoes back whatever the provider was configured with.

Why this resource exists

This resource exists to verify that the provider Configure() function actually ran. Terraform/OpenTofu does not call a provider's Configure method at all if the configuration doesn't actually use that provider anywhere (no resource, no data source). A provider block that nothing references is still parsed and schema-validated - so, e.g., passing the wrong type for input_val would still be caught - but the plugin's own Configure logic never executes, and plan/apply succeed regardless of whether the value would have been usable by real code.

clusterconn_testme has no arguments of its own; it exists purely so the provider is genuinely used, which makes Terraform actually call Configure(). There are two independent ways to confirm it actually ran with the real value, both verified directly against this project:

1. State attribute

clusterconn_testme's input_val attribute echoes back what the provider received:

terragrunt state show clusterconn_testme.bar

2. TF_LOG=DEBUG log line

The Configure() function calls:

tflog.Debug(ctx, fmt.Sprintf("ZZZZZZZZZZZZZZZZZ var.input_val = %d", inputVal))

So tflog.Debug only surfaces at TF_LOG=DEBUG or more verbose; it's silently dropped otherwise. The "ZZZZZ" part of the message was made to be easier to find in the verbose Terraform log output.

Recommended

TF_LOG_PATH with a relative path provides good detail. Each Terragrunt unit runs tofu/terraform from its own .terragrunt-cache/.../ working directory, so a relative TF_LOG_PATH resolves independently per unit - you get one debug.log per unit, and Terragrunt's normal, [foo]/[bar]- tagged console output stays completely untouched:

TF_LOG=DEBUG TF_LOG_PATH=debug.log terragrunt run --all -- apply

Then:

find . -name debug.log

Build and Install The Provider

Since local/demo/clusterconn isn't a real, published provider, Terraform needs to be told where to find it some other way. A Terraform filesystem mirror is checked by terraform init / tofu init itself, before it ever tries the network - unlike dev_overrides, which init ignores entirely. terragrunt run --all always runs init automatically, so this is the mechanism that actually works with it. This section will present the steps to build and install the provider as a filesystem mirror and then configure Terraform to use it.

1. Build the binary and stage it into the mirror layout Terraform expects

Terraform expects <mirror>/<host>/<namespace>/<type>/<version>/<os>_<arch>/<executable>.

cd providers/terraform-provider-clusterconn
VERSION=0.1.0
TARGET="$HOME/.terraform.d/plugin-mirror/local/demo/clusterconn/$VERSION/$(go env GOOS)_$(go env GOARCH)"
mkdir -p "$TARGET"
go build -o "$TARGET/terraform-provider-clusterconn_v$VERSION" .

2. Point a CLI config file at it

The CLI config is scoped to just this provider so every other provider (hashicorp/local, hashicorp/random, etc.) still installs normally from the real Terraform registry

provider_installation {
  filesystem_mirror {
    path    = "/<replace with your home dir>/.terraform.d/plugin-mirror"
    include = ["local/demo/clusterconn"]
  }

  direct {
    exclude = ["local/demo/clusterconn"]
  }
}

This project locates the CLI config file in the repo root provider-mirror.tfrc. You can see that the committed file has a hard-coded path that works on my filesystem. If you want to reproduce the project you'll have to edit that to be correct for your filesystem.

Project structure

These directories comprise the project:

.
├── infrastructure
│   ├── delaware
│   │   ├── bar
│   │   └── foo
│   └── maryland
│       ├── bar
│       └── foo
├── modules
│   ├── bar-module
│   └── foo-module
└── providers
    └── terraform-provider-clusterconn
        └── internal
            └── provider

Annotations on the structure above:

  1. The infrastructure directory contains two stacks maryland and delaware each of which deploys two modules foo-module and bar-module.
  2. The modules directory has the foo-module and the bar-module.
  3. The provider directory has the tiny hand-built provider served from the file system rather than a public registry.

About

Uses terragrunt to deploy two modules with one module's outputs initializing the provider in another module.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages