This is the full developer documentation for Terragrunt # Contributing > Contributing to Terragrunt ## Contribution Guidelines [Section titled “Contribution Guidelines”](#contribution-guidelines) Contributions to Terragrunt are very welcome! We follow a fairly standard [pull request process](https://help.github.com/articles/about-pull-requests/) for contributions, subject to the following guidelines: ### File a GitHub issue or write an RFC [Section titled “File a GitHub issue or write an RFC”](#file-a-github-issue-or-write-an-rfc) Before starting any work, we recommend filing a GitHub issue in this repo. This is your chance to ask questions and get feedback from the maintainers and the community before you sink a lot of time into writing (possibly the wrong) code. If there is anything you’re unsure about, just ask! Sometimes, the scope of the feature proposal is large enough that it requires major updates to the code base to implement. In these situations, a maintainer may suggest writing up an RFC that describes the feature in more details than what can be reasonably captured in an enhancement. To write an RFC, click the [RFC](https://github.com/gruntwork-io/terragrunt/issues/new?assignees=\&labels=rfc%2Cpending-decision\&projects=\&template=02-rfc.yml) button in the issues tab. This will present you a template you can fill out to describe the feature you want to propose. ### Update the documentation [Section titled “Update the documentation”](#update-the-documentation) We recommend updating the documentation *before* updating any code (see [Readme Driven Development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html)). This ensures the documentation stays up to date and allows you to think through the problem at a high level before you get lost in the weeds of coding. The documentation is built with [Starlight](https://github.com/withastro/starlight) and hosted on [Vercel](https://vercel.com/) from the `docs` folder on `main` branch. Read this [README.md](https://github.com/gruntwork-io/terragrunt/tree/main/docs#terragrunt-docs-rewrite) to learn more about making updates to the docs. ### Update the tests [Section titled “Update the tests”](#update-the-tests) We also recommend updating the automated tests *before* updating any code (see [Test Driven Development](https://en.wikipedia.org/wiki/Test-driven_development)). That means you add or update a test case, verify that it’s failing with a clear error message, and *then* make the code changes to get that test to pass. This ensures the tests stay up to date and verify all the functionality in this Module, including whatever new functionality you’re adding in your contribution. Check out [Developing Terragrunt](#developing-terragrunt) for instructions on running the automated tests. ### Update the code [Section titled “Update the code”](#update-the-code) At this point, make your code changes and use your new test case to verify that everything is working. Check out [Developing Terragrunt](#developing-terragrunt) for instructions on how to build and run Terragrunt locally. We have a [style guide](https://gruntwork.io/guides/style%20guides/golang-style-guide/) for the Go programming language, in which we documented some best practices for writing Go code. Please ensure your code adheres to the guidelines outlined in the guide. ### Create a pull request [Section titled “Create a pull request”](#create-a-pull-request) [Create a pull request](https://help.github.com/articles/creating-a-pull-request/) with your changes. Please make sure to include the following: 1. A description of the change, including a link to your GitHub issue. 2. The output of your automated test run, preferably in a [GitHub Gist](https://gist.github.com/). We cannot run automated tests for pull requests automatically due to [security concerns](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#using-secrets-in-a-workflow), so we need you to manually provide this test output so we can verify that everything is working. 3. Any notes on backwards incompatibility. ### Merge and release [Section titled “Merge and release”](#merge-and-release) The maintainers for this repo will review your code and provide feedback. If everything looks good, they will merge the code and release a new version, which you’ll be able to find in the [releases page](https://github.com/gruntwork-io/terragrunt/releases). ## Developing Terragrunt [Section titled “Developing Terragrunt”](#developing-terragrunt) ### Running locally [Section titled “Running locally”](#running-locally) To run Terragrunt locally, use the `go run` command: ```bash go run main.go plan ``` ### Testing on Windows [Section titled “Testing on Windows”](#testing-on-windows) Running tests on Windows is currently limited. Not all tests pass reliably, and additional configuration is required for proper functionality. Specifically: * Long file paths must be enabled on Windows. * A Bash shell (e.g., via Git Bash or WSL) must be available in the environment. For setup instructions and requirements, `.github/scripts/windows-setup.ps1`. ### Dependencies [Section titled “Dependencies”](#dependencies) Terragrunt uses go modules (read more about the modules system in [the official wiki](https://github.com/golang/go/wiki/Modules)). This means that dependencies are automatically installed when you use any go command that compiles the code (`build`, `run`, `test`, etc.). ### Linting [Section titled “Linting”](#linting) Terragrunt uses [golangci-lint](https://golangci-lint.run/) to lint the golang code in the codebase. This is a helpful form of static analysis that can catch common bugs and issues related to performance, style and maintainability. We use the linter as a guide to learn about how we can improve the Terragrunt codebase. We do not enforce 100% compliance with the linter. If you believe that an error thrown by the linter is irrelevant, use the documentation on [false-positives](https://golangci-lint.run/usage/false-positives/) to suppress that error, along with an explanation of why you believe the error is a false positive. If you feel like the linter is missing a check that would be useful for improving the code quality of Terragrunt, please open an issue to discuss it, then open a pull request to add the check. The default in the `.golangci.yml` configuration file is used when running `golangci-lint run`. These lints **must** pass before any code is merged into the `main` branch. #### Default linter [Section titled “Default linter”](#default-linter) Before any tests run in our continuous integration suite, they must pass the default linter. This is to ensure an acceptable floor for code quality in the codebase. To run the default linter directly, use: ```bash golangci-lint run ``` There’s also a Makefile recipe that runs the default linter: ```bash make run-lint ``` If possible, you are advised to [integrate the linter into your code editor](https://golangci-lint.run/welcome/integrations/) to get immediate feedback as edit Terragrunt code. #### Markdownlint [Section titled “Markdownlint”](#markdownlint) In addition to the golang linter, we also use [markdownlint](https://github.com/DavidAnson/markdownlint) to lint the markdown files in the codebase. This is to ensure that the documentation is consistent and easy to read. You’ll want to check that the markdown files are linted correctly before submitting a pull request to update the docs. You can do this by running: ```bash markdownlint \ --disable MD013 MD024 \ -- \ docs ``` ### Running tests [Section titled “Running tests”](#running-tests) There are multiple different kinds of tests in the Terragrunt codebase, and each serves a different purpose. #### Unit tests [Section titled “Unit tests”](#unit-tests) These are tests that test individual functions in the codebase. They are located in the same package as the code they are testing and are suffixed `*_test.go`. They use a package directive that is suffixed `_test` of the package they test to force them to only test exported functions of that package, while residing in the same directory. The idea behind this practice is to keep the tests close to the code they are testing, and to force them to only test the public API of the package. This allows implementation details of particular functions to change without breaking tests, as long as the public API behaves the same. In general, if you are editing Terragrunt code, and there isn’t a unit test that covers the code you are updating, it’s probably a good idea to add one. If there is a unit test for the code you are updating, you should make sure that you run that test after any update to ensure that you haven’t broken anything. When possible, introduce new tests for code *before* you start making changes. This is a practice known as [Test Driven Development](https://en.wikipedia.org/wiki/Test-driven_development). You can run the unit tests for a particular package by running: ```bash go test ./path/to/package ``` To specifically run a single test, you can use the `-run` flag: ```bash go test -run TestFunctionName ./path/to/package ``` There are many ways to customize the `go test` command, including using flags like `-v` to get more verbose output. To learn more about go testing, read the [official documentation](https://pkg.go.dev/testing). #### Integration tests [Section titled “Integration tests”](#integration-tests) These are tests that test integrations between multiple parts of the Terragrunt codebase, and external services. They generally invoke Terragrunt as if you were using it from the command line. These tests are located in the `test` directory, and are suffixed `*_test.go`. Often, these tests run against test fixtures, which are small Terragrunt configurations that emulate specific real-world scenarios. These test fixtures are located in the `test/fixtures` directory. To run the integration tests, you can use the `go test` command: ```bash go test ./test ``` Note that integration tests can be slow, as they often involve running full Terragrunt commands, and that frequently involves spawning new processes. As a result, you may want to run only a subset of the tests while developing. You can do this by using the `-run` flag: ```bash go test -run 'TestBeginningOfFunctionName*' ./test ``` This will run all tests that start with `TestBeginningOfFunctionName`. Note that some tests may require that you opt-in for them to be tested. This is because they may require access to external services that you need to authenticate with or use a specific external tool that you might not have installed. In these cases, we use [golang build tags](https://pkg.go.dev/go/build) to conditionally compile the tests. You can run these tests by setting the appropriate build tag before testing. For example, AWS tests are tagged using the `aws` build tag. To run these tests, you can use the `-tags` flag set in the `GOFLAGS` environment variable like so: ```bash GOFLAGS='-tags=aws' go test -run 'TestAwsInitHookNoSourceWithBackend' . ``` Depending on how you’ve configured your editor, you may need to make sure that your editor has the `GOFLAGS` environment variable set before starting for the best development experience: ```bash export GOFLAGS='-tags=aws' neovim . ``` In general, we try to make sure that any test that requires a build tag is also consistently prefixed a certain way so that they can be tested independently. For example, all AWS tests are prefixed with `TestAws*`. Terragrunt also includes integration tests for Google Cloud Platform (GCP). These tests are prefixed with `TestGcp*` and are tagged with the `gcp` build tag. To run these tests, you can use the `-tags` flag set in the `GOFLAGS` environment variable, similar to AWS tests: ```bash GOFLAGS='-tags=gcp' go test -run 'TestGcp*' . ``` To successfully run the GCP tests, you must set the following environment variables: * `GCLOUD_SERVICE_KEY`: The service account JSON key used for authentication. * `GOOGLE_CLOUD_PROJECT` or `GOOGLE_PROJECT_ID`: The GCP project name. * `GOOGLE_COMPUTE_ZONE`: The compute zone name. * `GOOGLE_IDENTITY_EMAIL`: The service account identity email. * `GCLOUD_SERVICE_KEY_IMPERSONATOR`: (Optional) An additional service account key used in impersonation tests. Make sure these environment variables are set in your shell before running the tests. For example: ```bash export GCLOUD_SERVICE_KEY="/path/to/service-account.json" export GOOGLE_CLOUD_PROJECT="your-gcp-project" export GOOGLE_COMPUTE_ZONE="us-central1-a" export GOOGLE_IDENTITY_EMAIL="service-account@your-gcp-project.iam.gserviceaccount.com" export GCLOUD_SERVICE_KEY_IMPERSONATOR="/path/to/impersonator-service-account.json" ``` The service account used for GCP tests must have the following IAM roles in your GCP project: * `roles/storage.admin` * `roles/iam.serviceAccountTokenCreator` You can assign these roles using the following gcloud commands: ```bash gcloud projects add-iam-policy-binding \ --member="" \ --role="roles/storage.admin" gcloud projects add-iam-policy-binding \ --member="" \ --role="roles/iam.serviceAccountTokenCreator" ``` #### Race tests [Section titled “Race tests”](#race-tests) Given that Terragrunt is a tool that frequently involves concurrently running multiple things at once, there’s always a risk for race conditions to occur. As such, there are dedicated tests that are run with the `-race` flag in CI to use golang’s built-in tooling for identifying race conditions. In general, when encountering a bug caused by a race condition in the wild, we endeavor to write a test for it, and add it to the `./test/race_test.go` file to avoid regressions in the future. If you want to make sure that new code you are writing doesn’t introduce a race condition, add a test for it in the `race_test.go` file. We can do a better job of finding candidates for additional testing here, so if you are interested in helping out, please open an issue to discuss it. The convention we use for race tests is to prefix them with `WithRacing`. The Terragrunt Continuous Integration workflow will run these tests with the `-race` flag as part of the test suite. #### Benchmark tests [Section titled “Benchmark tests”](#benchmark-tests) Benchmark tests are tests that are run with the `-bench` flag to the `go test` command. They are used to measure the performance of a particular function or set of functions. You can find them by looking for tests that start with `Benchmark*` instead of `Test*` in the codebase. For more information on Terragrunt performance, read the dedicated [Performance documentation](/troubleshooting/performance). In general, we have inadequate benchmark testing in the Terragrunt codebase, and want to improve this. If you are interested in helping out, please open an issue to discuss it. Prior to the release of Terragrunt 1.0, we will have a concerted effort to improve the benchmark testing in the codebase. #### Continuous Integration [Section titled “Continuous Integration”](#continuous-integration) All of the testing mentioned above is run automatically as part of our continuous integration suite in GitHub Actions. ### Debug logging [Section titled “Debug logging”](#debug-logging) If you set the `TG_DEBUG_INPUTS` environment variable to “true”, the stack trace for any error will be printed to stdout when you run the app. Additionally, newer features introduced in v0.19.0 (such as `locals` and `dependency` blocks) can output more verbose logging if you set the `TG_LOG` environment variable to `debug`. ### Error handling [Section titled “Error handling”](#error-handling) In this project, we try to ensure that: 1. Every error has a stacktrace. This makes debugging easier. 2. Every error generated by our own code (as opposed to errors from Go built-in functions or errors from 3rd party libraries) has a custom type. This makes error handling more precise, as we can decide to handle different types of errors differently. To accomplish these two goals, we have created an `errors` package that has several helper methods, such as `errors.New(err error)`, which wraps the given `error` in an Error object that contains a stacktrace. Under the hood, the `errors` package is using the [go-errors](https://github.com/go-errors/errors) library, but this may change in the future, so the rest of the code should not depend on `go-errors` directly. Here is how the `errors` package should be used: 1. Any time you want to create your own error, create a custom type for it, and when instantiating that type, wrap it with a call to `errors.New`. That way, any time you call a method defined in the Terragrunt code, you know the error it returns already has a stacktrace and you don’t have to wrap it yourself. 2. Any time you get back an error object from a function built into Go or a 3rd party library, immediately wrap it with `errors.New`. This gives us a stacktrace as close to the source as possible. 3. If you need to get back the underlying error, you can use the `errors.IsError` and `errors.Unwrap` functions. ### Formatting [Section titled “Formatting”](#formatting) Every source file in this project should be formatted with `go fmt`. There are few helper scripts and targets in the Makefile that can help with this (mostly taken from the [terraform repo](https://github.com/hashicorp/terraform/) when it was MPL licensed): 1. `make fmtcheck` Checks to see if all source files are formatted. Exits 1 if there are unformatted files. 2. `make fmt` Formats all source files with `gofmt`. 3. `make install-pre-commit-hook` Installs a git pre-commit hook that will run all of the source files through `gofmt`. To ensure that your changes get properly formatted, please install the git pre-commit hook with `make install-pre-commit-hook`. ### Development Containers [Section titled “Development Containers”](#development-containers) [Development Containers](https://containers.dev/) enable you to capture an entire development environment within a container. They can specify the required binaries, languages, extensions, and settings for a project. They can even define commands to run when entering the container. The [Dev Container spec](https://containers.dev/implementors/spec/) is met by a number of [supporting tools and editors](https://containers.dev/supporting), but here we demonstrate a Visual Studio Code example for contributing to the Terragrunt project. 1. Install the [Dev Containers VSCode extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). 2. Create a `.devcontainer.json` file at the project root. The example below: * Launches a container configured with the appropriate version of Go. * Integrates `golangci-lint` (the standard Golang linter) into the editor. * Installs the `markdownlint` extension with specific rules disabled: * `MD013` * `MD024` * Includes Node.js and OpenTofu. * Starts the Astro Starlight docs upon container startup. .devcontainer.json ```json { "name": "Terragrunt Contributing IDE", "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm", "runArgs": ["--network=host"], "customizations": { "vscode": { "settings": { "go.lintTool": "golangci-lint", "go.lintFlags": [ "--fast" ], "markdownlint.config": { "MD013": false, "MD024": false } }, "extensions": [ "davidanson.vscode-markdownlint" ] } }, "features": { "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/robbert229/devcontainer-features/opentofu:1": {} }, "postCreateCommand": "cd docs && npm install && npm run dev" } ``` 3. Open the project as a VSCode workspace and select `Reopen in Container` when prompted. Tip If you miss the prompt, just open the command palette and run: ```plaintext Dev Containers: Rebuild and Reopen in Container ``` ## Releasing your changes [Section titled “Releasing your changes”](#releasing-your-changes) To learn about how changes get released, read the [Releases documentation](/process/releases). # License > This code is released under the MIT License. Read more here. This code is released under the MIT License. See [LICENSE.txt](https://github.com/gruntwork-io/terragrunt/blob/main/LICENSE.txt). # Support > Get help with Terragrunt ## Github Discussions [Section titled “Github Discussions”](#github-discussions) Search [Terragrunt GitHub Discussions](https://gruntwork-io/terragrunt/discussions) for existing questions or ask your own. [Gruntwork GitHub Discussions](https://github.com/gruntwork-io/knowledge-base/discussions) is a good place for general discussions and questions about Gruntwork tools. ## Join the Discord Community [Section titled “Join the Discord Community”](#join-the-discord-community) Join the [Gruntwork Discord Community](/community/invite) to chat with maintainers and members of the community. ## Github Issues [Section titled “Github Issues”](#github-issues) Read through [existing issues](https://github.com/gruntwork-io/terragrunt/issues) or post a new one. Github issues is a good place to: * Report a bug * Ask for improvements * Propose a change to how Terragrunt works * Start contributing by solving issues ## Commercial support [Section titled “Commercial support”](#commercial-support) Does your company rely on Terragrunt in production? If so, you can get commercial support directly from Gruntwork, the creators of Terragrunt! Check out the [Gruntwork Support Page](https://gruntwork.io/support) for more details. # Caching > Learn how Terragrunt optimizes performance through provider caching and content deduplication. Terragrunt provides several caching mechanisms to improve performance and reduce bandwidth usage when working with OpenTofu/Terraform. These features ensure that providers and content are downloaded and stored efficiently, avoiding redundant work across multiple units. Terragrunt’s caching ecosystem includes: * **Provider caching** to avoid redundant downloads of large provider binaries * **Content deduplication** to share identical content across multiple configurations # Automatic Provider Cache Dir > Learn how Terragrunt automatically configures OpenTofu's native provider caching to improve performance and reduce bandwidth usage. This feature has been stabilized and is now enabled by default when using OpenTofu >= 1.10. *Automatic Provider Cache Dir* is a feature of Terragrunt that automatically configures OpenTofu’s native provider caching mechanism by setting the `TF_PLUGIN_CACHE_DIR` environment variable. This enables efficient provider caching without the need to manually configure provider cache directories or use Terragrunt’s provider cache server. When using OpenTofu >= 1.10, Terragrunt will automatically configure OpenTofu to use a shared provider cache directory, which provides several benefits: * **Improved performance**: Providers are downloaded once and reused across multiple configurations * **Reduced bandwidth usage**: Eliminates redundant provider downloads * **Better concurrency**: OpenTofu 1.10+ handles concurrent access to the provider cache safely * **Simplified setup**: No need for manual provider cache configuration ## Requirements [Section titled “Requirements”](#requirements) The Automatic Provider Cache Dir feature has specific requirements: * **OpenTofu version >= 1.10** is required * **Only works with OpenTofu** (not Terraform) * If requirements are not met, the experiment silently does nothing ## Usage [Section titled “Usage”](#usage) When using OpenTofu >= 1.10, this feature is enabled by default. No additional configuration is required: ```bash terragrunt run --all apply ``` ## How it Works [Section titled “How it Works”](#how-it-works) When enabled, Terragrunt automatically: 1. **Detects OpenTofu version** and ensures it meets the minimum requirement (>= 1.10) 2. **Sets up provider cache directory** using the default cache location or a custom path 3. **Configures TF\_PLUGIN\_CACHE\_DIR** environment variable for OpenTofu processes 4. **Ensures directory exists** with proper permissions The default provider cache directory is located at: * `$HOME/.terragrunt-cache/providers` on Unix systems * `$HOME/Library/Caches/terragrunt/providers` on macOS * `%LocalAppData%\terragrunt\providers` on Windows ## Customizing the Cache Directory [Section titled “Customizing the Cache Directory”](#customizing-the-cache-directory) You can customize the provider cache directory using the `--provider-cache-dir` flag: ```bash terragrunt apply --provider-cache-dir /custom/path/to/cache ``` Or with environment variables: ```bash TG_PROVIDER_CACHE_DIR='/custom/path/to/cache' terragrunt apply ``` ## Disabling Auto Provider Cache Dir [Section titled “Disabling Auto Provider Cache Dir”](#disabling-auto-provider-cache-dir) You can disable the feature for specific runs using the `--no-auto-provider-cache-dir` flag: ```bash terragrunt run --all apply --no-auto-provider-cache-dir ``` This is particularly useful when: * You want manual control over provider caching for specific environments * Testing configurations without provider caching * Using custom provider cache configurations ## Comparison with Provider Cache Server [Section titled “Comparison with Provider Cache Server”](#comparison-with-provider-cache-server) Terragrunt also provides a [Provider Cache Server](/features/caching/provider-cache-server) feature. Here’s when to use each: **Use Auto Provider Cache Dir when:** * Using OpenTofu 1.10+ * You want a simple, low-maintenance caching solution * You prefer native OpenTofu caching mechanisms * You need good concurrent access handling **Use Provider Cache Server when:** * Using older versions of OpenTofu/Terraform * You need advanced caching features * You want to share providers across different filesystems * You need custom registry configurations ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) If the feature doesn’t seem to be working: 1. **Check OpenTofu version**: Ensure you’re using OpenTofu 1.10 or later 2. **Check cache directory**: Ensure the cache directory is accessible and has proper permissions 3. **Review environment variables**: Verify `TF_PLUGIN_CACHE_DIR` is not already set by another tool You can enable debug logging to see more information: ```bash terragrunt apply --log-level debug ``` # Content Addressable Store (CAS) > Learn how Terragrunt supports deduplication of content using a Content Addressable Store (CAS). Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. Terragrunt supports a Content Addressable Store (CAS) to deduplicate content across multiple Terragrunt configurations. The CAS is used to speed up catalog cloning, OpenTofu/Terraform source fetching, and stack generation by avoiding redundant downloads of remote sources. See [Supported sources](#supported-sources) for the full list of supported getters and a dedicated page on how each one interacts with the CAS. You can disable the CAS at any time using the `--no-cas` flag. This flag is available on the [`run`](/reference/cli/commands/run), [`stack generate`](/reference/cli/commands/stack/generate), [`stack run`](/reference/cli/commands/stack/run), and [`catalog`](/reference/cli/commands/catalog) commands. The same commands accept the `--cas-clone-depth` flag, which controls the `git clone --depth` value the CAS uses when cloning a Git source (default `1`; `-1` clones the full history). ## Supported sources [Section titled “Supported sources”](#supported-sources) The CAS sits in front of every getter Terragrunt uses to fetch a source. Each getter resolves a source through a **cheap probe**: a low-cost remote request that yields a cache key without downloading the full payload (e.g. for Git this is `git ls-remote`). When the probe key is already present in the store, Terragrunt links the cached content to a target directory and skips the download entirely. Getters with no cheap probe always download (or, for local paths, copy) and then key the result by the content hash of what they fetched. Each getter has a dedicated page describing its exact mechanics: | Source | Cheap probe | Deduplication | | ------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------- | | [Git](/features/caching/cas/git) | `git ls-remote` resolves a ref to a commit hash | Native Git object hash, shared across repositories | | [HTTP / HTTPS](/features/caching/cas/http) | `HEAD` request reads the `ETag` or `Last-Modified` header | URL-scoped (the validator is not a portable content hash) | | [Amazon S3](/features/caching/cas/s3) | `HeadObject` reads the object checksum or `ETag` | Content-addressed when a checksum is available, otherwise URL-scoped | | [Google Cloud Storage](/features/caching/cas/gcs) | Object metadata read exposes the MD5 or CRC32C checksum | Content-addressed | | [Mercurial](/features/caching/cas/mercurial) | `hg identify` resolves a revision to a node hash | Content-addressed | | [OpenTofu/Terraform Registry](/features/caching/cas/registry) | Registry protocol resolves the underlying archive URL | Content-addressed on the immutable archive | | [SMB](/features/caching/cas/smb) | None; always downloads | Content-addressed on the downloaded tree | | [Local paths](/features/caching/cas/local) | None; always copies | Content-addressed on the copied tree | Regardless of the probe, every getter shares the same back half: download (or copy) the source, hash its files into the store as content-addressed blobs, record a tree describing their layout, and hard link the result into the target directory. Identical files therefore occupy disk space once no matter which getter fetched them or how many configurations use them. ## Usage [Section titled “Usage”](#usage) When the CAS is enabled, Terragrunt automatically routes every compatible source through it. Git is the most common case and is used as the running example throughout this page, but the same flow applies to the other [supported sources](#supported-sources). ### Catalog Usage [Section titled “Catalog Usage”](#catalog-usage) root.hcl ```hcl catalog { urls = [ "git@github.com:acme/modules.git" ] } ``` ### OpenTofu/Terraform Source Usage [Section titled “OpenTofu/Terraform Source Usage”](#opentofuterraform-source-usage) terragrunt.hcl ```hcl terraform { source = "git@github.com:acme/infrastructure-modules.git//vpc?ref=v1.0.0" } ``` ### Stack Usage [Section titled “Stack Usage”](#stack-usage) Tip This functionality is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. When authoring stacks in a catalog, you can use the `update_source_with_cas` attribute to allow for relative paths to other components in the catalog in `source` attributes. This removes the need to plumb remote Git URLs through `values` expressions. The `source` must be a literal string: interpolation, function calls, and references such as `local.foo` cause stack generation to fail. ```hcl # stacks/my-stack/terragrunt.stack.hcl (in your catalog repository) unit "service" { source = "../..//units/my-service" update_source_with_cas = true path = "service" } ``` The referenced unit can also use relative paths: ```hcl # units/my-service/terragrunt.hcl (in your catalog repository) terraform { source = "../..//modules/my-module" update_source_with_cas = true } ``` During stack generation, Terragrunt rewrites these relative sources to `cas::` references that point to content stored in the CAS. The repository is cloned once, and subsequent stack generations resolve content from the local store without network access. Generated `.terragrunt-stack` files contain deterministic CAS references instead of version variables, so they do not produce diffs on regeneration. The catalog source can be either a remote Git URL or a local filesystem path (absolute, or relative to the current working directory). Local sources are copied into a temporary directory before rewriting, so the original catalog directory is never modified. This makes the same catalog layout usable against a published Git ref or a local checkout, which is useful when iterating on a catalog before tagging a release. For more details on using this with stacks, see [Explicit Stacks: CAS Integration](/features/stacks/explicit#cas-integration). Caution Setting `update_source_with_cas = true` requires that the `cas` experiment is enabled and that `--no-cas` is not set. Terragrunt errors out otherwise, since the relative source must be updated to a synthetic tree stored in the CAS. When Terragrunt fetches a Git source while using the CAS, if the content is not found in the CAS, Terragrunt fetches into the central bare repository for that remote URL and stores the resulting blobs and trees in the CAS for future use. If the central store is unavailable, Terragrunt falls back to cloning the repository from the original URL into a temporary directory. Other getters follow the equivalent pattern for their source type, described on the [per-getter pages](#supported-sources). When materializing content from the CAS, Terragrunt will hard link entries from the CAS into the target directory. This allows Terragrunt to deduplicate content across multiple sources. In the event that hard linking fails due to some operating system / host incompatibility with hard links, Terragrunt will fall back to performing copies of the content from the CAS. ## Storage [Section titled “Storage”](#storage) The CAS lives under the platform user cache directory: | Platform | Path | | -------- | --------------------------------------------------------------------------- | | Linux | `$XDG_CACHE_HOME/terragrunt/cas`, falling back to `~/.cache/terragrunt/cas` | | macOS | `~/Library/Caches/terragrunt/cas` | | Windows | `%LocalAppData%\terragrunt\cas` | This directory can be deleted to reclaim disk space when no Terragrunt processes are running against it. Terragrunt will regenerate the CAS on the next run. Avoid deleting it while a Terragrunt operation is in progress, since that can race with in-flight reads, writes, and locks in the store. Avoid partial deletions of the CAS directory without care, as that might result in partially cloned repositories and unexpected behavior. ## How it works [Section titled “How it works”](#how-it-works) Terragrunt’s CAS uses a content-addressable storage model to deduplicate fetched content, saving disk space and improving performance. Each stored file is identified by its hash, allowing identical content to be shared across multiple sources and repeated fetches. ### Content Addressing [Section titled “Content Addressing”](#content-addressing) For Git sources, CAS uses Git’s native content addressing scheme where each object is uniquely identified by its hash. Terragrunt detects the hash algorithm used by the repository (`sha1` or `sha256`) via `git rev-parse --show-object-format`. This means: * **Identical content** across different repositories shares the same hash * **Same commit hash** always represents the same content * **Storage is partitioned** by the first two characters of the hash (e.g., `ab/abc123...`) * **Both SHA-1 and SHA-256** repositories are supported Non-Git sources reuse the same store but address content differently. Their file blobs are hashed with SHA-256, and the tree they produce is keyed by one of two things: a **probe token** when the getter has a cheap probe (an S3 checksum, an HTTP `ETag`, a Mercurial node hash, and so on), or the **content hash of the downloaded tree** when it does not. Probe tokens that are themselves content hashes deduplicate across URLs; tokens that are not (such as an `ETag`) are scoped to their URL. The [per-getter pages](#supported-sources) spell out which applies to each source. ### Storage Structure [Section titled “Storage Structure”](#storage-structure) The CAS store is organized into namespaced directories: * \~/.cache/terragrunt/cas/store/ * blobs/ (file content from every fetched source, plus synthetic sources) * ab/ * abc123…xyz * abc123…xyz.lock * cd/ * cd7890…xyz * trees/ (tree structures describing the layout of a fetched source) * f3/ * f39ea0…xyz * synth/ * trees/ (synthetic trees created during CAS-backed stack generation) * de/ * def456…xyz * git/ (one bare repository per remote URL, used for incremental fetches) * 1a2b3c4d5e6f7890/ * repo/ * … * lock The `blobs/` directory stores all file content, identified by hash. Blobs are purely content-addressed, so the same file content always maps to the same hash regardless of origin. The `trees/` directory stores the tree structures that describe the layout of files in a fetched source, whether that source is a Git ref, an object store download, or a local directory. The `synth/trees/` directory stores synthetic tree structures created during CAS-backed stack generation when `update_source_with_cas` is used. These synthetic trees use a deterministic hash based on the source reference and path. The `git/` directory holds one bare Git repository per remote URL, keyed by a hash of the URL, so cache misses for Git sources can fetch only the new objects instead of re-cloning the repository. Non-Git getters do not use `git/`; they download into a temporary directory and ingest the result. Each content object within a namespace is stored at `{hash[:2]}/{hash}`, where the first two characters create a partition directory to avoid degraded file system performance from large flat directories. ### Fetch Flow [Section titled “Fetch Flow”](#fetch-flow) Every source follows the same two-path shape. Terragrunt runs the getter’s cheap probe to derive a cache key, then either links already-stored content (a cache hit) or fetches, ingests, and links it (a cache miss). The steps below describe this generic flow; behavior unique to a particular getter is called out where it applies. #### Cache Miss [Section titled “Cache Miss”](#cache-miss) When the content is not already in the CAS: 1. **Probe the source.** Terragrunt runs the getter’s cheap probe to derive a cache key without downloading the payload. Getters with no probe skip this step and key the result by content hash once it has been fetched. Git sources How the probe resolves the ref depends on its type for Git sources: * A **branch or tag** is resolved to a commit hash with `git ls-remote`, without downloading any objects. That commit hash is used as the cache key. * A **commit SHA** is used as-is. `git ls-remote` only lists named refs and cannot confirm a bare SHA, so when the commit is not already cached Terragrunt skips the probe and lets the fetch step resolve it, canonicalizing the SHA with `git rev-parse` against the central Git store once the objects are present. 2. **Check the store.** The cache key is not present in the CAS, confirming the miss. 3. **Fetch the source.** Terragrunt downloads (or, for local paths, copies) the source into a temporary location. Git sources Terragrunt fetches into a persisted bare repository kept per remote URL under `git/`. Branch and tag refs use a shallow fetch; a bare commit SHA uses a full-history fetch so it can be resolved locally. Subsequent misses against the same URL reuse the repository and transfer only new objects, and concurrent units targeting the same URL share a single fetch instead of cloning in parallel. 4. **Ingest into the store.** Each file is hashed and stored as a content-addressed blob addressed by its hash, and a tree file describing the layout in the filesystem under its own cache key. 5. **Link to the target.** The tree’s blobs are hard linked into the target directory. #### Cache Hit [Section titled “Cache Hit”](#cache-hit) When the content is already in the CAS: 1. **Probe the source.** Terragrunt derives the same cache key from the getter’s cheap probe. Git sources The ref type determines what happens on a hit: * A **branch or tag** runs `git ls-remote` to resolve the ref to a commit hash, but no objects are downloaded once that hash is found in the store. * A **commit SHA** that is already cached locally is resolved from the central Git store without contacting the remote at all. 2. **Check the store.** The cache key is present, confirming the hit. 3. **Read the tree.** The stored tree is read directly from the CAS, with no download. 4. **Link to the target.** The tree’s blobs are hard linked into the target directory. #### Immutable by default [Section titled “Immutable by default”](#immutable-by-default) Stored blobs are written read-only, and the link step hard links them into the target directory instead of copying. The working copy and the stored blob are the same file on disk, so the content cannot be edited in place: writing to a linked file fails with a permission error. This is deliberate. A single stored blob is shared by every target that references it, so permitting edits would corrupt the content other configurations depend on. When you need an editable working copy, set `mutable = true`. Terragrunt then copies the content into the target with its write permissions intact rather than hard linking it, producing a tree that is independent of the store and safe to modify. This trades the disk savings of shared inodes for a writable copy, since each `mutable` target keeps its own. The attribute is available on the `terraform` block, and on the `unit` and `stack` blocks of a stack configuration. terragrunt.hcl ```hcl terraform { source = "git@github.com:acme/infrastructure-modules.git//vpc?ref=v1.0.0" mutable = true } ``` Outside the CAS, fetched sources are already plain writable copies, so `mutable` only takes effect when the CAS is enabled. #### Graceful Fallbacks [Section titled “Graceful Fallbacks”](#graceful-fallbacks) If hard linking fails (for example across a filesystem boundary, or on a host that does not support hard links), Terragrunt copies the content from the store instead. If a Git fetch through the central repository hangs or fails, Terragrunt logs a warning and falls back to a clone in a temporary directory. #### Flow Diagram [Section titled “Flow Diagram”](#flow-diagram) The diagram below illustrates the cache-miss and cache-hit paths for a Git source. ![Diagram](/d2/docs/03-features/07-caching/04-cas/index-0.svg) ### Deduplication Mechanism [Section titled “Deduplication Mechanism”](#deduplication-mechanism) CAS achieves deduplication through hard links, which allows multiple files to use the same physical space on disk, avoiding duplicated content across the sources Terragrunt fetches. * **Hard Links**: When the same content is requested multiple times, CAS creates hard links from the read-only store to each target directory * **Automatic Fallback**: If hard linking fails (e.g., cross-filesystem boundaries, operating system limitations), CAS automatically falls back to copying the content instead ### Performance Benefits [Section titled “Performance Benefits”](#performance-benefits) CAS provides significant performance improvements: * **Faster Subsequent Fetches**: Once content is in CAS, a probe hit skips the network download and ingest steps entirely * **Reduced Disk Usage**: Hard links share the same inode, so duplicate content only consumes disk space once, regardless of how many sources fetched by Terragrunt reference the file # Google Cloud Storage > How the CAS probes and deduplicates Google Cloud Storage sources. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. ## Overview [Section titled “Overview”](#overview) A GCS source is identified by a bucket and an object. Terragrunt reads the object’s metadata to derive a content-addressed cache key. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) The probe Terragrunt uses to attempt to avoid fetching GCS sources is an object metadata read. It downloads no body. Terragrunt reads the object’s MD5 checksum when present, falling back to its CRC32C checksum, which GCS populates for every object. ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) Both MD5 and CRC32C are content hashes, so the key is content-addressed and identical object bytes deduplicate across URLs and buckets. File blobs are content-addressed. ## Cache miss [Section titled “Cache miss”](#cache-miss) If the metadata read fails, or returns no usable checksum, Terragrunt downloads the object and keys the tree by the content hash of what it fetched. A fresh probe key that is not yet stored also downloads, then stores under that key. ## Cache hit [Section titled “Cache hit”](#cache-hit) When the probe returns a checksum already present in the store, Terragrunt links the cached tree without downloading. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) Any error reading object metadata makes the probe yield no key, and Terragrunt falls back to downloading and content-hashing the object. # Git > How the CAS resolves, fetches, and deduplicates Git sources. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. Git is the most common source Terragrunt fetches and has the richest support in the CAS. ## Overview [Section titled “Overview”](#overview) A Git source is identified by a remote URL and a ref (a branch, tag, or commit). Terragrunt resolves that ref to a commit hash, which serves as both the cache key and the Git object name it fetches. Content keeps Git’s native content addressing, so identical objects are shared across repositories and refs. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) The probe Terragrunt uses to attempt to avoid fetching Git sources is `git ls-remote`. This asks the remote what commit a branch or tag currently points at without transferring any repository objects. The resolved commit hash becomes the cache key. Two special cases bypass or extend the probe: * **Pinned commit SHA already cached.** If you pin to a full commit SHA and that commit is already in the local store, Terragrunt answers from the store and skips `ls-remote` entirely, so the resolution stays offline. This is especially useful if you plan to use Terragrunt in an air-gapped environment. * **Commit SHA.** `ls-remote` only lists named refs. If it returns no match (you supplied a commit SHA directly), Terragrunt defers resolution to the fetch step, which canonicalizes the ref with `git rev-parse` against the local store after a full-history fetch. ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) The cache key for the root tree is the commit hash itself, content-addressed by Git’s native scheme. Both SHA-1 and SHA-256 repositories are supported, with the algorithm detected per repository. Because the commit hash is content-addressed, the same commit reached through two repositories or two refs resolves to the same stored tree, and identical file blobs are shared across every repository that contains them. ## Cache miss [Section titled “Cache miss”](#cache-miss) On a miss, Terragrunt fetches into a central bare repository kept per remote URL under `git/` in the store (initialized on first use), holding a per-URL lock for the duration: 1. Branch and tag refs use a shallow fetch. A bare commit SHA triggers a full-history fetch of every branch and tag so the SHA can be resolved locally. 2. The blobs and trees reachable from the commit are extracted and stored, partitioned by hash prefix. 3. The tree is read back from the store and hard linked into the target directory. Partial cache misses Git is unique among the getters in that a cache miss is not always all-or-nothing. Because the bare repository for each remote URL persists under `git/`, subsequent misses against the same URL reuse it and transfer only the objects that are new, rather than re-cloning. Other getters re-fetch the entire source on a miss. ## Cache hit [Section titled “Cache hit”](#cache-hit) When the commit hash is already keyed in the store, Terragrunt reads the tree and hard links its blobs into the target without fetching again from the remote. For a pinned commit SHA that is already cached, even the ref-resolution step stays offline. ## Submodules [Section titled “Submodules”](#submodules) A submodule is recorded in the parent repository as a pointer to a commit in another repository. During a fetch, Terragrunt reads the submodule URL from `.gitmodules`, fetches the pinned commit from that URL into the same central store layout, and materializes the submodule contents in place. Nested submodules are handled the same way, and relative URLs (such as `../sibling.git`) are resolved against the parent repository URL. Each submodule tree is keyed by its pinned commit hash, so submodule contents are deduplicated like any other content: two repositories pinning the same submodule commit share one stored tree, and a submodule that does not change between parent commits is never fetched again. A committed submodule path with no `.gitmodules` entry (typically an accidentally committed nested repository) becomes an empty directory, the same result a plain `git clone` produces. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) Concurrent units that target the same remote URL share one Git fetch instead of cloning in parallel. If that shared fetch through the central repository hangs or fails, Terragrunt logs a warning and falls back to a plain clone in a temporary directory. If the host cannot hard link from the store (for example across a filesystem boundary), Terragrunt copies the content instead. # HTTP > How the CAS probes and deduplicates HTTP sources. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. ## Overview [Section titled “Overview”](#overview) An HTTP source is identified by its URL. Terragrunt cannot key it by a content hash up front, so it asks the server for a validator (an `ETag` or `Last-Modified` value) and keys the cached tree by that validator, scoped to the URL. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) The probe Terragrunt uses to attempt to avoid fetching HTTP sources is a `HEAD` request to the source URL. Terragrunt reads the `ETag` header when present, falling back to `Last-Modified`. Whichever it finds becomes the cache key. [`go-getter`](https://github.com/hashicorp/go-getter/) consumes a few query parameters itself rather than forwarding them to the server (`archive`, `checksum`, and `filename`). Terragrunt strips these before the `HEAD` request, because probing with them attached would split cache entries that resolve to the same downloaded bytes. ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) The validator is treated as opaque, and the key is scoped to the URL. An `ETag` is not a portable content hash: a server may assign it however it likes, and multipart or weak ETags are not hashes of the body. Two different URLs that serve byte-identical content therefore do not deduplicate at the tree level. The underlying file blobs are still content-addressed, so identical files across sources continue to share blob storage. Weak-validator markers (`W/`) and surrounding quotes are normalized, so the same response keyed either way maps to one entry. ## Cache miss [Section titled “Cache miss”](#cache-miss) If the probe produces no usable validator, or the validator is not yet present in the store, Terragrunt downloads the resource, walks it, hashes its files, and stores the tree. When the probe produced a key, the tree is stored under that key so the next run hits; when it did not, the tree is keyed by the content hash of what was downloaded. ## Cache hit [Section titled “Cache hit”](#cache-hit) When the probe returns a validator already present in the store, Terragrunt links the cached tree and skips downloading the body. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) Any network error, non-2xx response, or timeout makes the probe yield no key. Terragrunt then falls back to downloading and content-hashing the result. The fetch still happens; only the short-circuit is skipped. ## Supported URL forms / refs [Section titled “Supported URL forms / refs”](#supported-url-forms--refs) `http://` and `https://` URLs are accepted. The go-getter magic parameters (`archive=`, `checksum=`, `filename=`) are honored on the download but ignored by the probe. `.netrc` credentials are honored on the download. # Local paths > How the CAS handles local filesystem sources, which have no cheap probe. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. ## Overview [Section titled “Overview”](#overview) A local source is a directory on the machine running Terragrunt. There is nothing remote to probe, so Terragrunt reads the directory directly and keys it by the content hash of its tree. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) None. Local content is read directly. Terragrunt walks the directory in deterministic (lexical) order and hashes it. ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) Each file is hashed with SHA-256, and the directory is keyed by the content hash of the resulting tree. Identical files and identical trees deduplicate across configurations through the shared store. Symbolic links are stored as link entries that record their target, and Terragrunt validates that a target stays inside the source directory so the store never persists a path that escapes it. ## Cache miss [Section titled “Cache miss”](#cache-miss) When the tree hash is not yet present, Terragrunt stores the blobs and tree, then links them into the target directory. Blobs that already exist in the store are not copied again. ## Cache hit [Section titled “Cache hit”](#cache-hit) When the tree hash is already present, Terragrunt links the cached tree into the target without re-storing the content. The directory is still walked and hashed to compute the key, but that work is local and cheap. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) There is no network step to fall back from. When materializing content into the target, the usual hard-link-to-copy fallback applies if the host cannot hard link from the store. # Mercurial > How the CAS probes and deduplicates Mercurial sources. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. ## Overview [Section titled “Overview”](#overview) A Mercurial source is identified by a repository URL and an optional revision. Terragrunt resolves that revision to a node hash and keys the cached tree by it. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) The probe Terragrunt uses to attempt to avoid fetching Mercurial sources is `hg identify` against the remote, which resolves the requested revision to its full node hash without cloning. The revision comes from the URL’s `?rev=` query parameter; absent or empty means `tip`. The full 40-character node hash is used rather than the abbreviated short form so the key is collision-safe. ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) The node hash is a content identifier, so the key is content-addressed and the same revision reached through different URLs deduplicates to one stored tree. File blobs are content-addressed. ## Cache miss [Section titled “Cache miss”](#cache-miss) When the resolved node hash is not yet in the store, Terragrunt clones the revision, ingests it, and links it into the target. ## Cache hit [Section titled “Cache hit”](#cache-hit) When the node hash is already present, Terragrunt links the cached tree without cloning. # OpenTofu/Terraform Registry > How the CAS probes and deduplicates module registry (tfr) sources. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. ## Overview [Section titled “Overview”](#overview) A registry address is an indirection: the registry resolves a module and version to an underlying archive, such as a versioned tarball or a Git commit. The CAS keys that resolved archive, so caching tracks the immutable artifact rather than the registry address. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) The probe Terragrunt uses to attempt to avoid fetching tfr sources is the registry’s service-discovery and module-download protocol against the registry host. When the URL omits a host (the `tfr:///` form), the default registry is used. The probe requires an explicit `version` in the URL. It resolves the module to the download location the registry returns and uses that location as the cache key. A `//subdir` selector is stripped before the probe, so two requests that differ only by subdirectory share one key; the download still extracts the requested subdirectory. ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) The resolved archive location is content-addressed, so two `tfr://` requests that resolve to the same archive share one CAS entry. Because the key tracks the underlying archive rather than the version string, republishing a different artifact under the same version resolves to a new key instead of serving stale content. File blobs are content-addressed. ## Cache miss [Section titled “Cache miss”](#cache-miss) When the resolved archive location is not yet in the store, Terragrunt downloads the archive, ingests it, and links it into the target. ## Cache hit [Section titled “Cache hit”](#cache-hit) When the resolved location is already present, Terragrunt links the cached tree without downloading. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) A malformed URL, a missing `version`, or any registry error makes the probe yield no key, and Terragrunt falls back to downloading and content-hashing the result. If the download itself fails, that underlying error surfaces on the real fetch attempt. # Amazon S3 > How the CAS probes and deduplicates Amazon S3 and S3-compatible sources. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. ## Overview [Section titled “Overview”](#overview) An S3 source is identified by a bucket, an object key, and an optional version. Terragrunt reads the object’s metadata to derive a cache key, preferring a real content checksum over the weaker `ETag`. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) The probe Terragrunt uses to attempt to avoid fetching S3 sources is a `HeadObject` call with checksum mode enabled. It reads the object’s stored checksum, preferring the strongest available in this order: 1. SHA-256 2. CRC64NVME 3. SHA-1 4. CRC32C 5. CRC32 6. `ETag` (last resort) ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) When any checksum is available, the key is content-addressed, so the same object bytes reached through different URLs or buckets deduplicate to one stored tree. When only an `ETag` is available, the key is scoped to the URL, because a multipart `ETag` (`-`) is not a content hash. File blobs are content-addressed in every case. ## Cache miss [Section titled “Cache miss”](#cache-miss) If the object has neither a checksum nor an `ETag`, or the probe errors, Terragrunt downloads the object and keys the tree by the content hash of what it fetched. A fresh probe key that is not yet stored also downloads, then stores the tree under that key so the next run hits. ## Cache hit [Section titled “Cache hit”](#cache-hit) When the probe returns a key already present in the store, Terragrunt links the cached tree with no download. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) Any AWS or network error makes the probe yield no key, and Terragrunt falls back to downloading and content-hashing the object. # SMB > How the CAS handles SMB sources, which have no cheap probe. Experimental CAS source fetching is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. ## Overview [Section titled “Overview”](#overview) An SMB source is identified by a share path. SMB exposes no low-cost way to resolve a version without reading the content, so Terragrunt always downloads the source and then keys the result by its content hash. The CAS still deduplicates the bytes once they are in the store. ## The cheap probe [Section titled “The cheap probe”](#the-cheap-probe) None. There is no probe step, so SMB never short-circuits the download. ## Cache key & deduplication [Section titled “Cache key & deduplication”](#cache-key--deduplication) After downloading, Terragrunt keys the tree by the content hash of what it fetched. Identical files deduplicate at the blob level across every source, and an unchanged share re-ingests to the same tree, so repeated fetches do not grow the store. ## Cache miss [Section titled “Cache miss”](#cache-miss) Every fetch downloads the source. New content is hashed and stored, and the tree is linked into the target. ## Cache hit [Section titled “Cache hit”](#cache-hit) Because there is no probe, Terragrunt never skips the download. When the downloaded content matches what is already stored, ingest collapses onto the existing blobs and tree instead of writing duplicates. The benefit for SMB is storage deduplication, not skipped transfers. ## Fallback behavior [Section titled “Fallback behavior”](#fallback-behavior) The download path is always taken, so there is no probe fallback. When materializing content into the target, the usual hard-link-to-copy fallback still applies if the host cannot hard link from the store. # Provider Cache Server > Learn how to use the Terragrunt provider cache server. Tip If you’re using OpenTofu >= 1.10, you’ll use the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature by default on the latest version of Terragrunt. You might not necessarily get any performance benefits when using this feature if so. Only use this feature if you are using an older version of OpenTofu, if you are using Terraform or if you are reaching a performance bottleneck that is not addressed by the Automatic Provider Cache Dir feature. Terragrunt has the ability to cache OpenTofu/Terraform providers across all OpenTofu/Terraform runs. The Provider Cache Server feature ensures that each provider is only ever downloaded and stored on disk exactly once by running a local provider cache server while Terragrunt runs OpenTofu/Terraform commands. The Provider Cache Server is a performance optimization. For more details on performance optimizations, their tradeoffs, and other performance tips, read the dedicated [Performance documentation](/troubleshooting/performance). ## Why caching is useful [Section titled “Why caching is useful”](#why-caching-is-useful) Let’s imagine that your project consists of 50 Terragrunt units, and each of them uses the same `aws` provider. Without caching, each of them will download the provider from the Internet, and store it in its own `.terraform` directory. For clarity, the downloadable archive `terraform-provider-aws_5.36.0_darwin_arm64.zip` has a size of \~100MB, and when unzipped it takes up \~450MB of disk space. It’s easy to calculate that initializing such a project with 50 modules will cost you 5GB of traffic and 22.5GB of free space instead of 100MB and 450MB using the cache. ## Why OpenTofu/Terraform’s built-in provider caching doesn’t work [Section titled “Why OpenTofu/Terraform’s built-in provider caching doesn’t work”](#why-opentofuterraforms-built-in-provider-caching-doesnt-work) OpenTofu/Terraform has a provider caching feature, the [Provider Plugin Cache](https://opentofu.org/docs/cli/config/config-file/#provider-plugin-cache), that does the job well… unless you run multiple OpenTofu/Terraform processes simultaneously, such as when you use `terragrunt run --all`. Then the OpenTofu/Terraform processes begin conflict by overwriting each other’s cache, which causes an error such as `Error: Failed to install provider`. As a result, Terragrunt previously had to disable concurrency for `init` steps in `run --all`, which is significantly slower. If you enable Terragrunt Provider Caching, as described in this section, that will no longer be necessary, and you should see significant performance improvements with `init`, as well as significant savings in terms of bandwidth and disk space usage. Note This isn’t necessarily true for the latest version of OpenTofu anymore! For more information, see the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature documentation. ## Usage [Section titled “Usage”](#usage) The Terragrunt Provider Cache Server is disabled by default. To enable it, you need to use the flag [`provider-cache`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache): ```shell terragrunt run --all --provider-cache apply ``` or the environment variable `TG_PROVIDER_CACHE`: ```shell TG_PROVIDER_CACHE=1 terragrunt run --all apply ``` By default, cached providers are stored in `terragrunt/providers` folder, which is located in the user cache directory: * `$HOME/.terragrunt-cache/terragrunt/providers` on Unix systems * `$HOME/Library/Caches/terragrunt/providers` on Darwin * `%LocalAppData%\terragrunt\providers` on Windows The file structure of the cache directory is identical to the OpenTofu/Terraform [plugin\_cache\_dir](https://opentofu.org/docs/cli/config/config-file/#provider-plugin-cache) directory. If you already have a directory with providers cached by OpenTofu/Terraform [plugin\_cache\_dir](https://opentofu.org/docs/cli/config/config-file/#provider-plugin-cache), you can set this path using the flag [`provider-cache-dir`](/reference/cli/commands/run#provider-cache-dir), to enable the Provider Cache Server to reuse existing cached providers. ```shell terragrunt plan \ --provider-cache \ --provider-cache-dir /new/path/to/cache/dir ``` or the environment variable `TG_PROVIDER_CACHE_DIR`: ```shell TG_PROVIDER_CACHE=1 \ TG_PROVIDER_CACHE_DIR=/new/path/to/cache/dir \ terragrunt plan ``` By default, Terragrunt only caches providers from the following registries: `registry.terraform.io`, `registry.opentofu.org`. You can override this list using the flag [`provider-cache-registry-names`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-registry-names): ```shell terragrunt apply \ --provider-cache \ --provider-cache-registry-names example1.com \ --provider-cache-registry-names example2.com ``` or the environment variable `TG_PROVIDER_CACHE_REGISTRY_NAMES`: ```shell TG_PROVIDER_CACHE=1 \ TG_PROVIDER_CACHE_REGISTRY_NAMES=example1.com,example2.com \ terragrunt apply ``` ## How Terragrunt Provider Caching works [Section titled “How Terragrunt Provider Caching works”](#how-terragrunt-provider-caching-works) * Start a server on localhost. This is the *Terragrunt Provider Cache server*. * Configure OpenTofu/Terraform instances to use the Terragrunt Provider Cache server as a remote registry: * Create local CLI config file `.terraformrc` for each module that concatenates the user configuration from the OpenTofu/Terraform [CLI config file](https://opentofu.org/docs/cli/config/config-file/) with additional sections: * [provider-installation](https://opentofu.org/docs/cli/config/config-file/#provider-installation) forces OpenTofu/Terraform to look for the required providers in the cache directory and create symbolic links to them, if not found, then request them from the remote registry. * [host](https://github.com/hashicorp/terraform/issues/28309) forces OpenTofu/Terraform to [forward](#how-forwarding-requests-through-the-provider-cache-server-works) all provider requests through the Terragrunt Provider Cache server. The address link contains [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) and is unique for each module, used by Terragrunt Provider Cache server to associate modules with the requested providers. * Set environment variables: * [TF\_CLI\_CONFIG\_FILE](https://opentofu.org/docs/cli/config/environment-variables/#tf_plugin_cache_dir) sets to use just created local CLI config `.terragrunt-cache/.terraformrc` * [TF*TOKEN*\*](https://opentofu.org/docs/cli/config/config-file/#environment-variable-credentials) sets per-remote-registry tokens for authentication to Terragrunt Provider Cache server. * Any time Terragrunt is going to run `init`: * Call `tofu/terraform init`. This gets OpenTofu/Terraform to request all the providers it needs from the Terragrunt Provider Cache server. * The Terragrunt Provider Cache server will download the provider from the remote registry, unpack and store it into the cache directory or [create a symlink](#reusing-providers-from-the-user-plugins-directory) if the required provider exists in the user plugins directory. Note that the Terragrunt Provider Cache server will ensure that each unique provider is only ever downloaded and stored on disk once, handling concurrency (from multiple OpenTofu/Terraform and Terragrunt instances) correctly. Along with the provider, the cache server downloads hashes and signatures of the providers to check that the files are not corrupted. * The Terragrunt Provider Cache server returns the HTTP status [*423 Locked*](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/423) to OpenTofu/Terraform. This is because we do *not* want OpenTofu/Terraform to actually download any providers as a result of calling `tofu/terraform init`; we only use that command to request the Terragrunt Provider Cache Server to start caching providers. * At this point, all providers are downloaded and cached, so finally, we run `terragrunt init` a second time, which will find all the providers it needs in the cache, and it’ll create symlinks to them nearly instantly, with no additional downloading. * Note that if a OpenTofu/Terraform module doesn’t have a lock file, OpenTofu/Terraform does *not* use the cache, so it would end up downloading all the providers from scratch. To work around this, we generate `.terraform.lock.hcl` based on the request made by `tofu/terraform init` to the Terragrunt Provider Cache server. Since `terraform init` only requests the providers that need to be added/updated, we can keep track of them using the Terragrunt Provider Cache server and update the OpenTofu/Terraform lock file with the appropriate hashes without having to parse `tf` configs. ### Reusing providers from the user plugins directory [Section titled “Reusing providers from the user plugins directory”](#reusing-providers-from-the-user-plugins-directory) Some plugins for some operating systems may not be available in the remote registries. Thus, the cache server will not be able to download the requested provider. As an example, plugin `template v2.2.0` for `darwin-arm64`, see [Template v2.2.0 does not have a package available - Mac M1](https://discuss.hashicorp.com/t/template-v2-2-0-does-not-have-a-package-available-mac-m1/35099). The workaround is to compile the plugin from source code and put it into the user plugins directory or use the automated solution . For this reason, the cache server first tries to create a symlink from the user’s plugin directory if the required provider already exists there: * %APPDATA%\terraform.d\plugins on Windows * \~/.terraform.d/plugins on other systems ### How forwarding requests through the Provider Cache Server works [Section titled “How forwarding requests through the Provider Cache Server works”](#how-forwarding-requests-through-the-provider-cache-server-works) OpenTofu/Terraform has an official documented setting [network\_mirror](https://developer.hashicorp.com/terraform/cli/config/config-file#network_mirror), that works great, but has one major drawback for the local cache server - the need to use an HTTPS connection with a trusted certificate. Fortunately, there is another way - using the undocumented [host](https://github.com/hashicorp/terraform/issues/28309) setting, which allows OpenTofu/Terraform to create connections to the caching server over HTTP. ### Provider Cache with `providers lock` command [Section titled “Provider Cache with providers lock command”](#provider-cache-with-providers-lock-command) If you run `providers lock` with enabled Terragrunt Provider Cache, Terragrunt creates the provider cache and generates the lock file. ```shell terragrunt run --provider-cache -- providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=freebsd_amd64 ``` ## Configure the Provider Cache Server [Section titled “Configure the Provider Cache Server”](#configure-the-provider-cache-server) Since the Provider Cache Server is essentially a Private Registry server that accepts requests from OpenTofu/Terraform, downloads and saves providers to the cache directory, there are a few more flags that are unlikely to be needed, but are useful to know about: * [`provider-cache-hostname`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-hostname) - Default: `localhost`. * [`provider-cache-port`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-port) - Default: Assigned random port automatically. * [`provider-cache-token`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-token) - Default: Generated randomly. To enhance security, the Terragrunt Provider Cache has authentication to prevent unauthorized connections from third-party applications. You can set your own token using any character set. ```shell terragrunt apply \ --provider-cache \ --provider-cache-host 192.168.0.100 \ --provider-cache-port 5758 \ --provider-cache-token my-secret ``` or using environment variables: ```shell TG_PROVIDER_CACHE=1 \ TG_PROVIDER_CACHE_HOST=192.168.0.100 \ TG_PROVIDER_CACHE_PORT=5758 \ TG_PROVIDER_CACHE_TOKEN=my-secret \ terragrunt apply ``` # Catalog > Learn how to search, browse, and scaffold modules with Terragrunt's catalog ecosystem. Terragrunt’s catalog ecosystem provides two complementary tools for working with OpenTofu/Terraform modules: * **[Catalog TUI](/features/catalog/tui)** — A terminal user interface for browsing and searching your module catalog. * **[Scaffold](/features/catalog/scaffold)** — A command for generating `terragrunt.hcl` files from module templates. You can use Scaffold standalone, or launch it directly from the Catalog TUI by pressing `S` on a selected module. ## Security Configuration [Section titled “Security Configuration”](#security-configuration) The `catalog` block supports security-related configuration options: * `no_shell` (bool): When set to `true`, disables shell command execution in boilerplate templates during scaffolding. This is useful for organizations that want to prevent the use of shell commands in templates for security reasons. * `no_hooks` (bool): When set to `true`, disables hook execution in boilerplate templates during scaffolding. This is useful for organizations that want to prevent the use of hooks in templates for security reasons. Caution Do not use catalog/scaffold to scaffold untrusted templates. IaC configurations are inherently powerful, as they can run arbitrary code on your system, so make sure to only use trusted templates you have reviewed and approved. These configuration values can be overridden by their corresponding CLI flags: ```bash terragrunt catalog --no-shell --no-hooks terragrunt scaffold module-url --no-shell ``` **Priority order**: CLI flags > catalog configuration > defaults (both default to `false`, allowing `shell` and `hooks` to be used by default) ## Custom templates for scaffolding [Section titled “Custom templates for scaffolding”](#custom-templates-for-scaffolding) Terragrunt has a basic template built-in for rendering `terragrunt.hcl` files, but you can provide your own templates to customize how code is generated! Scaffolding is done via [boilerplate](https://github.com/gruntwork-io/boilerplate), and Terragrunt allows you to specify custom boilerplate templates via multiple mechanisms: 1. You can define a custom Boilerplate template in a `.boilerplate` sub-directory of any OpenTofu/Terraform module. 2. You can specify a custom Boilerplate template in the catalog configuration using the `default_template` option. 3. You can specify a custom boilerplate template to use as the second argument of the [`scaffold` command](/features/catalog/scaffold). See the [Catalog TUI](/features/catalog/tui) and [Scaffold](/features/catalog/scaffold) pages for more details on configuring templates. # Scaffold > Learn how to scaffold Terragrunt units. Terragrunt scaffolding can generate files for you automatically using [boilerplate](https://github.com/gruntwork-io/boilerplate) templates. Currently, one boilerplate template is supported out-of-the-box, which you can use to generate a best-practices `terragrunt.hcl` that configures an OpenTofu/Terraform module for deployment: ```bash terragrunt scaffold [TEMPLATE_URL] [--var] [--var-file] [--no-include-root] [--root-file-name] [--no-dependency-prompt] ``` Description: * `MODULE_URL` - This parameter specifies the URL to an OpenTofu/Terraform module. It can be a local file path, git URL, registry URL, or any other [module source URL](https://developer.hashicorp.com/terraform/language/modules/sources). * `TEMPLATE_URL` - This optional parameter specifies the URL to a custom boilerplate template to generate HCL files. It can be a local file path, git URL, registry URL, or any other [module source URL](https://developer.hashicorp.com/terraform/language/modules/sources). If not specified, Terragrunt will: * Look for a `.boilerplate` folder in the module at `MODULE_URL`, and if found, use the boilerplate template in that folder. * Failing to find that, Terragrunt will use a default boilerplate template that is built-in, which creates a simple Terragrunt unit for deploying that OpenTofu/Terraform module. For example, here’s how you can generate a `terragrunt.hcl` file to instantiate an [example MySQL OpenTofu/Terraform module](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/modules/mysql) for deployment: ```bash terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysql ``` This will create a `terragrunt.hcl` in your current working directory, with roughly the following contents: terragrunt.hcl ```hcl # This is a Terragrunt unit generated by Gruntwork Boilerplate (https://github.com/gruntwork-io/boilerplate). terraform { source = "git::https://github.com/gruntwork-io/terragrunt-infrastructure-modules-example.git//modules/mysql?ref=v0.8.1" } inputs = { # -------------------------------------------------------------------------------------------------------------------- # Required input variables # -------------------------------------------------------------------------------------------------------------------- # Type: string # Description: The AWS region to deploy to (e.g. us-east-1) aws_region = "" # TODO: fill in value # Type: string # Description: The name of the DB name = "" # TODO: fill in value # Type: string # Description: The instance class of the DB (e.g. db.t2.micro) instance_class = "" # TODO: fill in value # (... full list of inputs omitted for brevity ...) } ``` Important notes: * The `source` URL is configured for you automatically, with the `ref` pointing to the latest “release” tag of the module (found by scanning git tags). * The `inputs` section is generated for you automatically, and will list all required and optional variables from the module, with their types, descriptions, and defaults, so you can easily fill them in to configure the unit as you like. ## Custom templates for scaffolding [Section titled “Custom templates for scaffolding”](#custom-templates-for-scaffolding) Terragrunt has a basic template built-in for rendering `terragrunt.hcl` files, but you can provide your own templates to customize what code is generated! Scaffolding is done via [boilerplate](https://github.com/gruntwork-io/boilerplate), and Terragrunt allows you to specify custom boilerplate templates via three mechanisms - listed in order of priority: 1. You can specify a custom boilerplate template to use as the second argument of the `scaffold` command. 2. You can define a custom boilerplate template in a `.boilerplate` subfolder of your module. 3. You can define a default custom boilerplate template in the [catalog config](/features/catalog/tui). If you define input variables in your boilerplate template, Terragrunt will prompt users for the values. Those values can also be passed in via `--var` and `--var-file` arguments. There are also a set of variables that Terragrunt will automatically expose to your boilerplate templates for rendering: * `sourceUrl` - URL to module * `requiredVariables` - list of required variables in the unit being scaffolded (see below) * `optionalVariables` - list of optional variables in the unit being scaffolded (see below) The elements in the `requiredVariables` and `optionalVariables` lists are structs with the following fields: * `Name` - variable name * `Description` - variable description * `Type` - variable type (string, number, bool, list, map, object) [Type Constants](https://developer.hashicorp.com/packer/docs/templates/hcl_templates/variables#type-constraints) * `DefaultValue` - variable default value * `DefaultValuePlaceholder` - default value placeholder (e.g. `""` for a string or `0` for a number) Optional variables which can be passed to `scaffold` command: * `Ref` - git tag or branch name for module to be used * `EnableRootInclude` - add in default `terragrunt.hcl` inclusion for the root unit, by default `true` * `RootFileName` - name of the root configuration file, by default `terragrunt.hcl` \* * `SourceUrlType` - if set to `git-ssh` module url will be converted to Git/SSH format * `SourceGitSshUser` - git user for Git/SSH format, by default `git` \* **NOTE**: `RootFileName` is set to `terragrunt.hcl` by default to ensure backwards compatibility, but the pattern of using a `terragrunt.hcl` file at the root of Terragrunt projects has since been deprecated. When the [root-terragrunt-hcl](/reference/strict-controls/active#root-terragrunt-hcl) strict control is enabled, the default configuration file will change to `root.hcl`, which is considered a better practice. For more details, see [Migrating from root `terragrunt.hcl`](/migrate/migrating-from-root-terragrunt-hcl). ### Convenience flags [Section titled “Convenience flags”](#convenience-flags) * `--no-include-root` - Disable inclusion of the root include in the generated `terragrunt.hcl` file (equivalent to using `--var=EnableRootInclude=false`, and will be overridden if the corresponding `var` value is set). * `--root-file-name` - Set the name of the root configuration file to include in the generated `terragrunt.hcl` file (equivalent to using `--var=RootFileName=`, and will be overridden if the corresponding `var` value is set). * `--no-dependency-prompt` - Disable dependency confirmation, but keep the interactive mode enabled (skip asking for confirmation about including dependencies defined in the boilerplate template). \* **NOTE**: `RootFileName` is set to `terragrunt.hcl` by default to ensure backwards compatibility, but the pattern of using a `terragrunt.hcl` file at the root of Terragrunt projects has since been deprecated. See the note above on the [root-terragrunt-hcl](/reference/strict-controls/active#root-terragrunt-hcl) strict control for more information. ## Examples [Section titled “Examples”](#examples) Scaffold new project but use specific module version: ```bash terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs --var=Ref=v0.68.4 ``` Scaffold new project but use Git/SSH URLs: ```bash terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs --var=SourceUrlType=git-ssh ``` terragrunt.hcl ```hcl terraform { source = "git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.68.4" } ``` Scaffold new project using a template from a Git repository: ```bash terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/scaffold/module-with-template # The template from the .boilerplate directory will be used to generate terragrunt.hcl ``` **NOTE**: Scaffolding infrastructure from an external repository might introduce security or stability risks. Always review code from trusted external sources before running it. Scaffold new project using an external template: ```bash terragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template # The files external-template.txt and terragrunt.hcl will be created from that external template ``` # Catalog TUI > Browse and search your module catalog with Terragrunt's interactive terminal UI. Launch the user interface for searching and managing your module catalog. ```bash terragrunt catalog [--no-include-root] [--root-file-name] ``` ![screenshot](/_vercel/image?url=_astro%2Fcatalog-screenshot.yuB4A5KX.png\&w=2048\&q=100) If `` is provided, the repository will be cloned into a temporary directory, otherwise: 1. The repository list are searched in the config file `terragrunt.hcl`. if `terragrunt.hcl` does not exist in the current directory, the config are searched in the parent directories. 2. If the repository list is not found in the configuration file, the modules are looked for in the current directory. An example of how to define the optional default template and the list of repositories for the `catalog` command in the `terragrunt.hcl` configuration file: terragrunt.hcl ```hcl catalog { default_template = "git@github.com/acme/example.git//path/to/template" # Optional default template to use for scaffolding urls = [ "relative/path/to/repo", # will be converted to the absolute path, relative to the path of the configuration file. "/absolute/path/to/repo", "github.com/gruntwork-io/terraform-aws-lambda", # url to remote repository "http://github.com/gruntwork-io/terraform-aws-lambda", # same as above ] no_shell = true # Optional: disable shell commands in boilerplate templates for security no_hooks = true # Optional: disable hooks in boilerplate templates for security } ``` This will recursively search for OpenTofu/Terraform modules in the root of the repo and the `modules` directory and show a table with all the modules. You can then: 1. Search and filter the table: `/` and start typing. 2. Select a module in the table: use the arrow keys to go up and down and next/previous page using the left and right arrow keys. 3. Read the docs for a selected module: `ENTER`. 4. Use [`terragrunt scaffold`](/features/catalog/scaffold) to render a `terragrunt.hcl` for using the module with an interactive form: `s`. Inside the form, `ctrl+d` writes the file even when required fields are still unset, leaving them as `# TODO` placeholders for you to fill in by hand. Catalog Redesign Experiment The interactive form is only available when the [`catalog-redesign`](/reference/experiments/active#catalog-redesign) experiment is enabled. Without the experiment, `s` produces a placeholder-style file with every input marked `# TODO`. ## Scaffolding Flags [Section titled “Scaffolding Flags”](#scaffolding-flags) The following `catalog` flags control behavior of the underlying `scaffold` command launched via `s`: * `--no-include-root` - Do not include the root configuration file in any generated `terragrunt.hcl` during scaffolding. * `--root-file-name` - The name of the root configuration file to include in any generated `terragrunt.hcl` during scaffolding. This value also controls the name of the root configuration file to search for when trying to determine Catalog urls. ## Excluding paths from discovery [Section titled “Excluding paths from discovery”](#excluding-paths-from-discovery) Catalog Redesign Experiment This section describes behavior that is only available when the [`catalog-redesign`](/reference/experiments/active#catalog-redesign) experiment is enabled. Catalog authors can keep directories out of discovery by committing a `.terragrunt-catalog-ignore` file at the root of the repository. Typical uses are skipping `examples/`, `test/`, or integration fixtures that aren’t intended to be scaffolded as modules or templates. The file format borrows from `.gitignore`: * One pattern per line. Blank lines and lines starting with `#` are ignored. * Patterns are matched against repo-relative, forward-slash paths (e.g. `examples/vpc`). A lone `*` does not cross `/`; use `**` to match across path separators. * A leading `!` negates a previous rule, re-including a sibling that an earlier pattern excluded. * Trailing `/` is optional and ignored. * The last matching rule wins. Matching evaluates every path against the rule list in order, so a later `!` pattern can re-include a path that an earlier rule excluded, including a descendant of a previously matched directory. .terragrunt-catalog-ignore ```plaintext # Keep examples out of the catalog. examples examples/** # Skip everything under test/ except test/keep. test/** !test/keep ``` With the file above, a repo containing `modules/vpc/main.tf`, `examples/vpc/main.tf`, `test/drop/main.tf`, and `test/keep/main.tf` surfaces only `modules/vpc` and `test/keep` in the catalog. ### Layering an additional ignore file [Section titled “Layering an additional ignore file”](#layering-an-additional-ignore-file) Pass `--ignore-file ` to load an additional ignore file on top of whatever `.terragrunt-catalog-ignore` is committed at the repo root. The extra rules are appended after the repo’s rules, so last-match-wins semantics let the extra file either add new exclusions or re-include paths the repo file excluded via `!` negation. ```bash terragrunt catalog --ignore-file ./my-ignore-rules ``` This is useful when you don’t control the upstream repository, or when you want to temporarily scope discovery without editing files under version control. It can also be set via the `TG_IGNORE_FILE` environment variable. The path must exist; a missing file is an error. ## Browse tabs [Section titled “Browse tabs”](#browse-tabs) Catalog Redesign Experiment This section describes behavior that is only available when the [`catalog-redesign`](/reference/experiments/active#catalog-redesign) experiment is enabled. The list view is split into three tabs: `All`, `Modules`, and `Templates`. `All` is selected when the TUI launches. Press `tab` to move to the next tab and `shift+tab` to move to the previous; cycling wraps at either end. The active tab filters the list down to components of that kind, and each tab keeps its own cursor position and search filter, so switching back preserves where you were. ## README front-matter [Section titled “README front-matter”](#readme-front-matter) Catalog Redesign Experiment This section describes behavior that is only available when the [`catalog-redesign`](/reference/experiments/active#catalog-redesign) experiment is enabled. Component authors can override the catalog UI’s default title and description by adding a YAML block at the top of the component’s `README.md`. Two forms are accepted. The conventional dash-separated form: ```markdown --- name: VPC App description: A VPC for application workloads. --- # VPC App ... ``` An HTML-comment-wrapped form, which stays invisible on Markdown viewers that render dash-separated front-matter as a horizontal rule. The opening tag must be ` # VPC App ... ``` Both forms are parsed the same way. Recognized keys: * `name`: the component title shown in the list view. When unset, the title falls back to the first `# H1` heading in the README, then to the directory name. * `description`: the short description shown beneath the title in the list view. When unset, it is derived from the README body. A third key, `tags`, controls the colored pills shown next to each component; see the next section. Unknown keys are ignored. ## Component tags [Section titled “Component tags”](#component-tags) Catalog Redesign Experiment This section describes behavior that is only available when the [`catalog-redesign`](/reference/experiments/active#catalog-redesign) experiment is enabled. Catalog authors can attach tags to a component by adding a `tags` key to the component’s `README.md` front-matter. Tags appear as colored pills next to the component in the list view, and as a row above the rendered README in the detail view. Either inline-array or dash-list YAML form is accepted: ```markdown ``` ```markdown ``` Tag values that name a component type also promote the component into that type’s tab. For example, a component classified as a `template` whose tags include `module` appears under both the `Templates` tab (by its native kind) and the `Modules` tab (by tag), without changing how it’s scaffolded. ## Keybindings [Section titled “Keybindings”](#keybindings) Each view has its own keymap. The hint bar at the bottom of the screen shows the most common keys for the current view; the table below covers every key that is bound, including alternates that aren’t advertised. ### List view [Section titled “List view”](#list-view) The default view, shown after discovery completes. | Key | Action | | -------------------------- | -------------------------------------------------- | | `j`, `↓`, `ctrl+n` | Move down one row | | `k`, `↑`, `ctrl+p` | Move up one row | | `l`, `→`, `pgdn`, `ctrl+v` | Next page | | `h`, `←`, `pgup`, `alt+v` | Previous page | | `home`, `ctrl+a` | Jump to first row | | `end`, `ctrl+e` | Jump to last row | | `enter` | View the selected component’s README | | `s` | Scaffold the selected component (interactive form) | | `tab` | Next tab (redesign experiment) | | `shift+tab` | Previous tab (redesign experiment) | | `/` | Search and filter the visible list | | `?` | Toggle the expanded help view | | `q`, `esc` | Quit | | `ctrl+c` | Force quit | ### Component detail (pager) [Section titled “Component detail (pager)”](#component-detail-pager) Active after pressing `enter` on a component. | Key | Action | | -------------------------- | ----------------------------------------- | | `j`, `↓`, `ctrl+n` | Scroll down one line | | `k`, `↑`, `ctrl+p` | Scroll up one line | | `l`, `→`, `pgdn`, `ctrl+v` | Page down | | `h`, `←`, `pgup`, `alt+v` | Page up | | `tab` | Focus the next button in the footer | | `shift+tab` | Focus the previous button in the footer | | `enter` | Activate the focused button | | `s` | Scaffold (interactive form) | | `w` | Toggle soft-wrapping of long README lines | | `?` | Toggle the expanded help view | | `q`, `esc` | Back to the list | | `ctrl+c` | Force quit | ### Interactive scaffold form [Section titled “Interactive scaffold form”](#interactive-scaffold-form) Catalog Redesign Experiment The interactive form is only available when the [`catalog-redesign`](/reference/experiments/active#catalog-redesign) experiment is enabled. The form opens in *navigate* mode. Pressing `enter` on a text field switches to *edit* mode, where most keys are forwarded to the underlying text input; press `esc` to return to navigate mode. #### Navigate mode [Section titled “Navigate mode”](#navigate-mode) | Key | Action | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `j`, `↓` | Next field | | `k`, `↑` | Previous field | | `l`, `→`, `pgdn`, `ctrl+v` | Next page of fields | | `h`, `←`, `pgup`, `alt+v` | Previous page of fields | | `home`, `ctrl+a` | Jump to the first field | | `end`, `ctrl+e` | Jump to the last field | | `enter`, `i` | Enter edit mode on a text field, or toggle a checkbox | | `x` | Unset the focused field, clearing its value and any error (optional fields fall back to their source default; required fields read as missing again) | | `r` | Reset the whole form: unset every field and clear all errors | | `/` | Filter fields by name | | `s` | Scaffold; if any required field is unset, the cursor jumps to the first one and a status line reports how many are missing, prompting you to press `ctrl+d` to write anyway | | `ctrl+d` | Scaffold immediately, leaving unset required fields as `# TODO` | | `esc` | Cancel and return to the previous view | #### Edit mode [Section titled “Edit mode”](#edit-mode) Active while typing into a text field. Keys not listed here are forwarded to the text input. | Key | Action | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `esc` | Finish editing and return to navigate mode | | `enter` | On a text/HCL field, finish editing and return to navigate mode (same as `esc`). On a bool field, toggle between `[x] true` and `[ ] false` | | `tab` | Commit the current field and jump to the next visible one, staying in edit mode | | `shift+tab` | Commit the current field and jump to the previous visible one, staying in edit mode | | `ctrl+d` | Scaffold immediately, skipping the required-field check | ### Welcome screen [Section titled “Welcome screen”](#welcome-screen) Catalog Redesign Experiment The welcome and loading screens are only shown when the [`catalog-redesign`](/reference/experiments/active#catalog-redesign) experiment is enabled. Shown briefly during initial discovery and, if no catalog sources are configured, as a persistent help screen. | Key | Action | | -------------------- | ---------------------------------------------------------------------- | | `h` | Open the catalog docs in your browser (only when no sources are found) | | `q`, `esc`, `ctrl+c` | Exit | # Overview > Learn how to use the --filter flag to target specific infrastructure The `--filter` flag provides a sophisticated querying syntax for targeting specific [units](/features/units) and [stacks](/features/stacks) in Terragrunt commands. This unified approach offers powerful filtering capabilities using a flexible query language. ## Filter Syntax Overview [Section titled “Filter Syntax Overview”](#filter-syntax-overview) The filter syntax allows you to target units and stacks using several different approaches. Usage of the filter flag in a command can look like this: ```bash $ terragrunt find --filter './prod/** | name=web' prod/services/web ``` Where the result of `find` above might have been: ```bash $ terragrunt find prod/services/web prod/services/api prod/data/db dev/services/web dev/services/api dev/data/db ``` For the following file tree: * prod * services * **web** <— Matched by the filter * terragrunt.hcl * api * terragrunt.hcl * data * db * terragrunt.hcl * dev * services * web * terragrunt.hcl * api * terragrunt.hcl * data * db * terragrunt.hcl ## Filter Expressions [Section titled “Filter Expressions”](#filter-expressions) There are several different types of filter expressions, and particular ways in which they can be combined to achieve different results. You can learn more about that below. | Filter Type | Description | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | [Name](/features/filter/name) | Match units and stacks by their name. | | [Path](/features/filter/path) | Match units and stacks by their file system path. | | [Attribute](/features/filter/attributes) | Match units and stacks by their configuration attributes. | | [Negated](/features/filter/combining#negated-expressions) | Exclude units and stacks using the `!` prefix. | | [Intersection](/features/filter/combining#intersection-expressions) | Use the `\|` operator to refine results. | | [Union](/features/filter/combining#union-expressions) | Combine filter results using multiple `--filter` flags. | | [Graph](/features/filter/graph) | Filter units based on their dependency relationships using graph traversal operators. | | [Git](/features/filter/git) | Filter units and stacks based on Git diffs using Git expressions. | ## Usage with Commands [Section titled “Usage with Commands”](#usage-with-commands) The following commands all support the `--filter` flag using the same filtering syntax (note the section below on [special interactions](#special-interactions)): * [find](/reference/cli/commands/find) * [list](/reference/cli/commands/list) * [run](/reference/cli/commands/run) * [hcl fmt](/reference/cli/commands/hcl/fmt) * [hcl validate](/reference/cli/commands/hcl/validate) * [stack run](/reference/cli/commands/stack/run) * [stack generate](/reference/cli/commands/stack/generate) This flag is intended to be a flexible way to target specific infrastructure that allows you to dry-run infrastructure targeting using discovery commands (like `find` and `list`) before running a command that actually affects infrastructure (like `run`). ## Comparison with Queue Control Flags [Section titled “Comparison with Queue Control Flags”](#comparison-with-queue-control-flags) The `--filter` flag provides a unified alternative to multiple queue control flags. | Legacy Flag | Filter Equivalent | | ------------------------------------------ | ------------------------------- | | `--queue-include-dir=./path` | `--filter='./path'` | | `--queue-exclude-dir=./path` | `--filter='!./path'` | | `--queue-include-external` | `--filter='{./**}...'` | | `--queue-include-units-reading=shared.hcl` | `--filter='reading=shared.hcl'` | Queue flag aliases If you are currently using those queue control flags, note that you are actually already using the equivalent filter expressions, as they are aliased internally. You will generally have a better experience using the filter flag instead, as it provides a more powerful and flexible way to target infrastructure. ## Special Interactions [Section titled “Special Interactions”](#special-interactions) Certain commands have special interactions with the `--filter` flag that are worth noting. ### `hcl fmt` [Section titled “hcl fmt”](#hcl-fmt) Unlike when used for most commands, the `--filter` flag is used to filter on individual HCL files when used with the `hcl fmt`. All other commands use `--filter` to filter on units and/or stacks (which are directories). As a result, only path-based filter expressions are supported. Attribute-based filters like `type=unit` or `name=my-app` are not applicable to file-level operations. Example: ```bash # Supported: Path-based expressions terragrunt hcl fmt --filter './prod/**/*.hcl' # Not supported: Attribute-based expressions terragrunt hcl fmt --filter 'type=unit' # This will not work ``` ### `stack generate` [Section titled “stack generate”](#stack-generate) When using `--filter` with `stack generate`, filter expressions will only be recognized if they explicitly target stacks. This is to ensure that filters are not over-applied, preventing any stack generation from occurring. ```bash # Supported: Only generate the stacks that match the filter, as we are explicitly indicating that we are targeting stacks. terragrunt stack generate --filter 'name=prod | type=stack' # Not supported: This filter will be ignored, as we are not explicitly indicating that we are targeting stacks. terragrunt stack generate --filter 'name=prod' # This will not work ``` The reason for this is that stack generation can also be done automatically as part of other commands, like `run`, and thus we need to make it clear that we’re trying to control stack generation rather than run behavior. ```bash # This will run any unit named 'vpc' terragrunt run --all --filter 'vpc' -- plan # This will run any unit named 'vpc', and prevent stack generation in any stack not named 'dev' (including any stacks named 'vpc') terragrunt stack run --filter 'vpc' --filter 'name=dev | type=stack' -- apply ``` # Attribute Expressions > Match units and stacks by their configuration attributes Match units and stacks by their configuration attributes. ```bash # Filter by component type terragrunt find --filter 'type=unit' terragrunt find --filter 'type=stack' # Filter by external dependency status terragrunt find --filter '{./**}... | external=false' terragrunt find --filter '{./**}... | external=true' # Explicitly filter by name (useful for explicitly indicating that this is a name expression) terragrunt find --filter 'name=stack*' ``` * . * **unit1** <— Matched by the first filter * terragrunt.hcl * **stack1** <— Matched by the second and fifth filter * terragrunt.stack.hcl * .. * dependencies <— Note that this directory is sibling to the current working directory * **dependency-of-app1** <— Matched by the fourth filter, but not the third filter * terragrunt.hcl The following are the attributes supported for attribute-based expressions: | Attribute | Description | | --------- | -------------------------------------------------------------------------------------------------------------------------- | | name | Match units and stacks by their directory basename. | | type | Match units and stacks by their type. | | external | Match units and stacks if they are external to the current working directory. | | reading | Match units and stacks by the files they read. | | source | Match units and stacks by their Terraform source URL or path specified in the `terraform` block of `terragrunt.hcl` files. | ## Reading-Based Expressions [Section titled “Reading-Based Expressions”](#reading-based-expressions) Match units and stacks by the files they read. Consider the following file tree: * reading-shared-hcl * terragrunt.hcl * also-reading-shared-hcl * terragrunt.hcl * not-reading-shared-hcl * terragrunt.hcl * shared.hcl Suppose that `reading-shared-hcl` and `also-reading-shared-hcl` both read `shared.hcl` in their configurations, like so: terragrunt.hcl ```hcl locals { shared = read_terragrunt_config(find_in_parent_folders("shared.hcl")) } ``` If you run the command `terragrunt run --all --filter 'reading=shared.hcl' -- plan` from the root folder, both `reading-shared-hcl` and `also-reading-shared-hcl` will be run; not `not-reading-shared-hcl`. This is because the `read_terragrunt_config` HCL function has a special hook that allows Terragrunt to track that it has read the file `shared.hcl`. This hook is used by all native HCL functions that Terragrunt supports which read files. Note, however, that there are certain scenarios where Terragrunt may not be able to track that a file has been read this way. For example, you may be using a bash script to read a file via [`run_cmd`](/reference/hcl/functions/#run_cmd), or reading the file via OpenTofu/Terraform code. To support these use-cases, the [`mark_as_read`](/reference/hcl/functions/#mark_as_read) function can be used to explicitly mark a file as read in the unit. That would look something like this: terragrunt.hcl ```hcl locals { filename = mark_as_read("file-read-by-tofu.txt") } inputs = { filename = local.filename } ``` Caution Due to how Terragrunt parses configurations during `run --all`, functions will only properly mark files as read if they are used outside the `inputs` attribute. Reading a file directly in the `inputs` attribute will not mark the file as read, as the `inputs` attribute is not parsed until after the queue has already been populated to support rendering of dependency outputs, which are only available after dependencies have been run. To mark many files at once, use [`mark_glob_as_read`](/reference/hcl/functions/#mark_glob_as_read), which accepts a glob (with `**` support) and marks every matching file. If a unit’s `terraform` block points at a local module source, enable the [`mark-many-as-read`](/reference/experiments/active#mark-many-as-read) experiment to have Terragrunt automatically mark that module’s `*.tf`, `*.tf.json`, `*.hcl`, `*.tofu`, and `*.tofu.json` files as read. Reading-based filters will then cascade local module changes to every unit that consumes the module. The same experiment also enables the [`mark_glob_as_read`](/reference/hcl/functions/#mark_glob_as_read) HCL function. ## Source-Based Expressions [Section titled “Source-Based Expressions”](#source-based-expressions) Match units and stacks by their Terraform source URL or path specified in the `terraform` block of `terragrunt.hcl` files. ```bash # Filter by exact source match terragrunt find --filter 'source=github.com/acme/foo' terragrunt find --filter 'source=gitlab.com/example/baz' terragrunt find --filter 'source=./module' # Filter by source using glob patterns terragrunt find --filter 'source=*github.com**acme/*' terragrunt find --filter 'source=git::git@github.com:acme/**' terragrunt find --filter 'source=**github.com**' terragrunt find --filter 'source=gitlab.com/**' ``` * . * **github-acme-foo** <— Matched by source=github.com/acme/foo and source=*github.com\*\*acme/* * terragrunt.hcl (source: github.com/acme/foo) * **github-acme-bar** <— Matched by source=*github.com\*\*acme/* and source=git:::acme/\*\* * terragrunt.hcl (source: git:::acme/bar) * **gitlab-example-baz** <— Matched by source=gitlab.com/example/baz and source=gitlab.com/\*\* * terragrunt.hcl (source: gitlab.com/example/baz) * **local-module** <— Matched by source=./module * terragrunt.hcl (source: ./module) * module * main.tf * other-unit * terragrunt.hcl (source: s3://bucket/module) Note The `source=` filter matches against the Terraform source URL or path specified in the `terraform` block of `terragrunt.hcl` files in units. It supports glob patterns, allowing you to match multiple sources with patterns like `*github.com**` or `gitlab.com/**`. This is useful for filtering units that use specific module sources, such as all units using a particular GitHub organization’s modules or all local modules. This attribute may be supported on stacks in the future. # Combining Expressions > Combine filter expressions using negation, intersection, and union operators ## Negated Expressions [Section titled “Negated Expressions”](#negated-expressions) Negate filter expressions using the `!` prefix. Negated expressions are always evaluated after all positive expressions have been evaluated. ```bash # Exclude by name terragrunt find --filter '!app1' # Exclude by path terragrunt find --filter '!./prod/**' # Exclude by type terragrunt find --filter '!type=stack' ``` * envs * prod * apps * app1 <— Excluded by *both* the first and second filter * terragrunt.hcl * app2 <— Matched by all filters *except* the second filter * terragrunt.hcl * stacks * **stack1** <— Excluded by *both* the second and third filter * terragrunt.stack.hcl * stage * apps * **app1** <— Matched by all filters *except* the first filter * terragrunt.hcl * **app2** <— Matched by all filters * terragrunt.hcl * stacks * **stack1** <— Matched by all filters *except* the third filter * terragrunt.stack.hcl ## Intersection Expressions [Section titled “Intersection Expressions”](#intersection-expressions) Use the `|` operator to refine results from left to right. Results must match all filters in the chain to be included. ```bash # Find all components in ./prod/** that are also units terragrunt find --filter './prod/** | type=unit' # Find all components in ./prod/** that are not units terragrunt find --filter './prod/** | !type=unit' # You can chain as many filters as you want to further refine the results terragrunt find --filter './dev/** | type=unit | !name=unit1' ``` * prod * units * **unit1** <— Matched by the first filter * terragrunt.hcl * **unit2** <— Matched by the first filter * terragrunt.hcl * stacks * **stack1** <— Matched by the second filter * terragrunt.stack.hcl * **stack2** <— Matched by the second filter * terragrunt.stack.hcl * dev * units * unit1 * terragrunt.hcl * **unit2** <— Matched by the third filter * terragrunt.hcl * stacks * stack1 * terragrunt.stack.hcl * stack2 * terragrunt.stack.hcl ### Path resolution in intersection chains [Section titled “Path resolution in intersection chains”](#path-resolution-in-intersection-chains) In an intersection chain (`A | B | C`), the left-most expression determines which [**components**](/getting-started/terminology#component) flow through the rest of the chain. Each component carries its own **discovery context**, including a working directory where the component was discovered. Relative path expressions (like `./apps/*`) in any part of the chain resolve against the working directory of the component’s discovery context — **not** the user’s current working directory. These two are the same when discovering components in the current working directory. ```bash # Referencing the file tree above terragrunt run --working-dir prod --filter './units/** | unit1' ``` When using [Git expressions](/features/filter/git), components are discovered in temporary Git worktrees (see [How it works](/features/filter/git#how-it-works) for more details. As such, relative paths resolve relative to the root of the Git worktree where the component was discovered. ```bash # Note that we still need to specify 'prod' here in the path. terragrunt run --working-dir prod --filter '[main...HEAD] | ./prod/units/**' ``` ## Union Expressions [Section titled “Union Expressions”](#union-expressions) Specify multiple `--filter` flags to merge results from multiple filters. ```bash # Find components named 'unit1' and 'stack1' terragrunt find --filter unit1 --filter stack1 # Find components in ./envs/prod/* and ./envs/stage/* terragrunt find --filter './envs/prod/*' --filter './envs/stage/*' # Find components named 'stack2' _except_ those in ./envs/prod/* and ./envs/stage/* terragrunt find --filter stack2 --filter '!./envs/prod/**' --filter '!./envs/stage/**' ``` * envs * prod * **unit1** <— Matched by the first filter *and* the second filter * terragrunt.hcl * **unit2** <— Matched by the second filter * terragrunt.hcl * **stack1** <— Matched by the first filter *and* the second filter * terragrunt.stack.hcl * **stack2** <— Matched by the second filter * terragrunt.stack.hcl * stage * **unit1** <— Matched by the first filter *and* the second filter * terragrunt.hcl * **unit2** <— Matched by the second filter * terragrunt.hcl * **stack1** <— Matched by the first filter *and* the second filter * terragrunt.stack.hcl * **stack2** <— Matched by the second filter * terragrunt.stack.hcl * dev * **unit1** <— Matched by the first filter * terragrunt.hcl * unit2 * terragrunt.hcl * **stack1** <— Matched by the first filter * terragrunt.stack.hcl * **stack2** <— Matched by the third filter * terragrunt.stack.hcl ### Unions of negated filters [Section titled “Unions of negated filters”](#unions-of-negated-filters) When a filter query starts with a negation (`!`), the result is applied after *all* positive filters have been applied. This means that if you have a filter query like this: ```bash terragrunt find --filter '!type=unit' --filter 'name=unit1' ``` The result will be the components that are not units *and* are named `unit1`. This means that you should be able to expect negative filters to take effect regardless of how other positive filters may result in the addition of results. Using Negated Expressions If you have infrastructure that you *never* want to run, you can consider leveraging the [`--filters-file`](/features/filter/filters-file) to automatically negate them. ### Unions with Git expressions [Section titled “Unions with Git expressions”](#unions-with-git-expressions) Union deduplication in filter results is based on the **absolute path** of each discovered component. When combining [Git expressions](/features/filter/git) with non-Git expressions in a union, it might seem like the same unit has appeared twice. e.g. ```bash $ terragrunt find --filter '[main...HEAD]' --filter ./live/foo live/foo live/foo ``` The reason for this is usually that a unit with the same name has been discovered twice: once discovered from your current working directory and once from a git worktree. From Terragrunt’s perspective, these are two different units and can be operated on independently. If your intent is not to use Terragrunt in this way (to operate on units in temporary Git worktrees and units in your current working directory independently), you can use the `find` command instead to perform the logic of discovering components that have changed between Git references, then separately perform a `run` against units in your current working directory. ```bash terragrunt find --filter '[main...HEAD]' | awk '{printf "{%s}\n", $0}' > /tmp/diffs.txt terragrunt run --all --filters-file /tmp/diffs.txt -- plan ``` Explicit path filters Note the use of `awk` here to wrap paths in `{}`. This is to explicitly mark the paths as [path expressions](/features/filter/path). This is only necessary for disambiguation between [path expressions](/features/filter/path) and [name expressions](/features/filter/name). If you aren’t concerned with this ambiguity, you can omit this step. Supporting destroys Note that this approach won’t support destroys. For Git expressions to support destroys, Terragrunt needs to be able to perform runs on units that might not exist in the current working directory (as they’ve been removed in a commit). If you want to support the full infrastructure lifecycle with Git expressions, you will want to use Git expressions directly in runs, or add additional tooling. # Filters File > Use a filters file to define reusable filter expressions for Terragrunt If you want to ensure that certain units are always included or excluded, you can use a filters file. Filters files are simple text files that contain filter expressions delimited by newlines. Empty lines and lines starting with `#` are ignored. filters.txt ```text ./subtree/** !./subtree/dependency/** ``` ```bash terragrunt run --all --filters-file filters.txt -- plan ``` Running Terragrunt like this is equivalent to running it with the following flags: ```bash terragrunt run --all --filter './subtree/**' --filter '!./subtree/dependency/**' -- plan ``` Tip If you want to automatically configure certain filters to be applied by default, you can name the filters file `.terragrunt-filters`. Terragrunt will automatically read filters from this file if it exists in the current working directory. # Git Expressions > Filter units and stacks based on Git diffs using Git expressions Match units and stacks based on changes between Git references. This is useful for targeting infrastructure that has been modified, added, or removed between commits, branches, or tags. Git-based expressions are written between `[` and `]` characters, and use the `...` operator to indicate the range of changes to compare. ```bash # Compare between two references terragrunt find --filter '[main...HEAD]' # Shorthand: compare reference to HEAD terragrunt find --filter '[main]' # Compare between specific commits terragrunt find --filter '[abc123...def456]' # Compare between tags terragrunt find --filter '[v1.0.0...v2.0.0]' # Compare using relative references terragrunt find --filter '[HEAD~1...HEAD]' # Compare between branches terragrunt find --filter '[feature-branch...main]' ``` * . * **modified-unit** <— Matched by \[main…HEAD] (terragrunt.hcl was modified) * terragrunt.hcl (modified) * **new-unit** <— Matched by \[main…HEAD] (terragrunt.hcl was added) * terragrunt.hcl (added) * **removed-unit** <— Matched by \[main…HEAD] (terragrunt.hcl was removed) * (directory removed) * unchanged-unit * terragrunt.hcl (unchanged) ## How it works [Section titled “How it works”](#how-it-works) When evaluating a Git-based filter, Terragrunt will first generate a worktree for every reference that needs to be evaluated, and assess the diffs between Git references. e.g. For a filter like `[main...HEAD]`, Terragrunt will generate a worktree for `main` and one for `HEAD` in temporary directories, and use `git diff` to assess the diffs between the two references. Then, for any unit that is discovered within those worktrees, Terragrunt will enqueue that unit for a run in the run queue *in the worktree where it was discovered*. In the example above, the `modified-unit` will be discovered in a “to” temporary directory (e.g. `/tmp/.../terragrunt-worktree-HEAD.../modified-unit`), whereas the `removed-unit` would be discovered in the “from” temporary directory (e.g. `/tmp/.../terragrunt-worktree-main.../removed-unit`). This is important to recognize, as it’s how destroys will be possible despite the unit no longer being present in the current working directory. As a consequence, however, you may find that paths don’t behave how you expect, as you will be performing runs in the temporary directories created for the relevant worktrees. Remote State Recommended When using Git-based filter expressions (e.g. `[HEAD~1...HEAD]`), it is **strongly recommended** to use remote state configurations. Units discovered using Git-based filter expressions may not properly detect dependency outputs when using local state, which can lead to unexpected outcomes such as mock outputs being used instead of actual dependency outputs. Testing with Local State If you need to test with local state while using Git-based filter expressions, you can work around the limitations by placing state files in a separate location using absolute paths in your `remote_state` configuration. This ensures that state files are stored consistently regardless of which worktree directory Terragrunt is operating in. For example, instead of using a relative path: ```hcl remote_state { backend = "local" config = { path = "${get_parent_terragrunt_dir()}/.state/${path_relative_to_include()}/tofu.tfstate" } } ``` Consider using an absolute path to a shared location: ```hcl remote_state { backend = "local" config = { path = "/tmp/terragrunt-state/${path_relative_to_include()}/tofu.tfstate" } } ``` Note that this is a workaround for testing purposes only. For production use, remote state backends (such as S3, GCS, or Azure Storage) are strongly recommended. ## Interaction with the run command [Section titled “Interaction with the run command”](#interaction-with-the-run-command) When using Git-based expressions and the `run` command, you are required to use one of the `plan` or `apply` commands, and not the `-destroy` flag. This is because whether a unit will be destroyed is determined by logic relevant to inspecting changes in Git. When units are added or modified between two Git references, they will be planned or applied. When the units are removed between two Git references, they will be planned for destruction (with `plan -destroy`) or destroyed (with `apply -destroy`). In the scenario above, running the following: ```bash terragrunt run --all --filter '[main...HEAD]' -- plan ``` Will result in the following: * `modified-unit` will be planned (`tofu plan`) * `new-unit` will be planned (`tofu plan`) * `removed-unit` will be planned for destruction (`tofu plan -destroy`) * `unchanged-unit` will be ignored Allowing destroys When using Git-based filter expressions and the run command, Terragrunt won’t destroy units that are removed between the two Git references unless you use the `--filter-allow-destroy` flag. ```bash terragrunt run --all --filter '[main...HEAD]' --filter-allow-destroy -- destroy ``` This is a safeguard to prevent unintended destruction of infrastructure. ## The `--filter-affected` flag [Section titled “The --filter-affected flag”](#the---filter-affected-flag) For the common use case of comparing the default branch (typically `main`) with `HEAD`, you can use the `--filter-affected` flag as a convenient shorthand: ```bash # Find components affected by changes between main and HEAD terragrunt find --filter-affected # Equivalent to: terragrunt find --filter '[main...HEAD]' ``` The `--filter-affected` flag automatically detects your repository’s default branch and compares it with `HEAD`. # Graph Expressions > Filter units based on their dependency relationships using graph traversal operators Filter units and stacks based on their dependency relationships using graph traversal operators. This allows you to find components that depend on a target, or components that a target depends on. Graph-based expressions use the ellipsis (`...`) operator to indicate graph traversal direction and the caret (`^`) operator to exclude the target from results. ## Include Dependencies [Section titled “Include Dependencies”](#include-dependencies) Use `...` *after* a target expression to include the target and all of its dependencies: ```bash # Find 'service' and everything it depends on terragrunt find --filter 'service...' ``` * . * **service** <— Matched (target) * terragrunt.hcl (depends on: db, cache, vpc) * **db** <— Matched (dependency of service) * terragrunt.hcl (depends on: vpc) * **cache** <— Matched (dependency of service) * terragrunt.hcl (depends on: vpc) * **vpc** <— Matched (dependency of service, db, cache) * terragrunt.hcl ## Include Dependents [Section titled “Include Dependents”](#include-dependents) Use `...` *before* a target expression to include the target and all components that depend on it: ```bash # Find 'vpc' and everything that depends on it terragrunt find --filter '...vpc' ``` * . * **vpc** <— Matched (target) * terragrunt.hcl * **db** <— Matched (depends on vpc) * terragrunt.hcl (depends on: vpc) * **cache** <— Matched (depends on vpc) * terragrunt.hcl (depends on: vpc) * **service** <— Matched (depends on vpc via db and cache) * terragrunt.hcl (depends on: db, cache) ## Include Both Directions [Section titled “Include Both Directions”](#include-both-directions) Use `...` *before and after* a target expression to include a target, all its dependencies, and all its dependents: ```bash # Find 'db' and its complete dependency graph terragrunt find --filter '...db...' ``` * . * **vpc** <— Matched (dependency of db) * terragrunt.hcl * **db** <— Matched (target) * terragrunt.hcl (depends on: vpc) * **service** <— Matched (depends on db) * terragrunt.hcl (depends on: db, cache) ## Exclude Target [Section titled “Exclude Target”](#exclude-target) Use `^` before a target expression to exclude the target from results. This is useful when you want only the dependencies or dependents, but not the target itself: ```bash # Find all dependents of 'vpc' but exclude 'vpc' itself terragrunt find --filter '...^vpc' ``` * . * vpc <— Not matched (due to ’^’ operator) * terragrunt.hcl * **db** <— Matched (depends on vpc) * terragrunt.hcl (depends on: vpc) * **cache** <— Matched (depends on vpc) * terragrunt.hcl (depends on: vpc) * **service** <— Matched (depends on vpc via db and cache) * terragrunt.hcl (depends on: db, cache) ## Depth-Limited Traversal [Section titled “Depth-Limited Traversal”](#depth-limited-traversal) You can limit how many levels of dependencies or dependents to traverse by adding a numeric depth before or after the ellipsis (`...`) operator. This is useful when you only want immediate or nearby relationships rather than the full transitive closure. ```bash # Find 'service' and only its direct dependencies (1 level deep) terragrunt find --filter 'service...1' # Find 'vpc' and only components that directly depend on it (1 level) terragrunt find --filter '1...vpc' # Find 'db' with 2 levels of dependencies and 1 level of dependents terragrunt find --filter '1...db...2' ``` Given this dependency graph where service depends on db and cache, which both depend on vpc: * . * vpc * terragrunt.hcl * db * terragrunt.hcl (depends on: vpc) * cache * terragrunt.hcl (depends on: vpc) * service * terragrunt.hcl (depends on: db, cache) Using `service...1` (dependencies with depth 1): * . * vpc <— Not matched (2 hops away, beyond depth limit) * terragrunt.hcl * **db** <— Matched (1 hop from service) * terragrunt.hcl (depends on: vpc) * **cache** <— Matched (1 hop from service) * terragrunt.hcl (depends on: vpc) * **service** <— Matched (target) * terragrunt.hcl (depends on: db, cache) Multiple Targets and Depth When a filter matches multiple targets, the depth limit applies independently to each target. If a component is reachable from multiple targets at different distances, it will be included if it is within the depth limit of *any* target. For example, if target A can reach component X in 3 hops and target B can reach X in 1 hop, with a depth limit of 2, X will be included because it is within 2 hops of target B. Tip Graph expressions require dependency/dependent information to work correctly. When using graph expressions, Terragrunt automatically discovers dependency relationships between components to enable graph traversal. This may add some overhead compared to simple name or path filters, as Terragrunt will need to recursively parse and evaluate HCL files to determine whether there are more dependencies or dependents to include. Note that this overhead is especially noticeable in *dependent* graph expressions, as Terragrunt will need to recursively parse *all* units that could *possibly* depend on the target. Use this expression judiciously. # Name Expressions > Match units and stacks by their name Match units and stacks by their name. This is the simplest form of filtering. ```bash # Exact match terragrunt find --filter app1 # Glob pattern terragrunt find --filter 'app*' ``` * apps * **app1** <— Matched by the first and second filter * terragrunt.hcl * **app2** <— Matched only by the second filter * terragrunt.hcl * other * terragrunt.hcl Note Note that `app1` and `app2` were selected *within* the `apps` directory. Filtering on names will match *any* unit/stack that has a directory basename name matching a filter. # Path Expressions > Match units and stacks by their file system path Match units and stacks by their file system path. ```bash # Relative paths terragrunt find --filter './envs/prod/apps/app1' terragrunt find --filter './envs/stage/**' # Absolute paths terragrunt find --filter '/absolute/path/to/envs/dev/apps/*' # Wrapped paths (useful for explicitly indicating that this is a path expression) terragrunt find --filter '{./envs/prod/apps/app2}' ``` * envs * prod * apps * **app1** <— Matched by the first filter * terragrunt.hcl * **app2** <— Matched by the fourth filter * terragrunt.hcl * stage * apps * **app1** <— Matched by the second filter * terragrunt.hcl * **app2** <— Also matched by the second filter * terragrunt.hcl * dev * apps * **app1** <— Matched by the third filter * terragrunt.hcl * **app2** <— Also matched by the third filter * terragrunt.hcl Note Note that globs used in path-based expressions will not recursively match nested directories unless you use the `**` wildcard. (That’s why `./envs/stage/**` is used above) Note Glob patterns must use Unix forward slashes `/` to separate directories, even on Windows machines. # Stacks > Learn how to work with multiple units at once using implicit and explicit stacks. A **stack** in Terragrunt is a collection of related units that can be managed together. Stacks provide a way to: * Deploy multiple infrastructure components with a single command * Manage dependencies between units automatically * Control the blast radius of changes * Organize infrastructure into logical groups Terragrunt supports two approaches to defining stacks: 1. **Implicit Stacks**: Created by organizing units in a directory structure. 2. **Explicit Stacks**: Defined using `terragrunt.stack.hcl` files. ## Choosing Between Implicit and Explicit Stacks [Section titled “Choosing Between Implicit and Explicit Stacks”](#choosing-between-implicit-and-explicit-stacks) ### Use Implicit Stacks When: [Section titled “Use Implicit Stacks When:”](#use-implicit-stacks-when) * You have a small number of units. * Each unit is unique and not repeated across environments. * You don’t mind a high file count. * You’re just getting started with Terragrunt. * You need maximum explicitness and transparency. ### Use Explicit Stacks When: [Section titled “Use Explicit Stacks When:”](#use-explicit-stacks-when) * You have multiple environments (dev, staging, prod). * You want to reuse collections of related infrastructure patterns. * You have many similar units that differ only in values. * You want to version collections of infrastructure patterns. * You’re building infrastructure catalogs or templates. Tip Start with implicit stacks to get familiar with the concept, then gradually introduce explicit stacks for reusable patterns as your infrastructure grows. ## Examples [Section titled “Examples”](#examples) For detailed examples, see the [Gruntwork Terragrunt Infrastructure Catalog Stack Examples](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/examples/terragrunt/stacks). These have full-featured examples of stacks that deploy real, stateful infrastructure in an AWS account. # Explicit Stacks > Define stacks using terragrunt.stack.hcl files for reusable patterns. For an alternate approach (that is more flexible, but not necessarily always the better solution), you can define explicit stacks using `terragrunt.stack.hcl` files. These are **blueprints** that programmatically generate units at runtime. ### What is a terragrunt.stack.hcl file? [Section titled “What is a terragrunt.stack.hcl file?”](#what-is-a-terragruntstackhcl-file) A `terragrunt.stack.hcl` file is a blueprint that defines how to generate Terragrunt configurations programmatically. It tells Terragrunt: * What units to create. * Where to get their configurations from. * Where to place them in the directory structure. * What values to pass to each unit. Caution It is invalid for a unit to contain a `terragrunt.stack.hcl` file and for a stack to contain a `terragrunt.hcl` file. If either is true, Terragrunt will throw an error. This is to prevent ambiguity in deciding whether a component is a unit or a stack. ### Supported Configuration Blocks [Section titled “Supported Configuration Blocks”](#supported-configuration-blocks) #### `unit` blocks - Define Individual Infrastructure Components [Section titled “unit blocks - Define Individual Infrastructure Components”](#unit-blocks---define-individual-infrastructure-components) * **Purpose**: Define a single, deployable piece of infrastructure. * **Use case**: When you want to create a single piece of isolated infrastructure (e.g. a specific VPC, database, or application). * **Result**: Generates a directory with a single `terragrunt.hcl` file in the specified `path` from the specified `source`. #### `stack` blocks - Define Reusable Infrastructure Patterns [Section titled “stack blocks - Define Reusable Infrastructure Patterns”](#stack-blocks---define-reusable-infrastructure-patterns) * **Purpose**: Define a stack of units to be deployed together. * **Use case**: When you have a common, multi-unit pattern (like “dev environment” or “three-tier web application”) that you want to deploy multiple times. * **Result**: Generates a directory with another `terragrunt.stack.hcl` file in the specified `path` from the specified `source`. ### Example: Simple Stack with Units [Section titled “Example: Simple Stack with Units”](#example-simple-stack-with-units) terragrunt.stack.hcl ```hcl unit "vpc" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1" path = "vpc" values = { vpc_name = "main" cidr = "10.0.0.0/16" } } unit "database" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1" path = "database" values = { engine = "postgres" version = "13" vpc_path = "../vpc" } } ``` Running `terragrunt stack generate` in the directory containing that `terragrunt.stack.hcl` file generates: * .terragrunt-stack * vpc * terragrunt.hcl * terragrunt.values.hcl * database * terragrunt.hcl * terragrunt.values.hcl Note Note that the contents are generated into a `.terragrunt-stack` directory. This is to make it convenient to add `.terragrunt-stack` to your `.gitignore` file, and always generate the stack on demand instead of checking it in. Also note the `terragrunt.values.hcl` files generated next to the `terragrunt.hcl` files of the units. These files contain the values specified in the `values` block for the unit. ### Example: Nested Stack with Reusable Patterns [Section titled “Example: Nested Stack with Reusable Patterns”](#example-nested-stack-with-reusable-patterns) terragrunt.stack.hcl ```hcl stack "dev" { source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1" path = "dev" values = { environment = "development" cidr = "10.0.0.0/16" } } stack "prod" { source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1" path = "prod" values = { environment = "production" cidr = "10.1.0.0/16" } } ``` The referenced stack might contain: stacks/environment/terragrunt.stack.hcl ```hcl unit "vpc" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1" path = "vpc" values = { vpc_name = values.environment cidr = values.cidr } } unit "database" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1" path = "database" values = { environment = values.environment vpc_path = "../vpc" } } ``` Running `terragrunt stack generate` in the directory containing that `terragrunt.stack.hcl` file generates: * .terragrunt-stack * dev * terragrunt.stack.hcl * .terragrunt-stack * vpc * terragrunt.hcl * terragrunt.values.hcl * database * terragrunt.hcl * terragrunt.values.hcl * prod * terragrunt.stack.hcl * .terragrunt-stack * vpc * terragrunt.hcl * terragrunt.values.hcl * database * terragrunt.hcl * terragrunt.values.hcl ### Working with Explicit Stacks [Section titled “Working with Explicit Stacks”](#working-with-explicit-stacks) ```bash # Generate units from the `terragrunt.stack.hcl` file in the current # working directory (and all stacks in child directories). terragrunt stack generate # Deploy all generated units defined using the `terragrunt.stack.hcl` file # in the current working directory (and any units generated by stacks in this file). # # Note that this will also automatically generate the stack if it is not already generated. terragrunt stack run apply ``` ### Advantages of Explicit Stacks [Section titled “Advantages of Explicit Stacks”](#advantages-of-explicit-stacks) * **Reusability**: Define patterns once, reuse them across environments. * **Consistency**: Ensure all environments follow the same structure. * **Version Control**: Version collections of infrastructure patterns alongside the units of infrastructure that make them up. * **Automation**: Generate complex infrastructure from simple blueprints. * **Flexibility**: Easy to create variations with different values. ### Limitations of Explicit Stacks [Section titled “Limitations of Explicit Stacks”](#limitations-of-explicit-stacks) * **Complexity**: Requires understanding another configuration file. * **Generation Overhead**: Units must be generated before use. * **Debugging**: Generated files can be harder to debug if you accidentally generate files that are not what you intended. ## Declaring Dependencies Between Units [Section titled “Declaring Dependencies Between Units”](#declaring-dependencies-between-units) Experimental The `autoinclude` block is part of the [`stack-dependencies` experiment](/reference/experiments/active#stack-dependencies). Enable it with `--experiment stack-dependencies`. A unit in a stack can depend on a sibling unit without the catalog source knowing anything about the relationship. Add an `autoinclude` block to the `unit` block and declare the `dependency` there: live/terragrunt.stack.hcl ```hcl unit "vpc" { source = "../catalog/units/vpc" path = "vpc" } unit "app" { source = "../catalog/units/app" path = "app" autoinclude { dependency "vpc" { config_path = unit.vpc.path mock_outputs = { vpc_id = "vpc-mock" } } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } } } ``` The `unit.vpc.path` variable resolves to the generated directory of the `vpc` unit. Running `terragrunt stack generate --experiment stack-dependencies` writes the dependency into a `terragrunt.autoinclude.hcl` file next to the `app` unit’s `terragrunt.hcl`: * terragrunt.stack.hcl * .terragrunt-stack * vpc * terragrunt.hcl * app * **terragrunt.autoinclude.hcl** * terragrunt.hcl .terragrunt-stack/app/terragrunt.autoinclude.hcl ```hcl # Generated by Terragrunt from autoinclude block. Do not edit manually. dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "vpc-mock" } } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } ``` Terragrunt merges this file into the `app` unit’s configuration whenever the unit is parsed, so `run --all` orders `vpc` before `app`, and `app` receives `vpc_id` as an input. The `vpc` unit’s own `terragrunt.hcl` stays untouched. See the [`autoinclude` block reference](/reference/hcl/blocks#autoinclude) for the full contract: what an autoinclude body may contain, how the merge works, stack-level autoincludes, and depending on entire stacks. ## Stack Outputs [Section titled “Stack Outputs”](#stack-outputs) When defining a stack using a `terragrunt.stack.hcl` file, you also have the ability to interact with the aggregated outputs of all the units in the stack from the CLI. To do this, use the [`stack output`](/reference/cli/commands/stack/output) command (not the [`stack run output`](/reference/cli/commands/stack/run) command). ```bash $ terragrunt stack output backend_app = { domain = "backend-app.example.com" } frontend_app = { domain = "frontend-app.example.com" } mysql = { endpoint = "terraform-20250504140737772400000001.abcdefghijkl.us-east-1.rds.amazonaws.com" } valkey = { endpoint = "serverless-valkey-01.amazonaws.com" } vpc = { vpc_id = "vpc-1234567890" } ``` This returns a single aggregated HCL object aggregating all the outputs for all the units within the stack. Tip You can use the `--format json` flag to get the output in JSON format, which can be useful for accessing values programmatically. ## Using Local State with Stacks [Section titled “Using Local State with Stacks”](#using-local-state-with-stacks) When using Explicit Stacks, you might want to use local state files instead of remote state for development, testing, or specific use cases. However, this presents a challenge because the generated `.terragrunt-stack` directory can be safely deleted and regenerated using `terragrunt stack clean && terragrunt stack generate`, which would normally cause local state files to be lost. To solve this problem, you can configure your stack to store state files outside of the `.terragrunt-stack` directory, in a persistent location that survives stack regeneration. ### Configuration [Section titled “Configuration”](#configuration) Here’s how to configure local state that persists across stack regeneration: **1. Create a `root.hcl` file with local backend configuration:** root.hcl ```hcl remote_state { backend = "local" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { path = "${get_parent_terragrunt_dir()}/.terragrunt-local-state/${path_relative_to_include()}/tofu.tfstate" } } ``` **2. Create your stack definition:** live/terragrunt.stack.hcl ```hcl unit "vpc" { source = "${find_in_parent_folders("units/vpc")}" path = "vpc" } unit "database" { source = "${find_in_parent_folders("units/database")}" path = "database" } unit "app" { source = "${find_in_parent_folders("units/app")}" path = "app" } ``` **3. Configure your units to include the root configuration:** units/vpc/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "." } ``` **4. Add a `.gitignore` file to exclude state files from version control:** .gitignore ```text .terragrunt-local-state ``` **Important:** Local state files should never be committed to version control as they may contain sensitive information and can cause conflicts when multiple developers work on the same infrastructure. ### How It Works [Section titled “How It Works”](#how-it-works) The key insight is using `path_relative_to_include()` in the state path configuration. This function returns the relative path from each unit to the `root.hcl` file, creating unique state file paths like: ```text .terragrunt-local-state/live/.terragrunt-stack/vpc/tofu.tfstate .terragrunt-local-state/live/.terragrunt-stack/database/tofu.tfstate .terragrunt-local-state/live/.terragrunt-stack/app/tofu.tfstate ``` Since these state files are stored in `.terragrunt-local-state/` (outside of `.terragrunt-stack/`), they persist when you run: ```bash terragrunt stack clean && terragrunt stack generate ``` ### Directory Structure [Section titled “Directory Structure”](#directory-structure) After running the stack, your directory structure will look like this: * . * root.hcl * .gitignore (Excludes .terragrunt-local-state) * .terragrunt-local-state/ (Persistent state files - ignored by git) * live/ * .terragrunt-stack/ * vpc/ * tofu.tfstate * database/ * tofu.tfstate * app/ * tofu.tfstate * live/ * terragrunt.stack.hcl * .terragrunt-stack/ (Generated stack - can be deleted) * vpc/ * terragrunt.hcl * main.tf * database/ * terragrunt.hcl * main.tf * app/ * terragrunt.hcl * main.tf * units/ (Reusable unit definitions) * vpc/ * … * database/ * … * app/ * … ## CAS Integration [Section titled “CAS Integration”](#cas-integration) Tip This functionality is part of the [`cas` experiment](/reference/experiments/active#cas). Enable it with `--experiment cas`. See the [CAS feature documentation](/features/caching/cas) for more details. When authoring stacks in a catalog, you can use the `update_source_with_cas` attribute on nested blocks to replace remote Git URLs with relative paths. Without CAS, catalog authors need to plumb remote Git URLs through `values` expressions so that generated files reference the correct remote sources: stacks/service/terragrunt.stack.hcl ```hcl unit "service" { source = "git::git@github.com:acme/catalog.git//units/service?ref=${values.version}" path = "service" values = { version = values.version } } ``` This is error-prone and creates unnecessary diffs when the version changes. It also requires a network fetch for every nested source during generation. With `update_source_with_cas`, you can use relative paths instead: stacks/service/terragrunt.stack.hcl ```hcl unit "service" { source = "../..//units/service" update_source_with_cas = true path = "service" } ``` The `//` separator works the same as with any Terragrunt source. The part before `//` is the base path, and the part after `//` is the subdirectory. The attribute can also be used in `terragrunt.hcl` files within the `terraform` block: units/service/terragrunt.hcl ```hcl terraform { source = "../..//modules/service" update_source_with_cas = true } ``` When `update_source_with_cas = true` is set: 1. Terragrunt clones the catalog repository once via CAS and stores the content. 2. It follows relative source paths within the cloned repository. 3. At each level, it rewrites the `source` attribute to a `cas::` reference (e.g., `cas::sha1:abc123...`) that points to stored CAS content. 4. Generated `.terragrunt-stack` files contain deterministic CAS references instead of remote URLs. 5. Subsequent generation waves resolve `cas::` references directly from the CAS store, with no network access required. Consumers do not set `update_source_with_cas` themselves. When the `cas` experiment is enabled and the source is remote, Terragrunt uses the CAS path automatically. The attribute only has effect inside catalog files, where it flags nested `source` attributes for rewriting. `unit` and `stack` blocks also accept a `mutable` attribute. When `mutable = true`, the unit’s or stack’s content under `.terragrunt-stack` is copied from the CAS store and the working tree is editable. The default is `false`: files are materialized read-only so an accidental edit cannot reach back into the shared CAS store. Given the catalog files above, and an ordinary consumer stack like: live/terragrunt.stack.hcl ```hcl stack "service" { source = "git::git@github.com:acme/catalog.git//stacks/service?ref=v1.0.0" path = "service" } ``` Running `terragrunt --experiment cas stack generate` produces: * live/ * terragrunt.stack.hcl * .terragrunt-stack/ * service/ * terragrunt.stack.hcl (sources rewritten to cas:: references) * .terragrunt-stack/ * service/ * terragrunt.hcl (terraform source rewritten to cas:: reference) The generated stack file at `.terragrunt-stack/service/terragrunt.stack.hcl` contains the rewritten source: .terragrunt-stack/service/terragrunt.stack.hcl ```hcl unit "service" { source = "cas::sha1:a1b2c3d4..." update_source_with_cas = true path = "service" } ``` And the generated unit file at `.terragrunt-stack/service/.terragrunt-stack/service/terragrunt.hcl` contains: .terragrunt-stack/service/.terragrunt-stack/service/terragrunt.hcl ```hcl terraform { source = "cas::sha1:e5f6a7b8...//modules/service" update_source_with_cas = true } ``` The first `cas::` reference points to a synthetic tree in the CAS that contains the exact files needed for the unit. The second points to the full repository tree, with `//modules/service` telling Terragrunt which subdirectory to use as the OpenTofu/Terraform module source. Tip The `cas::` reference format includes a hash algorithm prefix (`sha1:` or `sha256:`) to support both SHA-1 and SHA-256 Git repositories. Requirements: * The `cas` experiment must be enabled (`--experiment cas`). * The `--no-cas` flag must not be set. * Sources using `update_source_with_cas` must be relative paths within the same repository. ### Local catalog sources [Section titled “Local catalog sources”](#local-catalog-sources) The consumer stack’s `source` can be a local filesystem path in addition to a remote Git URL. Both go through the same CAS-backed rewrite pipeline: live/terragrunt.stack.hcl ```hcl stack "service" { source = "../catalog//stacks/service" path = "service" } ``` Terragrunt copies the referenced directory into a temporary directory, computes a content-addressed root hash over `(relative path, mode, file-content hash)` triples using SHA-256, and rewrites nested `source` attributes against that copy. The original directory is never modified, so the catalog on disk stays clean even though the rewritten `source` values need somewhere to live. This is useful when iterating on a catalog before tagging a release: the same `update_source_with_cas = true` attributes in the catalog’s `terragrunt.stack.hcl` and `terragrunt.hcl` files apply unchanged, whether consumers pull the catalog over Git or point at a local checkout. ## Known Limitations of Explicit Stacks [Section titled “Known Limitations of Explicit Stacks”](#known-limitations-of-explicit-stacks) There are currently some known limitations with explicit stacks that you should be aware of as you start to adopt them. ### Dependencies cannot be set on stacks [Section titled “Dependencies cannot be set on stacks”](#dependencies-cannot-be-set-on-stacks) The `dependency` block cannot set the value of the `config_path` attribute to that of a stack. This is functionality that is planned for the future, but is not currently supported. As such, if you currently have multiple stacks that need to depend on each other, or on units within each other’s stacks, you will need to either use implicit stacks, or work around this limitation by setting the `config_path` attribute to the path of the unit within the stack, and carefully ensuring that all stacks are generated before any units are run. The [`stack-dependencies` experiment](/reference/experiments/active#stack-dependencies) lifts this limitation: a `dependency` declared in an [`autoinclude` block](/reference/hcl/blocks#autoinclude) can target a stack directory and read the aggregated outputs of its units. See [Declaring Dependencies Between Units](#declaring-dependencies-between-units). ### Deeply nested stack generation can be slow [Section titled “Deeply nested stack generation can be slow”](#deeply-nested-stack-generation-can-be-slow) Every generation of a stack from a `terragrunt.stack.hcl` file can potentially result in network traffic to fetch the source for the stack and filesystem traffic to copy the generated units to the `.terragrunt-stack` directory. This can result in slow stack generation if you have very deeply nested stacks. To mitigate this, enable the [`cas` experiment](/reference/experiments/active#cas) and use `update_source_with_cas = true` in your catalog’s stack and unit files. This deduplicates repository clones and resolves content from the local CAS store instead of fetching from the network on every generation. See [CAS Integration](#cas-integration) for details. ### Includes are not supported in `terragrunt.stack.hcl` files [Section titled “Includes are not supported in terragrunt.stack.hcl files”](#includes-are-not-supported-in-terragruntstackhcl-files) The `include` block is not supported in `terragrunt.stack.hcl` files. This isn’t functionality that is planned for future implementation, but may change based on community feedback, and proven use-cases. The current design of explicit stacks is that, when necessary, stacks can be nested into other stacks making them better organized and reusable without relying on includes to share configuration between stacks. # Implicit Stacks > Create stacks by organizing units in a directory structure. The simplest way to create a stack is to organize your units in a directory structure in your repository. When you have multiple units in a directory, Terragrunt automatically treats that directory as a stack for the purposes of commands like `terragrunt run --all apply`. ### Converting Terraform Modules to Units [Section titled “Converting Terraform Modules to Units”](#converting-terraform-modules-to-units) Let’s say your infrastructure is defined across multiple OpenTofu/Terraform root modules: * root * backend-app * main.tf * frontend-app * main.tf * mysql * main.tf * valkey * main.tf * vpc * main.tf To convert these to Terragrunt units, simply add a `terragrunt.hcl` file to each directory: * root * backend-app * main.tf * terragrunt.hcl * frontend-app * main.tf * terragrunt.hcl * mysql * main.tf * terragrunt.hcl * valkey * main.tf * terragrunt.hcl * vpc * main.tf * terragrunt.hcl Now you have an **implicit stack**! The `root` directory contains all your units and can be managed as a single stack. ### Working with Implicit Stacks [Section titled “Working with Implicit Stacks”](#working-with-implicit-stacks) Use the [`--all` flag](/reference/cli/commands/run/#all) to run an OpenTofu/Terraform command on all units in the implicit stack in the current working directory: ```bash # Deploy all units discovered in the current working directory terragrunt run --all apply # Plan changes across all units discovered in the current working directory terragrunt run --all plan # Destroy all units discovered in the current working directory terragrunt run --all destroy # View outputs from all units discovered in the current working directory terragrunt run --all output ``` You can also use the [`--graph` flag](/reference/cli/commands/run/#graph) to run an OpenTofu/Terraform command on all units in the [DAG](/getting-started/terminology/#directed-acyclic-graph-dag) of the unit in the current working directory. ```bash # Run an OpenTofu/Terraform command on all units in the DAG of the unit in the current working directory terragrunt run --graph apply ``` ### Advantages of Implicit Stacks [Section titled “Advantages of Implicit Stacks”](#advantages-of-implicit-stacks) * **Simple**: Just organize units in directory trees. * **Familiar**: Organized following best practices for OpenTofu/Terraform repository structures. * **Flexible**: Easy to add/remove units by creating/deleting directories. * **Version Control Friendly**: Each unit is a separate directory with its own history. * **Backwards Compatible**: This has been the default way to work with Terragrunt for over eight years, and the majority of existing Terragrunt configurations use this approach. ### Limitations of Implicit Stacks [Section titled “Limitations of Implicit Stacks”](#limitations-of-implicit-stacks) * **Manual Management**: Each unit must be manually created and configured. * **No Reusability**: Patterns can’t be easily shared across environments. * **Repetitive**: Similar configurations must be duplicated or referenced from [includes](/features/units/includes). # Run Queue > Learn how Terragrunt orchestrates multiple concurrent OpenTofu/Terraform runs. Terragrunt’s “Run Queue” is the mechanism it uses to manage the run order and concurrency when running OpenTofu/Terraform commands across multiple Terragrunt [units](/features/units). This is particularly relevant when using the [`run --all`](/reference/cli/commands/run#all) or [`run --graph`](/reference/cli/commands/run#graph) commands. ## How it Works: The Dependency Graph (DAG) [Section titled “How it Works: The Dependency Graph (DAG)”](#how-it-works-the-dependency-graph-dag) At its core, the Run Queue relies on a [Directed Acyclic Graph (DAG)](/getting-started/terminology#directed-acyclic-graph-dag) built from the dependencies defined between your Terragrunt units. These dependencies are typically established using [`dependency`](/reference/hcl/blocks#dependency) or [`dependencies`](/reference/hcl/blocks#dependencies) blocks in your `terragrunt.hcl` files. Terragrunt analyzes these dependencies to determine the correct order of operations: 1. **Discovery:** Terragrunt discovers configurations that might be relevant to a run based on the current working directory. 2. **Constructing the Queue:** Based on the command being run, Terragrunt creates an ordered queue. * For commands like `plan` or `apply`, dependencies are run *before* the units that depend on them. * For commands like `destroy`, dependent units are run *before* their dependencies. 3. **Runs:** Terragrunt dequeues the units in the queue and runs them, respecting the queue order. By default, it runs units concurrently up to a certain limit (controlled by the [`--parallelism`](/reference/cli/commands/run#parallelism) flag), but it will always wait for a unit’s dependencies (or dependents for destroys) to complete successfully before running that unit. ### Example DAG [Section titled “Example DAG”](#example-dag) Consider a setup where: * Unit “dependent” depends on unit “dependency”. * Unit “dependency” depends on unit “ancestor-dependency”. * Unit “independent” has no dependencies nor dependents. - root * subtree * dependent * terragrunt.hcl * dependency * terragrunt.hcl * ancestor-dependency * terragrunt.hcl * independent * terragrunt.hcl ![Diagram](/d2/docs/03-features/02-stacks/06-run-queue-0.svg) Assuming a current working directory of the `root` directory, Terragrunt would run units in the following order: * **`run --all plan` Order:** Terragrunt would run `independent` and `ancestor-dependency` concurrently. Once `ancestor-dependency` finishes, `dependency` would run. Once `dependency` finishes, `dependent` would run. * **`run --all destroy` Order:** Terragrunt would run `dependent` and `independent` concurrently. Once `dependent` finishes, `dependency` would run. Once `dependency` finishes, `ancestor-dependency` would run. ## Controlling the Queue [Section titled “Controlling the Queue”](#controlling-the-queue) Several flags allow you to customize how Terragrunt builds and executes the run queue. By default, Terragrunt will include all units that are in the current working directory. ### Include by default [Section titled “Include by default”](#include-by-default) By default, when using the `--all` flag, Terragrunt will include all units that are in the current working directory. Using any positive filter triggers “Exclude by default” behavior, meaning that Terragrunt will no longer automatically include all units in the current working directory, and will instead selectively include units only if they match a positive filter (and don’t match any negative filters). More on this in the [Filtering Units](#filtering-units) section. ### Filtering Units [Section titled “Filtering Units”](#filtering-units) You can control which units are included or excluded from the queue using the [`--filter` flag](/features/filter). #### Positive Filters [Section titled “Positive Filters”](#positive-filters) Any filter that doesn’t start with a `!` prefix is considered a positive filter. When Terragrunt sees that any positive filter is present, it will evaluate every path it encounters while walking the directory tree, and only include units that match a positive filter. ```bash terragrunt run --all --filter './subtree/**' -- plan ``` This will tell Terragrunt only to run the units that match the glob pattern `./subtree/**` (any directory found in the current working directory starting with `subtree`). There are many more types of filters you can use, and those details are covered in the [Filter feature documentation](/features/filter). #### Negative Filters [Section titled “Negative Filters”](#negative-filters) Any filter that starts with a `!` prefix is considered a negative filter. Negative filters are always evaluated after positive filters (if present). ```bash terragrunt run --all --filter '!./subtree/**' -- plan ``` This will tell Terragrunt to run all units in the current working directory except those that match the glob pattern `./subtree/**` (any directory found in the current working directory starting with `subtree`). #### Combining Positive and Negative Filters [Section titled “Combining Positive and Negative Filters”](#combining-positive-and-negative-filters) You can combine positive and negative filters to ensure that only the units you want are run. ```bash terragrunt run --all --filter './subtree/**' --filter '!./subtree/dependency/**' -- plan ``` This will tell Terragrunt to run all units in the directory `subtree` except those that match the glob pattern `./subtree/dependency/**` (any directory found in the current working directory starting with `subtree/dependency`). ### Modifying Order and Error Handling [Section titled “Modifying Order and Error Handling”](#modifying-order-and-error-handling) * [`--queue-construct-as`](/reference/cli/commands/list#queue-construct-as) (`--as`): Build the run queue *as if* a particular command was run. Useful for performing dry-runs of [`run`](/reference/cli/commands/run) using discovery commands, like [`find`](/reference/cli/commands/find) and [`list`](/reference/cli/commands/list). e.g. `terragrunt list --queue-construct-as destroy` This lists the units in the order they’d be processed for `run --all destroy`: ```bash $ terragrunt list --as destroy -l Type Path unit independent unit subtree/dependent unit subtree/dependency unit ancestor-dependency ``` ```bash $ terragrunt list --as plan -l Type Path unit ancestor-dependency unit independent unit subtree/dependency unit subtree/dependent ``` * [`--queue-ignore-dag-order`](/reference/cli/commands/run#queue-ignore-dag-order): Run units in the queue concurrently without respecting the dependency order. e.g. `terragrunt run --all plan --queue-ignore-dag-order` Run `plan` on `ancestor-dependency`, `subtree/dependency`, `subtree/dependent`, and `independent` all concurrently, without waiting for their defined dependencies. For instance, `subtree/dependent`’s plan would not wait for `subtree/dependency`’s plan to complete. Caution This flag is useful for faster runs in stateless commands like `validate` or `plan`, but is **dangerous** for commands that modify state like `apply` or `destroy`. You might encounter failed applies if unit dependencies are not applied before dependents, and conversely, failed destroys if unit dependents are not destroyed before dependencies. * [`--queue-ignore-errors`](/reference/cli/commands/run#queue-ignore-errors): Continue processing the queue even if some units fail. e.g. `terragrunt run --all plan --queue-ignore-errors` If `ancestor-dependency`’s plan fails, Terragrunt will still attempt to run `plan` for `subtree/dependency`, then `subtree/dependent`, and also for `independent`. Caution This flag is useful for identifying all errors at once, but can lead to inconsistent state if used with `apply` or `destroy`. You might encounter failed applies if unit dependencies are not applied successfully before dependents, and conversely, failed destroys if unit dependents are not destroyed successfully before dependencies. * [`--fail-fast`](/reference/cli/commands/run#fail-fast): Fail the run if any unit fails, stopping all remaining units immediately. e.g. `terragrunt run --all plan --fail-fast` If `independent`’s plan fails, Terragrunt will stop running any more units and fail the run, even if `ancestor-dependency`’s plan succeeds. Tip This flag is useful when you want to fail the run as soon as any unit fails, and stop running any more units. ## Important Considerations [Section titled “Important Considerations”](#important-considerations) Caution When using `run --all plan` with units that have dependencies (e.g. via `dependency` or `dependencies` blocks), the command will fail if those dependencies have never been deployed. This is because Terragrunt cannot resolve dependency outputs without existing state. To work around this issue, use [mock outputs in dependency blocks](/reference/hcl/blocks/#dependency). Caution Do not set `TF_PLUGIN_CACHE_DIR` when using `run --all` (unless using OpenTofu >= 1.10). This can cause concurrent access issues with the provider cache. Instead, use Terragrunt’s built-in [Provider Cache Server](/features/caching/provider-cache-server/). Caution When using `run --all` with `apply` or `destroy`, Terragrunt automatically adds the `-auto-approve` flag due to limitations with shared stdin making individual approvals impossible. Use [`--no-auto-approve`](/reference/cli/commands/run#no-auto-approve) to override this, but be aware you might need alternative approval workflows. # Run Report > Learn how Terragrunt provides detailed reports of runs, and at-a-glance summaries of them. Terragrunt uses an internal data store to track the results of runs when multiple are done at once. You can view this data, both with a high-level summary that is displayed at the end of each run, and via a detailed report that can be requested on-demand (coming soon). ## Run Summary [Section titled “Run Summary”](#run-summary) By default, when performing a queue-based run (e.g. `terragrunt run --all plan`), Terragrunt will emit some additional information to the console after the run is complete. ```bash $ terragrunt run --all plan # Omitted for brevity... ❯❯ Run Summary 3 units 62ms ──────────────────────────── Succeeded 3 ``` This output is called the “Run Summary”. It provides at-a-glance information about the run that was just performed, including the following (as relevant): * Duration: The duration of the run. * Units: The number of units that were run. * Succeeded: The number of units that succeeded (if any did). * Failed: The number of units that failed (if any did). * Excluded: The number of units that were excluded from the run (if any were). * Early Exits: The number of units that exited early, due to a failure in a dependency (if any did). ### Showing Unit Durations [Section titled “Showing Unit Durations”](#showing-unit-durations) You can enable showing the duration of each unit in the run summary by using the `--summary-per-unit` flag. ```bash $ terragrunt run --all plan --summary-per-unit # Omitted for brevity... ❯❯ Run Summary 3 units 10m ────────────────────────────── Succeeded (3) long-running-unit 10m medium-running-unit 12s short-running-unit 5ms ``` The units are sorted by duration, with the longest-running units shown first. ### Disabling the summary [Section titled “Disabling the summary”](#disabling-the-summary) You can disable the summary output by using the `--summary-disable` flag. ```bash terragrunt run --all plan --summary-disable ``` The internal report will still be tracked, and is available for generation if requested. ## Run Report [Section titled “Run Report”](#run-report) Optionally, you can also generate a detailed report of the run, which has all the information used to generate the run summary. ```bash terragrunt run --all plan --report-file report.csv ``` You can specify the format of the report using the `--report-format` flag, which supports either `csv` or `json`: ```bash terragrunt run --all plan --report-file report.json --report-format json ``` The format can also be inferred from the file extension. If no format is specified and the file has no extension, CSV will be used by default: ```bash # Will generate a CSV report terragrunt run --all plan --report-file report # Will generate a JSON report terragrunt run --all plan --report-file report.json # Will generate a CSV report terragrunt run --all plan --report-file report.csv ``` The report will be generated in the specified format at the given path in the current working directory. Here’s an example of what the CSV format looks like: ```csv Name,Started,Ended,Result,Reason,Cause first-exclude,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,excluded,exclude block, second-exclude,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,excluded,exclude block, first-failure,2025-06-05T16:28:41-04:00,2025-06-05T16:28:42-04:00,failed,run error, first-success,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,succeeded,, second-failure,2025-06-05T16:28:41-04:00,2025-06-05T16:28:42-04:00,failed,run error, second-success,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,succeeded,, second-early-exit,2025-06-05T16:28:42-04:00,2025-06-05T16:28:42-04:00,early exit,run error, first-early-exit,2025-06-05T16:28:42-04:00,2025-06-05T16:28:42-04:00,early exit,run error, ``` And here’s an example of what the JSON format looks like: ```json [ { "Name": "first-exclude", "Started": "2025-06-05T16:28:41-04:00", "Ended": "2025-06-05T16:28:41-04:00", "Result": "excluded", "Reason": "exclude block" }, { "Name": "first-success", "Started": "2025-06-05T16:28:41-04:00", "Ended": "2025-06-05T16:28:41-04:00", "Result": "succeeded" } ] ``` You can use this file to determine details for each unit run, including the name of the unit, the start and end times, the result, the reason for that result, and the cause for that reason. Note that in the JSON format, empty fields (Reason and Cause) are omitted entirely rather than being set to empty values. In general, the schema for this report should change infrequently, but we’ll try to keep it up to date here. You can also generate a JSON schema file for the report, so that you have a programmatic way to validate that the report is going to conform to an expected schema. ```bash terragrunt run --all plan --report-schema-file report.schema.json ``` The schema will be generated at the given path in the current working directory. The generated schema conforms to the [JSON Schema](https://json-schema.org/) standard. This generated schema will look like the following: run/report/v4/schema.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://docs.terragrunt.com/schemas/run/report/v4/schema.json", "items": { "properties": { "Started": { "type": "string", "format": "date-time" }, "Ended": { "type": "string", "format": "date-time" }, "Reason": { "type": "string", "enum": [ "retry succeeded", "error ignored", "run error", "exclude block", "ancestor error" ] }, "Cause": { "type": "string" }, "Name": { "type": "string" }, "Result": { "type": "string", "enum": [ "succeeded", "failed", "early exit", "excluded" ] }, "Ref": { "type": "string" }, "Cmd": { "type": "string" }, "Args": { "items": { "type": "string" }, "type": "array" } }, "additionalProperties": false, "type": "object", "required": [ "Started", "Ended", "Name", "Result" ], "title": "Terragrunt Run Report Schema", "description": "Schema for Terragrunt run report" }, "type": "array", "title": "Terragrunt Run Report Schema", "description": "Array of Terragrunt runs" } ``` Note the `$id` field, which is used to identify the schema. This is useful to quickly determine which version of the schema is being used. You can also fetch the schema remotely from that URL. ### Results [Section titled “Results”](#results) Results are high level outcomes of a unit run, and will always be one of the following: * `succeeded`: The unit run succeeded. * `failed`: The unit run failed. * `excluded`: The unit was excluded from the run. * `early exit`: The unit exited early, due to a failure in a dependency. ### Reasons [Section titled “Reasons”](#reasons) Reasons are more granular details of those results, and will always be one of the following, based on the result of the unit run: * `succeeded`: * “: When the unit run succeeded without any special conditions, an empty string will be found here. * `retry succeeded`: When the unit run initially failed, but was retried due to a `retry` block, and succeeded on a subsequent attempt, you can expect to see a value of `retry succeeded` here. * `error ignored`: When the unit run failed, but the error was ignored due to an `ignore` block, you can expect to see a value of `error ignored` here. * `failed`: * `run error`: When the unit run failed due to a run error, you can expect to see a value of `run error` here. * `excluded`: * `exclude block`: When the unit was excluded from the run due to an `exclude` block, you can expect to see a value of `exclude block` here. * `early exit`: * `ancestor error`: When the unit exited early due to an error in the run of a dependency, you can expect to see a value of `ancestor error` here. ### Causes [Section titled “Causes”](#causes) Causes indicate the specific reason for a given result, and are generally not guessable. These provide information on the exact mechanism that caused the result. * `error ignored`: You will find the name of the `ignore` block that resulted in the error being ignored. * `run error`: You will find the actual error message of the unit that failed. * `ancestor error`: You will find the name of the unit that failed. # Stack Operations > Work with dependencies, visualize the DAG, control parallelism, and manage stack runs. ## Passing outputs between units [Section titled “Passing outputs between units”](#passing-outputs-between-units) Consider the following file structure: * root * backend-app * terragrunt.hcl * mysql * terragrunt.hcl * valkey * terragrunt.hcl * vpc * terragrunt.hcl Suppose that you wanted to pass in the VPC ID of the VPC that is created from the `vpc` unit in the directory structure above to the `mysql` unit as an input variable. Or that you wanted to pass in the subnet IDs of the private subnet that is allocated as part of the `vpc` unit. You can use the `dependency` block to extract those outputs and use them as `inputs` to the `mysql` unit. For example, suppose the `vpc` unit outputs the ID under the output named `vpc_id`. To access that output, you would specify in `mysql/terragrunt.hcl`: mysql/terragrunt.hcl ```hcl dependency "vpc" { config_path = "../vpc" } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } ``` When you apply this unit, the output will be read from the `vpc` unit and passed in as an input to the `mysql` unit right before calling `tofu apply`/`terraform apply`. You can also specify multiple `dependency` blocks to access the outputs of multiple units. For example, in the above folder structure, you might want to reference the `domain` output of the `valkey` and `mysql` units for use as `inputs` in the `backend-app` unit. To access those outputs, you would specify the following in `backend-app/terragrunt.hcl`: backend-app/terragrunt.hcl ```hcl dependency "mysql" { config_path = "../mysql" } dependency "valkey" { config_path = "../valkey" } inputs = { mysql_url = dependency.mysql.outputs.domain valkey_url = dependency.valkey.outputs.domain } ``` Note that each `dependency` block results in a relevant status in the Terragrunt [DAG](/getting-started/terminology/#directed-acyclic-graph-dag). This means that when you run `run --all apply` on a config that has `dependency` blocks, Terragrunt will not attempt to deploy the config until all the units referenced in `dependency` blocks have been applied. So for the above example, the order for the `run --all apply` command would be: 1. Deploy the VPC 2. Deploy MySQL and valkey in parallel 3. Deploy the backend-app If any of the units failed to deploy, then Terragrunt will not attempt to deploy the units that depend on them. **Note**: Not all blocks can access outputs passed by `dependency` blocks. See the section on [Configuration parsing order](/reference/hcl#configuration-parsing-order) for more information. ### Unapplied dependency and mock outputs [Section titled “Unapplied dependency and mock outputs”](#unapplied-dependency-and-mock-outputs) Terragrunt will return an error if the unit referenced in a `dependency` block has not been applied yet. This is because you cannot actually fetch outputs out of an unapplied unit, even if there are no resources being created in the unit. This is most problematic when running commands that do not modify state (e.g `run --all plan` and `run --all validate`) on a completely new setup where no infrastructure has been deployed. You won’t be able to `plan` or `validate` a unit if you can’t determine the `inputs`. If the unit depends on the outputs of another unit that hasn’t been applied yet, you won’t be able to compute the `inputs` unless the dependencies are all applied. Of course, in real life usage, you typically need the ability to run `run --all validate` or `run --all plan` on a completely new set of infrastructure. To address this, you can provide mock outputs to use when a unit hasn’t been applied yet. This is configured using the `mock_outputs` attribute on the `dependency` block and it corresponds to a map that will be injected in place of the actual dependency outputs if the target config hasn’t been applied yet. Using a mock output is typically the best solution here, as you typically don’t actually care that an *accurate* value is used for a given value at this stage, just that it will plan successfully. When you actually apply the unit, that’s when you want to be sure that a real value is used. For example, in the previous scenario with a `mysql` unit and `vpc` unit, suppose you wanted to mock a value for the `vpc_id` during a `run --all validate` for the `mysql` unit. You can specify that in `mysql/terragrunt.hcl`: mysql/terragrunt.hcl ```hcl dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "mock-vpc-id" } } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } ``` You can now run `validate` on this config before the `vpc` unit is applied because Terragrunt will use the map `{vpc_id = "mock-vpc-id"}` as the `outputs` attribute on the dependency instead of erroring out. What if you wanted to restrict this behavior to only the `validate` command? For example, you might not want to use the defaults for a `plan` operation because the plan doesn’t give you any indication of what is actually going to be created. You can use the `mock_outputs_allowed_terraform_commands` attribute to indicate that the `mock_outputs` should only be used when running those OpenTofu/Terraform commands. So to restrict the `mock_outputs` to only when `validate` is being run, you can modify the above `terragrunt.hcl` file to: mysql/terragrunt.hcl ```hcl dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "temporary-dummy-id" } mock_outputs_allowed_terraform_commands = ["validate"] } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } ``` Note that indicating `validate` means that the `mock_outputs` will be used either with `validate` or with `run --all validate`. You can also use `skip_outputs` on the `dependency` block to specify the dependency without pulling in the outputs: mysql/terragrunt.hcl ```hcl dependency "vpc" { config_path = "../vpc" skip_outputs = true } ``` When `skip_outputs` is used with `mock_outputs`, mocked outputs will be returned without attempting to load outputs from OpenTofu/Terraform. This can be useful when you disable backend initialization (`remote_state.disable_init`) in CI for example. mysql/terragrunt.hcl ```hcl dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "temporary-dummy-id" } skip_outputs = true } ``` You can also use `mock_outputs_merge_strategy_with_state` on the `dependency` block to merge mocked outputs and real outputs: mysql/terragrunt.hcl ```hcl dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "temporary-dummy-id" new_output = "temporary-dummy-value" } mock_outputs_merge_strategy_with_state = "shallow" } ``` If real outputs only contain `vpc_id`, `dependency.outputs` will contain a real value for `vpc_id` and a mocked value for `new_output`. ### Passing outputs between units in explicit stacks [Section titled “Passing outputs between units in explicit stacks”](#passing-outputs-between-units-in-explicit-stacks) When defining units using a `terragrunt.stack.hcl` file, you might need to perform some indirection to pass outputs between units, as the dependency relationship of each unit is explicitly defined in each unit’s `terragrunt.hcl` file. For example, say you wanted to generate the stack above using the following `terragrunt.stack.hcl` file: terragrunt.stack.hcl ```hcl unit "vpc" { source = "github.com/acme/infrastructure-catalog//units/vpc?ref=v1.0.0" path = "vpc" } unit "mysql" { source = "github.com/acme/infrastructure-catalog//units/mysql?ref=v1.0.0" path = "mysql" } unit "valkey" { source = "github.com/acme/infrastructure-catalog//units/valkey?ref=v1.0.0" path = "valkey" } unit "backend_app" { source = "github.com/acme/infrastructure-catalog//units/backend-app?ref=v1.0.0" path = "backend-app" } ``` Generating this stack would generate the following: * .terragrunt-stack * vpc * terragrunt.hcl * mysql * terragrunt.hcl * valkey * terragrunt.hcl * backend-app * terragrunt.hcl The `backend-app` unit would need to access the outputs of the `mysql` and `valkey` units to use as inputs. To do this, you can use the `dependency` block to access the outputs of the `mysql` and `backend-app` units. github.com/acme/infrastructure-catalog//units/mysql/terragrunt.hcl ```hcl dependency "vpc" { config_path = values.vpc_path } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } ``` github.com/acme/infrastructure-catalog//units/backend-app/terragrunt.hcl ```hcl dependency "mysql" { config_path = values.mysql_path } dependency "valkey" { config_path = values.valkey_path } inputs = { mysql_url = dependency.mysql.outputs.domain valkey_url = dependency.valkey.outputs.domain } ``` And update the `terragrunt.stack.hcl` file to: terragrunt.stack.hcl ```hcl unit "vpc" { source = "github.com/acme/infrastructure-catalog//units/vpc?ref=v1.0.0" path = "vpc" } unit "mysql" { source = "github.com/acme/infrastructure-catalog//units/mysql?ref=v1.0.0" path = "mysql" values = { vpc_path = "../vpc" } } unit "valkey" { source = "github.com/acme/infrastructure-catalog//units/valkey?ref=v1.0.0" path = "valkey" values = { vpc_path = "../vpc" } } unit "backend_app" { source = "github.com/acme/infrastructure-catalog//units/backend-app?ref=v1.0.0" path = "backend-app" values = { mysql_path = "../mysql" valkey_path = "../valkey" } } ``` Following this pattern, the path to dependencies are passed in as `values` to the unit, and units themselves define dependency blocks that utilize those values. Note You might not like this design! Take a look at RFC [#4067](https://github.com/gruntwork-io/terragrunt/issues/4067) for an alternate proposal from a member of the Terragrunt community, and follow the conversation there. ## Dependencies between units [Section titled “Dependencies between units”](#dependencies-between-units) You can also specify dependencies without accessing any of the outputs of units. Consider the following file structure: * root * backend-app * terragrunt.hcl * frontend-app * terragrunt.hcl * mysql * terragrunt.hcl * valkey * terragrunt.hcl * vpc * terragrunt.hcl Let’s assume you have the following dependencies between OpenTofu/Terraform units: * `backend-app` depends on `mysql`, `valkey`, and `vpc` * `frontend-app` depends on `backend-app` and `vpc` * `mysql` depends on `vpc` * `valkey` depends on `vpc` * `vpc` has no dependencies You can express these dependencies in your `terragrunt.hcl` config files using a `dependencies` block. For example, in `backend-app/terragrunt.hcl` you would specify: backend-app/terragrunt.hcl ```hcl dependencies { paths = ["../vpc", "../mysql", "../valkey"] } ``` Similarly, in `frontend-app/terragrunt.hcl`, you would specify: frontend-app/terragrunt.hcl ```hcl dependencies { paths = ["../vpc", "../backend-app"] } ``` Once you’ve specified these dependencies in each `terragrunt.hcl` file, Terragrunt will be able to perform updates respecting the [DAG](/getting-started/terminology/#directed-acyclic-graph-dag) of dependencies. For the example at the start of this section, the order of runs for the `run --all apply` command would be: 1. Deploy the VPC 2. Deploy MySQL and valkey in parallel 3. Deploy the backend-app 4. Deploy the frontend-app Any error encountered in an individual unit during a `run --all` command will prevent Terragrunt from proceeding with the deployment of any dependent units. To check all of your dependencies and validate the code in them, you can use the `run --all validate` command. Note During `destroy` runs, Terragrunt will try to find all dependent units and show a confirmation prompt with a list of detected dependencies. This is because Terragrunt knows that once resources in a dependency are destroyed, any commands run on dependent units may fail. For example, if `destroy` was called on the `Valkey` unit, you’d be asked for confirmation, as the `backend-app` depends on `Valkey`. You can suppress the prompt by using the `--non-interactive` flag. ## Visualizing the DAG [Section titled “Visualizing the DAG”](#visualizing-the-dag) To visualize the dependency graph you can use the `dag graph` command (similar to the `tofu/terraform graph` command), or its equivalent `list --format=dot --dependencies --external` command. The graph is output in DOT format. The typical program used to render this file format is GraphViz, but many web services are available that can do this as well. ```bash terragrunt dag graph | dot -Tsvg > graph.svg # Or equivalently: terragrunt list --format=dot --dependencies --external | dot -Tsvg > graph.svg ``` The example above generates the following graph: ![terragrunt dag graph](/_vercel/image?url=_astro%2Fgraph.COlg21Dx.png\&w=640\&q=100) Note that this graph shows the dependency relationship in the direction of the arrow, with the tip pointing to the dependency (e.g. `frontend-app` depends on `backend-app`). For plans and applies, Terragrunt will run units in the opposite direction, however (e.g. `backend-app` would be applied before `frontend-app`). The exception to this rule is during the `destroy` (and `plan/apply -destroy`) commands, where Terragrunt will run in the direction of the arrow (e.g. `frontend-app` would be destroyed before `backend-app`). ## Testing multiple units locally [Section titled “Testing multiple units locally”](#testing-multiple-units-locally) If you are using Terragrunt to download [remote OpenTofu/Terraform modules](/features/units/#remote-opentofuterraform-modules) and all of your units have the `source` parameter set to a Git URL, but you want to test with a local checkout of the code, you can use the `--source` parameter to override that value: ```bash terragrunt run --all --source /source/modules -- plan ``` If you set the `--source` parameter, the `run --all` command will assume that parameter is pointing to a folder on your local file system that has a local checkout of all of your OpenTofu/Terraform modules. For each unit that is being processed via a `run --all` command, Terragrunt will: 1. Read in the `source` parameter in that unit’s `terragrunt.hcl` file. 2. Parse out the path (the portion after the double-slash). 3. Append the path to the `--source` parameter to create the final local path for that unit. For example, consider the following `terragrunt.hcl` file: terragrunt.hcl ```hcl terraform { source = "git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1" } ``` Running the following: ```bash terragrunt run --all --source /source/infrastructure-modules -- apply ``` Will result in a unit with the configuration for the source above being resolved to `/source/infrastructure-modules//networking/vpc`. ## Limiting run parallelism [Section titled “Limiting run parallelism”](#limiting-run-parallelism) By default, Terragrunt will not impose a limit on the number of units it executes when it traverses the dependency graph, meaning that if it finds 5 units without dependencies, it’ll run OpenTofu/Terraform 5 times in parallel, once in each unit. Sometimes, this can create a problem if there are a lot of units in the dependency graph, like hitting a rate limit on a cloud provider. To limit the maximum number of unit executions at any given time use the `--parallelism [number]` flag ```sh terragrunt run --all --parallelism 4 -- apply ``` ## Limiting stack output parallelism [Section titled “Limiting stack output parallelism”](#limiting-stack-output-parallelism) `terragrunt stack output` reads unit outputs in parallel. Use the same `--parallelism` flag to lower concurrency if a large stack hits resource limits (for example, file descriptors or cloud-provider rate limits): ```sh terragrunt stack output --parallelism 4 ``` ## Saving OpenTofu/Terraform plan output [Section titled “Saving OpenTofu/Terraform plan output”](#saving-opentofuterraform-plan-output) A powerful feature of OpenTofu/Terraform is the ability to save the result of a plan as a binary file using the [-out flag](https://opentofu.org/docs/cli/commands/plan/). Terragrunt provides special tooling in `run --all` execution to ensure that the saved plan for a `run --all` against a stack has a corresponding entry for each unit in the stack in a directory structure that mirrors the stack structure. To save every plan generated when running a stack, use the `--out-dir` flag (or `TG_OUT_DIR` environment variable) as demonstrated below: ```bash $ terragrunt run --all --out-dir /tmp/tfplan -- plan ``` * app1 * tfplan.tfplan * app2 * tfplan.tfplan * app3 * tfplan.tfplan * project-2 * project-2-app1 * tfplan.tfplan This integration also exists for the `apply` command, where the generated plan will be used when performing the apply. ```bash $ terragrunt run --all --out-dir /tmp/tfplan -- apply ``` Note Performing a `run --all --out-dir -- apply` requires that a plan already exists for each unit in the stack. If a plan is missing for any unit, the command will fail. If a plan job was scoped with `--filter`, you must pass the **same** `--filter` to the apply so the apply discovery set matches the artifact contents. For example: ```bash # Plan `` and its dependencies: terragrunt run --all --filter '...' --out-dir /tmp/tfplan -- plan # Apply must reuse the same filter: terragrunt run --all --filter '...' --out-dir /tmp/tfplan -- apply ``` For planning a destroy operation, use the following commands: ```bash terragrunt run --all --out-dir /tmp/tfplan -- plan -destroy terragrunt run --all --out-dir /tmp/tfplan -- apply ``` Caution If you are leveraging [mock outputs](#unapplied-dependency-and-mock-outputs) in your stack, you may get unexpected results when applying saved plans, as the plans will have mock outputs in them. There’s a workaround documented [here](https://github.com/gruntwork-io/terragrunt/issues/2178#issuecomment-2615842856), but no first-class feature in Terragrunt to address this issue, currently. If you would like Terragrunt to have first-class support for a solution to this, please [create an RFC](https://github.com/gruntwork-io/terragrunt/issues/new?template=03-rfc.yml) to propose it. To save plans in json format use the `--json-out-dir` flag: ```bash terragrunt run --all --json-out-dir /tmp/json -- plan ``` * app1 * tfplan.json * app2 * tfplan.json * app3 * tfplan.json * project-2 * project-2-app1 * tfplan.json ```bash terragrunt run --all --out-dir /tmp/all --json-out-dir /tmp/all -- plan ``` * app1 * tfplan.json * tfplan.tfplan * app2 * tfplan.json * tfplan.tfplan * app3 * tfplan.json * tfplan.tfplan * project-2 * project-2-app1 * tfplan.json * tfplan.tfplan ## Nested Stacks [Section titled “Nested Stacks”](#nested-stacks) Note that you can also have nested stacks. For example, consider the following file structure: * root * us-east-1 * app * terragrunt.hcl * db * terragrunt.hcl * us-west-2 * app * terragrunt.hcl * db * terragrunt.hcl In this example, there’s the `root` stack, that contains all the infrastructure you’ve defined so far, and there’s also the `us-east-1` and `us-west-2` stacks, that contain the infrastructure for the `app` and `db` units in those regions. You can run `run --all` commands at any depth of the stack to run the units in that stack and all of its children. For example, to run all the units in the `us-east-1` stack, you can run: ```sh cd root/us-east-1 terragrunt run --all apply ``` Terragrunt will only include the units in the `us-east-1` stack and its children in the queue of units to run. This is the primary tool Terragrunt users use to control the blast radius of their changes. For the most part, it is the current working directory that determines the blast radius of a `run --all` command. In addition to using your working directory to control what’s included in a [run queue](/getting-started/terminology/#run-queue), you can also use [Filters](/features/filter/) to control this. There are more flags that control the behavior of the `run` command, which you can find in the [`run` docs](/reference/cli/commands/run). # Units > Learn how Terragrunt units result in atomic deployments and immutable infrastructure. Consider the following file structure in a typical OpenTofu/Terraform project, which defines three environments (prod, qa, stage) with the same infrastructure in each one (an app, a MySQL database, and a VPC): * live * prod * app * main.tf * mysql * main.tf * vpc * main.tf * qa * app * main.tf * mysql * main.tf * vpc * main.tf * stage * app * main.tf * mysql * main.tf * vpc * main.tf The contents of each environment could be more or less identical, except perhaps for a few settings (e.g. the prod environment may run more or bigger servers). As the size of the infrastructure grows, having to maintain all of this duplicated code between environments becomes more error prone. You can reduce the amount of copy paste using [OpenTofu/Terraform modules](https://blog.gruntwork.io/how-to-create-reusable-infrastructure-with-terraform-modules-25526d65f73d), but even the code to instantiate a module and set up input variables, output variables, providers, and remote state can still create a lot of maintenance overhead. How can you keep your OpenTofu/Terraform code [DRY](/getting-started/terminology/#dont-repeat-yourself-dry) so that you can maximize code reuse and minimize maintenance overhead? Moreover, how can you ensure that you are reproducing as close to the same infrastructure as possible across environments, so that you can be confident that what you test in qa will work when you deploy to prod? ## Terragrunt units [Section titled “Terragrunt units”](#terragrunt-units) A unit in Terragrunt is a directory containing a `terragrunt.hcl` file. This hermetic unit of infrastructure is the smallest deployable entity in Terragrunt. It’s also the most important feature Terragrunt has. Units are designed to be contained, and can be operated on independently of other units. Infrastructure changes to units are also meant to be atomic. The interface you have with a unit is a `terragrunt.hcl` file, and any change you make to it should result in one reproducible change to a limited subset of your infrastructure. Units are designed to work with immutable OpenTofu/Terraform modules. As a best practice, OpenTofu/Terraform modules referenced by a unit should be versioned, and the version of that module should be immutable. This ensures that the infrastructure you deploy is consistent across environments, and that you are confident you can reproduce the same pattern of infrastructure as many times as you need. ## Remote OpenTofu/Terraform modules [Section titled “Remote OpenTofu/Terraform modules”](#remote-opentofuterraform-modules) Terragrunt has the ability to download remote OpenTofu/Terraform configurations. The idea is that you define the OpenTofu/Terraform code for your infrastructure just once, in a single repo, called, for example, `modules`: * modules * app * main.tf * mysql * main.tf * vpc * main.tf This repo contains typical OpenTofu/Terraform code, with one important design constraint: anything in your OpenTofu/Terraform code that should be different between environments should be exposed as an [input variable](https://opentofu.org/docs/language/values/variables/). For example, the `app` module might expose the following variables: variables.tf ```hcl variable "instance_count" { description = "How many servers to run" } variable "instance_type" { description = "What kind of servers to run (e.g. t3.large)" } ``` These variables allow you to run smaller/fewer servers in qa and stage to save money and larger/more servers in prod to ensure availability and scalability. They also define the *variability* of this infrastructure pattern. When instantiating the `app` module as a Terragrunt unit, you can be fairly confident that the only variance you are likely to see between environments is in the values of these variables. In a separate repo, called, for example, `live`, you define the code for all of your environments, which now consists of just one `terragrunt.hcl` file per unit (e.g. `app/terragrunt.hcl`, `mysql/terragrunt.hcl`, etc). This gives you the following file layout: * live * prod * app * terragrunt.hcl * mysql * terragrunt.hcl * vpc * terragrunt.hcl * qa * app * terragrunt.hcl * mysql * terragrunt.hcl * vpc * terragrunt.hcl * stage * app * terragrunt.hcl * mysql * terragrunt.hcl * vpc * terragrunt.hcl Notice how there are no OpenTofu/Terraform configurations (`.tf` files) in any of the folders. Instead, each `terragrunt.hcl` file specifies a `terraform { …​ }` block that specifies from where to download the OpenTofu/Terraform code, as well as the environment-specific values for the input variables in that OpenTofu/Terraform code. For example, `stage/app/terragrunt.hcl` may look like this: terragrunt.hcl ```hcl terraform { # Deploy version v0.0.3 in stage source = "git::git@github.com:foo/modules.git//app?ref=v0.0.3" } inputs = { instance_count = 3 instance_type = "t4g.micro" } ``` Note The double slash (`//`) in the `source` parameter is intentional and required. It’s part of OpenTofu/Terraform’s Git syntax for [module sources](https://opentofu.org/docs/language/modules/sources/). OpenTofu/Terraform may display a “OpenTofu/Terraform initialized in an empty directory” warning, but you can safely ignore it. And `prod/app/terragrunt.hcl` may look like this: terragrunt.hcl ```hcl terraform { # Deploy version v0.0.1 in prod source = "git::git@github.com:foo/modules.git//app?ref=v0.0.1" } inputs = { instance_count = 10 instance_type = "m8g.large" } ``` You can now deploy the modules in your `live` repo. For example, to deploy the `app` module in stage, you would do the following: ```bash cd live/stage/app terragrunt apply ``` When Terragrunt finds the `terraform` block with a `source` parameter in `live/stage/app/terragrunt.hcl` file, it will: 1. Download the configurations specified via the `source` parameter into the `--download-dir` folder (by default `.terragrunt-cache` in the working directory, which we recommend adding to `.gitignore`). The `source` parameter supports the same syntax as the OpenTofu/Terraform [module source](https://opentofu.org/docs/language/modules/sources/) parameter (via [go-getter](https://github.com/hashicorp/go-getter)), including local file paths, Git URLs, and `ref` parameters for pinning a tag, commit, or branch. Terragrunt downloads all the code before the double-slash `//` so that relative paths between modules work correctly. 2. Copy all files from the current working directory into the temporary folder. 3. Execute the OpenTofu/Terraform command you specified in that temporary folder (assuming you are performing a [run](/getting-started/terminology/#run)). 4. Pass any variables defined in the `inputs = { …​ }` block as environment variables (prefixed with `TF_VAR_`) when running OpenTofu/Terraform. Notice how the `inputs` block in `stage/app/terragrunt.hcl` deploys fewer and smaller instances than prod. Check out the [terragrunt-infrastructure-modules-example](https://github.com/gruntwork-io/terragrunt-infrastructure-modules-example) and [terragrunt-infrastructure-live-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-example) repos for fully-working sample code that demonstrates our recommended folder structure for successful infrastructure management. ## Immutable modules and atomic deployments [Section titled “Immutable modules and atomic deployments”](#immutable-modules-and-atomic-deployments) With this approach, copy/paste between environments is minimized. The `terragrunt.hcl` files contain solely the `source` URL of the module to deploy and the `inputs` to set for that module in the current environment. To create a new unit, you copy an old one and update just the environment-specific `inputs` in the `terragrunt.hcl` files, which is about as close to the “essential complexity” of the problem as you can get. Just as importantly, since the OpenTofu/Terraform module code is now defined in a single repo, you can version it (e.g., using Git tags and referencing them using the `ref` parameter in the `source` URL, as in the `stage/app/terragrunt.hcl` and `prod/app/terragrunt.hcl` examples above), and promote a single, immutable version through each environment (e.g., qa → stage → prod). This is especially powerful when thinking about how the pattern is deployed. Because all of the configuration for a unit is defined using a versioned URL and a set of inputs, it’s easy to reliably promote an infrastructure change across environments as one atomic change. It’s also easy to roll back to a previous version of the infrastructure by changing the `ref` parameter in the `source` URL. This idea is inspired by Kief Morris’ blog post [Using Pipelines to Manage Environments with Infrastructure as Code](https://medium.com/@kief/https-medium-com-kief-using-pipelines-to-manage-environments-with-infrastructure-as-code-b37285a1cbf5). ## Working locally [Section titled “Working locally”](#working-locally) If you’re testing changes to a local copy of the `modules` repo, you can use the `--source` command-line option or the `TG_SOURCE` environment variable to override the `source` parameter. This is useful to point Terragrunt at a local checkout of your code so you can do rapid, iterative, make-a-change-and-rerun development: ```bash cd live/stage/app terragrunt apply --source ../../../modules//app ``` Note: the double slash (`//`) is required here too — see the note above for details. ## Working with lock files [Section titled “Working with lock files”](#working-with-lock-files) OpenTofu/Terraform lock files (`.terraform.lock.hcl`) are supported by Terragrunt. The lock file will be generated next to your `terragrunt.hcl`, and you should check it into version control. See the [Lock File Handling docs](/reference/lock-files) for more details. ## Terragrunt caching [Section titled “Terragrunt caching”](#terragrunt-caching) The first time you set the `source` parameter to a remote URL, Terragrunt will download the code from that URL into a tmp folder. It will *NOT* download it again afterwards unless you change that URL. Therefore, when working locally, you should use the `--source` parameter and point it at a local file path as described in the previous section. Terragrunt will copy the local files every time you run it, which is nearly instantaneous, and doesn’t require reinitializing everything, so you’ll be able to iterate quickly. If you need to force Terragrunt to redownload something from a remote URL, run Terragrunt with the `--source-update` flag, and it’ll delete the tmp folder, download the files from scratch, and reinitialize everything. This can take a while, so avoid it and use `--source` when you can! ## Working with relative file paths [Section titled “Working with relative file paths”](#working-with-relative-file-paths) Caution When you run `terragrunt apply` in folder `foo`, OpenTofu/Terraform actually runs in a temporary folder such as `.terragrunt-cache/foo`. Relative file paths will be relative to that temporary folder, not the folder where you ran Terragrunt! In particular: * **Command line**: When using file paths on the command line, such as passing an extra `-var-file` argument, you should use absolute paths: ```bash # Use absolute file paths on the CLI! terragrunt apply -var-file /foo/bar/extra.tfvars # Or use the PWD environment variable to construct # an absolute path before passing it to Terragrunt # $ terragrunt apply -var-file "$PWD/extra.tfvars" ``` * **Terragrunt configuration**: When using file paths directly in your Terragrunt configuration (`terragrunt.hcl`), such as in an `extra_arguments` block, you can’t use hard-coded absolute file paths, or it won’t work on your teammates’ computers. Therefore, you should utilize the Terragrunt built-in function `get_terragrunt_dir()` to use a relative file path: terragrunt.hcl ```hcl terraform { source = "git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3" extra_arguments "custom_vars" { commands = [ "apply", "plan", "import", "push", "refresh" ] # With the get_terragrunt_dir() function, you can use relative paths! arguments = [ "-var-file=${get_terragrunt_dir()}/../common.tfvars", "-var-file=example.tfvars" ] } } ``` See the [`get_terragrunt_dir()`](/reference/hcl/functions/#get_terragrunt_dir) documentation for more details. ## Using Terragrunt with private Git repos [Section titled “Using Terragrunt with private Git repos”](#using-terragrunt-with-private-git-repos) The easiest way to use Terragrunt with private Git repos is to use SSH authentication. Configure your Git account so you can use it with SSH (see the [guide for GitHub here](https://help.github.com/articles/connecting-to-github-with-ssh/)) and use the SSH URL for your repo: terragrunt.hcl ```hcl terraform { source = "git@github.com:foo/modules.git//path/to/module?ref=v0.0.1" } ``` Look up the Git repo for your repository to find the proper format. Note In automated pipelines, you may need to run the following command for your Git repository prior to calling `terragrunt` to ensure that the SSH host is registered locally: ```bash ssh -T -oStrictHostKeyChecking=accept-new git@github.com || true ``` ## Generate blocks [Section titled “Generate blocks”](#generate-blocks) In an ideal world, all that units do would be to run versioned, immutable OpenTofu/Terraform modules with environment-specific inputs. In the real world, however, certain scenarios arise when you have to inject additional configurations to the immutable OpenTofu/Terraform modules you use. This is where [generate blocks](/reference/hcl/blocks#generate) prove useful. When you define a `generate` block, Terragrunt will do the following before any run: 1. Fetch any module referenced in a source URL in the `terraform` block into the `.terragrunt-cache` folder (if there is none, it will run in the current working directory). 2. Generate the file specified in the `generate` block into the directory where Terragrunt will run OpenTofu/Terraform. 3. Run the OpenTofu/Terraform command. The most common example of this is to dynamically generate a `provider.tf` file that includes provider configurations. Most OpenTofu/Terraform modules leave provider configuration to the consumer, which is a good practice — it lets each consumer define the provider in a way that suits their needs. Consider a setup where you want to always assume a specific IAM role when calling a given OpenTofu/Terraform module. Not all modules expose the right variables for configuring the `aws` provider, so you can use a Terragrunt `generate` block to create a `provider.tf` with the correct configuration. Add an `env.hcl` file for each environment in the `live` repo: * live * prod * env.hcl * app * terragrunt.hcl * mysql * terragrunt.hcl * vpc * terragrunt.hcl * qa * env.hcl * app * terragrunt.hcl * mysql * terragrunt.hcl * vpc * terragrunt.hcl * stage * env.hcl * app * terragrunt.hcl * mysql * terragrunt.hcl * vpc * terragrunt.hcl Each `env.hcl` file (the one at the environment level, e.g `prod/env.hcl`) should define a `generate` block to generate the AWS provider configuration to assume the role for that environment. For example, if you wanted to assume the role `arn:aws:iam::0123456789:role/terragrunt` in all the units for the prod account, you would put the following in `prod/env.hcl`: prod/env.hcl ```hcl generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = < Learn how Terragrunt helps you automate authentication workflows. AWS is by far the most popular OpenTofu/Terraform provider, and most Terragrunt users are using it to manage AWS infrastructure, at least in part. As a consequence, Terragrunt has a number of features that make it easier to work with AWS. The most secure way to manage multiple AWS accounts is to segment infrastructure between [multiple AWS accounts](https://aws.amazon.com/organizations/getting-started/best-practices). Segmenting infrastructure in this way can ensure that developers are not granted permissions they don’t need on infrastructure they don’t manage. It’s also a best practice from a safety perspective, as it helps to prevent accidental changes to sensitive resources like production infrastructure. When working with multiple AWS accounts, a best practice is to temporarily assume roles within those AWS accounts to perform actions using mechanisms like [IAM Identity Center](https://aws.amazon.com/iam/identity-center/) or [OIDC](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html). When using these technologies, users don’t need any static users or credentials. All access is temporary, and permissions are determined by the role they assume. These technologies allow you to securely (and temporarily) acquire secrets for least privilege access to a target AWS account, and perform actions that can only impact that AWS account, limiting blast radius. There are a few ways to assume IAM roles when using AWS CLI tools, such as OpenTofu/Terraform: 1. One option is to create a named [profile](http://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html), each with a different [role\_arn](http://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html) parameter. You then tell OpenTofu/Terraform which profile to use via the `AWS_PROFILE` environment variable. The downside to using profiles is that they can vary between users. One user might have a profile named `dev` that assumes a role in the `dev` account, while another user might have a profile named `development` that assumes the same role. This can lead to confusion and errors when sharing code between users. It also results in a requirement that all users have profiles set up on their local machines. Finally, this also presents a problem in CI/CD pipelines, where you typically avoid storing AWS credentials on disk so they are less likely to leak. 2. Another option is to use the [AWS CLI](https://aws.amazon.com/cli/). As a standard operating procedure, users are required to assume a role *before* invoking OpenTofu/Terraform by running something like `aws sts assume-role --role-arn `, use the output of that command to set the appropriate environment variables, and the tool is run with those temporary credentials stored as environment variables. The downside to this approach is that it requires that users know this process and remember to do it correctly every time they want to use OpenTofu/Terraform. It’s also a tedious process, and requires several steps to complete correctly. Worse yet, it requires that users repeat this process often, as the credentials you get back from the `assume-role` command expire. This is especially problematic if the OpenTofu/Terraform run is expected to take longer than the role assumption duration, and can expire mid-run. 3. A final option is to modify your AWS provider with the [assume\_role configuration block](https://search.opentofu.org/provider/hashicorp/aws/latest/docs#assume_role-configuration-block) and your S3 backend with the [role\_arn parameter](https://opentofu.org/docs/language/settings/backends/s3/#assume-role-configuration). The downside to managing your role assumption with the AWS provider is that all runs have to be performed with the same IAM role. This can be problematic if you have different users who assume different roles, depending on their need for elevated access, and as a best practice, the role assumed by CI/CD pipelines should be different from the role assumed by developers. The *way* in which these roles are assumed also differ, as developers might use a web-based SSO portal to acquire temporary credentials, while CI/CD pipelines might use OIDC and assume a role using a web identity token. Caution Temporary credentials obtained via `assume-role` can expire during long-running OpenTofu/Terraform operations. Terragrunt’s built-in role assumption (described below) handles automatic credential refresh, avoiding this problem. To avoid these frustrating trade-offs, you can configure Terragrunt to assume an IAM role for you. ## Configuring Terragrunt to assume an IAM role [Section titled “Configuring Terragrunt to assume an IAM role”](#configuring-terragrunt-to-assume-an-iam-role) To tell Terragrunt to assume an IAM role, just set the [`--iam-assume-role`](/reference/cli/commands/run#iam-assume-role) command line argument: ```bash terragrunt apply --iam-assume-role "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" ``` Alternatively, you can set the `TG_IAM_ASSUME_ROLE` environment variable: ```bash export TG_IAM_ASSUME_ROLE="arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" terragrunt apply ``` Additionally, you can specify an `iam_role` property in the terragrunt config: terragrunt.hcl ```hcl iam_role = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" ``` Terragrunt will resolve the value of the option by first looking for the cli argument, then looking for the environment variable, then defaulting to the value specified in the config file. Terragrunt will call the `sts assume-role` API on your behalf and expose the credentials it gets back as environment variables when running OpenTofu/Terraform. The advantage of this approach is that you can store your AWS credentials in a secret store and never write them to disk in plaintext, you get fresh credentials on every run of Terragrunt, without the complexity of calling `assume-role` yourself, and you don’t have to modify your OpenTofu/Terraform code or backend configuration at all. ## Leveraging OIDC role assumption [Section titled “Leveraging OIDC role assumption”](#leveraging-oidc-role-assumption) In addition, you can combine the `--iam-assume-role` flag with the [`--iam-assume-role-web-identity-token`](/reference/cli/commands/run#iam-assume-role-web-identity-token) to use the `AssumeRoleWithWebIdentity` API instead of the `AssumeRole` API. This is especially convenient in the context of CI/CD pipelines, as it’s generally a best practice to assume roles there via OIDC. Configuring OIDC role assumption largely works like the `--iam-assume-role` flag, with the addition of the `--iam-assume-role-web-identity-token` flag. Tip The `--iam-assume-role-web-identity-token` flag accepts both a raw token value and the path to a file containing the token. Terragrunt will automatically detect which one you’ve provided. As a command line argument: ```bash terragrunt apply --iam-assume-role "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" --iam-assume-role-web-identity-token "$TOKEN" ``` As environment variables: ```bash export TG_IAM_ASSUME_ROLE="arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" export TG_IAM_ASSUME_ROLE_WEB_IDENTITY_TOKEN="$TOKEN" terragrunt apply ``` In the Terragrunt configuration: terragrunt.hcl ```hcl iam_role = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" iam_web_identity_token = get_env("AN_OIDC_TOKEN") ``` ## Auth provider command [Section titled “Auth provider command”](#auth-provider-command) Finally, there is also a special flag that allows you to use an external command to provide the role assumption credentials. This is the most powerful and flexible option for setting up Terragrunt authentication, but it does require a bit more setup. This technique is especially useful in the following circumstances: * In a CI/CD pipelines, where you might want to use different role assumption mechanisms for different stages of the pipeline (like a read-only, plan role during pull request open, and a read-write, apply role during merge). * On a shared development repository, where you might want to use different roles for different developers, or even different roles for the same developer, depending on the task at hand. * In a setup where units in different accounts depend on each other, and you want to assume a different role for each account. The [`--auth-provider-cmd`](/reference/cli/commands/run#auth-provider-cmd) flag allows you to specify a command that can be executed by Terragrunt to fetch credentials at runtime. ```bash terragrunt apply --auth-provider-cmd /path/to/auth-script.sh ``` As with all other flags, you can also set this as an environment variable: ```bash export TG_AUTH_PROVIDER_CMD="/path/to/auth-script.sh" terragrunt apply ``` When Terragrunt executes this script, it will expect a response in stdout that obeys the following schema: auth-provider-cmd/v2/schema.json ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://docs.terragrunt.com/schemas/auth-provider-cmd/v2/schema.json", "title": "Terragrunt Auth Provider Command Response Schema", "description": "Schema for the JSON response expected from an auth provider command", "type": "object", "properties": { "awsCredentials": { "type": "object", "description": "AWS credentials to set as environment variables", "properties": { "ACCESS_KEY_ID": { "type": "string", "description": "AWS access key ID" }, "SECRET_ACCESS_KEY": { "type": "string", "description": "AWS secret access key" }, "SESSION_TOKEN": { "type": "string", "description": "AWS session token (optional)" } }, "required": [ "ACCESS_KEY_ID", "SECRET_ACCESS_KEY" ], "additionalProperties": false }, "awsRole": { "type": "object", "description": "AWS role to assume", "properties": { "roleARN": { "type": "string", "description": "The ARN of the IAM role to assume" }, "roleSessionName": { "type": "string", "description": "The session name for the assumed role" }, "duration": { "type": "integer", "description": "Duration in seconds for the assumed role session", "minimum": 0 }, "webIdentityToken": { "type": "string", "description": "Web identity token for OIDC-based role assumption" } }, "required": [ "roleARN" ], "additionalProperties": false }, "envs": { "type": "object", "description": "Additional environment variables to set", "additionalProperties": { "type": "string" } } }, "additionalProperties": false } ``` All top-level objects are optional, and you can provide multiple. * `awsCredentials` is the standard AWS credential object, which can be used to set the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and (optionally) `AWS_SESSION_TOKEN` environment variables before running OpenTofu/Terraform. * `awsRole` is the role assumption object, which can be used to dynamically perform role assumption on the `roleARN` role with the `roleSessionName` session name, for a `duration` of time, and with a `webIdentityToken` if needed. Terragrunt will automatically refresh this role assumption when the duration expires. * `envs` is a map of environment variables that will be set before running OpenTofu/Terraform. Note The auth provider command runs in the context of the Terragrunt working directory. You can use this to author logic in your script that determines which credentials to return based on the context of the Terragrunt run. This feature is integrated with the [Gruntwork Pipelines](https://www.gruntwork.io/platform/pipelines) product to provide a secure and flexible way to manage assumption of different roles in different accounts based on context. ## Required Permissions [Section titled “Required Permissions”](#required-permissions) You are ultimately responsible for ensuring that the IAM role you are assuming has the minimal and necessary permissions required to perform the activity you are attempting. At a minimum, however there is some guidance that you can follow for ensuring that you have sufficient permissions. Granting the following permissions on an IAM role: permissions.json ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowAllDynamoDBActionsOnAllTerragruntTables", "Effect": "Allow", "Action": "dynamodb:*", "Resource": [ "arn:aws:dynamodb:*:1234567890:table/terragrunt*" ] }, { "Sid": "AllowAllS3ActionsOnTerragruntBuckets", "Effect": "Allow", "Action": "s3:*", "Resource": [ "arn:aws:s3:::terragrunt*", "arn:aws:s3:::terragrunt*/*" ] } ] } ``` Will grant Terragrunt more than enough permissions to perform what it needs to do in AWS (replacing `1234567890` with your AWS account ID, and `terragrunt*` with the desired names of your Terragrunt resources). Note that these permissions might be too broad for your circumstances, however. A more minimal policy might look like the following: permissions.json ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowCreateAndListS3ActionsOnSpecifiedTerragruntBucket", "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketVersioning", "s3:GetBucketAcl", "s3:GetBucketLogging", "s3:CreateBucket", "s3:PutBucketPublicAccessBlock", "s3:PutBucketTagging", "s3:PutBucketPolicy", "s3:PutBucketVersioning", "s3:PutEncryptionConfiguration", "s3:PutBucketAcl", "s3:PutBucketLogging", "s3:GetEncryptionConfiguration", "s3:GetBucketPolicy", "s3:GetBucketPublicAccessBlock", "s3:PutLifecycleConfiguration", "s3:PutBucketOwnershipControls" ], "Resource": "arn:aws:s3:::BUCKET_NAME" }, { "Sid": "AllowGetAndPutS3ActionsOnSpecifiedTerragruntBucketPath", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject" ], "Resource": "arn:aws:s3:::BUCKET_NAME/some/path/here" }, { "Sid": "AllowCreateAndUpdateDynamoDBActionsOnSpecifiedTerragruntTable", "Effect": "Allow", "Action": [ "dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:DescribeTable", "dynamodb:DeleteItem", "dynamodb:CreateTable" ], "Resource": "arn:aws:dynamodb:*:*:table/TABLE_NAME" } ] } ``` As you can see, the permissions are getting locked down, and the risk you run by adopting these permissions is that you might not realize that you need certain permissions until you run into an error. Tip Start with permissions that are too narrow, and expand them as necessary. It’s easier to grant additional permissions when you encounter an error than to audit overly broad permissions later. Additionally, while Terragrunt *can* provision the S3 bucket and DynamoDB table it uses for S3 state storage, it doesn’t *need* to. You can create these resources manually, then grant Terragrunt permissions to interact with them (but not create them). A policy that allows this would look like the following: permissions.json ```json { "Version": "2012-10-17", "Statement": [ { "Action": [ "s3:GetBucketLocation", "s3:List*" ], "Resource": [ "arn:aws:s3:::" ], "Effect": "Allow" }, { "Action": [ "s3:DeleteObject", "s3:GetObject", "s3:PutObject", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::/*" ], "Effect": "Allow" }, { "Sid": "AllowCreateAndUpdateDynamoDBActionsOnSpecifiedTerragruntTable", "Effect": "Allow", "Action": [ "dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:DescribeTable", "dynamodb:DeleteItem", ], "Resource": "arn:aws:dynamodb:*:*:table/TABLE_NAME" } ] } ``` You’ll want to make sure that you set configurations like `skip_bucket_versioning` in [remote\_state](/reference/hcl/blocks#remote_state) to prevent Terragrunt from attempting to validate the bucket or table is in the proper configuration without requisite permissions. ## Further Reading [Section titled “Further Reading”](#further-reading) * [Units Overview](/features/units) — for context on how units work with remote modules and source URLs * [State Backend](/features/units/state-backend) — remote state also involves AWS credentials and role assumption * [`--iam-assume-role` CLI reference](/reference/cli/commands/run#iam-assume-role) — CLI flag for IAM role assumption * [`--auth-provider-cmd` CLI reference](/reference/cli/commands/run#auth-provider-cmd) — CLI flag for external auth provider commands # Auto-init > Learn how Terragrunt makes it so that you don't have to explicitly call `init` when using it. *Auto-Init* is a feature of Terragrunt that makes it so that `terragrunt init` does not need to be called explicitly before other terragrunt commands. When Auto-Init is enabled (the default), terragrunt will automatically call [`tofu init`](https://opentofu.org/docs/cli/commands/init/)/[`terraform init`](https://www.terraform.io/docs/commands/init.html) before other commands (e.g. `terragrunt plan`) when terragrunt detects that any of the following are true: * `init` has never been called. * `source` needs to be downloaded. * The `.terragrunt-init-required` file is in the downloaded module directory (`.terragrunt-cache/aaa/bbb/modules/`). * The modules or remote state used by a module have changed since the previous call to `init`. As [mentioned](/features/units/extra-arguments/#extra_arguments-for-init), `extra_arguments` can be configured to allow customization of the `tofu init` command. Note that there might be cases where Terragrunt does not detect that `tofu init` needs to be called. In such cases, OpenTofu/Terraform may fail, and re-running `terragrunt init` can resolve the issue. ## Disabling Auto-Init [Section titled “Disabling Auto-Init”](#disabling-auto-init) In some cases, it might be desirable to disable Auto-Init. For example, you might want to specify a different `-plugin-dir` option to `tofu init` (and don’t want to have it set in `extra_arguments`). To disable Auto-Init, use the `--no-auto-init` command line option or set the `TG_NO_AUTO_INIT` environment variable to `true`. Disabling Auto-Init requires you to explicitly run `terragrunt init` before executing any other Terragrunt commands for that configuration. If Auto-Init is disabled and Terragrunt detects that `init` should have been run, it will throw an error. # IaC Engines > Learn how to dynamically control OpenTofu/Terraform runs using IaC engines. IaC engines allow you to customize and configure how IaC updates are orchestrated by Terragrunt. This feature is still experimental and not recommended for general production usage. To try it out, all you need to do is include the following in your `terragrunt.hcl`: terragrunt.hcl ```hcl engine { source = "github.com/gruntwork-io/terragrunt-engine-opentofu" version = "v0.1.0" } ``` This example leverages the official OpenTofu engine, [publicly available on GitHub](https://github.com/gruntwork-io/terragrunt-engine-opentofu). This engine currently leverages the locally available installation of the `tofu` binary, just like Terragrunt does by default without use of engine configurations. It provides a convenient example of how to build engines for Terragrunt. In the future, this engine will expand in capability to include additional features and configurations. Since this functionality is experimental and not recommended for production, set the following environment variable to enable it: ```sh export TG_EXPERIMENTAL_ENGINE=1 ``` ## Use Cases [Section titled “Use Cases”](#use-cases) IaC Engines were introduced to offer advanced users of Terragrunt a level of customization over how exactly IaC updates are performed with a given set of Terragrunt configurations. Without usage of IaC Engines, Terragrunt will determine how IaC updates will be performed by doing things like invoking the `tofu` or `terraform` binary directly. For most users, this is fine. However, advanced users have more complex use cases that require more control over how those IaC updates are executed, given certain Terragrunt configurations. e.g. * Emitting custom logging or metrics whenever the `tofu` binary is executed. * Running `tofu` in a remote environment, such as a separate Kubernetes pod from the one executing Terragrunt. * Using different versions of `tofu` for different Terragrunt configurations in the same `run --all` execution. ## HTTPS Sources [Section titled “HTTPS Sources”](#https-sources) Use an HTTP(S) URL to specify the path to the engine: terragrunt.hcl ```hcl engine { source = "https://github.com/gruntwork-io/terragrunt-engine-opentofu/releases/download/v0.1.0/terragrunt-iac-engine-opentofu_rpc_v0.1.0_linux_amd64.zip" } ``` ## Local Sources [Section titled “Local Sources”](#local-sources) Specify a local absolute path as the source: terragrunt.hcl ```hcl engine { source = "/home/users/iac-engines/terragrunt-iac-engine-opentofu_v0.1.0" } ``` ## Parameters [Section titled “Parameters”](#parameters) * `source`: (Required) The source of the plugin. Multiple engine approaches are supported, including GitHub repositories, HTTP(S) paths, and local absolute paths. * `version`: (Optional) The version of the engine to download from GitHub releases. If not specified, the latest release is always downloaded. * `type`: (Optional) Currently, the only supported type is `rpc`. * `meta`: (Optional) A block for setting engine-specific metadata. This can include various configuration settings required by the engine. ## Caching [Section titled “Caching”](#caching) Engines are cached locally by default to enhance performance and minimize repeated downloads. The cached engines are stored in the following directory by default: `~/.cache/terragrunt/plugins/iac-engine/rpc/` If you need to use a different path, set the environment variable `TG_ENGINE_CACHE_PATH` accordingly. Downloaded engines are checked for integrity using the SHA256 checksum GPG key. If the checksum does not match, the engine is not executed. To disable this feature, set the environment variable: ```sh export TG_ENGINE_SKIP_CHECK=0 ``` To configure a custom log level for the engine, set the `TG_ENGINE_LOG_LEVEL` environment variable to one of `debug`, `info`, `warn`, `error`. ```sh export TG_ENGINE_LOG_LEVEL=debug ``` ## Engine Metadata [Section titled “Engine Metadata”](#engine-metadata) The `meta` block is used to pass metadata to the engine. This metadata can be used to configure the engine or pass additional information to the engine. The metadata block is a map of key-value pairs. Engines can read the information passed via the metadata map to configure themselves. terragrunt.hcl ```hcl engine { source = "github.com/gruntwork-io/terragrunt-engine-opentofu" # Optionally set metadata for the engine. meta = { key_1 = ["value1", "value2"] key_2 = "1.6.0" } } ``` Configurations you might want to set with `meta` include: * Connection configurations * Tool versions * Feature flags * Other configurations that the engine might want to be variable in different `terragrunt.hcl` files # Extra Arguments > Learn how to pass extra arguments to every OpenTofu/Terraform run. Sometimes you need to pass extra CLI arguments every time you run certain `tofu`/`terraform` commands. For example, you may want to set the `lock-timeout` setting to 20 minutes for all commands that may modify remote state so that OpenTofu/Terraform will keep trying to acquire a lock for up to 20 minutes if someone else already has the lock rather than immediately exiting with an error. You can configure Terragrunt to pass specific CLI arguments for specific commands using an `extra_arguments` block in your `terragrunt.hcl` file: terragrunt.hcl ```hcl terraform { # Force OpenTofu/Terraform to keep trying to acquire a lock for # up to 20 minutes if someone else already has the lock extra_arguments "retry_lock" { commands = [ "init", "apply", "refresh", "import", "plan", "taint", "untaint" ] arguments = [ "-lock-timeout=20m" ] env_vars = { TF_VAR_var_from_environment = "value" } } } ``` Each `extra_arguments` block includes: * A **label** — an arbitrary name for the block (in the example above, `retry_lock`) * **`commands`** — a list of OpenTofu/Terraform commands to which the extra arguments should be added * **`arguments`**, **`required_var_files`**, or **`optional_var_files`** — the extra arguments or var-files to pass You can also pass custom environment variables using the `env_vars` attribute, which stores environment variables in key value pairs. With the configuration above, when you run `terragrunt apply`, Terragrunt will call OpenTofu/Terraform as follows: ```bash $ terragrunt apply # tofu apply -lock-timeout=20m ``` You can even use built-in functions such as [`get_terraform_commands_that_need_locking`](/reference/hcl/functions/#get_terraform_commands_that_need_locking) to conveniently populate the list of OpenTofu/Terraform commands that need locking: terragrunt.hcl ```hcl terraform { # Force OpenTofu/Terraform to keep trying to acquire a lock for up to 20 minutes if someone else already has the lock extra_arguments "retry_lock" { commands = get_terraform_commands_that_need_locking() arguments = ["-lock-timeout=20m"] } } ``` Terragrunt provides four similar helper functions for populating the `commands` list: * [`get_terraform_commands_that_need_locking`](/reference/hcl/functions/#get_terraform_commands_that_need_locking) * [`get_terraform_commands_that_need_vars`](/reference/hcl/functions/#get_terraform_commands_that_need_vars) * [`get_terraform_commands_that_need_input`](/reference/hcl/functions/#get_terraform_commands_that_need_input) * [`get_terraform_commands_that_need_parallelism`](/reference/hcl/functions/#get_terraform_commands_that_need_parallelism) ## Multiple extra\_arguments blocks [Section titled “Multiple extra\_arguments blocks”](#multiple-extra_arguments-blocks) You can specify one or more `extra_arguments` blocks. The `arguments` in each block will be applied any time you call `terragrunt` with one of the commands in the `commands` list. If more than one `extra_arguments` block matches a command, the arguments will be added in the order of appearance in the configuration. For example, in addition to lock settings, you may also want to pass custom `-var-file` arguments to several commands: terragrunt.hcl ```hcl terraform { # Force OpenTofu/Terraform to keep trying to acquire a lock for # up to 20 minutes if someone else already has the lock extra_arguments "retry_lock" { commands = get_terraform_commands_that_need_locking() arguments = ["-lock-timeout=20m"] } # Pass custom var files to OpenTofu/Terraform extra_arguments "custom_vars" { commands = [ "apply", "plan", "import", "push", "refresh" ] arguments = [ "-var", "foo=bar", "-var", "region=us-west-1" ] } } ``` With the configuration above, when you run `terragrunt apply`, Terragrunt will call OpenTofu/Terraform as follows: ```bash $ terragrunt apply # tofu apply -lock-timeout=20m -var foo=bar -var region=us-west-1 ``` ## `extra_arguments` for `init` [Section titled “extra\_arguments for init”](#extra_arguments-for-init) Extra arguments for the `init` command have some additional behavior and constraints. In addition to being appended to the `tofu init`/`terraform init` command that is run when you explicitly run `terragrunt init`, `extra_arguments` for `init` will also be appended to the `init` commands that are automatically run during other commands (see [Auto-Init](/features/units/auto-init)). Caution You must *not* specify the `-from-module` option or the `DIR` argument in the `extra_arguments` for `init`. This option and argument will be provided automatically by Terragrunt. Here’s an example of configuring `extra_arguments` for `init` in an environment in which OpenTofu/Terraform plugins are manually installed, rather than relying on OpenTofu/Terraform to automatically download them. terragrunt.hcl ```hcl terraform { # ... extra_arguments "init_args" { commands = [ "init" ] arguments = [ "-plugin-dir=/my/tofu/plugin/dir", ] } } ``` Tip You’re encouraged to use one of the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) or [Provider Cache Server](/features/caching/provider-cache-server) features instead of manually installing plugins in most cases. ## Required and optional var-files [Section titled “Required and optional var-files”](#required-and-optional-var-files) One common usage of `extra_arguments` is to include tfvars files. Instead of using `arguments`, you can use `required_var_files` or `optional_var_files`. Both add `-var-file=` for each file specified, but they differ in how they handle missing files: * **`required_var_files`** — all listed files must exist. Terragrunt will exit with an error if any are missing. * **`optional_var_files`** — files that don’t exist are silently skipped. This is useful for conditional configurations based on environment variables. Here’s an example: * root.hcl * prod.tfvars * us-west-2.tfvars * backend-app * main.tf * dev.tfvars * terragrunt.hcl * frontend-app * main.tf * us-east-1.tfvars * terragrunt.hcl backend-app/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { extra_arguments "conditional_vars" { commands = [ "apply", "plan", "import", "push", "refresh" ] required_var_files = [ "${get_parent_terragrunt_dir()}/tofu.tfvars" ] optional_var_files = [ "${get_parent_terragrunt_dir("root")}/${get_env("TF_VAR_env", "dev")}.tfvars", "${get_parent_terragrunt_dir("root")}/${get_env("TF_VAR_region", "us-east-1")}.tfvars", "${get_terragrunt_dir()}/${get_env("TF_VAR_env", "dev")}.tfvars", "${get_terragrunt_dir()}/${get_env("TF_VAR_region", "us-east-1")}.tfvars" ] } } ``` See the [`get_terragrunt_dir()`](/reference/hcl/functions/#get_terragrunt_dir) and [`get_parent_terragrunt_dir()`](/reference/hcl/functions/#get_parent_terragrunt_dir) documentation for more details. With the configuration above, when you run `terragrunt run --all apply`, Terragrunt will call OpenTofu/Terraform as follows: ```bash $ terragrunt run --all apply [backend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/backend-app/dev.tfvars [frontend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/frontend-app/us-east-1.tfvars $ TF_VAR_env=prod terragrunt run --all apply [backend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars [frontend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars -var-file=/my/tf/frontend-app/us-east-1.tfvars $ TF_VAR_env=prod TF_VAR_region=us-west-2 terragrunt run --all apply [backend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars -var-file=/my/tf/us-west-2.tfvars [frontend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars -var-file=/my/tf/us-west-2.tfvars ``` ## Handling whitespace [Section titled “Handling whitespace”](#handling-whitespace) The list of arguments cannot include whitespace, so if you need to pass command line arguments that include spaces (e.g. `-var bucket=example.bucket.name`), then each of the arguments will need to be a separate item in the `arguments` list: terragrunt.hcl ```hcl terraform { extra_arguments "bucket" { commands = [ "apply", "plan", "import", "push", "refresh" ] arguments = [ "-var", "bucket=example.bucket.name", ] } } ``` With the configuration above, when you run `terragrunt apply`, Terragrunt will call OpenTofu/Terraform as follows: ```bash $ terragrunt apply # tofu apply -var bucket=example.bucket.name ``` ## Further reading [Section titled “Further reading”](#further-reading) * [`extra_arguments` block reference](/reference/hcl/blocks/#terraform) * [Helper functions reference](/reference/hcl/functions/) — includes the `get_terraform_commands_that_need_*` family * [Auto-Init feature](/features/units/auto-init) — how Terragrunt automatically runs `init` # Hooks > Learn how to execute custom code before or after running OpenTofu/Terraform, or when errors occur. *Before Hooks*, *After Hooks* and *Error Hooks* are a feature of terragrunt that make it possible to define custom actions that will be called before/after running an `tofu`/`terraform` command. They allow you to *orchestrate* certain operations around IaC updates so that you have a consistent way to run custom code before or after running OpenTofu/Terraform. Here’s an example: terragrunt.hcl ```hcl terraform { before_hook "before_hook" { commands = ["apply", "plan"] execute = ["echo", "Running OpenTofu"] } after_hook "after_hook" { commands = ["apply", "plan"] execute = ["echo", "Finished running OpenTofu"] run_on_error = true } error_hook "import_resource" { commands = ["apply"] execute = ["echo", "Error Hook executed"] on_errors = [ ".*", ] } } ``` In this example configuration, whenever Terragrunt runs `tofu apply` or `tofu plan` (or the `terraform` equivalent), three things will happen: * Before Terragrunt runs `tofu`/`terraform`, it will output `Running OpenTofu` to the console. * After Terragrunt runs `tofu`/`terraform`, it will output `Finished running OpenTofu`, regardless of whether the command failed. * If an error occurs during the `tofu apply` command, Terragrunt will output `Error Hook executed`. You can learn more about all the various configuration options supported in [the reference docs for the terraform block](/reference/hcl/blocks#terraform). ## Hook Context [Section titled “Hook Context”](#hook-context) All hooks add extra environment variables when executing the hook’s run command: * `TG_CTX_TF_PATH` * `TG_CTX_COMMAND` * `TG_CTX_HOOK_NAME` When the [`hook-context-env`](/reference/experiments/active#hook-context-env) experiment is enabled, three additional variables are also set: * `TG_CTX_HOOK_TYPE` — `before_hook`, `after_hook`, or `error_hook` * `TG_CTX_SOURCE` — the resolved terraform source URL (CLI `--source` override, else evaluated `terraform.source` with source-map applied, else `.`) * `TG_CTX_TERRAGRUNT_DIR` — the directory of the current Terragrunt config For example: terragrunt.hcl ```hcl terraform { before_hook "test_hook" { commands = ["apply"] execute = ["hook.sh"] } } ``` Where `hook.sh` is: hook.sh ```bash echo "TF_PATH=${TG_CTX_TF_PATH} COMMAND=${TG_CTX_COMMAND} HOOK_NAME=${TG_CTX_HOOK_NAME}" ``` Will result in the following output when Terragrunt runs `tofu apply`/`terraform apply`: ```bash TF_PATH=tofu COMMAND=apply HOOK_NAME=test_hook ``` Note that hooks are executed within the working directory where OpenTofu/Terraform would be run. If using the `source` attribute for the `terraform` block, this will result in the hook running in the hidden `.terragrunt-cache` directory. This also means that you can use `tofu`/`terraform` commands within hooks to access any outputs needed for hook logic. For example: ```bash # Get the bucket_name output from OpenTofu/Terraform state BUCKET_NAME="$("$TG_CTX_TF_PATH" output -raw bucket_name)" # Use the AWS CLI to list the contents of the bucket aws s3 ls "s3://$BUCKET_NAME" ``` Note that the `TG_CTX_TF_PATH` environment variable is used here to ensure compatibility, regardless of the value of [tf-path](/reference/cli/commands/run#tf-path). This can be a useful practice if you are migrating between OpenTofu or Terraform. You will also have access to all the `inputs` set in the `terragrunt.hcl` file as environment variables prefixed by `TF_VAR_`, as that’s how the variables are set for use in OpenTofu/Terraform. For example, if you have the following `inputs` block in your `terragrunt.hcl` file: terragrunt.hcl ```hcl inputs = { bucket_name = "my-bucket" } ``` You can access the `bucket_name` input in your hook as follows: ```bash # Get the bucket_name input from the terragrunt.hcl file BUCKET_NAME="$TF_VAR_bucket_name" # Use the AWS CLI to list the contents of the bucket aws s3 ls "s3://$BUCKET_NAME" ``` ## Orchestrating execution outside IaC [Section titled “Orchestrating execution outside IaC”](#orchestrating-execution-outside-iac) Hooks can be used to handle operations that need to happen, but are not directly related to the OpenTofu/Terraform. For example, you may be using Terragrunt to manage an [AWS ECS service](https://aws.amazon.com/ecs/). You can use a `before_hook` to build and push a new image to the [Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) before running `tofu apply`. terragrunt.hcl ```hcl terraform { before_hook "build_and_push_image" { commands = ["plan", "apply"] execute = ["./build_and_push_image.sh"] } } ``` Where `build_and_push_image.sh` is something like: build\_and\_push\_image.sh ```bash #!/usr/bin/env bash set -eou pipefail ACCOUNT_ID="123456789012" REGION="us-east-1" REPOSITORY="my-repository" TAG="latest" IMAGE_TAG="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY}:${TAG}" # Build the Docker image docker build -t "$IMAGE_TAG" . # Push the Docker image to ECR aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com docker push "$IMAGE_TAG" ``` The hard-coding of values in the script could be replaced with context, as shown in the previous section. Similarly, you may want to smoke-test newly deployed infrastructure after running `tofu apply`. terragrunt.hcl ```hcl terraform { after_hook "smoke_test" { commands = ["apply"] execute = ["./smoke_test.sh"] run_on_error = true } } ``` Where `smoke_test.sh` is something like: smoke\_test.sh ```bash #!/usr/bin/env bash set -eou pipefail # Get the URL for the service from OpenTofu/Terraform state SERVICE_URL="$("$TG_CTX_TF_PATH" output -raw service_url)" # Use curl to check the service is up curl -sSf "$SERVICE_URL" ``` You might even decide to integrate with a product like [Terratest](https://github.com/gruntwork-io/terratest) for more complex testing. ## Hook Ordering [Section titled “Hook Ordering”](#hook-ordering) You can have multiple before and after hooks. Each hook will execute in the order they are defined. For example: terragrunt.hcl ```hcl terraform { before_hook "before_hook_1" { commands = ["apply", "plan"] execute = ["echo", "Will run OpenTofu"] } before_hook "before_hook_2" { commands = ["apply", "plan"] execute = ["echo", "Running OpenTofu"] } } ``` This configuration will cause Terragrunt to output `Will run OpenTofu` and then `Running OpenTofu` before the call to OpenTofu/Terraform. ## Hook exit codes [Section titled “Hook exit codes”](#hook-exit-codes) Hooks control the exit code Terragrunt returns. If any `before_hook`, `after_hook`, or `error_hook` exits with a non-zero status, the `terragrunt` command exits with a non-zero status, even when the underlying `tofu`/`terraform` command succeeded. A failing `before_hook` short-circuits the run: Terragrunt does not invoke `tofu`/`terraform` at all, and does not run any `after_hook` whose `run_on_error` is not set. For example, suppose you run `terragrunt apply` on a unit with no `before_hook` and one `after_hook`. Terragrunt runs `tofu apply` and it returns `0`. Terragrunt then runs the `after_hook`, which returns `1`. The `terragrunt apply` command exits with `1`, even though `tofu apply` succeeded. ## Tflint hook [Section titled “Tflint hook”](#tflint-hook) *Before Hooks* or *After Hooks* natively support *tflint*, a linter for OpenTofu/Terraform code. It will validate the OpenTofu/Terraform code used by Terragrunt, and its inputs. This support includes automatically running `tflint init`, and passing in variables. Here’s an example: terragrunt.hcl ```hcl terraform { before_hook "before_hook" { commands = ["apply", "plan"] execute = ["tflint"] } } ``` The `.tflint.hcl` should exist in the same folder as `terragrunt.hcl` or one of it’s parents. If Terragrunt can’t find a `.tflint.hcl` file, it won’t execute tflint and return an error. All configurations should be in a `config` block in this file, as per [Tflint’s docs](https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/config.md). .tflint.hcl ```hcl plugin "aws" { enabled = true version = "0.21.0" source = "github.com/terraform-linters/tflint-ruleset-aws" } config { module = true } ``` ### Configuration [Section titled “Configuration”](#configuration) Any desired extra configuration should be added in the `.tflint.hcl` file. It will work with a `.tflint.hcl` file in the current folder or any parent folder. To utilize an alternative configuration file, use the `--config` flag with the path to the configuration file. For example: terragrunt.hcl ```hcl terraform { before_hook "tflint" { commands = ["apply", "plan"] execute = ["tflint", "--minimum-failure-severity=error", "--config", "custom.tflint.hcl"] } } ``` If you need to bypass the integration behavior (auto init or passing in variables), you can specify an absolute path to `tflint`, as this integration works by checking the first value in the `execute` array: terragrunt.hcl ```hcl terraform { before_hook "before_hook" { commands = ["apply", "plan"] execute = ["/usr/local/bin/tflint", "some", "args"] } } ``` ### Authentication for tflint rulesets [Section titled “Authentication for tflint rulesets”](#authentication-for-tflint-rulesets) *Public rulesets* `tflint` works without any authentication for public rulesets (hosted on public repositories). *Private rulesets* If you want to run the `tflint` hook with custom rulesets defined in a private repository, you will need to export a valid `GITHUB_TOKEN` token. ### Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **`flag provided but not defined: -act-as-bundled-plugin` error** If you have an `.tflint.hcl` file that is empty, or uses the `terraform` ruleset without version or source constraint, it can return the following error: ```log Failed to initialize plugins; Unrecognized remote plugin message: Incorrect Usage. flag provided but not defined: -act-as-bundled-plugin ``` To fix this, make sure that the configuration for the `terraform` ruleset, in the `.tflint.hcl` file contains a version constraint: .tflint.hcl ```hcl plugin "terraform" { enabled = true version = "0.2.1" source = "github.com/terraform-linters/tflint-ruleset-terraform" } ``` # Includes > Learn how to reuse partial Terragrunt configurations to DRY up your configurations. ## Motivation [Section titled “Motivation”](#motivation) As covered in [Units](/features/units) and [State Backend](/features/units/state-backend), it quickly becomes important to define base Terragrunt configuration files that are included in units. This is to ensure that all units have a consistent configuration, and to avoid repeating the same configuration across multiple units. For example, you might have a **root** Terragrunt configuration that defines the remote state and provider configurations for all your units: root.hcl ```hcl remote_state { backend = "s3" config = { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = < Learn how Terragrunt allows for runtime control using feature flags, error handling, and excludes. Sometimes, you need to have Terragrunt behave differently at runtime due to specific context that you have in your environment. The following configuration blocks have been designed to work together in concert to provide you a great deal of flexibility in how Terragrunt behaves at runtime. ## Feature Flags [Section titled “Feature Flags”](#feature-flags) Defined using the [feature](/reference/hcl/blocks#feature) configuration block, Terragrunt allows for the control of specific features at runtime using feature flags. For example: terragrunt.hcl ```hcl feature "s3_version" { default = "v1.0.0" } terraform { source = "git::git@github.com:acme/infrastructure-modules.git//storage/s3?ref=${feature.s3_version.value}" } ``` The configuration above allows you to set a default version for the `s3_version` feature flag, controlling the tag used for fetching the `s3` module from the `infrastructure-modules` repository. At runtime, you can override the default value by doing one of the following: ```bash terragrunt apply --feature s3_version=v1.1.0 ``` Or by setting the corresponding environment variable: ```bash export TG_FEATURE="s3_version=v1.1.0" terragrunt apply ``` This can be a useful way to opt in to new features or to test changes in your infrastructure. Setting a different version of an OpenTofu/Terraform module in a lower environment can be useful for testing changes before rolling them out to production. Users will always use the default version unless they explicitly set a different value. ## Errors [Section titled “Errors”](#errors) Defined using the [errors](/reference/hcl/blocks#errors) configuration block, Terragrunt allows for fine-grained control of errors at runtime. For example: terragrunt.hcl ```hcl errors { # Retry block for transient errors retry "transient_errors" { retryable_errors = [".*Error: transient network issue.*"] max_attempts = 3 sleep_interval_sec = 5 } # Ignore block for known safe-to-ignore errors ignore "known_safe_errors" { ignorable_errors = [ ".*Error: safe warning.*", "!.*Error: do not ignore.*" ] message = "Ignoring safe warning errors" signals = { alert_team = false } } } ``` This configuration allows for control over how Terragrunt handles errors at runtime. In the example above, Terragrunt will retry up to three times with a five-second pause between each retry for any error that matches the regex `.*Error: transient network issue.*`. It will also ignore any error that matches the regex `.*Error: safe warning.*`, but will not ignore any error that matches the regex `.*Error: do not ignore.*`. When it ignores an error that it can safely ignore, it will output the message `Ignoring safe warning errors`, and will generate a file named `error-signals.json` in the working directory with the following content: error-signals.json ```json { "alert_team": false } ``` You can learn more about how this configuration block works in the documentation linked above, but for now, what’s important to know is that it allows you to determine what Terragrunt should do when it encounters an error at runtime. Note that these configurations can also be adjusted dynamically. You can use a combination of feature flags and errors to control how Terragrunt behaves at runtime. Say, for example, a developer was trying to roll out a new version of your module that is known to be potentially flaky. You want to integrate your new module update with the rest of your team, but you don’t want to break runs that aren’t ready for the new module. You could use a feature flag to control introduction of that new module, and an error block to ignore any errors that you know are safe to ignore. terragrunt.hcl ```hcl feature "enable_flaky_module" { default = false } locals { version = feature.enable_flaky_module.value ? "v1.0.0" : "v1.1.0" } terraform { source = "git::git@github.com:acme/infrastructure-modules.git//storage/s3?ref=${local.version}" } errors { # Ignore errors when set ignore "flaky_module_errors" { ignorable_errors = feature.enable_flaky_module.value ? [ ".*Error: flaky module error.*" ] : [] message = "Ignoring flaky module error" signals = { send_notification = true } } } ``` In this example, the `enable_flaky_module` feature flag sets *both* the version of the module, and the error handling behavior for the unit that consumes it. This would allow the developer to integrate the unit configuration update with the rest of the codebase, enable the flag that introduces the module update in a lower environment, and then ignore any errors that are known to be safe to ignore. This pattern allows for greater speed of integration with larger codebases, and can be a useful tool for managing risk in your infrastructure. ## Excludes [Section titled “Excludes”](#excludes) Defined using the [exclude](/reference/hcl/blocks#exclude) configuration block, Terragrunt allows for the exclusion of specific units at runtime. For example: terragrunt.hcl ```hcl locals { day_of_week = formatdate("EEE", timestamp()) ban_deploy = contains(["Fri", "Sat", "Sun"], local.day_of_week) } exclude { if = local.ban_deploy actions = ["apply", "destroy"] } ``` In this example, the `exclude` block will prevent the `apply` command from running in a given unit on Fridays, Saturdays, and Sundays, as all good DevOps engineers know that deploying that close to a weekend is a recipe for disaster. While a toy example, this demonstrates how you can use the `exclude` block to use dynamic information at runtime to control the [run queue](/getting-started/terminology/#run-queue). You can use this block to prevent certain units from running in certain environments, or to prevent certain commands from running in certain units. Note that, just like with the other blocks mentioned so far, you can use a combination of configurations mentioned here to ensure that Terragrunt behaves exactly as you need it to at runtime. A more practical use of the `exclude` block would be to control which environments are run in `run --all` commands. For example: dev/root.hcl ```hcl feature "dev" { default = true } exclude { if = !feature.dev.value actions = ["all_except_output"] } ``` stage/root.hcl ```hcl feature "stage" { default = false } exclude { if = !feature.stage.value actions = ["all_except_output"] } ``` prod/root.hcl ```hcl feature "prod" { default = false } exclude { if = !feature.prod.value actions = ["all_except_output"] } ``` In this example, the `dev`, `stage` and `prod` directories have their own root configurations that are included by all units in their respective environments. The assumption of a configuration like this is that each environment is fully self-contained, and that the team has a desire to always update `dev` units, but wants to opt in changes to `stage` and `prod` units. In this setup, any `run --all` command like the following: ```bash terragrunt run --all plan ``` Will exclude all units in both the `stage` and `prod` directories, as the `feature` block in each of those directories is set to `false` by default. As a result, the only units that are run are those in the `dev` directory. When a user wants to opt in to updates for the `stage` environment, they could do something like this: ```bash terragrunt run --all --feature stage=true plan ``` They can even mix and match feature flags to opt-in/out of multiple environments at once: ```bash terragrunt run --all --feature dev=false --feature stage=true --feature prod=true plan ``` This allows for a great deal of flexibility in how you programmatically control the behavior of Terragrunt at runtime. ### Exclusion from the Run Queue [Section titled “Exclusion from the Run Queue”](#exclusion-from-the-run-queue) The `exclude` block will only exclude the unit from the run queue, which is only relevant in the context of a `run --all` command. A user could still explicitly navigate to the unit directory and run the command manually. If you would like to explicitly prevent a command from being run, even if a user was to navigate to the unit directory and run the command manually, you can use a combination of the `exclude` block and a `before_hook` block to prevent the command from running. For example: terragrunt.hcl ```hcl locals { day_of_week = formatdate("EEE", timestamp()) ban_deploy = contains(["Fri", "Sat", "Sun"], local.day_of_week) } exclude { if = local.ban_deploy actions = ["apply", "destroy"] } terraform { before_hook "prevent_deploy" { commands = ["apply", "destroy"] execute = local.ban_deploy ? ["bash", "-c", "echo 'Deploying on weekends is not allowed. Go home.' && exit 1"] : [] } } ``` Note that this will result in an exit code of 1, rather than merely excluding the unit from the run queue, which is slightly different behavior. # State Backend > Learn how Terragrunt can create and manage remote state backends. OpenTofu/Terraform supports [remote state storage](https://opentofu.org/docs/language/state/remote/) via various [backends](https://opentofu.org/docs/language/settings/backends/configuration/) that you normally configure in your `.tf` files as follows: main.tf ```hcl terraform { backend "s3" { bucket = "my-tofu-state" key = "frontend-app/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } ``` Unfortunately, the `backend` configuration does not currently support expressions, variables, or functions. This makes it hard to keep your code [DRY](/getting-started/terminology/#dont-repeat-yourself-dry) if you have multiple OpenTofu/Terraform modules. For example, consider the following filesystem layout, which uses different OpenTofu/Terraform modules to deploy a backend app, frontend app, MySQL database, and a VPC: * backend-app * main.tf * frontend-app * main.tf * mysql * main.tf * vpc * main.tf To use remote state with each of these modules, you would have to copy/paste the identical `backend` configuration into each of the `main.tf` files. The only thing that would differ between the configurations would be the `key` parameter: e.g., the `key` for `mysql/main.tf` might be `mysql/terraform.tfstate` and the `key` for `frontend-app/main.tf` might be `frontend-app/terraform.tfstate`. In addition, the resources used for remote state will be provisioned *somewhere else*, and that *somewhere else* needs to be managed. Most users end up using “click-ops” to provision the S3 bucket and DynamoDB table used for AWS remote state (clicking around in the AWS console until they have what they need). This is error-prone, difficult to reproduce, and makes it hard to do the *right thing* consistently (e.g., enabling versioning, encryption, and access logging). Luckily, Terragrunt has built-in tooling to make it easy to manage remote state. ## Generating remote state settings with Terragrunt [Section titled “Generating remote state settings with Terragrunt”](#generating-remote-state-settings-with-terragrunt) To fill in the settings via Terragrunt, create a `root.hcl` file in the root folder, plus one `terragrunt.hcl` file in each of the OpenTofu/Terraform modules: * root.hcl * backend-app * main.tf * terragrunt.hcl * frontend-app * main.tf * terragrunt.hcl * mysql * main.tf * terragrunt.hcl * vpc * main.tf * terragrunt.hcl In your `root.hcl` file, you can define your entire remote state configuration just once in a `generate` block, to generate a `backend.tf` file that includes the backend configuration: root.hcl ```hcl generate "backend" { path = "backend.tf" if_exists = "overwrite_terragrunt" contents = < Install Terragrunt! ## Quick Install [Section titled “Quick Install”](#quick-install) The quickest way to install Terragrunt on Linux or macOS: ```bash curl -sSfL --proto '=https' --tlsv1.2 https://terragrunt.com/install | bash ``` Script options and inspecting the script Run `curl -sSfL --proto '=https' --tlsv1.2 https://terragrunt.com/install | bash -s -- --help` to see all options, or [read the script on GitHub](https://github.com/gruntwork-io/terragrunt/blob/main/docs/public/install). Piping a remote script directly into `bash` means executing code you haven’t read. If you’d rather review the script before running it, download it to your local filesystem, read through it, and then run it: ```bash curl -sSfL --proto '=https' --tlsv1.2 -o install.sh https://terragrunt.com/install less install.sh bash install.sh ``` ## Download from releases page [Section titled “Download from releases page”](#download-from-releases-page) 1. Go to the [Releases Page](https://github.com/gruntwork-io/terragrunt/releases). 2. Download the archive for your operating system: e.g., if you’re on a Mac, download `terragrunt_darwin_amd64.tar.gz`; if you’re on Windows, download `terragrunt_windows_amd64.exe.zip`, etc. 3. Download `SHA256SUMS` and optionally `SHA256SUMS.gpgsig` for signature verification. 4. Verify the checksum and optionally the signature (see [Verifying the checksum](#verifying-the-checksum) below). 5. Extract the archive: e.g., `tar -xzf terragrunt_darwin_amd64.tar.gz` or unzip on Windows. 6. Add execute permissions to the binary (Linux/Mac): `chmod u+x terragrunt`. 7. Put the binary somewhere on your `PATH`: e.g., On Linux and Mac: `mv terragrunt /usr/local/bin/terragrunt`. ### Verifying the checksum [Section titled “Verifying the checksum”](#verifying-the-checksum) When you download the binary from the releases page, you can also use the checksum file to verify the integrity of the binary. This can be useful for ensuring that you have an intact binary and that it has not been tampered with. To verify the integrity of the file, do the following: 1. Have the binary downloaded, and accessible. 2. Generate the SHA256 checksum of the binary. 3. Download the `SHA256SUMS` file from the releases page. 4. Find the expected checksum for the binary you downloaded. 5. If the checksums match, the binary is intact and has not been tampered with. 6. Optionally, verify the GPG signature: ```bash # Import Gruntwork's public key (first time only) curl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import # Verify signature gpg --verify SHA256SUMS.gpgsig SHA256SUMS ``` Verify Key Fingerprint After importing the key, verify its fingerprint matches exactly: ```bash gpg --fingerprint 577774ACA847CC49 ``` Expected output: ```plaintext pub ed25519 2026-01-12 [SC] 68C8 0F86 DF98 E710 C0F2 2E2E 5777 74AC A847 CC49 uid [ unknown] Gruntwork (Code Signing Key) ``` 7. Alternatively, verify with Cosign: ```bash cosign verify-blob SHA256SUMS \ --bundle SHA256SUMS.sigstore.json \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp "github.com/gruntwork-io/terragrunt" ``` ### Convenience Scripts [Section titled “Convenience Scripts”](#convenience-scripts) * Linux (x86) ```bash set -euo pipefail OS="linux" ARCH="amd64" VERSION="v1.0.7" BINARY_NAME="terragrunt_${OS}_${ARCH}" BASE_URL="https://github.com/gruntwork-io/terragrunt/releases/download/$VERSION" # Download binary and verification files curl -sL "$BASE_URL/$BINARY_NAME" -o "$BINARY_NAME" curl -sL "$BASE_URL/SHA256SUMS" -o SHA256SUMS curl -sL "$BASE_URL/SHA256SUMS.gpgsig" -o SHA256SUMS.gpgsig # First: Import Gruntwork signing key and verify GPG signature of checksum file curl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import 2>/dev/null if gpg --verify SHA256SUMS.gpgsig SHA256SUMS 2>/dev/null; then echo "GPG signature verified!" else echo "GPG signature verification failed!" exit 1 fi # Second: Verify checksum of binary against trusted SHA256SUMS CHECKSUM="$(sha256sum "$BINARY_NAME" | awk '{print $1}')" EXPECTED_CHECKSUM="$(awk -v binary="$BINARY_NAME" '$2 == binary {print $1; exit}' SHA256SUMS)" if [ "$CHECKSUM" != "$EXPECTED_CHECKSUM" ]; then echo "Checksum verification failed!" exit 1 fi echo "Checksum verified!" echo "Terragrunt $VERSION downloaded and verified successfully" ``` * macOS (ARM) ```bash set -euo pipefail OS="darwin" ARCH="arm64" VERSION="v1.0.7" BINARY_NAME="terragrunt_${OS}_${ARCH}" BASE_URL="https://github.com/gruntwork-io/terragrunt/releases/download/$VERSION" # Download binary and verification files curl -sL "$BASE_URL/$BINARY_NAME" -o "$BINARY_NAME" curl -sL "$BASE_URL/SHA256SUMS" -o SHA256SUMS curl -sL "$BASE_URL/SHA256SUMS.gpgsig" -o SHA256SUMS.gpgsig # First: Import Gruntwork signing key and verify GPG signature of checksum file curl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import 2>/dev/null if gpg --verify SHA256SUMS.gpgsig SHA256SUMS 2>/dev/null; then echo "GPG signature verified!" else echo "GPG signature verification failed!" exit 1 fi # Second: Verify checksum of binary against trusted SHA256SUMS CHECKSUM="$(shasum -a 256 "$BINARY_NAME" | awk '{print $1}')" EXPECTED_CHECKSUM="$(awk -v binary="$BINARY_NAME" '$2 == binary {print $1; exit}' SHA256SUMS)" if [ "$CHECKSUM" != "$EXPECTED_CHECKSUM" ]; then echo "Checksum verification failed!" exit 1 fi echo "Checksum verified!" echo "Terragrunt $VERSION downloaded and verified successfully" ``` * Windows ```powershell $os = "windows" $arch = "amd64" $version = "v1.0.7" $binaryName = "terragrunt_${os}_${arch}.exe" try { $ProgressPreference = 'SilentlyContinue' $baseUrl = "https://github.com/gruntwork-io/terragrunt/releases/download/$version" Write-Host "Downloading Terragrunt $version..." Invoke-WebRequest -Uri "$baseUrl/$binaryName" -OutFile $binaryName -UseBasicParsing Invoke-WebRequest -Uri "$baseUrl/SHA256SUMS" -OutFile "SHA256SUMS" -UseBasicParsing Invoke-WebRequest -Uri "$baseUrl/SHA256SUMS.gpgsig" -OutFile "SHA256SUMS.gpgsig" -UseBasicParsing # First: Verify GPG signature of checksum file (requires gpg installed) Write-Host "Importing Gruntwork signing key..." Invoke-WebRequest -Uri "https://gruntwork.io/.well-known/pgp-key.txt" -OutFile "pgp-key.txt" -UseBasicParsing gpg --import pgp-key.txt 2>$null Write-Host "Verifying GPG signature of SHA256SUMS..." gpg --verify SHA256SUMS.gpgsig SHA256SUMS if ($LASTEXITCODE -ne 0) { Write-Error "GPG signature verification failed" exit 1 } Write-Host "GPG signature verified!" # Second: Verify checksum of binary against trusted SHA256SUMS $actualChecksum = (Get-FileHash -Algorithm SHA256 $binaryName).Hash.ToLower() $expectedChecksum = (Get-Content "SHA256SUMS" | ForEach-Object { $parts = $_ -split 's+'; if ($parts[1] -eq $binaryName) { return $parts[0].ToLower() } } | Select-Object -First 1) if ($actualChecksum -ne $expectedChecksum) { Write-Error "Checksum verification failed" exit 1 } Write-Host "Checksum verified!" Write-Host "Terragrunt $version downloaded and verified successfully" } catch { Write-Error "Failed: $_" exit 1 } finally { $ProgressPreference = 'Continue' } ``` * Linux (ARM) ```bash set -euo pipefail OS="linux" ARCH="arm64" VERSION="v1.0.7" BINARY_NAME="terragrunt_${OS}_${ARCH}" BASE_URL="https://github.com/gruntwork-io/terragrunt/releases/download/$VERSION" # Download binary and verification files curl -sL "$BASE_URL/$BINARY_NAME" -o "$BINARY_NAME" curl -sL "$BASE_URL/SHA256SUMS" -o SHA256SUMS curl -sL "$BASE_URL/SHA256SUMS.gpgsig" -o SHA256SUMS.gpgsig # First: Import Gruntwork signing key and verify GPG signature of checksum file curl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import 2>/dev/null if gpg --verify SHA256SUMS.gpgsig SHA256SUMS 2>/dev/null; then echo "GPG signature verified!" else echo "GPG signature verification failed!" exit 1 fi # Second: Verify checksum of binary against trusted SHA256SUMS CHECKSUM="$(sha256sum "$BINARY_NAME" | awk '{print $1}')" EXPECTED_CHECKSUM="$(awk -v binary="$BINARY_NAME" '$2 == binary {print $1; exit}' SHA256SUMS)" if [ "$CHECKSUM" != "$EXPECTED_CHECKSUM" ]; then echo "Checksum verification failed!" exit 1 fi echo "Checksum verified!" echo "Terragrunt $VERSION downloaded and verified successfully" ``` * macOS (x86) ```bash set -euo pipefail OS="darwin" ARCH="x86" VERSION="v1.0.7" BINARY_NAME="terragrunt_${OS}_${ARCH}" BASE_URL="https://github.com/gruntwork-io/terragrunt/releases/download/$VERSION" # Download binary and verification files curl -sL "$BASE_URL/$BINARY_NAME" -o "$BINARY_NAME" curl -sL "$BASE_URL/SHA256SUMS" -o SHA256SUMS curl -sL "$BASE_URL/SHA256SUMS.gpgsig" -o SHA256SUMS.gpgsig # First: Import Gruntwork signing key and verify GPG signature of checksum file curl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import 2>/dev/null if gpg --verify SHA256SUMS.gpgsig SHA256SUMS 2>/dev/null; then echo "GPG signature verified!" else echo "GPG signature verification failed!" exit 1 fi # Second: Verify checksum of binary against trusted SHA256SUMS CHECKSUM="$(shasum -a 256 "$BINARY_NAME" | awk '{print $1}')" EXPECTED_CHECKSUM="$(awk -v binary="$BINARY_NAME" '$2 == binary {print $1; exit}' SHA256SUMS)" if [ "$CHECKSUM" != "$EXPECTED_CHECKSUM" ]; then echo "Checksum verification failed!" exit 1 fi echo "Checksum verified!" echo "Terragrunt $VERSION downloaded and verified successfully" ``` Note These scripts automatically verify the SHA256 checksum and GPG signature before completing. ## Install via a package manager [Section titled “Install via a package manager”](#install-via-a-package-manager) Note that all the different package managers are third party. The third party Terragrunt packages may not be updated with the latest version, but are often close. Please check your version against the latest available on the [Releases Page](https://github.com/gruntwork-io/terragrunt/releases). If you want the latest version, the recommended installation option is to [download from the releases page](https://github.com/gruntwork-io/terragrunt/releases). * **Windows**: You can install Terragrunt on Windows using [Chocolatey](https://chocolatey.org/) ```bash choco install terragrunt ``` * **macOS**: You can install Terragrunt on macOS using [Homebrew](https://brew.sh/): ```bash brew install terragrunt ``` * **Linux (Homebrew)**: Most Linux users can use [Homebrew](https://docs.brew.sh/Homebrew-on-Linux): ```bash brew install terragrunt ``` * **Linux (Pacman)**: Arch Linux users can use [pacman](https://archlinux.org/packages/extra/x86_64/terragrunt/): ```bash pacman -S terragrunt ``` * **Linux (Gentoo)**: Gentoo users can use [emerge](https://repology.org/project/terragrunt/versions): ```bash emerge -a app-admin/terragrunt-bin ``` * **FreeBSD**: You can install Terragrunt on FreeBSD using [Pkg](https://www.freebsd.org/cgi/man.cgi?pkg\(7\)): ```bash pkg install terragrunt ``` ## Install via tool manager [Section titled “Install via tool manager”](#install-via-tool-manager) A best practice when using Terragrunt is to pin the version you are using to ensure that you, your colleagues and your CI/CD pipelines are all using the same version. This also allows you to easily upgrade to new versions and rollback to previous versions if needed. You can use a tool manager to install and manage Terragrunt versions. * **mise**: You can install Terragrunt using [mise](https://mise.jdx.dev) ```bash mise install terragrunt v1.0.7 ``` * **asdf**: You can install Terragrunt using [asdf](https://asdf-vm.com) ```bash asdf plugin add terragrunt asdf install terragrunt v1.0.7 ``` Both of these tools allow you to pin the version of Terragrunt you are using in a `.tool-versions` (and `.mise.toml` for mise) file in your project directory. Colleagues and CI/CD pipelines can then install the associated tool manager, and run using the pinned version. Note that the tools Terragrunt integrates with, such as OpenTofu and Terraform, can also be managed by these tool managers, so you can also pin the versions of those tools in the same file. **Backend details:** * **mise** uses [aqua](https://aquaproj.github.io/) as its default backend to install Terragrunt. * **asdf** uses the asdf-terragrunt plugin, which is maintained by Gruntwork: ## Building from source [Section titled “Building from source”](#building-from-source) If you’d like to build from source, you can use `go` to build Terragrunt yourself, and install it: ```shell git clone https://github.com/gruntwork-io/terragrunt.git cd terragrunt # Feel free to checkout a particular tag, etc if you want here. go install ``` ## Install tip or test builds [Section titled “Install tip or test builds”](#install-tip-or-test-builds) Every successful CI run on the `main` branch produces a **tip build** of Terragrunt. Maintainers can also trigger **test builds** from any branch. Both are useful for testing unreleased changes or reproducing issues against the most recent code. Tip builds are served via the builds API at `https://builds.terragrunt.com` and are available for all platforms supported in official releases. While tip builds are always built on every push to `main`, test builds are only built when maintainers explicitly want to distribute a test build of Terragrunt from a branch other than `main` to get stakeholder feedback. Maintainers may ask that you try out a test build to see if a WIP solution solves an issue you are facing, and you are free to request a test build if you want to try the latest in some in-progress work. No retention guarantee Tip and test builds may be reclaimed at any time to reduce hosting costs. Do not rely on a specific build being available indefinitely. Use [official releases](https://github.com/gruntwork-io/terragrunt/releases) for production. ### Quick install for tip and test builds [Section titled “Quick install for tip and test builds”](#quick-install-for-tip-and-test-builds) The same install script used for official releases supports tip and test builds via flags. To install the latest tip build: ```bash curl -sSfL --proto '=https' --tlsv1.2 https://terragrunt.com/install | bash -s -- --tip ``` To install a tip build at a specific commit: ```bash curl -sSfL --proto '=https' --tlsv1.2 https://terragrunt.com/install | bash -s -- --tip --commit ``` To install a test build at a specific commit: ```bash curl -sSfL --proto '=https' --tlsv1.2 https://terragrunt.com/install | bash -s -- --test --commit ``` If you prefer to run the install steps manually, the tabs below show the equivalent commands: * Latest tip ```bash set -euo pipefail # Set your platform (linux, darwin, windows) and architecture (amd64, arm64, 386) OS="linux" ARCH="amd64" ARCHIVE="terragrunt_${OS}_${ARCH}.tar.gz" # Download the archive and checksums curl -sL -o "$ARCHIVE" \ "https://builds.terragrunt.com/api/v1/tip/latest/download?os=${OS}&arch=${ARCH}" curl -sL -o SHA256SUMS \ "https://builds.terragrunt.com/api/v1/tip/latest/download?filename=SHA256SUMS" # Verify checksum EXPECTED=$(awk -v f="$ARCHIVE" '$2 == f {print $1; exit}' SHA256SUMS) ACTUAL=$(sha256sum "$ARCHIVE" | awk '{print $1}') # macOS: use "shasum -a 256" if [ "$EXPECTED" != "$ACTUAL" ]; then echo "Checksum verification failed!" exit 1 fi echo "Checksum verified!" # Extract and install tar -xzf "$ARCHIVE" chmod +x "terragrunt_${OS}_${ARCH}" sudo mv "terragrunt_${OS}_${ARCH}" /usr/local/bin/terragrunt ``` * Specific commit ```bash set -euo pipefail # Set the commit SHA, platform, and architecture COMMIT="abc123def456" OS="linux" ARCH="amd64" ARCHIVE="terragrunt_${OS}_${ARCH}.tar.gz" # Download the archive and checksums curl -sL -o "$ARCHIVE" \ "https://builds.terragrunt.com/api/v1/tip/${COMMIT}/download?os=${OS}&arch=${ARCH}" curl -sL -o SHA256SUMS \ "https://builds.terragrunt.com/api/v1/tip/${COMMIT}/download?filename=SHA256SUMS" # Verify checksum EXPECTED=$(awk -v f="$ARCHIVE" '$2 == f {print $1; exit}' SHA256SUMS) ACTUAL=$(sha256sum "$ARCHIVE" | awk '{print $1}') # macOS: use "shasum -a 256" if [ "$EXPECTED" != "$ACTUAL" ]; then echo "Checksum verification failed!" exit 1 fi echo "Checksum verified!" # Extract and install tar -xzf "$ARCHIVE" chmod +x "terragrunt_${OS}_${ARCH}" sudo mv "terragrunt_${OS}_${ARCH}" /usr/local/bin/terragrunt ``` * Test build ```bash set -euo pipefail # Set the commit SHA, platform, and architecture COMMIT="abc123def456" OS="linux" ARCH="amd64" ARCHIVE="terragrunt_${OS}_${ARCH}.tar.gz" BASE_URL="https://builds.terragrunt.com/test/${COMMIT}" # Download the archive and checksums curl -sL -o "$ARCHIVE" "${BASE_URL}/${ARCHIVE}" curl -sL -o SHA256SUMS "${BASE_URL}/SHA256SUMS" # Verify checksum EXPECTED=$(awk -v f="$ARCHIVE" '$2 == f {print $1; exit}' SHA256SUMS) ACTUAL=$(sha256sum "$ARCHIVE" | awk '{print $1}') # macOS: use "shasum -a 256" if [ "$EXPECTED" != "$ACTUAL" ]; then echo "Checksum verification failed!" exit 1 fi echo "Checksum verified!" # Extract and install tar -xzf "$ARCHIVE" chmod +x "terragrunt_${OS}_${ARCH}" sudo mv "terragrunt_${OS}_${ARCH}" /usr/local/bin/terragrunt ``` ### Verifying signatures [Section titled “Verifying signatures”](#verifying-signatures) Tip and test builds are signed using the same process as official releases. You can optionally verify the GPG or Cosign signature after downloading: ```bash # Download signature files (replace "latest" with a commit SHA if needed) curl -sL -o SHA256SUMS.gpgsig \ "https://builds.terragrunt.com/api/v1/tip/latest/download?filename=SHA256SUMS.gpgsig" # GPG verification curl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import gpg --verify SHA256SUMS.gpgsig SHA256SUMS ``` Alternatively, verify with Cosign: ```bash curl -sL -o SHA256SUMS.sigstore.json \ "https://builds.terragrunt.com/api/v1/tip/latest/download?filename=SHA256SUMS.sigstore.json" cosign verify-blob SHA256SUMS \ --bundle SHA256SUMS.sigstore.json \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp "github.com/gruntwork-io/terragrunt" ``` Note For full API documentation and more details about Terragrunt is distributed, see the [Releases documentation](/process/releases#tip-builds). ## Enable tab completion [Section titled “Enable tab completion”](#enable-tab-completion) If you use either Bash or Zsh, you can enable tab completion for Terragrunt commands. To enable autocomplete, first ensure that a config file exists for your chosen shell. For Bash shell. ```shell touch ~/.bashrc ``` For Zsh shell. ```shell touch ~/.zshrc ``` Then install the autocomplete package. ```shell terragrunt --install-autocomplete ``` Once the autocomplete support is installed, you will need to restart your shell. ## Gruntwork Pipelines [Section titled “Gruntwork Pipelines”](#gruntwork-pipelines) Gruntwork offers a commercial CI/CD solution for Terragrunt called [Pipelines](https://www.gruntwork.io/platform/pipelines). Pipelines is a fully managed CI/CD service that is designed to work seamlessly with Terragrunt. It provides an out of the box solution for running Terragrunt in CI/CD without the need to setup and maintain your own CI/CD infrastructure. ## Terragrunt GitHub Action [Section titled “Terragrunt GitHub Action”](#terragrunt-github-action) Terragrunt is also available as a GitHub Action. Instructions on how to use it can be found at . # Overview > Get a high level overview of the most important Terragrunt features. The following is a simple overview of the main features in Terragrunt. It includes configurations that are a bit more complex than the ones found in the [Quick Start](/getting-started/quick-start/), but don’t panic! We’ll walk you through each one, and you don’t need to understand everything right away. Knowing that these features are available as you start to use Terragrunt can give you a tool to reach for when you encounter common problems that typically require one or more of these solutions. This guide is geared towards users who have either already gone through the [Quick Start](/getting-started/quick-start/) or are joining a team of users that are already using Terragrunt. As a consequence, we’ll be using more complex configurations, discussing more advanced features, and showing how to use Terragrunt to manage real AWS infrastructure. If you are unfamiliar with OpenTofu/Terraform, you may want to also read [OpenTofu](https://opentofu.org/docs/intro/) or [Terraform](https://developer.hashicorp.com/terraform/intro) documentation after reading this guide. ## Following Along [Section titled “Following Along”](#following-along) What follows isn’t a tutorial in the same sense as the [Quick Start](/getting-started/quick-start/), but more of a guided tour of some of the more commonly used features of Terragrunt. You don’t need to follow along to understand the concepts, but if you want to, you can. The code samples provided here are available as individual “steps” [here](https://github.com/gruntwork-io/terragrunt/tree/main/test/fixtures/docs/02-overview). If you would prefer it, you can clone the [Terragrunt repository](https://github.com/gruntwork-io/terragrunt.git), and follow along with the examples in your own environment without any copy + paste. Just make sure to replace the values prefixed `__FILL_IN_` with values relevant to your AWS account. If you don’t have an AWS account, you can either sign up for a free tier account at [aws.amazon.com](https://aws.amazon.com/) or adapt the examples to use a different cloud provider. ## Example [Section titled “Example”](#example) Here is a typical `terragrunt.hcl` file you might find in a Terragrunt project\*: terragrunt.hcl ```hcl # Configure the remote backend remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { bucket = "my-tofu-state" key = "tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } # Configure the AWS provider generate "provider" { path = "provider.tf" if_exists = "overwrite_terragrunt" contents = < Start using Terragrunt today! ## Install Terragrunt [Section titled “Install Terragrunt”](#install-terragrunt) If you haven’t already installed Terragrunt, you can do so by following the instructions in the [Install Terragrunt](/getting-started/install/) guide. ## Add `terragrunt.hcl` to your project [Section titled “Add terragrunt.hcl to your project”](#add-terragrunthcl-to-your-project) If you are currently using OpenTofu or Terraform, and you want to start using Terragrunt in your project, simply run the following where your OpenTofu project is located: ```shell touch terragrunt.hcl ``` This creates an empty Terragrunt configuration file in the directory where you are using OpenTofu. You can now start using `terragrunt` instead of `tofu` or `terraform` to run your OpenTofu/Terraform commands as if you were simply using OpenTofu or Terraform. Depending on why you’re looking to adopt Terragrunt, this may be all you need to do! With just this empty file, you’ve already made it so that you no longer need to run `tofu init` or `terraform init` before running `tofu apply` or `terraform apply`. Terragrunt will automatically run `init` for you if necessary. This is a feature called [Auto-init](/features/units/auto-init/). This might not be very impressive so far, so you may be wondering *why* one might want to start using Terragrunt to manage their OpenTofu/Terraform projects. The next section will give you a very gentle introduction to using Terragrunt, and show you how you can start to leverage Terragrunt to manage your OpenTofu/Terraform projects more effectively. ## Tutorial [Section titled “Tutorial”](#tutorial) What follows is a gentle step-by-step guide to integrating Terragrunt into a new (or existing) OpenTofu/Terraform project. For the sake of this tutorial, a minimal set of OpenTofu configurations will be used so that you can follow along. Following these steps will give you an idea of how to integrate Terragrunt into an existing project, even if yours is more complex. This tutorial will assume the following: 1. You have [OpenTofu](https://opentofu.org/docs/intro/install/) or [Terraform](https://developer.hashicorp.com/terraform/install) installed\*. 2. You have a basic understanding of what OpenTofu/Terraform do. 3. You are using a Unix-like operating system. This tutorial will not assume the following: 1. You have any subscriptions to any cloud providers. 2. You have any experience with Terragrunt. 3. You have any existing Terragrunt, OpenTofu or Terraform projects. \* Note that if you have *both* OpenTofu and Terraform installed, you’ll want to read the [tf-path](/reference/cli/commands/run#tf-path) docs to understand how Terragrunt determines which binary to use. If you would like a less gentle introduction geared towards users with an active AWS account, familiarity with OpenTofu/Terraform, and potentially a team actively using Terragrunt, consider starting with the [Overview](/getting-started/overview/). If you start to feel lost, or don’t understand a concept, consider reading the [Terminology](/getting-started/terminology/) page before continuing with this tutorial. It has a brief overview of most of the common terms used when discussing Terragrunt. Finally, note that all of the files created in this tutorial can be copied directly from the code block, none of them are partial files, so you don’t have to worry about figuring out where to put the code. Just copy and paste! You can also see what to expect in your filesystem at each step [here](https://github.com/gruntwork-io/terragrunt/tree/main/test/fixtures/docs/01-quick-start). 1. **Create a new Terragrunt project** Let’s say you have the following `main.tf` in directory `foo`: foo/main.tf ```hcl resource "local_file" "file" { content = "Hello, World!" filename = "${path.module}/hi.txt" } ``` As we learned above, integrating this OpenTofu project with Terragrunt is as simple as creating a `terragrunt.hcl` file in the same directory: ```bash touch foo/terragrunt.hcl ``` You can now run `terragrunt` commands within the `foo` directory, as if you were using `tofu` or `terraform`. ```bash $ cd foo $ terragrunt apply -auto-approve 18:44:26.066 STDOUT tofu: Initializing the backend... 18:44:26.067 STDOUT tofu: Initializing provider plugins... 18:44:26.067 STDOUT tofu: - Finding latest version of hashicorp/local... 18:44:26.717 STDOUT tofu: - Installing hashicorp/local v2.5.2... 18:44:27.033 STDOUT tofu: - Installed hashicorp/local v2.5.2 (signed, key ID 0C0AF313E5FD9F80) 18:44:27.033 STDOUT tofu: Providers are signed by their developers. 18:44:27.033 STDOUT tofu: If you'd like to know more about provider signing, you can read about it here: 18:44:27.033 STDOUT tofu: https://opentofu.org/docs/cli/plugins/signing/ 18:44:27.034 STDOUT tofu: OpenTofu has created a lock file .terraform.lock.hcl to record the provider 18:44:27.034 STDOUT tofu: selections it made above. Include this file in your version control repository 18:44:27.034 STDOUT tofu: so that OpenTofu can guarantee to make the same selections by default when 18:44:27.034 STDOUT tofu: you run "tofu init" in the future. 18:44:27.034 STDOUT tofu: OpenTofu has been successfully initialized! 18:44:27.035 STDOUT tofu: 18:44:27.035 STDOUT tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see 18:44:27.035 STDOUT tofu: any changes that are required for your infrastructure. All OpenTofu commands 18:44:27.035 STDOUT tofu: should now work. 18:44:27.035 STDOUT tofu: If you ever set or change modules or backend configuration for OpenTofu, 18:44:27.035 STDOUT tofu: rerun this command to reinitialize your working directory. If you forget, other 18:44:27.035 STDOUT tofu: commands will detect it and remind you to do so if necessary. 18:44:27.362 STDOUT tofu: OpenTofu used the selected providers to generate the following execution 18:44:27.362 STDOUT tofu: plan. Resource actions are indicated with the following symbols: 18:44:27.362 STDOUT tofu: + create 18:44:27.362 STDOUT tofu: OpenTofu will perform the following actions: 18:44:27.362 STDOUT tofu: # local_file.file will be created 18:44:27.362 STDOUT tofu: + resource "local_file" "file" { 18:44:27.362 STDOUT tofu: + content = "Hello, World!" 18:44:27.362 STDOUT tofu: + content_base64sha256 = (known after apply) 18:44:27.362 STDOUT tofu: + content_base64sha512 = (known after apply) 18:44:27.362 STDOUT tofu: + content_md5 = (known after apply) 18:44:27.362 STDOUT tofu: + content_sha1 = (known after apply) 18:44:27.362 STDOUT tofu: + content_sha256 = (known after apply) 18:44:27.362 STDOUT tofu: + content_sha512 = (known after apply) 18:44:27.362 STDOUT tofu: + directory_permission = "0777" 18:44:27.362 STDOUT tofu: + file_permission = "0777" 18:44:27.362 STDOUT tofu: + filename = "./hi.txt" 18:44:27.362 STDOUT tofu: + id = (known after apply) 18:44:27.362 STDOUT tofu: } 18:44:27.362 STDOUT tofu: Plan: 1 to add, 0 to change, 0 to destroy. 18:44:27.362 STDOUT tofu: 18:44:27.383 STDOUT tofu: local_file.file: Creating... 18:44:27.384 STDOUT tofu: local_file.file: Creation complete after 0s [id=0a0a9f2a6772942557ab5355d76af442f8f65e01] 18:44:27.392 STDOUT tofu: 18:44:27.392 STDOUT tofu: Apply complete! Resources: 1 added, 0 changed, 0 destroyed. 18:44:27.392 STDOUT tofu: ``` You might notice that this is a little more verbose than the output you’re used to seeing from running `tofu` or `terraform` directly. This is because Terragrunt does a bit of work behind the scenes to make sure that you can scale your OpenTofu/Terraform usage without running into common problems. As you get more comfortable with using Terragrunt on larger projects, you may find the extra information helpful. If you prefer that Terragrunt terminal output look more like that from `tofu` or `terraform`, you can use the `--log-format bare` flag (or set the environment variable `TG_LOG_FORMAT=bare`) to reduce the verbosity of the output. e.g. ```bash $ terragrunt --log-format bare apply local_file.file: Refreshing state... [id=0a0a9f2a6772942557ab5355d76af442f8f65e01] No changes. Your infrastructure matches the configuration. OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed. Apply complete! Resources: 0 added, 0 changed, 0 destroyed. ``` The way dynamicity is handled in OpenTofu is via `variable` configuration blocks. Let’s add one to our `main.tf` so that we can control the content of the file we’re creating: foo/main.tf ```hcl variable "content" {} resource "local_file" "file" { content = var.content filename = "${path.module}/hi.txt" } ``` Now, just like when using `tofu` alone, you can pass in the value for the `content` variable using the `-var` flag: ```bash terragrunt apply -auto-approve -var content='Hello, Terragrunt!' ``` This is a common pattern when working with Infrastructure as Code (IaC). You typically create IaC that is relatively static, and then as you need to make configurations dynamic, you add variables to your configuration files to introduce dynamicity. 2. **Add a new Terragrunt unit** In the context of Terragrunt, a “unit” is a directory that contains a `terragrunt.hcl` file, and it represents a single piece of infrastructure. You can think of a unit as a single instance of an OpenTofu/Terraform module. Let’s create a copy of the `foo` directory and call it `bar`: ```bash cd .. cp -r foo bar ``` We now have two identical units in our project, `foo` and `bar`. We also have identical code in each of these directories, which is not ideal if we want to be able to avoid duplicating effort when we make changes to our infrastructure. 3. **Create a shared module** To avoid this duplication, we can introduce a new `shared` directory, and reference that directory from both `foo` and `bar`. This way, we can make changes to our infrastructure in one place and have those changes apply to both units. Let’s create a new directory called `shared`: ```bash mkdir shared ``` Now, copy the `main.tf` file from `foo` to `shared`: ```bash cp foo/main.tf shared/main.tf ``` Finally, let’s update the `foo` and `bar` main.tf files to reference the `shared` directory. Update the `main.tf` files in both `foo` and `bar` to look like this: foo/main.tf ```hcl variable "content" {} module "shared" { source = "../shared" content = var.content } ``` bar/main.tf ```hcl variable "content" {} module "shared" { source = "../shared" content = var.content } ``` There’s now one place where the logic for the resource `local_file.file` is defined, and both `foo` and `bar` reference that logic. You can imagine that as your infrastructure grows, it can become more and more advantageous to put repeated logic into shared modules like this. This setup does have some problems, however. While you could keep navigating to the different units and running `terragrunt apply` in each one with the appropriate `-var` flags, this can quickly become tedious, as you have to know which units require which set of vars applied. You might decide to work around this by creating a file named `terraform.tfvars` in each unit directory, but this also comes with some limitations that Terragrunt can help you avoid. 4. **Use Terragrunt to manage your units** Luckily, Terragrunt has a built-in feature to control the inputs passed to your OpenTofu/Terraform configurations. This feature is called (aptly enough) [inputs](/reference/hcl/attributes/#inputs). Let’s add inputs to both `terragrunt.hcl` files in the `foo` and `bar` directories: foo/terragrunt.hcl ```hcl inputs = { content = "Hello from foo, Terragrunt!" } ``` bar/terragrunt.hcl ```hcl inputs = { content = "Hello from bar, Terragrunt!" } ``` You don’t have to maintain the extra `main.tf` files just to instantiate the `module` blocks. You can use the `terraform` block to handle this for you. Update the `terragrunt.hcl` files in `foo` and `bar` to look like this: foo/terragrunt.hcl ```hcl terraform { source = "../shared" } inputs = { content = "Hello from foo, Terragrunt!" } ``` bar/terragrunt.hcl ```hcl terraform { source = "../shared" } inputs = { content = "Hello from bar, Terragrunt!" } ``` And you can delete the `main.tf` files from both `foo` and `bar`: ```bash rm foo/main.tf bar/main.tf ``` This saves you some duplicated content, as you no longer need to maintain that extra `content` variable in each `main.tf` file. You can imagine that for especially large modules, the ability to define inputs in the `terragrunt.hcl` file can save you a lot of time and effort. The patterns for your infrastructure are exclusively defined in `.tf` files now, and the `terragrunt.hcl` files are used to manage the instances of those patterns as units. If you run `terragrunt apply -auto-approve` in the `foo` and `bar` directories, you’ll see that the `content` variable is set to the value you defined in the `inputs` block of the `terragrunt.hcl` file. You might also notice that there’s now a special `.terragrunt-cache` directory generated for you in each unit directory. This is where Terragrunt copies the contents of modules, and performs any necessary additional code generation to make sure that your OpenTofu/Terraform code is ready to be run. The `.terragrunt-cache` directory is typically added to `.gitignore` files, similar to the `.terraform` directory that OpenTofu generates. 5. **Use Terragrunt to manage your stacks** In the context of Terragrunt, a “stack” is a collection of units that are managed together. You can think of a stack as a single environment, such as `dev`, `staging`, or `prod`, or an entire project. One of the main reasons users adopt Terragrunt is that it can help manage the complexity of managing multiple units across multiple environments. e.g. Let’s say we wanted to update both our `foo` and `bar` environments simultaneously. In the directory above `foo` and `bar`, run the following: ```bash $ terragrunt run --all apply 08:42:00.150 INFO The stack at . will be processed in the following order for command apply: Group 1 - Module ./bar - Module ./foo Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) y 08:43:10.702 STDOUT [foo] tofu: local_file.file: Refreshing state... [id=c4ae21736a6297f44ea86791e528338e9d14a0e9] 08:43:10.702 STDOUT [bar] tofu: local_file.file: Refreshing state... [id=f855394a0316da09618c8b1fde7b91e00e759f80] 08:43:10.708 STDOUT [bar] tofu: No changes. Your infrastructure matches the configuration. 08:43:10.708 STDOUT [bar] tofu: OpenTofu has compared your real infrastructure against your configuration and 08:43:10.708 STDOUT [bar] tofu: found no differences, so no changes are needed. 08:43:10.708 STDOUT [foo] tofu: No changes. Your infrastructure matches the configuration. 08:43:10.708 STDOUT [foo] tofu: OpenTofu has compared your real infrastructure against your configuration and 08:43:10.708 STDOUT [foo] tofu: found no differences, so no changes are needed. 08:43:10.716 STDOUT [foo] tofu: 08:43:10.716 STDOUT [foo] tofu: Apply complete! Resources: 0 added, 0 changed, 0 destroyed. 08:43:10.716 STDOUT [foo] tofu: 08:43:10.720 STDOUT [bar] tofu: 08:43:10.720 STDOUT [bar] tofu: Apply complete! Resources: 0 added, 0 changed, 0 destroyed. 08:43:10.720 STDOUT [bar] tofu: ``` This is where that additional verbosity in Terragrunt logging is really handy. You can see that Terragrunt concurrently ran `apply -auto-approve` in both the `foo` and `bar` units. The extra logging for Terragrunt also included information on the names of the units that were processed, and disambiguated the output from each unit. When Terragrunt runs these commands, it creates a `.terragrunt-cache` directory in each unit’s directory. This cache directory serves as Terragrunt’s scratch directory where it: * Downloads your remote OpenTofu/Terraform configurations * Runs your OpenTofu/Terraform commands * Stores downloaded modules and providers * Stores generated files (in this case, the `hi.txt` file will be created under `.terragrunt-cache/[HASH]/[HASH]/hi.txt` rather than directly in the `foo` or `bar` directories) The `.terragrunt-cache` directory is typically added to `.gitignore` files, similar to the `.terraform` directory that OpenTofu generates. You can safely delete this folder at any time, and Terragrunt will recreate it as necessary. If you want to control where the files are created, you can modify the module to accept an output directory parameter. For example, you can update the `shared/main.tf` file to: ```hcl variable "content" {} variable "output_dir" {} resource "local_file" "file" { content = var.content filename = "${var.output_dir}/hi.txt" } ``` Then in your `foo/terragrunt.hcl` and `bar/terragrunt.hcl` files, you can use the `get_terragrunt_dir()` built-in function to get the directory where the `terragrunt.hcl` file is located: ```hcl terraform { source = "../shared" } inputs = { output_dir = get_terragrunt_dir() content = "Hello from bar, Terragrunt!" } ``` With this configuration, the `hi.txt` files will be created directly in the `foo` and `bar` directories instead of the `.terragrunt-cache` directory. Similar to the `tofu` CLI, there is a prompt to confirm that you are sure you want to run the command in each unit when performing a command that’s potentially destructive. You can skip this prompt by using the `--non-interactive` flag, just as you would with `-auto-approve` in OpenTofu. ```bash terragrunt run --all --non-interactive apply ``` 6. **Use Terragrunt to manage your DAG** In the context of Terragrunt, a Directed Acyclic Graph (DAG) is a data structure that represents the relationship between units in your stack, as determined by their dependencies. Don’t worry if that doesn’t make sense right now. The important thing to know is that Terragrunt uses the DAG to determine the order in which it performs runs across your stack. Once you see how Terragrunt uses the DAG to determine the order in which to run commands across your stack, you’ll understand why this is important. For example, let’s say that the `content` of the `bar` unit depended on the `content` of the `foo` unit. You can express this dependency first by adding an `output` block to the `shared` module: shared/output.tf ```hcl output "content" { value = local_file.file.content } ``` Then, you can update the `bar` unit to depend on the `foo` unit by using the `dependencies` block in the `terragrunt.hcl` file: bar/terragrunt.hcl ```hcl terraform { source = "../shared" } dependency "foo" { config_path = "../foo" } inputs = { output_dir = get_terragrunt_dir() content = "Foo content: ${dependency.foo.outputs.content}" } ``` Being good citizens of the IaC world, we should run a `plan` before an `apply` to see what changes Terragrunt will make to our infrastructure (note that you will get an error here. This is expected, and we’ll fix it in the next step): ```bash $ terragrunt run --all plan 08:57:09.271 INFO The stack at . will be processed in the following order for command plan: Group 1 - Module ./foo Group 2 - Module ./bar ... 08:57:09.936 ERROR [bar] Module ./bar has finished with an error 08:57:09.936 ERROR error occurred: * ./foo/terragrunt.hcl is a dependency of ./bar/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs. If this dependency is accessed before the outputs are ready (which can happen during the planning phase of an unapplied stack), consider using mock_outputs: dependency "foo" { config_path = "../foo" mock_outputs = { foo_output = "mock-foo-output" } } For more info, see: https://docs.terragrunt.com/features/stacks/#unapplied-dependency-and-mock-outputs If you do not require outputs from your dependency, consider using the dependencies block instead: https://docs.terragrunt.com/reference/config-blocks-and-attributes/#dependencies ``` Oh no! We got an error. This happens because the way in which dependencies are resolved by default in Terragrunt is to run `terragrunt output` within the dependency for use in the dependent unit. In this case, the `foo` unit has not been applied yet, so there are no outputs to fetch. You should notice, however, that Terragrunt has already figured out the order in which to run the `plan` command across the units in your stack. This is what we mean when we say that Terragrunt uses a DAG to determine the order of runs in your stack. Terragrunt analyzes the dependencies in your stack, and determines an order for runs so that outputs are ready to be used as inputs in dependent units. If you decided to run `terragrunt run --all apply` instead, you would instead see Terragrunt complete the `apply` in the `foo` unit first, and then complete the `apply` in the `bar` unit, as it’s aware that the `bar` unit might need some outputs from the `foo` unit. 7. **Use mocks to handle unavailable outputs** In this scenario, most Terragrunt users leverage `mock_outputs` to handle unavailable outputs (see [limitations on accessing exposed config](https://docs.terragrunt.com/reference/config-blocks-and-attributes/#limitations-on-accessing-exposed-config)). Given that it’s expected that the `foo` unit won’t be able to provide outputs until it’s applied, you can use the `mock_outputs` block to provide a placeholder value for the `content` output during the `plan` phase. bar/terragrunt.hcl ```hcl terraform { source = "../shared" } dependency "foo" { config_path = "../foo" mock_outputs = { content = "Mocked content from foo" } } inputs = { output_dir = get_terragrunt_dir() content = "Foo content: ${dependency.foo.outputs.content}" } ``` Re-running the `plan` command should now complete successfully: ```bash $ terragrunt run --all plan 09:29:03.461 INFO The stack at . will be processed in the following order for command plan: Group 1 - Module ./foo Group 2 - Module ./bar ... 09:29:03.644 WARN [bar] Config ./foo/terragrunt.hcl is a dependency of ./bar/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output. ... 09:29:03.898 STDOUT [bar] tofu: + resource "local_file" "file" { 09:29:03.898 STDOUT [bar] tofu: + content = "Foo content: Mocked content from foo" 09:29:03.898 STDOUT [bar] tofu: + content_base64sha256 = (known after apply) 09:29:03.898 STDOUT [bar] tofu: + content_base64sha512 = (known after apply) 09:29:03.898 STDOUT [bar] tofu: + content_md5 = (known after apply) 09:29:03.898 STDOUT [bar] tofu: + content_sha1 = (known after apply) 09:29:03.898 STDOUT [bar] tofu: + content_sha256 = (known after apply) 09:29:03.898 STDOUT [bar] tofu: + content_sha512 = (known after apply) 09:29:03.898 STDOUT [bar] tofu: + directory_permission = "0777" 09:29:03.898 STDOUT [bar] tofu: + file_permission = "0777" 09:29:03.898 STDOUT [bar] tofu: + filename = "./hi.txt" 09:29:03.898 STDOUT [bar] tofu: + id = (known after apply) 09:29:03.898 STDOUT [bar] tofu: } ``` If you’re concerned about the `mock_outputs` attribute resulting in invalid configurations, note that during an apply, the outputs of `foo` will be known, and Terragrunt won’t use `mock_outputs` to resolve the outputs of `foo`. ```bash $ terragrunt run --all --non-interactive apply ... 09:31:21.587 STDOUT [bar] tofu: + resource "local_file" "file" { 09:31:21.587 STDOUT [bar] tofu: + content = "Foo content: Hello from foo, Terragrunt!" 09:31:21.587 STDOUT [bar] tofu: + content_base64sha256 = (known after apply) 09:31:21.587 STDOUT [bar] tofu: + content_base64sha512 = (known after apply) 09:31:21.587 STDOUT [bar] tofu: + content_md5 = (known after apply) 09:31:21.587 STDOUT [bar] tofu: + content_sha1 = (known after apply) 09:31:21.587 STDOUT [bar] tofu: + content_sha256 = (known after apply) 09:31:21.587 STDOUT [bar] tofu: + content_sha512 = (known after apply) 09:31:21.587 STDOUT [bar] tofu: + directory_permission = "0777" 09:31:21.587 STDOUT [bar] tofu: + file_permission = "0777" 09:31:21.587 STDOUT [bar] tofu: + filename = "./hi.txt" 09:31:21.587 STDOUT [bar] tofu: + id = (known after apply) 09:31:21.587 STDOUT [bar] tofu: } ... ``` You can also be explicit about the fact that you only want to use `mock_outputs` during the `plan` phase by specifying that in your `dependency` configuration: bar/terragrunt.hcl ```hcl terraform { source = "../shared" } dependency "foo" { config_path = "../foo" mock_outputs = { content = "Mocked content from foo" } mock_outputs_allowed_terraform_commands = ["plan"] } inputs = { output_dir = get_terragrunt_dir() content = "Foo content: ${dependency.foo.outputs.content}" } ``` Something a little subtle just happened there. Note that the `inputs` attribute is dynamic. This addresses some of the limitations mentioned earlier about using `terraform.tfvars` files to manage inputs for units. Given that the `bar` unit is dependent on output values from the `foo` unit, you wouldn’t be able to use a `terraform.tfvars` file to populate this variable without some additional tooling to populate it dynamically. Terragrunt was spawned organically out of supporting Gruntwork customers using Terraform at scale, and features in the product are designed to address common problems like these that arise when managing OpenTofu/Terraform projects at scale in production. 8. **Continue learning and exploring** Hopefully, following this simple tutorial has given you confidence in integrating Terragrunt into your existing OpenTofu/Terraform projects. Starting small, and gradually introducing more complex Terragrunt features is a great way to learn how Terragrunt can help you manage your infrastructure more effectively. The next step of the Getting Started guide is to follow the [Overview](/getting-started/overview/) guide. This guide will introduce you to more advanced Terragrunt features, and show you how to use Terragrunt to manage your infrastructure across multiple environments in a real-world AWS account. If you’re ready to get your hands dirty with more advanced Terragrunt features yourself, you can skip ahead to the [Features](/features/units) section of the documentation. If you ever need help with a particular problem, take a look at the resources available to you in the [Support](/community/support/) section. You are especially encouraged to join the [Terragrunt Discord](/community/invite) server, and become part of the Terragrunt community. # Terminology > Quickly understand commonly use terms in Terragrunt. Infrastructure as Code (IaC) tooling necessarily requires a lot of terminology to describe various concepts and features due to the breadth of the domain. Whenever possible, Terragrunt terminology attempts to align with wider industry standards, but there are always exceptions. There are going to be times when certain terms are used in different tools, but have special meaning in Terragrunt, and there are times when the same term might have different meaning in different contexts. This document aims to provide a quick reference for the most important and commonly used terms in Terragrunt and generally in Gruntwork products. Whenever terminology used in Terragrunt deviates from this document, it should either be explained or adjusted to align with this document. ## Terms [Section titled “Terms”](#terms) *** ### Terragrunt [Section titled “Terragrunt”](#terragrunt) Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in [OpenTofu](https://opentofu.org/)/[Terraform](https://www.terraform.io/) to scale. It differs from many other IaC tools in that it is designed to be an orchestrator for OpenTofu/Terraform execution, rather than primarily provisioning infrastructure itself. Terragrunt users write OpenTofu/Terraform code to define high-level patterns of infrastructure that they want to create, then use Terragrunt to dynamically apply those generic patterns in particular ways. Because of this separation of concerns, most of what Terragrunt does is designed to extend the capabilities of OpenTofu/Terraform, rather than replace them. Most of the [features](/features/units) of Terragrunt are designed to make it easier to manage large infrastructure estates, or to provide additional capabilities that are inconvenient or impossible to achieve with OpenTofu/Terraform alone. ### OpenTofu [Section titled “OpenTofu”](#opentofu) [OpenTofu](https://opentofu.org/) is an open-source Infrastructure as Code tool spawned as a fork of [Terraform](https://www.terraform.io/) after the license change from the [Mozilla Public License (MPL)](https://en.wikipedia.org/wiki/Mozilla_Public_License) to the [Business Source License (BSL)](https://en.wikipedia.org/wiki/Business_Source_License). OpenTofu was created as a drop-in replacement for Terraform (as it was forked from the same MPL source code), and is designed to be fully compatible with Terraform configurations and modules. You may notice that Terragrunt documentation uses the phrase “OpenTofu/Terraform” to refer to the IaC tooling that Terragrunt orchestrates. This is because Terragrunt is generally agnostic to the specific IaC tooling that is being used to drive infrastructure updates. When relevant, Terragrunt documentation endeavors to explicitly indicate that functionality is specific to one tool or the other. From the perspective of Terragrunt, the two are usually interchangeable, though Terragrunt will default to using OpenTofu if both are available. Note that some documentation refers to Terraform alone in some instances as a consequence of the historical context in which Terragrunt was created, as it predates the creation of OpenTofu. Conversely, some documentation may refer to OpenTofu alone simply because of the fact that OpenTofu is the default IaC tool that Terragrunt uses. ### Unit [Section titled “Unit”](#unit) A unit is a single instance of infrastructure managed by Terragrunt. It has its own state, and can be detected by the presence of a `terragrunt.hcl` file in a directory. Units typically represent a minimal useful piece of infrastructure that should be independently managed. e.g. A unit might represent a single VPC, a single database, or a single server. While not a requirement, a general tendency experienced when working with Terragrunt is that units tend to decrease in size. This is because Terragrunt makes it easy to segment pieces of infrastructure into their own state, and to have them interact with each other through the use of [dependency blocks](/reference/hcl/blocks#dependency). Smaller units are quicker to update, easier to reason about and safer to work with. A common pattern used in the repository structure for Terragrunt projects is to have a single `root.hcl` file located at the root of the repository, and multiple subdirectories each containing their own `terragrunt.hcl` file. This is typically done to promote code-reuse, as it allows for any configuration common to all units to be defined in the `root.hcl` file, and for unit-specific configuration to be defined in child directories. In this pattern, the `root.hcl` file is not considered a unit, while all the child directories containing `terragrunt.hcl` files are. Note that units don’t technically need to call their configuration files `terragrunt.hcl` (that’s configurable via the [—config](/reference/cli/commands/run#config)), and users don’t technically need to use `root.hcl` as the root configuration file or to name it that. This is the most common pattern followed by the community, however, and deviation from this pattern should be justified in the context of the project. It can help others with Terragrunt experience understand the project more easily if industry standard patterns are followed. ### Stack [Section titled “Stack”](#stack) A stack is a collection of units managed by Terragrunt. There is ([as of writing](https://github.com/gruntwork-io/terragrunt/issues/3313)) work underway to provide a top level artifact for interacting with stacks via a `terragrunt.stack.hcl` file, but as of now, stacks are generally defined by a directory with a tree of units. Units within a stack can be dependent on each other, and can be updated in a specific order to ensure that dependencies are resolved in the correct order. Stacks typically represent a collection of units that need to be managed in concert. e.g. A stack might represent a collection of units that together form a single application environment, a business unit, or a region. The design of `terragrunt.stack.hcl` files is to ensure that they function entirely as a convenient shorthand for an equivalent directory structure of units. This is to ensure that users are able to easily transition between the two paradigms, and are able to decide for themselves which approach to structuring infrastructure is most appropriate for their use case. ### Component [Section titled “Component”](#component) Component is a generic term to refer to something that is either a unit or a stack. Certain Terragrunt commands operate on components (e.g. [`find`](/reference/cli/commands/find) and [`list`](/reference/cli/commands/list)) while others operate on particular types of components (e.g. [`run`](/reference/cli/commands/run) only runs units whereas [`stack generate`](/reference/cli/commands/stack/generate) and [`stack output`](/reference/cli/commands/stack/output) commands run on stacks). ### Module [Section titled “Module”](#module) A module is an [OpenTofu/Terraform construct](https://opentofu.org/docs/language/modules/) defined using a collection of OpenTofu/Terraform configurations ending in `.tf` (or `.tofu` in the case of OpenTofu) that represent a general pattern of infrastructure that can be instantiated multiple times. Modules typically represent a generic pattern of infrastructure that can be instantiated multiple times, with different configurations exposed as variables. e.g. A module might represent a generic pattern for a VPC, a database, or a server. Note that this differs from a unit, which represents a single instance of a provisioned VPC, database, or server. Modules can be located either in the local filesystem, in a remote repository, or in any of [these supported locations](https://opentofu.org/docs/language/modules/sources/). To integrate a module into a Terragrunt unit, reference the module using the `source` attribute of the [terraform block](/reference/hcl/blocks#terraform). Terragrunt users typically spend a good deal of time authoring modules, as they are the primary way of defining the infrastructure patterns that Terragrunt is going to be orchestrating. Using tooling like [Terratest](https://github.com/gruntwork-io/terratest) can help to ensure that modules are well-tested and reliable. A common pattern in Terragrunt usage is to only ever provision versioned, immutable modules. This is because Terragrunt is designed to be able to manage infrastructure over long periods of time, and it is important to be able to reproduce the state of infrastructure at any point in time. ### Resource [Section titled “Resource”](#resource) A resource is a low level building block of infrastructure that is defined in OpenTofu/Terraform configurations. Resources are typically defined in modules, but don’t have to be. Terragrunt can provision resources defined with `.tf` files that are not part of a module, located adjacent to the `terragrunt.hcl` file of a unit. e.g. A resource might represent a single S3 bucket, or a single load balancer. Resources generally correspond to the smallest piece of infrastructure that can be managed by OpenTofu/Terraform, and each resource has a specific address in state. ### State [Section titled “State”](#state) Terragrunt stores the current state of infrastructure in one or more OpenTofu/Terraform [state files](https://opentofu.org/docs/language/state/). State is an extremely important concept in the context of OpenTofu/Terraform, and it’s helpful to read the relevant documentation there to understand what Terragrunt does to it. Terragrunt has myriad capabilities that are designed to make working with state easier, including tooling to bootstrap state backend resources on demand, managing unit interaction with external state, and segmenting state. The most common way in which state is segmented in Terragrunt projects is to take advantage of filesystem directory structures. Most Terragrunt projects are configured to store state in remote backends like S3 with keys that correspond to the relative path to the unit directory within a project, relative to the root `terragrunt.hcl` file. ### Directed Acyclic Graph (DAG) [Section titled “Directed Acyclic Graph (DAG)”](#directed-acyclic-graph-dag) The way in which units are resolved within a stack is via a [Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph#:~:text=A%20directed%20acyclic%20graph%20is,a%20path%20with%20zero%20edges). This graph is also used to determine the order in which resources are resolved within a unit. Dependencies in a DAG determine the order in which resources are created, updated, or destroyed. For creations and updates, resources are updated such that dependencies are always resolved before their dependents. For destructions, resources are destroyed such that dependents are always destroyed before their dependencies. This is still true even when working with multiple units in a stack. Terragrunt will resolve the dependencies of all units in a stack (resolving the DAG within each unit first), and then apply the changes to all units in the stack in the correct order. Note that DAGs are *Acyclic*, meaning that there are no loops in the graph. This is because loops would create circular dependencies, which would make it impossible to determine the correct order to resolve resources. ### Don’t Repeat Yourself (DRY) [Section titled “Don’t Repeat Yourself (DRY)”](#dont-repeat-yourself-dry) The [Don’t Repeat Yourself (DRY)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) principle is a software development principle that states that duplication in code should be avoided. Early on, a lot of Terragrunt functionality was designed to make it easier to follow the DRY principle. This was because Terraform users at the time found that they were often repeating the same, or very similar code across multiple configurations. Examples of this included the limitation that remote state and provider configurations needed to be repeated in every root module, and that there were limitations in the dynamicity of these configurations. Over time, Terragrunt has evolved to provide more features that make it easier to manage infrastructure at scale, and the focus has shifted to offering more tooling for *orchestrating* infrastructure, rather than simply making it easier to avoid repeating yourself. Many of the features still serve to make it easier to follow the DRY principle, but this is no longer the primary focus of the tool. Much of the marketing around Terragrunt still emphasizes the DRY principle, as it is a useful way to explain the value of Terragrunt to new users. However, you might miss the forest for the trees if you focus too much on the DRY principle when evaluating Terragrunt. Terragrunt is a powerful tool that can be used to manage infrastructure at scale, and it is worth evaluating it based on its capabilities to do so. ### Blast Radius [Section titled “Blast Radius”](#blast-radius) [Blast Radius](https://en.wikipedia.org/wiki/Blast_radius) is a term used in software development to describe the potential impact of a change, derived from the term used to describe the potential impact of an explosion. In the context of infrastructure management, blast radius is used to describe the potential impact (negative or positive) of a change to infrastructure. The larger the blast radius, the more potential impact a change has. Terragrunt was born out of a need to reduce the blast radii of infrastructure changes. By making it easier to segment state in infrastructure, and to manage dependencies between units, Terragrunt makes it easier to reason about the impact of changes to infrastructure, and to ensure that changes can be made safely. When using Terragrunt, there is very frequently a mapping between your filesystem and the infrastructure you have provisioned with OpenTofu/Terraform. As such, when changing your current working directory in a Terragrunt project, you end up implicitly changing the blast radius of Terragrunt commands. The more units you have as children of your current working directory (the units in your stack), the more infrastructure you are likely to impact with a Terragrunt command. As an adage, you can generally think of this property as: “Your current working directory is your blast radius”. ### Run [Section titled “Run”](#run) A run is a single invocation of OpenTofu/Terraform by Terragrunt. Runs are the primary way that Terragrunt does work. When you run `terragrunt plan` or `terragrunt apply`, Terragrunt will invoke OpenTofu/Terraform to drive the infrastructure update accordingly. Note that runs abstract away a lot of the complexity that comes from working with OpenTofu/Terraform directly. Terragrunt might automatically perform some code generation, provision requisite resources, or add/modify to the underlying OpenTofu/Terraform configuration to ensure that day to day operations are as smooth as possible. The way in which these complexities are abstracted is via Terragrunt configuration files (`terragrunt.hcl`), which can be used to define how Terragrunt should forward commands to OpenTofu/Terraform. There is an explicit list of [supported shortcuts](https://docs.terragrunt.com/reference/cli/commands/opentofu-shortcuts/) that Terragrunt will forward to OpenTofu/Terraform by default. For all other commands that need to be forwarded to OpenTofu/Terraform, use the `run` command (e.g., `terragrunt run -- workspace ls`). In the simplest case, a run in a unit with an empty `terragrunt.hcl` file will be equivalent to running OpenTofu/Terraform directly in the unit directory (with some small additional features like automatic initialization and logging adjustments). ### Execution [Section titled “Execution”](#execution) An execution is a single command run by Terragrunt, which does not necessarily have anything to do with OpenTofu/Terraform. Ways in which Terragrunt can perform executions are limited to features like [hooks](/features/units/hooks/), [run\_cmd](/reference/hcl/functions#run_cmd), etc. These utilities are part of what makes Terragrunt so powerful, as they allow users to move infrastructure management complexity out of modules. ### Run Queue [Section titled “Run Queue”](#run-queue) The Run Queue is the queue of all units that Terragrunt will do work on over one or more runs. Certain commands like [run —all](/reference/cli/commands/run#all) populate the Run Queue with all units in a stack, while other commands like `plan` or `apply` will only populate the Run Queue with the unit that the command was run in. The [`--filter`](/features/filter) flag can be used to adjust the Run Queue, including or excluding units based on filter expressions. For example, `--filter './networking'` includes a specific path, while `--filter '!./legacy'` excludes one. Terragrunt will always attempt to run until the Run Queue is empty. ### Runner Pool [Section titled “Runner Pool”](#runner-pool) The Runner Pool is the pool of available resources that Terragrunt can use to execute runs. Units are dequeued from the Run Queue into the Runner Pool depending on factors like [parallelism](/reference/cli/commands/run#parallelism) and the DAG. Units are only considered “running” when they are in the Runner Pool. ### Dependency [Section titled “Dependency”](#dependency) A dependency is a relationship between two units in a stack that results in data being passed from the dependency to the dependent unit. Dependencies are defined in Terragrunt configuration files using the [dependency block](/reference/hcl/blocks#dependency). Dependencies are important for resolving the DAG, and the DAG is one of the most important properties to understand with Terragrunt. In an effort to avoid confusing users, Terragrunt maintainers attempt to overload the term “dependency” as little as possible. Other relationships may be described as “reading” or “including” to avoid any ambiguity as to what is relevant to the DAG. ### Include [Section titled “Include”](#include) The term “include” is used in two different contexts in Terragrunt. 1. **Include in configuration**: This is when one configuration file is included as partial configuration in another configuration file. This is done using the [include block](/reference/hcl/blocks#include) in Terragrunt configuration files. 2. **Include in the Run Queue**: This is when a unit is included in the Run Queue. There are multiple ways for a unit to be included in the Run Queue. ### Exclude [Section titled “Exclude”](#exclude) The term “exclude” is only used in the context of excluding units from the Run Queue. ### Variable [Section titled “Variable”](#variable) A variable is a named dynamic value that is exposed by OpenTofu/Terraform configurations. To avoid ambiguity, Terragrunt maintainers try to avoid using the term “variable” in Terragrunt documentation. ### Input [Section titled “Input”](#input) An input is a value configured in Terragrunt configurations to set the value of OpenTofu/Terraform variables. Inputs are defined in Terragrunt configuration files using the [inputs attribute](/reference/hcl/attributes#inputs). Under the hood, these inputs result in `TF_VAR_` prefixed environment variables being populated before initiating a run. ### Output [Section titled “Output”](#output) An output is a value that is returned by OpenTofu/Terraform after a run is completed. By default, Terragrunt will interact with OpenTofu/Terraform in order to retrieve these outputs via [dependency blocks](/reference/hcl/blocks#dependency). Terragrunt does have the ability to mock outputs, which is useful when dependencies do not yet have outputs to be consumed (e.g. during the run of a unit with a dependency that has not been applied). Terragrunt also has the ability to fetch outputs without interacting with OpenTofu/Terraform via [—dependency-fetch-output-from-state](/reference/cli/commands/run#dependency-fetch-output-from-state) for dependencies where state is stored in AWS. This is an experimental feature, and more tooling is planned to make this easier to use. ### Feature [Section titled “Feature”](#feature) A [feature](/reference/cli/commands/run#feature) is a configuration that can be dynamically controlled in Terragrunt configurations. They operate very similarly to variables, but are designed to be used to dynamically adjust the behavior of Terragrunt configurations, rather than OpenTofu/Terraform configurations. Features can be adjusted using feature flags, which are set in Terragrunt configurations using the [feature block](/reference/hcl/blocks#feature) and the [feature flag](/reference/cli/commands/run#feature) attribute. Like all good feature flags, you are encouraged to use them with good judgement and to avoid using them as a crutch to avoid making decisions about permanent adjustments to your infrastructure. ### IaC Engine [Section titled “IaC Engine”](#iac-engine) [IaC Engines](/features/units/engine/) (typically abbreviated “Engines”) are a way to extend the capabilities of Terragrunt by allowing users to control exactly how Terragrunt performs runs. Engines allow Terragrunt users to author custom logic for how runs are to be executed in plugins, including defining exactly how OpenTofu/Terraform is to be invoked, where OpenTofu/Terraform is to be invoked, etc. ### Infrastructure Estate [Section titled “Infrastructure Estate”](#infrastructure-estate) An infrastructure estate is all the infrastructure that a person or organization manages. This can be as small as a single resource, or as large as a collection of repositories containing one or more stacks. Generally speaking, the larger the infrastructure estate, the more important it is to have good tooling for managing it. Terragrunt is designed to be able to manage infrastructure estates of any size, and is used by organizations of all sizes to manage their infrastructure efficiently. ## CLI Redesign [Section titled “CLI Redesign”](#cli-redesign) Note that some of the language used in this page may be adjusted in the near future due to RFC [#3445](https://github.com/gruntwork-io/terragrunt/issues/3445). To make terminology and overall UI/UX of using Terragrunt more consistent and easier to understand, the RFC proposes a number of changes to the CLI. This includes renaming some flags, reorganizing some commands, and adjusting some terminology. As of this writing, the RFC is still in the proposal stage, so share your thoughts on the RFC if you have any opinions on the proposed changes. # Introduction > Introduction to the Continuous Integration with Terragrunt guide [Continuous Integration (CI)](https://en.wikipedia.org/wiki/Continuous_integration) is the practice of merging code changes frequently and running checks against the integrated result. That gives you a single source of truth for what the codebase looks like, and surfaces conflicts between changes early. This guide will help you learn more about Continuous Integration and Continuous Deployment (also referred to as Continuous Delivery) in the context of Terragrunt. You’ll set up a free CI/CD pipeline that automatically evaluates every proposed code change in your Terragrunt project through plan comments and automatic deployments of your code changes through applies. You’ll do this in a GitHub Actions repository deploying AWS resources. If you don’t have a GitHub account or an AWS account, they are free to create, and you won’t be deploying anything expensive as part of this guide. If you’ve heard about CI, or heard the term CI/CD before, you might not have learned about it in this context. You might have learned about automation tools like [GitHub Actions](https://github.com/features/actions) or [GitLab CI/CD Pipelines](https://docs.gitlab.com/ci/pipelines/), and understood that to be what CI/CD was. Colloquially, that is what folks usually mean when they talk about CI/CD, but the general practice of Continuous Integration and Continuous Deployment (CD) are broader than any particular automation tool. ## Why infrastructure CI is different [Section titled “Why infrastructure CI is different”](#why-infrastructure-ci-is-different) This matters when you’re working with Infrastructure as Code, because some of the complexities in CI are amplified when managing infrastructure as opposed to CI for application code. When working with OpenTofu or Terraform: * `plan` tells you what will change before you change it. Every plan should be reviewed, and you should understand what’s going to change in your infrastructure before you `apply` it. * `apply` changes real infrastructure. Unlike a bad application deploy that you can roll back, a bad `apply` can delete databases, tear down networks, or take down production services. Rolling back isn’t always straightforward when working with stateful infrastructure. * Dependencies between resources and units need to be respected. If unit B depends on an output from unit A, you can’t apply them in arbitrary order without risking failures. * Credentials need to be handled carefully. In an automation context, long-lived AWS access keys stored as CI secrets are a security liability. Short-lived, least-privilege credentials are the standard practice. This is especially important for IaC because credentials used for CI in IaC are typically far more powerful than credentials used for application CI. These kinds of issues can be present when deploying applications as well. They’re just more pronounced when managing infrastructure, because the risk involved in updates can be more widespread, and resources are frequently stateful (there’s no rollback if you delete your database and its backups). ## What good IaC CI looks like [Section titled “What good IaC CI looks like”](#what-good-iac-ci-looks-like) When you put all of this together, a good CI setup for IaC has a few specific properties: 1. **Plan on every pull request**, so that reviewers can review both the code change *and* the computed dry-run of the code change before it gets merged. 2. **Apply on merge to main**, so that the main branch is always the source of truth for what’s actually deployed. 3. **DAG-aware orchestration**, so that dependencies between infrastructure units are respected during both plan and apply. 4. **OIDC-based authentication**, so that CI jobs get temporary, scoped credentials instead of long-lived secrets. Setting all of this up from scratch is a significant amount of engineering work. Change detection, dependency graph traversal, concurrent execution, credential management, and surfacing plan results back to pull requests all need to work together reliably. Many teams have started building this and either abandoned it halfway or ended up maintaining a fragile collection of scripts. [Terragrunt Scale](https://terragrunt.com/terragrunt-scale) provides these properties out of the box. This guide will walk through setting up Terragrunt Scale Free Tier, understanding what it configures for you, and using it to provision real AWS infrastructure through pull requests. ## What you’ll learn [Section titled “What you’ll learn”](#what-youll-learn) * How CI works in the context of IaC, and what makes it different from CI for application code. * How Terragrunt Scale sets up GitHub Actions workflows for plan-on-PR and apply-on-merge. * How OIDC authentication works and why it replaces long-lived AWS credentials. * How DAG-aware orchestration handles changes across multiple infrastructure units. ## What you’ll do [Section titled “What you’ll do”](#what-youll-do) 1. Sign up for Terragrunt Scale Free Tier and walk through onboarding. 2. Explore the repository structure, workflows, and authentication that onboarding configures. 3. Make an infrastructure change, open a PR, and see the plan. 4. Merge the PR and watch the apply. 5. Make a multi-unit change and see how Terragrunt orchestrates it across the dependency graph. # Deploying an app > Deploy a real application through your Terragrunt Scale pipeline Now that we’ve explored what onboarding produced, we can drive Pipelines directly from the repo without the Gruntwork developer portal. We’re going to introduce a [Terragrunt Stack](/features/stacks/), open a pull request, let the Pipelines workflow trigger a plan, then merge it to deploy our infrastructure update. We’re going to be modeling the changes you’ll make here off the [terragrunt-infrastructure-live-stacks-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example) repository. It’s a recommended starting template for how infrastructure can be modeled in a scalable fashion with stacks. ## Cloning the repository [Section titled “Cloning the repository”](#cloning-the-repository) Pipelines is a GitOps product, and all the changes that you’re going to drive happen from pull requests created and merged in the GitHub repository you created during Terragrunt Scale onboarding. Clone your GitHub repository like so (assuming you use SSH for cloning): ```bash git clone git@github.com:/.git ``` Or like this if you clone your repos over HTTPS: ```bash git clone https://github.com//.git ``` ## Adjusting Permissions [Section titled “Adjusting Permissions”](#adjusting-permissions) Before we deploy any new infrastructure, we’ll want to adjust the permissions used by the IAM roles that were bootstrapped during onboarding. They don’t have the requisite permissions we’ll need to deploy the app, so we’ll have to make a pull request to change that first. The default permissions created during bootstrapping are enough for most basic infrastructure updates, but they don’t include permissions to manage the `my-special-app-db` DynamoDB table we’re going to introduce in a bit. To address this, we’re going to copy the default policy that ships with the stack, and adjust it to include permissions for managing `my-special-app-db`. These changes provide sufficient permissions, but they may be more or less permissive than you are comfortable with, or than your organization allows. Always carefully evaluate access control changes and determine if they are right for you and your organization. Note that these permissions don’t just provide the permissions required for deploying your application, but also for managing the infrastructure that was bootstrapped during Terragrunt Scale onboarding. If you want to, you can set up Terragrunt Scale such that the IAM role used to manage your application *doesn’t* have permissions to manage the underlying infrastructure provisioned during bootstrapping. To support that, you can learn about the [infrastructure-live-access-control](https://docs.gruntwork.io/2.0/docs/accountfactory/architecture/repository-topology/#infrastructure-live-access-control) pattern. ### Defining the IAM policies [Section titled “Defining the IAM policies”](#defining-the-iam-policies) To grant the plan and apply roles the requisite permissions to deploy the app, create the following `plan_iam_policy.json` and `apply_iam_policy.json` files in your `account-name/_global/bootstrap` stack directory. * plan\_iam\_policy.json account-name/\_global/bootstrap/plan\_iam\_policy.json ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "S3StateBucketAccess", "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketVersioning", "s3:GetBucketAcl", "s3:GetBucketLogging", "s3:CreateBucket", "s3:PutBucketPublicAccessBlock", "s3:PutBucketTagging", "s3:PutBucketPolicy", "s3:PutBucketVersioning", "s3:PutEncryptionConfiguration", "s3:PutBucketAcl", "s3:PutBucketLogging", "s3:GetEncryptionConfiguration", "s3:GetBucketPolicy", "s3:GetBucketPublicAccessBlock", "s3:PutLifecycleConfiguration", "s3:PutBucketOwnershipControls" ], "Resource": "arn:aws:s3:::${state_bucket_name}" }, { "Sid": "S3StateBucketObjectAccess", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject" ], "Resource": "arn:aws:s3:::${state_bucket_name}/*" }, { "Sid": "DynamoDBLocksTableAccess", "Effect": "Allow", "Action": [ "dynamodb:DescribeTable", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem" ], "Resource": "arn:${aws_partition}:dynamodb:*:*:table/terraform-locks" }, { "Sid": "DynamoDBReadAccess", "Effect": "Allow", "Action": [ "dynamodb:DescribeTable", "dynamodb:DescribeContinuousBackups", "dynamodb:DescribeTimeToLive", "dynamodb:ListTagsOfResource" ], "Resource": "arn:${aws_partition}:dynamodb:*:*:table/my-special-app-db" }, { "Sid": "LambdaReadAccess", "Effect": "Allow", "Action": [ "lambda:GetFunction", "lambda:GetFunctionConfiguration", "lambda:GetFunctionCodeSigningConfig", "lambda:GetFunctionUrlConfig", "lambda:GetPolicy", "lambda:ListVersionsByFunction", "lambda:ListTags" ], "Resource": "arn:${aws_partition}:lambda:*:*:function:my-special-app*" }, { "Sid": "IAMAppRoleReadAccess", "Effect": "Allow", "Action": [ "iam:GetRole", "iam:GetRolePolicy", "iam:ListRolePolicies", "iam:ListAttachedRolePolicies", "iam:ListInstanceProfilesForRole", "iam:ListRoleTags" ], "Resource": "arn:${aws_partition}:iam::*:role/my-special-app*" }, { "Sid": "IAMPipelinesRoleReadAccess", "Effect": "Allow", "Action": [ "iam:GetRole", "iam:ListRolePolicies", "iam:ListAttachedRolePolicies", "iam:ListInstanceProfilesForRole", "iam:ListRoleTags" ], "Resource": [ "arn:${aws_partition}:iam::*:role/pipelines-plan", "arn:${aws_partition}:iam::*:role/pipelines-apply" ] }, { "Sid": "IAMPipelinesPolicyReadAccess", "Effect": "Allow", "Action": [ "iam:GetPolicy", "iam:GetPolicyVersion", "iam:ListPolicyVersions" ], "Resource": [ "arn:${aws_partition}:iam::*:policy/pipelines-plan", "arn:${aws_partition}:iam::*:policy/pipelines-apply" ] }, { "Sid": "OIDCProviderReadAccess", "Effect": "Allow", "Action": [ "iam:GetOpenIDConnectProvider" ], "Resource": "arn:${aws_partition}:iam::*:oidc-provider/token.actions.githubusercontent.com" } ] } ``` * apply\_iam\_policy.json account-name/\_global/bootstrap/apply\_iam\_policy.json ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "S3StateBucketAccess", "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketVersioning", "s3:GetBucketAcl", "s3:GetBucketLogging", "s3:CreateBucket", "s3:PutBucketPublicAccessBlock", "s3:PutBucketTagging", "s3:PutBucketPolicy", "s3:PutBucketVersioning", "s3:PutEncryptionConfiguration", "s3:PutBucketAcl", "s3:PutBucketLogging", "s3:GetEncryptionConfiguration", "s3:GetBucketPolicy", "s3:GetBucketPublicAccessBlock", "s3:PutLifecycleConfiguration", "s3:PutBucketOwnershipControls" ], "Resource": "arn:aws:s3:::${state_bucket_name}" }, { "Sid": "S3StateBucketObjectAccess", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject" ], "Resource": "arn:aws:s3:::${state_bucket_name}/*" }, { "Sid": "DynamoDBLocksTableAccess", "Effect": "Allow", "Action": [ "dynamodb:DescribeTable", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem" ], "Resource": "arn:${aws_partition}:dynamodb:*:*:table/terraform-locks" }, { "Sid": "DynamoDBFullAccess", "Effect": "Allow", "Action": [ "dynamodb:CreateTable", "dynamodb:DeleteTable", "dynamodb:UpdateTable", "dynamodb:DescribeTable", "dynamodb:DescribeContinuousBackups", "dynamodb:DescribeTimeToLive", "dynamodb:UpdateTimeToLive", "dynamodb:UpdateContinuousBackups", "dynamodb:TagResource", "dynamodb:UntagResource", "dynamodb:ListTagsOfResource" ], "Resource": "arn:${aws_partition}:dynamodb:*:*:table/my-special-app-db" }, { "Sid": "LambdaFullAccess", "Effect": "Allow", "Action": [ "lambda:CreateFunction", "lambda:DeleteFunction", "lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration", "lambda:GetFunction", "lambda:GetFunctionConfiguration", "lambda:GetFunctionCodeSigningConfig", "lambda:GetFunctionUrlConfig", "lambda:CreateFunctionUrlConfig", "lambda:UpdateFunctionUrlConfig", "lambda:DeleteFunctionUrlConfig", "lambda:GetPolicy", "lambda:AddPermission", "lambda:RemovePermission", "lambda:ListVersionsByFunction", "lambda:ListTags", "lambda:TagResource", "lambda:UntagResource", "lambda:PublishVersion" ], "Resource": "arn:${aws_partition}:lambda:*:*:function:my-special-app*" }, { "Sid": "IAMAppRoleFullAccess", "Effect": "Allow", "Action": [ "iam:CreateRole", "iam:DeleteRole", "iam:UpdateRole", "iam:GetRole", "iam:GetRolePolicy", "iam:PutRolePolicy", "iam:DeleteRolePolicy", "iam:ListRolePolicies", "iam:ListAttachedRolePolicies", "iam:ListInstanceProfilesForRole", "iam:ListRoleTags", "iam:TagRole", "iam:UntagRole", "iam:PassRole", "iam:AttachRolePolicy", "iam:DetachRolePolicy" ], "Resource": "arn:${aws_partition}:iam::*:role/my-special-app*" }, { "Sid": "IAMPipelinesRoleFullAccess", "Effect": "Allow", "Action": [ "iam:CreateRole", "iam:DeleteRole", "iam:UpdateRole", "iam:UpdateAssumeRolePolicy", "iam:GetRole", "iam:ListRolePolicies", "iam:ListAttachedRolePolicies", "iam:ListInstanceProfilesForRole", "iam:ListRoleTags", "iam:TagRole", "iam:UntagRole", "iam:AttachRolePolicy", "iam:DetachRolePolicy" ], "Resource": [ "arn:${aws_partition}:iam::*:role/pipelines-plan", "arn:${aws_partition}:iam::*:role/pipelines-apply" ] }, { "Sid": "IAMPipelinesPolicyFullAccess", "Effect": "Allow", "Action": [ "iam:CreatePolicy", "iam:DeletePolicy", "iam:GetPolicy", "iam:GetPolicyVersion", "iam:CreatePolicyVersion", "iam:DeletePolicyVersion", "iam:ListPolicyVersions", "iam:TagPolicy", "iam:UntagPolicy" ], "Resource": [ "arn:${aws_partition}:iam::*:policy/pipelines-plan", "arn:${aws_partition}:iam::*:policy/pipelines-apply" ] }, { "Sid": "OIDCProviderFullAccess", "Effect": "Allow", "Action": [ "iam:CreateOpenIDConnectProvider", "iam:DeleteOpenIDConnectProvider", "iam:UpdateOpenIDConnectProviderThumbprint", "iam:GetOpenIDConnectProvider", "iam:AddClientIDToOpenIDConnectProvider", "iam:RemoveClientIDFromOpenIDConnectProvider", "iam:TagOpenIDConnectProvider", "iam:UntagOpenIDConnectProvider" ], "Resource": "arn:${aws_partition}:iam::*:oidc-provider/token.actions.githubusercontent.com" } ] } ``` ### Wiring policies into the bootstrap stack [Section titled “Wiring policies into the bootstrap stack”](#wiring-policies-into-the-bootstrap-stack) Now that those two files are in place, we can adjust the `account-name/_global/bootstrap/terragrunt.stack.hcl` file so that it references them. The highlighted lines below are the new ones that wire the policy templates into the bootstrap stack. account-name/\_global/bootstrap/terragrunt.stack.hcl ```hcl // Bootstrap stack: provisions the GitHub OIDC provider and plan/apply IAM roles in this environment. // Terragrunt Stacks: https://docs.terragrunt.com/features/stacks/ locals { // Read from parent configurations instead of defining these values locally // so that other stacks and units in this directory can reuse the same configurations. account_hcl = read_terragrunt_config(find_in_parent_folders("account.hcl")) } stack "bootstrap" { // To upgrade: update the ?ref= tag and review https://github.com/gruntwork-io/terragrunt-scale-catalog/releases source = "github.com/gruntwork-io/terragrunt-scale-catalog//stacks/aws/github/pipelines-bootstrap?ref=v1.10.1" path = "bootstrap" values = { // Should match the ?ref= above. terragrunt_scale_catalog_ref = "v1.10.1" aws_account_id = "123456789012" // Prefix for the IAM roles created: -plan and -apply. oidc_resource_prefix = "pipelines" // Only Actions workflows in this org/repo can assume the IAM roles. github_org_name = "john-doe" github_repo_name = "acme-infrastructure-live" deploy_branch = "main" state_bucket_name = local.account_hcl.locals.state_bucket_name plan_iam_policy = templatefile("${get_terragrunt_dir()}/plan_iam_policy.json", { state_bucket_name = local.account_hcl.locals.state_bucket_name aws_partition = "aws" }) apply_iam_policy = templatefile("${get_terragrunt_dir()}/apply_iam_policy.json", { state_bucket_name = local.account_hcl.locals.state_bucket_name aws_partition = "aws" }) // ========================================================================= // Import Variables // // The following variables are used to import existing AWS resources into // OpenTofu/Terraform state. Once the stack has been applied and resources // have been successfully imported, it is safe to remove this entire section. // ========================================================================= oidc_provider_import_arn = "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" plan_iam_role_import_existing = true plan_iam_policy_import_arn = "arn:aws:iam::123456789012:policy/pipelines-plan" plan_iam_role_policy_attachment_import_arn = "pipelines-plan/arn:aws:iam::123456789012:policy/pipelines-plan" apply_iam_role_import_existing = true apply_iam_policy_import_arn = "arn:aws:iam::123456789012:policy/pipelines-apply" apply_iam_role_policy_attachment_import_arn = "pipelines-apply/arn:aws:iam::123456789012:policy/pipelines-apply" // ========================================================================= // End Import Variables // ========================================================================= } } ``` This is how the JSON definition of the permissions used for the plan/apply roles get plumbed down to the relevant plan/apply units. ### Submitting the permissions PR [Section titled “Submitting the permissions PR”](#submitting-the-permissions-pr) Commit the changes here in a new branch, and push them to your remote repository. ```bash git checkout -b chore/adjusting-permissions-for-roles git add . git commit -m "chore: Adjusting permissions for roles" git push -u origin chore/adjusting-permissions-for-roles ``` You can click the link you get from GitHub in stdout (or use the [`gh` CLI](https://cli.github.com/) if you have it installed) to create the pull request. ```bash gh pr create --title 'chore: Adjusting permissions for roles' --body 'Adjusting the permissions for the roles provisioned during Terragrunt Scale baseline.' ``` If you use the `gh` CLI for this, you can also run the following to load the pull request in your browser. ```bash gh pr view -w ``` You should see a pull request with a comment like the following on GitHub. ![Pipelines plan summary comment on the permissions PR](/_astro/permissions-pr-plan-comment.DZDjZoU1.png) Clicking on those “Plan Summary” and “Plan Output” summaries will disclose more details on the plan of your pull request. Take a look at the changes this pull request is going to result in, then merge the pull request. After a bit, you’ll see another comment like the following displaying the permissions updates. ![Pipelines apply comment after merging the permissions PR](/_astro/permissions-pr-apply-comment.BMUOo0y_.png) This is the general flow that you’ll go through whenever you interact with Terragrunt Scale products. They’re all designed around GitOps principles, and the code is the source of truth for your infrastructure. To change your infrastructure, change the code and open a pull request. ## Deploying the app [Section titled “Deploying the app”](#deploying-the-app) Now that we have the requisite permissions to deploy our app, let’s go ahead and deploy it. ### Adding the application stack [Section titled “Adding the application stack”](#adding-the-application-stack) Switch back to `main`, check out a new branch, and add a new stack adapted from the [infrastructure-live-stacks-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example) repository. First, run the following in your local repository to prepare your local branch for the new content: ```bash git checkout main git pull git checkout -b feat/adding-my-special-app ``` Next, you can add the following `terragrunt.stack.hcl` file to a new directory in `account-name/us-east-1/app`: account-name/us-east-1/app/terragrunt.stack.hcl ```hcl locals { name = "my-special-app" version = "v1.1.1" } unit "lambda_service" { source = "github.com/gruntwork-io/terragrunt-infrastructure-catalog-example//units/js-lambda-stateful-service?ref=${local.version}" path = "service" values = { version = local.version name = local.name // Required inputs runtime = "nodejs22.x" source_dir = "./src" handler = "index.handler" zip_file = "handler.zip" // Optional inputs memory = 128 timeout = 3 // Dependency paths role_path = "../roles/lambda-iam-role-to-dynamodb" dynamodb_table_path = "../db" } } unit "db" { source = "github.com/gruntwork-io/terragrunt-infrastructure-catalog-example//units/dynamodb-table?ref=${local.version}" path = "db" values = { version = local.version name = "${local.name}-db" hash_key = "Id" hash_key_type = "S" } } unit "role" { source = "github.com/gruntwork-io/terragrunt-infrastructure-catalog-example//units/lambda-iam-role-to-dynamodb?ref=${local.version}" path = "roles/lambda-iam-role-to-dynamodb" values = { version = local.version name = "${local.name}-role" dynamodb_table_path = "../../db" } } ``` ### Submitting the deployment PR [Section titled “Submitting the deployment PR”](#submitting-the-deployment-pr) Now you can commit this file, push your branch up, and create a pull request to deploy your infrastructure in the same way you adjusted the permissions for the IAM roles earlier. ```bash git add . git commit -m "feat: Adding my special app" git push -u origin feat/adding-my-special-app ``` And again, you can either click the link that GitHub gives you, or you can use the `gh` CLI to create the merge request. ```bash gh pr create --title 'feat: Adding my special app' --body 'Adding my special app.' ``` Just like before, you’ll get a plan with the changes that you’re about to deploy that you can review. ![Pipelines plan comment for the new app PR](/_astro/app-pr-plan-comment.KRwbj2jq.png) Once you merge, you’ll get the comment with the apply update. ![Pipelines apply comment after merging the new app PR](/_astro/app-pr-apply-comment.CWo-H4Ll.png) If you click “Apply Output for `account-name/us-east-1/app/.terragrunt-stack/service`”, you’ll get the full apply output, including the URL for the service you just deployed! ![Apply output showing the deployed Lambda function URL](/_astro/app-apply-output.BNMKZU8S.png) ## Testing the app [Section titled “Testing the app”](#testing-the-app) If you navigate to that link, or use curl locally, you’ll be able to see a simple JSON object indicating the number of times the service has received a POST request. ```bash $ curl -s 'https://abuncharandomcharacters.lambda-url.us-east-1.on.aws/' {"count":0} ``` You can also send some POST requests to see the value increment up to prove to yourself that it’s properly integrated with the database. ```bash $ curl -s -XPOST 'https://abuncharandomcharacters.lambda-url.us-east-1.on.aws/' {"count":1} $ curl -s -XPOST 'https://abuncharandomcharacters.lambda-url.us-east-1.on.aws/' {"count":2} $ curl -s -XPOST 'https://abuncharandomcharacters.lambda-url.us-east-1.on.aws/' {"count":3} $ curl -s 'https://abuncharandomcharacters.lambda-url.us-east-1.on.aws/' {"count":3} ``` ## Clean-up [Section titled “Clean-up”](#clean-up) Cleanup is simple with GitOps: revert the pull request in the GitHub UI. ![Reverting the app PR from the GitHub UI](/_astro/revert-pr.D-hNzJOM.png) Pipelines is pretty smart, and it’s able to work out that removing this content in a pull request signals that it needs to run Terragrunt to destroy those resources. You’ll get a plan that shows those units in the stack getting `plan -destroy` called on them, and when you merge, you’ll be done with your clean-up! ![Pipelines destroy plan comment on the revert PR](/_astro/cleanup-plan-destroy.jAaXCkkz.png) Note that the plan lists the units as being deleted, with a count of the resources being destroyed in each unit. Once you merge the pull request, you’ll get a comment like the following: ![Pipelines destroy apply comment after merging the revert PR](/_astro/cleanup-apply-destroy.BjUJ-T75.png) # Lessons Learned > Lessons learned from setting up continuous integration with Terragrunt That went by pretty fast! Hopefully, this experience has given you some confidence in your ability to manage infrastructure using Terragrunt and Terragrunt Scale. Let’s go over some of the main lessons learned from this guide. ## Single source of truth [Section titled “Single source of truth”](#single-source-of-truth) At every stage in this process the deploy branch (`main` here) was always the source of truth for what infrastructure is *supposed* to be like. This is the core of what it means for code to be *continuously integrated*. You never have to guess which branch represents reality. It’s always `main`. All proposed changes are pull requests targeting the `main` branch, and that helps to keep the scope of changes small and easy to work with. Plans are always generated off the diff between `main` and the source branch targeting `main`. ## Automated deployments [Section titled “Automated deployments”](#automated-deployments) The changes you made to deploy your application and tear it down were to introduce a stack in your source code, and then removing that stack. At no point in this process did you need to explicitly work out what to run, where or in what order for the infrastructure to be deployed correctly. This is the core of what it means for code to be *continuously deployed.* ## Seamless integration with Terragrunt [Section titled “Seamless integration with Terragrunt”](#seamless-integration-with-terragrunt) Pipelines ran Terragrunt in the correct order both times: when you added the stack, and when you reverted it. During PR creation, Pipelines generated the relevant stacks (and only the relevant stacks) in the source branch (`feat/adding-my-special-app`), then ran a plan on the units in that stack, ensuring that `db` was planned before `role`, which was planned before `service`. It also deployed them in that order on merge to the deploy branch (`main`). `service` reads outputs from `role` and `db`, and `role` reads outputs from `db`. Each unit needs its dependencies applied first. This is why it’s important that Pipelines respects the [Directed Acyclic Graph (DAG)](/getting-started/terminology/#directed-acyclic-graph-dag). On the revert PR, destroys ran in the opposite order. `service` had to be destroyed first, because it depends on `role` and `db`. The destroys also had to run on the source branch, where the stack still existed, not on `main`, where it had just been removed. Pipelines handled both. When CI/CD works well, this is the kind of experience you want to have. You and your team focus on writing code. The pipeline figures out what to do and in what order. # Prerequisites > Prerequisites for the Continuous Integration with Terragrunt guide This guide assumes familiarity with Terragrunt units, stacks, and HCL configuration. If you’re new to Terragrunt, the [Quick Start](/getting-started/quick-start), [Overview](/getting-started/overview/) and [Terralith to Terragrunt](/guides/terralith-to-terragrunt/) guide are good places to start. You will need: * An AWS account with permissions to create IAM roles, S3 buckets, Lambda functions, DynamoDB tables, and an OIDC provider. * A GitHub account. * Git installed locally. If you don’t have these prerequisites, you can sign-up or install them for free at the following links: * [AWS account sign-up](https://signin.aws.amazon.com/signup?request_type=register) * [GitHub account sign-up](https://github.com/signup) * [Git installation](https://git-scm.com/downloads) Caution When you first create an AWS account, you will be logged in as the root user of the AWS account. Never use this account for provisioning workloads. Create a separate administrator account using it, then log into that one to follow this guide. Read the [documentation on root users from Amazon](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html) for more information. ## Getting help [Section titled “Getting help”](#getting-help) If you feel lost, confused, or need extra help while going through this guide, ask in the [Terragrunt Discord Server](/community/invite). There are hundreds of Terragrunt practitioners in there, and they’d be happy to help. You can also reference the [Terminology](/getting-started/terminology/) page in the docs, which contains a glossary that can help you understand any unfamiliar terms you encounter. # Terragrunt Scale > Signing up for Terragrunt Scale Free Tier and walking through the onboarding wizard Terragrunt Scale is Gruntwork’s solution for running Terragrunt at scale. You can learn more about it [here](/terragrunt-scale/overview/). By the end of this section, you’ll have a GitHub repository with Terragrunt infrastructure code, GitHub Actions workflows for plan-on-PR and apply-on-merge automation, OIDC authentication to your AWS account, and sample infrastructure ready to deploy. ## Create a new GitHub repository [Section titled “Create a new GitHub repository”](#create-a-new-github-repository) Before signing up, [create a new empty GitHub repository](https://github.com/new) that Terragrunt Scale will bootstrap. The onboarding wizard will install the Gruntwork GitHub App on this repository, open a pull request that scaffolds the Terragrunt configuration, and configure the GitHub Actions workflows that drive your pipeline. A fresh, empty repository works best. If you point the wizard at an existing repository, the bootstrap pull request can conflict with files already in place. ## Sign up for Terragrunt Scale Free Tier [Section titled “Sign up for Terragrunt Scale Free Tier”](#sign-up-for-terragrunt-scale-free-tier) Head to the [Terragrunt Scale Free Tier signup](https://app.gruntwork.io/signup/terragrunt-scale-free-tier) to create an account. The free tier supports up to 25 [units](/getting-started/terminology/#unit) with a convenient onboarding wizard for GitHub and AWS. No credit card is required for signup. ![Terragrunt Scale Free Tier signup page](/_astro/signup-page.RupYvEr6.png) ## Walk through the onboarding [Section titled “Walk through the onboarding”](#walk-through-the-onboarding) The onboarding wizard will walk you through a few steps to get your environment set up. The walkthrough below captures every screen so you can see exactly what the experience looks like before you commit to running it yourself; expand it when you’re ready to follow along. Walkthrough screenshots ![Onboarding wizard landing screen](/_astro/onboarding-wizard.OaiNc2eU.png) ### Installing the GitHub App [Section titled “Installing the GitHub App”](#installing-the-github-app) Right now, the only Source Control Manager (SCM) the Terragrunt Scale Free Tier onboarding wizard supports is GitHub. Pipelines also works with GitLab, but the onboarding wizard doesn’t support GitLab yet. The wizard will prompt you to install the [Gruntwork.io](https://gruntwork.io/) GitHub App on your GitHub account or organization. This app is what allows Terragrunt Scale to post plan results as comments on your pull requests and checkout Git repositories you give it access to. It acquires this access from within your GitHub Actions workflows, and it does not access your cloud infrastructure or your OpenTofu state. ![Install the Gruntwork GitHub App](/_astro/install-github-app.AZDzvgtN.png) ### Bootstrapping AWS resources [Section titled “Bootstrapping AWS resources”](#bootstrapping-aws-resources) Right now, the only supported cloud provider for the Terragrunt Scale Free Tier onboarding wizard is AWS. Pipelines has support for Azure and GCP (and custom self-configured authentication for anything else), but the onboarding wizard only supports AWS at the time of writing. ![AWS bootstrap script step in the onboarding wizard](/_astro/bootstrap-script.D63qf6Ko.png) The wizard generates a script that you run against your AWS account. This script provisions an [OpenID Connect (OIDC) provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html) and IAM roles that GitHub Actions will assume when running plans and applies. If you’re unsure about running it, you can read the full source to learn what it does. You can even use the convenient “Analyze script with AI?” to help you understand the different parts. Tip The Free Tier is self-serve. If you’re on a paid Terragrunt Scale plan, Gruntwork support can help walk you through this bootstrap step and tailor it to your account setup. We’ll look at what OIDC does and why it matters in the next section. ![Bootstrap completion and bootstrap pull request prompt](/_astro/bootstrap-completion.CCCejVmR.png) In the next step, you’ll get prompted to create a pull request to bootstrap your repository. This sets up your repository for the basic configurations required to get up and running with Terragrunt Scale. Once the bootstrap is complete, the wizard will prompt you to create your first unit in your repository as part of a basic tutorial. Go through that tutorial, and read along to learn more about how Terragrunt Scale works, and how it drives infrastructure updates in a GitOps workflow. ## Understanding what the onboarding wizard set up [Section titled “Understanding what the onboarding wizard set up”](#understanding-what-the-onboarding-wizard-set-up) The onboarding did a lot of work automatically. Before we start using the pipeline, it’s worth slowing down a second and exploring what’s been provisioned. ### Repository structure [Section titled “Repository structure”](#repository-structure) [Clone the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) that onboarding created and take a look at the directory tree. * .github * workflows * pipelines-unlock.yml * pipelines.yml * .gitignore * .gruntwork * environment-account-name.hcl * repository.hcl * .mise.toml * account-name * \_global * bootstrap * terragrunt.stack.hcl * region.hcl * account.hcl * README.md * root.hcl If you’ve worked with Terragrunt before, this structure should look familiar. If you want to see the canonical version of this pattern, take a look at the [terragrunt-infrastructure-live-stacks-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example) repository. The key files: * `.github/workflows` contains the GitHub Actions workflows that Terragrunt Scale configured. * `.gruntwork` contains the configurations for Gruntwork products to understand how to properly interact with your environments (more on that later). * `.mise.toml` defines the versions pinned for Terragrunt and OpenTofu using [mise](https://mise.jdx.dev/). * Account directories (`account-name`) organize infrastructure by AWS account. * `terragrunt.stack.hcl` files define stacks, which are collections of related units. * `root.hcl` contains common configuration inherited by all units, including backend and provider settings. ### The GitHub Actions workflows [Section titled “The GitHub Actions workflows”](#the-github-actions-workflows) Open the `.github/workflows/` directory. The main file you want to focus on is `pipelines.yml`. The [Pipelines](https://www.gruntwork.io/platform/pipelines) workflow is triggered both when a PR is opened/updated and when it is merged. When the PR is opened/updated, Pipelines will run a Terragrunt plan on the source branch (e.g. `tutorial/add-unit-1776108877709`), and when it is merged, Pipelines will run a Terragrunt apply/destroy (for added/changed or removed units/stacks respectively) on the deploy branch (e.g. `main`). Pipelines will minimize the blast radius of these applies/destroys by ensuring that runs will only be performed in the units/stacks that are impacted by changes in the pull request. Pipelines also runs Terragrunt intelligently such that the [Directed Acyclic Graph (DAG)](/getting-started/terminology/#directed-acyclic-graph-dag) is respected. This is the plan-on-PR, apply-on-merge pattern described in the introduction. **The main branch is always the source of truth for what’s deployed.** ### Gruntwork HCL configuration [Section titled “Gruntwork HCL configuration”](#gruntwork-hcl-configuration) Pipelines uses [HCL](https://docs.gruntwork.io/2.0/reference/pipelines/configurations-as-code/) to configure how it operates. The onboarding wizard installed two files in `.gruntwork` to configure Pipelines for you: `repository.hcl` and `environment-account-name.hcl`. #### `repository.hcl` [Section titled “repository.hcl”](#repositoryhcl) repository.hcl ```hcl // Gruntwork Pipelines repository-wide configuration. // Docs: https://docs.gruntwork.io/2.0/docs/pipelines/configuration/settings repository { // Commits on this branch trigger `terragrunt apply`. // PRs against it trigger `terragrunt plan`. // If you change this, also update the branch trigger in your CI workflow file. deploy_branch_name = "main" env { PIPELINES_FEATURE_EXPERIMENT_IGNORE_UNITS_WITHOUT_ENVIRONMENT = "true" } } ``` The `repository` block defines configurations that are global to the repository. These are configurations that should apply in all scenarios. The default configurations include: 1. The `deploy_branch_name`, which configures the branch that drives infrastructure deployment. Code updated in this branch results in apply/destroy runs. 2. The `env`, which configures environment variables that are always set for Pipelines and Terragrunt. In the default bootstrap generated by Pipelines, the opt-in `PIPELINES_FEATURE_EXPERIMENT_IGNORE_UNITS_WITHOUT_ENVIRONMENT` feature flag is set to ensure that any units without environments configured (more on that later) don’t result in runs. #### `environment-account-name.hcl` [Section titled “environment-account-name.hcl”](#environment-account-namehcl) environment-account-name.hcl ```hcl // Pipelines environment config for the account-name AWS account. // Pipelines reads all .hcl files in .gruntwork/. // Add a new file here to register a new environment. // Docs: https://docs.gruntwork.io/2.0/docs/pipelines/configuration/settings environment "account-name" { // Defines the environment as matching all units under account-name/. filter { paths = ["account-name/*"] } authentication { // Pipelines assumes these IAM roles via OIDC. No static credentials needed. // plan role: read-only, used on PRs. apply role: write, used on merge to deploy branch. // Both roles are created by the bootstrap stack in _global/bootstrap/. aws_oidc { account_id = "123456789012" plan_iam_role_arn = "arn:aws:iam::123456789012:role/pipelines-plan" apply_iam_role_arn = "arn:aws:iam::123456789012:role/pipelines-apply" } } } ``` The `environment` block defines configurations for a particular set of units in your repository. You use the `filter` block to select those units by glob, and an `authentication` block to define how those units should be authenticated. There are a couple [different ways to authenticate](https://docs.gruntwork.io/2.0/docs/pipelines/concepts/cloud-auth/) in Pipelines. The one that’s been pre-configured here is the AWS OIDC authentication method. In this method, authentication takes place in an AWS account with the account ID `account_id`, and the `plan_iam_role_arn` IAM role is assumed on PR creation/update, whereas the `apply_iam_role_arn` IAM role is assumed on pushes to the deploy branch (e.g. `main`). ### OIDC authentication [Section titled “OIDC authentication”](#oidc-authentication) When the bootstrap step ran, it created an [OIDC provider](https://docs.gruntwork.io/2.0/docs/pipelines/concepts/cloud-auth/#overview) and [IAM roles](https://docs.gruntwork.io/2.0/docs/pipelines/concepts/cloud-auth/aws?config-type=hcl#how-pipelines-knows-what-aws-principals-to-authenticate-as) in your AWS account. Here’s [what’s happening when a CI job runs](https://docs.gruntwork.io/2.0/docs/pipelines/concepts/cloud-auth/aws?config-type=hcl#publicly-available-cicd-platforms): 1. GitHub Actions requests a short-lived token from GitHub’s OIDC provider. 2. That token is exchanged with AWS STS for temporary credentials by assuming the IAM role that the bootstrap step created. 3. The CI job uses those temporary credentials to run `terragrunt plan` or `terragrunt apply/destroy`. 4. The credentials expire after the job finishes. At no point are long-lived AWS access keys stored in GitHub. There are no `AWS_ACCESS_KEY_ID` or `AWS_SECRET_ACCESS_KEY` values sitting in your repository secrets. Each CI run gets temporary credentials scoped to the permissions it needs (read-only on plan, and read-write on apply/destroy). If you want to learn more about how Terragrunt handles authentication in different contexts, read the [Authentication documentation](/features/units/authentication/). Note that the roles provisioned in your AWS account had trust policies that only allowed role assumption when particular claims were made on the JWT token sent from GitHub Actions to AWS. #### The plan role trust policy [Section titled “The plan role trust policy”](#the-plan-role-trust-policy) This is what the trust policy will look like for the plan role: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringLike": { "token.actions.githubusercontent.com:sub": "repo:/:*" } } } ] } ``` Note that it will allow any branch on the `/` repository to assume it. This is necessary, given that this role is going to be assumed when generating a plan during pull request open/update. It’s for this reason that the plan role is only given read-only permissions by default in the onboarding wizard. You want to ensure that IaC changes undergo review before they make any changes to live infrastructure. #### The apply role trust policy [Section titled “The apply role trust policy”](#the-apply-role-trust-policy) Note how this differs from the trust policy that is used in the apply role: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:sub": "repo:/:ref:refs/heads/main" } } } ] } ``` The trust policy in this role *only* trusts the `main` branch of the `/` repository. Branch protection on `main` becomes infrastructure protection: if a user can’t push to `main` or bypass required checks, they can’t apply changes to live infrastructure either. # What's next > Next steps after completing the Continuous Integration with Terragrunt guide You’ve gone from a blank account to a working CI pipeline that plans on every PR and applies on merge, with OIDC authentication and DAG-aware orchestration. You can continue to use the same setup for your real production infrastructure going forward. Some things worth exploring beyond this guide: * **Granular Modifications**: What happens when you redeploy the stack, then edit the `memory` value for the `service` unit? What happens when you add another `service` unit? What happens when you remove one of them? How many units get planned? How many get applied? * **Building your own catalog**: The sample infrastructure uses modules from an example catalog. When you’re ready to write your own reusable modules, the [terragrunt-infrastructure-catalog-example](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example) repository is a good starting point. * **Multi-account, multi-region**: The sample repository has `non-prod` and `prod` account directories and `us-east-1` as a region. The [terragrunt-infrastructure-live-stacks-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example) repository demonstrates the full pattern for managing infrastructure across multiple accounts and regions. Read through the [Pipelines AWS Authentication documentation](https://docs.gruntwork.io/2.0/docs/pipelines/concepts/cloud-auth/aws) to learn how to configure Pipelines for multiple AWS accounts. Try setting cross account dependencies! * **Terralith to Terragrunt**: The [Terralith to Terragrunt](/guides/terralith-to-terragrunt/) guide is another long form guide where you’ll learn how to provision a much more interesting application than the one deployed here. Try deploying it using Terragrunt Scale! * **The Full Terragrunt Scale Experience**: In this guide, we only took advantage of the features in the Terragrunt Scale Free Tier. If you’re interested in Drift Detection to automatically remediate drift between your IaC and deployed infrastructure or Patcher to automatically keep your infrastructure up to date (even through breaking changes), check out the full [Terragrunt Scale](https://terragrunt.com/terragrunt-scale) offering. If you get stuck or have questions, the [Terragrunt Discord](/community/invite) community is active and happy to help. # Introduction > Introduction to the Terralith to Terragrunt guide A common challenge that emerges as infrastructure grows is the “Terralith,” a portmanteau of Terraform and Monolith. This pattern, also referred to as a “Megamodule” or an “All In One State” configuration, describes a scenario where a large, complex infrastructure estate is managed within a single state file. Imagine you’re a platform engineer, and what once felt like an instant `tofu apply` to update your infrastructure now drags on for minutes. You once had confidence that you could reliably update exactly the infrastructure you cared about changing in every `tofu apply`, but things have gotten complicated. You now have to sift through a massive wall of plan text to confirm that your intended tag update on a resource doesn’t bring down production. You’re seeing irrelevant timestamp updates, changes introduced out-of-band from colleagues in the AWS console, and more. Maybe it’s faster (and safer) to just go ahead and make the update out-of-band yourself instead of dealing with this monstrosity, exacerbating the issue. This is the scenario that platform engineers find themselves in when they’re struggling to deal with a Terralith. This isn’t a hypothetical scenario, it’s daily life for teams that take what once worked perfectly fine at smaller scales, and incrementally added more and more complexity and technical debt until they find themselves in the position where they can no effectively longer use the Infrastructure as Code (IaC) that served them so well in the past. This is a comprehensive, hands-on guide that will demonstrate how you can naturally find yourself managing a Terralith, and how you can get yourself out, using Terragrunt. Along the way, you’ll learn skills like: * Strategies for effectively organizing your IaC for maximum productivity and safety. * Principles of state manipulation, and a look under the hood as to how OpenTofu/Terraform store state. * Modern best practices for authoring scalable and reliable Terragrunt configuration. The guide will do this by having you: 1. Set up a local development environment for managing IaC. 2. Build a fun toy project, and provision it in a real AWS account. 3. Expand that toy project until you can start to see the impact of managing infrastructure following a Terralith architecture pattern. 4. Break down that Terralith to gain improvements in scalability and reliability. 5. Add Terragrunt to improve your ability to orchestrate your IaC. 6. Leverage more of Terragrunt to further improve your DevEx with IaC. This guide will not assume a significant amount of technical skill with Terragrunt, OpenTofu, AWS or NodeJS, but you will use these tools along the way. The guide will gently guide you through their usage, focusing on teaching you lessons as they pertain to IaC. In the next step of this guide, we’ll make sure you have all of these tools installed (and that you’re signed up for an AWS account). The guide will assume that you’re comfortable using a terminal, and that you have access to a Unix-like environment, either by using a Linux/macOS workstation, or by using Windows Subsystem for Linux (WSL) on Windows. It will also assume that you’re OK with not worrying about certain technical details like how the NodeJS application that you deploy as part of this guide works, as some of those technical details will be glossed over in the interest of focus on the technical details that are relevant in this guide. While not a requirement, it would be good to have a basic understanding of how Git works, so that you can commit updates to your copy of the project as you go along. If you get lost or confused at any point, ask for help in the [Terragrunt Discord](/community/invite)! There are plenty of passionate Terragrunt community members that are more than happy to help. # Overview > Overview of the Terralith to Terragrunt guide To demonstrate the journey from a Terralith to a scalable Terragrunt setup, we will build and deploy a complete, real-world application early on in this guide, then spend the rest of the guide refactoring the IaC that manages the infrastructure hosting it. The architecture for our sample project is a simple serverless web application hosted in AWS, which consists of three main components: 1. A [Lambda](https://aws.amazon.com/lambda/)-backed website. 2. An [S3 bucket](https://aws.amazon.com/s3/) to store static assets. 3. A [DynamoDB table](https://aws.amazon.com/dynamodb/) to store metadata on those assets. This application will allow users to view and vote on their favorite AI-generated images of cats. We’ve intentionally chosen these AWS serverless offerings as they are cost-effective and should be very cheap, if not free, for anyone following along with a new AWS account. ## What You’ll Need [Section titled “What You’ll Need”](#what-youll-need) To provision the application we build as part of this guide, you will need an AWS account, and permissions to provision resources within it. If you don’t have one, you can follow the official [instructions to sign up](https://signin.aws.amazon.com/signup?request_type=register) for one for free. To manage the development dependencies for this project, this guide uses [mise](https://mise.jdx.dev/), a tool that helps manage project-specific runtimes and tools. You are welcome to install the required tools manually, but using `mise` (or another tool manager) is recommended when working with IaC, as reproducibility is paramount for ensuring that you can work effectively with colleagues (including future you) on shared infrastructure. If you are happy to install development dependencies with `mise`, you can install it using the official [Mise](https://mise.jdx.dev/getting-started.html) installation guide. If you would like to manually install all the development dependencies for this guide, you can install them here: * [Terragrunt](https://docs.terragrunt.com/getting-started/install/) * [OpenTofu](https://opentofu.org/docs/intro/install/) * [NodeJS](https://nodejs.org/en/download) * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) We will build this project from the ground up, but if you get lost at any point or want to skip ahead, you can find the completed project (as of each step) [here](https://github.com/gruntwork-io/terragrunt/tree/main/docs/src/fixtures/terralith-to-terragrunt). Note that the content shown in code fences in this project will always be displayed in totality, so you can either copy them directly into the filename that’s labeled at the top of the code fence for a file, or run the command directly in your terminal for commands. If a command starts with a `$`, the intent of the code fence is to demonstrate expected output, so you aren’t expected to copy and paste it directly into your terminal. # Setup > Setup of the Terralith to Terragrunt guide During the setup phase, you’re going to: * Setup a Git repository where this project will live. * Install all required dependencies (if you haven’t already). * Set up the NodeJS application that you’re going to deploy to the cloud. * Acquire the assets used for the application. Let’s get building! ## Create the Git repository [Section titled “Create the Git repository”](#create-the-git-repository) Create a new Git project where we’re going to store the IaC for our live infrastructure. ```bash mkdir terralith-to-terragrunt cd terralith-to-terragrunt git init ``` ## Install dependencies with `mise` [Section titled “Install dependencies with mise”](#install-dependencies-with-mise) (Assuming you are using it) Use `mise` to download, install and pin the version of tools you’re going to use in this project. ```bash mise use terragrunt@0.95.0 mise use opentofu@1.11.1 mise use aws@2.27.63 mise use node@22.17.1 ``` You should now have a local `mise.toml` file that looks like the following with all the tools pinned that you need. mise.toml ```toml [tools] aws = "2.27.63" node = "22.17.1" opentofu = "1.10.3" terragrunt = "0.83.2" ``` ## Setting up the app [Section titled “Setting up the app”](#setting-up-the-app) Now that you have the tools installed that are going to be used for this project, you’ll want to setup the application we’re going to be managing throughout the project. It’s not a very interesting application (it was vibe coded pretty quickly), and the details of how it works aren’t that important to the topic of this blog post. Corners were also cut when designing the application to minimize the resources you have to provision, so don’t design any of your applications based on what you see there. ### Create the application directory structure [Section titled “Create the application directory structure”](#create-the-application-directory-structure) First, create the application directory structure: ```bash mkdir -p app/best-cat cd app/best-cat ``` Next, copy the application files into the new directory you just created. * package.json ```json { "name": "best-cat", "version": "1.0.0", "description": "Vote for the best cat", "main": "index.js", "type": "module", "scripts": { "package": "zip -r ../../dist/best-cat.zip . -x '*.git*' 'node_modules/.cache/*'" }, "dependencies": { "@aws-sdk/client-s3": "^3.993.0", "@aws-sdk/client-dynamodb": "^3.993.0", "@aws-sdk/lib-dynamodb": "^3.993.0", "@aws-sdk/s3-request-presigner": "^3.993.0", "handlebars": "^4.7.9" }, "engines": { "node": ">=22.0.0" } } ``` * index.js ```javascript import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, GetCommand, UpdateCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import Handlebars from 'handlebars'; const s3Client = new S3Client({ maxAttempts: 3, requestHandler: { keepAlive: true } }); const dynamoClient = new DynamoDBClient({ maxAttempts: 3, requestHandler: { keepAlive: true } }); const dynamodb = DynamoDBDocumentClient.from(dynamoClient); // Get the directory path for ES modules const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Load static files const templateHtml = readFileSync(join(__dirname, 'template.html'), 'utf8'); const stylesCss = readFileSync(join(__dirname, 'styles.css'), 'utf8'); const scriptJs = readFileSync(join(__dirname, 'script.js'), 'utf8'); // Compile Handlebars template const template = Handlebars.compile(templateHtml); // Server-side cache for presigned URLs const presignedUrlCache = new Map(); // Server-side cache for S3 list response const s3ListCache = { data: null, lastUpdated: 0, ttl: 10 * 1000 // 10 seconds }; // Cache cleanup interval (every 5 minutes) const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // Initialize cache cleanup setInterval(cleanupExpiredCache, CACHE_CLEANUP_INTERVAL); // Function to clean up expired cache entries function cleanupExpiredCache() { const now = Date.now(); // Clean up presigned URL cache for (const [key, cacheEntry] of presignedUrlCache.entries()) { if (now > cacheEntry.expiresAt) { presignedUrlCache.delete(key); } } // Clean up S3 list cache if expired if (s3ListCache.data && (now - s3ListCache.lastUpdated) > s3ListCache.ttl) { s3ListCache.data = null; s3ListCache.lastUpdated = 0; } } // Function to get or generate presigned URL with caching async function getCachedPresignedUrl(bucketName, imageKey) { const cacheKey = `${bucketName}:${imageKey}`; const now = Date.now(); // Check if we have a valid cached URL const cached = presignedUrlCache.get(cacheKey); if (cached && now < cached.expiresAt) { return cached.url; } // Generate new presigned URL const getObjectCommand = new GetObjectCommand({ Bucket: bucketName, Key: imageKey }); const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 3600 }); // Cache the URL with expiration presignedUrlCache.set(cacheKey, { url: presignedUrl, expiresAt: now + (3600 * 1000) // 1 hour from now }); return presignedUrl; } // Function to get cached S3 list data async function getCachedS3List(bucketName) { const now = Date.now(); // Check if we have valid cached data if (s3ListCache.data && (now - s3ListCache.lastUpdated) < s3ListCache.ttl) { console.log('Using cached S3 list data'); return s3ListCache.data; } // Fetch fresh data from S3 console.log('Fetching fresh S3 list data'); const listCommand = new ListObjectsV2Command({ Bucket: bucketName, MaxKeys: 100 }); const listResponse = await s3Client.send(listCommand); // Cache the response if we got data if (listResponse.Contents && listResponse.Contents.length > 0) { s3ListCache.data = listResponse; s3ListCache.lastUpdated = now; console.log(`Cached S3 list with ${listResponse.Contents.length} objects`); } return listResponse; } export async function handler(event) { const bucketName = process.env.S3_BUCKET_NAME; const tableName = process.env.DYNAMODB_TABLE_NAME; try { // Parse the request - Lambda function URLs have a different event structure const method = event.requestContext?.http?.method || event.httpMethod; const path = event.rawPath || event.path || '/'; console.log('Request:', { method, path, event }); // Serve static files if (method === 'GET' && path === '/styles.css') { return { statusCode: 200, headers: { 'Content-Type': 'text/css', 'Cache-Control': 'public, max-age=3600' }, body: stylesCss }; } if (method === 'GET' && path === '/script.js') { return { statusCode: 200, headers: { 'Content-Type': 'application/javascript', 'Cache-Control': 'public, max-age=3600' }, body: scriptJs }; } // Serve images with caching headers if (method === 'GET' && path.startsWith('/image/')) { const imageKey = path.substring(7); // Remove '/image/' prefix try { const presignedUrl = await getCachedPresignedUrl(bucketName, imageKey); return { statusCode: 302, // Redirect to presigned URL headers: { 'Location': presignedUrl, 'Cache-Control': 'public, max-age=3600, immutable' } }; } catch (error) { console.error('Error generating presigned URL:', error); return { statusCode: 404, headers: { 'Content-Type': 'text/html' }, body: '

Image not found

' }; } } // Main page - serve HTML if (method === 'GET' && (path === '/' || path === '')) { // Get all images from S3 (with caching) const listResponse = await getCachedS3List(bucketName); // Get all vote counts from DynamoDB const scanCommand = new ScanCommand({ TableName: tableName }); const votesResponse = await dynamodb.send(scanCommand); const votesMap = {}; if (votesResponse.Items) { votesResponse.Items.forEach(item => { votesMap[item.image_id] = item.votes || 0; }); } // Filter and prepare image data first const imageObjects = (listResponse.Contents || []).filter(obj => obj.Key && obj.Key.match(/\.(jpg|jpeg|png|gif|webp)$/i) ); // Generate presigned URLs in parallel for better performance const imagesWithUrls = await Promise.all( imageObjects.map(async (obj) => { const presignedUrl = await getCachedPresignedUrl(bucketName, obj.Key); return { key: obj.Key, keyId: obj.Key.replace(/[^a-zA-Z0-9]/g, '-'), url: presignedUrl, votes: votesMap[obj.Key] || 0 }; }) ); // Sort images by vote count (highest votes first) imagesWithUrls.sort((a, b) => b.votes - a.votes); // Render the template with data const html = template({ images: imagesWithUrls }); return { statusCode: 200, headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=60, s-maxage=300' }, body: html }; } // API endpoint for voting if (method === 'POST' && path.startsWith('/vote/')) { const parts = path.split('/'); const imageId = parts[2]; const voteType = parts[3]; // 'up' or 'down' const voteIncrement = voteType === 'up' ? 1 : -1; // Update vote count in DynamoDB const updateCommand = new UpdateCommand({ TableName: tableName, Key: { image_id: imageId }, UpdateExpression: 'SET votes = if_not_exists(votes, :zero) + :inc', ExpressionAttributeValues: { ':inc': voteIncrement, ':zero': 0 }, ReturnValues: 'UPDATED_NEW' }); const result = await dynamodb.send(updateCommand); return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: 'Vote recorded successfully', image_id: imageId, new_vote_count: result.Attributes.votes }) }; } // API endpoint to get current vote counts if (method === 'GET' && path === '/api/votes') { const scanCommand = new ScanCommand({ TableName: tableName }); const votes = await dynamodb.send(scanCommand); return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ votes: votes.Items || [] }) }; } // For any other GET request, serve the main page (helpful for debugging) if (method === 'GET') { console.log('Serving main page for path:', path); // Get all images from S3 (with caching) const listResponse = await getCachedS3List(bucketName); // Get all vote counts from DynamoDB const scanCommand = new ScanCommand({ TableName: tableName }); const votesResponse = await dynamodb.send(scanCommand); const votesMap = {}; if (votesResponse.Items) { votesResponse.Items.forEach(item => { votesMap[item.image_id] = item.votes || 0; }); } // Filter and prepare image data efficiently const imageObjects = (listResponse.Contents || []).filter(obj => obj.Key && obj.Key.match(/\.(jpg|jpeg|png|gif|webp)$/i) ); const imagesWithUrls = imageObjects.map(obj => ({ key: obj.Key, keyId: obj.Key.replace(/[^a-zA-Z0-9]/g, '-'), votes: votesMap[obj.Key] || 0 })); // Sort images by vote count (highest votes first) imagesWithUrls.sort((a, b) => b.votes - a.votes); // Render the template with data const html = template({ images: imagesWithUrls }); return { statusCode: 200, headers: { 'Content-Type': 'text/html', 'Cache-Control': 'public, max-age=60, s-maxage=300' }, body: html }; } // Default response for unknown endpoints return { statusCode: 404, headers: { 'Content-Type': 'text/html' }, body: ` 404 - Not Found

404 - Page Not Found

The requested page could not be found.

Method: ${method}, Path: ${path}

Go back to the main page ` }; } catch (error) { console.error('Error:', error); return { statusCode: 500, headers: { 'Content-Type': 'text/html' }, body: ` 500 - Server Error

500 - Internal Server Error

An error occurred while processing your request.

Go back to the main page ` }; } } ``` * template.html ```html Best Cat Voting

🐱 Vote for the Best Cat! 🐱

{{#if images.length}} {{#each images}}
Cat image
{{this.votes}}
{{/each}} {{else}}
No images found. Upload some cat pictures to get started!
{{/if}}
``` * styles.css ```css * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; margin: 0; padding: 0; display: flex; align-items: center; justify-content: center; } .container { max-width: 1200px; width: 100%; padding: 20px; text-align: center; } h1 { text-align: center; color: white; margin-bottom: 40px; font-size: 3rem; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); font-weight: 700; letter-spacing: 1px; } .images-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 25px; margin-top: 30px; justify-items: center; } .image-card { background: white; border-radius: 20px; overflow: hidden; box-shadow: 0 15px 35px rgba(0,0,0,0.15); transition: all 0.3s ease; width: 100%; max-width: 350px; } .image-card:hover { transform: translateY(-8px); box-shadow: 0 20px 50px rgba(0,0,0,0.25); } .image-container { position: relative; width: 100%; height: 250px; overflow: hidden; } .image-container img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; } .image-card:hover .image-container img { transform: scale(1.05); } .voting-section { padding: 20px; text-align: center; } .vote-count { font-size: 1.8rem; font-weight: bold; color: #333; margin-bottom: 20px; background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .vote-buttons { display: flex; justify-content: center; gap: 10px; } .vote-btn { background: none; border: 2px solid #e0e0e0; border-radius: 50%; width: 55px; height: 55px; font-size: 1.6rem; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; } .vote-btn:hover { border-color: #667eea; background: linear-gradient(135deg, #667eea, #764ba2); color: white; transform: scale(1.1); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); } .vote-btn:active { transform: scale(0.95); } .vote-btn.up:hover { border-color: #4CAF50; background: linear-gradient(135deg, #4CAF50, #45a049); box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); } .vote-btn.down:hover { border-color: #f44336; background: linear-gradient(135deg, #f44336, #d32f2f); box-shadow: 0 5px 15px rgba(244, 67, 54, 0.4); } .vote-btn:disabled { cursor: not-allowed; opacity: 0.6; transform: none !important; } .vote-btn:disabled:hover { border-color: #e0e0e0; background: none; box-shadow: none; transform: none; } .loading { text-align: center; color: white; font-size: 1.2rem; margin: 50px 0; } .no-images { text-align: center; color: white; font-size: 1.4rem; margin: 60px auto; line-height: 1.6; background: rgba(255, 255, 255, 0.1); padding: 40px; border-radius: 15px; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); max-width: 500px; width: 100%; grid-column: 1 / -1; justify-self: center; } @media (max-width: 768px) { .images-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; } h1 { font-size: 2.2rem; margin-bottom: 30px; } .vote-btn { width: 50px; height: 50px; font-size: 1.4rem; } .no-images { font-size: 1.2rem; padding: 30px; margin: 40px 0; } } ``` * script.js ```javascript // Global variable to store the base URL for API calls let baseUrl = ''; // Initialize the application document.addEventListener('DOMContentLoaded', function() { // Set the base URL from the current location baseUrl = window.location.origin; // Add click effects to vote buttons const voteButtons = document.querySelectorAll('.vote-btn'); voteButtons.forEach(button => { button.addEventListener('click', function() { // Create a subtle click effect this.style.transform = 'scale(0.9)'; setTimeout(() => { this.style.transform = ''; }, 150); }); }); }); // Function to handle voting asynchronously async function vote(imageKey, voteType) { const voteElement = document.getElementById(`votes-${imageKey.replace(/[^a-zA-Z0-9]/g, '-')}`); const voteButtons = document.querySelectorAll(`[data-image-key="${imageKey}"] .vote-btn`); if (!voteElement) return; // Get current vote count const currentVotes = parseInt(voteElement.textContent) || 0; const voteIncrement = voteType === 'up' ? 1 : -1; // Optimistically update the UI immediately const newVoteCount = currentVotes + voteIncrement; voteElement.textContent = newVoteCount; // Add immediate visual feedback voteElement.style.transform = 'scale(1.2)'; voteElement.style.color = voteType === 'up' ? '#4CAF50' : '#f44336'; // Disable vote buttons to prevent double-clicking voteButtons.forEach(btn => { btn.disabled = true; btn.style.opacity = '0.6'; }); // Send vote request asynchronously const votePromise = fetch(`${baseUrl}/vote/${imageKey}/${voteType}`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); try { const response = await votePromise; if (response.ok) { const result = await response.json(); // Update with actual server response voteElement.textContent = result.new_vote_count; // Success animation voteElement.style.transform = 'scale(1.1)'; setTimeout(() => { voteElement.style.transform = 'scale(1)'; voteElement.style.color = ''; }, 300); } else { // Revert optimistic update on error voteElement.textContent = currentVotes; console.error('Vote failed:', response.statusText); // Show error feedback voteElement.style.transform = 'scale(1.1)'; voteElement.style.color = '#f44336'; setTimeout(() => { voteElement.style.transform = 'scale(1)'; voteElement.style.color = ''; }, 300); // Show user-friendly error message showNotification('Vote failed. Please try again.', 'error'); } } catch (error) { // Revert optimistic update on network error voteElement.textContent = currentVotes; console.error('Error voting:', error); // Show error feedback voteElement.style.transform = 'scale(1.1)'; voteElement.style.color = '#f44336'; setTimeout(() => { voteElement.style.transform = 'scale(1)'; voteElement.style.color = ''; }, 300); // Show user-friendly error message showNotification('Network error. Please check your connection.', 'error'); } finally { // Re-enable vote buttons voteButtons.forEach(btn => { btn.disabled = false; btn.style.opacity = '1'; }); } } // Function to show notifications function showNotification(message, type = 'info') { // Remove existing notifications const existingNotification = document.querySelector('.notification'); if (existingNotification) { existingNotification.remove(); } // Create notification element const notification = document.createElement('div'); notification.className = `notification notification-${type}`; notification.textContent = message; // Add styles notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; font-weight: 500; z-index: 1000; transform: translateX(100%); transition: transform 0.3s ease; max-width: 300px; word-wrap: break-word; `; // Set background color based on type if (type === 'error') { notification.style.backgroundColor = '#f44336'; } else if (type === 'success') { notification.style.backgroundColor = '#4CAF50'; } else { notification.style.backgroundColor = '#2196F3'; } // Add to page document.body.appendChild(notification); // Animate in setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 100); // Auto-remove after 3 seconds setTimeout(() => { notification.style.transform = 'translateX(100%)'; setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 300); }, 3000); } ``` * package-lock.json ```json { "name": "best-cat", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "best-cat", "version": "1.0.0", "dependencies": { "@aws-sdk/client-dynamodb": "^3.993.0", "@aws-sdk/client-s3": "^3.993.0", "@aws-sdk/lib-dynamodb": "^3.993.0", "@aws-sdk/s3-request-presigner": "^3.993.0", "handlebars": "^4.7.9" }, "engines": { "node": ">=22.0.0" } }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@aws-crypto/crc32c": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "node_modules/@aws-crypto/sha1-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" } }, "node_modules/@aws-crypto/util": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.993.0.tgz", "integrity": "sha512-Yxal65s8pKK+f+C5vFrtBKhe5dN57ITq63NpitQ0NKBoNNRfmseIP4UGaa6/KvgAkF20p/EgC+CXg7pPix+1cQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/dynamodb-codec": "^3.972.12", "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-s3": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.993.0.tgz", "integrity": "sha512-0slCxdbo9O3rfzqD7/PsBOrZ6vcwFzPAvGeUu5NZApI5WyjEfMLLi2T9QW8R9N9TQeUfiUQiHkg/NV0LPS61/g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", "@aws-sdk/middleware-expect-continue": "^3.972.3", "@aws-sdk/middleware-flexible-checksums": "^3.972.9", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-location-constraint": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-sdk-s3": "^3.972.11", "@aws-sdk/middleware-ssec": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/signature-v4-multi-region": "3.993.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-blob-browser": "^4.2.9", "@smithy/hash-node": "^4.2.8", "@smithy/hash-stream-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-sso": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.993.0.tgz", "integrity": "sha512-VLUN+wIeNX24fg12SCbzTUBnBENlL014yMKZvRhPkcn4wHR6LKgNrjsG3fZ03Xs0XoKaGtNFi1VVrq666sGBoQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/core": { "version": "3.973.11", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.11.tgz", "integrity": "sha512-wdQ8vrvHkKIV7yNUKXyjPWKCdYEUrZTHJ8Ojd5uJxXp9vqPCkUR1dpi1NtOLcrDgueJH7MUH5lQZxshjFPSbDA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.5", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/crc64-nvme": { "version": "3.972.0", "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.9.tgz", "integrity": "sha512-ZptrOwQynfupubvcngLkbdIq/aXvl/czdpEG8XJ8mN8Nb19BR0jaK0bR+tfuMU36Ez9q4xv7GGkHFqEEP2hUUQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-http": { "version": "3.972.11", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.11.tgz", "integrity": "sha512-hECWoOoH386bGr89NQc9vA/abkGf5TJrMREt+lhNcnSNmoBS04fK7vc3LrJBSQAUGGVj0Tz3f4dHB3w5veovig==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.9.tgz", "integrity": "sha512-zr1csEu9n4eDiHMTYJabX1mDGuGLgjgUnNckIivvk43DocJC9/f6DefFrnUPZXE+GHtbW50YuXb+JIxKykU74A==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-login": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.9.tgz", "integrity": "sha512-m4RIpVgZChv0vWS/HKChg1xLgZPpx8Z+ly9Fv7FwA8SOfuC6I3htcSaBz2Ch4bneRIiBUhwP4ziUo0UZgtJStQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.972.10", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.10.tgz", "integrity": "sha512-70nCESlvnzjo4LjJ8By8MYIiBogkYPSXl3WmMZfH9RZcB/Nt9qVWbFpYj6Fk1vLa4Vk8qagFVeXgxdieMxG1QA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-process": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.9.tgz", "integrity": "sha512-gOWl0Fe2gETj5Bk151+LYKpeGi2lBDLNu+NMNpHRlIrKHdBmVun8/AalwMK8ci4uRfG5a3/+zvZBMpuen1SZ0A==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-sso": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.9.tgz", "integrity": "sha512-ey7S686foGTArvFhi3ifQXmgptKYvLSGE2250BAQceMSXZddz7sUSNERGJT2S7u5KIe/kgugxrt01hntXVln6w==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/client-sso": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/token-providers": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.9.tgz", "integrity": "sha512-8LnfS76nHXoEc9aRRiMMpxZxJeDG0yusdyo3NvPhCgESmBUgpMa4luhGbClW5NoX/qRcGxxM6Z/esqANSNMTow==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/dynamodb-codec": { "version": "3.972.12", "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.12.tgz", "integrity": "sha512-hX5lIhIACrmYPxW3sKoHxKJO87SPlnYBF8ztrQwm74tJEoX8eFo/iVjiEP56zkVvwOtMMqblNgmd7Jr0zZcbGA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@smithy/core": "^3.23.2", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/endpoint-cache": { "version": "3.972.2", "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.2.tgz", "integrity": "sha512-3L7mwqSLJ6ouZZKtCntoNF0HTYDNs1FDQqkGjoPWXcv1p0gnLotaDmLq1rIDqfu4ucOit0Re3ioLyYDUTpSroA==", "license": "Apache-2.0", "dependencies": { "mnemonist": "0.38.3", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/lib-dynamodb": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.993.0.tgz", "integrity": "sha512-m8C06zb/EK+8cVq3Ji9J+yz60dOERIlzCzE3cf9YsvQZaHBTqY5DCNh+rWZzO7FGBYqENgsyh9Z2M1kuW1aAbQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/util-dynamodb": "3.993.0", "@smithy/core": "^3.23.2", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" }, "peerDependencies": { "@aws-sdk/client-dynamodb": "^3.993.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz", "integrity": "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-endpoint-discovery": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.3.tgz", "integrity": "sha512-xAxA8/TOygQmMrzcw9CrlpTHCGWSG/lvzrHCySfSZpDN4/yVSfXO+gUwW9WxeskBmuv9IIFATOVpzc9EzfTZ0Q==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/endpoint-cache": "^3.972.2", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-expect-continue": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz", "integrity": "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.9.tgz", "integrity": "sha512-E663+r/UQpvF3aJkD40p5ZANVQFsUcbE39jifMtN7wc0t1M0+2gJJp3i75R49aY9OiSX5lfVyPUNjN/BNRCCZA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/crc64-nvme": "3.972.0", "@aws-sdk/types": "^3.973.1", "@smithy/is-array-buffer": "^4.2.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-location-constraint": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz", "integrity": "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-sdk-s3": { "version": "3.972.11", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.11.tgz", "integrity": "sha512-Qr0T7ZQTRMOuR6ahxEoJR1thPVovfWrKB2a6KBGR+a8/ELrFodrgHwhq50n+5VMaGuLtGhHiISU3XGsZmtmVXQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-arn-parser": "^3.972.2", "@smithy/core": "^3.23.2", "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-ssec": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz", "integrity": "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.972.11", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.11.tgz", "integrity": "sha512-R8CvPsPHXwzIHCAza+bllY6PrctEk4lYq/SkHJz9NLoBHCcKQrbOcsfXxO6xmipSbUNIbNIUhH0lBsJGgsRdiw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@smithy/core": "^3.23.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/nested-clients": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.993.0.tgz", "integrity": "sha512-iOq86f2H67924kQUIPOAvlmMaOAvOLoDOIb66I2YqSUpMYB6ufiuJW3RlREgskxv86S5qKzMnfy/X6CqMjK6XQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/region-config-resolver": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/s3-request-presigner": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.993.0.tgz", "integrity": "sha512-HM6CtVNvQb0w7FlIC4wjgTV0bE6QzVG8RgmuoNdSpsE9V5WGQTJJLf6JXSANlrK1+CTL1Di2fSyki0HUwkllRg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.993.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-format-url": "^3.972.3", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.993.0.tgz", "integrity": "sha512-6l20k27TJdqTozJOm+s20/1XDey3aj+yaeIdbtqXuYNhQiWHajvYThcI1sHx2I1W4NelZTOmYEF+dj1mya01eg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.11", "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/token-providers": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.993.0.tgz", "integrity": "sha512-+35g4c+8r7sB9Sjp1KPdM8qxGn6B/shBjJtEUN4e+Edw9UEQlZKIzioOGu3UAbyE0a/s450LdLZr4wbJChtmww==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "^3.973.11", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/types": { "version": "3.973.1", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-arn-parser": { "version": "3.972.2", "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-dynamodb": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.993.0.tgz", "integrity": "sha512-bS+7s/r9RfzHHi3cwrLX1wLz1M6g3fr4bqW3gNDSjkOE5CLHsWmtBSJmcET3ex+KO8u27F73S/r6SeogusWNow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" }, "peerDependencies": { "@aws-sdk/client-dynamodb": "^3.993.0" } }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.993.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.993.0.tgz", "integrity": "sha512-j6vioBeRZ4eHX4SWGvGPpwGg/xSOcK7f1GL0VM+rdf3ZFTIsUEhCFmD78B+5r2PgztcECSzEfvHQX01k8dPQPw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-format-url": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.3.tgz", "integrity": "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.804.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { "version": "3.972.9", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.9.tgz", "integrity": "sha512-JNswdsLdQemxqaSIBL2HRhsHPUBBziAgoi5RQv6/9avmE5g5RSdt1hWr3mHJ7OxqRYf+KeB11ExWbiqfrnoeaA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "peerDependenciesMeta": { "aws-crt": { "optional": true } } }, "node_modules/@aws-sdk/xml-builder": { "version": "3.972.19", "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.19.tgz", "integrity": "sha512-Cw8IOMdBUEIl8ZlhRC3Dc/E64D5B5/8JhV6vhPLiPfJwcRC84S6F8aBOIi/N4vR9ZyA4I5Cc0Ateb/9EHaJXeQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", "fast-xml-parser": "5.7.1", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" } }, "node_modules/@nodable/entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/nodable" } ], "license": "MIT" }, "node_modules/@smithy/abort-controller": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/chunked-blob-reader": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/chunked-blob-reader-native": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/config-resolver": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/core": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.2.tgz", "integrity": "sha512-HaaH4VbGie4t0+9nY3tNBRSxVTr96wzIqexUa6C2qx3MPePAuz7lIxPxYtt1Wc//SPfJLNoZJzfdt0B6ksj2jA==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/credential-provider-imds": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/eventstream-codec": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/eventstream-serde-browser": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/eventstream-serde-config-resolver": { "version": "4.3.8", "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/eventstream-serde-node": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/eventstream-serde-universal": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", "license": "Apache-2.0", "dependencies": { "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/fetch-http-handler": { "version": "5.3.9", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/hash-blob-browser": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz", "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/hash-node": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/hash-stream-node": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz", "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/invalid-dependency": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/is-array-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/md5-js": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-endpoint": { "version": "4.4.16", "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.16.tgz", "integrity": "sha512-L5GICFCSsNhbJ5JSKeWFGFy16Q2OhoBizb3X2DrxaJwXSEujVvjG9Jt386dpQn2t7jINglQl0b4K/Su69BdbMA==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-retry": { "version": "4.4.33", "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.33.tgz", "integrity": "sha512-jLqZOdJhtIL4lnA9hXnAG6GgnJlo1sD3FqsTxm9wSfjviqgWesY/TMBVnT84yr4O0Vfe0jWoXlfFbzsBVph3WA==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-serde": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/middleware-stack": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/node-config-provider": { "version": "4.3.8", "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/node-http-handler": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz", "integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/property-provider": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/protocol-http": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/querystring-builder": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/querystring-parser": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/service-error-classification": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/signature-v4": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/smithy-client": { "version": "4.11.5", "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.5.tgz", "integrity": "sha512-xixwBRqoeP2IUgcAl3U9dvJXc+qJum4lzo3maaJxifsZxKUYLfVfCXvhT4/jD01sRrHg5zjd1cw2Zmjr4/SuKQ==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.2", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/types": { "version": "4.14.1", "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/url-parser": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", "license": "Apache-2.0", "dependencies": { "@smithy/querystring-parser": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-base64": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-body-length-browser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-body-length-node": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-buffer-from": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-config-provider": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-defaults-mode-browser": { "version": "4.3.32", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.32.tgz", "integrity": "sha512-092sjYfFMQ/iaPH798LY/OJFBcYu0sSK34Oy9vdixhsU36zlZu8OcYjF3TD4e2ARupyK7xaxPXl+T0VIJTEkkg==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-defaults-mode-node": { "version": "4.2.35", "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.35.tgz", "integrity": "sha512-miz/ggz87M8VuM29y7jJZMYkn7+IErM5p5UgKIf8OtqVs/h2bXr1Bt3uTsREsI/4nK8a0PQERbAPsVPVNIsG7Q==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-endpoints": { "version": "3.2.8", "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-hex-encoding": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-middleware": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-retry": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", "license": "Apache-2.0", "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-stream": { "version": "4.5.12", "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz", "integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-uri-escape": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-utf8": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/util-waiter": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/uuid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/fast-xml-builder": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], "license": "MIT", "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz", "integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], "license": "MIT", "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.5", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "node_modules/handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "bin": { "handlebars": "bin/handlebars" }, "engines": { "node": ">=0.4.7" }, "optionalDependencies": { "uglify-js": "^3.1.4" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mnemonist": { "version": "0.38.3", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", "license": "MIT", "dependencies": { "obliterator": "^1.6.1" } }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, "node_modules/obliterator": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", "license": "MIT" }, "node_modules/path-expression-matcher": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/strnum": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" }, "engines": { "node": ">=0.8.0" } }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, "node_modules/xml-naming": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], "license": "MIT", "engines": { "node": ">=16.0.0" } } } } ``` You should end up with an `app/best-cat` directory that looks like this: * app * best-cat * index.js * package-lock.json * package.json * script.js * styles.css * template.html ## Packaging the app [Section titled “Packaging the app”](#packaging-the-app) Once you have the app stored in `app/best-cat`, you’ll want to create the `dist` directory, then package the application for delivery to a lambda function. ```bash mkdir dist cd app/best-cat npm i npm run package ``` I also recommend adding the following `.gitignore` file to your `dist` directory so you don’t accidentally commit any other content in this directory to your repository: dist/.gitignore ```bash * !.gitignore ``` ## Generating assets [Section titled “Generating assets”](#generating-assets) You’ll also want some assets to use in this project to make it more fun. I generated a bunch of cat pictures using [Gemini](https://gemini.google.com/), but feel free to use stock photos or something else to generate the assets. I would recommend that you place the images in the same location I did (`dist/static`), so that the convenience scripts I wrote work out of the box without modification. This is what my `dist` directory looks like after following these steps: * dist * best-cat.zip * static * 01-cat.png * 02-cat.png * 03-cat.png * 04-cat.png * 05-cat.png * 06-cat.png * 07-cat.png * 08-cat.png * 09-cat.png * 10-cat.png Our end goal is to host a site that looks like this in AWS using these artifacts: ![terralith-to-terragrunt-app-goal](/_vercel/image?url=_astro%2Fapp-goal.UG71XwFU.png\&w=1920\&q=100) # Step 1: Starting the Terralith > Starting the Terralith During this step we’re going to take the contents of the `dist` directory, and deploy the application in AWS using OpenTofu. The content that we’re going to create here is not intended to be a best-practices IaC setup, but rather what one might do if they were to naively put together the IaC for this application from scratch without knowledge of best practices, or planning for future modifications. This will be reiterated at the end of this step, but note that this statement is not intended to be judgemental. There are perfectly valid reasons for building an MVP quickly without worrying about architecting the most optimally scaling IaC setup from day 1, and everyone has to start somewhere in their IaC journey. As you progress through this guide, you’ll be exposed to the challenges that might arise if you design your IaC as prescribed at each step, and this guide will do its best to highlight those trade-offs. The goal is for you to be able to make judgements around the IaC design that is best suited to your infrastructure management. ## Tutorial [Section titled “Tutorial”](#tutorial) To start, create the `live` directory where you’re going to be provisioning some infrastructure. Creating a dedicated `live` directory/repository separate from reusable infrastructure patterns is an established recommended best practice from Gruntwork that will become important in later iterations. ```bash mkdir live ``` To get started, we’ll want to define our [providers](https://opentofu.org/docs/language/providers/), which is how OpenTofu will actually effectuate infrastructure in AWS. live/providers.tf ```hcl provider "aws" { region = var.aws_region } ``` Don’t worry about that `var.aws_region` bit there. We’ll define it later. When we do, we’ll be defining the region in which AWS will have resources provisioned. It’s best practice to tell OpenTofu what your version constraints are, so that your IaC is reliably reusable. You’ll pin version `>= 1.10` of OpenTofu here, as you’re going to be using an OpenTofu 1.10+ feature later on, and you’ll pin version `~> 6.0` because any change that makes the `resource` definitions we define here break *should* be in a future major release of the `aws` provider. live/versions.tf ```hcl terraform { required_version = ">= 1.10" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } ``` Now define the resources that are provisioned in this step. First, we’ll add the database that we use to provision resources in this project. For this guide, we’ll be using DynamoDB, which is a fast, NoSQL database offered by AWS. The primary construct in DynamoDB is a table. All DynamoDB tables have a name, and a primary key (also called the hash key). This is what we’ll use to uniquely reference items in the table. In this project, DynamoDB tables will be used to store the metadata about cats and the number of votes they’ve gotten as being the best cat. live/ddb.tf ```hcl resource "aws_dynamodb_table" "asset_metadata" { name = "${var.name}-asset-metadata" billing_mode = "PAY_PER_REQUEST" hash_key = "image_id" attribute { name = "image_id" type = "S" } tags = { Name = "${var.name}-asset-metadata" } } ``` Next, we’ll add our object store. For this guide, we’ll be using S3, which is a cheap, scalable object store provided by AWS. The primary construct of S3 is a bucket. All buckets must have a name that is globally unique, so we’ll want to make sure that the value we select later on for `name` is appropriately unique. live/s3.tf ```hcl resource "aws_s3_bucket" "static_assets" { bucket = "${var.name}-static-assets" force_destroy = var.force_destroy } ``` That `force_destroy` attribute there is important. It determines whether we can destroy the S3 bucket without getting rid of all its contents. You typically want this to be set to `false` in production environments, but it can be convenient to set this to `true` in test/ephemeral environments, where you expect the bucket to be short-lived. In addition to provisioning resources using OpenTofu, you can also lookup data using `data` configuration blocks. These can be useful ways to access frequently needed data in AWS resources, like AWS account IDs. live/data.tf ```hcl data "aws_caller_identity" "current" {} ``` Using that `data` block, let’s provision the IAM role that’s used for our Lambda function. We’ll want it to trust the Lambda service, so that the Lambda function is allowed to assume it, and have permissions to: 1. Get and list the objects in S3 (our cat images). 2. Interact with the DynamoDB table used for storing metadata on our assets (the votes for best cat). 3. Basic permissions required to operate a Lambda function (the ability to log to [CloudWatch Logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html)). live/iam.tf ```hcl resource "aws_iam_role" "lambda_role" { name = "${var.name}-lambda-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } } ] }) } resource "aws_iam_policy" "lambda_s3_read" { name = "${var.name}-lambda-s3-read" description = "Policy for Lambda to read from S3 bucket" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:ListBucket" ] Resource = [ aws_s3_bucket.static_assets.arn, "${aws_s3_bucket.static_assets.arn}/*" ] } ] }) } resource "aws_iam_policy" "lambda_dynamodb" { name = "${var.name}-lambda-dynamodb" description = "Policy for Lambda to read/write to DynamoDB table" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ] Resource = aws_dynamodb_table.asset_metadata.arn } ] }) } resource "aws_iam_policy" "lambda_basic_execution" { name = "${var.name}-lambda-basic-execution" description = "Policy for Lambda basic execution (CloudWatch logs)" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] Resource = "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*" } ] }) } resource "aws_iam_role_policy_attachment" "lambda_s3_read" { role = aws_iam_role.lambda_role.name policy_arn = aws_iam_policy.lambda_s3_read.arn } resource "aws_iam_role_policy_attachment" "lambda_dynamodb" { role = aws_iam_role.lambda_role.name policy_arn = aws_iam_policy.lambda_dynamodb.arn } resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { role = aws_iam_role.lambda_role.name policy_arn = aws_iam_policy.lambda_basic_execution.arn } ``` The final resource you’re going to provision is the Lambda function. Lambda functions are a form of cheap, ephemeral compute that are especially useful for demo guides like this where you might forget to clean up some dummy resources. They won’t cost you anything while they’re not doing anything! live/lambda.tf ```hcl resource "aws_lambda_function" "main" { function_name = "${var.name}-function" filename = var.lambda_zip_file source_code_hash = filebase64sha256(var.lambda_zip_file) role = aws_iam_role.lambda_role.arn handler = var.lambda_handler runtime = var.lambda_runtime timeout = var.lambda_timeout memory_size = var.lambda_memory_size architectures = var.lambda_architectures environment { variables = { S3_BUCKET_NAME = aws_s3_bucket.static_assets.bucket DYNAMODB_TABLE_NAME = aws_dynamodb_table.asset_metadata.name } } depends_on = [ aws_iam_role_policy_attachment.lambda_s3_read, aws_iam_role_policy_attachment.lambda_dynamodb, aws_iam_role_policy_attachment.lambda_basic_execution ] } resource "aws_lambda_function_url" "main" { function_name = aws_lambda_function.main.function_name authorization_type = "NONE" } ``` Now let’s add some variables that we want to specify for this project. You can think of these as the *inputs* that you supply to your generically defined IaC to get infrastructure customized to your needs. As a matter of best practice, we’re going to separate the required variables from the optional ones. live/vars-required.tf ```hcl variable "name" { description = "Name used for all resources" type = string } variable "lambda_zip_file" { description = "Path to the Lambda function zip file" type = string } ``` live/vars-optional.tf ```hcl variable "aws_region" { description = "AWS region for all resources" type = string default = "us-east-1" } variable "lambda_runtime" { description = "Lambda function runtime" type = string default = "nodejs22.x" } variable "lambda_handler" { description = "Lambda function handler" type = string default = "index.handler" } variable "lambda_timeout" { description = "Lambda function timeout in seconds" type = number default = 30 } variable "lambda_memory_size" { description = "Lambda function memory size in MB" type = number default = 128 } variable "lambda_architectures" { description = "Lambda function architectures" type = list(string) default = ["arm64"] } variable "force_destroy" { description = "Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)" type = bool default = false } ``` Also add an `.auto.tfvars` file to define values for these variables automatically. This isn’t strictly required as you’ll be prompted for the values of required variables interactively if you don’t supply them here, but it does make your life easier. Note You’ll want to make sure that `name` is set to something globally unique, as it’ll be used as part of an S3 bucket name, which might conflict with a bucket created by somebody else otherwise (a simple way to decrease your odds of a collision is to use something like the date in your bucket name). live/.auto.tfvars ```hcl # Required: Name used for all resources (must be unique) name = "best-cat-2025-09-24-2359" # Required: Path to your Lambda function zip file lambda_zip_file = "../dist/best-cat.zip" ``` You’ll also want to add some *outputs* so that you can easily interact with the infrastructure you create. live/outputs.tf ```hcl output "lambda_function_url" { description = "URL of the Lambda function" value = aws_lambda_function_url.main.function_url } output "lambda_function_name" { description = "Name of the Lambda function" value = aws_lambda_function.main.function_name } output "s3_bucket_name" { description = "Name of the S3 bucket for static assets" value = aws_s3_bucket.static_assets.bucket } output "s3_bucket_arn" { description = "ARN of the S3 bucket for static assets" value = aws_s3_bucket.static_assets.arn } output "dynamodb_table_name" { description = "Name of the DynamoDB table for asset metadata" value = aws_dynamodb_table.asset_metadata.name } output "dynamodb_table_arn" { description = "ARN of the DynamoDB table for asset metadata" value = aws_dynamodb_table.asset_metadata.arn } output "lambda_role_arn" { description = "ARN of the Lambda execution role" value = aws_iam_role.lambda_role.arn } ``` As a best practice, you’ll want to add a `backend` configuration so that state isn’t stored locally. This is important if you want to collaborate with others in infrastructure management. Note that you’ll likely want to change the name of the bucket you use here, as it also has to be globally unique to avoid conflicts with anyone else. live/backend.tf ```hcl terraform { backend "s3" { bucket = "terragrunt-to-terralith-tfstate-2025-09-24-2359" key = "tofu.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } } ``` Note We’re using the special new ✨ lockfile-based state locking ✨ made available in OpenTofu 1.10! This is especially convenient for this guide, as it saves us from provisioning an additional DynamoDB table to handle state locking. Unfortunately, OpenTofu will not provision this bucket for us automatically, so we will have to provision that manually. ```bash aws s3api create-bucket --bucket 'terragrunt-to-terralith-tfstate-2025-09-24-2359' --region 'us-east-1' aws s3api put-bucket-versioning --bucket 'terragrunt-to-terralith-tfstate-2025-09-24-2359' --versioning-configuration 'Status=Enabled' ``` ## Project Layout Check-in [Section titled “Project Layout Check-in”](#project-layout-check-in) At this stage, you should have a `live` directory that looks like this: * live * backend.tf * data.tf * ddb.tf * iam.tf * lambda.tf * outputs.tf * providers.tf * s3.tf * vars-optional.tf * vars-required.tf * versions.tf ## Applying Updates [Section titled “Applying Updates”](#applying-updates) We can now apply our live infrastructure! ```bash cd live tofu init tofu apply ``` You’ll receive a prompt to approve the apply (type `yes` then enter to continue). Make sure to review the plan thoroughly, then approve it. Assuming everything went well, you’ll see a bunch of outputs at the end of the apply. One of them will be an output that looks like the following: ```text lambda_function_url = "https://somerandomcharacters.lambda-url.us-east-1.on.aws/" ``` Copy that link, and paste it into your browser to see a page like the following: ![app-without-images](/_vercel/image?url=_astro%2Fapp-without-images.DibdLfwK.png\&w=1920\&q=100) Congratulations! You’ve got live infrastructure you built yourself running in AWS! We can see an error that the site doesn’t have any images, and a prompt to upload some cat pictures to get started. To get those pictures uploaded, we’ll want to grab the name of the S3 bucket, and use the AWS CLI to upload the assets. ```bash # (Assuming you're using bash or zsh and you're in the `live` directory). # Grab the bucket name into the `bucket_name` variable. bucket_name="$(tofu output -raw s3_bucket_name)" # Navigate to the root of the git repository. cd "$(git rev-parse --show-toplevel)" # Navigate to the directory where you stored your cat pictures. cd dist/static # Use the AWS CLI to sync the assets to the bucket. aws s3 sync . "s3://${bucket_name}/" ``` If you reload the website, you should be able to see the cat images you uploaded. ![app-with-images](/_vercel/image?url=_astro%2Fapp-goal.UG71XwFU.png\&w=1920\&q=100) ## Wrap Up [Section titled “Wrap Up”](#wrap-up) You’ve successfully built and deployed a complete, serverless web application using OpenTofu! All of your infrastructure including: * An S3 bucket * A DynamoDB table * An IAM role * A Lambda function Are defined and managed within a single root module. This configuration, could be called a “Terralith” or “Megamodule,” but it’s probably not obvious that there’s anything wrong with this setup. This a common and perfectly acceptable starting point for many projects. It’s simple and direct, but as you continue to adjust and refactor this project, its monolithic nature will present challenges in reusability and safe environment management as you scale. In the next step, you’ll begin to address these challenges by refactoring your code into reusable modules. # Step 2: Refactoring > Refactoring One of the most important skills you can learn when managing IaC is learning how to refactor IaC. The code you write directly relates to infrastructure that delivers value to your team and wider organization, so knowing how to safely reorganize your code so that it’s easier to reuse and reason about without incurring risk for the infrastructure you support is invaluable. Some of the most common reasons you might engage in this kind of refactoring includes: * Rewriting bespoke IaC as consumption of a reusable module so that you can repurpose the common IaC in other environments/projects. * Standardizing IaC patterns for consistent application of security/cost best practices. * Abstracting away the implementation details of one or more resources as a module so that you can focus on the higher level abstraction of how that module integrates with the rest of your infrastructure. * Creating a generic module with a well defined API for a component in your infrastructure so that you can easily swap out the module with another module that shares a compatible (or close enough to compatible) API. In this step, we’ll start going down the road of making our infrastructure components modular so that we are well prepared for the next step, when we introduce a secondary environment as a replica of the environment we provisioned in the last step. ## Tutorial [Section titled “Tutorial”](#tutorial) The Gruntwork recommended best practice for creating reusable IaC is to create a dedicated `catalog` directory (or a dedicated `catalog` repository) outside the `live` directory (or `live` repository) where reusable IaC patterns like OpenTofu/Terraform modules are stored. To reorganize the resources that we’ve created so far into reusable modules, we’ll create a directory called `catalog/modules` where we can store our modules for reusability. We’ll create an OpenTofu module for each piece of high-level functionality that we are provisioning in our current environment (`s3`, `lambda`, `iam` and `ddb`). ```bash mkdir -p catalog/modules/{s3,lambda,iam,ddb} ``` Now we can move over the files that were provisioning these independent resources into their own modules so we can establish APIs for them and start reusing some of this code. It’s a pretty standard convention to name the core file used in a module `main.tf`. Good modules do one thing, and if you can’t figure out what a module does by the name of the module, it’s probably indicative that you’re making an odd abstraction. ```bash mv live/ddb.tf catalog/modules/ddb/main.tf mv live/iam.tf catalog/modules/iam/main.tf mv live/data.tf catalog/modules/iam/data.tf mv live/lambda.tf catalog/modules/lambda/main.tf mv live/s3.tf catalog/modules/s3/main.tf ``` The contents of some of these files need a little massaging, however, as the IaC didn’t have clear boundaries between the constituent components. Let’s fix that by providing an API for each of these modules in the form of variables for inputs and outputs…. for outputs. catalog/modules/ddb/vars-required.tf ```hcl variable "name" { description = "The name of the DynamoDB table" type = string } ``` catalog/modules/ddb/outputs.tf ```hcl output "name" { value = aws_dynamodb_table.asset_metadata.name } output "arn" { value = aws_dynamodb_table.asset_metadata.arn } ``` catalog/modules/s3/vars-required.tf ```hcl variable "name" { description = "Name used for all resources" type = string } ``` catalog/modules/s3/vars-optional.tf ```hcl variable "force_destroy" { description = "Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)" type = bool default = false } ``` catalog/modules/s3/outputs.tf ```hcl output "name" { value = aws_s3_bucket.static_assets.bucket } output "arn" { value = aws_s3_bucket.static_assets.arn } ``` catalog/modules/iam/vars-required.tf ```hcl variable "name" { description = "The name of the IAM role" type = string } variable "aws_region" { description = "The AWS region to deploy the resources to" type = string } variable "s3_bucket_arn" { description = "The ARN of the S3 bucket" type = string } variable "dynamodb_table_arn" { description = "The ARN of the DynamoDB table" type = string } ``` catalog/modules/iam/outputs.tf ```hcl output "name" { value = aws_iam_role.lambda_role.name } output "arn" { value = aws_iam_role.lambda_role.arn } ``` For the `iam` module, we’re also going to need to make adjustments to the `main.tf` file to account for previous tight coupling between resources. The updates here take advantage of those new `s3_bucket_arn` and `dynamodb_table_arn` variables for message passing between modules, exposed by the outputs of the `ddb` and `s3` modules. catalog/modules/iam/main.tf ```hcl resource "aws_iam_role" "lambda_role" { name = "${var.name}-lambda-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } } ] }) } resource "aws_iam_policy" "lambda_s3_read" { name = "${var.name}-lambda-s3-read" description = "Policy for Lambda to read from S3 bucket" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:ListBucket" ] Resource = [ var.s3_bucket_arn, "${var.s3_bucket_arn}/*" ] } ] }) } resource "aws_iam_policy" "lambda_dynamodb" { name = "${var.name}-lambda-dynamodb" description = "Policy for Lambda to read/write to DynamoDB table" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ] Resource = var.dynamodb_table_arn } ] }) } resource "aws_iam_policy" "lambda_basic_execution" { name = "${var.name}-lambda-basic-execution" description = "Policy for Lambda basic execution (CloudWatch logs)" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] Resource = "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*" } ] }) } resource "aws_iam_role_policy_attachment" "lambda_s3_read" { role = aws_iam_role.lambda_role.name policy_arn = aws_iam_policy.lambda_s3_read.arn } resource "aws_iam_role_policy_attachment" "lambda_dynamodb" { role = aws_iam_role.lambda_role.name policy_arn = aws_iam_policy.lambda_dynamodb.arn } resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { role = aws_iam_role.lambda_role.name policy_arn = aws_iam_policy.lambda_basic_execution.arn } ``` catalog/modules/lambda/vars-optional.tf ```hcl variable "lambda_runtime" { description = "Lambda function runtime" type = string default = "nodejs22.x" } variable "lambda_handler" { description = "Lambda function handler" type = string default = "index.handler" } variable "lambda_timeout" { description = "Lambda function timeout in seconds" type = number default = 30 } variable "lambda_memory_size" { description = "Lambda function memory size in MB" type = number default = 128 } variable "lambda_architectures" { description = "Lambda function architectures" type = list(string) default = ["arm64"] } ``` catalog/modules/lambda/vars-required.tf ```hcl variable "name" { description = "Name used for all resources" type = string } variable "aws_region" { description = "AWS region to deploy the resources to" type = string } variable "lambda_zip_file" { description = "Path to the Lambda function zip file" type = string } variable "lambda_role_arn" { description = "Lambda function role ARN" type = string } variable "s3_bucket_name" { description = "S3 bucket name" type = string } variable "dynamodb_table_name" { description = "DynamoDB table name" type = string } ``` catalog/modules/lambda/outputs.tf ```hcl output "name" { value = aws_lambda_function.main.function_name } output "arn" { value = aws_lambda_function.main.arn } output "url" { value = aws_lambda_function_url.main.function_url } ``` Again, for the Lambda module we’re going to need to make updates to the `main.tf` file to account for the tight coupling between resources now that we’re wiring them together via variables and outputs. catalog/modules/lambda/main.tf ```hcl resource "aws_lambda_function" "main" { function_name = "${var.name}-function" filename = var.lambda_zip_file source_code_hash = filebase64sha256(var.lambda_zip_file) role = var.lambda_role_arn handler = var.lambda_handler runtime = var.lambda_runtime timeout = var.lambda_timeout memory_size = var.lambda_memory_size architectures = var.lambda_architectures environment { variables = { S3_BUCKET_NAME = var.s3_bucket_name DYNAMODB_TABLE_NAME = var.dynamodb_table_name } } } resource "aws_lambda_function_url" "main" { function_name = aws_lambda_function.main.function_name authorization_type = "NONE" } ``` Let’s make sure that our modules have a copy of the `versions.tf` file that was in the root module (if you’re not comfortable with using the `find` command below, you can just copy the `versions.tf` file into each of the modules you’ve created so far manually). It’s a best practice to have reusable modules define their version constraints so that they can explicitly signal to module consumers when they use features in newer provider versions that might require a provider upgrade or are dodging a bug in a particular version of a provider that consumers should avoid. ```bash find catalog/modules -mindepth 1 -type d -exec cp live/versions.tf {}/versions.tf \; ``` To use these modules, we need to use OpenTofu `module` blocks to reference them in a new `main.tf` file placed in the `live` directory (the OpenTofu root module). What we’re doing here is simply instantiating each of the modules that we’ve created so far by referencing them using a relative path to the module in the `source` attribute, setting values for their required inputs (some of which are acquired as outputs from other modules). live/main.tf ```hcl module "s3" { source = "../catalog/modules/s3" name = var.name force_destroy = var.force_destroy } module "ddb" { source = "../catalog/modules/ddb" name = var.name } module "iam" { source = "../catalog/modules/iam" name = var.name aws_region = var.aws_region s3_bucket_arn = module.s3.arn dynamodb_table_arn = module.ddb.arn } module "lambda" { source = "../catalog/modules/lambda" name = var.name aws_region = var.aws_region s3_bucket_name = module.s3.name dynamodb_table_name = module.ddb.name lambda_zip_file = var.lambda_zip_file lambda_role_arn = module.iam.arn } ``` We also want to forward outputs from these modules into our root module so that we can access them from the `tofu` CLI. live/outputs.tf ```hcl output "lambda_function_url" { description = "URL of the Lambda function" value = module.lambda.url } output "lambda_function_name" { description = "Name of the Lambda function" value = module.lambda.name } output "s3_bucket_name" { description = "Name of the S3 bucket for static assets" value = module.s3.name } output "s3_bucket_arn" { description = "ARN of the S3 bucket for static assets" value = module.s3.arn } output "dynamodb_table_name" { description = "Name of the DynamoDB table for asset metadata" value = module.ddb.name } output "dynamodb_table_arn" { description = "ARN of the DynamoDB table for asset metadata" value = module.ddb.arn } output "lambda_role_arn" { description = "ARN of the Lambda execution role" value = module.iam.arn } ``` We can also reduce the amount of content in the optional variables file, now that each of the modules define the variables that matter to them. This keeps the API of each module clean, as each module exposes the variables that specifically control them. live/vars-optional.tf ```hcl variable "aws_region" { description = "AWS region for all resources" type = string default = "us-east-1" } variable "force_destroy" { description = "Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)" type = bool default = false } ``` After all this refactoring, we’ll want to run a `plan` to make sure we can safely apply our changes. Note You’ll need to re-initialize now that you’re using modules here. live ```bash $ tofu init $ tofu plan ... Plan: 11 to add, 0 to change, 11 to destroy. ... ``` Oh no! After all our refactors, we’ve introduced changes that would *completely destroy* all of the infrastructure we’ve created so far! This is a common scenario that you need to become comfortable with as you learn how to refactor and adjust IaC for scalability and maintainability. You leveraged the built-in protections of plans to give you a dry-run of your infrastructure updates, and can reason about why OpenTofu is trying to do what it’s doing here to avoid catastrophe. We, as authors of the IaC, know that all we’ve done in this step is move some files into different directories, but as far as OpenTofu is concerned, we’ve deleted resources at addresses like the following: ```bash # aws_lambda_function.main will be destroyed # (because aws_lambda_function.main is not in configuration) ``` And introduced resources at addresses the like the following: ```bash # module.lambda.aws_lambda_function.main will be created ``` The reason for this is that OpenTofu doesn’t really have a way of knowing the difference between moving a file like that for the sake of reorganization and completely removing infrastructure in one place and adding it in another without some help from IaC authors. The way we communicate to OpenTofu that a resource at one address has simply moved to a new address is to introduce `moved` blocks. For each resource that we want to move, we’ll want to introduce a `moved` block with a `from` of the old address (what OpenTofu reports as being destroyed in our plan) and a `to` of the equivalent new address (what OpenTofu reports as being created in our plan). live/moved.tf ```hcl moved { from = aws_dynamodb_table.asset_metadata to = module.ddb.aws_dynamodb_table.asset_metadata } moved { from = aws_iam_policy.lambda_basic_execution to = module.iam.aws_iam_policy.lambda_basic_execution } moved { from = aws_iam_policy.lambda_dynamodb to = module.iam.aws_iam_policy.lambda_dynamodb } moved { from = aws_iam_policy.lambda_s3_read to = module.iam.aws_iam_policy.lambda_s3_read } moved { from = aws_iam_role.lambda_role to = module.iam.aws_iam_role.lambda_role } moved { from = aws_iam_role_policy_attachment.lambda_basic_execution to = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution } moved { from = aws_iam_role_policy_attachment.lambda_dynamodb to = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb } moved { from = aws_iam_role_policy_attachment.lambda_s3_read to = module.iam.aws_iam_role_policy_attachment.lambda_s3_read } moved { from = aws_lambda_function.main to = module.lambda.aws_lambda_function.main } moved { from = aws_lambda_function_url.main to = module.lambda.aws_lambda_function_url.main } moved { from = aws_s3_bucket.static_assets to = module.s3.aws_s3_bucket.static_assets } ``` It’s worth noting that we haven’t been working with any infrastructure that’s important to preserve in this demo so far. We can easily reproduce this infrastructure without much effort. It’s important to know how to perform refactors without having to recreate infrastructure, though, as we need to be able to avoid paying the penalty of outages or data loss — especially when working with production infrastructure. If, for example, the database or s3 bucket being managed here had real customer information, it would be *extremely* important to avoid recreating these resources. OpenTofu doesn’t always know that recreating a stateful resource can cause permanent data loss. If you want the benefits we mentioned earlier of refactored IaC, you’ll want to know how to carefully handle state manipulation in OpenTofu and understand what it’s trying to do. So, as a small tangent, let’s discuss what actually happened when we introduced these `moved` blocks. There are multiple ways to configure OpenTofu backend state configurations, but the way that we’ve configured it here is to have the state files stored in S3 as JSON files. What we did under the hood with our `moved` blocks was update the content of that JSON file in `s3://[your-state-bucket]/tofu.tfstate` so that each of the `resources` in your state file used updated values for their resource addresses. In the example of this move: ```hcl moved { from = aws_dynamodb_table.asset_metadata to = module.ddb.aws_dynamodb_table.asset_metadata } ``` We updated one of the JSON objects in the state file from one that had these values: ```json # Some stuff "mode": "managed", "type": "aws_dynamodb_table", "name": "asset_metadata", "provider": "provider[\"registry.opentofu.org/hashicorp/aws\"]", # More stuff ``` To one that had these values: ```json # Some stuff "module": "module.ddb", "mode": "managed", "type": "aws_dynamodb_table", "name": "asset_metadata", "provider": "provider[\"registry.opentofu.org/hashicorp/aws\"]", # More stuff ``` When OpenTofu wants to know the current state of `aws_dynamodb_table.asset_metadata`, it can look it up using the first value, and when it wants to lookup the state of `module.ddb.aws_dynamodb_table.asset_metadata` it uses the second value. By moving the value in state, we’re just telling OpenTofu that we’re calling the resource by a different name now, without actually changing anything in AWS. ## Project Layout Check-in [Section titled “Project Layout Check-in”](#project-layout-check-in) You should have a filesystem layout that look like the following for your IaC now: * catalog * modules * ddb * main.tf * outputs.tf * vars-required.tf * versions.tf * iam * data.tf * main.tf * outputs.tf * vars-required.tf * versions.tf * lambda * main.tf * outputs.tf * vars-optional.tf * vars-required.tf * versions.tf * s3 * main.tf * outputs.tf * vars-optional.tf * vars-required.tf * versions.tf * live * backend.tf * main.tf * moved.tf * outputs.tf * providers.tf * vars-optional.tf * vars-required.tf * versions.tf ## Applying Updates [Section titled “Applying Updates”](#applying-updates) You can now run `tofu apply` with no changes (don’t worry, you’ll get a chance to confirm you want to proceed before you have to commit to anything). live ```bash $ tofu apply ... Plan: 0 to add, 0 to change, 0 to destroy. ... Do you want to perform these actions? OpenTofu will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: ``` ## Trade-Offs [Section titled “Trade-Offs”](#trade-offs) Before moving on to the next step, where we’ll duplicate our entire infrastructure estate to introduce a new development environment, it’s important to pause here and evaluate the trade-offs of this refactor. Both the infrastructure in step 1 and step 2 provisioned the exact same infrastructure (remember that there were `0 to add, 0 to change, 0 to destroy.`). In fact, with the exception of the next step where we introduce the new `dev` environment, every step will result in the exact same infrastructure being provisioned. Why then is this refactor valuable? What do we gain by refactoring our IaC like this? What do we trade away in exchange? ### Pros [Section titled “Pros”](#pros) * **Abstraction by encapsulation**. Instead of one large set of variables that could be used by any resource, or one large set of resources that could interact in ways that are difficult to understand, there are modules that encapsulate subsets of infrastructure so that they have explicit interfaces via variables and outputs. * **More code reusability**. Each of these modules can be reused in `live` infrastructure or in other `catalog` modules (which we’ll see in the next step). ### Cons [Section titled “Cons”](#cons) * **Increased complexity**. Instead of one self-contained directory with files directly defining resources to be provisioned, there’s a layer of indirection via modules. As someone consuming the module, you have to either trust it has been authored well (and that it’s well documented, tested, etc.) or vet the module yourself. * **State Adjustment**. State manipulation or resource recreation is required to migrate to this pattern. Every subsequent stage is going to continue incurring trade-offs. You (or someone experienced you trust) must to decide whether these trade-offs are appropriate for your organization and your infrastructure estate. ## Wrap Up [Section titled “Wrap Up”](#wrap-up) This was a significant refactoring step. You’ve transformed your flat configuration into a set of distinct, reusable modules, each with a well-defined API of variables and outputs. The most critical lesson here was mastering the `moved` block. This powerful feature allowed you to completely reorganize your code’s structure without OpenTofu needing to destroy and recreate your existing infrastructure, a vital skill for managing real-world infrastructure. While this adds a layer of indirection, the trade-off is greater code reusability and clearer separation of concerns. With this new modular structure, you’re now perfectly positioned to create a second environment with ease. # Step 3: Adding dev > Adding dev In the last step, you engaged in the foundational work of refactoring your monolithic configuration into a set of reusable modules, still instantiated in a single root module. Now it’s time to leverage those newly develop skills to create new infrastructure. One of the main advantages gained in creating infrastructure using IaC is improved reproducibility. The naive approach to creating new infrastructure is to directly copy and paste IaC to duplicate it, but there’s frequently advantage in packaging the infrastructure you’re going to replicate as a new pattern in your `catalog` so that you have a single source of truth for your shared IaC patterns. In this step, you’ll take the infrastructure you’ve created so far, do one more refactor to encapsulate it as a single reusable module, then instantiate it a second time as a second `dev` environment. ## Tutorial [Section titled “Tutorial”](#tutorial) Let’s introduce that new higher level module as a new module named `best_cat`. It will provision the `s3`, `ddb`, `lambda` and `iam` modules we added in the last step, and wire them together. This will give us a single entity that we can duplicate across environments. Note We’re basically taking all the stuff in `live` and shoving it into this new `best_cat` module. catalog/modules/best\_cat/main.tf ```hcl module "s3" { source = "../s3" name = var.name force_destroy = var.force_destroy } module "ddb" { source = "../ddb" name = var.name } module "iam" { source = "../iam" name = var.name aws_region = var.aws_region s3_bucket_arn = module.s3.arn dynamodb_table_arn = module.ddb.arn } module "lambda" { source = "../lambda" name = var.name aws_region = var.aws_region s3_bucket_name = module.s3.name dynamodb_table_name = module.ddb.name lambda_zip_file = var.lambda_zip_file lambda_role_arn = module.iam.arn } ``` catalog/modules/best\_cat/outputs.tf ```hcl output "lambda_function_url" { description = "URL of the Lambda function" value = module.lambda.url } output "lambda_function_name" { description = "Name of the Lambda function" value = module.lambda.name } output "s3_bucket_name" { description = "Name of the S3 bucket for static assets" value = module.s3.name } output "s3_bucket_arn" { description = "ARN of the S3 bucket for static assets" value = module.s3.arn } output "dynamodb_table_name" { description = "Name of the DynamoDB table for asset metadata" value = module.ddb.name } output "dynamodb_table_arn" { description = "ARN of the DynamoDB table for asset metadata" value = module.ddb.arn } output "lambda_role_arn" { description = "ARN of the Lambda execution role" value = module.iam.arn } ``` catalog/modules/best\_cat/vars-optional.tf ```hcl variable "aws_region" { description = "AWS region for all resources" type = string default = "us-east-1" } variable "force_destroy" { description = "Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)" type = bool default = false } ``` catalog/modules/best\_cat/vars-required.tf ```hcl variable "name" { description = "Name used for all resources" type = string } variable "lambda_zip_file" { description = "Path to the Lambda function zip file" type = string } ``` Similar to what we did before with the constituent modules, we can simply replace the content in `live` with a reference to our new `best_cat` module. live/main.tf ```hcl module "dev" { source = "../catalog/modules/best_cat" name = "${var.name}-dev" aws_region = var.aws_region lambda_zip_file = var.lambda_zip_file force_destroy = var.force_destroy } module "prod" { source = "../catalog/modules/best_cat" name = var.name aws_region = var.aws_region lambda_zip_file = var.lambda_zip_file force_destroy = var.force_destroy } ``` Once again, we get the scary `tofu plan` that tells us we would recreate all our infrastructure if we were to naively apply here: live ```bash $ tofu plan ... Plan: 11 to add, 0 to change, 11 to destroy. ... ``` Luckily, we already know how to handle this. We’re going to update our `moved.tf` file to declare all the moves that need to be performed to transition the old addresses of resources to their new addresses. live/moved.tf ```hcl moved { from = module.ddb.aws_dynamodb_table.asset_metadata to = module.prod.module.ddb.aws_dynamodb_table.asset_metadata } moved { from = module.iam.aws_iam_policy.lambda_basic_execution to = module.prod.module.iam.aws_iam_policy.lambda_basic_execution } moved { from = module.iam.aws_iam_policy.lambda_dynamodb to = module.prod.module.iam.aws_iam_policy.lambda_dynamodb } moved { from = module.iam.aws_iam_policy.lambda_s3_read to = module.prod.module.iam.aws_iam_policy.lambda_s3_read } moved { from = module.iam.aws_iam_role.lambda_role to = module.prod.module.iam.aws_iam_role.lambda_role } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution to = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb to = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read to = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_s3_read } moved { from = module.lambda.aws_lambda_function.main to = module.prod.module.lambda.aws_lambda_function.main } moved { from = module.lambda.aws_lambda_function_url.main to = module.prod.module.lambda.aws_lambda_function_url.main } moved { from = module.s3.aws_s3_bucket.static_assets to = module.prod.module.s3.aws_s3_bucket.static_assets } ``` Our apply now successfully completes without doing anything! live ```bash $ tofu apply ... Apply complete! Resources: 0 added, 0 changed, 0 destroyed. ... ``` Now the stage is set to add the additional `dev` environment. We can do that by duplicating the `prod` module, and labeling the new `module` block `dev` (you’ll also want to add a little suffix to the end of the `name` input to avoid naming collisions). live/main.tf ```hcl module "dev" { source = "../catalog/modules/best_cat" name = "${var.name}-dev" aws_region = var.aws_region lambda_zip_file = var.lambda_zip_file force_destroy = var.force_destroy } module "prod" { source = "../catalog/modules/best_cat" name = var.name aws_region = var.aws_region lambda_zip_file = var.lambda_zip_file force_destroy = var.force_destroy } ``` We also need to expose some of the outputs of the new `dev` module, but if we just duplicated all the `prod` outputs, we’d end up with a massive wall of outputs that would be hard to parse. Luckily, we only need two outputs to be externally accessible per environment, so we can drop a bunch of outputs to streamline things. live/outputs.tf ```hcl output "dev_lambda_function_url" { description = "URL of the Lambda function" value = module.dev.lambda_function_url } output "dev_s3_bucket_name" { description = "Name of the S3 bucket for static assets" value = module.dev.s3_bucket_name } output "prod_lambda_function_url" { description = "URL of the Lambda function" value = module.prod.lambda_function_url } output "prod_s3_bucket_name" { description = "Name of the S3 bucket for static assets" value = module.prod.s3_bucket_name } ``` ## Project Layout Check-in [Section titled “Project Layout Check-in”](#project-layout-check-in) We should have a project layout that looks like this now: * catalog * modules * **best\_cat** * **main.tf** * **outputs.tf** * **vars-optional.tf** * **vars-required.tf** * ddb * main.tf * outputs.tf * vars-required.tf * versions.tf * iam * data.tf * main.tf * outputs.tf * vars-required.tf * versions.tf * lambda * main.tf * outputs.tf * vars-optional.tf * vars-required.tf * versions.tf * s3 * main.tf * outputs.tf * vars-optional.tf * vars-required.tf * versions.tf * live * backend.tf * main.tf * moved.tf * outputs.tf * providers.tf * vars-optional.tf * vars-required.tf * versions.tf ## Applying Updates [Section titled “Applying Updates”](#applying-updates) Now we can deploy our changes. Note We need to re-initialize here as we’ve added a new module. live ```bash tofu init tofu apply ``` We now have our new, fresh dev environment! ![fresh-dev-environment](/_vercel/image?url=_astro%2Fapp-without-images.DibdLfwK.png\&w=1920\&q=100) Note This environment is *very* fresh. We don’t have the static assets uploaded, and we’re working with a fresh database. This guide isn’t going to into the ways in which Terragrunt could integrate into your build system to do this for you automatically, but know that it *is* part of Terragrunt’s feature set to handle this kind of thing. There are hints at the end of this guide to point those capabilities out and encourage your own exploration. ## Trade-offs [Section titled “Trade-offs”](#trade-offs) ### Pros [Section titled “Pros”](#pros) We have officially reached the stage where we’re hitting **risk increase** due to our ***Terralith***! This is the configuration of IaC that a lot of infrastructure estates grow to naturally as they tack on more resources and add environments. It’s a tipping point in maintainability that is best caught early, and addressed. We gained the ability to easily provision new infrastructure via reusable modules and could simply copy and paste (then season to taste) some configuration in our `live/main.tf` file. We also had a single source of truth for representing all the infrastructure that we were provisioning, in both the reusable module, and our `live` OpenTofu root module. We traded that for additional risk incurred, as every `apply` or `destroy` now has the potential to modify or destroy multiple environments, and you have to carefully avoid misconfiguration by reading plans (and trusting that they’re accurate) to avoid accidentally damaging the wrong environment. Furthermore, you also have to be very careful that you only modify the resources that you intend to modify *within* a given environment when you make updates to it (are you accidentally destroying your database when attempting a tagging update for your Lambda function?). The reason for this is that all your resources are in the same state file. OpenTofu has to make one atomic change to that single state file with every update, so all the resources in state are at risk when any change is made. For your information, there are tools out there, like [OPA](https://www.openpolicyagent.org/) that enable automated reasoning about plan risk, but those tools are typically adopted by more advanced infrastructure teams, and there is typically a significant amount of overhead in authoring and maintaining the policies that assess plan risk (and driving behavior off those assessments). There are hints at the end of this guide to point those capabilities out and encourage your own exploration on this topic. Generally, the approach that teams take to structurally reduce this risk is to start to ***break down the Terralith*** into separate root modules, each with their own state. This gives teams confidence that they ***can only*** modify `dev` when they set their current working directory to the `dev` root module, and `prod` when their current working directory is the `prod` root module. When thinking through access control, this can also be convenient, as you can segment the access control that you use for one root module from another. Teams frequently configure their setups so that they need to explicitly use different credentials via role assumption, etc. when running commands in root modules related to different environments (e.g. `dev` vs `prod` ) to avoid accidental updates in the wrong environment. ### Cons [Section titled “Cons”](#cons) The downside to that approach, as we’ll see in the next step, is that it *does* increase the management burden of orchestrating and maintaining your IaC, and additional tooling like Terragrunt is a good way to handle that additional orchestration burden. ## Wrap Up [Section titled “Wrap Up”](#wrap-up) You’ve successfully spun up a second, isolated development environment by reusing your new `best_cat` module. However, this is also the point where the *Terralith* design pattern starts to incur some serious drawbacks. At this stage, all your infrastructure for both your environments (`dev` and `prod`) now lives in a single state file. This introduces significant risk. A small mistake intended for `dev` could potentially damage or destroy your `prod` environment because OpenTofu sees it all as one atomic unit to manage, and you’re responsible for reasoning about the generated plan to see if you should proceed with an apply. The next step is the most critical step in maturing your IaC estate (as far as this guide is concerned) as you break this monolith apart to limit the blast radius of your updates. # Step 4: Breaking the Terralith > Breaking the Terralith In the previous step, you successfully duplicated your `prod` environment as a new `dev` environment by replicating your `prod` module declaration as a new `dev` module instance in your `live/main.tf` file. While this demonstrated the power of reusable modules, it also introduced significant risk: the ***Terralith***. You’ve tightly coupled management of both your `dev` and `prod` environments in a single state file, and you introduce risk to one whenever you attempt to make changes to another. In this step, you’ll solve this problem by breaking apart your Terralith apart. You’ll refactor your `live` root module into two distinct `dev` and `prod` root modules. Each will have its own state file, completely eliminating the risk of accidental cross-environment changes. ## Tutorial [Section titled “Tutorial”](#tutorial) Breaking down your Terralith so that you have multiple root modules is fairly simple now that you understand state manipulation a bit better. First, let’s create a top-level directory for `prod` in `live`. live ```bash mkdir prod ``` Next, let’s move everything into the `prod` directory (If you’re not comfortable with using the `find` command here, you can just drag the content into the `prod` directory). live ```bash find . -mindepth 1 -maxdepth 1 -not -name 'prod' -exec mv {} prod/ \; ``` To complete our new multi-environment setup, let’s duplicate that `prod` directory to a new `dev` directory. live ```bash cp -R prod dev ``` We need to edit the contents of the `dev` and `prod` directories to make some key adjustments. First, we’ll want to make sure that the `backend.tf` files are updated to use new keys so that the two root modules don’t conflict with each other. live/dev/backend.tf ```hcl terraform { backend "s3" { bucket = "terragrunt-to-terralith-tfstate-2025-09-24-2359" key = "dev/tofu.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } } ``` live/prod/backend.tf ```hcl terraform { backend "s3" { bucket = "terragrunt-to-terralith-tfstate-2025-09-24-2359" key = "prod/tofu.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } } ``` We’ll also want to update the references to the shared module, update the `.auto.tfvars` file and edit the outputs to handle all the changes necessary for this project. live/dev/main.tf ```hcl module "main" { source = "../../catalog/modules/best_cat" name = var.name aws_region = var.aws_region lambda_zip_file = var.lambda_zip_file force_destroy = var.force_destroy } ``` live/prod/main.tf ```hcl module "main" { source = "../../catalog/modules/best_cat" name = var.name aws_region = var.aws_region lambda_zip_file = var.lambda_zip_file force_destroy = var.force_destroy } ``` Note These two files are now *exactly the same*. This will be important to keep in mind later. Given that we’ve renamed the the module, we’ll also need to add `moved` blocks to handle the state moves that need to take place here. If you’re not sure what we’re doing here, consider reviewing earlier steps. live/dev/moved.tf ```hcl moved { from = module.dev.module.ddb.aws_dynamodb_table.asset_metadata to = module.main.module.ddb.aws_dynamodb_table.asset_metadata } moved { from = module.dev.module.iam.aws_iam_policy.lambda_basic_execution to = module.main.module.iam.aws_iam_policy.lambda_basic_execution } moved { from = module.dev.module.iam.aws_iam_policy.lambda_dynamodb to = module.main.module.iam.aws_iam_policy.lambda_dynamodb } moved { from = module.dev.module.iam.aws_iam_policy.lambda_s3_read to = module.main.module.iam.aws_iam_policy.lambda_s3_read } moved { from = module.dev.module.iam.aws_iam_role.lambda_role to = module.main.module.iam.aws_iam_role.lambda_role } moved { from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution to = module.main.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution } moved { from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb to = module.main.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb } moved { from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_s3_read to = module.main.module.iam.aws_iam_role_policy_attachment.lambda_s3_read } moved { from = module.dev.module.lambda.aws_lambda_function.main to = module.main.module.lambda.aws_lambda_function.main } moved { from = module.dev.module.lambda.aws_lambda_function_url.main to = module.main.module.lambda.aws_lambda_function_url.main } moved { from = module.dev.module.s3.aws_s3_bucket.static_assets to = module.main.module.s3.aws_s3_bucket.static_assets } ``` live/prod/moved.tf ```hcl moved { from = module.prod.module.ddb.aws_dynamodb_table.asset_metadata to = module.main.module.ddb.aws_dynamodb_table.asset_metadata } moved { from = module.prod.module.iam.aws_iam_policy.lambda_basic_execution to = module.main.module.iam.aws_iam_policy.lambda_basic_execution } moved { from = module.prod.module.iam.aws_iam_policy.lambda_dynamodb to = module.main.module.iam.aws_iam_policy.lambda_dynamodb } moved { from = module.prod.module.iam.aws_iam_policy.lambda_s3_read to = module.main.module.iam.aws_iam_policy.lambda_s3_read } moved { from = module.prod.module.iam.aws_iam_role.lambda_role to = module.main.module.iam.aws_iam_role.lambda_role } moved { from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution to = module.main.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution } moved { from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb to = module.main.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb } moved { from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_s3_read to = module.main.module.iam.aws_iam_role_policy_attachment.lambda_s3_read } moved { from = module.prod.module.lambda.aws_lambda_function.main to = module.main.module.lambda.aws_lambda_function.main } moved { from = module.prod.module.lambda.aws_lambda_function_url.main to = module.main.module.lambda.aws_lambda_function_url.main } moved { from = module.prod.module.s3.aws_s3_bucket.static_assets to = module.main.module.s3.aws_s3_bucket.static_assets } ``` Next, we’ll update the outputs, just like we did for the `main.tf` files. live/dev/outputs.tf ```hcl output "lambda_function_url" { description = "URL of the Lambda function" value = module.main.lambda_function_url } output "s3_bucket_name" { description = "Name of the S3 bucket for static assets" value = module.main.s3_bucket_name } ``` live/prod/outputs.tf ```hcl output "lambda_function_url" { description = "URL of the Lambda function" value = module.main.lambda_function_url } output "s3_bucket_name" { description = "Name of the S3 bucket for static assets" value = module.main.s3_bucket_name } ``` Note These two files are *also exactly the same*. This will be important to keep in mind later. Finally, we need to update the `.auto.tfvars` files to reflect the difference in inputs passed to variables in these two root modules. live/prod/.auto.tfvars ```hcl # Required: Name used for all resources (must be unique) name = "best-cat-2025-09-24-2359" # Required: Path to your Lambda function zip file lambda_zip_file = "../../dist/best-cat.zip" ``` live/dev/.auto.tfvars ```hcl # Required: Name used for all resources (must be unique) name = "best-cat-2025-09-24-2359-dev" # Required: Path to your Lambda function zip file lambda_zip_file = "../../dist/best-cat.zip" ``` Note These two files differ in that they have different `name` values. They’re not identical, but they’re very similar. It’s time for some more state manipulation! We currently have a single state file in S3 at `s3://[your-state-bucket]/tofu.tfstate`. Our plan for splitting the state is to basically duplicate state for both the `dev` and `prod` root modules, then remove resources that we don’t need from state in each of the root modules. In addition to having the state in S3, we also have a local copy of state in each root module. Running the `tofu init -migrate-state` command with the `.terraform` directory populated by copy of state from the previous configuration of the project will copy state to the new location in each new root module. live/dev ```bash $ tofu init -migrate-state Initializing the backend... Backend configuration changed! OpenTofu has detected that the configuration specified for the backend has changed. OpenTofu will now check for existing state in the backends. Do you want to copy existing state to the new backend? Pre-existing state was found while migrating the previous "s3" backend to the newly configured "s3" backend. No existing state was found in the newly configured "s3" backend. Do you want to copy this state to the new "s3" backend? Enter "yes" to copy and "no" to start with an empty state. Enter a value: yes Successfully configured the backend "s3"! OpenTofu will automatically use this backend unless the backend configuration changes. ``` live/prod ```bash $ tofu init -migrate-state Initializing the backend... Backend configuration changed! OpenTofu has detected that the configuration specified for the backend has changed. OpenTofu will now check for existing state in the backends. Do you want to copy existing state to the new backend? Pre-existing state was found while migrating the previous "s3" backend to the newly configured "s3" backend. No existing state was found in the newly configured "s3" backend. Do you want to copy this state to the new "s3" backend? Enter "yes" to copy and "no" to start with an empty state. Enter a value: yes Successfully configured the backend "s3"! OpenTofu will automatically use this backend unless the backend configuration changes. ``` We now have the state in `s3://[your-state-bucket]/tofu.tfstate` copied to both: * `s3://[your-state-bucket]/dev/tofu.tfstate` * `s3://[your-state-bucket]/prod/tofu.tfstate` We need to remove the resources from state that aren’t relevant in the new root modules, now so that we don’t deploy `prod` resources in the `dev` root module and vice versa. live/dev/removed.tf ```hcl removed { from = module.prod.module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.prod.module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.prod.module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.prod.module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.prod.module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.prod.module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.prod.module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.prod.module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` live/prod/removed.tf ```hcl removed { from = module.dev.module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.dev.module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.dev.module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.dev.module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.dev.module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.dev.module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.dev.module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.dev.module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` ## Project Layout Check-in [Section titled “Project Layout Check-in”](#project-layout-check-in) At this stage, we should have a `live` directory that looks like the following (the `catalog` directory shouldn’t have changed at all): * live * dev * backend.tf * main.tf * moved.tf * outputs.tf * providers.tf * removed.tf * vars-optional.tf * vars-required.tf * versions.tf * prod * backend.tf * main.tf * moved.tf * outputs.tf * providers.tf * removed.tf * vars-optional.tf * vars-required.tf * versions.tf ## Applying Updates [Section titled “Applying Updates”](#applying-updates) We should now see that we’re simply going to forget the removed resources instead of destroying them. live/dev ```bash $ tofu plan ... Plan: 0 to add, 1 to change, 0 to destroy, 11 to forget. ... ``` Let’s apply both `dev` and `prod` to finalize the moves and removals. live/dev ```bash $ tofu apply ... Apply complete! Resources: 0 added, 1 changed, 0 destroyed, 11 forgotten. ... ``` live/prod ```bash $ tofu apply ... Apply complete! Resources: 0 added, 1 changed, 0 destroyed, 11 forgotten. ... ``` ## Trade-offs [Section titled “Trade-offs”](#trade-offs) ### Pros [Section titled “Pros”](#pros) We did it! We successfully broke apart our Terralith using OpenTofu alone. Some organizations get to this stage in their IaC journey, and are perfectly happy with managing their infrastructure like this. You can limit the blast radius of your `dev` and `prod` environments this way, and it’s fairly straightforward to adjust your current working directory to the `dev` root module when making modifications to the `dev` environment, and adjusting your working directory to the `prod` root module when making modifications to the `prod` environment. This is actually the pattern that Gruntwork was initially helping customers achieve early on to make their infrastructure safer, and more manageable by teams. ### Cons [Section titled “Cons”](#cons) There are, however, some downsides to how we’re managing infrastructure here. 1. There’s some annoying boilerplate that’s inconvenient to create and maintain. The following files are identical in each environment, but need to be present just to get OpenTofu to provision the same module: 1. `main.tf` 2. `outputs.tf` 3. `providers.tf` 4. `vars-optional.tf` 5. `vars-required.tf` 2. We also have *almost* the same file in each of these, and their values aren’t really that interesting. 1. `backend.tf` 2. `.auto.tfvars` 3. We also don’t have a convenient way to run multiple root modules at once. What if we want to update both `dev` *and* `prod` at once? What if we want to break down the environments further? * As you might have guessed, the next step is to introduce Terragrunt to address some of these downsides, and unlock even more capabilities for managing infrastructure at scale. ## Wrap Up [Section titled “Wrap Up”](#wrap-up) This is a pivotal moment in this guide. You have successfully started to break down the Terralith! By migrating your state and refactoring your configuration, you have split your single, high-risk state file into two separate ones: one for `dev` and one for `prod`. The primary benefit is safety. You’ve drastically reduced the blast radius, as running `tofu apply` in the `dev` directory can now *only* affect development resources and running `tofu apply` in the `prod` directory can *only* affect production resources. However, this safety has come at the cost of duplication. Your `dev` and `prod` directories contain a lot of identical, boilerplate `.tf` files, and it isn’t very scalable. What if you have twice as many environments? What if you have ten times as many? How are you going to handle making all those updates? Helping customers solve these problems and more at scale is what Terragrunt was designed for, which we’ll introduce next to streamline your workflow. # Step 5: Adding Terragrunt > Adding Terragrunt In the last step, you took a massive leap forward in safety by breaking your Terralith into separate `dev` and `prod` root modules. The trade-off, however, is that you’ve created a significant amount of boilerplate and duplication. Your `dev` and `prod` directories are filled with nearly identical `.tf` files (if not completely identical), and managing them involves a lot of careful copy-pasting. You also can’t conveniently manage multiple root modules at once. This isn’t scalable and is prone to error. This is the problem Terragrunt was created to solve. It acts as an orchestrator for OpenTofu/Terraform, helping you write DRY (Don’t Repeat Yourself) infrastructure code that scales. In this step, you’ll introduce Terragrunt to drastically reduce that boilerplate. You will: * Replace the duplicated `.tf` and `.auto.tfvars` files in each environment with a single, concise `terragrunt.hcl` file. * Use Terragrunt’s `terraform`, `inputs`, and `generate` blocks to define the module source, pass variables, and create configuration files on the fly. * Centralize common configurations (like your S3 `backend` configuration) in a single `root.hcl` file using the `include` block, ensuring your setup is easy to maintain. By the end of this step, your `live` directory will be dramatically leaner, paving the way for easier management and scaling. ## Tutorial [Section titled “Tutorial”](#tutorial) Now that we’ve structured our project to segment environments into their own root modules (and their own state files), it’s pretty simple to convert our root modules to Terragrunt units. In Terragrunt terminology, a [unit](https://docs.terragrunt.com/getting-started/terminology/#unit) is a single instance of infrastructure managed by Terragrunt. They’re easy to manage, and they come with a lot of tooling to support common IaC needs, like code generation, authentication, error handling, and more. The process of converting an OpenTofu root module to a Terragrunt unit simply involves adding an empty `terragrunt.hcl` file to each root module (that’s all the `find` command below does). This allows Terragrunt to recognize the contents of the directory as a Terragrunt unit, and orchestrate infrastructure updates within it. live ```bash find . -mindepth 1 -maxdepth 1 -type dir -exec touch {}/terragrunt.hcl \; ``` Now, we can use Terragrunt to orchestrate runs across both of these units. live ```bash $ terragrunt run --all plan 15:07:02.593 INFO The runner at . will be processed in the following order for command plan: Group 1 - Unit ./dev - Unit ./prod ... ``` We can also selectively run the plan for the `dev` environment by changing the working directory to `dev`, or using the [`--filter`](/features/filter) flag. live/dev ```bash $ terragrunt plan ``` live ```bash $ terragrunt run --all --filter ./dev plan 15:09:17.090 INFO The runner at . will be processed in the following order for command plan: Group 1 - Unit ./dev ... ``` Terragrunt is frequently adopted gradually in this manner. If you have an infrastructure problem you want addressed, you can gradually introduce more and more Terragrunt tooling to address those problems. We can also simplify things significantly now that we’re using Terragrunt. Terragrunt is designed to work well in this pattern where the majority of logic is abstracted away to a shared module. We can eliminate the need for some boilerplate now that we have access to the `terraform` block in `terragrunt.hcl` files (It’s named `terraform` for legacy reasons. It’s 100% compatible with OpenTofu). live/dev/terragrunt.hcl ```hcl terraform { source = "../../catalog/modules//best_cat" } ``` live/prod/terragrunt.hcl ```hcl terraform { source = "../../catalog/modules//best_cat" } ``` Note Those `//` are there on purpose. They’re how `go-getter`, the library that Terragrunt uses (just like OpenTofu), indicates that it’s working with a directory *within* a module source. This allows relative references like `../s3` to work within the `best_cat` module. With those changes, we can now remove the unnecessary boilerplate related to invoking the shared module. live ```bash rm -f ./*/main.tf ./*/outputs.tf ./*/vars-*.tf ./*/versions.tf ``` We can also leverage the `inputs` attribute in the `terragrunt.hcl` file to set inputs instead of relying on the separate `.auto.tfvars` file. live/dev/terragrunt.hcl ```hcl terraform { source = "../../catalog/modules//best_cat" } inputs = { name = "best-cat-2025-09-24-2359-dev" lambda_zip_file = "${get_repo_root()}/dist/best-cat.zip" } ``` live/prod/terragrunt.hcl ```hcl terraform { source = "../../catalog/modules//best_cat" } inputs = { name = "best-cat-2025-09-24-2359" lambda_zip_file = "${get_repo_root()}/dist/best-cat.zip" } ``` Note the use of `get_repo_root()`. This is a simple convenience function you can use to get the path to the root of your Git repository. You can use almost all of the same HCL functions you can use in OpenTofu, with some additional functions supplied by Terragrunt for tasks that are more useful in the context of Terragrunt (you can see the full list in the official Terragrunt [HCL functions](https://docs.terragrunt.com/reference/built-in-functions/) reference here). live ```bash rm -f ./*/.auto.tfvars ./*/.auto.tfvars.example ``` We can also get Terragrunt to generate that `backend.tf` file for us on-demand using the `remote_state` block. live/dev/terragrunt.hcl ```bash remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "terragrunt-to-terralith-tfstate-2025-09-24-2359" key = "dev/tofu.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } } terraform { source = "../../catalog/modules//best_cat" } inputs = { name = "best-cat-2025-09-24-2359-dev" lambda_zip_file = "${get_repo_root()}/dist/best-cat.zip" } ``` live/prod/terragrunt.hcl ```bash remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "terragrunt-to-terralith-tfstate-2025-09-24-2359" key = "prod/tofu.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } } terraform { source = "../../catalog/modules//best_cat" } inputs = { name = "best-cat-2025-09-24-2359" lambda_zip_file = "${get_repo_root()}/dist/best-cat.zip" } ``` live ```bash rm -f ./*/backend.tf ``` In fact, we can have Terragrunt generate any arbitrary file we need on-demand, including boilerplate files like we had in the `providers.tf` file. live/dev/terragrunt.hcl ```bash remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "terragrunt-to-terralith-tfstate-2025-09-24-2359" key = "dev/tofu.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } } generate "providers" { path = "providers.tf" if_exists = "overwrite_terragrunt" contents = < Breaking the Terralith Further You’ve successfully added Terragrunt to your project, eliminating significant boilerplate from each of your `dev` and `prod` environments. While your environments are now isolated from each other, the resources *within* each environment (your S3 bucket, DynamoDB table, IAM role, and Lambda function) are still managed together in a single state file. This is essentially a smaller-scale Terralith within each environment. This tight coupling poses its own risks. Do you really want a routine update to your Lambda function’s application code to require a plan that also evaluates your production database? Stateful resources like databases and storage buckets change infrequently and require maximum stability, while stateless application code changes constantly. Coupling them in the same state file means a mistake in one could still impact the other, increasing the blast radius of any single change. In this step, you will break the Terralith down even further. You will transform each environment from a single large unit into a collection of smaller, independent units, one for each core component (S3, DDB, IAM, and Lambda). This granular approach provides far more safety and flexibility, and is common in Terragrunt projects. To connect these newly independent components, you’ll learn one of Terragrunt’s most powerful features: the `dependency` block, which allows units to share outputs, such as passing the ARN of your S3 bucket to your IAM policy, and control the order of updates in your infrastructure units. ## Tutorial [Section titled “Tutorial”](#tutorial) We’re going to follow a very similar process to what we did when breaking apart the Terralith into two environments. First, we’ll create a directory for each of the new units we want to create for all the constituent modules of the `best_cat` megamodule. In each of our environments (`dev` and `prod`). live ```bash mkdir -p {dev,prod}/{s3,ddb,iam,lambda} ``` Next, we’ll create the `terragrunt.hcl` files in each of these directories. live/dev/ddb/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//ddb" } inputs = { name = "best-cat-2025-09-24-2359-dev" } ``` live/prod/ddb/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//ddb" } inputs = { name = "best-cat-2025-09-24-2359" } ``` live/dev/s3/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//s3" } inputs = { name = "best-cat-2025-09-24-2359-dev" # Optional: Force destroy S3 buckets even when they have objects in them. # You're generally advised not to do this with important infrastructure, # however this makes testing and cleanup easier for this guide. force_destroy = true } ``` live/prod/s3/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//s3" } inputs = { name = "best-cat-2025-09-24-2359" # Optional: Force destroy S3 buckets even when they have objects in them. # You're generally advised not to do this with important infrastructure, # however this makes testing and cleanup easier for this guide. force_destroy = true } ``` In units where we need to integrate with other units (like the `iam` unit), we’ll need to add a `dependency` block to tell Terragrunt how it can fetch outputs from relevant dependencies for use as inputs. Terragrunt has to integrate different units like this, as they don’t have the same state file, so OpenTofu needs an external tool, like Terragrunt to pull outputs out of state from one unit and pass in inputs to another unit. live/dev/iam/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//iam" } dependency "s3" { config_path = "../s3" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:s3:::mock-bucket-name" } } dependency "ddb" { config_path = "../ddb" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:dynamodb:us-east-1:123456789012:table/mock-table-name" } } inputs = { name = "best-cat-2025-09-24-2359-dev" aws_region = "us-east-1" s3_bucket_arn = dependency.s3.outputs.arn dynamodb_table_arn = dependency.ddb.outputs.arn } ``` live/prod/iam/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//iam" } dependency "s3" { config_path = "../s3" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:s3:::mock-bucket-name" } } dependency "ddb" { config_path = "../ddb" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:dynamodb:us-east-1:123456789012:table/mock-table-name" } } inputs = { name = "best-cat-2025-09-24-2359" aws_region = "us-east-1" s3_bucket_arn = dependency.s3.outputs.arn dynamodb_table_arn = dependency.ddb.outputs.arn } ``` Note Unfortunately, OpenTofu doesn’t have a way of specifying certain inputs as “unknown” [as of yet](https://github.com/opentofu/opentofu/issues/812), but we can work around this limitation by taking advantage of Terragrunt’s ability to mock outputs from dependencies using `mock_outputs`. This allows us to plan successfully without worrying about getting errors that required variables aren’t passed in. Note that some providers like the AWS provider require these inputs to be well formed (in this case, they have to be valid AWS ARNs). In these scenarios, it can be important to provide valid looking ARNs as a consequence to satisfy provider validations. If you just passed `mock-bucket-arn` as the value of the input `s3_bucket_arn`, the AWS provider might throw an error during plans, as it expects the value to look more like `arn:aws:s3:::mock-bucket-name`, and it assumes that the user made an error. We’ve also set the `mock_outputs_allowed_terraform_commands` attribute. By default, Terragrunt will use mocked outputs whenever a dependency returns no outputs. This is typically only the case for plans, but we can be explicit about when Terragrunt is allowed to mock outputs to avoid any accidental applies with mocked values. Other commands that might benefit from mocking are commands like `destroy` and `validate`. I don’t anticipate needing them mocked here, so I’ve only allowed mocking for commands where I know we’re going to need them mocked during this guide (you’ll see why `state` can get mocked outputs in a bit). Finally, note that we’ve also set the `mock_outputs_merge_strategy_with_state` attribute. By default, Terragrunt treats mocking as something binary: Either outputs are mocked, or they’re not. This is because you typically don’t have a need to partially mock some outputs and not others. In our use-case, where we’re migrating over state we will need to do this, as we’ll be pushing existing state to units, but their outputs are also changing. We’ll see what that looks like later. live/dev/lambda/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//lambda" } dependency "s3" { config_path = "../s3" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { name = "mock-bucket-name" } } dependency "ddb" { config_path = "../ddb" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { name = "mock-table-name" } } dependency "iam" { config_path = "../iam" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:iam::123456789012:role/mock-role-name" } } inputs = { name = "best-cat-2025-09-24-2359-dev" aws_region = "us-east-1" s3_bucket_name = dependency.s3.outputs.name dynamodb_table_name = dependency.ddb.outputs.name lambda_role_arn = dependency.iam.outputs.arn lambda_zip_file = "${get_repo_root()}/dist/best-cat.zip" } ``` live/prod/lambda/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//lambda" } dependency "s3" { config_path = "../s3" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { name = "mock-bucket-name" } } dependency "ddb" { config_path = "../ddb" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { name = "mock-table-name" } } dependency "iam" { config_path = "../iam" mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:iam::123456789012:role/mock-role-name" } } inputs = { name = "best-cat-2025-09-24-2359" aws_region = "us-east-1" s3_bucket_name = dependency.s3.outputs.name dynamodb_table_name = dependency.ddb.outputs.name lambda_role_arn = dependency.iam.outputs.arn lambda_zip_file = "${get_repo_root()}/dist/best-cat.zip" } ``` ## Project Layout Check-in [Section titled “Project Layout Check-in”](#project-layout-check-in) We should now have a file tree that looks like the following (we’ll be getting rid of the two top-level `terragrunt.hcl` and `moved.tf` files in each environment soon): * live * dev * ddb * terragrunt.hcl * iam * terragrunt.hcl * lambda * terragrunt.hcl * s3 * terragrunt.hcl * terragrunt.hcl (This is being removed soon) * moved.tf (This is being removed soon) * prod * ddb * terragrunt.hcl * iam * terragrunt.hcl * lambda * terragrunt.hcl * s3 * terragrunt.hcl * terragrunt.hcl (This is being removed soon) * moved.tf (This is being removed soon) * root.hcl ## Migrating State to Individual Units [Section titled “Migrating State to Individual Units”](#migrating-state-to-individual-units) It’s time to engage in our favorite solution for IaC refactoring, state manipulation! We’re going to use the tools we’ve learned so far, and *pull* state from those two top-level units, then *push* them into the constituent units we’ve broken the megamodule down into. We expect to need to both move resource addresses in state, and forget particular resources to avoid accidentally destroying anything. live/dev ```bash terragrunt state pull > /tmp/tofu.tfstate cd ddb && terragrunt state push /tmp/tofu.tfstate cd ../iam && terragrunt state push /tmp/tofu.tfstate cd ../lambda && terragrunt state push /tmp/tofu.tfstate cd ../s3 && terragrunt state push /tmp/tofu.tfstate ``` live/prod ```bash terragrunt state pull > /tmp/tofu.tfstate cd ddb && terragrunt state push /tmp/tofu.tfstate cd ../iam && terragrunt state push /tmp/tofu.tfstate cd ../lambda && terragrunt state push /tmp/tofu.tfstate cd ../s3 && terragrunt state push /tmp/tofu.tfstate ``` We can now clean up the extraneous files mentioned earlier at the root of the environments. live ```bash rm -f {dev,prod}/{terragrunt.hcl,moved.tf} ``` Go ahead and run the following to see very similar plan output to what we’ve seen in the past when we needed to make state moves & removes. live ```bash terragrunt run --all plan # Lots of destroys! ``` The following moves and removes will handle the state transitions necessary here. live/dev/ddb/moved.tf ```hcl moved { from = module.ddb.aws_dynamodb_table.asset_metadata to = aws_dynamodb_table.asset_metadata } ``` live/dev/ddb/removed.tf ```hcl removed { from = module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` live/dev/iam/moved.tf ```hcl moved { from = module.iam.aws_iam_policy.lambda_basic_execution to = aws_iam_policy.lambda_basic_execution } moved { from = module.iam.aws_iam_policy.lambda_dynamodb to = aws_iam_policy.lambda_dynamodb } moved { from = module.iam.aws_iam_policy.lambda_s3_read to = aws_iam_policy.lambda_s3_read } moved { from = module.iam.aws_iam_role.lambda_role to = aws_iam_role.lambda_role } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution to = aws_iam_role_policy_attachment.lambda_basic_execution } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb to = aws_iam_role_policy_attachment.lambda_dynamodb } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read to = aws_iam_role_policy_attachment.lambda_s3_read } ``` live/dev/iam/removed.tf ```hcl removed { from = module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` live/dev/lambda/moved.tf ```hcl moved { from = module.lambda.aws_lambda_function.main to = aws_lambda_function.main } moved { from = module.lambda.aws_lambda_function_url.main to = aws_lambda_function_url.main } ``` live/dev/lambda/removed.tf ```hcl removed { from = module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } ``` live/dev/s3/moved.tf ```hcl moved { from = module.s3.aws_s3_bucket.static_assets to = aws_s3_bucket.static_assets } ``` live/dev/s3/removed.tf ```hcl removed { from = module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` live/prod/ddb/moved.tf ```hcl moved { from = module.ddb.aws_dynamodb_table.asset_metadata to = aws_dynamodb_table.asset_metadata } ``` live/prod/ddb/removed.tf ```hcl removed { from = module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` live/prod/iam/moved.tf ```hcl moved { from = module.iam.aws_iam_policy.lambda_basic_execution to = aws_iam_policy.lambda_basic_execution } moved { from = module.iam.aws_iam_policy.lambda_dynamodb to = aws_iam_policy.lambda_dynamodb } moved { from = module.iam.aws_iam_policy.lambda_s3_read to = aws_iam_policy.lambda_s3_read } moved { from = module.iam.aws_iam_role.lambda_role to = aws_iam_role.lambda_role } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution to = aws_iam_role_policy_attachment.lambda_basic_execution } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb to = aws_iam_role_policy_attachment.lambda_dynamodb } moved { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read to = aws_iam_role_policy_attachment.lambda_s3_read } ``` live/prod/iam/removed.tf ```hcl removed { from = module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` live/prod/lambda/moved.tf ```hcl moved { from = module.lambda.aws_lambda_function.main to = aws_lambda_function.main } moved { from = module.lambda.aws_lambda_function_url.main to = aws_lambda_function_url.main } ``` live/prod/lambda/removed.tf ```hcl removed { from = module.s3.aws_s3_bucket.static_assets lifecycle { destroy = false } } removed { from = module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } ``` live/prod/s3/moved.tf ```hcl moved { from = module.s3.aws_s3_bucket.static_assets to = aws_s3_bucket.static_assets } ``` live/prod/s3/removed.tf ```hcl removed { from = module.ddb.aws_dynamodb_table.asset_metadata lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role.lambda_role lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_policy.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb lifecycle { destroy = false } } removed { from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function.main lifecycle { destroy = false } } removed { from = module.lambda.aws_lambda_function_url.main lifecycle { destroy = false } } ``` That was a ton of work! The effort of making these state moves might encourage you to do some early planning to avoid the need to do these kinds of state moves down the line as you plan your infrastructure estate. Folks sometimes feel like they don’t really want or need to adopt Terragrunt before they reach a point where scaling up IaC further becomes painful. Deciding to avoid learning Terragrunt before this point is a form of tech debt accrual. Doing the work up-front to follow the patterns that Terragrunt enables (like segmenting state at granular levels) helps to mitigate the severity of refactor work down the line. If we had architected our IaC ahead of time to use small, focused units, we never would have had to do the work of these state moves. Hopefully, going through these state moves in this guide gives you confidence that you *can* do it if you need to, however. As long as you move carefully, and know what you’re doing, you can break down even the largest Terraliths with time! ## Applying Updates [Section titled “Applying Updates”](#applying-updates) Now, let’s repeat our plan to confirm that we won’t destroy anything important. If you *do* see any destroys, you probably have something misconfigured in one of your `moved.tf` or `removed.tf` files. Review them carefully. live ```bash terragrunt run --all plan # No destroys! # You might see some creates, but that's a side-effect of how # OpenTofu tracks state internally. You are safe to ignore them. ``` Thankfully, now that we’ve segmented state we can carefully run across the `dev` units before running in `prod`, with *zero risk* that we’re going to accidentally break anything there. We can actually perform our updates *even more carefully* by updating one unit at a time, but that’s not really necessary for our use-case here. Consider when it might make sense to do that for your own real infrastructure, however. If you are doing state manipulation like this on stateful production resources like databases or blob stores for example, it’s a good idea to move slower to avoid data loss or outages. live/dev ```bash terragrunt run --all apply # Migration complete! ``` live/prod ```bash terragrunt run --all apply # Migration complete! ``` ## Trade-offs [Section titled “Trade-offs”](#trade-offs) You’ve now reached the most granular and arguably the safest way to structure a Terragrunt project (while remaining practical about avoiding over-segmenting resources). By breaking down each environment into component-specific units, you’ve moved from a “one state file per environment” model to a “one state file per component, per environment” model. This is a common and highly recommended pattern for mature Infrastructure as Code (IaC) management, but it comes with its own set of trade-offs. * Pros * **Safety and Granular Blast Radius**: This is the single biggest advantage. A change to a stateless resource that changes frequently, like the **Lambda function**, now has **zero chance of impacting a stateful resource** that changes rarely, like the **DynamoDB table** or **S3 bucket**. * **Reduced Lock Contention**: State locks are now per-component, meaning an `apply` on the Lambda function won’t block a simultaneous `apply` on the IAM role, enabling more concurrent infrastructure work by platform teams. * **Faster Feedback Loops**: When you run `terragrunt plan` inside a specific component directory (e.g., `live/dev/lambda`), OpenTofu only needs to refresh the state for that single component. This is significantly faster than refreshing the state for the entire environment, which is a huge productivity win on large projects. * Cons * **Increased Configuration Complexity**: The number of directories and `terragrunt.hcl` files has multiplied. While each file is simple, managing the overall structure requires more discipline. The cognitive load shifts from understanding a single large module to understanding how many small, interconnected modules form a complete system. * **Explicit Dependency Management**: You now *must* explicitly define the relationships between your components using `dependency` blocks. This is powerful but also creates another layer of configuration to maintain. Forgetting a dependency or referencing it incorrectly will cause failures. * **Mocking Outputs**: As demonstrated in the tutorial, you can’t `plan` a component that depends on another component that doesn’t exist yet. This necessitates using `mock_outputs` if you want to perform a `run --all plan` against a stack with unapplied dependencies, which is a powerful workaround but adds another concept that engineers must learn and manage correctly. ## Wrap Up [Section titled “Wrap Up”](#wrap-up) You’ve now taken modularity to the next level. Instead of one state file per environment, you now have one state file per *component* (S3, DDB, IAM, Lambda) within each environment. This provides the ultimate level of granular control and safety. You can now update your application’s Lambda function with zero risk of accidentally modifying your stateful database or storage bucket. The core lesson here was learning how to use the `dependency` block, Terragrunt’s mechanism for wiring together independent units by passing outputs from one unit as inputs to another. You also learned to use `mock_outputs` to solve the problem that arises when planning interdependent infrastructure that doesn’t exist yet. However, this safety came with a trade-off: a proliferation of `terragrunt.hcl` files across your codebase. In the next step, you will eliminate this final piece of boilerplate by using Terragrunt Stacks, which allow you to generate entire collections of units on-demand from a single `terragrunt.stack.hcl` file. # Step 7: Taking advantage of Terragrunt Stacks > Taking advantage of Terragrunt Stacks Up until relatively recently in the history of Terragrunt, the proliferation of `terragrunt.hcl` files was the trade-off platform engineers had to accept for state segmentation. Luckily, with the advent of [Terragrunt Stacks](/features/stacks/), that’s not the case anymore. Collections of units, like the ones you created in the last step, can be generated on-demand using `terragrunt.stack.hcl` files. This saves you from having to duplicate `terragrunt.hcl` files across your codebase. In this step, you will migrate to this modern pattern. You’ll start by persisting your unit definitions in your `catalog`. Then, you’ll replace the numerous `terragrunt.hcl` files in your `live` directory with a single `terragrunt.stack.hcl` file in each environment. To handle the slight configuration differences between `dev` and `prod`, you’ll use Terragrunt’s `values` attributes, which allow you to parameterize your reusable unit definitions. ## Tutorial [Section titled “Tutorial”](#tutorial) Let’s start migrating to Terragrunt Stacks by persisting unit definitions in the `catalog`. Tip We copy over `.terraform.lock.hcl` files in addition to `terragrunt.hcl` files here. Persisting `.terraform.lock.hcl` files is a best practice to ensure reproducibility of units, and to maximize the performance of OpenTofu (as OpenTofu relies on the availability of this file when determining whether it can safely reuse content in the provider cache directory). ```bash mkdir -p catalog/units/{ddb,iam,lambda,s3} cp live/dev/ddb/{terragrunt.hcl,.terraform.lock.hcl} catalog/units/ddb/ cp live/dev/iam/{terragrunt.hcl,.terraform.lock.hcl} catalog/units/iam/ cp live/dev/lambda/{terragrunt.hcl,.terraform.lock.hcl} catalog/units/lambda/ cp live/dev/s3/{terragrunt.hcl,.terraform.lock.hcl} catalog/units/s3/ ``` You might remember that the unit configurations differed very slightly between `dev` and `prod`. Luckily, Terragrunt has special tooling to handle that via the usage of `values` variables. catalog/units/ddb/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//ddb" } inputs = { name = values.name } ``` By specifying `values.name` there, we’re allowing values to be used in our unit configurations from `terragrunt.stack.hcl` files. You’ll see how these values are set later in this step. catalog/units/iam/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//iam" } dependency "s3" { config_path = values.s3_path mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:s3:::mock-bucket-name" } } dependency "ddb" { config_path = values.ddb_path mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:dynamodb:us-east-1:123456789012:table/mock-table-name" } } inputs = { name = values.name aws_region = values.aws_region s3_bucket_arn = dependency.s3.outputs.arn dynamodb_table_arn = dependency.ddb.outputs.arn } ``` Note We’re not just using the `values` variable for the inputs that differ between stacks, like the `name` and `aws_region`. We can use them for any value we want to substitute in our `terragrunt.hcl` files, including the relative paths to other units (which we might want to be dynamic if we’re going to refactor or add additional dependencies, etc.) catalog/units/lambda/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//lambda" } dependency "s3" { config_path = values.s3_path mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { name = "mock-bucket-name" } } dependency "ddb" { config_path = values.ddb_path mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { name = "mock-table-name" } } dependency "iam" { config_path = values.iam_path mock_outputs_allowed_terraform_commands = ["plan", "state"] mock_outputs_merge_strategy_with_state = "shallow" mock_outputs = { arn = "arn:aws:iam::123456789012:role/mock-role-name" } } inputs = { name = values.name aws_region = values.aws_region s3_bucket_name = dependency.s3.outputs.name dynamodb_table_name = dependency.ddb.outputs.name lambda_role_arn = dependency.iam.outputs.arn lambda_zip_file = "${get_repo_root()}/dist/best-cat.zip" } ``` catalog/units/s3/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "${find_in_parent_folders("catalog/modules")}//s3" } inputs = { name = values.name # Optional: Force destroy S3 buckets even when they have objects in them. force_destroy = try(values.force_destroy, false) } ``` Now we can replace the `terragrunt.hcl` files in `live` with a single `terragrunt.stack.hcl` file in each environment to generate them on-demand using `unit` blocks. By default, units generated by Terragrunt are generated into `.terragrunt-stack` directories. We opt out of that by setting `no_dot_terragrunt_stack` to `true`. live/dev/terragrunt.stack.hcl ```hcl locals { name = "best-cat-2025-09-24-2359-dev" aws_region = "us-east-1" units_path = find_in_parent_folders("catalog/units") } unit "ddb" { source = "${local.units_path}/ddb" path = "ddb" no_dot_terragrunt_stack = true values = { name = local.name } } unit "s3" { source = "${local.units_path}/s3" path = "s3" no_dot_terragrunt_stack = true values = { name = local.name # Optional: Force destroy S3 buckets even when they have objects in them. # You're generally advised not to do this with important infrastructure, # however this makes testing and cleanup easier for this guide. force_destroy = true } } unit "iam" { source = "${local.units_path}/iam" path = "iam" no_dot_terragrunt_stack = true values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" } } unit "lambda" { source = "${local.units_path}/lambda" path = "lambda" no_dot_terragrunt_stack = true values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" iam_path = "../iam" } } ``` We’ll also add this `.gitignore` file to avoid recommitting the generated files in our repository, as they’ll be regenerated whenever we need them. We’ll see how we can remove the need for this in a future step. live/dev/.gitignore ```hcl * !.gitignore !terragrunt.stack.hcl ``` Now that we can generate these unit configurations on demand, we can remove the copies that we created manually! live/dev ```bash rm -rf .terraform.lock.hcl ddb iam lambda s3 terragrunt run --all plan ``` All that’s left now is to repeat the same thing for prod. live/prod/terragrunt.stack.hcl ```hcl locals { name = "best-cat-2025-09-24-2359" aws_region = "us-east-1" units_path = find_in_parent_folders("catalog/units") } unit "ddb" { source = "${local.units_path}/ddb" path = "ddb" no_dot_terragrunt_stack = true values = { name = local.name } } unit "s3" { source = "${local.units_path}/s3" path = "s3" no_dot_terragrunt_stack = true values = { name = local.name # Optional: Force destroy S3 buckets even when they have objects in them. # You're generally advised not to do this with important infrastructure, # however this makes testing and cleanup easier for this guide. force_destroy = true } } unit "iam" { source = "${local.units_path}/iam" path = "iam" no_dot_terragrunt_stack = true values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" } } unit "lambda" { source = "${local.units_path}/lambda" path = "lambda" no_dot_terragrunt_stack = true values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" iam_path = "../iam" } } ``` live/prod/.gitignore ```hcl * !.gitignore !terragrunt.stack.hcl ``` live/prod ```bash rm -rf .terraform.lock.hcl ddb iam lambda s3 terragrunt run --all plan ``` ## Project Layout Check-in [Section titled “Project Layout Check-in”](#project-layout-check-in) If you clean out the `.gitignore`’ed files and take a look at the file tree, you should see that your `live` file count has shrunk down again! live ```bash rm -rf ./***/ddb ./***/iam ./***/lambda ./***/s3 ``` * live * dev * terragrunt.stack.hcl * prod * terragrunt.stack.hcl * root.hcl ## Trade-offs [Section titled “Trade-offs”](#trade-offs) You’ve now adopted one of Terragrunt’s most advanced features, **Terragrunt Stacks**, to achieve an exceptionally clean and DRY (Don’t Repeat Yourself) infrastructure codebase. By generating your component configurations on the fly from a central catalog, you’ve eliminated the last major source of boilerplate in your Terragrunt configurations. However, this abstraction comes with its own set of trade-offs. ### Pros [Section titled “Pros”](#pros) * **Maximum Reusability and Deduplication**: This is the most significant benefit of using Terragrunt Stacks. Instead of having multiple `terragrunt.hcl` files scattered across each environment’s subdirectories, you now have a single, reusable unit definition for each component in your `catalog`. Adding a new environment is as simple as creating a new `terragrunt.stack.hcl` and defining its unique inputs. * **Simplified `live` Directory**: Your `live` directory is now incredibly lean and easy to navigate. Each environment is represented by a single `terragrunt.stack.hcl` file, which serves as a clear manifest of all the components that make up that environment. This is a similar layout to that which was achieved in step 5, but we’ve gained the ability to retain state segmentation and operate granularly. * **Centralized Configuration Catalog**: If you need to update the configuration for a component across *all* environments (e.g., add a new dependency or change a `mock_output`), you only need to edit the corresponding file in `catalog/units`. This drastically reduces the chance of configuration drift and makes maintenance much easier. ### Cons [Section titled “Cons”](#cons) * **Increased Abstraction**: The biggest trade-off is the added layer of indirection. Engineers no longer have all the configuration for their Terragrunt units committed to their repository. If they want to read through their configurations, they need to generate the stack, or read the contents stored in their `catalog`. * **Steeper Learning Curve**: The concepts of `terragrunt.stack.hcl`, `unit` blocks, and the `values` attribute are powerful but are also more advanced Terragrunt features. Onboarding new team members may require more time to explain this higher level of abstraction compared to the more explicit file-based approach from the previous step. ## Wrap Up [Section titled “Wrap Up”](#wrap-up) You’ve conquered the final major source of Terragrunt boilerplate! In this step, you adopted one of Terragrunt’s most powerful features: **Terragrunt Stacks**. By centralizing your generic unit configurations into the `catalog`, you were able to replace the numerous `terragrunt.hcl` files in each environment with a single, clean `terragrunt.stack.hcl` file. The core concept you mastered was using the `values` object to **parameterize** these reusable units, allowing you to define a component’s configuration once and deploy it many times with environment-specific values. However, there’s one piece of technical debt left from our migration: the state paths in your S3 backend don’t yet align with Terragrunt’s default conventions, requiring those annoying `.gitignore` files. In the final step, you’ll perform one last set of state migrations to finalize your layout, fully mastering this guide. # Step 8: Refactoring state with Terragrunt Stacks > Refactoring state with Terragrunt Stacks You’ve just completed a major refactor using **Terragrunt Stacks**. However, there’s one final piece of technical debt remaining to complete this guide. To make the transition in the previous step smoother, we used the `no_dot_terragrunt_stack` attribute, which generated the unit configurations directly into directories like `dev/s3` and `prod/lambda`. While this worked perfectly fine for our migration, and is a recommended first step to adopting Terragrunt Stacks, it’s not the standard configuration you would arrive at if you wrote the configurations by hand. By default, Terragrunt generates unit configurations into a hidden `.terragrunt-stack` directory within each environment. This keeps your generated code is neatly tucked away and easily ignored by version control. Our current setup requires `.gitignore` files in each stack directory to prevent committing this generated code. In this final step, you will perform one last state migration to align your project with Terragrunt’s best practices. You will remove the `no_dot_terragrunt_stack` attribute and move your state to match the default, conventional directory structure. ## Tutorial [Section titled “Tutorial”](#tutorial) To review, this is what our S3 layout looks like for our state (ignoring the state that we’ve left behind during our refactors): ```bash $ aws s3 ls --recursive s3://[your-state-bucket] | awk '{print $4}' | rg -v '^tofu.tfstate$' | rg -v '^dev/tofu.tfstate$' | rg -v '^prod/tofu.tfstate$' dev/ddb/tofu.tfstate dev/iam/tofu.tfstate dev/lambda/tofu.tfstate dev/s3/tofu.tfstate prod/ddb/tofu.tfstate prod/iam/tofu.tfstate prod/lambda/tofu.tfstate prod/s3/tofu.tfstate ``` What we’d like our state keys to look like is the following, which is how it would look if we provisioned our stack without usage of `no_dot_terragrunt_stack` from the beginning: ```bash dev/.terragrunt-stack/ddb/tofu.tfstate dev/.terragrunt-stack/iam/tofu.tfstate dev/.terragrunt-stack/lambda/tofu.tfstate dev/.terragrunt-stack/s3/tofu.tfstate prod/.terragrunt-stack/ddb/tofu.tfstate prod/.terragrunt-stack/iam/tofu.tfstate prod/.terragrunt-stack/lambda/tofu.tfstate prod/.terragrunt-stack/s3/tofu.tfstate ``` Given that there’s a close relationship between filesystem layout and backend state keys, we can achieve this by having our units generated into the default `.terragrunt-stack` directories instead of being generated directly adjacent to `terragrunt.stack.hcl` files. What we’ll want to do is perform state migration while having both unit layouts generated locally. If you remember earlier steps, the way to that this is to use the `state pull` and `state push` commands. First, let’s make sure we have our stack generated as-is without removing the `no_dot_terragrunt_stack` attribute. live ```bash terragrunt stack generate 16:36:50.794 INFO Generating stack from ./dev/terragrunt.stack.hcl 16:36:50.797 INFO Generating stack from ./prod/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit s3 from ./dev/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit ddb from ./dev/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit lambda from ./dev/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit iam from ./dev/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit lambda from ./prod/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit iam from ./prod/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit ddb from ./prod/terragrunt.stack.hcl 16:36:50.798 INFO Processing unit s3 from ./prod/terragrunt.stack.hcl ``` Now let’s edit our `terragrunt.stack.hcl` files to remove the `no_dot_terragrunt_stack` attribute. This will generate units into the desired final directory structure. live/dev/terragrunt.stack.hcl ```hcl locals { name = "best-cat-2025-09-24-2359-dev" aws_region = "us-east-1" units_path = find_in_parent_folders("catalog/units") } unit "ddb" { source = "${local.units_path}/ddb" path = "ddb" values = { name = local.name } } unit "s3" { source = "${local.units_path}/s3" path = "s3" values = { name = local.name # Optional: Force destroy S3 buckets even when they have objects in them. # You're generally advised not to do this with important infrastructure, # however this makes testing and cleanup easier for this guide. force_destroy = true } } unit "iam" { source = "${local.units_path}/iam" path = "iam" values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" } } unit "lambda" { source = "${local.units_path}/lambda" path = "lambda" values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" iam_path = "../iam" } } ``` live/prod/terragrunt.stack.hcl ```hcl locals { name = "best-cat-2025-09-24-2359" aws_region = "us-east-1" units_path = find_in_parent_folders("catalog/units") } unit "ddb" { source = "${local.units_path}/ddb" path = "ddb" values = { name = local.name } } unit "s3" { source = "${local.units_path}/s3" path = "s3" values = { name = local.name # Optional: Force destroy S3 buckets even when they have objects in them. # You're generally advised not to do this with important infrastructure, # however this makes testing and cleanup easier for this guide. force_destroy = true } } unit "iam" { source = "${local.units_path}/iam" path = "iam" values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" } } unit "lambda" { source = "${local.units_path}/lambda" path = "lambda" values = { name = local.name aws_region = local.aws_region s3_path = "../s3" ddb_path = "../ddb" iam_path = "../iam" } } ``` Now let’s generate again to get that generated `.terragrunt-stack` directory. live ```bash terragrunt stack generate ``` ## Project Layout Check-in [Section titled “Project Layout Check-in”](#project-layout-check-in) Should see a layout like the following now, with both a stack generated within `.terragrunt-stack` and one generated outside of it: * live * dev * **.terragrunt-stack** * **ddb** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * **iam** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * **lambda** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * **s3** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * ddb * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * iam * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * lambda * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * s3 * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * terragrunt.stack.hcl * prod * **.terragrunt-stack** * **ddb** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * **iam** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * **lambda** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * **s3** * **.terraform.lock.hcl** * **.terragrunt-stack-manifest** * **terragrunt.hcl** * **terragrunt.values.hcl** * ddb * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * iam * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * lambda * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * s3 * .terraform.lock.hcl * .terragrunt-stack-manifest * terragrunt.hcl * terragrunt.values.hcl * terragrunt.stack.hcl ## Applying Updates [Section titled “Applying Updates”](#applying-updates) To migrate state from the old unit paths to the new paths, we can run the following: live ```bash # Migrate dev state cd dev/ddb terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/ddb terragrunt state push /tmp/tofu.tfstate cd ../../s3 terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/s3 terragrunt state push /tmp/tofu.tfstate cd ../../iam terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/iam terragrunt state push /tmp/tofu.tfstate cd ../../lambda terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/lambda terragrunt state push /tmp/tofu.tfstate # Migrate prod state cd ../../../prod/ddb terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/ddb terragrunt state push /tmp/tofu.tfstate cd ../../s3 terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/s3 terragrunt state push /tmp/tofu.tfstate cd ../../iam terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/iam terragrunt state push /tmp/tofu.tfstate cd ../../lambda terragrunt state pull > /tmp/tofu.tfstate cd ../.terragrunt-stack/lambda terragrunt state push /tmp/tofu.tfstate ``` We can now remove the `.gitignore` files, and prove to ourselves that state has migrated successfully! live ```bash rm -rf ./***/.gitignore ./***/ddb ./***/iam ./***/lambda ./***/s3 terragrunt run --all plan # No changes! ``` ## Trade-offs [Section titled “Trade-offs”](#trade-offs) This final refactor brings your project into alignment with Terragrunt’s standard conventions, but there are some minor trade-offs to consider. ### Pros [Section titled “Pros”](#pros) * **Cleaner Working Directory**: The most significant advantage is the cleanliness of your `live` directory. All generated code now resides in a hidden `.terragrunt-stack` directory, leaving your environment folders (e.g., `live/dev`) containing only your manually-managed `terragrunt.stack.hcl` file. * **Simplified Version Control**: You can now remove the environment-specific `.gitignore` files. A single, global entry to ignore `.terragrunt-stack` and `.terragrunt-cache` is all that’s needed, making your version control rules simpler and more reliable. ### Cons [Section titled “Cons”](#cons) * **State Migration**: The primary cost is the one-time effort of performing the state migration. While powerful, any direct state manipulation requires careful execution to avoid errors. This refactor is an investment of time and attention to detail. * **Tooling Requirements**: If you currently use a CI/CD tool that supports Terragrunt, it has to have built-in awareness of how Terragrunt Stack generation, like [Gruntwork Pipelines](https://www.gruntwork.io/platform/pipelines). CI/CD tools that have been around for a long while might not prioritize handling stack generation, and lack support as a consequence. Placing all generated units in a `.gitignore` file, CI/CD tools might not be able to track when units change, and make selective changes to IaC. ## Wrap Up [Section titled “Wrap Up”](#wrap-up) This final step was about aligning with Terragrunt’s standard conventions. By removing the `no_dot_terragrunt_stack` attribute, you enabled Terragrunt’s default behavior of generating code into a hidden `.terragrunt-stack` directory. This required one last, careful state migration. You used **`terragrunt state pull`** to download state from old unit keys and **`terragrunt state push`** to the new, conventional backend keys that matched the updated directory structure from stack generation. Your project is now not only easy to manage but also immediately familiar to any engineer experienced with Terragrunt, featuring a state backend structure aligned with your filesystem. # Wrap Up > Wrap Up Congratulations on making it through the Terralith to Terragrunt guide! You’ve successfully navigated the entire journey from a cumbersome Terralith to a clean, scalable, and maintainable IaC setup managed by Terragrunt. This process mirrors the real-world challenges many teams face as their infrastructure grows, and the skills you’ve developed are invaluable for managing IaC effectively at any scale. Along the way, you’ve practiced and mastered critical techniques that will serve you well on your path to IaC mastery. You now have hands-on experience with: * **Advanced State Manipulation**: Safely refactoring your infrastructure’s state without destroying and recreating critical resources. You’ve used OpenTofu’s `moved` and `removed` blocks and `state pull` and `state push` commands to migrate state between different file system layouts and backend configurations. * **Modular Infrastructure Design**: Breaking down a monolithic configuration into smaller, reusable modules with clear, encapsulated APIs. You learned to structure your project with a `catalog` of components that can be consumed by your `live` infrastructure. * **Boilerplate Reduction with Terragrunt**: Eliminating repetitive code by using `terragrunt.hcl` to generate provider and backend configurations, define module sources, and pass inputs dynamically using functions like `get_repo_root()` and `path_relative_to_include()`. * **Dependency Management**: Orchestrating complex deployments across multiple, isolated state files. You learned to use Terragrunt’s `dependency` blocks to pass outputs from one component as inputs to another, and how to use `mock_outputs` to enable successful planning even before dependencies have been applied. * **Dynamic Stack Generation**: Transitioning from manually managing individual `terragrunt.hcl` files to defining entire environments on-demand with a single `terragrunt.stack.hcl` file, making your infrastructure definitions radically simpler and easier to maintain. If you’d like to continue practicing on your own, consider how you might continue experimenting with this configuration. * **Nested Stacks**: What if you wanted to manage both the `dev` and `prod` environments from a single, top-level file using the [Environment-based Stacks pattern](https://docs.terragrunt.com/features/stacks/explicit)? Think about how you would refactor your current setup to achieve this. Would you need to perform more state manipulation? If so, which techniques from this guide would you leverage to accomplish it safely? Tip Review the state migration steps throughout this guide! * **Integrating Terragrunt Into Your Build System**: In this guide, the artifact that’s deployed to Lambda is manually packaged by you before provisioning any infrastructure. Terragrunt has special tooling in [hooks](https://docs.terragrunt.com/features/units/hooks/) to automate manual tasks like this for you on-demand as you interact with your infrastructure. How would you integrate that into the configurations you’ve built so far? Tip You might find an example of this in our [example catalog](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/examples/terragrunt)! * **Testing Our IaC**: If you were to use this infrastructure in production, you might want the underlying patterns (and perhaps the live infrastructure) tested automatically, rather than just applying the IaC and manually verifying that it is working correctly. How would you go about doing that? Tip You might find a helpful tool to accomplish that in [Terratest](https://terratest.gruntwork.io/)! * **Automating Plan Assessments**: Throughout this guide, you engaged in careful manual evaluation of plans, and it was hinted early on that tools like [OPA](https://www.openpolicyagent.org/) provide a way to automatically assess the risk of plans. How would you integrate them into Terragrunt? Tip You might find it useful to review the documentation on [hooks](https://docs.terragrunt.com/features/units/hooks/)! This was a simple example, but if you’re struggling with a Terralith, your use-case is almost definitely more complex! Gruntwork offers support for assisting customers transition out of Terraliths into maintainable, best practices IaC codebases. Send an email to for [Terragrunt Enterprise Support](https://www.gruntwork.io/services/terragrunt) for more information. # Bare Include > Migration guide to avoid using bare includes ## Migrating from bare includes [Section titled “Migrating from bare includes”](#migrating-from-bare-includes) The earliest form of include support in Terragrunt was a bare include. e.g. terragrunt.hcl ```hcl include { path = find_in_parent_folders("root.hcl") } ``` Once Terragrunt supported the ability to define multiple includes, and to expose the values in includes as variables, users could optionally use named includes instead of a bare include. e.g. terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } ``` HCL parsing does not support the ability to parse HCL configuration and accept that a configuration block has zero or more attributes, so a workaround in Terragrunt internals was to parse the configuration, then rewrite it internally to avoid breaking backwards compatibility for bare includes. e.g. terragrunt.hcl ```hcl include { path = find_in_parent_folders("root.hcl") } ``` becomes: terragrunt.hcl ```hcl include "" { path = find_in_parent_folders("root.hcl") } ``` Especially on large projects, this extra work is not worth the performance penalty, and Terragrunt has deprecated support for bare includes. In a future version of Terragrunt, users will be required to use named includes for all includes. # CLI Redesign > Migration guide to adopt changes from RFC ## Background [Section titled “Background”](#background) As part of the redesign in [#3445](https://github.com/gruntwork-io/terragrunt/issues/3445), several CLI adjustments have been made to improve user experience and consistency. This guide will help you understand the changes and how to adapt to them. Note that this guide is being written while deprecations are in effect, so some of the changes may not be breaking yet. We’ll do our best to keep this guide up to date as the changes are finalized. To opt in to breaking changes early, you can use the relevant [strict control](/reference/strict-controls/). The high-level changes made as a part of that RFC that require migration for current users are as follows: * The `terragrunt-` prefix has been removed from all flags. * All environment variables have had their prefixes renamed to `TG_` instead of `TERRAGRUNT_`. * A new `run` command has been introduced to the CLI that also handles the responsibilities of deprecated `run-all` and `graph` commands. * A new `backend` command has been introduced to support users in interacting with backends. * Infrequently used commands have been reorganized into a structure that makes it easier to find them. * Arguments are no longer sent to `tofu` / `terraform` by default. ## Migration guide [Section titled “Migration guide”](#migration-guide) All of the changes you need to make to adopt to this new CLI design involve changing how you invoke Terragrunt from the command line. ### Remove `terragrunt-` prefix from flags [Section titled “Remove terragrunt- prefix from flags”](#remove-terragrunt--prefix-from-flags) If you are currently using flags that are prefixed with `terragrunt-`, you will need to stop using that flag, and use a differently named one instead (usually the same exact flag with `terragrunt-` removed from the beginning, but not always). For example, if you are using the `--terragrunt-non-interactive` flag, you will need to switch to the [`--non-interactive`](/reference/cli/global-flags/#non-interactive) flag instead. Before: ```bash terragrunt plan --terragrunt-non-interactive ``` After: ```bash terragrunt plan --non-interactive ``` Sometimes, the flag change might be slightly more involved than simply removing the `terragrunt-` prefix. For example, if you are using the `--terragrunt-debug` flag, you will need to switch to the [`--inputs-debug`](/reference/cli/commands/run/#inputs-debug) flag instead. Before: ```bash terragrunt plan --terragrunt-debug ``` After: ```bash terragrunt plan --inputs-debug ``` You can find the new flag names in the [CLI reference](/reference/cli/) (including the deprecated flags they replace). ### CLI Flag Migration Table [Section titled “CLI Flag Migration Table”](#cli-flag-migration-table) Below is a comprehensive mapping of old CLI flag names to their modern counterparts: | Old Flag | New Flag | | ------------------------------------------------- | --------------------------------------------------------- | | `--terragrunt-check` | `--check` | | `--terragrunt-config` | `--config` | | `--terragrunt-debug` | `--inputs-debug` | | `--terragrunt-diff` | `--diff` | | `--terragrunt-disable-bucket-update` | `--disable-bucket-update` | | `--terragrunt-disable-command-validation` | `--disable-command-validation` (deprecated) | | `--terragrunt-download-dir` | `--download-dir` | | `--terragrunt-exclude-dir` | `--queue-exclude-dir` | | `--terragrunt-excludes-file` | `--queue-excludes-file` | | `--terragrunt-fail-on-state-bucket-creation` | removed (no equivalent; backend provisioning is explicit) | | `--terragrunt-fetch-dependency-output-from-state` | `--dependency-fetch-output-from-state` | | `--terragrunt-forward-tf-stdout` | `--tf-forward-stdout` | | `--terragrunt-hclfmt-exclude-dir` | `--exclude-dir` | | `--terragrunt-hclfmt-file` | `--file` | | `--terragrunt-hclfmt-stdin` | `--stdin` | | `--terragrunt-hclvalidate-json` | `--json` | | `--terragrunt-hclvalidate-show-config-path` | `--show-config-path` | | `--terragrunt-iam-assume-role-duration` | `--iam-assume-role-duration` | | `--terragrunt-iam-role` | `--iam-assume-role` | | `--terragrunt-iam-web-identity-token` | `--iam-assume-role-web-identity-token` | | `--terragrunt-ignore-dependency-errors` | `--queue-ignore-errors` | | `--terragrunt-ignore-dependency-order` | `--queue-ignore-dag-order` | | `--terragrunt-ignore-external-dependencies` | `--queue-exclude-external` (deprecated) | | `--terragrunt-include-dir` | `--queue-include-dir` | | `--terragrunt-include-external-dependencies` | `--queue-include-external` | | `--terragrunt-json-disable-dependent-modules` | `--disable-dependent-modules` (deprecated) | | `--terragrunt-json-out-dir` | `--json-out-dir` | | `--terragrunt-json-out` | `--out` | | `--terragrunt-log-custom-format` | `--log-custom-format` | | `--terragrunt-log-disable` | `--log-disable` | | `--terragrunt-log-format` | `--log-format` | | `--terragrunt-log-level` | `--log-level` | | `--terragrunt-log-show-abs-paths` | `--log-show-abs-paths` | | `--terragrunt-modules-that-include` | `--units-that-include` (deprecated) | | `--terragrunt-no-auto-approve` | `--no-auto-approve` | | `--terragrunt-no-auto-init` | `--no-auto-init` | | `--terragrunt-no-auto-retry` | `--no-auto-retry` | | `--terragrunt-no-color` | `--no-color` | | `--terragrunt-no-destroy-dependencies-check` | `--destroy-dependencies-check` | | `--terragrunt-out-dir` | `--out-dir` | | `--terragrunt-parallelism` | `--parallelism` | | `--terragrunt-provider-cache-dir` | `--provider-cache-dir` | | `--terragrunt-provider-cache-hostname` | `--provider-cache-hostname` | | `--terragrunt-provider-cache-port` | `--provider-cache-port` | | `--terragrunt-provider-cache-registry-names` | `--provider-cache-registry-names` | | `--terragrunt-provider-cache-token` | `--provider-cache-token` | | `--terragrunt-provider-cache` | `--provider-cache` | | `--terragrunt-queue-include-units-reading` | `--queue-include-units-reading` | | `--terragrunt-source-map` | `--source-map` | | `--terragrunt-source-update` | `--source-update` | | `--terragrunt-source` | `--source` | | `--terragrunt-strict-include` | `--queue-strict-include` (deprecated) | | `--terragrunt-strict-validate` | `--strict-validate` | | `--terragrunt-tfpath` | `--tf-path` | | `--terragrunt-use-partial-parse-config-cache` | `--use-partial-parse-config-cache` | | `--terragrunt-working-dir` | `--working-dir` | | `--terragrunt-non-interactive` | `--non-interactive` | ### Update environment variables [Section titled “Update environment variables”](#update-environment-variables) If you are currently using environment variables to configure Terragrunt, you will need to stop using that environment variable, and use a differently named one instead (usually the same exact environment variable with `TERRAGRUNT_` replaced with `TG_`, but not always). For example, if you are using the `TERRAGRUNT_NON_INTERACTIVE` environment variable, you will need to switch to the [`TG_NON_INTERACTIVE`](/reference/cli/global-flags/#non-interactive) environment variable instead. Before: ```bash export TERRAGRUNT_NON_INTERACTIVE=true ``` After: ```bash export TG_NON_INTERACTIVE=true ``` Sometimes, the environment variable change might be slightly more involved than simply replacing `TERRAGRUNT_` with `TG_`. For example, if you are using the `TERRAGRUNT_DEBUG` environment variable, you will need to switch to the [`TG_INPUTS_DEBUG`](/reference/cli/commands/run/#inputs-debug) environment variable instead. Before: ```bash export TERRAGRUNT_DEBUG=true ``` After: ```bash export TG_INPUTS_DEBUG=true ``` You can find the new environment variable names in the [CLI reference](/reference/cli/) (including the deprecated environment variables they replace). ### Use the new `run` command [Section titled “Use the new run command”](#use-the-new-run-command) Default behavior change (v0.88.0): Terragrunt no longer forwards unknown commands to OpenTofu/Terraform by default. If you previously ran commands like `terragrunt workspace ls`, use the explicit `run` form instead: ```bash terragrunt run -- workspace ls ``` The `run` command has been introduced to the CLI to handle the responsibility currently held by the default command in Terragrunt. If you want to tell Terragrunt that what you are running is a command in the orchestrated IaC tool (OpenTofu/Terraform), you can use the `run` command to explicitly indicate this. For example, if you are currently using the `terragrunt` command to run `plan`, you can switch to the `run plan` command instead. Before: ```bash terragrunt plan ``` After: ```bash terragrunt run plan ``` Note that certain shortcuts will be supported out of the box, such as `terragrunt plan`, so you can continue to use most `run` commands without the `run` keyword, as you were doing before. For example, the following command will continue to work as expected: ```bash terragrunt plan ``` The commands that will not receive shortcuts are OpenTofu/Terraform commands that are not recommended for usage with Terragrunt, or have a conflict with the Terragrunt CLI API. For example, the `workspace` command will not receive a shortcut, as you are encouraged not to use workspaces when working with Terragrunt. Terragrunt manages state isolation for you, so you don’t need to use them. If you would like to explicitly run a command that does not have a shortcut, you can use the `run` command to do so. We recommend separating Terragrunt flags from OpenTofu/Terraform arguments with `--`: ```bash terragrunt run -- workspace ls ``` Similarly, commands like `graph` won’t be supported as a shortcut, as `graph` is a now deprecated command in the Terragrunt CLI. Supporting it as a shortcut would be misleading, so you can use the `run` command to run it explicitly: ```bash terragrunt run graph ``` You might want to explicitly indicate that the flag you are using is one for OpenTofu/Terraform, and not a Terragrunt flag. To do this, you can use the `--` argument to explicitly separate the Terragrunt flags from the OpenTofu/Terraform flags: ```bash terragrunt run -- apply -auto-approve ``` This usually isn’t necessary, except when combining a complicated series of flags and arguments, which can be difficult to parse for the CLI. In addition to allowing for explicit invocation of OpenTofu/Terraform instead of using shortcuts, the `run` command also takes on the responsibilities of the now deprecated `run-all` and `graph` commands using flags. For example, if you are currently using the `terragrunt run-all` command, you can switch to the `run` command with the `--all` flag instead. Before: ```bash terragrunt run-all plan ``` After: ```bash terragrunt run --all plan ``` ### Take advantage of the new `exec` command [Section titled “Take advantage of the new exec command”](#take-advantage-of-the-new-exec-command) Previously, Terragrunt only supported orchestrating the `tofu` and `terraform` binaries as the main program being executed when Terragrunt was invoked. With the introduction of the new [exec](/reference/cli/commands/exec/) command, this is no longer the case. You can now orchestrate any program you want, and integrate it with Terragrunt’s ability to fetch outputs, download OpenTofu/Terraform modules, set `inputs`, and more. For example, if you want to use Terragrunt to list the contents of an AWS S3 bucket, you can do the following: ```bash terragrunt exec -- bash -c 'aws s3 ls s3://$TF_VAR_bucket_name' ``` With the following `terragrunt.hcl` file: ```hcl inputs = { bucket_name = "my-bucket" } ``` Terragrunt will load the `inputs` for the unit, and make them available as `TF_VAR_` prefixed environment variables for the executed command. This offers a flexible way to integrate Terragrunt with other tools, besides just OpenTofu/Terraform for simple operational tasks. ### Use the new `backend` capabilities [Section titled “Use the new backend capabilities”](#use-the-new-backend-capabilities) Previously, Terragrunt would automatically provision any backend resources defined in the [remote\_state](/reference/hcl/blocks#remote_state) block of a `terragrunt.hcl` file. This was a source of confusion for many users, as it was potentially performing additional actions that users did not intend without asking for it. As part of the CLI Redesign, Terragrunt now supports a dedicated [backend command](/reference/cli/commands/backend/bootstrap/) to handle processes involved with interacting with OpenTofu/Terraform backends. This includes the ability to bootstrap (provision) backend resources, migrate state between backend state files, and delete backend state files. In addition, the `--backend-bootstrap` flag has been introduced, which preserves the legacy behavior of automatically provisioning backend resources. As this flag requires explicit opt in, you will want to explicitly set this flag (or the corresponding environment variable `TG_BACKEND_BOOTSTRAP` to `true`) if you want to continue to have Terragrunt automatically provision backend resources. Before: ```bash terragrunt plan ``` After: ```bash terragrunt plan --backend-bootstrap ``` ### Use the new `find` and `list` commands [Section titled “Use the new find and list commands”](#use-the-new-find-and-list-commands) The [find](/reference/cli/commands/find/) and [list](/reference/cli/commands/list/) commands have been introduced to help you discover configurations in your Terragrunt projects. The `find` command is useful when you want to perform programmatic discovery of a Terragrunt unit or configuration of that unit, and the `list` command is useful when you want to get a high-level overview of the Terragrunt units and configurations in your project. If you are currently using the `output-module-groups` command, you can switch to the `find --dag --json` command to get a more fine grained outlook on the nature of your Terragrunt configurations. You can also use the `list --dag --tree` command to get a better overview of how your units interact in the Directed Acyclic Graph (DAG) of Terragrunt units. Before: ```bash terragrunt output-module-groups { "Group 1": [ "/absolute/path/to/vpc" ], "Group 2": [ "/absolute/path/to/db" ], "Group 3": [ "/absolute/path/to/ec2" ] } ``` After: ```bash terragrunt find --dag --json [ { "type": "unit", "path": "vpc" }, { "type": "unit", "path": "db" }, { "type": "unit", "path": "ec2" } ] ``` ```bash terragrunt list --dag --tree . ╰── vpc ├── db │ ╰── ec2 ╰── ec2 ``` You might be wondering why these commands no longer reference “Groups”. That’s because the concurrency model of Terragrunt will change in a future release (see [#3629](https://github.com/gruntwork-io/terragrunt/issues/3629)), and at that point, Terragrunt will no longer run units in “Groups” of units, but instead run each unit when it is free to run. If you are currently relying on the `output-module-groups` to programmatically determine when units can run, you’ll want to switch to using the `find --dag --json --dependencies` command to get a detailed breakdown of dependencies between units, and use that information to determine when units can run. ```bash terragrunt find --dag --json --dependencies [ { "type": "unit", "path": "vpc" }, { "type": "unit", "path": "db", "dependencies": [ "vpc" ] }, { "type": "unit", "path": "ec2", "dependencies": [ "vpc", "db" ] } ] ``` ### Use the newly renamed commands [Section titled “Use the newly renamed commands”](#use-the-newly-renamed-commands) Aside from the adjustments listed above, you’ll also want to replace usage of deprecated commands with their newly renamed counterparts. * `hclfmt` (use `hcl fmt` instead) * `hclvalidate` (use `hcl validate` instead) * `validate-inputs` (use `hcl validate --inputs` instead) * `terragrunt-info` (use `info print` instead) * `render-json` (use `render --json -w` instead) * `graph-dependencies` (use `dag graph` instead) * `run-all` (use `run --all` instead) * `graph` (use `run --graph` instead) # Migrating from Deprecated Attributes > Learn how to migrate from the deprecated skip and retryable_errors attributes to their modern replacements. This guide explains how to migrate from the deprecated `skip` and `retryable_errors` attributes to their modern, more powerful replacements. Terragrunt has deprecated two attributes in favor of more flexible block-based configurations: * **`skip`** → Use the **`exclude`** block instead * **`retryable_errors`** → Use the **`errors`** block with **`retry`** sub-blocks instead These new blocks provide more granular control and composability compared to the simple attributes they replace. ## Migrating from `skip` to `exclude` [Section titled “Migrating from skip to exclude”](#migrating-from-skip-to-exclude) ### Why was `skip` deprecated? [Section titled “Why was skip deprecated?”](#why-was-skip-deprecated) The `skip` attribute was a simple boolean that would exclude a unit from the run queue. The new `exclude` block provides much more flexibility: * Exclude the unit only for specific OpenTofu/Terraform commands (e.g., only `plan` but not `apply`) * Use conditional logic to determine when to exclude the unit * Combine multiple conditions * Better integration with other Terragrunt features ### Basic Migration [Section titled “Basic Migration”](#basic-migration) **Before:** ```hcl skip = true ``` **After:** ```hcl exclude { if = true actions = ["all"] } ``` ### Conditional Skip [Section titled “Conditional Skip”](#conditional-skip) **Before:** ```hcl skip = get_env("ENVIRONMENT") == "production" ``` **After:** ```hcl exclude { if = get_env("ENVIRONMENT") == "production" actions = ["all"] } ``` ### Skip Specific Actions [Section titled “Skip Specific Actions”](#skip-specific-actions) The new `exclude` block allows you to exclude the unit only for specific OpenTofu/Terraform commands: ```hcl exclude { if = get_env("SKIP_DESTROY") == "true" actions = ["destroy"] } ``` This wasn’t possible with the old `skip` attribute! ## Migrating from `retryable_errors` to `errors` Block [Section titled “Migrating from retryable\_errors to errors Block”](#migrating-from-retryable_errors-to-errors-block) ### Why was `retryable_errors` deprecated? [Section titled “Why was retryable\_errors deprecated?”](#why-was-retryable_errors-deprecated) The `retryable_errors` attribute was a simple list of error patterns. The new `errors` block with `retry` sub-blocks provides: * **Multiple retry configurations** with different patterns and settings * **Named retry blocks** for better documentation * **Per-retry configuration** of max attempts and sleep intervals * **Composability** - combine multiple retry strategies * **Better organization** for complex retry logic ### Basic Migration [Section titled “Basic Migration”](#basic-migration-1) **Before:** ```hcl retryable_errors = [ ".*Error: transient network issue.*", ".*Error: timeout.*" ] retry_max_attempts = 3 retry_sleep_interval_sec = 5 ``` **After:** ```hcl errors { retry "transient_errors" { retryable_errors = [ ".*Error: transient network issue.*", ".*Error: timeout.*" ] max_attempts = 3 sleep_interval_sec = 5 } } ``` ### Using Default Retryable Errors [Section titled “Using Default Retryable Errors”](#using-default-retryable-errors) If you were using the `get_default_retryable_errors()` function: **Before:** ```hcl retryable_errors = concat( get_default_retryable_errors(), [".*custom error.*"] ) ``` **After:** ```hcl errors { retry "default_errors" { retryable_errors = get_default_retryable_errors() max_attempts = 3 sleep_interval_sec = 5 } retry "custom_errors" { retryable_errors = [".*custom error.*"] max_attempts = 5 sleep_interval_sec = 10 } } ``` Note: The `get_default_retryable_errors()` function still works and returns the default list for use within the `errors` block. ### Multiple Retry Strategies [Section titled “Multiple Retry Strategies”](#multiple-retry-strategies) The new `errors` block allows you to define different retry strategies for different types of errors: ```hcl errors { # Quick retries for transient network issues retry "network_errors" { retryable_errors = [ ".*connection reset.*", ".*timeout.*" ] max_attempts = 5 sleep_interval_sec = 2 } # Slower retries for rate limiting retry "rate_limit_errors" { retryable_errors = [ ".*rate limit exceeded.*", ".*too many requests.*" ] max_attempts = 10 sleep_interval_sec = 30 } # Few retries for potential transient API issues retry "api_errors" { retryable_errors = [ ".*internal server error.*" ] max_attempts = 3 sleep_interval_sec = 15 } } ``` This level of granularity wasn’t possible with the old `retryable_errors` attribute! ## Error Messages [Section titled “Error Messages”](#error-messages) If you try to use the deprecated attributes, Terragrunt will fail with an HCL parsing error: **For `skip` attribute:** ```plaintext Error: Unsupported argument on terragrunt.hcl line 2: 2: skip = true An argument named "skip" is not expected here. ``` **For `retryable_errors` attribute:** ```plaintext Error: Unsupported argument on terragrunt.hcl line 4: 4: retryable_errors = [".*Error: transient.*"] An argument named "retryable_errors" is not expected here. ``` These errors indicate that the attributes have been completely removed from Terragrunt. Please refer to the migration examples below. ## How Retry Errors Are Collected [Section titled “How Retry Errors Are Collected”](#how-retry-errors-are-collected) When you define multiple `retry` blocks within the `errors` block, Terragrunt automatically collects **all** the `retryable_errors` patterns from all retry blocks and uses them for error matching. **Example:** ```hcl errors { retry "network_errors" { retryable_errors = [".*timeout.*", ".*connection reset.*"] max_attempts = 5 sleep_interval_sec = 2 } retry "api_errors" { retryable_errors = [".*rate limit.*", ".*429.*"] max_attempts = 10 sleep_interval_sec = 30 } } ``` In this example, Terragrunt will retry on any error matching: * `.*timeout.*` * `.*connection reset.*` * `.*rate limit.*` * `.*429.*` Each retry block can have its own `max_attempts` and `sleep_interval_sec`, allowing fine-grained control over retry behavior for different error types—for example, one block can retry at 2-second intervals while another uses 30-second intervals. ## Further Reading [Section titled “Further Reading”](#further-reading) * [Exclude Block Reference](/reference/hcl/blocks#exclude) * [Errors Block Reference](/reference/hcl/blocks#errors) * [All Attributes Reference](/reference/hcl/attributes) # Migrating from root terragrunt.hcl > Migrate from using a root `terragrunt.hcl` file. ## Problem [Section titled “Problem”](#problem) The recommended best practice for Terragrunt usage was previously that users defined two types of `terragrunt.hcl` files for any significantly large code base: 1. A root `terragrunt.hcl` file that defined the Terragrunt configuration common to all units in the code base. 2. Child `terragrunt.hcl` files that defined the Terragrunt configuration specific to each [unit](/getting-started/terminology/#unit) in the code base. This was a simple pattern that made it very obvious what these files were used for (Terragrunt), and certain Terragrunt features (like `find_in_parent_folders`) assumed this default structure. Over time, this caused confusion for users of Terragrunt, however. See [#3181](https://github.com/gruntwork-io/terragrunt/issues/3181) for an example of the confusion this has caused. At the end of the day, from a functional perspective, it doesn’t actually help users to have the root configuration named `terragrunt.hcl`. It makes it more confusing to determine what is shared configuration and what is configuration for a unit. It also complicates Terragrunt usage, as commands like `run --all` need to be run from a directory where all child directories will be `terragrunt.hcl` files corresponding to units that need to be run. ## Recommended Solution [Section titled “Recommended Solution”](#recommended-solution) To simplify Terragrunt usage and make it more clear what the root configuration is, it is now recommended that users rename the root `terragrunt.hcl` file to something else (e.g. `root.hcl`). This will simplify Terragrunt usage, as you will no longer need to carefully avoid running Terragrunt commands in a way that might make it think the root `terragrunt.hcl` file is unit configuration, and it will make it more obvious to users what is and isn’t a unit. Note that in addition to renaming the root `terragrunt.hcl` file, you will also need to update any Terragrunt configurations that assume the root configuration will be named `terragrunt.hcl`. The most common example of this would be usage of `find_in_parent_folders` without any arguments. By default, this will look for a `terragrunt.hcl` file, so you will need to update this to look for the new root configuration file name. e.g. /some/path/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders() } ``` To: /some/path/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } ``` ## Additional Considerations [Section titled “Additional Considerations”](#additional-considerations) If you use [Scaffold](/features/catalog/scaffold) and [Catalog](/features/catalog), you may need to use additional flags to control how new units are generated. It was previously a safe assumption that most users would leverage a root `terragrunt.hcl` file, and thus, the default behavior was to generate a new unit that would look for a `terragrunt.hcl` file above it. You can use the `--root-file-name` and `--no-include-root` flags of both `catalog` and `scaffold` to explicitly control how new units are generated, and what they will look for as the root configuration file (or if they should look for one at all). e.g. ```bash terragrunt catalog ``` To: ```bash terragrunt catalog --root-file-name root.hcl ``` ## Strict Control [Section titled “Strict Control”](#strict-control) To enforce this recommended pattern, you can also enable the [root-terragrunt-hcl](/reference/strict-controls/active#root-terragrunt-hcl) strict control to throw an error when Terragrunt detects that a root `terragrunt.hcl` file is being used. e.g. ```bash terragrunt plan ``` To: ```bash terragrunt plan --strict-control=root-terragrunt-hcl ``` Or: ```bash TG_STRICT_CONTROL=root-terragrunt-hcl terragrunt plan ``` By enabling the strict control, you will also have the default behavior of `scaffold` and `catalog` commands changed to use `root.hcl` as the default root configuration file name if none are provided. ## Future Behavior [Section titled “Future Behavior”](#future-behavior) For now, warnings will be emitted when this pattern is detected in order to encourage users to change to the new pattern, but this behavior will be an explicit error in a future version of Terragrunt. Given how long this has been the standard pattern, we want to assure users that they will have a *very* long time to migrate to this new pattern. For the most part, using the old pattern results in very little practical difference in Terragrunt behavior, assuming Terragrunt usage is already working fine. As an explicit promise, Terragrunt will not remove support for the old pattern until at least Terragrunt 2.0, and even then, it will be a removal with a long warning period. You can take your time to migrate to the new pattern for older codebases, and are encouraged to share any feedback you have on this change so that we can make it as smooth a transition as possible for you. ## Frequently Asked Questions [Section titled “Frequently Asked Questions”](#frequently-asked-questions) ### Could a different default value be used for `find_in_parent_folders` (e.g. `root.hcl`)? [Section titled “Could a different default value be used for find\_in\_parent\_folders (e.g. root.hcl)?”](#could-a-different-default-value-be-used-for-find_in_parent_folders-eg-roothcl) Yes, it could, but this would be a different, immediate breaking change as users might have both `root.hcl` files and `terragrunt.hcl` files in their repositories, and this could result in Terragrunt finding the wrong configuration file. It also doesn’t address a significant part of the problem, which is that the following frequently confuses new users to Terragrunt: ```hcl include "root" { path = find_in_parent_folders() } ``` It does not communicate *what* Terragrunt will look to include in parent folders, and having a hidden extra fallback value is not a good pattern for Terragrunt to encourage. Furthermore, `find_in_parent_folders` *already* supports a fallback value in the second parameter, when used. Having two different ways to specify a fallback value would be confusing. Lastly, the `root` include does not have any special meaning in Terragrunt, from a functional perspective, it’s merely a convention. Terragrunt users do not have to supply a root include, and users can have as many includes as they like. By requiring that users specify the root include filename explicitly, Terragrunt is encouraging users to think about what the root configuration is, and what they want in it. ### Is it better for the root configuration to be named `root.hcl`? [Section titled “Is it better for the root configuration to be named root.hcl?”](#is-it-better-for-the-root-configuration-to-be-named-roothcl) Naming the root file `root.hcl` is the recommended pattern, but it is not a requirement. Our documentation and examples are updated to reference this new pattern, and following this pattern will allow users to pattern match when writing their own configurations. ### Is there any name I *shouldn’t* use for the root configuration? [Section titled “Is there any name I shouldn’t use for the root configuration?”](#is-there-any-name-i-shouldnt-use-for-the-root-configuration) The only names that we would strongly encourage you don’t adopt for root configuration is any name that begins with `terragrunt` (e.g. `terragrunt.hcl` or `terragrunt.stack.hcl`). It is not formally a reserved name, but there are currently only two special filenames in Terragrunt: 1. `terragrunt.hcl` - The default configuration file name for a Terragrunt unit. 2. `terragrunt.stack.hcl` - The default configuration file name for a Terragrunt stack. Using a name that begins with `terragrunt` could cause confusion for users, as they might expect that Terragrunt has special behavior for files with that name. # Migrating from Queue Flags to Filter > Learn how to migrate from the legacy queue control flags to the modern --filter flag. This guide explains how to migrate from the legacy queue control flags to the modern `--filter` flag. The queue control flags are currently aliased to their equivalent `--filter` expressions, but will be deprecated in a future version of Terragrunt. We recommend migrating to `--filter` to take advantage of its more flexible and composable syntax. ## Overview [Section titled “Overview”](#overview) | Legacy Flag | Status | Filter Equivalent | | ----------------------------- | -------------------- | --------------------------- | | `--queue-include-dir=` | Aliased to filter | `--filter=''` | | `--queue-exclude-dir=` | Aliased to filter | `--filter='!'` | | `--queue-exclude-external` | Now default behavior | Not needed | | `--queue-strict-include` | Now default behavior | Not needed | | `--units-that-include=` | Aliased to filter | `--filter='reading='` | | `--queue-include-external` | Current | `--filter='{./**}...'` | ## Migrating `--queue-include-dir` [Section titled “Migrating --queue-include-dir”](#migrating---queue-include-dir) **Before:** ```bash terragrunt run --all --queue-include-dir=./networking -- plan ``` **After:** ```bash terragrunt run --all --filter='./networking' -- plan ``` ## Migrating `--queue-exclude-dir` [Section titled “Migrating --queue-exclude-dir”](#migrating---queue-exclude-dir) **Before:** ```bash terragrunt run --all --queue-exclude-dir=./legacy -- plan ``` **After:** ```bash terragrunt run --all --filter='!./legacy' -- plan ``` ## Migrating `--queue-exclude-external` [Section titled “Migrating --queue-exclude-external”](#migrating---queue-exclude-external) This flag is no longer needed. External dependencies are now excluded by default. **Before:** ```bash terragrunt run --all --queue-exclude-external -- plan ``` **After:** ```bash # No flag needed — external dependencies are excluded by default terragrunt run --all -- plan ``` If you need to *include* external dependencies, use: ```bash terragrunt run --all --filter='{./**}...' -- plan ``` ## Migrating `--queue-strict-include` [Section titled “Migrating --queue-strict-include”](#migrating---queue-strict-include) This flag is no longer needed. The behavior it enabled (only including units matching `--queue-include-dir`) is now the default. **Before:** ```bash terragrunt run --all --queue-include-dir=./networking --queue-strict-include -- plan ``` **After:** ```bash terragrunt run --all --filter='./networking' -- plan ``` ## Migrating `--units-that-include` [Section titled “Migrating --units-that-include”](#migrating---units-that-include) **Before:** ```bash terragrunt run --all --units-that-include=shared.hcl -- plan ``` **After:** ```bash terragrunt run --all --filter='reading=shared.hcl' -- plan ``` # Terragrunt Stacks > Migration guide to migrate to Terragrunt Stacks ## Migrating from the `terragrunt-infrastructure-live-example` repository [Section titled “Migrating from the terragrunt-infrastructure-live-example repository”](#migrating-from-the-terragrunt-infrastructure-live-example-repository) If you have an existing repository that was started using the [terragrunt-infrastructure-live-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-example) repository as a starting point, follow the steps below to migrate your existing configurations to take advantage of the patterns available using Terragrunt Stacks. ### Step 1: Assess your current infrastructure [Section titled “Step 1: Assess your current infrastructure”](#step-1-assess-your-current-infrastructure) Before you get started adjusting any of your existing configurations, it’s important to understand the current state of your infrastructure. How much of it do you regularly update? Does any of it result in frustration or difficulty? Why? Determine whether it’s a good time to be migrating your infrastructure to new patterns, and if so, how much of it you’re willing to migrate. If you are happy, and successful with your current patterns, you may not need to migrate any existing configuration, and that’s great! Consider this a best practice that you can adopt when you start to introduce new infrastructure, and that you may want to adjust your existing infrastructure configurations over time to take advantage of new patterns. The advantages of using the new paradigm with Terragrunt Stacks are: * You can more easily manage your infrastructure at scale. * You can more easily manage your infrastructure in different environments. * You can more easily manage your infrastructure across multiple accounts and regions. * You can more easily manage your infrastructure across multiple teams and organizations. We, at Gruntwork, generally consider this paradigm to be the best practice for managing Infrastructure as Code (IaC) at scale, which is why we’ve created this migration guide to help you transition to it. If you get overwhelmed at any point, read the [Support docs](/community/support/) to learn how you can get help. ### Step 2: Update your Terragrunt version [Section titled “Step 2: Update your Terragrunt version”](#step-2-update-your-terragrunt-version) Now that you’ve determined that you want to migrate some or all of your infrastructure to new patterns, the next step is to ensure that you have a version of Terragrunt that supports the `terragrunt.stack.hcl` file. You can do this by updating the version of Terragrunt you use to the latest available version. If you would like more information on how to update your Terragrunt version, see the [Installation](/getting-started/install/) guide. ### Step 3: Add `.terragrunt-stack` directories to your repository `.gitignore` file [Section titled “Step 3: Add .terragrunt-stack directories to your repository .gitignore file”](#step-3-add-terragrunt-stack-directories-to-your-repository-gitignore-file) Now that you’re adopting Terragrunt Stacks, you’ll want to add the `.terragrunt-stack` directories to your repository `.gitignore` file. ```bash echo ".terragrunt-stack" >> .gitignore git add .gitignore git commit -m "Add .terragrunt-stack to .gitignore" ``` This will prevent you from accidentally committing `.terragrunt-stack` directories to your repository, which is good because you can always regenerate them on demand using the `terragrunt stack generate` command. All other `terragrunt stack` commands also automatically generate the `.terragrunt-stack` directory on demand, so you can safely ignore it. ### Step 4: Re-define existing infrastructure using `terragrunt.stack.hcl` files [Section titled “Step 4: Re-define existing infrastructure using terragrunt.stack.hcl files”](#step-4-re-define-existing-infrastructure-using-terragruntstackhcl-files) The infrastructure that you already have can be re-defined using `terragrunt.stack.hcl` files, reducing the amount of code that you need to maintain in your repository. To do this, you’ll need to: 1. Create an `infrastructure-catalog` repository if you don’t already have one to store your infrastructure configurations. 2. Define the units that you want to reproduce from your `infrastructure-catalog` repository in your `infrastructure-live` repository via `terragrunt.stack.hcl` files. 3. Find a collection of units that you want to abstract into a stack, and define a `terragrunt.stack.hcl` file for them. For example, say you have a collection of units like this, that you want to abstract into a stack: * non-prod * us-east-1 * stateful-ec2-asg-service * service * terragrunt.hcl * db * terragrunt.hcl * sgs * asg * terragrunt.hcl This collection of units can be abstracted into a single stack by creating a `terragrunt.stack.hcl` file in the `stateful-ec2-asg-service` directory that references each unit configuration, as defined in your `infrastructure-catalog` repository (in this example, the `infrastructure-catalog` repository is hosted at `git@github.com:acme/infrastructure-catalog.git`): ```hcl ## non-prod/us-east-1/stateful-ec2-asg-service/terragrunt.stack.hcl unit "service" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/ec2-asg-stateful-service?ref=v1.0.0" path = "service" no_dot_terragrunt_stack = true ## Add any additional configuration for the service unit here } unit "db" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/mysql?ref=v1.0.0" path = "db" no_dot_terragrunt_stack = true ## Add any additional configuration for the db unit here } unit "asg-sg" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/security-group?ref=v1.0.0" path = "sgs/asg" no_dot_terragrunt_stack = true ## Add any additional configuration for the asg-sg unit here } ``` **Note the use of the `no_dot_terragrunt_stack` attribute.** This is used to prevent Terragrunt from automatically generating the units into a `.terragrunt-stack` directory. This is important, because you are probably using `path_relative_to_include()` in the `key` attribute of the `remote_state` block of the root `root.hcl` file, which is included in every unit. By specifying `no_dot_terragrunt_stack = true`, the generated units will be generated into the same directory as they were before, and the `path_relative_to_include()` function will resolve to the same path as before. Migrating to a `terragrunt.stack.hcl` file in this way allows you to migrate your infrastructure to the new patterns outlined here at your own pace, and to migrate state between the old and new patterns if you want to. Now, you can remove the existing unit configurations, and regenerate them on demand using the `terragrunt stack generate` command. ```bash cd non-prod/us-east-1/stateful-ec2-asg-service rm -rf service db sgs ``` If you have identical unit configurations after performing the following, you can remove the unit configurations again, add them to a `.gitignore` file, and commit the new `terragrunt.stack.hcl` file. ```bash terragrunt stack generate ``` * non-prod * us-east-1 * stateful-ec2-asg-service * **terragrunt.stack.hcl** * service * terragrunt.hcl < This should be identical to the unit configuration before * db * terragrunt.hcl < This should be identical to the unit configuration before * sgs * asg * terragrunt.hcl < This should be identical to the unit configuration before Now that you’ve confirmed generation is working, you can remove the unit configurations again, add them to a `.gitignore` file, and commit the new `terragrunt.stack.hcl` file. ```bash rm -rf service db sgs git add terragrunt.stack.hcl service db sgs git commit -m "Remove unit configurations and add terragrunt.stack.hcl" echo "service" >> .gitignore echo "db" >> .gitignore echo "sgs" >> .gitignore git add .gitignore git commit -m "Add unit configurations to .gitignore" ``` Your repository should now look like this: * non-prod * us-east-1 * stateful-ec2-asg-service * **.gitignore** * terragrunt.stack.hcl You can repeat this process as much as you want, abstracting more and more of your infrastructure into Terragrunt Stacks. ### Step 5: Remove reliance on the `_envcommon` directory [Section titled “Step 5: Remove reliance on the \_envcommon directory”](#step-5-remove-reliance-on-the-_envcommon-directory) The `_envcommon` directory is no longer needed to create “Don’t Repeat Yourself” (DRY) configurations with Terragrunt, and is no longer recommended as a best practice. If you would like to remove usage of the `_envcommon` directory, you can do so by replacing usage of the `include` block referencing the `_envcommon` directory with content directly committed to `terragrunt.hcl` files. For example, say you have a `terragrunt.hcl` file that looks like this: ```hcl ## non-prod/us-east-1/mysql/terragrunt.hcl include "root" { path = find_in_parent_folders("root.hcl") } include "envcommon" { path = "${dirname(find_in_parent_folders("root.hcl"))}/_envcommon/mysql.hcl" expose = true } terraform { source = "${include.envcommon.locals.base_source_url}?ref=v0.8.0" } inputs = { instance_class = "db.t2.medium" allocated_storage = 100 } ``` and an `_envcommon/mysql.hcl` file that looks like this: ```hcl ## _envcommon/mysql.hcl locals { environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl")) env = local.environment_vars.locals.environment base_source_url = "git::git@github.com:acme/infrastructure-catalog.git//modules/mysql" } inputs = { name = "mysql_${local.env}" instance_class = "db.t2.micro" allocated_storage = 20 storage_type = "standard" master_username = "admin" } ``` This pattern was previously used to create “Don’t Repeat Yourself” (DRY) configurations with Terragrunt. However, this pattern is no longer recommended as a best practice, and is no longer needed to create DRY configurations with Terragrunt. Instead, you can create a `terragrunt.hcl` file in your `infrastructure-catalog` repository that looks like this: ```hcl ## units/mysql/terragrunt.hcl include "root" { path = find_in_parent_folders("root.hcl") } terraform { source = "git::git@github.com:acme/infrastructure-catalog.git//modules/mysql?ref=${values.version}" } inputs = { ## Required inputs name = values.name instance_class = values.instance_class allocated_storage = values.allocated_storage storage_type = values.storage_type master_username = values.master_username master_password = values.master_password ## Optional inputs skip_final_snapshot = try(values.skip_final_snapshot, null) engine_version = try(values.engine_version, null) } ``` Then reference that `terragrunt.hcl` file in your `terragrunt.stack.hcl` files, like so: ```hcl ## non-prod/us-east-1/terragrunt.stack.hcl unit "mysql" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/mysql?ref=v1.0.0" path = "mysql" ## As discussed above, this prevents Terragrunt from automatically generating the units into a `.terragrunt-stack` directory. no_dot_terragrunt_stack = true values = { version = "v0.8.0" name = "mysql_dev" instance_class = "db.t2.micro" allocated_storage = 20 storage_type = "standard" master_username = "admin" } } ``` Now, all your unit configurations can be found directly in the `terragrunt.hcl` file in the `infrastructure-catalog` repository, without having to bounce around between different included or referenced files, and you have an explicit interface for the values that can be set externally, via the `values` attribute. Different environments can pin different versions of the unit, and that allows for easy atomic updates (and rollbacks) of both OpenTofu/Terraform module versions *and* Terragrunt unit configurations if needed. Tip This is one of the main reasons why we recommend using Terragrunt Stacks over the old `_envcommon` directory pattern. In the old `_envcommon` directory pattern, there was no simple way to version the shared configuration referenced by all units in your `live` repository. All units always referenced the same version of the shared configuration in `_envcommon`. Now that you’re using Terragrunt Stacks, you can use Git tags to version the shared configuration you reference in your `terragrunt.stack.hcl` files, and different environments can pin the version of the shared configuration they want to use. ### Step 6: Update your CI/CD pipeline [Section titled “Step 6: Update your CI/CD pipeline”](#step-6-update-your-cicd-pipeline) Chances are, if you’re currently performing Terragrunt updates via a CI/CD pipeline (and you aren’t using [Gruntwork Pipelines](https://www.gruntwork.io/platform/pipelines)), your CI/CD pipeline doesn’t yet have integration with Terragrunt Stacks. There are a few options for how to proceed here. 1. You can simply commit the generated `.terragrunt-stack` directories to your repository. This is the easiest option when managing CI/CD yourself, but it also means that you won’t gain some of the benefits that come from using Terragrunt Stacks. When getting started, however, this is a good way to avoid the additional technical debt that comes from having to update your CI/CD pipeline to support Terragrunt Stacks, while learning how to use `terragrunt.stack.hcl` files, and reorganizing your infrastructure configurations. To do this, remove the `.terragrunt-stack` entry from your `.gitignore` file, and commit the changes to your repository. You can then manually run the `terragrunt stack generate` command to generate the `.terragrunt-stack` directories on demand, and commit them to your repository, allowing your CI/CD pipeline to completely ignore the fact that you’re using Terragrunt Stacks. The units generated by the `terragrunt stack generate` command are completely compatible with units that you can author manually, so you don’t have to worry about any incompatibility issues that might arise from this approach. 2. You can configure your CI/CD pipeline to run the `terragrunt stack generate` command whenever your pipeline runs, and leverage the generated `.terragrunt-stack` directories in your pipeline. Depending on the complexity of your CI/CD pipeline, this might be as simple as performing the following: ```bash terragrunt stack generate terragrunt run --all plan/apply --non-interactive ``` This doesn’t account for destroys or the reduction of blast radius for changes by carefully inspecting Git diffs, but it’s a good start for users that don’t have CI/CD pipelines that are too complex. There is an open RFC in GitHub ([Filter Flag](https://github.com/gruntwork-io/terragrunt/issues/4060)) that would allow for this kind of complex filtering out of the box with Terragrunt, but at the moment, it’s still an open RFC. # Upgrading to Terragrunt 0.19.x > Migration guide to upgrade to terragrunt 0.19.x ## Background [Section titled “Background”](#background) Terraform 0.12 was released in May, 2019, and it included a few major changes: 1. More strict rules around what can go in a `.tfvars` file. In particular, any variable defined in a `.tfvars` file that does not match a corresponding `variable` definition in your `.tf` files produces an error. 2. A shift from HCL to HCL2 as the main syntax. This included support for first-class expressions (i.e., using variables and functions without having to wrap everything in `${...}`). Before version 0.19.0, Terragrunt had you define its configuration in a `terragrunt = { ... }` variable in a `terraform.tfvars` file, but due to item (1) this no longer works with Terraform 0.12 and newer. That means we had to move to a new file format. This requires a migration, which is unfortunate, but as a nice benefit, item (2) gives us a nicer syntax and new functionality! ## Migration guide [Section titled “Migration guide”](#migration-guide) The following sections outline the steps you may need to take in order to migrate from Terragrunt <= v0.18.x to Terragrunt 0.19.x and newer: * [Background](#background) * [Migration guide](#migration-guide) * [Move from terraform.tfvars to terragrunt.hcl](#move-from-terraformtfvars-to-terragrunthcl) * [Move input variables into inputs](#move-input-variables-into-inputs) * [Use first-class expressions](#use-first-class-expressions) * [Check attributes vs blocks usage](#check-attributes-vs-blocks-usage) * [Rename a few built-in functions](#rename-a-few-built-in-functions) * [Use older Terraform](#use-older-terraform) Check out [this PR in the terragrunt-infrastructure-live-example repo](https://github.com/gruntwork-io/terragrunt-infrastructure-live-example/pull/17) for an example of what the code changes look like. ### Move from terraform.tfvars to terragrunt.hcl [Section titled “Move from terraform.tfvars to terragrunt.hcl”](#move-from-terraformtfvars-to-terragrunthcl) Since Terraform 0.12 has more strict rules about what can go into `terraform.tfvars` files, you now need to move your Terragrunt configuration from `terraform.tfvars` to a `terragrunt.hcl` file, removing the `terragrunt = { ... }` wrapping along the way. For example, if you had the following in `terraform.tfvars`: terraform.tfvars ```hcl terragrunt = { terraform { source = "git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3" extra_arguments "custom_vars" { commands = ["apply", "plan"] arguments = ["-var", "foo=42"] } } remote_state { backend = "s3" config = { bucket = "my-terraform-state" key = "${path_relative_to_include()}/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } } ``` You would migrate this to `terragrunt.hcl` as follows: terragrunt.hcl ```hcl terraform { source = "git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3" extra_arguments "custom_vars" { commands = ["apply", "plan"] arguments = ["-var", "foo=42"] } } remote_state { backend = "s3" config = { bucket = "my-terraform-state" key = "${path_relative_to_include()}/terraform.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } ``` ### Move input variables into inputs [Section titled “Move input variables into inputs”](#move-input-variables-into-inputs) When we were using `terraform.tfvars` files for Terragrunt configuration, we were piggybacking on the fact that Terraform [automatically loads variables from tfvars files](https://www.terraform.io/docs/configuration/variables.html#variable-definitions-tfvars-files) to set variables for our modules: terraform.tfvars ```hcl # Terragrunt configuration terragrunt = { terraform { # ... } remote_state { # ... } } # Input variables to set for your Terraform module instance_type = "t2.micro" instance_count = 10 ``` With the move to `terragrunt.hcl`, we no longer get this behavior for free. However, Terragrunt can simulate this behavior for you if you define your input variables by specifying `inputs = { ... }`: terragrunt.hcl ```hcl terraform { # ... } remote_state { # ... } # Input variables to set for your Terraform module inputs = { instance_type = "t2.micro" instance_count = 10 } ``` Whenever you run a Terragrunt command, such as `terragrunt apply`, Terragrunt will make these variables available to your Terraform module as environment variables. ### Use first-class expressions [Section titled “Use first-class expressions”](#use-first-class-expressions) Terraform 0.11 only allowed special behavior, such as function calls, using “interpolation syntax,” where you wrapped the code with `${...}`. Terragrunt included a handful of functions you could call using interpolation syntax, but *only* within the `terragrunt = { ... }` block: terraform.tfvars ```hcl terragrunt = { terraform { extra_arguments "retry_lock" { # Using a function within interpolation syntax commands = "${get_terraform_commands_that_need_locking()}" arguments = ["-lock-timeout=20m"] } } } # Using interpolation syntax outside of the terragrunt config did NOT work before foo = "${get_env("FOO", "default")}" ``` Terraform 0.12 has moved to HCL2, which has first-class support for expressions. That means you can call functions without having to wrap them in `${...}`. Terragrunt embraces HCL2, and thanks to HCL2’s nice parser, that means we not only get first-class expressions, but we can also use those expressions *everywhere* in `terragrunt.hcl`! terragrunt.hcl ```hcl terraform { extra_arguments "retry_lock" { # Using a function within first-class expressions! commands = get_terraform_commands_that_need_locking() arguments = ["-lock-timeout=20m"] } } inputs = { # This now works with Terragrunt 0.19.x and newer! foo = get_env("FOO", "default") } ``` ### Check attributes vs blocks usage [Section titled “Check attributes vs blocks usage”](#check-attributes-vs-blocks-usage) HCL2 is more strict about the difference between attributes: ```hcl # Attributes use an equals sign before the curly brace foo = { bar = "baz" } ``` And blocks: ```hcl # Blocks do not use equal signs before the curly brace foo { bar = "baz" } ``` Since Terragrunt uses HCL2, we now have to be more strict with which parts of the Terragrunt configuration are attributes and which are blocks: terragrunt.hcl ```hcl # terraform is a block, so make sure NOT to include an equals sign terraform { source = "git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3" # extra_arguments is a block, so make sure NOT to include an equals sign extra_arguments "custom_vars" { commands = ["apply", "plan"] arguments = ["-var", "foo=42"] } } # remote_state is a block, so make sure NOT to include an equals sign remote_state { backend = "s3" # config is an attribute, so an equals sign is REQUIRED config = { bucket = "foo" # s3_bucket_tags is an attribute, so an equals sign is REQUIRED s3_bucket_tags = { owner = "terragrunt integration test" name = "Terraform state storage" } # dynamodb_table_tags is an attribute, so an equals sign is REQUIRED dynamodb_table_tags = { owner = "terragrunt integration test" name = "Terraform lock table" } # accesslogging_bucket_tags is an attribute, so an equals sign is REQUIRED accesslogging_bucket_tags = { owner = "terragrunt integration test" name = "Terraform access log storage" } } } # include is a block, so make sure NOT to include an equals sign include { path = find_in_parent_folders("root.hcl") } # dependencies is a block, so make sure NOT to include an equals sign dependencies { paths = ["../vpc", "../mysql", "../redis"] } # Inputs is an attribute, so an equals sign is REQUIRED inputs = { instance_type = "t2.micro" instance_count = 10 } ``` ### Rename a few built-in functions [Section titled “Rename a few built-in functions”](#rename-a-few-built-in-functions) Two built-in functions were renamed: 1. `get_tfvars_dir()` is now called `get_terragrunt_dir()`. 2. `get_parent_tfvars_dir()` is now called `get_parent_terragrunt_dir()`. Make sure to make the corresponding updates in your `terragrunt.hcl` file! ### Use older Terraform [Section titled “Use older Terraform”](#use-older-terraform) Although it is not officially supported and not tested, it is still possible to use terraform<0.12 with terragrunt >=0.19. Just install a different version of terraform into a directory of your choice outside of `PATH` and specify path to the binary in `terragrunt.hcl` as `terraform_binary`, plus you need to lower the version check constraint: ```hcl terraform_binary = "~/bin/terraform-v11/terraform" terraform_version_constraint = ">= 0.11" ``` # Terragrunt 1.0 Guarantees > Stability and compatibility guarantees for Terragrunt 1.0 and the 1.x release line. The term “breaking change” means something different to different people, and every change can, in some way, be a breaking change to someone. [![Relevant XKCD](https://imgs.xkcd.com/comics/workflow.png)](https://xkcd.com/1172/) *Relevant XKCD (#1172)* With the release of Terragrunt 1.0, there are some concrete compatibility guarantees that you can rely on for the duration of the 1.x line of Terragrunt, which is intended to remain the latest major version of Terragrunt for the foreseeable future. Any violations of these guarantees is a bug that will be fixed in a future 1.x release, or an oversight on the part of maintainers that will result in an update to this document. This is a living document that will be updated as questions are answered regarding what is and isn’t considered a breaking change in Terragrunt 1.x. ## What compatibility guarantees are being made in 1.0? [Section titled “What compatibility guarantees are being made in 1.0?”](#what-compatibility-guarantees-are-being-made-in-10) ### The CLI [Section titled “The CLI”](#the-cli) CLI commands and flags present in Terragrunt 1.0 will not be removed or renamed during 1.x. Commands and flags will continue to work the same way throughout 1.x. You may see deprecation warnings, and new commands or flags may be introduced that let you opt in to different functionality, but running the same commands will produce the same results. New CLI commands and flags introduced during 1.x will also maintain backwards compatibility for the remainder of 1.x. ### HCL Configurations [Section titled “HCL Configurations”](#hcl-configurations) Terragrunt HCL configurations valid in 1.0 will remain valid throughout 1.x. The HCL parser will not drop support for any block, attribute, or function during 1.x. You may see deprecation warnings, and new HCL configurations will continue to be introduced, but using the same configurations will produce the same results. New HCL configurations introduced during 1.x will also maintain backwards compatibility for the remainder of 1.x. ### Run Report [Section titled “Run Report”](#run-report) The [Run Report](/features/stacks/run-report/) generated using the [`--report-file`](/reference/cli/commands/run/#report-file) flag can be parsed using the schema output by [`--report-schema-file`](/reference/cli/commands/run/#report-schema-file). You can also find the schema at the URL listed in the `$id` field, e.g., . Any modifications made to the schema that break parsing of existing report files using a modern JSON parser will only be done on an opt-in basis for the duration of 1.x, and you will be able to use the `$id` field of the generated schema to confirm that you are parsing a file with an expected schema. Note that no guarantee is made that new fields won’t be added to the generated report file. If you are not using a modern deserialization library that can ignore unknown fields, this may cause issues for you, and you’ll have to take note of the `$id` field of the generated `--report-schema-file` to catch any such cases. Modern deserialization libraries in popular programming languages like Golang have built-in support for ignoring unknown fields, and it’s expected that most users will be using one such deserialization library or a tool like `jq` to parse reports. As such, new versions of the report schema that *add* new fields will not be considered breaking, whereas *removing* fields will be. ### Find [Section titled “Find”](#find) The [find](/reference/cli/commands/find/) command provides the ability to discover Terragrunt configurations programmatically. Maintaining a stable schema for `find` output is something you will be able to rely on. For the duration of 1.x, you will be able to expect the same output from any usage of the `find` command. You will never have a field in `find` output disappear, or for the general shape of the JSON schema to change. New opt-in flags that allow you to discover more about the configurations in your codebase might be introduced, and backwards compatibility of their behavior will be ensured for the duration of 1.x. ### OpenTofu/Terraform stdout/stderr [Section titled “OpenTofu/Terraform stdout/stderr”](#opentofuterraform-stdoutstderr) There will be special consideration made to ensure that no onerous changes are made to OpenTofu/Terraform stdout/stderr forwarding. You should expect OpenTofu/Terraform stdout/stderr logs to be [enriched](/reference/logging/) the same way when using the same logging configurations, and consistency in whether the output is forwarded to Terragrunt stdout/stderr. ## What compatibility guarantees aren’t being made in 1.0? [Section titled “What compatibility guarantees aren’t being made in 1.0?”](#what-compatibility-guarantees-arent-being-made-in-10) ### Experimental Features [Section titled “Experimental Features”](#experimental-features) Terragrunt supports the ability to utilize certain [experimental features](/reference/experiments/) on an opt-in basis. These are features that will explicitly not be given any stability guarantees, and can change in any given release. Big new features in Terragrunt are very likely to be introduced as experiments and iterated on in collaboration with early adopters before they are stabilized, to ensure that they are feasible to support without breaking changes for the duration of 1.x. No guarantees are made regarding the stability of experimental features, or even that any given experiment will ever be generally available. Experiments give maintainers the opportunity to iterate quickly without the same stability guarantees made for the rest of Terragrunt. ### List [Section titled “List”](#list) The [list](/reference/cli/commands/list/) command differs from the `find` command in that it is designed to be understood by humans (as opposed to being parsed programmatically in `find`), and as such, changes to how information is presented might be encountered during 1.x releases. These changes might be done to improve the legibility of results, or provide additional useful information by default without requiring complex invocations of the `list` command. No guarantees that list output will have the same colors, or that list results will be displayed with the same general shape for the duration of 1.x. Any change made in this regard will be made carefully, however, and maintainers will do their best to ensure that they are communicated effectively and opt-in where possible. ### Catalog [Section titled “Catalog”](#catalog) The [catalog](/reference/cli/commands/catalog/) command has a Terminal User Interface (TUI), and is also designed to be understood and interacted with by humans on the terminal. Changes to how information is displayed in the catalog command is subject to change, and stability in the UI is not guaranteed in 1.x releases. You should be able to expect that you won’t lose any capabilities in the catalog TUI, however. New buttons and columns might appear in future 1.x releases, and you may have to navigate the TUI in different ways. Major changes to TUIs in Terragrunt will occur gradually over multiple releases as experimental features when possible. ### Stdout/Stderr [Section titled “Stdout/Stderr”](#stdoutstderr) With the exception of OpenTofu/Terraform stdout/stderr and certain structured data, like report files and `find` output, no guarantees are made that you will see the exact same stdout/stderr for the same invocation of the Terragrunt CLI. This includes logging. You should not rely on the same log entries or error messages existing from one release of Terragrunt to another, or that they are worded the same way. You also shouldn’t rely on the same run summary being printed with the same content. Log messages including error messages will be continuously adjusted to improve the user experience of using the Terragrunt CLI. This will be done to make it easier to understand what is happening in Terragrunt runs, and troubleshoot errors. ### Bugs [Section titled “Bugs”](#bugs) The most important guarantee that we cannot make in Terragrunt 1.x is that no bugs will be created. It is possible that we will *accidentally* introduce regressions that result in breaking changes to Terragrunt. Those are bugs that will be fixed in a future release of Terragrunt 1.x. If you depend on functionality in Terragrunt that is considered to be a bug (either as of 1.0, or introduced in a 1.x release), it is possible that fixing that bug will result in a breaking change to your workflows. Whenever possible, maintainers will be judicious with any such fix, and will attempt to preserve backwards compatibility despite the bug fix via opt-in functionality. ### Integrations [Section titled “Integrations”](#integrations) It is possible that changes to third party software that Terragrunt integrates with will require a breaking change in Terragrunt. In those scenarios, it might not always be possible for the change to be opt-in. These scenarios will be handled on a case-by-case basis, and maintainers endeavor to minimize the impact of these changes to you. ### Performance [Section titled “Performance”](#performance) The fastest thing you can do in software is to do nothing at all. In the pursuit of improving performance, maintainers may prevent Terragrunt from doing work understood to be unnecessary. In a large enough user base, *someone* is potentially going to be impacted by changes like this. Whenever possible, maintainers will strive to err on the side of maintaining exactly the same functionality, and only changing functionality if there’s little to no justification for how the behavior would be beneficial to Terragrunt users. In the event changes like this cause disruption to users, maintainers will take this disruption very seriously and work to remediate the disruption, or revert the performance improvement. ### Golang Compatibility [Section titled “Golang Compatibility”](#golang-compatibility) Maintainers will frequently update the build system to use the latest version of Golang when compiling Terragrunt. Newer versions of Golang may drop support for older operating systems, which means that newer versions of compiled Terragrunt binaries might no longer run on older operating systems. When the Golang version is upgraded, the `go.mod` file will also be updated accordingly. If you compile Terragrunt from source, you may need to upgrade your local Golang installation to match the version specified in `go.mod`, or manually downgrade the version in `go.mod` to continue compiling Terragrunt using an older Golang version. If you are impacted by a Golang version upgrade and would like to request a policy adjustment, please reach out to the maintainers by starting a GitHub discussion or reaching out in the Terragrunt Discord. ### Golang Library Compatibility [Section titled “Golang Library Compatibility”](#golang-library-compatibility) Using Terragrunt as a Go library has no backwards compatibility guarantees. Most Go code in the Terragrunt repository lives in [`internal`](https://github.com/gruntwork-io/terragrunt/tree/main/internal), and maintainers don’t expect external parties to depend on Terragrunt packages directly. When packages are generally useful to internal Gruntwork parties, they will be migrated to [`pkg`](https://github.com/gruntwork-io/terragrunt/tree/main/pkg). Breaking changes to packages in `pkg` are still possible at any time, but maintainers will coordinate directly with known consumers to help prevent upgrade issues. When external parties need a stable dependency on shared code, dedicated libraries will be created in separate, versioned repositories (e.g., [terragrunt-engine-go](https://github.com/gruntwork-io/terragrunt-engine-go)). If you are relying on code in `pkg` or vendoring code from `internal`, you are heavily encouraged to start a conversation with Terragrunt maintainers so that we can plan out a path to an external library that you can rely on to be stable and independently versioned from Terragrunt. ## Versioning Policy [Section titled “Versioning Policy”](#versioning-policy) For the duration of 1.x, only the minor and patch versions of Terragrunt releases will be used in the [semantic versioning scheme](https://semver.org/), just like it was in Terragrunt 0.x. The difference as of 1.0 is that no backwards incompatible breaking changes will be introduced in any minor release. Bug fixes will continue to be released as patch versions, and minor releases will be used to introduce new [backward compatible](https://en.wikipedia.org/wiki/Backward_compatibility) major features. All [*forward incompatible changes*](https://en.wikipedia.org/wiki/Forward_compatibility) will be released in minor releases, and will usually be supported on an experimental basis before they are generally available. e.g. If a new flag is going to be introduced, it will likely be an experimental feature introduced in a minor or patch release, then eventually promoted to a generally available feature in a future minor release. Versions of Terragrunt released before that minor release will not be able to use that flag without enabling the experiment, but every version of Terragrunt released afterwards in 1.x will support it. Release candidates of minor releases will be published to give users a chance to test out the stability of new releases before they are fully released. ## Deprecations [Section titled “Deprecations”](#deprecations) If functionality is deprecated in 1.x, it will likely be removed in 2.x. On a case-by-case basis, maintainers may decide to explicitly extend the lifetime of deprecated functionality beyond 2.x. ## Feedback [Section titled “Feedback”](#feedback) If you have any feedback on these guarantees, or would like clarification on something that isn’t covered here, please open a [discussion topic](https://github.com/gruntwork-io/terragrunt/discussions) or [create an issue](https://github.com/gruntwork-io/terragrunt/issues) in the Terragrunt GitHub repository. # Changelog > A log of notable changes to Terragrunt. # CLI Rules > Learn the rules for how the Terragrunt CLI is designed. These are the rules that Terragrunt maintainers endeavor to follow when working on the CLI. Whenever maintainers break these rules, that is a bug and should be reported. The maintainers will either fix the behavior, or update the rules to reflect the reason for the discrepancy. In addition, if you find that a certain pattern that’s reliably followed in the CLI is not documented here, please let us know so we can update this document to encourage consistency. 1. The final argument to a Terragrunt command will always be a verb. 2. All the arguments preceding the final argument will usually be a noun. Exception The exceptions to this rule are commands like `terragrunt run`, as these will frequently have two **verbs** in sequence (e.g. `terragrunt run plan`). This is an exception to the rule because it is exceptional behavior. The end of Terragrunt’s responsibility (from a CLI design perspective) is to process the `run` command, so what follows is not subject to the rules dictated here. 3. All flags will usually start with a noun. Exception The exception to this rule will be for negation as the flag will start with `no`/ `non` , as discussed below. If the flag is controlling a single configuration for Terragrunt, that configuration will be the name of the flag. Example `--working-dir`: Set the `working directory` configuration for Terragrunt. If a Terragrunt system can be controlled entirely by referencing the name of the system, or the flag can control high level behavior of Terragrunt on its own, that will be the name of the flag. Example `--provider-cache`: The system is the `provider cache` server. The Provider Cache Server be enabled if this flag is set. For brevity, prefer this to flags like `--provider-cache-enable`. If a configuration is being set for a system, another noun will follow the name of the system after a dash. The flag will accept a parameter that sets the configuration for that system. Example * `--log-level`: The system is `log`, the `level` is the configuration. * `--provider-cache-dir`: The system is the `provider cache` server. The directory is the configuration. If an operation will be performed on a system, a verb will follow. If necessary, a noun will follow the verb to indicate what the parameter for that flag corresponds to, or the setting it controls for the operation. Example `--queue-include-unit` - The system is the runner `queue`. The operation being performed to that system is `include`. The parameter of `unit` indicates that the parameter to the flag will be a `unit` being `included` in the `queue`. 4. Behavior on the same systems will always share the same stem. Example All flags that have to do with the Terragrunt Provider Cache Server will start with `provider-cache`. A user looking through the flags available in Terragrunt sorted in alphabetical order will find them right next to each other. 5. All boolean flags will accept an optional parameter of `true` or `false` . `true` will usually correspond to the default behavior of the flag, and `false` will correspond to the inverse. Exception The exception to this rule is when the default behavior of a flag changes. For example, the [terragrunt-include-module-prefix](https://docs.terragrunt.com/reference/cli-options/#terragrunt-include-module-prefix) flag was previously opt-in, but users were better served with the behavior being opt-out. To preserve backwards compatibility until the next release, the flag remained, but to use it, users had to set it via `--terragrunt-include-module-prefix=false`. In this scenario, whenever applicable, a different flag will be made available that does obey this rule (like [tf-forward-stdout](https://docs.terragrunt.com/reference/cli-options/#tf-forward-stdout)). When a flag prevents something from happening that Terragrunt would do by default, it will be proceeded by the prefix `no`/ `non`. Example `--no-color` has a default value of `true`, and setting it to `false` will make it so that the behavior of not setting the flag is active (Terragrunt will emit colorful output). The alternative would be to have a `--color` flag, and using that flag to disable color would require that they do something like `--color=false`. This would violate the rule that the default behavior of the flag wouldn’t change anything, as Terragrunt emits color by default. 6. Commands and flags will always be backwards compatible until the next major release of Terragrunt. This includes instances where behavior violates one of the other rules listed here. 7. If naming a command or flag following these rules would make it harder to understand or longer than it needs to be, the exception will be allowed and documented. 8. Flags that specifically control the behavior of OpenTofu/Terraform will be prefixed `tf`. Example `--tf-path` controls the path to the OpenTofu/Terraform binary. 9. Every flag will have at least one corresponding environment variable that is exactly the same text as the flag, but converted to `SHOUTY_SNAKE_CASE` instead of `kebab-case`, and prefixed with `TG_`. When more than one environment variable controls a flag, it will be to support backwards compatibility. Example `iam-assume-role-duration` —> `TG_IAM_ASSUME_ROLE_DURATION` # Releases > Learn about the Terragrunt release process. Terragrunt releases use [semantic versions (semver)](https://semver.org/). Note that as of 2026/01/27, Terragrunt is still pre-1.0, so breaking changes may still be introduced in minor releases. We will try to minimize these changes as much as possible, but they may still happen. Once 1.0 is released, Terragrunt backwards compatibility will be guaranteed for all minor releases (see [Terragrunt 1.0 Guarantees](/process/1-0-guarantees) for details on what constitutes a breaking change). This documentation should be updated at that time to reflect the new policy. If it has not, please file a bug report. ## Immutable Releases [Section titled “Immutable Releases”](#immutable-releases) Terragrunt uses [GitHub’s immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases) to ensure that once a release is published, its tag and assets cannot be modified or deleted. This means the binary you download today is the same binary that was uploaded when the release was published. To support immutability, releases are built as **drafts** first. All artifacts are uploaded and verified before the release is published. Once published, the git tag is created and the release is locked. Not yet enabled Immutable releases are not yet turned on for the `gruntwork-io/terragrunt` repository. The release workflow has been updated to support them, and maintainers want to run a few releases through the new workflow before enabling the setting on GitHub. ## When to Cut a New Release [Section titled “When to Cut a New Release”](#when-to-cut-a-new-release) While Terragrunt is still pre-1.0, maintainers will cut a new release whenever a new feature is added or a bug is fixed. Maintainers will exercise their best judgment to determine when a new release is necessary, and bias towards cutting a new release as frequently as possible when in doubt. ## How to Create a New Release [Section titled “How to Create a New Release”](#how-to-create-a-new-release) To release a new version of Terragrunt: 1. Go to the [Releases Page](https://github.com/gruntwork-io/terragrunt/releases) and click **Draft a new release**. 2. In the **Choose a tag** field, enter the version (e.g., `v1.0.2`). Select **Create new tag: v1.0.2 on publish**. 3. **Set the target to an explicit commit SHA**, not a branch name. This prevents new commits from being pulled into the release between draft creation and publishing. You can find the commit SHA from the tip of the branch you want to release (e.g., `main`). 4. Set the release title to the version tag (e.g., `v1.0.2`). 5. Click **Save draft**. 6. Trigger the [Release workflow](https://github.com/gruntwork-io/terragrunt/actions/workflows/release.yml) via **Run workflow**, providing the version string you just drafted (e.g., `v1.0.2`). The workflow looks up the draft release, reads its target commit SHA, and will: * Validate the tag is a valid semantic version * Verify the target is a commit SHA (not a branch name) * Build binaries for every supported OS and architecture * Sign macOS and Windows binaries * Generate checksums and cryptographic signatures * Upload all assets to the draft release 7. Once the workflow succeeds, go back to the draft release and verify the assets are present and downloadable. 8. Write or finalize the release notes. Re-running after edits If you edit the draft (for example, to retarget a new commit SHA), re-run the Release workflow with the same version to rebuild and overwrite the assets. Editing the draft does not automatically trigger a rebuild. 9. When ready: * For stable releases: uncheck **Set as a pre-release** and check **Set as the latest release**. * For pre-releases: check **Set as a pre-release**. 10. Click **Publish release**. This creates the git tag at the pinned commit, makes the release immutable, and triggers the release announcement. ### Retrying a Failed Build [Section titled “Retrying a Failed Build”](#retrying-a-failed-build) If the build workflow fails mid-way, re-run the Release workflow via **workflow\_dispatch** with the same version (e.g., `v1.0.2`). It looks up the existing draft release and rebuilds, overwriting any previously uploaded assets. ## Pre-releases [Section titled “Pre-releases”](#pre-releases) Occasionally, Terragrunt maintainers will cut a pre-release to get feedback on the UI/UX for a new feature or to give the community a chance to test it in the wild before making it generally available. All pre-releases use semver-compliant version strings: * `v1.0.0-alpha.1`: Alpha pre-releases, recommended for testing in non-production environments only. Intended for testing new features with stakeholders external to Gruntwork before a general release. * `v1.0.0-beta.1`: Beta pre-releases, for broader testing. * `v1.0.0-rc1`: Release candidates, which undergo more thorough testing (automated and manual) before the final release. This pre-release system is subject to change, and maintainers will update this documentation to reflect any changes. ## Tip Builds [Section titled “Tip Builds”](#tip-builds) Every successful CI run on the `main` branch automatically produces a **tip build**. Tip builds are builds of Terragrunt from the latest commit at the time of build (the tip of the `main` branch). Tip builds are versioned as `tip-{commit_sha}` and are available for all supported platforms as `.tar.gz` archives. Windows and macOS binaries are not signed as they are in full releases, and a reduced set of assets are included. Tip builds are useful for testing the latest changes on `main` before a release is cut, or for reproducing issues against the most recent code. No retention guarantee Tip builds may be reclaimed at any time to reduce hosting costs. Do not rely on a specific tip build being available indefinitely. Use official releases for long-lived references. ### Accessing Tip Builds [Section titled “Accessing Tip Builds”](#accessing-tip-builds) Tip builds are served via the builds API at `https://builds.terragrunt.com`. The API can be used to get metadata for the latest tip build, or for a specific build by commit SHA. The `download` endpoints accept either `os` and `arch` query parameters to download a platform-specific archive, or a `filename` query parameter to download a specific file by name. * Latest **Get metadata** — returns build info for the latest tip build. ```plaintext GET /api/v1/tip/latest ``` ```bash curl https://builds.terragrunt.com/api/v1/tip/latest ``` **Download by platform** — redirects to the `.tar.gz` archive for the given OS and architecture. ```plaintext GET /api/v1/tip/latest/download?os=&arch= ``` ```bash curl -L -o terragrunt.tar.gz \ "https://builds.terragrunt.com/api/v1/tip/latest/download?os=linux&arch=amd64" ``` **Download by filename** — redirects to a specific file from the build (e.g., `SHA256SUMS`). ```plaintext GET /api/v1/tip/latest/download?filename= ``` ```bash curl -L -o SHA256SUMS \ "https://builds.terragrunt.com/api/v1/tip/latest/download?filename=SHA256SUMS" ``` * By Commit SHA **Get metadata** — returns build info for a specific commit. ```plaintext GET /api/v1/tip/{ref} ``` ```bash curl https://builds.terragrunt.com/api/v1/tip/abc123def456 ``` **Download by platform** — redirects to the `.tar.gz` archive for the given OS and architecture. ```plaintext GET /api/v1/tip/{ref}/download?os=&arch= ``` ```bash curl -L -o terragrunt.tar.gz \ "https://builds.terragrunt.com/api/v1/tip/abc123def456/download?os=darwin&arch=arm64" ``` **Download by filename** — redirects to a specific file from the build (e.g., `SHA256SUMS`). ```plaintext GET /api/v1/tip/{ref}/download?filename= ``` ```bash curl -L -o SHA256SUMS \ "https://builds.terragrunt.com/api/v1/tip/abc123def456/download?filename=SHA256SUMS" ``` ### Signing and Verification [Section titled “Signing and Verification”](#signing-and-verification) Tip builds are signed using the same process as official releases. Each build includes: * `SHA256SUMS` — checksums for all archives * `SHA256SUMS.gpgsig` — GPG signature of the checksums file * `SHA256SUMS.sigstore.json` — Sigstore (Cosign) signature of the checksums file ## Test Builds [Section titled “Test Builds”](#test-builds) Maintainers can manually trigger a test build from any branch using the [`test-build.yml`](https://github.com/gruntwork-io/terragrunt/actions/workflows/test-build.yml) workflow via `workflow_dispatch`. Test builds are versioned as `test-{commit_sha}` and uploaded under the `test` prefix (or `tip` if triggered from `main`). This is useful for giving stakeholders a binary to test before a feature branch is merged. # Overview > Learn how the Terragrunt CLI works The Terragrunt CLI is designed to make it as easy as possible to manage infrastructure at any scale. To support that design, there are certain patterns that are used throughout the CLI. This document will help you understand those patterns so you can use the CLI more effectively. ## Usage [Section titled “Usage”](#usage) Most of the time, if you are trying to use Terragrunt to run a command that you would normally run with OpenTofu/Terraform, you can just replace `tofu`/ `terraform` with `terragrunt`. Terragrunt will pass the command to `tofu`/ `terraform` with the same arguments. ```bash terragrunt plan ``` Terragrunt doesn’t always *just* pass the command. It frequently does some additional processing to make it easier to manage infrastructure at scale. For example, in the previous `plan` command, you wouldn’t have to explicitly run `init` like you would with `tofu`/ `terraform`. Terragrunt takes advantage of a feature called [Auto-init](/features/units/auto-init) to automatically run `init` when necessary. Using Terragrunt in this way is taking advantage of the **OpenTofu Shortcuts** that Terragrunt provides. [OpenTofu Shortcuts ](/reference/cli/commands/opentofu-shortcuts)Use Terragrunt as a drop-in replacement for OpenTofu/Terraform commands. Terragrunt also has some other commands that are unique to Terragrunt. ## Main Commands [Section titled “Main Commands”](#main-commands) These are the main commands you will use with Terragrunt: [exec ](/reference/cli/commands/exec)Execute an arbitrary command, wrapped by Terragrunt. [run ](/reference/cli/commands/run)Run OpenTofu/Terraform commands. ## Backend Commands [Section titled “Backend Commands”](#backend-commands) These are the commands that are used when working with OpenTofu/Terraform state backends: [backend bootstrap ](/reference/cli/commands/backend/bootstrap)Bootstrap OpenTofu/Terraform backend infrastructure. [backend delete ](/reference/cli/commands/backend/delete)Delete backend state used by a unit. [backend migrate ](/reference/cli/commands/backend/migrate)Migrate OpenTofu/Terraform state from one unit to another. ## Stack Commands [Section titled “Stack Commands”](#stack-commands) These are the commands that are used when working with a `terragrunt.stack.hcl` file: [stack clean ](/reference/cli/commands/stack/clean)Remove \`.terragrunt-stack\` directories created by \`stack\` commands. [stack generate ](/reference/cli/commands/stack/generate)Generate a stack of units based on configurations in a terragrunt.stack.hcl file. [stack output ](/reference/cli/commands/stack/output)Get outputs from a stack of units. [stack run ](/reference/cli/commands/stack/run)Run a command against a stack of units defined in a terragrunt.stack.hcl file. ## Catalog Commands [Section titled “Catalog Commands”](#catalog-commands) These are the commands that are used when working with a Terragrunt catalog: [catalog ](/reference/cli/commands/catalog)Launch a Terminal User Interface (TUI) to browse and use OpenTofu/Terraform modules. [scaffold ](/reference/cli/commands/scaffold)Generate Terragrunt configuration files from a catalog. ## Discovery Commands [Section titled “Discovery Commands”](#discovery-commands) These are the commands that are used to discover units in your Terragrunt project: [find ](/reference/cli/commands/find)Find relevant Terragrunt configurations. [list ](/reference/cli/commands/list)List Terragrunt configurations in your codebase. ## Configuration Commands [Section titled “Configuration Commands”](#configuration-commands) These are the commands that are used to interact directly with Terragrunt configuration: [dag graph ](/reference/cli/commands/dag/graph)Graph the Directed Acyclic Graph (DAG) in DOT language. [hcl fmt ](/reference/cli/commands/hcl/fmt)Recursively find HashiCorp Configuration Language (HCL) files and rewrite them into a canonical format. [hcl validate ](/reference/cli/commands/hcl/validate)Recursively find HashiCorp Configuration Language (HCL) files and validate them. [info print ](/reference/cli/commands/info/print)Print out a short description of Terragrunt context. [info strict ](/reference/cli/commands/info/strict)List and inspect strict controls. [render ](/reference/cli/commands/render)Render the Terragrunt configuration in the current working directory, with as much work done as possible beforehand (that is, with all includes merged, dependencies resolved/interpolated, function calls executed, etc). ## Global Flags [Section titled “Global Flags”](#global-flags) There are some flags that are available to all Terragrunt commands: [Global Flags ](/reference/cli/global-flags)Global flags for the Terragrunt CLI. # bootstrap > Interact with OpenTofu/Terraform backend infrastructure. # delete > Delete OpenTofu/Terraform state. # migrate > Migrate OpenTofu/Terraform state from one location to another. # catalog > Launch a Terminal User Interface (TUI) to browse and use OpenTofu/Terraform modules. # graph > Graph the Directed Acyclic Graph (DAG) in DOT language. # exec > Execute an arbitrary command, wrapped by Terragrunt. # find > Find relevant Terragrunt configurations. # fmt > Recursively find HashiCorp Configuration Language (HCL) files and rewrite them into a canonical format. # validate > Recursively find HashiCorp Configuration Language (HCL) files and validate them. # print > Print out a short description of Terragrunt context. # strict > List and inspect strict controls. # list > List Terragrunt configurations. # OpenTofu Shortcuts > Interact with OpenTofu/Terraform backend infrastructure. # render > Render a simplified, but equivalent Terragrunt config. # run > Run OpenTofu/Terraform commands. # scaffold > Generate Terragrunt configuration files from a catalog. # clean > Remove the auto-generated `.terragrunt-stack` directories created by `stack` commands. # generate > Generate a stack of units based on configurations in a terragrunt.stack.hcl file. # output > Retrieve outputs from units defined in a terragrunt.stack.hcl file as an aggregated output. # run > Run a command against a stack of units defined in a terragrunt.stack.hcl file. # Global Flags > Global flags for the Terragrunt CLI. The Terragrunt CLI supports the following global flags: ## —experiment [Section titled “—experiment”](#experiment) \--experiment Enables a specific experiment. For a list of available experiments, see the [experiments documentation](/reference/experiments). Type: string Environment Variables: * `TG_EXPERIMENT` ## —experiment-mode [Section titled “—experiment-mode”](#experiment-mode) \--experiment-mode Enables experiment mode for Terragrunt. For more information, see the [experiments documentation](/reference/experiments). Type: boolean Environment Variables: * `TG_EXPERIMENT_MODE` ## —log-custom-format [Section titled “—log-custom-format”](#log-custom-format) \--log-custom-format Set the custom log formatting. For more information, see the [log formatting documentation](/reference/logging/formatting). Type: string Environment Variables: * `TG_LOG_CUSTOM_FORMAT` ## —log-disable [Section titled “—log-disable”](#log-disable) \--log-disable Disable logging. When enabled, Terragrunt will disable all logging output. This is useful when you want to see only the OpenTofu/Terraform output or when using Terragrunt in scripts where logging isn’t needed. Note that this automatically enables the `--tf-forward-stdout` flag to ensure OpenTofu/Terraform output is still visible. Type: bool Environment Variables: * `TG_LOG_DISABLE` ## —log-format [Section titled “—log-format”](#log-format) \--log-format Set the format for Terragrunt's log output. For a list of available formats and their descriptions, see the [log formatting documentation](/reference/logging/formatting). Type: string Environment Variables: * `TG_LOG_FORMAT` ## —log-level [Section titled “—log-level”](#log-level) \--log-level Sets the logging level for Terragrunt. Controls the verbosity of Terragrunt’s logging output. For more information about available log levels and their meanings, see the [log levels documentation](/reference/logging#log-levels). Type: string Environment Variables: * `TG_LOG_LEVEL` ## —log-show-abs-paths [Section titled “—log-show-abs-paths”](#log-show-abs-paths) \--log-show-abs-paths Show absolute paths in logs. When enabled, Terragrunt will show absolute paths in log messages instead of relative paths. For more information, see the [log formatting documentation](/reference/logging/formatting). Type: bool Environment Variables: * `TG_LOG_SHOW_ABS_PATHS` ## —no-color [Section titled “—no-color”](#no-color) \--no-color Disable color output for both Terragrunt and OpenTofu/Terraform. When enabled, Terragrunt will disable colored output in both its own logs and in OpenTofu/Terraform output. This is useful when running in environments where color codes might cause issues, such as CI/CD pipelines or when redirecting output to files. Type: bool Environment Variables: * `TG_NO_COLOR` ## —no-tip [Section titled “—no-tip”](#no-tip) \--no-tip Disable specific tips from being displayed. Disables specific tips from being displayed. This flag can be used multiple times to disable multiple tips. To disable all tips at once, use `--no-tips` instead. Type: string Environment Variables: * `TG_NO_TIP` ## —no-tips [Section titled “—no-tips”](#no-tips) \--no-tips Disable all tips from being displayed. When enabled, Terragrunt will not display any tips in the output. To disable only specific tips, use `--no-tip` instead. Type: bool Environment Variables: * `TG_NO_TIPS` ## —non-interactive [Section titled “—non-interactive”](#non-interactive) \--non-interactive Assume "yes" for all prompts. When enabled, Terragrunt will automatically answer “yes” to any prompts that would normally require user input. This is particularly useful in automated environments or CI/CD pipelines where user interaction isn’t possible. Caution When using `--non-interactive`, Terragrunt will automatically answer “yes” to all prompts except for external dependency inclusion prompts. These will default to “no” for safety. Use [`--queue-include-external`](/reference/cli/commands/run#queue-include-external) to explicitly include external dependencies. Type: bool Environment Variables: * `TG_NON_INTERACTIVE` ## —strict-control [Section titled “—strict-control”](#strict-control) \--strict-control Enables specific strict controls. Enables specific strict controls in Terragrunt. For a list of available controls and their descriptions, run `terragrunt info strict`. This flag provides finer-grained control compared to `--strict-mode`, allowing you to enable specific controls rather than all of them. For more information, see the [strict controls documentation](/reference/strict-controls). Caution When accessing complex data structures using the `raw` format, you must use index-based access. Attempting to access nested structures directly may result in unexpected output or errors. Type: string Environment Variables: * `TG_STRICT_CONTROL` ## —strict-mode [Section titled “—strict-mode”](#strict-mode) \--strict-mode Enables strict mode for Terragrunt. When enabled, Terragrunt will operate in strict mode, which opts in the user to all future breaking changes. For more information about what strict mode enables, run `terragrunt info strict`. For more information about strict mode, see the [strict controls documentation](/reference/strict-controls). Type: bool Environment Variables: * `TG_STRICT_MODE` ## —working-dir [Section titled “—working-dir”](#working-dir) \--working-dir The path where all commands should be run. Default is current directory. Specifies the working directory where Terragrunt should execute its commands. By default, Terragrunt uses the current directory. Example: ```bash terragrunt run plan --working-dir /path/to/infrastructure/prod ``` Note The `--working-dir` flag behaves differently for `run --all` commands versus single unit commands. For `run --all`, Terragrunt will execute in all subdirectories of the working directory, while for `run` commands it will execute only in the specified directory. Type: string Environment Variables: * `TG_WORKING_DIR` ## —help [Section titled “—help”](#help) \--help Show help information. Displays help information about Terragrunt commands and their usage. Can be used with specific commands to get detailed help about that command. Type: bool ## —version [Section titled “—version”](#version) \--version Show terragrunt version. Displays the current version of Terragrunt. Type: bool # Experiments > Opt-in to experimental features before they're stable. Terragrunt supports operating in a mode referred to as “Experiment Mode”. Experiment Mode is a set of controls that can be enabled to opt in to experimental features before they’re stable. These features are subject to change and may be removed or altered at any time. They generally provide early access to new features or changes that are being considered for inclusion in future releases. Those experiments will be documented here so that you know the following: 1. What the experiment is. 2. What the experiment does. 3. How to provide feedback on the experiment. 4. What criteria must be met for the experiment to be considered stable. Sometimes, the criteria for an experiment to be considered stable is unknown, as there may not be a clear path to stabilization. In that case, this will be noted in the experiment documentation, and collaboration with the community will be encouraged to help determine the future of the experiment. ## Controlling Experiment Mode [Section titled “Controlling Experiment Mode”](#controlling-experiment-mode) The simplest way to enable experiment mode is to set the [experiment-mode](/reference/cli/global-flags#experiment-mode) flag. This will enable experiment mode for all Terragrunt commands, for all experiments (note that this isn’t generally recommended, unless you are following Terragrunt development closely and are prepared for the possibility of breaking changes). ```bash terragrunt plan --experiment-mode ``` You can also use the environment variable, which can be more useful in CI/CD pipelines: ```bash TG_EXPERIMENT_MODE='true' terragrunt plan ``` Instead of enabling experiment mode, you can also enable specific experiments by setting the [experiment](/reference/cli/global-flags#experiment) flag to a value that’s specific to an experiment. This can allow you to experiment with a specific unstable feature that you think might be useful to you. ```bash terragrunt plan --experiment symlinks ``` Again, you can also use the environment variable, which can be more useful in CI/CD pipelines: ```bash TG_EXPERIMENT='symlinks' terragrunt plan ``` You can also enable multiple experiments at once. ```bash terragrunt --experiment symlinks plan ``` Including the environment variable: ```bash TG_EXPERIMENT='symlinks,stacks' terragrunt plan ``` ## Available Experiments [Section titled “Available Experiments”](#available-experiments) * [Active Experiments](/reference/experiments/active): experiments available for opt-in, including what each one does, how to provide feedback, and the criteria for stabilization. * [Completed Experiments](/reference/experiments/completed): experiments that have graduated to stable features. Their flags are no longer needed and setting them has no effect. # Overview > Learn how to configure Terragrunt Terragrunt configuration is defined in [HCL](https://github.com/hashicorp/hcl) files. This uses the same HCL syntax as OpenTofu/Terraform itself. Here’s an example: terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } dependencies { paths = ["../vpc", "../mysql", "../redis"] } ``` The core of Terragrunt configuration is that of the [unit](/getting-started/terminology#unit), which is canonically defined using `terragrunt.hcl` files. Terragrunt also supports [JSON-serialized HCL](https://github.com/hashicorp/hcl/blob/hcl2/json/spec.md) defined in `terragrunt.hcl.json` files. Where `terragrunt.hcl` is mentioned in documentation, you can always use `terragrunt.hcl.json` instead. When determining the configuration for a unit, Terragrunt figures out the path to its configuration file according to the following rules: 1. The value of the `--config` command-line option, if specified. 2. The value of the `TG_CONFIG` environment variable, if defined. 3. A `terragrunt.hcl` file in the current working directory, if it exists. 4. A `terragrunt.hcl.json` file in the current working directory, if it exists. 5. If none of these are found, exit with an error. Refer to the following pages for a complete reference of supported features in the terragrunt configuration file: * [Blocks](/reference/hcl/blocks) * [Attributes](/reference/hcl/attributes) * [Functions](/reference/hcl/functions) ## Configuration parsing order [Section titled “Configuration parsing order”](#configuration-parsing-order) It is important to be aware of the terragrunt configuration parsing order when using features like [locals](/reference/hcl/blocks/#locals) and [dependency outputs](/features/stacks/stack-operations#passing-outputs-between-units), where you can reference attributes of other blocks in the config in your `inputs`. For example, because `locals` are evaluated before `dependency` blocks, you cannot bind outputs from `dependency` into `locals`. On the other hand, for the same reason, you can use `locals` in the `dependency` blocks. Currently terragrunt parses the config in the following order: 1. `include` block 2. `locals` block 3. Evaluation of values for `iam_role`, `iam_assume_role_duration`, `iam_assume_role_session_name`, and `iam_web_identity_token` attributes, if defined 4. `dependencies` block 5. `dependency` blocks, including calling `terragrunt output` on the dependent units to retrieve the outputs 6. Everything else 7. The config referenced by `include` 8. A merge operation between the config referenced by `include` and the current config. Blocks that are parsed earlier in the process will be made available for use in the parsing of later blocks. Similarly, you cannot use blocks that are parsed later earlier in the process (e.g you can’t reference `dependency` in `locals`, `include`, or `dependencies` blocks). Note that the parsing order is slightly different when using the `--all` flag of the [`run`](/reference/cli/commands/run) command. When using the `--all` flag, Terragrunt parses the configuration twice. In the first pass, it follows the following parsing order: 1. `include` block of all configurations in the tree 2. `locals` block of all configurations in the tree 3. `dependency` blocks of all configurations in the tree, but does NOT retrieve the outputs 4. `terraform` block of all configurations in the tree 5. `dependencies` block of all configurations in the tree The results of this pass are then used to build the dependency graph of the units in the stack. Once the graph is constructed, Terragrunt will loop through the units and run the specified command. It will then revert to the single configuration parsing order specified above for each unit as it runs the command. This allows Terragrunt to avoid resolving `dependency` on units that haven’t been applied yet when doing a clean deployment from scratch with `run --all apply`. ## Stacks [Section titled “Stacks”](#stacks) When multiple units, each with their own `terragrunt.hcl` file exist in child directories of a single parent directory, that parent directory becomes a [stack](/getting-started/terminology#stack). > **New to stacks?** For a comprehensive introduction to the concept, see our [Stacks](/features/stacks) guide. ### What is a terragrunt.stack.hcl file? [Section titled “What is a terragrunt.stack.hcl file?”](#what-is-a-terragruntstackhcl-file) A `terragrunt.stack.hcl` file is a **blueprint** that defines how to generate Terragrunt configuration programmatically. It tells Terragrunt: * What units to create. * Where to get their configurations from. * Where to place them in the directory structure. * What values to pass to each unit. ### The Two Types of Blocks [Section titled “The Two Types of Blocks”](#the-two-types-of-blocks) #### `unit` blocks - Define Individual Infrastructure Components [Section titled “unit blocks - Define Individual Infrastructure Components”](#unit-blocks---define-individual-infrastructure-components) * **Purpose**: Define a single, deployable piece of infrastructure. * **Use case**: When you want to create a single piece of isolated infrastructure (e.g. a specific VPC, database, or application). * **Result**: Generates a single `terragrunt.hcl` file in the specified path. #### `stack` blocks - Define Reusable Infrastructure Patterns [Section titled “stack blocks - Define Reusable Infrastructure Patterns”](#stack-blocks---define-reusable-infrastructure-patterns) * **Purpose**: Define a collection of related units that can be reused. * **Use case**: When you have a common, multi-unit pattern (like “dev environment” or “three-tier web application”) that you want to deploy multiple times. * **Result**: Generates another `terragrunt.stack.hcl` file that can contain more units or stacks. ### Comparison: unit vs stack blocks [Section titled “Comparison: unit vs stack blocks”](#comparison-unit-vs-stack-blocks) | Aspect | `unit` block | `stack` block | | -------------------- | ----------------------------------------------- | ------------------------------------------------------------------------ | | **Purpose** | Define a single infrastructure component | Define a reusable collection of components | | **When to use** | For specific, one-off infrastructure pieces | For patterns of infrastructure pieces that you want provisioned together | | **Generated output** | A directory with a single `terragrunt.hcl` file | A directory with a `terragrunt.stack.hcl` file | ### The Complete Workflow [Section titled “The Complete Workflow”](#the-complete-workflow) 1. **Author**: Write a `terragrunt.stack.hcl` file with `unit` and/or `stack` blocks. 2. **Generate**: Run `terragrunt stack generate` to create the actual units\*. 3. **Deploy**: Run `terragrunt stack run apply` to deploy all units\*\*. \* Multiple commands (like `stack run` or `run --all`) automatically generate units from `terragrunt.stack.hcl` files for you. \*\* You can also just use `run --all apply` to deploy all units in the stack. ### Example: Simple Stack with Units [Section titled “Example: Simple Stack with Units”](#example-simple-stack-with-units) terragrunt.stack.hcl ```hcl unit "vpc" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1" path = "vpc" values = { vpc_name = "main" cidr = "10.0.0.0/16" } } unit "database" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1" path = "database" values = { engine = "postgres" version = "13" vpc_path = "../vpc" } } ``` Running `terragrunt stack generate` creates: * terragrunt.stack.hcl * .terragrunt-stack * vpc * terragrunt.hcl * terragrunt.values.hcl * database * terragrunt.hcl * terragrunt.values.hcl ### Example: Nested Stack with Reusable Patterns [Section titled “Example: Nested Stack with Reusable Patterns”](#example-nested-stack-with-reusable-patterns) terragrunt.stack.hcl ```hcl stack "dev" { source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1" path = "dev" values = { environment = "development" cidr = "10.0.0.0/16" } } stack "prod" { source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1" path = "prod" values = { environment = "production" cidr = "10.1.0.0/16" } } ``` The referenced stack might contain: stacks/environment/terragrunt.stack.hcl ```hcl unit "vpc" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1" path = "vpc" values = { vpc_name = values.environment cidr = values.cidr } } unit "database" { source = "git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1" path = "database" values = { environment = values.environment vpc_path = "../vpc" } } ``` For more information on these configuration blocks, see: * [unit](/reference/hcl/blocks#unit) * [stack](/reference/hcl/blocks#stack) * [locals](/reference/hcl/blocks#locals) These special configurations are used by the [stack generate command](/reference/cli/commands/stack/generate) (and all the other `stack` prefixed commands) to generate units programmatically, on demand. The units they generate are valid unit configurations, and can be read and used as if they were manually authored. ## Included Configurations [Section titled “Included Configurations”](#included-configurations) When configurations are *included* via the [include](/reference/hcl/blocks#include) configuration block, Terragrunt expects configurations to be valid unit configurations. Generally speaking, any HCL file found in a Terragrunt project that isn’t named `terragrunt.hcl`, `terragrunt.stack.hcl` or `.terraform.lock.hcl` is expected to be partial unit configurations that will be included by a Terragrunt unit. ## Formatting HCL files [Section titled “Formatting HCL files”](#formatting-hcl-files) You can rewrite the HCL files to a canonical format using the `hclfmt` command built into `terragrunt`. Similar to `tofu fmt`, this command applies a subset of [the OpenTofu/Terraform language style conventions](https://www.terraform.io/docs/configuration/style.html), along with other minor adjustments for readability. By default, this command will recursively search for hcl files and format all of them for a given stack. Consider the following file structure: * root * root.hcl * prod * terragrunt.hcl * dev * terragrunt.hcl * qa * terragrunt.hcl * services * services.hcl * service01 * terragrunt.hcl If you run `terragrunt hcl fmt` at the `root`, this will update: * `root/root.hcl` * `root/prod/terragrunt.hcl` * `root/dev/terragrunt.hcl` * `root/qa/terragrunt.hcl` * `root/qa/services/services.hcl` * `root/qa/services/service01/terragrunt.hcl` You can set `--diff` option. `terragrunt hcl fmt --diff` will output the diff in a unified format which can be redirected to your favourite diff tool. `diff` utility must be presented in PATH. Additionally, there’s a flag `--check`. `terragrunt hcl fmt --check` will only verify if the files are correctly formatted **without rewriting** them. The command will return exit status 1 if any matching files are improperly formatted, or 0 if all matching `.hcl` files are correctly formatted. You can exclude directories from the formatting process by using the `--exclude-dir` flag. For example, `terragrunt hcl fmt --exclude-dir=qa/services`. If you want to format a single file, you can use the `--file` flag. For example, `terragrunt hcl fmt --file qa/services/services.hcl`. # Attributes > Learn about terragrunt hcl attributes Terragrunt HCL configuration uses [attributes](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#attribute-definitions) when there are values that need to be defined for Terragrunt as a whole. Think of attributes as the values used for Terragrunt configuration, such as the inputs to pass an orchestrated OpenTofu/Terraform binary, or the download directory to use. ## inputs [Section titled “inputs”](#inputs) The `inputs` attribute is a map that is used to specify the input variables and their values to pass in to OpenTofu/Terraform. Each entry of the map is passed to OpenTofu/Terraform using [the environment variable mechanism](https://opentofu.org/docs/language/values/variables/#environment-variables). This means that each input will be set using the form `TF_VAR_variablename`, with the value in `json` encoded format. Note that because the values are being passed in with environment variables and `json`, the type information is lost when crossing the boundary between Terragrunt and OpenTofu/Terraform. You must specify the proper [type constraint](https://opentofu.org/docs/language/values/variables/#type-constraints) on the variable in OpenTofu/Terraform in order for OpenTofu/Terraform to process the inputs as the right type. Example: terragrunt.hcl ```hcl inputs = { string = "string" number = 42 bool = true list_string = ["a", "b", "c"] list_number = [1, 2, 3] list_bool = [true, false] map_string = { foo = "bar" } map_number = { foo = 42 bar = 12345 } map_bool = { foo = true bar = false baz = true } object = { str = "string" num = 42 list = [1, 2, 3] map = { foo = "bar" } } from_env = get_env("FROM_ENV", "default") } ``` Using this attribute is roughly equivalent to setting the corresponding `TF_VAR_` attribute. For example, setting this in your `terragrunt.hcl`: terragrunt.hcl ```hcl inputs = { instance_type = "t2.micro" instance_count = 10 tags = { Name = "example-app" } } ``` And running: ```bash terragrunt apply ``` This is roughly equivalent to running: ```bash TF_VAR_instance_type="t2.micro" \ TF_VAR_instance_count=10 \ TF_VAR_tags='{"Name":"example-app"}' \ tofu apply # or terraform apply ``` ### Variable Precedence [Section titled “Variable Precedence”](#variable-precedence) Variables loaded in OpenTofu/Terraform will consequently use the following precedence order (with the highest precedence being lowest on the list): 1. `inputs` set in `terragrunt.hcl` files. 2. Explicitly set `TF_VAR_` environment variables (these will override the `inputs` set in `terragrunt.hcl` if they conflict). 3. `terraform.tfvars` files if present. 4. `terraform.tfvars.json` files if present. 5. Any `*.auto.tfvars` or `*.auto.tfvars.json` files, processed in lexical order of their filenames. 6. Any `-var` and `-var-file` options on the command line, in the order they are provided. ## download\_dir [Section titled “download\_dir”](#download_dir) The terragrunt `download_dir` string option can be used to override the default download directory. The precedence is as follows: `--download-dir` command line option → `TG_DOWNLOAD_DIR` env variable → `download_dir` attribute of the `terragrunt.hcl` file in the module directory → `download_dir` attribute of the included `terragrunt.hcl`. It supports all terragrunt functions, i.e. `path_relative_from_include()`. ## prevent\_destroy [Section titled “prevent\_destroy”](#prevent_destroy) Terragrunt `prevent_destroy` boolean flag allows you to protect selected OpenTofu/Terraform module. It will prevent `destroy` or `run --all destroy` command from actually destroying resources of the protected module. This is useful for modules you want to carefully protect, such as a database, or a module that provides auth. Example: terragrunt.hcl ```hcl terraform { source = "git::git@github.com:foo/modules.git//app?ref=v0.0.3" } prevent_destroy = true ``` ## iam\_role [Section titled “iam\_role”](#iam_role) The `iam_role` attribute can be used to specify an IAM role that Terragrunt should assume before invoking OpenTofu/Terraform. The precedence is as follows: `--iam-assume-role` command line option → `TG_IAM_ASSUME_ROLE` env variable → `iam_role` attribute of the `terragrunt.hcl` file in the module directory → `iam_role` attribute of the included `terragrunt.hcl`. Example: terragrunt.hcl ```hcl iam_role = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" ``` **Notes:** * Value of `iam_role` can reference local variables * Definitions of `iam_role` included from other HCL files through `include` ## iam\_assume\_role\_duration [Section titled “iam\_assume\_role\_duration”](#iam_assume_role_duration) The `iam_assume_role_duration` attribute can be used to specify the STS session duration, in seconds, for the IAM role that Terragrunt should assume before invoking OpenTofu/Terraform. The precedence is as follows: `--iam-assume-role-duration` command line option → `TG_IAM_ASSUME_ROLE_DURATION` env variable → `iam_assume_role_duration` attribute of the `terragrunt.hcl` file in the module directory → `iam_assume_role_duration` attribute of the included `terragrunt.hcl`. Example: terragrunt.hcl ```hcl iam_assume_role_duration = 14400 ``` ## iam\_assume\_role\_session\_name [Section titled “iam\_assume\_role\_session\_name”](#iam_assume_role_session_name) The `iam_assume_role_session_name` attribute can be used to specify the STS session name, for the IAM role that Terragrunt should assume before running OpenTofu/Terraform. The precedence is as follows: `--iam-assume-role-session-name` command line option → `TG_IAM_ASSUME_ROLE_SESSION_NAME` env variable → `iam_assume_role_session_name` attribute of the `terragrunt.hcl` file in the module directory → `iam_assume_role_session_name` attribute of the included `terragrunt.hcl`. ## iam\_web\_identity\_token [Section titled “iam\_web\_identity\_token”](#iam_web_identity_token) The `iam_web_identity_token` attribute can be used along with `iam_role` to assume a role using AssumeRoleWithWebIdentity. `iam_web_identity_token` can be set to either the token value (typically using `get_env()`), or the path to a file on disk. The precedence is as follows: `--iam-assume-role-web-identity-token` command line option → `TG_IAM_ASSUME_ROLE_WEB_IDENTITY_TOKEN` env variable → `iam_web_identity_token` attribute of the `terragrunt.hcl` file in the module directory → `iam_web_identity_token` attribute of the included `terragrunt.hcl`. The primary benefit of using AssumeRoleWithWebIdentity over regular AssumeRole is that it enables you to run terragrunt in your CI/CD pipelines without static AWS credentials. ### Git Provider Configuration [Section titled “Git Provider Configuration”](#git-provider-configuration) To use AssumeRoleWithWebIdentity in your CI/CD environment, you must first configure an AWS [OpenID Connect provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html) to trust the OIDC service provided by your git provider. Follow the instructions below for whichever Git provider you use: * GitLab: [Configure OpenID Connect in AWS to retrieve temporary credentials](https://docs.gitlab.com/ee/ci/cloud_services/aws/) * GitHub: [Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) * CircleCI: [Using OpenID Connect tokens in jobs](https://circleci.com/docs/openid-connect-tokens/) Once you have configured your OpenID Connect Provider and configured the trust policy of your IAM role according to the above instructions, you can configure Terragrunt to use the Web Identity Token in the following manner. If your Git provider provides the OIDC token as an environment variable, pass it in to the `iam_web_identity_token` as follows terragrunt.hcl ```hcl iam_role = "arn:aws:iam:::role/" iam_web_identity_token = get_env("") ``` If your Git provider provides the OIDC token as a file, simply pass the file path to `iam_web_identity_token` terragrunt.hcl ```hcl iam_role = "arn:aws:iam:::role/" iam_web_identity_token = "/path/to/token/file" ``` ## terraform\_binary [Section titled “terraform\_binary”](#terraform_binary) The terragrunt `terraform_binary` string option can be used to override the default binary Terragrunt calls (which is `tofu`). The precedence is as follows: `--tf-path` command line option → `TG_TF_PATH` env variable → `terragrunt.hcl` in the module directory → included `terragrunt.hcl` ## terraform\_version\_constraint [Section titled “terraform\_version\_constraint”](#terraform_version_constraint) The terragrunt `terraform_version_constraint` string overrides the default minimum supported version of OpenTofu/Terraform. Terragrunt usually only officially supports the latest version of OpenTofu/Terraform, however, in some cases an older version of OpenTofu/Terraform is needed. Example: terragrunt.hcl ```hcl terraform_version_constraint = ">= 0.11" ``` ## terragrunt\_version\_constraint [Section titled “terragrunt\_version\_constraint”](#terragrunt_version_constraint) The terragrunt `terragrunt_version_constraint` string can be used to specify which versions of the Terragrunt CLI can be used with your configuration. If the running version of Terragrunt doesn’t match the constraints specified, Terragrunt will produce an error and exit without taking any further actions. Example: terragrunt.hcl ```hcl terragrunt_version_constraint = ">= 0.23" ``` # Blocks > Learn about Terragrunt HCL configuration blocks Terragrunt HCL configuration uses [configuration blocks](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#blocks) when there’s a structural configuration that needs to be defined for Terragrunt. Think of configuration blocks as a way to control different systems used by Terragrunt, whereas [attributes](/reference/hcl/attributes) are used to define values for those systems. ## terraform [Section titled “terraform”](#terraform) The `terraform` block is used to configure how Terragrunt will interact with OpenTofu/Terraform. This includes specifying where to find the OpenTofu/Terraform configuration files, any extra arguments to pass to the `tofu`/`terraform` binary, and any hooks to run before or after calling OpenTofu/Terraform. The `terraform` block supports the following arguments: * `source` (attribute): Specifies where to find OpenTofu/Terraform configuration files. This parameter supports the same syntax as the [module source](https://opentofu.org/docs/language/modules/sources/) parameter for OpenTofu/Terraform `module` blocks **except for the Terraform registry** (see below note), including local file paths, Git URLs, and Git URLS with `ref` parameters. Terragrunt will download all the code in the repo (i.e. the part before the double-slash `//`) so that relative paths work correctly between modules in that repo. * The `source` parameter can be configured to pull OpenTofu/Terraform modules from any Terraform module registry using the `tfr` protocol. The `tfr` protocol expects URLs to be provided in the format `tfr://REGISTRY_HOST/MODULE_SOURCE[?version=VERSION]`. For example, to pull the `terraform-aws-modules/vpc/aws` module from the public Terraform registry, you can use the following as the source parameter: `tfr://registry.terraform.io/terraform-aws-modules/vpc/aws?version=3.3.0`. The `version` query parameter is optional. When omitted, Terragrunt queries the registry’s list-versions endpoint and downloads the latest stable (non-prerelease) version. Pin a version explicitly for reproducible builds in production. * If you wish to access a private module registry (e.g., [Terraform Cloud/Enterprise](https://www.terraform.io/docs/cloud/registry/index.html)), you can provide the authentication to Terragrunt as an environment variable with the key `TG_TF_REGISTRY_TOKEN`. This token can be any registry API token. * The `tfr` protocol supports a shorthand notation where the `REGISTRY_HOST` can be omitted to default to the public registry. The default registry depends on the wrapped executable: for Terraform, it is `registry.terraform.io`, and for Opentofu, it is `registry.opentofu.org`. Additionally, if the environment variable `TG_TF_DEFAULT_REGISTRY_HOST` is set, this value will be used as the default registry host instead, overriding the standard defaults for the wrapped executable. * If you use `tfr:///` (note the three `/`). For example, the following will fetch the `terraform-aws-modules/vpc/aws` module from the public registry: `tfr:///terraform-aws-modules/vpc/aws?version=3.3.0`. * You can also use submodules from the registry using `//`. For example, to use the `iam-policy` submodule from the registry module [terraform-aws-modules/iam](https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest), you can use the following: `tfr:///terraform-aws-modules/iam/aws//modules/iam-policy?version=4.3.0`. * `include_in_copy` (attribute): A list of glob patterns (e.g., `["*.txt"]`) that should always be copied from the same directory containing `terragrunt.hcl` into the OpenTofu/Terraform working directory. When you use the `source` param in your Terragrunt config and run `terragrunt `, Terragrunt will download the code specified at source into a scratch folder (`.terragrunt-cache`, by default), copy the code in your current working directory into the same scratch folder, and then run `tofu ` (or `terraform `) in that scratch folder. By default, Terragrunt excludes hidden files and folders during the copy step. This feature allows you to specify glob patterns of files that should always be copied from the Terragrunt working directory. Additional notes: * The path should be specified relative to the source directory. * This list is also used when using a local file source (e.g., `source = "../modules/vpc"`). For example, if your OpenTofu/Terraform module source contains a hidden file that you want to copy over (e.g., a `.python-version` file), you can specify that in this list to ensure it gets copied over to the scratch copy (e.g., `include_in_copy = [".python-version"]`). * `exclude_from_copy` (attribute): A list of glob patterns (e.g., `["*.txt"]`) that should always be skipped from the same directory containing `terragrunt.hcl` when copying into the OpenTofu/Terraform working directory. All examples valid for `include_in_copy` can be used here. *Note that using `include_in_copy` and `exclude_from_copy` are not mutually exclusive.* If a file matches a pattern in both `include_in_copy` and `exclude_from_copy`, it will not be included. If you would like to ensure that the file *is* included, make sure the patterns you use for `include_in_copy` do not match the patterns in `exclude_from_copy`. *Note that if you wish to exclude files from being copied from a terraform module source, you should use the [before\_hook](/features/units/hooks) feature.* * `copy_terraform_lock_file` (attribute): In certain use cases, you don’t want to check the terraform provider lock file into your source repository from your working directory as described in [Lock File Handling](/reference/lock-files). This attribute allows you to disable the copy of the generated or existing `.terraform.lock.hcl` from the temp folder into the working directory. Default is `true`. * `extra_arguments` (block): Nested blocks used to specify extra CLI arguments to pass to the `tofu`/`terraform` binary. Learn more about its usage in the [Keep your CLI flags DRY](/features/units/extra-arguments) use case overview. Supports the following arguments: * `arguments` (required) : A list of CLI arguments to pass to `tofu`/`terraform`. * `commands` (required) : A list of `tofu`/`terraform` sub commands that the arguments will be passed to. * `env_vars` (optional) : A map of key value pairs to set as environment variables when calling `tofu`/`terraform`. * `required_var_files` (optional): A list of file paths to OpenTofu/Terraform vars files (`.tfvars`) that will be passed in to `terraform` as `-var-file=`. * `optional_var_files` (optional): A list of file paths to OpenTofu/Terraform vars files (`.tfvars`) that will be passed in to `tofu`/`terraform` like `required_var_files`, only any files that do not exist are ignored. * `before_hook` (block): Nested blocks used to specify command hooks that should be run before `tofu`/`terraform` is called. Hooks run from the directory with the OpenTofu/Terraform module, except for hooks related to `read-config` and `init-from-module`. These hooks run in the terragrunt configuration directory (the directory where `terragrunt.hcl` lives). Supports the following arguments: * `commands` (required) : A list of `tofu`/`terraform` sub commands for which the hook should run before. * `execute` (required) : A list of command and arguments that should be run as the hook. For example, if `execute` is set as `["echo", "Foo"]`, the command `echo Foo` will be run. * `working_dir` (optional) : The path to set as the working directory of the hook. Terragrunt will switch directory to this path before running the hook command. Defaults to the terragrunt configuration directory for `read-config` and `init-from-module` hooks, and the OpenTofu/Terraform module directory for other command hooks. * `run_on_error` (optional) : If set to true, this hook will run even if a previous hook hit an error, or in the case of “after” hooks, if the OpenTofu/Terraform command hit an error. Default is false. * `suppress_stdout` (optional) : If set to true, the stdout output of the executed commands will be suppressed. This can be useful when there are scripts relying on OpenTofu/Terraform’s output and any other output would break their parsing. * `if` (optional) : hook will be skipped when the argument is set or evaluates to `false`. * `after_hook` (block): Nested blocks used to specify command hooks that should be run after `tofu`/`terraform` is called. Hooks run from the terragrunt configuration directory (the directory where `terragrunt.hcl` lives). Supports the same arguments as `before_hook`. * `error_hook` (block): Nested blocks used to specify command hooks that run when an error is thrown. The error must match one of the expressions listed in the `on_errors` attribute. Error hooks are executed after the before/after hooks. To handle errors during source download (when using the `source` attribute), use `init-from-module` in the `commands` list. * `mutable` (attribute): When `true`, content fetched into `.terragrunt-cache` through the [content-addressable storage (CAS)](/features/caching/cas) is copied from the CAS store and the working tree under `.terragrunt-cache` is editable. The default is `false`: files are materialized read-only so an accidental edit cannot reach back into the shared CAS store. The flag has no effect when CAS is not used to fetch the source; the standard download path already produces an independent, writable copy. In addition to supporting before and after hooks for all OpenTofu/Terraform commands, the following specialized hooks are also supported: * `read-config` (after hook only): `read-config` is a special hook command that you can use with the `after_hook` subblock to run an action immediately after terragrunt finishes loading the config. This hook will run on every invocation of terragrunt. Note that you can only use this hook with `after_hooks`. Any `before_hooks` with the command `read-config` will be ignored. The working directory for hooks associated with this command will be the terragrunt config directory. * `init-from-module` and `init`: Terragrunt has two stages of initialization: one is to download [remote configurations](/features/units) using `go-getter`; the other is [Auto-Init](/features/units/auto-init), which configures the backend and downloads provider plugins and modules. If you wish to run a hook when Terragrunt is using `go-getter` to download remote configurations, use `init-from-module` for the command. This includes `error_hook` blocks to handle download failures. If you wish to execute a hook when Terragrunt is using `tofu init`/`terraform init` for Auto-Init, use `init` for the command. For example, an `after_hook` for the command `init-from-module` will run after terragrunt clones the module, while an `after_hook` for the command `init` will run after terragrunt runs `tofu init`/`terraform init` on the cloned module. * Hooks for both `init-from-module` and `init` only run if the requisite stage needs to run. That is, if terragrunt detects that the module is already cloned in the terragrunt cache, this stage will be skipped and thus the hooks will not run. Similarly, if terragrunt detects that it does not need to run `init` in the auto init feature, the `init` stage is skipped along with the related hooks. * The working directory for hooks associated with `init-from-module` will run in the terragrunt config directory, while the working directory for hooks associated with `init` will be the OpenTofu/Terraform module. Complete Example: terragrunt.hcl ```hcl terraform { # Pull the OpenTofu/Terraform configuration at the github repo "acme/infrastructure-modules", under the subdirectory # "networking/vpc", using the git tag "v0.0.1". source = "git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1" # For any OpenTofu/Terraform commands that use locking, make sure to configure a lock timeout of 20 minutes. extra_arguments "retry_lock" { commands = get_terraform_commands_that_need_locking() arguments = ["-lock-timeout=20m"] } # You can also specify multiple extra arguments for each use case. Here we configure terragrunt to always pass in the # `common.tfvars` var file located by the parent terragrunt config. extra_arguments "custom_vars" { commands = [ "apply", "plan", "import", "push", "refresh" ] required_var_files = ["${get_parent_terragrunt_dir()}/common.tfvars"] } # The following are examples of how to specify hooks # Before apply or plan, run "echo Foo". before_hook "before_hook_1" { commands = ["apply", "plan"] execute = ["echo", "Foo"] } # Before apply, run "echo Bar". Note that blocks are ordered, so this hook will run after the previous hook to # "echo Foo". In this case, always "echo Bar" even if the previous hook failed. before_hook "before_hook_2" { commands = ["apply"] execute = ["echo", "Bar"] run_on_error = true } # Note that you can use interpolations in subblocks. Here, we configure it so that before apply or plan, print out the # environment variable "HOME". before_hook "interpolation_hook_1" { commands = ["apply", "plan"] execute = ["echo", get_env("HOME", "HelloWorld")] run_on_error = false } # After running apply or plan, run "echo Baz". This hook is configured so that it will always run, even if the apply # or plan failed. after_hook "after_hook_1" { commands = ["apply", "plan"] execute = ["echo", "Baz"] run_on_error = true } # After an error occurs during apply or plan, run "echo Error Hook executed". This hook is configured so that it will run # after any error, with the ".*" expression. error_hook "error_hook_1" { commands = ["apply", "plan"] execute = ["echo", "Error Hook executed"] on_errors = [ ".*", ] } # Handle errors during source download (e.g., when the source URL is invalid or unreachable). # Use "init-from-module" as the command to catch errors during the go-getter download phase. error_hook "source_download_error" { commands = ["init-from-module"] execute = ["echo", "Source download failed"] on_errors = [".*"] } # A special after hook to always run after the init-from-module step of the Terragrunt pipeline. In this case, we will # copy the "foo.tf" file located by the parent terragrunt.hcl file to the current working directory. after_hook "init_from_module" { commands = ["init-from-module"] execute = ["cp", "${get_parent_terragrunt_dir()}/foo.tf", "."] } # A special after_hook. Use this hook if you wish to run commands immediately after terragrunt finishes loading its # configurations. If "read-config" is defined as a before_hook, it will be ignored as this config would # not be loaded before the action is done. after_hook "read-config" { commands = ["read-config"] execute = ["bash", "script/get_aws_credentials.sh"] } } ``` Local File Path Example with allowed hidden files: terragrunt.hcl ```hcl terraform { # Pull the OpenTofu/Terraform configuration from the local file system. Terragrunt will make a copy of the source folder in the # Terragrunt working directory (typically `.terragrunt-cache`). source = "../modules/networking/vpc" # Always include the following file patterns in the Terragrunt copy. include_in_copy = [ ".security_group_rules.json", "*.yaml", ] } ``` ### A note about using modules from the registry [Section titled “A note about using modules from the registry”](#a-note-about-using-modules-from-the-registry) The key design of Terragrunt is to act as a preprocessor to convert **shared service modules** in the registry into a **root module**. In OpenTofu/Terraform, modules can be loosely categorized into two types: * **Root Module**: An OpenTofu/Terraform module that is designed for running `tofu init`/`terraform init` and the other workflow commands (`apply`, `plan`, etc.). This is the entrypoint module for deploying your infrastructure. Root modules are identified by the presence of key blocks that setup configuration about how OpenTofu/Terraform behaves, like `backend` blocks (for configuring state) and `provider` blocks (for configuring how OpenTofu/Terraform interacts with the cloud APIs). * **Shared Module**: A OpenTofu/Terraform module that is designed to be included in other OpenTofu/Terraform modules through `module` blocks. These modules are missing many of the key blocks that are required for running the workflow commands of OpenTofu/Terraform. Terragrunt further distinguishes shared modules between **service modules** and **modules**: * **Shared Service Module**: An OpenTofu/Terraform module that is designed to be standalone and applied directly. These modules are not root modules in that they are still missing the key blocks like `backend` and `provider`, but aside from that do not need any additional configuration or composition to deploy. For example, the [terraform-aws-modules/vpc](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest) module can be deployed by itself without composing with other modules or resources. * **Shared Module**: An OpenTofu/Terraform module that is designed to be composed with other modules. That is, these modules must be embedded in another OpenTofu/Terraform module and combined with other resources or modules. For example, the [consul-security-group-rules module](https://registry.terraform.io/modules/hashicorp/consul/aws/latest/submodules/consul-security-group-rules) Terragrunt started off with features that help directly deploy **Root Modules**, but over the years have implemented many features that allow you to turn **Shared Service Modules** into **Root Modules** by injecting the key configuration blocks that are necessary for OpenTofu/Terraform modules to act as **Root Modules**. Modules on the Terraform Registry are primarily designed to be used as **Shared Modules**. That is, you won’t be able to `git clone` the underlying repository and run `tofu init`/`terraform init` or `apply` directly on the module without modification. Unless otherwise specified, almost all the modules will require composition with other modules/resources to deploy. When using modules in the registry, it helps to think about what blocks and resources are necessary to operate the module, and translating those into Terragrunt blocks that generate them. Note that often, Terragrunt may not be able to deploy modules from the registry. While Terragrunt has features to turn any **Shared Module** into a **Root Module**, there are two key technical limitations that prevent Terragrunt from converting ALL shared modules: * Every complex input must have a `type` associated with it. Otherwise, OpenTofu/Terraform will interpret the input that Terragrunt passes through as `string`. This includes `list` and `map`. * Derived sensitive outputs must be marked as `sensitive`. Refer to the [terraform tutorial on sensitive variables](https://learn.hashicorp.com/tutorials/terraform/sensitive-variables#reference-sensitive-variables) for more information on this requirement. **If you run into issues deploying a module from the registry, chances are that module is not a Shared Service Module, and thus not designed for use with Terragrunt. Depending on the technical limitation, Terragrunt may be able to support the transition to root module. Please always file [an issue on the terragrunt repository](https://github.com/gruntwork-io/terragrunt/issues) with the module + error message you are encountering, instead of the module repository.** ## remote\_state [Section titled “remote\_state”](#remote_state) The `remote_state` block is used to configure how Terragrunt will set up the remote state configuration of your OpenTofu/Terraform code. You can read more about Terragrunt’s remote state functionality in [Keep your remote state configuration DRY](/features/units/state-backend/) use case overview. The `remote_state` block supports the following arguments: * `backend` (attribute): Specifies which remote state backend will be configured. This should be one of the [available backends](https://opentofu.org/docs/language/settings/backends/configuration/#available-backends) that Opentofu/Terraform supports. * `disable_init` (attribute): When `true`, skip automatic creation and management of remote state resources by Terragrunt. Some backends can be automatically created if the storage backend does not already exist. Currently, `s3` and `gcs` are the two backends with support for automatic creation. Setting this to `true` prevents Terragrunt from creating or modifying these resources, but OpenTofu/Terraform will still initialize the backend normally. Defaults to `false`. **Note:** When using `generate` with `disable_init = true`, the backend configuration is written to the generated `.tf` file. OpenTofu/Terraform will still attempt to connect to the backend during init. The `--backend-bootstrap` flag controls whether Terragrunt creates backend resources (e.g., S3 buckets) before running `init`. It defaults to `false`. | `disable_init` | `--backend-bootstrap` | Backend exists | Result | | -------------- | --------------------- | -------------- | ------------------------------------------------------------- | | `false` | `true` | No | Terragrunt creates backend resources, init succeeds | | `false` | `true` | Yes | Terragrunt verifies backend config, init succeeds | | `false` | `false` | No | No creation, OpenTofu/Terraform init fails (bucket not found) | | `false` | `false` | Yes | No creation, init succeeds | | `true` | any | Yes | No creation, OpenTofu/Terraform inits normally | | `true` | any | No | No creation, OpenTofu/Terraform init fails (bucket not found) | * `disable_dependency_optimization` (attribute): When `true`, disable optimized dependency fetching for terragrunt modules using this `remote_state` block. See the documentation for [dependency block](#dependency) for more details. * `generate` (attribute): Configure Terragrunt to automatically generate a `.tf` file that configures the remote state backend. This is a map that expects two properties: * `path`: The path where the generated file should be written. If a relative path, it’ll be relative to the Terragrunt working dir (where the OpenTofu/Terraform code lives). * `if_exists` (attribute): What to do if a file already exists at `path`. Valid values are: * `overwrite` (overwrite the existing file) * `overwrite_terragrunt` (overwrite the existing file if it was generated by terragrunt; otherwise, error) * `skip` (skip code generation and leave the existing file as-is) * `error` (exit with an error) * `config` (attribute): An arbitrary map that is used to fill in the backend configuration in OpenTofu/Terraform. All the properties will automatically be included in the OpenTofu/Terraform backend block (with a few exceptions: see below). * `encryption` (attribute): A map that is used to configure state and plan encryption in OpenTofu. The properties will be transformed into an `encryption` block in the OpenTofu terraform block. The properties are specific to the respective `key_provider` (see below). For example, if you had the following `remote_state` block: terragrunt.hcl ```hcl remote_state { backend = "s3" config = { bucket = "mybucket" key = "path/to/my/key" region = "us-east-1" } } ``` This is equivalent to the following OpenTofu/Terraform code: main.tf ```hcl terraform { backend "s3" { bucket = "mybucket" key = "path/to/my/key" region = "us-east-1" } } ``` Note that `remote_state` can also be set as an attribute. This is useful if you want to set `remote_state` dynamically. For example, if in `common.hcl` you had: common.hcl ```hcl remote_state { backend = "s3" config = { bucket = "mybucket" key = "path/to/my/key" region = "us-east-1" } } ``` Then in a `terragrunt.hcl` file, you could dynamically set `remote_state` as an attribute as follows: terragrunt.hcl ```hcl locals { # Load the data from common.hcl common = read_terragrunt_config(find_in_parent_folders("common.hcl")) } # Set the remote_state config dynamically to the remote_state config in common.hcl remote_state = local.common.remote_state ``` ### backend [Section titled “backend”](#backend) Note that Terragrunt does special processing of the `config` attribute for the `s3` and `gcs` remote state backends, and supports additional keys that are used to configure the automatic initialization feature of Terragrunt. For the `s3` backend, the following additional properties are supported in the `config` attribute: * `region` - (Optional) The region of the S3 bucket. * `profile` - (Optional) This is the AWS profile name as set in the shared credentials file. * `endpoint` - (Optional) A custom endpoint for the S3 API. * `endpoints`: (Optional) A configuration `map` for custom service API (starting with Terraform 1.6). * `s3` - (Optional) A custom endpoint for the S3 API. Overrides `endpoint` argument. * `dynamodb` - (Optional) A custom endpoint for the DynamoDB API. Overrides `dynamodb_endpoint` argument. * `encrypt` - (Optional) Whether to enable server-side encryption of the state file. If disabled, a log warning will be issued in the console output to notify the user. If `skip_bucket_ssencryption` is enabled, the log will be written as a debug log. * `role_arn` - (Optional) The role to be assumed. * `shared_credentials_file` - (Optional) This is the path to the shared credentials file. If this is not set and a profile is specified, `~/.aws/credentials` will be used. * `external_id` - (Optional) The external ID to use when assuming the role. * `session_name` - (Optional) The session name to use when assuming the role. * `dynamodb_table` - (Optional) The name of a DynamoDB table to use for state locking and consistency. The table must have a primary key named LockID. If not present, locking will be disabled. * `use_lockfile` - (Optional) When `true`, enables native S3 locking using S3 object conditional writes for state locking. This feature requires OpenTofu >= 1.10. Can be used simultaneously with `dynamodb_table` during migration (both locks must be acquired successfully), but typically used as a replacement for DynamoDB locking. * `skip_bucket_versioning`: When `true`, the S3 bucket that is created to store the state will not be versioned. * `skip_bucket_ssencryption`: When `true`, the S3 bucket that is created to store the state will not be configured with server-side encryption. * `skip_bucket_accesslogging`: *DEPRECATED* If provided, will be ignored. A log warning will be issued in the console output to notify the user. * `skip_bucket_root_access`: When `true`, the S3 bucket that is created will not be configured with bucket policies that allow access to the root AWS user. * `skip_bucket_enforced_tls`: When `true`, the S3 bucket that is created will not be configured with a bucket policy that enforces access to the bucket via a TLS connection. * `skip_bucket_public_access_blocking`: When `true`, the S3 bucket that is created will not have public access blocking enabled. * `disable_bucket_update`: When `true`, disable update S3 bucket if not equal configured in config block * `enable_lock_table_ssencryption`: When `true`, the synchronization lock table in DynamoDB used for remote state concurrent access will be configured with server-side encryption. * `s3_bucket_tags`: A map of key value pairs to associate as tags on the created S3 bucket. * `dynamodb_table_tags`: A map of key value pairs to associate as tags on the created DynamoDB remote state lock table. * `accesslogging_bucket_tags`: A map of key value pairs to associate as tags on the created S3 bucket to store de access logs. * `disable_aws_client_checksums`: When `true`, disable computing and checking checksums on the request and response, such as the CRC32 check for DynamoDB. See [#1059](https://github.com/gruntwork-io/terragrunt/issues/1059) for issue where this is a useful workaround. * `accesslogging_bucket_name`: (Optional) When provided as a valid `string`, create an S3 bucket with this name to store the access logs for the S3 bucket used to store OpenTofu/Terraform state. If not provided, or string is empty or invalid S3 bucket name, then server access logging for the S3 bucket storing the Opentofu/Terraform state will be disabled. **Note:** When access logging is enabled supported encryption for state bucket is only `AES256`. Reference: [S3 server access logging](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html) * `accesslogging_target_object_partition_date_source`: (Optional) When provided as a valid `string`, it configures the `PartitionDateSource` option. This option is part of the `TargetObjectKeyFormat` and `PartitionedPrefix` AWS configurations, allowing you to configure the log object key format for the access log files. Reference: [Logging requests with server access logging](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html). * `accesslogging_target_prefix`: (Optional) When provided as a valid `string`, set the `TargetPrefix` for the access log objects in the S3 bucket used to store Opentofu/Terraform state. If set to **empty**`string`, then `TargetPrefix` will be set to **empty** `string`. If attribute is not provided at all, then `TargetPrefix` will be set to **default** value `TFStateLogs/`. This attribute won’t take effect if the `accesslogging_bucket_name` attribute is not present. * `skip_accesslogging_bucket_acl`: When set to `true`, the S3 bucket where access logs are stored will not be configured with bucket ACL. * `skip_accesslogging_bucket_enforced_tls`: When set to `true`, the S3 bucket where access logs are stored will not be configured with a bucket policy that enforces access to the bucket via a TLS connection. * `skip_accesslogging_bucket_public_access_blocking`: When set to `true`, the S3 bucket where access logs are stored will not have public access blocking enabled. * `skip_accesslogging_bucket_ssencryption`: When set to `true`, the S3 bucket where access logs are stored will not be configured with server-side encryption. * `bucket_sse_algorithm`: (Optional) The algorithm to use for server-side encryption of the state bucket. Defaults to `aws:kms`. * `bucket_sse_kms_key_id`: (Optional) The KMS Key to use when the encryption algorithm is `aws:kms`. Defaults to the AWS Managed `aws/s3` key. * `assume_role`: (Optional) A configuration `map` to use when assuming a role (starting with Terraform 1.6 for Terraform). Override top level arguments * `role_arn` - (Required) The role to be assumed. * `duration` - (Optional) The duration the credentials will be valid. * `external_id` - (Optional) The external ID to use when assuming the role. * `policy` - (Optional) Policy JSON to further restrict the role. * `policy_arns` - (Optional) A list of policy ARNs to further restrict the role. * `session_name` - (Optional) The session name to use when assuming the role. * `source_identity` - (Optional) The source identity to use when assuming the role. * `tags` - (Optional) A map of key value pairs used as assume role session tags. * `transitive_tag_keys` - (Optional) A list of tag keys that to be passed. * `assume_role_with_web_identity` - (Optional) A configuration `map` to use when assuming a role with a web identity token. * `role_arn` - (Required) The role to be assumed. * `duration` - (Optional) The duration the credentials will be valid. * `policy` - (Optional) Policy JSON to further restrict the role. * `policy_arns` - (Optional) A list of policy ARNs to further restrict the role. * `session_name` - (Optional) The session name to use when assuming the role. * `web_identity_token` - (Required) The web identity token to use when assuming the role. * `web_identity_token_file` - (Optional) The path to the file containing the web identity token to use when assuming the role. For the `gcs` backend, the following additional properties are supported in the `config` attribute: * `skip_bucket_creation`: When `true`, Terragrunt will skip the auto initialization routine for setting up the GCS bucket for use with remote state. * `skip_bucket_versioning`: When `true`, the GCS bucket that is created to store the state will not be versioned. * `enable_bucket_policy_only`: When `true`, the GCS bucket that is created to store the state will be configured to use uniform bucket-level access. * `project`: The GCP project where the bucket will be created. * `location`: The GCP location where the bucket will be created. * `gcs_bucket_labels`: A map of key value pairs to associate as labels on the created GCS bucket. * `credentials`: Local path to Google Cloud Platform account credentials in JSON format. * `access_token`: A temporary \[OAuth 2.0 access token] obtained from the Google Authorization server. Example with S3: root.hcl ```hcl # Configure OpenTofu/Terraform state to be stored in S3, in the bucket "my-tofu-state" in us-east-1 under a key that is # relative to included terragrunt config. For example, if you had the following filesystem layout: # # . # ├── root.hcl # └── child # ├── main.tf # └── terragrunt.hcl # # And the following is defined in the root terragrunt.hcl config that is included in the child, the state file for the # child module will be stored at the key "child/tofu.tfstate". # # Note that since we are not using any of the skip args, this will automatically create the S3 bucket # "my-tofu-state" and DynamoDB table "my-lock-table" if it does not already exist. remote_state { backend = "s3" config = { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } ``` child/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } ``` child/main.tf ```hcl terraform { backend "s3" {} } ``` Example with GCS: root.hcl ```hcl # Configure OpenTofu/Terraform state to be stored in GCS, in the bucket "my-tofu-state" in the "my-tofu" GCP project in # the eu region under a key that is relative to included terragrunt config. This will also apply the labels # "owner=terragrunt_test" and "name=tofu_state_storage" to the bucket if it is created by Terragrunt. # # For example, if you had the following filesystem layout: # # . # ├── root.hcl # └── child # ├── main.tf # └── terragrunt.hcl # # And the following is defined in the root terragrunt.hcl config that is included in the child, the state file for the # child module will be stored at the key "child/tofu.tfstate". # # Note that since we are not using any of the skip args, this will automatically create the GCS bucket # "my-tofu-state" if it does not already exist. remote_state { backend = "gcs" config = { project = "my-tofu" location = "eu" bucket = "my-tofu-state" prefix = "${path_relative_to_include()}/tofu.tfstate" gcs_bucket_labels = { owner = "terragrunt_test" name = "tofu_state_storage" } } } ``` child/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } ``` child/main.tf ```hcl terraform { backend "gcs" {} } ``` Example with S3 using native S3 locking (OpenTofu >= 1.10): ```hcl # Configure OpenTofu/Terraform state to be stored in S3 with native S3 locking instead of DynamoDB. # This uses S3 object conditional writes for state locking, which requires OpenTofu >= 1.10. remote_state { backend = "s3" config = { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } } ``` Example with S3 using both DynamoDB and native S3 locking during migration (OpenTofu >= 1.10): ```hcl # Configure OpenTofu/Terraform state with dual locking during migration from DynamoDB to S3 native locking. # Both locks must be successfully acquired before operations can proceed. # After the migration period, remove dynamodb_table to use only S3 native locking. # Note: This won't delete the DynamoDB table, it will just be unused. # You can delete it manually after the migration period. remote_state { backend = "s3" config = { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" # Remove this after migration period use_lockfile = true # New native S3 locking } } ``` ### encryption [Section titled “encryption”](#encryption) The encryption map needs a `key_provider` property, which can be set to any provider [supported by OpenTofu](https://opentofu.org/docs/language/state/encryption/#key-providers); including: `pbkdf2`, `aws_kms`, `gcp_kms`, or `openbao`. Documentation for each provider type and its possible configuration can be found in the [OpenTofu docs](https://opentofu.org/docs/language/state/encryption/#key-providers). A `terragrunt.hcl` file configuring PBKDF2 encryption could look like this: terragrunt.hcl ```hcl remote_state { backend = "s3" config = { bucket = "mybucket" key = "path/to/my/key" region = "us-east-1" } encryption = { key_provider = "pbkdf2" passphrase = get_env("PBKDF2_PASSPHRASE") } } ``` This would result in the following OpenTofu code: main.tf ```hcl terraform { backend "s3" { bucket = "mybucket" key = "path/to/my/key" region = "us-east-1" } encryption { key_provider "pbkdf2" "default" { passphrase = "SUPERSECRETPASSPHRASE" } method "aes_gcm" "default" { keys = key_provider.pbkdf2.default } state { method = method.aes_gcm.default } plan { method = method.aes_gcm.default } } } ``` ## include [Section titled “include”](#include) The `include` block is used to specify inheritance of Terragrunt configuration files. The included config (also called the `parent`) will be merged with the current configuration (also called the `child`) before processing. You can learn more about the inheritance properties of Terragrunt in the [Filling in remote state settings with Terragrunt section](/features/units/state-backend/#generating-remote-state-settings-with-terragrunt) of the “Keep your remote state configuration DRY” use case overview. You can have more than one `include` block, but each one must have a unique label. It is recommended to always label your `include` blocks. Bare includes (`include` block with no label - e.g., `include {}`) are currently supported for backward compatibility, but is deprecated usage and support may be removed in the future. `include` blocks support the following arguments: * `name` (label): You can define multiple `include` blocks in a single terragrunt config. Each include block must be labeled with a unique name to differentiate it from the other includes. e.g., if you had a block `include "remote" {}`, you can reference the relevant exposed data with the expression `include.remote`. * `path` (attribute): Specifies the path to a Terragrunt configuration file (the `parent` config) that should be merged with this configuration (the `child` config). * `expose` (attribute, optional): Specifies whether or not the included config should be parsed and exposed as a variable. When `true`, you can reference the data of the included config under the variable `include`. Defaults to `false`. Note that the `include` variable is a map of `include` labels to the parsed configuration value. * `merge_strategy` (attribute, optional): Specifies how the included config should be merged. Valid values are: `no_merge` (do not merge the included config), `shallow` (do a shallow merge - default), `deep` (do a deep merge of the included config). **NOTE**: At this time, Terragrunt only supports a single level of `include` blocks. That is, Terragrunt will error out if an included config also has an `include` block defined. If you are interested in this feature, please follow [#1566](https://github.com/gruntwork-io/terragrunt/issues/1566) to be notified when nested `include` blocks are supported. **Special case for shallow merge**: When performing a shallow merge, all attributes and blocks are merged shallowly with replacement, except for `dependencies` blocks (NOT `dependency` block). `dependencies` blocks are deep merged: that is, all the lists of paths from included configurations are concatenated together, rather than replaced in override fashion. Examples: ### Single include [Section titled “Single include”](#single-include) root.hcl ```hcl # If you have the following filesystem layout, and the following contents for ./child/terragrunt.hcl, this will include # and merge the configurations in the root.hcl file. # # . # ├── root.hcl # └── child # ├── main.tf # └── terragrunt.hcl remote_state { backend = "s3" config = { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } ``` child/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") expose = true } inputs = { remote_state_config = include.root.remote_state } ``` child/main.tf ```hcl terraform { backend "s3" {} } ``` ### Multiple includes [Section titled “Multiple includes”](#multiple-includes) root.hcl ```hcl # If you have the following filesystem layout, and the following contents for ./child/terragrunt.hcl, this will include # and merge the configurations in the root.hcl, while only loading the data in the region.hcl # configuration. # # . # ├── root.hcl # ├── region.hcl # └── child # └── terragrunt.hcl remote_state { backend = "s3" config = { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" } } ``` region.hcl ```hcl locals { region = "production" } ``` child/terragrunt.hcl ```hcl include "remote_state" { path = find_in_parent_folders("root.hcl") expose = true } include "region" { path = find_in_parent_folders("region.hcl") expose = true merge_strategy = "no_merge" } inputs = { remote_state_config = include.remote_state.remote_state region = include.region.locals.region } ``` child/main.tf ```hcl terraform { backend "s3" {} } ``` ### Limitations on accessing exposed config [Section titled “Limitations on accessing exposed config”](#limitations-on-accessing-exposed-config) In general, you can access all attributes on `include` when they are exposed (e.g., `include.locals`, `include.inputs`, etc.). However, to support `run --all`, Terragrunt is unable to expose all attributes when the included config has a `dependency` block. To understand this, consider the following example: root.hcl ```hcl dependency "vpc" { config_path = "${get_terragrunt_dir()}/../vpc" } inputs = { vpc_name = dependency.vpc.outputs.name } ``` terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") expose = true } dependency "alb" { config_path = ( include.root.inputs.vpc_name == "mgmt" ? "../alb-public" : "../alb-private" ) } inputs = { alb_id = dependency.alb.outputs.id } ``` In the child `terragrunt.hcl`, the `dependency` path for the `alb` depends on whether the VPC is the `mgmt` VPC or not, which is determined by the `dependency.vpc` in the root config. This means that the output from `dependency.vpc` must be available to parse the `dependency.alb` config. This causes problems when performing a `run --all apply` operation. During a `run --all` operation, Terragrunt first parses all the `dependency` blocks to build a dependency tree of the Terragrunt modules to figure out the order of operations. If all the paths are static references, then Terragrunt can determine all the dependency paths before any module has been applied. In this case there is no problem even if other config blocks access `dependency`, as by the time Terragrunt needs to parse those blocks, the upstream dependencies would have been applied during the `run --all apply`. However, if those `dependency` blocks depend on upstream dependencies, then there is a problem as Terragrunt would not be able to build the dependency tree without the upstream dependencies being applied. Therefore, to ensure that Terragrunt can build the dependency tree in a `run --all` operation, Terragrunt enforces the following limitation to exposed `include` config: If the included configuration has any `dependency` blocks, only `locals` and `include` are exposed and available to the child `include` and `dependency` blocks. There are no restrictions for other blocks in the child config (e.g., you can reference `inputs` from the included config in child `inputs`). Otherwise, if the included config has no `dependency` blocks, there is no restriction on which exposed attributes you can access. For example, the following alternative configuration is valid even if the alb dependency is still accessing the `inputs` attribute from the included config: root.hcl ```hcl inputs = { vpc_name = "mgmt" } ``` terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") expose = true } dependency "vpc" { config_path = "../vpc" } dependency "alb" { config_path = ( include.root.inputs.vpc_name == "mgmt" ? "../alb-public" : "../alb-private" ) } inputs = { vpc_name = dependency.vpc.outputs.name alb_id = dependency.alb.outputs.id } ``` **What is deep merge?** When the `merge_strategy` for the `include` block is set to `deep`, Terragrunt will perform a deep merge of the included config. For Terragrunt config, deep merge is defined as follows: * For simple types, the child overrides the parent. * For lists, the two attribute lists are combined together in concatenation. * For maps, the two maps are combined together recursively. That is, if the map keys overlap, then a deep merge is performed on the map value. * For blocks, if the label is the same, the two blocks are combined together recursively. Otherwise, the blocks are appended like a list. This is similar to maps, with block labels treated as keys. However, due to internal implementation details, some blocks are not deep mergeable. This will change in the future, but for now, terragrunt performs a shallow merge (that is, block definitions in the child completely override the parent definition). The following blocks have this limitation: - `remote_state` - `generate` Similarly, the `locals` block is deliberately omitted from the merge operation by design. That is, you will not be able to access parent config `locals` in the child config, and vice versa in a merge. However, you can access the parent locals in child config if you use the `expose` feature. Finally, `dependency` blocks have special treatment. When doing a `deep` merge, `dependency` blocks from **both** child and parent config are accessible in **both** places. For example, consider the following setup: root.hcl ```hcl dependency "vpc" { config_path = "../vpc" } inputs = { vpc_id = dependency.vpc.outputs.vpc_id db_id = dependency.mysql.outputs.db_id } ``` terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") merge_strategy = "deep" } dependency "mysql" { config_path = "../mysql" } inputs = { security_group_id = dependency.vpc.outputs.security_group_id } ``` In the example, note how the parent is accessing the outputs of the `mysql` dependency even though it is not defined in the parent. Similarly, the child is accessing the outputs of the `vpc` dependency even though it is not defined in the child. Full example: root.hcl ```hcl remote_state { backend = "s3" config = { encrypt = true bucket = "__FILL_IN_BUCKET_NAME__" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-west-2" } } dependency "vpc" { # This will get overridden by child terragrunt.hcl configs config_path = "" mock_outputs = { attribute = "hello" old_attribute = "old val" list_attr = ["hello"] map_attr = { foo = "bar" } } mock_outputs_allowed_terraform_commands = ["apply", "plan", "destroy", "output"] } inputs = { attribute = "hello" old_attribute = "old val" list_attr = ["hello"] map_attr = { foo = "bar" test = dependency.vpc.outputs.new_attribute } } ``` terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") merge_strategy = "deep" } remote_state { backend = "local" } dependency "vpc" { config_path = "../vpc" mock_outputs = { attribute = "mock" new_attribute = "new val" list_attr = ["mock"] map_attr = { bar = "baz" } } } inputs = { attribute = "mock" new_attribute = "new val" list_attr = ["mock"] map_attr = { bar = "baz" } dep_out = dependency.vpc.outputs } ``` ```hcl # Merged terragrunt.hcl # Child override parent completely due to deep merge limitation remote_state { backend = "local" } # mock_outputs are merged together with deep merge dependency "vpc" { config_path = "../vpc" # Child overrides parent mock_outputs = { attribute = "mock" # Child overrides parent old_attribute = "old val" # From parent new_attribute = "new val" # From child list_attr = [ "hello", # From parent "mock", # From child ] map_attr = { foo = "bar" # From parent bar = "baz" # From child } } # From parent mock_outputs_allowed_terraform_commands = ["apply", "plan", "destroy", "output"] } # inputs are merged together with deep merge inputs = { attribute = "mock" # Child overrides parent old_attribute = "old val" # From parent new_attribute = "new val" # From child list_attr = [ "hello", # From parent "mock", # From child ] map_attr = { foo = "bar" # From parent bar = "baz" # From child test = dependency.vpc.outputs.new_attribute # From parent, referencing dependency mock output from child } dep_out = dependency.vpc.outputs # From child } ``` ## locals [Section titled “locals”](#locals) The `locals` block is used to define aliases for Terragrunt expressions that can be referenced elsewhere in configuration. The `locals` block does not have a defined set of arguments that are supported. Instead, all the arguments passed into `locals` are available under the reference `local.` throughout the file where the `locals` block is defined. Example: terragrunt.hcl ```hcl # Make the AWS region a reusable variable within the configuration locals { aws_region = "us-east-1" } inputs = { region = local.aws_region name = "${local.aws_region}-bucket" } ``` ### Complex locals [Section titled “Complex locals”](#complex-locals) Some `local` variables can be complex types, such as `list` or `map`. For example: terragrunt.hcl ```hcl locals { # Define a list of regions regions = ["us-east-1", "us-west-2", "eu-west-1"] # Define a map of regions to their corresponding bucket names region_to_bucket_name = { us-east-1 = "east-bucket" us-west-2 = "west-bucket" eu-west-1 = "eu-bucket" } # The first region is accessed like this first_region = local.regions[0] # The bucket name for us-east-1 is accessed like this us_east_1_bucket = local.region_to_bucket_name["us-east-1"] } ``` These complex types can also arise when using values derived from reading other files. For example: region.hcl ```hcl locals { region = "us-east-1" } ``` unit/terragrunt.hcl ```hcl locals { # Load the data from region.hcl region_hcl = read_terragrunt_config(find_in_parent_folders("region.hcl")) # Access the region from the loaded file region = local.region_hcl.locals.region } inputs = { bucket_name = "${local.region}-bucket" } ``` Similarly, you might want to define this shared data using other serialization formats, like JSON or YAML: region.yml ```yaml region: us-east-1 ``` unit/terragrunt.hcl ```hcl locals { # Load the data from region.json region_yml = yamldecode(file(find_in_parent_folders("region.yml"))) # Access the region from the loaded file region = local.region_yml.region } inputs = { bucket_name = "${local.region}-bucket" } ``` ### Computed locals [Section titled “Computed locals”](#computed-locals) When reading Terragrunt HCL configurations, you might read in a computed configuration: computed.hcl ```hcl locals { computed_value = run_cmd("--terragrunt-quiet", "python3", "-c", "print('Hello,')") } ``` unit/terragrunt.hcl ```hcl locals { # Load the data from computed.hcl computed = read_terragrunt_config(find_in_parent_folders("computed.hcl")) # Access the computed value from the loaded file computed_value = "${local.computed.locals.computed_value} world!" # <-- This will be "Hello, world!" } ``` Note that this can be a powerful feature, but it can easily lead to performance issues if you are not careful, as each read will require a full parse of the HCL file and potentially execute expensive computation. Use this feature judiciously. ## dependency [Section titled “dependency”](#dependency) The `dependency` block is used to configure module dependencies. Each dependency block exports the outputs of the target module as block attributes you can reference throughout the configuration. You can learn more about `dependency` blocks in the [Dependencies between modules section](/features/stacks/stack-operations#dependencies-between-units) of the “Execute Opentofu/Terraform commands on multiple modules at once” use case overview. You can define more than one `dependency` block. Each label you provide to the block identifies another `dependency` that you can reference in your config. The `dependency` block supports the following arguments: * `name` (label): You can define multiple `dependency` blocks in a single terragrunt config. As such, each block needs a name to differentiate between the other blocks, which is what the first label of the block is used for. You can reference the specific dependency output by the name. E.g if you had a block `dependency "vpc"`, you can reference the outputs and inputs of this dependency with the expressions `dependency.vpc.outputs` and `dependency.vpc.inputs`. * `config_path` (attribute): Path to a Terragrunt module (folder with a `terragrunt.hcl` file) that should be included as a dependency in this configuration. * `enabled` (attribute): When `false`, excludes the dependency from execution. Defaults to `true`. * `skip_outputs` (attribute): When `true`, skip calling `terragrunt output` when processing this dependency. If `mock_outputs` is configured, set `outputs` to the value of `mock_outputs`. Otherwise, `outputs` will be set to an empty map. Put another way, setting `skip_outputs` means “use mocks all the time if `mock_outputs` are set.” * `mock_outputs` (attribute): A map of arbitrary key value pairs to use as the `outputs` attribute when no outputs are available from the target module, or if `skip_outputs` is `true`. However, it’s generally recommended not to set `skip_outputs` if using `mock_outputs`, because `skip_outputs` means “use mocks all the time if they are set” whereas `mock_outputs` means “use mocks only if real outputs are not available.” Use `locals` instead when `skip_outputs = true`. * `mock_outputs_allowed_terraform_commands` (attribute): A list of Terraform commands for which `mock_outputs` are allowed. If a command is used where `mock_outputs` is not allowed, and no outputs are available in the target module, Terragrunt will throw an error when processing this dependency. * `mock_outputs_merge_with_state` (attribute): DEPRECATED. Use `mock_outputs_merge_strategy_with_state`. When `true`, `mock_outputs` and the state outputs will be merged. That is, the `mock_outputs` will be treated as defaults and the real state outputs will overwrite them if the keys clash. * `mock_outputs_merge_strategy_with_state` (attribute): Specifies how any existing state should be merged into the mocks. Valid values are * `no_merge` (default) - any existing state will be used as is. If the dependency does not have an existing state (it hasn’t been applied yet), then the mocks will be used * `shallow` - the existing state will be shallow merged into the mocks. Mocks will only be used where the output does not already exist in the dependency’s state * `deep_map_only` - the existing state will be deeply merged into the mocks. If an output is a map, the mock key will be used where that key does not exist in the state. Lists will not be merged Example: terragrunt.hcl ```hcl # Run `terragrunt output` on the module at the relative path `../vpc` and expose them under the attribute # `dependency.vpc.outputs` dependency "vpc" { config_path = "../vpc" # Configure mock outputs for the `validate` command that are returned when there are no outputs available (e.g the # module hasn't been applied yet. mock_outputs_allowed_terraform_commands = ["validate"] mock_outputs = { vpc_id = "fake-vpc-id" } } # Another dependency, available under the attribute `dependency.rds.outputs` dependency "rds" { config_path = "../rds" } inputs = { vpc_id = dependency.vpc.outputs.vpc_id db_url = dependency.rds.outputs.db_url } ``` **IMPORTANT**: The `dependency..inputs` field has been deprecated and removed. You can only access dependency outputs via `dependency..outputs`. If you were previously using `dependency..inputs`, you should refactor your configuration to use `dependency..outputs` instead. **Can I speed up dependency fetching?** `dependency` blocks are fetched in parallel at each source level, but will serially parse each recursive dependency. For example, consider the following chain of dependencies: ```text account --> vpc --> securitygroup --> ecs ^ / ecr -- ``` In this chain, the `ecr` and `securitygroup` module outputs will be fetched concurrently when applying the `ecs` module, but the outputs for `account` and `vpc` will be fetched serially as terragrunt needs to recursively walk through the tree to retrieve the outputs at each level. This recursive parsing happens due to the necessity to parse the entire `terragrunt.hcl` configuration (including `dependency` blocks) in full before being able to call `tofu output`/`terraform output`. However, terragrunt includes an optimization to only fetch the lowest level outputs (`securitygroup` and `ecr` in this example) provided that the following conditions are met in the immediate dependencies: * The remote state is managed using `remote_state` blocks. * The dependency optimization feature flag is enabled (`disable_dependency_optimization = false`, which is the default). * The `remote_state` block itself does not depend on any `dependency` outputs (`locals` and `include` are ok). * You are not relying on `before_hook`, `after_hook`, or `extra_arguments` to the `tofu init`/`terraform init` call. NOTE: terragrunt will not automatically detect this and you will need to explicitly opt out of the dependency optimization flag. If these conditions are met, terragrunt will only parse out the `remote_state` blocks and use that to pull down the state for the target module without parsing the `dependency` blocks, avoiding the recursive dependency retrieval. ## dependencies [Section titled “dependencies”](#dependencies) The `dependencies` block is used to enumerate all the Terragrunt modules that need to be applied in order for this module to be able to apply. Note that this is purely for ordering the operations when using `run --all` commands of OpenTofu/Terraform. This does not expose or pull in the outputs like `dependency` blocks. The `dependencies` block supports the following arguments: * `paths` (attribute): A list of paths to modules that should be marked as a dependency. Example: terragrunt.hcl ```hcl # When applying this terragrunt config in an `run --all` command, make sure the modules at "../vpc" and "../rds" are # handled first. dependencies { paths = ["../vpc", "../rds"] } ``` ## generate [Section titled “generate”](#generate) The `generate` block can be used to arbitrarily generate a file in the terragrunt working directory (where `tofu`/`terraform` is called). This can be used to generate common OpenTofu/Terraform configurations that are shared across multiple OpenTofu/Terraform modules. For example, you can use `generate` to generate the provider blocks in a consistent fashion by defining a `generate` block in the parent terragrunt config. The `generate` block supports the following arguments: * `name` (label): You can define multiple `generate` blocks in a single terragrunt config. As such, each block needs a name to differentiate between the other blocks. * `path` (attribute): The path where the generated file should be written. If a relative path, it’ll be relative to the Terragrunt working dir (where the OpenTofu/Terraform code lives). * `if_exists` (attribute): What to do if a file already exists at `path`. Valid values are: * `overwrite` (overwrite the existing file) * `overwrite_terragrunt` (overwrite the existing file if it was generated by terragrunt; otherwise, error) * `skip` (skip code generation and leave the existing file as-is) * `error` (exit with an error) * `if_disabled` (attribute): What to do if a file already exists at `path` and `disable` is set to `true` (`skip` by default) Valid values are: * `remove` (remove the existing file) * `remove_terragrunt` (remove the existing file if it was generated by terragrunt; otherwise, error) * `skip` (skip removing and leave the existing file as-is). * `comment_prefix` (attribute): A prefix that can be used to indicate comments in the generated file. This is used by terragrunt to write out a signature for knowing which files were generated by terragrunt. Defaults to `#`. Optional. * `disable_signature` (attribute): When `true`, disables including a signature in the generated file. This means that there will be no difference between `overwrite_terragrunt` and `overwrite` for the `if_exists` setting. Defaults to `false`. Optional. - `contents` (attribute): The contents of the generated file. - `disable` (attribute): Disables this generate block. Example: terragrunt.hcl ```hcl # When using this terragrunt config, terragrunt will generate the file "provider.tf" with the aws provider block before # calling to OpenTofu/Terraform. Note that this will overwrite the `provider.tf` file if it already exists. generate "provider" { path = "provider.tf" if_exists = "overwrite" contents = <") } ``` Feature flags are used to conditionally control Terragrunt behavior at runtime, including the inclusion or exclusion of units. More on that in the [exclude](#exclude) block. ## exclude [Section titled “exclude”](#exclude) The `exclude` block in Terragrunt provides advanced configuration options to dynamically determine when and how specific units in the Terragrunt dependency graph are excluded. This feature allows for fine-grained control over which actions are executed and can conditionally exclude dependencies. Syntax: terragrunt.hcl ```hcl exclude { if = # Boolean to determine exclusion. no_run = # Boolean to prevent the unit from running (ignored for `--all` commands). actions = ["", ...] # List of actions to exclude (e.g., "plan", "apply", "all", "all_except_output"). exclude_dependencies = # Boolean to determine if dependencies should also be excluded. } ``` Attributes: | Attribute | Type | Description | | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `if` | boolean | Condition to dynamically determine whether the unit should be excluded. | | `actions` | list(string) | Specifies which actions to exclude when the condition is met. Options: `plan`, `apply`, `all`, `all_except_output` etc. | | `exclude_dependencies` | boolean | Indicates whether the dependencies of the excluded unit should also be excluded (default: `false`). | | `no_run` | boolean | When `true` and `if` is `true`, prevents the unit from running entirely for single unit commands (e.g., `terragrunt run plan`), but only when the current action matches the `actions` list. This attribute is ignored for `run --all` commands. | Examples: terragrunt.hcl ```hcl exclude { if = feature.feature_name.value # Dynamically exclude based on a feature flag. actions = ["plan", "apply"] # Exclude `plan` and `apply` actions. exclude_dependencies = false # Do not exclude dependencies. } ``` In this example, the unit is excluded for the `plan` and `apply` actions only when `feature.feature_name.value` evaluates to `true`. Dependencies are not excluded. terragrunt.hcl ```hcl exclude { if = feature.is_dev_environment.value # Exclude only for development environments. actions = ["all"] # Exclude all actions. exclude_dependencies = true # Exclude dependencies along with the unit. } ``` This configuration ensures the unit and its dependencies are excluded from all actions in the Terragrunt graph when the feature `is_dev_environment` evaluates to `true`. terragrunt.hcl ```hcl exclude { if = true # Explicitly exclude. actions = ["all_except_output"] # Allow `output` actions nonetheless. exclude_dependencies = false # Dependencies remain active. } ``` This setup is useful for scenarios where output evaluation is still needed, even if other actions like `plan` or `apply` are excluded. terragrunt.hcl ```hcl exclude { if = true no_run = true actions = ["plan"] } ``` This configuration prevents the unit from running when `if` is `true` AND the current action is “plan”. The `no_run` attribute only applies to single unit commands (e.g., `terragrunt run plan`) and is ignored for `run --all` commands. The exclusion only takes effect when the current action matches the `actions` list. Consider using this for units that are expensive to continuously update, and can be opted in when necessary. ## errors [Section titled “errors”](#errors) The `errors` block contains all the configurations for handling errors. It supports different nested configuration blocks like `retry` and `ignore` to define specific error-handling strategies. ### Retry Configuration [Section titled “Retry Configuration”](#retry-configuration) The `retry` block within the `errors` block defines rules for retrying operations when specific errors occur. This is useful for handling intermittent errors that may resolve after a short delay or multiple attempts. Example: Retry Configuration terragrunt.hcl ```hcl errors { retry "retry_example" { retryable_errors = [".*Error: transient.*"] # Matches errors containing 'Error: transient' max_attempts = 5 # Retry up to 5 times sleep_interval_sec = 10 # Wait 10 seconds between retries } } ``` Parameters: * `retryable_errors`: A list of regex patterns to match errors that are eligible to be retried. e.g. `".*Error: transient.*"` matches errors containing `Error: transient`. * `max_attempts`: The maximum number of retry attempts. e.g. `5` retries. * `sleep_interval_sec`: Time (in seconds) to wait between retries. e.g. `10` seconds. ### Ignore Configuration [Section titled “Ignore Configuration”](#ignore-configuration) The `ignore` block within the `errors` block defines rules for ignoring specific errors. This is useful when certain errors are known to be safe and should not prevent the run from proceeding. Example: Ignore Configuration terragrunt.hcl ```hcl errors { ignore "ignore_example" { ignorable_errors = [ ".*Error: safe-to-ignore.*", # Ignore errors containing 'Error: safe-to-ignore' "!.*Error: critical.*" # Do not ignore errors containing 'Error: critical' ] message = "Ignoring safe-to-ignore errors" # Optional message displayed when ignoring errors signals = { safe_to_revert = true # Indicates the operation is safe to revert on failure } } } ``` Parameters: * `ignorable_errors`: A list of regex patterns to define errors to ignore. * `"Error: safe-to-ignore.*"`: Ignores errors containing `Error: safe-to-ignore`. * `"!Error: critical.*"`: Ensures errors containing `Error: critical` are not ignored. * `message` (Optional): A warning message displayed when an error is ignored. * Example: `"Ignoring safe-to-ignore errors"`. * `signals` (Optional): Key-value pairs used to emit signals to external systems. * Example: `safe_to_revert = true` indicates it is safe to revert the operation if it fails. Populating values into the `signals` attribute results in a JSON file named `error-signals.json` being emitted on failure. This file can be inspected in CI/CD systems to determine the recommended course of action to address the failure. Example: If an error occurs and the author of the unit has signaled `safe_to_revert = true`, the CI/CD system could follow a standard process: * Identify all units with files named `error-signals.json`. * Checkout the previous commit for those units. * Apply the units in their previous state, effectively reverting their updates. This approach ensures consistent and automated error handling in complex pipelines. ### Combined Example [Section titled “Combined Example”](#combined-example) Below is a combined example showcasing both retry and ignore configurations within the `errors` block. terragrunt.hcl ```hcl errors { # Retry block for transient errors retry "transient_errors" { retryable_errors = [".*Error: transient network issue.*"] max_attempts = 3 sleep_interval_sec = 5 } # Ignore block for known safe-to-ignore errors ignore "known_safe_errors" { ignorable_errors = [ ".*Error: safe warning.*", "!.*Error: do not ignore.*" ] message = "Ignoring safe warning errors" signals = { alert_team = false } } } ``` Take note that: * All retry and ignore configurations must be defined within a single `errors` block. * Conditional logic can be used within `ignorable_errors` to enable or disable rules dynamically. Evaluation Order: * **Ignore Rules:** Errors are checked against the **ignore** rules first. If an error matches, it is ignored and will not trigger a retry. * **Retry Rules:** Once ignore rules are applied, the **retry** rules handle any remaining errors. > **Note:** Only the **first matching rule** is applied. If there are multiple conflicting rules, any matches after the first one are ignored. #### Errors during source fetching [Section titled “Errors during source fetching”](#errors-during-source-fetching) In addition to handling errors during OpenTofu/Terraform runs, the `errors` block will also handle errors that occur during source fetching. This can be particularly useful when fetching from artifact repositories that may be temporarily unavailable. Example: terragrunt.hcl ```hcl terraform { source = "https://unreliable-source.com/module.zip" } errors { retry "source_fetch" { retryable_errors = [".*Error: transient network issue.*"] max_attempts = 3 sleep_interval_sec = 5 } } ``` ## unit [Section titled “unit”](#unit) The `unit` block is used to define a deployment unit within a Terragrunt stack file (`terragrunt.stack.hcl`). Each unit represents a distinct infrastructure component that should be deployed as part of the stack. **Purpose**: Define a single, deployable piece of infrastructure. **Use case**: When you want to create a single piece of isolated infrastructure (e.g. a specific VPC, database, or application). **Result**: Generates a single `terragrunt.hcl` file in the specified path. The `unit` block supports the following arguments: * `name` (label): A unique identifier for the unit. This is used to reference the unit elsewhere in your configuration. * `source` (attribute): Specifies where to find the Terragrunt configuration files for this unit. This follows the same syntax as the `source` parameter in the `terraform` block. * `path` (attribute): The relative path where this unit should be deployed within the stack directory (`.terragrunt-stack`). Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this. * `values` (attribute, optional): A map of values that will be passed to the unit as inputs. * `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the unit **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations. * `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the unit’s configuration. When set to `true`, Terragrunt will skip validation checks for this unit. Example: terragrunt.stack.hcl ```hcl unit "vpc" { source = "git::git@github.com:acme/infrastructure-units.git//networking/vpc?ref=v0.0.1" path = "vpc" values = { vpc_name = "main" cidr = "10.0.0.0/16" } } ``` Note that each unit must have a unique name and path within the stack. When `values` are specified, generated units will have access to those values via a special `terragrunt.values.hcl` file generated next to the `terragrunt.hcl` file of the unit. * terragrunt.stack.hcl * .terragrunt-stack * vpc * **terragrunt.values.hcl** * terragrunt.hcl The `terragrunt.values.hcl` file will contain the values specified in the `values` block as top-level attributes: .terragrunt-stack/vpc/terragrunt.values.hcl ```hcl vpc_name = "main" cidr = "10.0.0.0/16" ``` The unit will be able to leverage those values via `values` variables. .terragrunt-stack/vpc/terragrunt.hcl ```hcl inputs = { vpc_name = values.vpc_name cidr = values.cidr } ``` Example usage of `no_dot_terragrunt_stack` attribute: terragrunt.stack.hcl ```hcl unit "vpc" { source = "git::git@github.com:acme/infrastructure-units.git//networking/vpc?ref=v0.0.1" path = "vpc" values = { vpc_name = "main" cidr = "10.0.0.0/16" } } unit "rds" { source = "git::git@github.com:acme/infrastructure-units.git//database/rds?ref=v0.0.1" path = "rds" values = { engine = "postgres" version = "13" } no_dot_terragrunt_stack = true } ``` With the above configuration, the resulting directory structure will be: * terragrunt.stack.hcl * .terragrunt-stack * vpc * terragrunt.values.hcl * terragrunt.hcl * rds * terragrunt.values.hcl * terragrunt.hcl The `vpc` unit is placed inside `.terragrunt-stack`, as expected. The `rds` unit is generated in the **same directory as `terragrunt.stack.hcl`**, rather than inside `.terragrunt-stack`, due to `no_dot_terragrunt_stack = true`. **Notes:** * The `source` value can be updated dynamically using the `--source-map` flag, just like `terraform.source`. * A pre-created `terragrunt.values.hcl` file can be provided in the unit source (sibling to the `terragrunt.hcl` file used as the source of the unit). If present, this file will be used as the default values for the unit. However, if the values attribute is defined in the unit block, the generated `terragrunt.values.hcl` will replace the pre-existing file. ### Comparison: unit vs stack blocks [Section titled “Comparison: unit vs stack blocks”](#comparison-unit-vs-stack-blocks) | Aspect | `unit` block | `stack` block | | -------------------- | ----------------------------------------------- | ------------------------------------------------------------------------ | | **Purpose** | Define a single infrastructure component | Define a reusable collection of components | | **When to use** | For specific, one-off infrastructure pieces | For patterns of infrastructure pieces that you want provisioned together | | **Generated output** | A directory with a single `terragrunt.hcl` file | A directory with a `terragrunt.stack.hcl` file | ## stack [Section titled “stack”](#stack) The `stack` block is used to define a stack of deployment units in a Terragrunt configuration file (`terragrunt.stack.hcl`). Stacks allow for nesting, enabling the organization of infrastructure components into modular, reusable groups, reducing redundancy and improving maintainability. **Purpose**: Define a collection of related units that can be reused. **Use case**: When you have a common, multi-unit pattern (like “dev environment” or “three-tier web application”) that you want to deploy multiple times. **Result**: Generates another `terragrunt.stack.hcl` file that can contain more units or stacks. Stacks are designed to be nestable, helping to mitigate the risk of stacks becoming too large or too repetitive. When a stack is generated, it can include nested stacks, ensuring that the configuration scales efficiently. The `stack` block supports the following arguments: * `name` (label): A unique identifier for the stack. This is used to reference the stack elsewhere in your configuration. * `source` (attribute): Specifies where to find the Terragrunt configuration files for this stack. This follows the same syntax as the `source` parameter in the `terraform` block. * `path` (attribute): The relative path within `.terragrunt-stack` where this stack should be generated.If an absolute path is provided here, Terragrunt will generate the stack in that location, instead of generating it in a path relative to the `.terragrunt-stack` directory. Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this. * `values` (attribute, optional): A map of custom values that can be passed to the stack. These values can be referenced within the stack’s configuration files, allowing for customization without modifying the stack source. * `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the stack **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations. * `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the stack’s configuration. When set to `true`, Terragrunt will skip validation checks for this stack. Example: terragrunt.stack.hcl ```hcl stack "services" { source = "github.com/gruntwork-io/terragrunt-stacks//stacks/mock/services?ref=v0.0.1" path = "services" values = { project = "dev-services" cidr = "10.0.0.0/16" } } ``` github.com/gruntwork-io/terragrunt-stacks//stacks/mock/services/terragrunt.stack.hcl ```hcl # ... unit "vpc" { # ... values = { cidr = values.cidr } } ``` In this example, the `services` stack is defined with path `services`, which will be generated at `.terragrunt-stack/services`. The stack is also provided with custom values for `project` and `cidr`, which can be used within the stack’s configuration files. Terragrunt will recursively generate a stack using the contents of the `.terragrunt-stack/services/terragrunt.stack.hcl` file until the entire stack is fully generated. **Notes:** * The `source` value can be updated dynamically using the `--source-map` flag, just like `terraform.source`. * A pre-created `terragrunt.values.hcl` file can be provided in the stack source (sibling to the `terragrunt.stack.hcl` file used as the source of the stack). If present, this file will be used as the default values for the stack. However, if the values attribute is defined in the stack block, the generated `terragrunt.values.hcl` will replace the pre-existing file. ## autoinclude [Section titled “autoinclude”](#autoinclude) Experimental The `autoinclude` block is part of the [`stack-dependencies` experiment](/reference/experiments/active#stack-dependencies). Enable it with `--experiment stack-dependencies`. The `autoinclude` block is used inside [`unit`](#unit) and [`stack`](#stack) blocks in Terragrunt stack files (`terragrunt.stack.hcl`) to inject configuration into the generated component during stack generation. Its main job is declaring `dependency` relationships between components of a stack without editing the unit sources in your catalog. **Purpose**: Inject dependencies and configuration overrides into a unit or stack at generation time. **Use case**: When units in a stack depend on each other (or on a sibling stack), or when a stack needs to patch a generated component with configuration that doesn’t belong in the catalog source. **Result**: Generates a `terragrunt.autoinclude.hcl` file next to the unit’s `terragrunt.hcl` (for `unit` blocks), or a `terragrunt.autoinclude.stack.hcl` file next to the nested stack’s `terragrunt.stack.hcl` (for `stack` blocks). The generated file is automatically merged into the component’s configuration whenever the component is parsed. The `autoinclude` block takes no label and has no arguments of its own. Its body is the configuration to inject. In an `autoinclude` on a `unit` block, the body can contain: * `dependency` blocks. The `config_path` attribute is resolved at generation time, typically from the `unit..path` and `stack..path` variables described below. All other attributes (`mock_outputs`, `mock_outputs_allowed_terraform_commands`, etc.) work as they do in a regular [`dependency`](#dependency) block. * The `inputs` attribute. * Any other block that is valid in a unit configuration: `feature`, `errors`, `generate`, `remote_state`, etc. Three things are rejected with an error: * A `locals` block fails generation. Declare locals at the stack level in `terragrunt.stack.hcl` instead, where they resolve uniformly at generate time. * The `values` attribute fails when the generated file is parsed, because the file is read as regular unit configuration and `values` is not a valid argument there. Pass per-unit values through the `values` attribute of the `unit` block instead. * A nested `autoinclude` block fails generation. Example: terragrunt.stack.hcl ```hcl unit "vpc" { source = "../catalog/units/vpc" path = "vpc" } unit "app" { source = "../catalog/units/app" path = "app" autoinclude { dependency "vpc" { config_path = unit.vpc.path mock_outputs = { vpc_id = "vpc-mock" } } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } } } ``` Running `terragrunt stack generate --experiment stack-dependencies` produces: * terragrunt.stack.hcl * .terragrunt-stack * vpc * terragrunt.hcl * app * **terragrunt.autoinclude.hcl** * terragrunt.hcl .terragrunt-stack/app/terragrunt.autoinclude.hcl ```hcl # Generated by Terragrunt from autoinclude block. Do not edit manually. dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "vpc-mock" } } inputs = { vpc_id = dependency.vpc.outputs.vpc_id } ``` Expressions in the autoinclude body resolve at generation time, in the context of the stack file: `local.*`, `values.*`, `unit..path`, `stack..path`, and function calls all become literals in the generated file. Because functions evaluate against the stack file rather than the unit, directory functions report the stack file’s location (`get_terragrunt_dir()` returns the stack file’s directory, and `path_relative_to_include()` returns `"."`). Only `dependency.*` references are kept verbatim, because dependency outputs exist only when the unit runs. ### Component path variables [Section titled “Component path variables”](#component-path-variables) With the experiment enabled, `terragrunt.stack.hcl` files expose a `unit..path` and `stack..path` variable for each declared component. They resolve to the generated directory of that component, following the same layout `stack generate` produces (including the effect of `no_dot_terragrunt_stack` on the referenced block). In a generated dependency, `config_path` is written relative to the unit’s directory. These variables are available inside `autoinclude` blocks and in the `values` attribute of `unit` and `stack` blocks, so a parent stack can pass a component’s path down to a child stack. Only components declared in the same stack file are visible; referencing an undeclared name fails generation. ### Merge behavior [Section titled “Merge behavior”](#merge-behavior) A generated `terragrunt.autoinclude.hcl` merges into the unit’s configuration using the same default strategy as a regular [`include`](#include): a shallow merge, with the autoinclude winning. Top-level keys from the unit and the autoinclude combine; on a conflict, the autoinclude’s value replaces the unit’s rather than deep merging nested maps. `inputs` maps combine key by key at the top level. A `dependency` block with the same name as one in the unit’s own `terragrunt.hcl` replaces it wholesale. The merge applies to any unit with a sibling `terragrunt.autoinclude.hcl` while the experiment is enabled. `stack generate` normally writes this file; treat it as a generated artifact rather than something to edit by hand. ### Depending on a stack [Section titled “Depending on a stack”](#depending-on-a-stack) A dependency’s `config_path` can point at a stack directory via `stack..path`. The dependency’s outputs aggregate the outputs of every unit in that stack, keyed by unit name: terragrunt.stack.hcl ```hcl stack "network" { source = "../catalog/stacks/network" path = "network" } unit "app" { source = "../catalog/units/app" path = "app" autoinclude { dependency "network" { config_path = stack.network.path mock_outputs_allowed_terraform_commands = ["validate", "plan"] mock_outputs = { vpc = { vpc_id = "mock-vpc" } } } inputs = { vpc_id = dependency.network.outputs.vpc.vpc_id } } } ``` Here the `network` stack contains a unit named `vpc` with a `vpc_id` output, so the dependent unit reads it as `dependency.network.outputs.vpc.vpc_id`. During `run --all`, a dependency on a stack expands to a dependency on each of the stack’s units, so the dependent unit waits for all of them to finish. ### Stack-level autoinclude [Section titled “Stack-level autoinclude”](#stack-level-autoinclude) An `autoinclude` on a `stack` block generates `terragrunt.autoinclude.stack.hcl` next to the nested stack’s `terragrunt.stack.hcl`. Its body may declare only `unit` and `stack` blocks, which are injected into the generated stack. An injected block whose name matches an existing `unit` or `stack` overrides that block wholesale; a new name adds a component. terragrunt.stack.hcl ```hcl stack "networking" { source = "../catalog/stacks/networking" path = "networking" autoinclude { unit "extra" { source = "${get_repo_root()}/catalog/units/extra" path = "extra" } } } ``` A `dependency` block is rejected in a stack-level autoinclude, because stacks themselves have no dependencies. Declare the dependency inside the target unit’s own `autoinclude`. ### Limitations [Section titled “Limitations”](#limitations) * Stacks are opaque dependency boundaries. `unit..path` and `stack..path` only see components declared in the same stack file, so an autoinclude cannot target a unit inside another stack. Depend on the whole stack instead, or pass a component’s path down to the child stack through `values`. * An `autoinclude` block cannot contain another `autoinclude` block. A `unit` or `stack` block injected by a stack-level autoinclude may declare its own `autoinclude`; it resolves during the next generation pass. * Only HCL stack files support `autoinclude`. A stack file written as JSON fails generation if it declares one. # Functions > Learn about the built-in functions available in Terragrunt. Terragrunt allows you to use built-in functions anywhere in `terragrunt.hcl`, just like OpenTofu/Terraform! ## OpenTofu/Terraform built-in functions [Section titled “OpenTofu/Terraform built-in functions”](#opentofuterraform-built-in-functions) All [OpenTofu/Terraform built-in functions (as of v0.15.3)](https://opentofu.org/docs/language/functions/) are supported in Terragrunt config files: terragrunt.hcl ```hcl terraform { source = "../modules/${basename(get_terragrunt_dir())}" } remote_state { backend = "s3" config = { bucket = trimspace(" my-tofu-bucket ") region = join("-", ["us", "east", "1"]) key = format("%s/tofu.tfstate", path_relative_to_include()) } } ``` Note: Any `file*` functions (`file`, `fileexists`, `filebase64`, etc.) are relative to the directory containing the `terragrunt.hcl` file they’re used in. Given the following structure: * terragrunt * common.tfvars * assets * mysql * assets.txt * terragrunt.hcl Then `assets.txt` could be read with the following function call: ```hcl file("assets/mysql/assets.txt") ``` **Note:** Terragrunt was originally able to take advantage of built-in OpenTofu/Terraform built-in functions automatically, as they were exposed via an exported package. Since `v0.15.3`, however, these functions are now `internal` to the respective codebases. As a result, Terragrunt users typically use different functions to resolve the same problems. e.g. Terragrunt users can execute arbitrary shell commands with [run\_cmd](#run_cmd) in whatever language they like instead of using a bespoke HCL function to solve a given problem. In the future, OpenTofu may expose these functions via a public package, which would allow Terragrunt to access them directly. Until such a time, Terragrunt will continue to provide its own set of functions to solve problems relevant to Terragrunt users. If there is a specific function you would like to see supported directly in Terragrunt, please [open an issue](https://github.com/gruntwork-io/terragrunt/issues) requesting it. Make sure to include the use case you have in mind so we can understand the problem you are trying to solve, and why existing Terragrunt functions are not sufficient. ## find\_in\_parent\_folders [Section titled “find\_in\_parent\_folders”](#find_in_parent_folders) `find_in_parent_folders` searches up the directory tree from the current `terragrunt.hcl` file, and returns the absolute path to the first file in a parent folder with a given name, or exits with an error if no such file is found. This is primarily useful in an `include` block to automatically find the path to a parent Terragrunt configuration: some/folder/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } ``` The function can also be used to find parent folders. some/folder/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("some") } ``` You can also pass an optional second `fallback` parameter, which causes the function to return the fallback value (instead of exiting with an error) if the file in the `name` parameter cannot be found: some/folder/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("some-other-file-name.hcl", "fallback.hcl") } ``` Note that this function searches relative to the `terragrunt.hcl` file when called from a parent config. For example, if you had the following filesystem layout: * root.hcl * prod * env.hcl * mysql * terragrunt.hcl And the root `root.hcl` contained the following: root.hcl ```hcl locals { env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl")) } ``` The `find_in_parent_folders` will search from the **child `terragrunt.hcl`** (`prod/mysql/terragrunt.hcl`) config, finding the `env.hcl` file in the `prod` directory. **NOTE:** This function has undocumented behavior that has since been deprecated. To learn more about this, see the [Migrating from root `terragrunt.hcl`](/migrate/migrating-from-root-terragrunt-hcl) guide. ## path\_relative\_to\_include [Section titled “path\_relative\_to\_include”](#path_relative_to_include) `path_relative_to_include()` returns the relative path between the current `terragrunt.hcl` file and the `path` specified in its `include` block. For example, consider the following filesystem layout: * root.hcl * prod * mysql * terragrunt.hcl * stage * mysql * terragrunt.hcl Imagine `prod/mysql/terragrunt.hcl` and `stage/mysql/terragrunt.hcl` include all settings from the root `root.hcl` file: prod/mysql/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } ``` The root `root.hcl` can use the `path_relative_to_include()` in its `remote_state` configuration to ensure each child stores its remote state at a different `key`: root.hcl ```hcl remote_state { backend = "s3" config = { bucket = "my-tofu-bucket" region = "us-east-1" key = "${path_relative_to_include()}/tofu.tfstate" } } ``` The resulting `key` will be `prod/mysql/tofu.tfstate` for the prod `mysql` module and `stage/mysql/tofu.tfstate` for the stage `mysql` module. If you have `include` blocks, this function requires a `name` parameter when used in the child config to specify which `include` block to base the relative path on. Example: prod/mysql/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } include "region" { path = find_in_parent_folders("region.hcl") } terraform { source = "../modules/${path_relative_to_include("root")}" } ``` ## path\_relative\_from\_include [Section titled “path\_relative\_from\_include”](#path_relative_from_include) `path_relative_from_include()` returns the relative path between the `path` specified in its `include` block and the current `terragrunt.hcl` file (it is the counterpart of `path_relative_to_include()`). For example, consider the following filesystem layout: * sources * mysql * \*.tf * secrets * mysql * \*.tf * terragrunt * root.hcl * common.tfvars * mysql * terragrunt.hcl * secrets * mysql * terragrunt.hcl Imagine `terragrunt/mysql/terragrunt.hcl` and `terragrunt/secrets/mysql/terragrunt.hcl` include all settings from the root `root.hcl` file: terragrunt/mysql/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } ``` The root `root.hcl` can use the `path_relative_from_include()` in combination with `path_relative_to_include()` in its `source` configuration to retrieve the relative OpenTofu/Terraform source code from the terragrunt configuration file: root.hcl ```hcl terraform { source = "${path_relative_from_include()}/../sources//${path_relative_to_include()}" } ``` The resulting `source` will be `../../sources//mysql` for `mysql` module and `../../../sources//secrets/mysql` for `secrets/mysql` module. Another use case would be to add an extra argument to include the `common.tfvars` file for all subdirectories: root.hcl ```hcl terraform { extra_arguments "common_var" { commands = [ "apply", "plan", "import", "push", "refresh" ] arguments = [ "-var-file=${get_terragrunt_dir()}/${path_relative_from_include()}/common.tfvars", ] } } ``` This allows proper retrieval of the `common.tfvars` from whatever the level of subdirectories we have. If you have `include` blocks, this function requires a `name` parameter when used in the child config to specify which `include` block to base the relative path on. Example: prod/mysql/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } include "region" { path = find_in_parent_folders("region.hcl") } terraform { source = "../modules/${path_relative_from_include("root")}" } ``` ## get\_env [Section titled “get\_env”](#get_env) `get_env(NAME)` return the value of variable named `NAME` or throws exceptions if that variable is not set. Example: terragrunt.hcl ```hcl remote_state { backend = "s3" config = { bucket = get_env("BUCKET") } } ``` `get_env(NAME, DEFAULT)` returns the value of the environment variable named `NAME` or `DEFAULT` if that environment variable is not set. Example: terragrunt.hcl ```hcl remote_state { backend = "s3" config = { bucket = get_env("BUCKET", "my-tofu-bucket") } } ``` Note that [OpenTofu/Terraform will read environment variables](https://opentofu.org/docs/cli/config/environment-variables/#tf_var_name) that start with the prefix `TF_VAR_`, so one way to share a variable named `foo` between OpenTofu/Terraform and Terragrunt is to set its value as the environment variable `TF_VAR_foo` and to read that value in using this `get_env()` built-in function. ## get\_platform [Section titled “get\_platform”](#get_platform) `get_platform()` returns the current Operating System. Example: terragrunt.hcl ```hcl inputs = { platform = get_platform() } ``` This function can also be used in a comparison to evaluate what to do based on the current operating system. Example: outputs.tf ```hcl output "platform" { value = var.platform == "darwin" ? "(value for MacOS)" : "(value for other OS's)" } ``` Some of the returned values can be: * `darwin` * `freebsd` * `linux` * `windows` ## get\_repo\_root [Section titled “get\_repo\_root”](#get_repo_root) `get_repo_root()` returns the absolute path to the root of the Git repository: terragrunt.hcl ```hcl inputs { very_important_config = "${get_repo_root()}/config/strawberries.conf" } ``` This function will error if the file is not located in a Git repository. ## get\_path\_from\_repo\_root [Section titled “get\_path\_from\_repo\_root”](#get_path_from_repo_root) `get_path_from_repo_root()` returns the path from the root of the Git repository to the current directory: terragrunt.hcl ```hcl remote_state { backend = "s3" config = { bucket = "tofu" dynamodb_table = "tofu" encrypt = true key = "${get_path_from_repo_root()}/tofu.tfstate" session_name = "tofu" region = "us-east-1" } } ``` This function will error if the file is not located in a Git repository. ## get\_path\_to\_repo\_root [Section titled “get\_path\_to\_repo\_root”](#get_path_to_repo_root) `get_path_to_repo_root()` returns the relative path to the root of the Git repository: terragrunt.hcl ```hcl terraform { source = "${get_path_to_repo_root()}//modules/example" } ``` This function will error if the file is not located in a Git repository. ## get\_terragrunt\_dir [Section titled “get\_terragrunt\_dir”](#get_terragrunt_dir) `get_terragrunt_dir()` returns the directory where the Terragrunt configuration file (by default `terragrunt.hcl`) lives. This is useful when you need to use relative paths with [remote OpenTofu/Terraform configurations](/features/units/#remote-opentofuterraform-modules) and you want those paths relative to your Terragrunt configuration file and not relative to the temporary directory where Terragrunt downloads the code. For example, imagine you have the following file structure: * common.tfvars * frontend-app * terragrunt.hcl Inside `tofu-code/frontend-app/terragrunt.hcl` you might try to write code that looks like this: tofu-code/frontend-app/terragrunt.hcl ```hcl terraform { source = "git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3" extra_arguments "custom_vars" { commands = [ "apply", "plan", "import", "push", "refresh" ] arguments = [ "-var-file=../common.tfvars" # Note: This relative path will NOT work correctly! ] } } ``` Note how the `source` parameter is set, so Terragrunt will download the `frontend-app` code from the `modules` repo into a temporary folder and run `tofu`/`terraform` in that temporary folder. Note also that there is an `extra_arguments` block that is trying to allow the `frontend-app` to read some shared variables from a `common.tfvars` file. Unfortunately, the relative path (`../common.tfvars`) won’t work, as it will be relative to the temporary folder! Moreover, you can’t use an absolute path, or the code won’t work on any of your teammates’ computers. To make the relative path work, you need to use `get_terragrunt_dir()` to combine the path with the folder where the `terragrunt.hcl` file lives: tofu-code/frontend-app/terragrunt.hcl ```hcl terraform { source = "git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3" extra_arguments "custom_vars" { commands = [ "apply", "plan", "import", "push", "refresh" ] # With the get_terragrunt_dir() function, you can use relative paths! arguments = [ "-var-file=${get_terragrunt_dir()}/../common.tfvars" ] } } ``` ## get\_working\_dir [Section titled “get\_working\_dir”](#get_working_dir) `get_working_dir()` returns the absolute path where Terragrunt runs OpenTofu/Terraform commands. This is useful when you need to manage substitutions of vars inside a \*.tfvars file located right inside terragrunt’s tmp dir. ## get\_parent\_terragrunt\_dir [Section titled “get\_parent\_terragrunt\_dir”](#get_parent_terragrunt_dir) `get_parent_terragrunt_dir()` returns the absolute directory where the Terragrunt parent configuration file lives (regardless of what it’s called). This is useful when you need to use relative paths with [remote OpenTofu/Terraform configurations](/features/units/#remote-opentofuterraform-modules) and you want those paths relative to your parent Terragrunt configuration file and not relative to the temporary directory where Terragrunt downloads the code. This function is very similar to [get\_terragrunt\_dir()](#get_terragrunt_dir) except it returns the root instead of the leaf of your terragrunt configurations. * root.hcl * common.tfvars * app1 * terragrunt.hcl * tests * app2 * terragrunt.hcl * app3 * terragrunt.hcl root.hcl ```hcl terraform { extra_arguments "common_vars" { commands = [ "apply", "plan", "import", "push", "refresh" ] arguments = [ "-var-file=${get_parent_terragrunt_dir()}/common.tfvars" ] } } ``` The common.tfvars located in the root folder will be included by all applications, whatever their relative location to the root. If you have `include` blocks, this function requires a `name` parameter when used in the child config to specify which `include` block to base the parent dir on. Example: prod/mysql/terragrunt.hcl ```hcl include "root" { path = find_in_parent_folders("root.hcl") } include "region" { path = find_in_parent_folders("region.hcl") } terraform { source = "${get_parent_terragrunt_dir("root")}/modules/vpc" } ``` ## get\_original\_terragrunt\_dir [Section titled “get\_original\_terragrunt\_dir”](#get_original_terragrunt_dir) `get_original_terragrunt_dir()` returns the directory where the original Terragrunt configuration file (by default `terragrunt.hcl`) lives. This is primarily useful when one Terragrunt config is being read from another: e.g., if `/tofu-code/terragrunt.hcl` calls `read_terragrunt_config("/foo/bar.hcl")`, and within `bar.hcl`, you call `get_original_terragrunt_dir()`, you’ll get back `/tofu-code`. During stack generation, this function returns the directory of the `terragrunt.stack.hcl` currently being read, even when invoked from other Terragrunt configs via `read_terragrunt_config()`. ## get\_terraform\_commands\_that\_need\_vars [Section titled “get\_terraform\_commands\_that\_need\_vars”](#get_terraform_commands_that_need_vars) `get_terraform_commands_that_need_vars()` returns the list of OpenTofu/Terraform commands that accept `-var` and `-var-file` parameters. This function is used when defining [extra\_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks). terragrunt.hcl ```hcl terraform { extra_arguments "common_var" { commands = get_terraform_commands_that_need_vars() arguments = ["-var-file=${get_aws_account_id()}.tfvars"] } } ``` ## get\_terraform\_commands\_that\_need\_input [Section titled “get\_terraform\_commands\_that\_need\_input”](#get_terraform_commands_that_need_input) `get_terraform_commands_that_need_input()` returns the list of OpenTofu/Terraform commands that accept the `-input=(true or false)` parameter. This function is used when defining [extra\_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks). terragrunt.hcl ```hcl terraform { # Force OpenTofu/Terraform to not ask for input value if some variables are undefined. extra_arguments "disable_input" { commands = get_terraform_commands_that_need_input() arguments = ["-input=false"] } } ``` ## get\_terraform\_commands\_that\_need\_locking [Section titled “get\_terraform\_commands\_that\_need\_locking”](#get_terraform_commands_that_need_locking) `get_terraform_commands_that_need_locking()` returns the list of terraform commands that accept the `-lock-timeout` parameter. This function is used when defining [extra\_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks). terragrunt.hcl ```hcl terraform { # Force OpenTofu/Terraform to keep trying to acquire a lock for up to 20 minutes if someone else already has the lock extra_arguments "retry_lock" { commands = get_terraform_commands_that_need_locking() arguments = ["-lock-timeout=20m"] } } ``` ## get\_terraform\_commands\_that\_need\_parallelism [Section titled “get\_terraform\_commands\_that\_need\_parallelism”](#get_terraform_commands_that_need_parallelism) `get_terraform_commands_that_need_parallelism()` returns the list of terraform commands that accept the `-parallelism` parameter. This function is used when defining [extra\_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks). terragrunt.hcl ```hcl terraform { # Force OpenTofu/Terraform to run with reduced parallelism extra_arguments "parallelism" { commands = get_terraform_commands_that_need_parallelism() arguments = ["-parallelism=5"] } } ``` ## get\_aws\_account\_alias [Section titled “get\_aws\_account\_alias”](#get_aws_account_alias) `get_aws_account_alias()` returns the AWS account alias associated with the current set of credentials. If the alias cannot be found, it will return an empty string. Example: terragrunt.hcl ```hcl inputs = { account_alias = get_aws_account_alias() } ``` **Note:** value returned by `get_aws_account_alias()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute. ## get\_aws\_account\_id [Section titled “get\_aws\_account\_id”](#get_aws_account_id) `get_aws_account_id()` returns the AWS account id associated with the current set of credentials. Example: terragrunt.hcl ```hcl remote_state { backend = "s3" config = { bucket = "mycompany-${get_aws_account_id()}" } } ``` **Note:** value returned by `get_aws_account_id()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute. ## get\_aws\_caller\_identity\_arn [Section titled “get\_aws\_caller\_identity\_arn”](#get_aws_caller_identity_arn) `get_aws_caller_identity_arn()` returns the ARN of the AWS identity associated with the current set of credentials. Example: terragrunt.hcl ```hcl inputs = { caller_arn = get_aws_caller_identity_arn() } ``` **Note:** value returned by `get_aws_caller_identity_arn()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute. ## get\_terraform\_command [Section titled “get\_terraform\_command”](#get_terraform_command) `get_terraform_command()` returns the current terraform command in execution. Example: terragrunt.hcl ```hcl inputs = { current_command = get_terraform_command() } ``` ## get\_terraform\_cli\_args [Section titled “get\_terraform\_cli\_args”](#get_terraform_cli_args) `get_terraform_cli_args()` returns cli args for the current terraform command in execution. Example: terragrunt.hcl ```hcl inputs = { current_cli_args = get_terraform_cli_args() } ``` ## get\_default\_retryable\_errors [Section titled “get\_default\_retryable\_errors”](#get_default_retryable_errors) `get_default_retryable_errors()` returns the default list of retryable error patterns Terragrunt uses for transient failures. Use it within the `errors` block to seed a `retry` configuration. terragrunt.hcl ```hcl errors { retry "default_errors" { retryable_errors = get_default_retryable_errors() max_attempts = 3 sleep_interval_sec = 5 } retry "custom_errors" { retryable_errors = [".*my custom error.*"] max_attempts = 5 sleep_interval_sec = 10 } } ``` ## get\_aws\_caller\_identity\_user\_id [Section titled “get\_aws\_caller\_identity\_user\_id”](#get_aws_caller_identity_user_id) `get_aws_caller_identity_user_id()` returns the UserId of the AWS identity associated with the current set of credentials. Example: terragrunt.hcl ```hcl inputs = { caller_user_id = get_aws_caller_identity_user_id() } ``` This allows uniqueness of the storage bucket per AWS account (since bucket name must be globally unique). It is also possible to configure variables specifically based on the account used: terragrunt.hcl ```hcl terraform { extra_arguments "common_var" { commands = get_terraform_commands_that_need_vars() arguments = ["-var-file=${get_aws_account_id()}.tfvars"] } } ``` **Note:** value returned by `get_aws_caller_identity_user_id()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute. ## run\_cmd [Section titled “run\_cmd”](#run_cmd) `run_cmd(command, arg1, arg2…​)` runs a shell command and returns the stdout as the result of the interpolation. The command is executed at the same folder as the `terragrunt.hcl` file. This is useful whenever you want to dynamically fill in arbitrary information in your Terragrunt configuration. ### Basic Usage [Section titled “Basic Usage”](#basic-usage) As an example, you could write a script that determines the bucket and DynamoDB table name based on the AWS account, instead of hardcoding the name of every account: terragrunt.hcl ```hcl remote_state { backend = "s3" config = { bucket = run_cmd("./get_names.sh", "bucket") dynamodb_table = run_cmd("./get_names.sh", "dynamodb") } } ``` ### Special Parameters [Section titled “Special Parameters”](#special-parameters) The `run_cmd` function accepts some special flags that can alter how the function evaluates commands on your behalf. Placing these `--terragrunt-` prefixed flags as the first argument(s) of a `run_cmd` call will result in the behavior of `run_cmd` being adjusted. You can mix and match the flags in any order, so long as they precede the command you are running with `run_cmd`. | Parameter | Description | Example | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `--terragrunt-quiet` | Redacts `run_cmd` stdout from Terragrunt logs while still returning the value to HCL. This keeps sensitive information out of log files. | `run_cmd("--terragrunt-quiet", "./decrypt_secret.sh", "foo")` | | `--terragrunt-global-cache` | Stores and reuses results in a global cache so the command only runs once per set of arguments, no matter which configuration references it. Useful when the output is directory-independent. | `run_cmd("--terragrunt-global-cache", "aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text")` | | `--terragrunt-no-cache` | Skips the cache entirely and forces the command to run on every evaluation. Use this when the output changes frequently (timestamps, tokens, random IDs, etc.). | `run_cmd("--terragrunt-no-cache", "date", "+%s")` | Terragrunt caches `run_cmd` results by default to avoid running the same command multiple times during parsing. The cache key includes the directory of the `terragrunt.hcl` file and the command arguments unless you opt into global caching or disable caching entirely. Parameters `--terragrunt-global-cache` and `--terragrunt-no-cache` are mutually exclusive, Terragrunt will return an error if both are provided. #### Examples [Section titled “Examples”](#examples) **Suppress output for sensitive values:** ```hcl # Output is redacted in logs, but still available to Terragrunt super_secret_value = run_cmd("--terragrunt-quiet", "./decrypt_secret.sh", "foo") ``` **Note:** This will prevent terragrunt from displaying the output from the command in its output. However, the value could still be displayed in the OpenTofu/Terraform output if OpenTofu/Terraform does not treat it as a [sensitive value](https://www.terraform.io/docs/configuration/outputs.html#sensitive-suppressing-values-in-cli-output). **Use global cache for directory-independent commands:** ```hcl # Same result regardless of which directory run_cmd is called from account_id = run_cmd("--terragrunt-global-cache", "aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text") ``` **Disable caching for dynamic values:** ```hcl # Generates a new UUID every time run_cmd is evaluated build_id = run_cmd("--terragrunt-no-cache", "uuidgen") # Gets current timestamp on each parse of the Terragrunt configuration timestamp = run_cmd("--terragrunt-no-cache", "date", "+%s") ``` **Combine multiple parameters:** ````hcl # Disable cache AND suppress output for a sensitive dynamic value session_token = run_cmd("--terragrunt-no-cache", "--terragrunt-quiet", "./generate-temp-token.sh") ### Caching Behavior By default, invocations of `run_cmd` are cached based on the current directory and executed command, so cached values are reused later rather than executed multiple times. Here's an example: ```hcl # terragrunt.hcl locals { uuid = run_cmd("echo", "uuid1", uuid()) uuid2 = run_cmd("echo", "uuid2", uuid()) uuid3 = run_cmd("echo", "uuid3", uuid()) potato = run_cmd("echo", "potato") potato2 = run_cmd("echo", "potato") carrot = run_cmd("echo", "carrot") } inputs = { potato3 = run_cmd("echo", "potato") uuid3 = run_cmd("echo", "uuid3", uuid()) uuid4 = run_cmd("echo", "uuid4", uuid()) carrot2 = run_cmd("echo", "carrot") } ```` Output: ```bash $ terragrunt init uuid1 b48379e1-924d-2403-8789-c72d50be964c uuid1 9f3a8398-b11f-5314-7783-dad176ee487d uuid1 649ac501-e5db-c935-1499-c59fb7a75625 uuid2 2d65972b-3fa9-181f-64fe-dcd574d944d0 uuid3 e345de60-9cfa-0455-79b7-af0d053a15a5 potato uuid3 7f90a4ed-96e3-1dd8-5fee-91b8c8e07650 uuid2 8638fe79-c589-bebd-2a2a-3e6b96f7fc34 uuid3 310d0447-f0a6-3f67-efda-e6b1521fa1fb uuid4 f8e80cc6-1892-8db7-bd63-6089fef00c01 uuid2 289ff371-8021-54c6-2254-72de9d11392a uuid3 baa19863-1d99-e0ef-11f2-ede830d1c58a carrot ``` **Key observations from the output:** * `carrot` and `potato` appear once because subsequent invocations used cached values * `uuid1`, `uuid2`, and `uuid3` appear multiple times because each call to `uuid()` generates a different cache key * `uuid3` appears one extra time because it’s declared in both `locals` and `inputs` * `uuid4` appears once since it’s declared in `inputs`, which is evaluated once This caching behavior can be modified using the special parameters described in the [Special Parameters](#special-parameters) section above. ## deep\_merge [Section titled “deep\_merge”](#deep_merge) `deep_merge(map1, map2, ...)` deeply merges maps/objects. For overlapping keys, values from later arguments override earlier ones. Nested maps are merged recursively, and lists are appended. This function takes positional arguments. `deep_merge` requires the [`deep-merge`](/reference/experiments/active#deep-merge) experiment. Calling it without the experiment enabled returns an error. Any `null` arguments are ignored. Basic example: ```hcl locals { config = deep_merge( { service = { retries = 1, mode = "safe" } }, { service = { retries = 3 }, tags = { env = "dev" } }, ) } ``` Example with multiple JSON files (`...` expands a list into positional arguments): ```hcl locals { config_json_files = sort(fileset(get_terragrunt_dir(), "*.json")) config = deep_merge([ for file in local.config_json_files : jsondecode(file("${get_terragrunt_dir()}/${file}")) ]...) } inputs = local.config ``` ## read\_terragrunt\_config [Section titled “read\_terragrunt\_config”](#read_terragrunt_config) `read_terragrunt_config(config_path, [default_val])` parses the terragrunt config at the given path and serializes the result into a map that can be used to reference the values of the parsed config. This function will expose all blocks and attributes of a terragrunt config. For example, suppose you had a config file called `common.hcl` that contains common input variables: common.hcl ```hcl inputs = { stack_name = "staging" account_id = "1234567890" } ``` You can read these inputs in another config by using `read_terragrunt_config`, and merge them into the inputs: terragrunt.hcl ```hcl locals { common_vars = read_terragrunt_config(find_in_parent_folders("common.hcl")) } inputs = merge( local.common_vars.inputs, { # additional inputs } ) ``` This function also takes in an optional second parameter which will be returned if the file does not exist: terragrunt.hcl ```hcl locals { common_vars = read_terragrunt_config(find_in_parent_folders("i-dont-exist.hcl", "i-dont-exist.hcl"), {inputs = {}}) } inputs = merge( local.common_vars.inputs, # This will be {} { # additional inputs } ) ``` Note that this function will also render `dependency` blocks. That is, the parsed config will make the outputs of the `dependency` blocks available. For example, suppose you had the following config in a file called `common_deps.hcl`: common\_deps.hcl ```hcl dependency "vpc" { config_path = "${get_terragrunt_dir()}/../vpc" } ``` You can access the outputs of the vpc dependency through the parsed outputs of `read_terragrunt_config`: terragrunt.hcl ```hcl locals { common_deps = read_terragrunt_config(find_in_parent_folders("common_deps.hcl")) } inputs = { vpc_id = local.common_deps.dependency.vpc.outputs.vpc_id } ``` Notes: * `read_terragrunt_config` can be also used to read `terragrunt.stack.hcl` and `terragrunt.values.hcl` files. ## sops\_decrypt\_file [Section titled “sops\_decrypt\_file”](#sops_decrypt_file) `sops_decrypt_file(file_path)` decrypts a yaml, json, ini, env or “raw text” file encrypted with `sops`. [sops](https://github.com/getsops/sops) is an editor of encrypted files that supports YAML, JSON, ENV, INI and BINARY formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, Hashicorp Vault and PGP. This allows static secrets to be stored encrypted within your Terragrunt repository. For example, suppose you have some static secrets required to bootstrap your infrastructure in `secrets.yaml`, you can decrypt and merge them into the inputs by using `sops_decrypt_file`: terragrunt.hcl ```hcl locals { secret_vars = yamldecode(sops_decrypt_file(find_in_parent_folders("secrets.yaml"))) } inputs = merge( local.secret_vars, { # additional inputs } ) ``` If you absolutely need to fallback to a default value you can make use of the OpenTofu/Terraform `try` function: terragrunt.hcl ```hcl locals { secret_vars = try(jsondecode(sops_decrypt_file(find_in_parent_folders("no-secrets-here.json"))), {}) } inputs = merge( local.secret_vars, # This will be {} { # additional inputs } ) ``` ## get\_terragrunt\_source\_cli\_flag [Section titled “get\_terragrunt\_source\_cli\_flag”](#get_terragrunt_source_cli_flag) `get_terragrunt_source_cli_flag()` returns the value passed in via the CLI `--source` or an environment variable `TG_SOURCE`. Note that this will return an empty string when either of those values are not provided. This is useful for constructing before and after hooks, or TF flags that only apply to local development (e.g., setting up debug flags, or adjusting the `iam_role` parameter). Some example use cases are: * Setting debug logging when doing local development. * Adjusting the kubernetes provider configuration so that it targets minikube instead of real clusters. * Providing special mocks pulled in from the local dev source (e.g., something like `mock_outputs = jsondecode(file("${get_terragrunt_source_cli_arg()}/dependency_mocks/vpc.json"))`). ## read\_tfvars\_file [Section titled “read\_tfvars\_file”](#read_tfvars_file) `read_tfvars_file(file_path)` reads a `.tfvars` or `.tfvars.json` file and returns a map of the variables defined in it. This is useful for reading variables from a `.tfvars` file, merging them into the inputs, or using them in a `locals` block: terragrunt.hcl ```hcl locals { inputs_from_tfvars = jsondecode(read_tfvars_file("common.tfvars")) } inputs = merge( local.inputs_from_tfvars, { # additional inputs } ) ``` Another example: terragrunt.hcl ```hcl locals { backend = jsondecode(read_tfvars_file("backend.tfvars")) } remote_state { backend = "s3" config = { bucket = "${get_env("TG_BUCKET_PREFIX", "tf-bucket")}-${get_aws_account_id()}" key = "${path_relative_to_include()}/terraform-${local.aws_region}.tfstate" region = local.backend.region } generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } } ``` ## mark\_as\_read [Section titled “mark\_as\_read”](#mark_as_read) `mark_as_read(file_path)` marks a file as read so that it can be picked up for inclusion by the [queue-include-units-reading](/reference/cli/commands/run#queue-include-units-reading) flag. This is useful for situations when you want to mark a file as read, but are not reading it using a native Terragrunt HCL function. For example: terragrunt.hcl ```hcl locals { filename = mark_as_read("/path/to/my/file-read-by-tofu.txt") many_files = [for f in fileset("./config", "*.yaml") : file(mark_as_read(abspath("${get_terragrunt_dir()}/config/${f}")))] } inputs = { filename = local.filename many_files = local.many_files } ``` By using `mark_as_read` on `file-read-by-tofu.txt`, you can ensure that the `terragrunt.hcl` file passing in the `file-read-by-tofu.txt` file as an input will be included in any `run --all` run where the flag `--queue-include-units-reading file-read-by-tofu.txt` is set. The same technique can be used to mark a file as read when a file is read using code in `run_cmd`. **NOTE**: Due to the way that Terragrunt enqueues files we require an absolute path for mark\_as\_read to avoid multiple inclusions. **NOTE**: Due to the way that Terragrunt parses configurations during a `run --all`, functions will only properly mark files as read if they are used in the `locals` block. Reading a file directly in the `inputs` block will not mark the file as read, as the `inputs` block is not evaluated until *after* the queue has been populated with units to run. ## mark\_glob\_as\_read [Section titled “mark\_glob\_as\_read”](#mark_glob_as_read) Caution `mark_glob_as_read` requires the [`mark-many-as-read`](/reference/experiments/active#mark-many-as-read) experiment. Calling it without the experiment enabled returns an error. Caution `mark_glob_as_read` writes to the same read tracker as [`mark_as_read`](#mark_as_read), so the same evaluation-order caveat applies: during a `run --all`, the queue is populated before `inputs` is evaluated, so a call placed in `inputs` (or any other later-evaluated block) will not influence queue construction. Call `mark_glob_as_read` from `locals` to ensure matched files are considered when the queue is built. `mark_glob_as_read(pattern)` expands the given glob and marks every matching file as read. It returns the list of absolute paths that matched, so it can be composed with other expressions. Pattern syntax follows [`gobwas/glob`](https://github.com/gobwas/glob), the same matcher used elsewhere in Terragrunt. `/` is the path separator, `*` matches within a single segment, `**` matches any sequence of characters including separators, `?` matches any single non-separator character, `[abc]` matches a character class, and `{a,b}` matches any of the listed alternatives. A backslash escapes the following metacharacter. Relative patterns are resolved against the current working directory, the same way `mark_as_read` resolves a relative file path. Caution Glob patterns must use forward slashes (`/`) as the path separator, even on Windows. The matcher treats `\` as an escape character, not a path separator, so a backslash-separated pattern will not match any files. Caution `**` only collapses the separators around it when the adjacent segments are literals. `config/**/*.yaml` matches files at least one directory below `config/`, but not files directly inside `config/`, because the trailing segment `*.yaml` is a wildcard. Use brace alternation to match at any depth: ```hcl mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}") ``` terragrunt.hcl ```hcl locals { configs = mark_glob_as_read("${get_terragrunt_dir()}/config/{*.yaml,**/*.yaml}") } inputs = { configs = [for f in local.configs : yamldecode(file(f))] } ``` A typical use case is a unit that consumes a directory of configuration files indirectly (for example, through `run_cmd`, `templatefile`, or a shared helper module) and wants changes to any of them to trigger the unit through [reading-based filter expressions](/features/filter/attributes#reading-based-expressions) such as `--filter 'reading=./config/**'`. If the pattern matches nothing, the function returns an empty list without error. ## constraint\_check [Section titled “constraint\_check”](#constraint_check) `constraint_check(version, constraint)` checks if a given version satisfies a given constraint. This particularly is useful for situations where you want to change the runtime behavior of Terragrunt based on the version of an OpenTofu/Terraform module. For example: ```hcl feature "module_version" { default = "1.2.3" } locals { module_version = feature.module_version.value needs_v2_adjustments = constraint_check(local.module_version, ">= 2.0.0") } terraform { source = "github.com/my-org/my-module.git//?ref=v${local.module_version}" } inputs = !local.needs_v2_adjustments ? { old_module_input_name = "old_module_input_value" } : { new_module_input_name = "new_module_input_value" } ``` In this example, the `v2.0.0` version of the module made a breaking change to rename an input variable from `old_module_input_name` to `new_module_input_name`. Instead of carefully coordinating the version update with the corresponding input change, users can set a feature flag to control opt-in of the new module version, and have Terragrunt dynamically adjust the input variable name based on the constraint check, that the module version is greater than or equal to `2.0.0`. The HCL function supports all the same constraints that you can use for version constraints in [terragrunt\_version\_constraint](/reference/hcl/attributes/#terragrunt_version_constraint) and [terraform\_version\_constraint](/reference/hcl/attributes/#terraform_version_constraint). # Lock File Handling > Learn how Terragrunt handles OpenTofu/Terraform lock files ## How to use lock files with Terragrunt [Section titled “How to use lock files with Terragrunt”](#how-to-use-lock-files-with-terragrunt) To use [OpenTofu/Terraform lock files](https://opentofu.org/docs/language/files/dependency-lock/) with Terragrunt, you need to: 1. Run Terragrunt as usual (e.g., run `terragrunt plan`, `terragrunt apply`, etc.). 2. Check the `.terraform.lock.hcl` file into version control. Everything else with OpenTofu/Terraform and Terragrunt should work as expected. To learn the details of how this works, read on! ## How Terragrunt handles lock files [Section titled “How Terragrunt handles lock files”](#how-terragrunt-handles-lock-files) ### What’s a lock file? [Section titled “What’s a lock file?”](#whats-a-lock-file) [Terraform 0.14 added support for a *lock file*](https://www.hashicorp.com/blog/terraform-0-14-introduces-a-dependency-lock-file-for-providers) which gets created or updated every time you run `tofu init`/`terraform init`. The file is typically generated into your working directory (i.e., the folder in which you ran `tofu init`/`terraform init`) and is called `.terraform.lock.hcl`. It captures the versions of all the OpenTofu/Terraform providers you’re using. Normally, you want to check this file into version control so that when your team members run OpenTofu/Terraform, they get the identical provider versions. ### The problem with mixing remote OpenTofu/Terraform configurations in Terragrunt and lock files [Section titled “The problem with mixing remote OpenTofu/Terraform configurations in Terragrunt and lock files”](#the-problem-with-mixing-remote-opentofuterraform-configurations-in-terragrunt-and-lock-files) Let’s say you are using Terragrunt with [remote OpenTofu/Terraform configurations](/features/units/) and you have the following folder structure: * live * prod * vpc * terragrunt.hcl * stage * vpc * terragrunt.hcl Imagine that in `live/stage/vpc/terragrunt.hcl`, you have the following contents: live/stage/vpc/terragrunt.hcl ```hcl terraform { source = "git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1" } ``` If you ran `terragrunt apply` in the `/live/stage/vpc` folder, Terragrunt will: 1. `git clone` the VPC module in the `source` URL into a temp folder in `.terragrunt-cache/xxx/vpc`, where `xxx` is dynamically determined based on the URL. 2. Run `tofu apply`/`terraform apply` in the `.terragrunt-cache/xxx/vpc` temp folder. As a result, the `.terraform.lock.hcl` file will be generated in the `.terragrunt-cache/xxx/vpc` temp folder, rather than in `/live/stage/vpc`. ### How Terragrunt solves this problem [Section titled “How Terragrunt solves this problem”](#how-terragrunt-solves-this-problem) To solve this problem, since version v0.27.0, Terragrunt implements the following logic for lock files: 1. If Terragrunt finds a `.terraform.lock.hcl` file in your working directory (e.g., in `/live/stage/vpc`), before running OpenTofu/Terraform, Terragrunt will copy that lock file into the temp folder it uses when running your OpenTofu/Terraform code (e.g., `.terragrunt-cache/xxx/vpc`). This way, if you had a lock file checked into version control, Terragrunt will respect and use it with your OpenTofu/Terraform code as you’d expect. 2. After running OpenTofu/Terraform, if Terragrunt finds a `.terraform.lock.hcl` in the temp folder (e.g., `.terragrunt-cache/xxx/vpc`), it will copy that lock file back to your working directory (e.g., to `/live/stage/vpc`). That way, you can commit the lock file (or the changes to the lock file) to version control as usual. ### Check the lock file in! [Section titled “Check the lock file in!”](#check-the-lock-file-in) After running Terragrunt on each of your modules, you should check your lock files in! That means your folder structure should end up looking something like this: * live * prod * vpc * .terraform.lock.hcl * terragrunt.hcl * stage * vpc * .terraform.lock.hcl * terragrunt.hcl Also, any time you change the providers you’re using, and re-run `init`, the lock file will be updated, so make sure to check the updates into version control too. ### Disabling the copy of the generated lock file [Section titled “Disabling the copy of the generated lock file”](#disabling-the-copy-of-the-generated-lock-file) In certain use cases, like when using a remote module containing a lock file within it, you probably don’t want Terragrunt to also copy the lock file into your working directory. In these scenarios, you can opt out of copying the `.terraform.lock.hcl` file by using `copy_terraform_lock_file = false` in the `terraform` configuration block as follows: terragrunt.hcl ```hcl terraform { ... copy_terraform_lock_file = false } ``` # Overview > Learn how Terragrunt decides what to log, and when. Terragrunt logs messages as it runs to help you understand what it’s doing. Given that Terragrunt is an IaC orchestrator, this can result in messages that are surprising if you don’t understand what Terragrunt is doing behind the scenes. ## Log Levels [Section titled “Log Levels”](#log-levels) To start with, Terragrunt has the following log levels: * `STDERR` * `STDOUT` * `ERROR` * `WARN` * `INFO` * `DEBUG` * `TRACE` The `STDOUT` and `STDERR` log levels are non-standard, and exist due to Terragrunt’s special responsibility as an IaC orchestrator. For the most part, whenever you use Terragrunt to run something using another tool (like OpenTofu or Terraform), Terragrunt will capture the stdout and stderr terminal output from that tool, enrich it with additional information, then *log* it as `STDOUT` or `STDERR` respectively. The exception to this is when Terragrunt is running a process in “Headless Mode”, where it will instead emit stdout and stderr terminal output to `INFO` and `ERROR` log levels respectively. All other log levels are standard, and are used by Terragrunt to log its own messages. For example: ```bash $ terragrunt --log-level debug plan 14:20:38.431 DEBUG Terragrunt Version: 0.0.0 14:20:38.431 DEBUG Did not find any locals block: skipping evaluation. 14:20:38.431 DEBUG Running command: tofu --version 14:20:38.431 DEBUG Engine is not enabled, running command directly in . 14:20:38.451 DEBUG tofu version: 1.8.5 14:20:38.451 DEBUG Reading Terragrunt config file at ./terragrunt.hcl 14:20:38.451 DEBUG Did not find any locals block: skipping evaluation. 14:20:38.451 DEBUG Did not find any locals block: skipping evaluation. 14:20:38.452 DEBUG Running command: tofu init 14:20:38.452 DEBUG Engine is not enabled, running command directly in . 14:20:38.469 INFO tofu: Initializing the backend... 14:20:38.470 INFO tofu: Initializing provider plugins... 14:20:38.470 INFO tofu: OpenTofu has been successfully initialized! 14:20:38.470 INFO tofu: 14:20:38.470 INFO tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see 14:20:38.470 INFO tofu: any changes that are required for your infrastructure. All OpenTofu commands 14:20:38.470 INFO tofu: should now work. 14:20:38.470 INFO tofu: If you ever set or change modules or backend configuration for OpenTofu, 14:20:38.470 INFO tofu: rerun this command to reinitialize your working directory. If you forget, other 14:20:38.470 INFO tofu: commands will detect it and remind you to do so if necessary. 14:20:38.470 DEBUG Running command: tofu plan 14:20:38.470 DEBUG Engine is not enabled, running command directly in . 14:20:38.490 STDOUT tofu: No changes. Your infrastructure matches the configuration. 14:20:38.490 STDOUT tofu: OpenTofu has compared your real infrastructure against your configuration and 14:20:38.490 STDOUT tofu: found no differences, so no changes are needed. ``` Here, we have three types of log messages: 1. `DEBUG` messages from Terragrunt itself. By default, Terragrunt’s log level is `INFO`, but we’ve set it to `DEBUG` using the `--log-level` flag. 2. `STDOUT` messages from OpenTofu. These are messages that OpenTofu would normally print directly to the terminal, but instead, Terragrunt captures them and logs them as `STDOUT` log messages, along with timestamps and other metadata. 3. `INFO` messages from Terragrunt [auto-init](/features/units/auto-init). These were initially emitted by OpenTofu. However, the user did not specifically ask for them, so Terragrunt logs them as `INFO` messages. ## Enrichment [Section titled “Enrichment”](#enrichment) The reason Terragrunt enriches stdout/stderr from the processes is that it is often very useful to have this extra metadata. For example: ```bash $ terragrunt run --all plan 14:27:45.359 INFO The stack at . will be processed in the following order for command plan: Group 1 - Module ./unit-1 - Module ./unit-2 14:27:45.399 INFO [unit-2] tofu: Initializing the backend... 14:27:45.399 INFO [unit-1] tofu: Initializing the backend... 14:27:45.400 INFO [unit-2] tofu: Initializing provider plugins... 14:27:45.400 INFO [unit-2] tofu: OpenTofu has been successfully initialized! 14:27:45.400 INFO [unit-2] tofu: 14:27:45.400 INFO [unit-2] tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see 14:27:45.400 INFO [unit-2] tofu: any changes that are required for your infrastructure. All OpenTofu commands 14:27:45.400 INFO [unit-2] tofu: should now work. 14:27:45.400 INFO [unit-2] tofu: If you ever set or change modules or backend configuration for OpenTofu, 14:27:45.400 INFO [unit-2] tofu: rerun this command to reinitialize your working directory. If you forget, other 14:27:45.400 INFO [unit-2] tofu: commands will detect it and remind you to do so if necessary. 14:27:45.400 INFO [unit-1] tofu: Initializing provider plugins... 14:27:45.400 INFO [unit-1] tofu: OpenTofu has been successfully initialized! 14:27:45.400 INFO [unit-1] tofu: 14:27:45.400 INFO [unit-1] tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see 14:27:45.400 INFO [unit-1] tofu: any changes that are required for your infrastructure. All OpenTofu commands 14:27:45.400 INFO [unit-1] tofu: should now work. 14:27:45.400 INFO [unit-1] tofu: If you ever set or change modules or backend configuration for OpenTofu, 14:27:45.400 INFO [unit-1] tofu: rerun this command to reinitialize your working directory. If you forget, other 14:27:45.400 INFO [unit-1] tofu: commands will detect it and remind you to do so if necessary. 14:27:45.422 STDOUT [unit-2] tofu: No changes. Your infrastructure matches the configuration. 14:27:45.423 STDOUT [unit-2] tofu: OpenTofu has compared your real infrastructure against your configuration and 14:27:45.423 STDOUT [unit-2] tofu: found no differences, so no changes are needed. 14:27:45.423 STDOUT [unit-1] tofu: No changes. Your infrastructure matches the configuration. 14:27:45.423 STDOUT [unit-1] tofu: OpenTofu has compared your real infrastructure against your configuration and 14:27:45.423 STDOUT [unit-1] tofu: found no differences, so no changes are needed. ``` Here you see two different units being run by Terragrunt concurrently, and stdout/stderr for each being emitted in real time. This is really helpful when managing IaC at scale, as it lets you know exactly what each unit in your stack is doing, and how long it is taking. It’s easier to see the impact of this enrichment if we turn it off, so let’s use the [bare](/reference/logging/formatting#bare) preset described in [Log Formatting](/reference/logging/formatting). ```bash $ terragrunt run --all --log-format bare plan INFO[0000] The stack at /Users/yousif/tmp/testing-stdout-stderr-split will be processed in the following order for command plan: Group 1 - Module /Users/yousif/tmp/testing-stdout-stderr-split/unit-1 - Module /Users/yousif/tmp/testing-stdout-stderr-split/unit-2 Initializing the backend... Initializing provider plugins... OpenTofu has been successfully initialized! You may now begin working with OpenTofu. Try running "tofu plan" to see any changes that are required for your infrastructure. All OpenTofu commands should now work. If you ever set or change modules or backend configuration for OpenTofu, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary. No changes. Your infrastructure matches the configuration. OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed. Initializing the backend... Initializing provider plugins... OpenTofu has been successfully initialized! You may now begin working with OpenTofu. Try running "tofu plan" to see any changes that are required for your infrastructure. All OpenTofu commands should now work. If you ever set or change modules or backend configuration for OpenTofu, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary. No changes. Your infrastructure matches the configuration. OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed. ``` This tells Terragrunt to log messages from OpenTofu/Terraform without any enrichment. As you can see, it’s not as easy to disambiguate the messages from the two units, so it helps to use Terragrunt’s default log format when managing IaC at scale. ## Exceptions to enrichment [Section titled “Exceptions to enrichment”](#exceptions-to-enrichment) There are exceptions to the general rule that Terragrunt logs stdout/stderr from the processes it runs as `STDOUT` and `STDERR` respectively when not in Headless Mode. Because Terragrunt is an IaC orchestrator, it uses its awareness of OpenTofu/Terraform usage to recognize certain circumstances when a user is likely to want stdout/stderr to be emitted exactly as it would when running a process directly. An example of this is `terragrunt output`: ```bash $ terragrunt output -json 15:20:07.759 INFO tofu: Initializing the backend... 15:20:07.759 INFO tofu: Initializing provider plugins... 15:20:07.759 INFO tofu: OpenTofu has been successfully initialized! 15:20:07.759 INFO tofu: 15:20:07.759 INFO tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see 15:20:07.759 INFO tofu: any changes that are required for your infrastructure. All OpenTofu commands 15:20:07.759 INFO tofu: should now work. 15:20:07.759 INFO tofu: If you ever set or change modules or backend configuration for OpenTofu, 15:20:07.759 INFO tofu: rerun this command to reinitialize your working directory. If you forget, other 15:20:07.759 INFO tofu: commands will detect it and remind you to do so if necessary. { "something": { "sensitive": false, "type": "string", "value": "Hello, World!" }, "something_else": { "sensitive": false, "type": "string", "value": "Goodbye, World!" } } ``` As you can see, the output from OpenTofu here isn’t being enriched, even though the user explicitly asked Terragrunt to run `output -json`. This is because the user is pretty likely to want to programmatically interact with the output of `output`, and so Terragrunt doesn’t enrich it. If, for example, you wanted to use a tool like `jq` to parse the output of `terragrunt output -json`, you could do that without having to worry about Terragrunt’s metadata getting in the way, or disabling anything with an extra flag. ```bash $ terragrunt output -json | jq '.something' 15:24:40.310 INFO tofu: Initializing the backend... 15:24:40.311 INFO tofu: Initializing provider plugins... 15:24:40.311 INFO tofu: OpenTofu has been successfully initialized! 15:24:40.311 INFO tofu: 15:24:40.311 INFO tofu: You may now begin working with OpenTofu. Try running "tofu plan" to see 15:24:40.311 INFO tofu: any changes that are required for your infrastructure. All OpenTofu commands 15:24:40.311 INFO tofu: should now work. 15:24:40.311 INFO tofu: If you ever set or change modules or backend configuration for OpenTofu, 15:24:40.311 INFO tofu: rerun this command to reinitialize your working directory. If you forget, other 15:24:40.311 INFO tofu: commands will detect it and remind you to do so if necessary. { "sensitive": false, "type": "string", "value": "Hello, World!" } ``` ## Streaming and buffering [Section titled “Streaming and buffering”](#streaming-and-buffering) While Terragrunt logs stdout from OpenTofu/Terraform in real time, it buffers each line of stdout before logging it. This is because Terragrunt needs to be able to buffer stdout to prevent different units from interleaving their log messages. Depending on what you’re doing with Terragrunt, this might occasionally result in issues when multiple units are running concurrently, and they are each producing multi-line output that is more convenient to be read independently. In these cases, you can do some post-processing on the logs to read the units in isolation. For example: ```bash $ terragrunt run --all apply --no-color --non-interactive > logs 16:01:51.164 INFO The stack at . will be processed in the following order for command apply: Group 1 - Module ./unit1 - Module ./unit2 ``` ```bash $ grep '\[unit1\]' < logs 16:01:51.272 STDOUT [unit1] tofu: null_resource.empty: Refreshing state... [id=3335573617542340690] 16:01:51.279 STDOUT [unit1] tofu: OpenTofu used the selected providers to generate the following execution 16:01:51.279 STDOUT [unit1] tofu: plan. Resource actions are indicated with the following symbols: 16:01:51.279 STDOUT [unit1] tofu: -/+ destroy and then create replacement 16:01:51.279 STDOUT [unit1] tofu: OpenTofu will perform the following actions: 16:01:51.279 STDOUT [unit1] tofu: # null_resource.empty must be replaced 16:01:51.279 STDOUT [unit1] tofu: -/+ resource "null_resource" "empty" { 16:01:51.279 STDOUT [unit1] tofu: ~ id = "3335573617542340690" -> (known after apply) 16:01:51.279 STDOUT [unit1] tofu: ~ triggers = { # forces replacement 16:01:51.280 STDOUT [unit1] tofu: ~ "always_run" = "2025-01-09T21:01:17Z" -> (known after apply) 16:01:51.280 STDOUT [unit1] tofu: } 16:01:51.280 STDOUT [unit1] tofu: } 16:01:51.280 STDOUT [unit1] tofu: Plan: 1 to add, 0 to change, 1 to destroy. 16:01:51.280 STDOUT [unit1] tofu: 16:01:51.297 STDOUT [unit1] tofu: null_resource.empty: Destroying... [id=3335573617542340690] 16:01:51.297 STDOUT [unit1] tofu: null_resource.empty: Destruction complete after 0s 16:01:51.300 STDOUT [unit1] tofu: null_resource.empty: Creating... 16:01:51.301 STDOUT [unit1] tofu: null_resource.empty: Provisioning with 'local-exec'... 16:01:51.301 STDOUT [unit1] tofu: null_resource.empty (local-exec): Executing: ["/bin/sh" "-c" "echo 'sleeping...'; sleep 1; echo 'done sleeping'"] 16:01:51.304 STDOUT [unit1] tofu: null_resource.empty (local-exec): sleeping... 16:01:52.311 STDOUT [unit1] tofu: null_resource.empty (local-exec): done sleeping 16:01:52.312 STDOUT [unit1] tofu: null_resource.empty: Creation complete after 1s [id=4749136145104485309] 16:01:52.322 STDOUT [unit1] tofu: 16:01:52.322 STDOUT [unit1] tofu: Apply complete! Resources: 1 added, 0 changed, 1 destroyed. 16:01:52.322 STDOUT [unit1] tofu: ``` ```bash $ grep '\[unit2\]' < logs 16:01:51.273 STDOUT [unit2] tofu: null_resource.empty: Refreshing state... [id=7532622543468447677] 16:01:51.280 STDOUT [unit2] tofu: OpenTofu used the selected providers to generate the following execution 16:01:51.280 STDOUT [unit2] tofu: plan. Resource actions are indicated with the following symbols: 16:01:51.280 STDOUT [unit2] tofu: -/+ destroy and then create replacement 16:01:51.280 STDOUT [unit2] tofu: OpenTofu will perform the following actions: 16:01:51.280 STDOUT [unit2] tofu: # null_resource.empty must be replaced 16:01:51.280 STDOUT [unit2] tofu: -/+ resource "null_resource" "empty" { 16:01:51.280 STDOUT [unit2] tofu: ~ id = "7532622543468447677" -> (known after apply) 16:01:51.280 STDOUT [unit2] tofu: ~ triggers = { # forces replacement 16:01:51.280 STDOUT [unit2] tofu: ~ "always_run" = "2025-01-09T21:01:17Z" -> (known after apply) 16:01:51.280 STDOUT [unit2] tofu: } 16:01:51.280 STDOUT [unit2] tofu: } 16:01:51.280 STDOUT [unit2] tofu: Plan: 1 to add, 0 to change, 1 to destroy. 16:01:51.280 STDOUT [unit2] tofu: 16:01:51.297 STDOUT [unit2] tofu: null_resource.empty: Destroying... [id=7532622543468447677] 16:01:51.297 STDOUT [unit2] tofu: null_resource.empty: Destruction complete after 0s 16:01:51.300 STDOUT [unit2] tofu: null_resource.empty: Creating... 16:01:51.301 STDOUT [unit2] tofu: null_resource.empty: Provisioning with 'local-exec'... 16:01:51.301 STDOUT [unit2] tofu: null_resource.empty (local-exec): Executing: ["/bin/sh" "-c" "echo 'sleeping...'; sleep 1; echo 'done sleeping'"] 16:01:51.303 STDOUT [unit2] tofu: null_resource.empty (local-exec): sleeping... 16:01:52.311 STDOUT [unit2] tofu: null_resource.empty (local-exec): done sleeping 16:01:52.312 STDOUT [unit2] tofu: null_resource.empty: Creation complete after 1s [id=6569505210291935319] 16:01:52.322 STDOUT [unit2] tofu: 16:01:52.322 STDOUT [unit2] tofu: Apply complete! Resources: 1 added, 0 changed, 1 destroyed. 16:01:52.322 STDOUT [unit2] tofu: ``` ## Disabling logs [Section titled “Disabling logs”](#disabling-logs) Finally, you can also disable logs entirely, like so: ```bash $ terragrunt --log-disable plan Initializing the backend... Initializing provider plugins... OpenTofu has been successfully initialized! You may now begin working with OpenTofu. Try running "tofu plan" to see any changes that are required for your infrastructure. All OpenTofu commands should now work. If you ever set or change modules or backend configuration for OpenTofu, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary. No changes. Your infrastructure matches the configuration. OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed. ``` This will give you the closest experience to using OpenTofu/Terraform directly, with Terragrunt doing all of its work in the background. # Formatting > Learn how to customize Terragrunt logging. Using the `--log-custom-format ` flag you can customize the way Terragrunt logs with total control over the logging format. The argument passed to this flag is a Terragrunt native format string that has special syntax, as described below. ## Placeholders [Section titled “Placeholders”](#placeholders) The format string consists of placeholders and text. Placeholders start with the `%` sign. e.g. ```shell --log-custom-format "%time %level %msg" ``` Output: ```shell 10:09:19.809 debug Running command: tofu --version ``` To escape the `%` character, use `%%`. e.g. ```shell --log-custom-format "%time %level %%msg" ``` Output: ```shell 10:09:19.809 debug %msg ``` Placeholders have preset names: * `%time` - Current time. * `%interval` - Seconds elapsed since Terragrunt started. * `%level` - Log level. * `%prefix` - Path to the working directory were Terragrunt is running. * `%msg` - Log message. * `%tf-path` - Path to the OpenTofu/Terraform executable (as defined by [tf-path](https://docs.terragrunt.com/reference/cli-options/#tf-path)). * `%tf-command` - Executed OpenTofu/Terraform command, e.g. `apply`. * `%tf-command-args` - Arguments of the executed OpenTofu/Terraform command, e.g. `apply -auto-approve`. * `%t` - Indent. * `%n` - Newline. Any other text is considered plain text. The parser always tries to find the longest name. For example, tofu command “apply -auto-approve” with format “%tf-command-args” will be replaced with “apply -auto-approve”, but not “apply-args”. If you need to replace it with “apply-args”, use empty brackets “%tf-command()-args”. More examples: “%tf-path” will be replaced with “tofu”, `%t()-path` will be replaced with ” -path”. e.g. ```shell --log-custom-format "time=%time level=%level message=%msg" ``` Output: ```shell time=00:10:44.716 level=debug message=Running command: tofu --version ``` Using the placeholder as shown above will display the value simply. If you would like to format the value, you can pass options to the placeholder. Placeholder formatting uses the following syntax: `%placeholder-name(option-name=option-value, option-name=option-value,...)` e.g. ```shell --log-custom-format "%time(format='Y-m-d H:i:sv') %level(format=short,case=upper) %msg" ``` Output: ```shell 2024-11-12 11:52:20.214 DEB Running command: tofu --version ``` In this example, the timestamp (as referenced by the `%time` placeholder) has been formatted with the `format` string `Y-m-d H:i:sv`. Similarly, the log level (as referenced by the `%level` placeholder), has been formatted to use the `short` `format`, and `upper` `case`. Even if you don’t pass options, the empty parenthesis are added implicitly. Thus `%time` is equivalent to `%time()`. Parenthesis are considered part of the syntax for specifying parameters to placeholders by default. Any parenthesis following a placeholder will be interpreted as specifying the parameters for the placeholder function. e.g. ```shell --log-custom-format "%time(plain-text)" ``` Output: ```shell invalid option name "plain-text" for placeholder "time" ``` If you would like to escape parentheses so that they appear as plain text in logs, make sure to use empty parentheses after a placeholder so that the following parentheses are not evaluated as specifying parameters for the placeholder function. e.g. ```shell --log-custom-format "%time()(plain-text)" ``` Output: ```shell 12:33:08.513(plain-text) ``` You can format plain text as well by using an unnamed placeholder. e.g. ```shell --log-custom-format "%(content='time=',color=magenta)%time %(content='level=',color=light-blue)%level %(content='msg=',color=green)%msg" ``` Output: ```shell time=12:33:08.513 level=debug msg=Running command: tofu --version ``` *Unfortunately, it is not possible to display color in a Markdown document, but in the above output, `time=` is colored magenta, `level=` is colored light blue and `msg=` is colored green.* ![screenshot](/_vercel/image?url=_astro%2Fcustom-log-format-1.CkmWyWWL.jpg\&w=1200\&q=100) ## Options [Section titled “Options”](#options) Options can be divided into common ones, which can be passed to any placeholder, and specific ones for each placeholder. Common options: * `content=` - Sets a placeholder value, typically used to set the initial value of an unnamed placeholder. * `case=[upper|lower|capitalize]` - Sets the case of the text. * `width=` - Sets the column width. * `align=[left|center|right]` - Aligns content relative to the edges of the column, used in conjunction with `width`. * `prefix=` - Prepends the prefix to the content. If the content of the placeholder is empty, the prefix will not be prepended. * `suffix=`- Appends the suffix to the content. If the content of the placeholder is empty, the suffix will not be appended. * `escape=[json]` - Escapes content for use as a value in a JSON string. * `color=[red|white|yellow|green|cayn|magenta|blue|...]` - Sets the color for the content. * `1..255` - Specifies a color using a [number](https://www.hackitu.de/termcolor256/), 1 to 255 * `red|white|yellow|green|cyan|magenta|blue|light-blue|light-black|light-red|light-green|light-yellow|light-magenta|light-cyan|light-white` - Specifies a color using a word * `gradient` - Specifies to use a different color each time the placeholder content changes. * `preset` - Specifies to use preset colors. For example, each log level name has its own preset color. * `disable` - Disables color, also removes colors set in terraform/tofu output. Specific options for placeholders: * `%level` * `format=[full|short|tiny]` - Specifies the format for log level names. * `full` - `stdout`, `stderr`, `error`, `warn`, `info`, `debug`, `trace` * `short` - `std`, `err`, `wrn`, `inf`, `deb`, `trc` * `tiny` - `s`, `e`, `w`, `i`, `d`, `t` * `%time` * `format=` - Sets the time format. Preset formats: * `date-time` - e.g. `2006-01-02 15:04:05` * `date-only` - e.g. `2006-01-02` * `time-only` - e.g. `15:04:05` * `rfc3339` - e.g. `2006-01-02T15:04:05Z07:00` * `rfc3339-nano` - e.g. `2006-01-02T15:04:05.999999999Z07:00` Custom format string characters: * `H` - 24-hour format of an hour with leading zeros, `00` to `23` * `h` - 12-hour format of an hour with leading zeros, `01` to `12` * `g` - 12-hour format of an hour without leading zeros, `1` to `12` * `i` - Minutes with leading zeros, `00` to `59` * `s` - Seconds with leading zeros, `00` to `59` * `v` - Milliseconds. e.g. `.654` * `u` - Microseconds, e.g. `.654321` * `Y` - A full numeric representation of a year, e.g. `1999`, `2003` * `y` - A two digit representation of a year, e.g. `99`, `03` * `m` - Numeric representation of a month, with leading zeros, `01` to `12` * `n` - Numeric representation of a month, without leading zeros, `1` to `12` * `M` - A short textual representation of a month, three letters, `Jan` to `Dec` * `d` - Day of the month, 2 digits with leading zeros, `01` to `31` * `j` - Day of the month without leading zeros, `1` to `31` * `D` - A textual representation of a day, three letters, `Mon` to `Sun` * `A` - Uppercase Ante meridiem and Post meridiem, `AM` or `PM` * `a` - Lowercase Ante meridiem and Post meridiem, `am` or `pm` * `T` - Timezone abbreviation, e.g. `EST`, `MDT` * `P` - Difference to Greenwich time (GMT) with colon between hours and minutes, e.g. `+02:00` * `O` - Difference to Greenwich time (GMT) without colon between hours and minutes, e.g. `+0200` * `%prefix` * `path=[relative|short-relative|short]` * `relative` - Outputs a relative path to the working directory. * `short-relative` - Outputs a relative path to the working directory, trims the leading slash `./` and hides the working directory path `.` * `short` - Outputs an absolute path, but hides the working directory path. * `%tf-path` * `path=[filename|dir]` * `filename` - Outputs the name of the executable. * `dir` - Outputs the directory name of the executable. * `%msg` * `path=[relative]` * `relative` - Converts all absolute paths to paths relative to the working directory. ## Presets [Section titled “Presets”](#presets) The examples below replicate the preset formats specified with `--log-format`. They can be useful if you need to change existing formats to suit your needs. ### Pretty [Section titled “Pretty”](#pretty) `--log-format pretty` ```shell --log-custom-format "%time(color=light-black) %level(case=upper,width=6,color=preset) %prefix(path=short-relative,color=gradient,suffix=' ')%tf-path(color=cyan,suffix=': ')%msg(path=relative)" ``` ### Bare [Section titled “Bare”](#bare) `--log-format bare` ```shell --tf-forward-stdout --log-custom-format "%level(case=upper,width=4)[%interval] %msg %prefix(path=short,prefix='prefix=[',suffix=']')" ``` ### Key-value [Section titled “Key-value”](#key-value) `--log-format key-value` ```shell --log-custom-format "time=%time(format=rfc3339) level=%level prefix=%prefix(path=short-relative) tf-path=%tf-path(path=filename) msg=%msg(path=relative,color=disable)" ``` ### JSON [Section titled “JSON”](#json) `--log-format json` ```shell --log-custom-format '{"time":"%time(format=rfc3339,escape=json)", "level":"%level(escape=json)", "prefix":"%prefix(path=short-relative,escape=json)", "tf-path":"%tf-path(path=filename,escape=json)", "msg":"%msg(path=relative,escape=json,color=disable)"}' ``` # Strict Controls > Opt-in to strict controls to avoid deprecated features and ensure your code is future-proof. Terragrunt supports operating in a mode referred to as “Strict Mode”. Strict Mode is a set of controls that can be enabled to ensure that your Terragrunt usage is future-proof by making deprecated features throw errors instead of warnings. This can be useful when you want to ensure that your Terragrunt code is up-to-date with the latest conventions to avoid breaking changes in future versions of Terragrunt. Whenever possible, Terragrunt will initially provide you with a warning when you use a deprecated feature, without throwing an error. However, in Strict Mode, these warnings will be converted to errors, which will cause the Terragrunt command to fail. A good practice for using strict controls is to enable Strict Mode in your CI/CD pipelines for lower environments to catch any deprecated features early on. This allows you to fix them before they become a problem in production in a future Terragrunt release. If you are unsure about the impact of enabling strict controls, you can enable them for specific controls to gradually increase your confidence in the future compatibility of your Terragrunt usage. ## Controlling Strict Mode [Section titled “Controlling Strict Mode”](#controlling-strict-mode) The simplest way to enable strict mode is to set the [strict-mode](/reference/strict-controls) flag. This will enable strict mode for all Terragrunt commands, for all strict mode controls. ```bash $ terragrunt run-all plan 15:26:08.585 WARN The `run-all plan` command is deprecated and will be removed in a future version. Use `terragrunt run --all plan` instead. ``` ```bash $ terragrunt --strict-mode run-all plan 15:26:23.685 ERROR The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead. ``` You can also use the environment variable, which can be more useful in CI/CD pipelines: ```bash $ TG_STRICT_MODE='true' terragrunt run-all plan 15:26:23.685 ERROR The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead. ``` Instead of enabling strict mode like this, you can also enable specific strict controls by setting the [strict-control](/reference/strict-controls) flag to a value that’s specific to a particular strict control. This can allow you to gradually increase your confidence in the future compatibility of your Terragrunt usage. ```bash $ terragrunt run-all plan --strict-control cli-redesign 15:26:08.585 WARN The `run-all plan` command is deprecated and will be removed in a future version. Use `terragrunt run --all plan` instead. ``` ```bash $ terragrunt run-all plan --strict-control cli-redesign 15:26:23.685 ERROR The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead. ``` Again, you can also use the environment variable, which might be more useful in CI/CD pipelines: ```bash $ TG_STRICT_CONTROL='cli-redesign' terragrunt run-all plan 15:26:23.685 ERROR The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead. ``` You can enable multiple strict controls at once: ```bash $ terragrunt run-all plan --strict-control cli-redesign --strict-control default-command 15:26:23.685 ERROR The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead. 15:26:46.521 ERROR Unable to determine underlying exit code, so Terragrunt will exit with error code 1 ``` ```bash $ terragrunt run-all apply --strict-control cli-redesign --strict-control default-command 15:26:46.564 ERROR The `run-all apply` command is no longer supported. Use `terragrunt run --all apply` instead. 15:26:46.564 ERROR Unable to determine underlying exit code, so Terragrunt will exit with error code 1 ``` You can also enable multiple strict controls at once when using the environment variable by using a comma-delimited list. ```bash $ TG_STRICT_CONTROL='cli-redesign,default-command' bash -c 'terragrunt run-all plan; terragrunt run-all apply' 15:26:46.521 ERROR The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead. 15:26:46.521 ERROR Unable to determine underlying exit code, so Terragrunt will exit with error code 1 15:26:46.564 ERROR The `run-all apply` command is no longer supported. Use `terragrunt run --all apply` instead. 15:26:46.564 ERROR Unable to determine underlying exit code, so Terragrunt will exit with error code 1 ``` You can also use [control categories](#control-categories) to enable certain categories of strict controls. ```bash $ terragrunt run-all plan --strict-control deprecated-commands 15:26:23.685 ERROR The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead. ``` ## Control Categories [Section titled “Control Categories”](#control-categories) Certain strict controls are grouped into categories to make it easier to enable multiple strict controls at once. These categories change over time, so you might want to use the specific strict controls if you want to ensure that only certain controls are enabled. ### deprecated-commands [Section titled “deprecated-commands”](#deprecated-commands) Throw an error when using the deprecated commands. **Controls**: * [default-command](/reference/strict-controls/active#default-command) * [cli-redesign](/reference/strict-controls/active#cli-redesign) **Note**: The individual `*-all` commands (`plan-all`, `apply-all`, `destroy-all`, `output-all`, `validate-all`, `spin-up`, `tear-down`) have been removed from Terragrunt and are no longer available as strict controls. Use `terragrunt run --all` for the modern equivalent. ### deprecated-flags [Section titled “deprecated-flags”](#deprecated-flags) Throw an error when using the deprecated flags. **Controls**: * [queue-exclude-external](/reference/strict-controls/active#queue-exclude-external) * [no-destroy-dependencies-check](/reference/strict-controls/active#no-destroy-dependencies-check) * [deprecated-hidden-flag](/reference/strict-controls/active#deprecated-hidden-flag) * [queue-strict-include](/reference/strict-controls/active#queue-strict-include) * [units-that-include](/reference/strict-controls/active#units-that-include) * [disable-command-validation](/reference/strict-controls/active#disable-command-validation) * [disable-dependent-modules](/reference/strict-controls/active#disable-dependent-modules) ### deprecated-env-vars [Section titled “deprecated-env-vars”](#deprecated-env-vars) Throw an error when using the deprecated environment variables. **Controls**: * [terragrunt-prefix-env-vars](/reference/strict-controls/active#terragrunt-prefix-env-vars) # OpenTofu and Terraform Version Compatibility Table > Learn which Terraform and OpenTofu versions are compatible with which versions of Terragrunt. ## Supported OpenTofu Versions [Section titled “Supported OpenTofu Versions”](#supported-opentofu-versions) The officially supported versions are: | OpenTofu Version | Terragrunt Version | | ---------------- | ---------------------------------------------------------------------------- | | 1.12.x | >= [1.0.5](https://github.com/gruntwork-io/terragrunt/releases/tag/v1.0.5) | | 1.11.x | >= [0.95.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.95.0) | | 1.10.x | >= [0.82.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.82.0) | | 1.9.x | >= [0.72.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.72.0) | | 1.8.x | >= [0.66.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.66.0) | | 1.7.x | >= [0.58.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.58.0) | | 1.6.x | >= [0.52.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.52.0) | ## Supported Terraform Versions [Section titled “Supported Terraform Versions”](#supported-terraform-versions) The officially supported versions are: | Terraform Version | Terragrunt Version | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | 1.14.x | >= [0.94.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.94.0) | | 1.13.x | >= [0.86.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.86.0) | | 1.12.x | >= [0.80.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.80.0) | | 1.11.x | >= [0.75.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.75.0) | | 1.10.x | >= [0.74.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.74.0) | | 1.9.x | >= [0.60.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.60.0) | | 1.8.x | >= [0.57.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.57.0) | | 1.7.x | >= [0.56.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.56.0) | | 1.6.x | >= [0.53.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.53.0) | | 1.5.x | >= [0.48.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.48.0) | | 1.4.x | >= [0.45.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.45.0) | | 1.3.x | >= [0.40.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.40.0) | | 1.2.x | >= [0.38.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.38.0) | | 1.1.x | >= [0.36.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.36.0) | | 1.0.x | >= [0.31.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.31.0) | | 0.15.x | >= [0.29.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.29.0) | | 0.14.x | >= [0.27.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.27.0) | | 0.13.x | >= [0.25.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.25.0) | | 0.12.x | [0.19.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.19.0) - [0.24.4](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.24.4) | | 0.11.x | [0.14.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.14.0) - [0.18.7](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.18.7) | **Note 1:** Terragrunt lists support for BSL versions of Terraform (>= 1.6.x) and core IaC functionality will work as expected. However, support for BSL Terraform-specific features is not guaranteed even if that version is in this table. **Note 2:** This table lists versions that are officially tested in the CI process. In practice, the version compatibility is more relaxed than documented above. For example, we’ve found that Terraform 0.13 works with any version above 0.19.0, and we’ve also found that terraform 0.11 works with any version above 0.19.18 as well. If you wish to use Terragrunt against an untested Terraform version, you can use the [terraform\_version\_constraint](https://docs.terragrunt.com/reference/config-blocks-and-attributes/#terraform_version_constraint) (introduced in Terragrunt [v0.19.18](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.19.18)) attribute to relax the version constraint. ## Compatibility API [Section titled “Compatibility API”](#compatibility-api) Version compatibility data is available via JSON API: | Endpoint | Description | | --------------------------------- | -------------- | | `/api/v1/compatibility` | All entries | | `/api/v1/compatibility/opentofu` | OpenTofu only | | `/api/v1/compatibility/terraform` | Terraform only | **Example:** ```bash curl https://docs.terragrunt.com/api/v1/compatibility/opentofu ``` **Response format:** ```json [ { "tool": "opentofu", "version": "1.11.x", "terragrunt_min": "0.95.0", "terragrunt_max": null } ] ``` | Field | Description | | ---------------- | ------------------------------------------------------------ | | `tool` | IaC tool name: `opentofu` or `terraform` | | `version` | Tool version pattern (e.g., `1.11.x`) | | `terragrunt_min` | Minimum compatible Terragrunt version | | `terragrunt_max` | Maximum compatible version, or `null` for open-ended support | # Terragrunt Cache > Learn what the `.terragrunt-cache` directory is and how to manage it. Terragrunt uses a cache directory (`.terragrunt-cache`) to store downloaded modules when using the `source` attribute in the `terraform` block. This cache directory is created whenever Terragrunt downloads a module from a remote source, and where it runs the OpenTofu/Terraform commands. It also stores any modules and providers that are downloaded as part of these commands by default. ## Clearing the Terragrunt cache [Section titled “Clearing the Terragrunt cache”](#clearing-the-terragrunt-cache) Terragrunt creates a `.terragrunt-cache` folder in the current working directory as its scratch directory. It downloads your remote OpenTofu/Terraform configurations into this folder, runs your OpenTofu/Terraform commands in this folder, and any modules and providers those commands download also get stored in this folder. You can safely delete this folder any time, and Terragrunt will recreate it as necessary. If you need to clean up a lot of these folders (e.g., after `terragrunt run --all apply`), you can use the following commands on Mac and Linux: Recursively find all the `.terragrunt-cache` folders that are children of the current folder: ```bash find . -type d -name ".terragrunt-cache" ``` If you are **SURE** you want to delete all the folders that come up in the previous command, you can recursively delete all of them as follows: ```bash find . -type d -name ".terragrunt-cache" -prune -exec rm -rf {} \; ``` Also consider setting the `TG_DOWNLOAD_DIR` environment variable if you wish to place the cache directories somewhere else. If the reason you are clearing out your Terragrunt cache is that you are struggling with running out of disk space, consider using the [Provider Cache](/features/caching/provider-cache-server) feature to store OpenTofu/Terraform provider plugins in a shared location, as those are typically the largest files stored in the `.terragrunt-cache` directory. # Drift Detection > Automatically detect and remediate infrastructure drift in your Terragrunt-managed resources. Drift Detection is the infrastructure reconciliation component of Terragrunt Scale. It regularly compares your deployed units against your Terragrunt code to identify drift and remediate it. ## How It Works [Section titled “How It Works”](#how-it-works) 1. Drift Detection runs `terragrunt plan` on a configurable schedule against your infrastructure units. 2. When drift is detected (the plan shows more than zero changes) it automatically opens a pull request identifying the changes for only the drifted units. 3. The PR includes details about what drifted and proposes remediation, so your team can review and merge to bring infrastructure back in line. ## Key Capabilities [Section titled “Key Capabilities”](#key-capabilities) * **Scheduled or on-demand scans** — Run drift detection manually from your VCS UI, or on a cron. * **Automatic remediation PRs** — Drift is surfaced as a PR/MR on a `drift-detection` branch. Merging it triggers `terragrunt apply` on the drifted units to bring infrastructure back in line. * **Glob-based filtering** — Target specific environments or directories using glob patterns (e.g. `management/**`), so you can start with a narrow scope and expand over time. * **Two remediation paths** — Either merge the drift PR/MR to apply the code as-is, or update the IaC on the `drift-detection` branch to match the desired cloud state before merging. ## Why It Matters [Section titled “Why It Matters”](#why-it-matters) Infrastructure drift happens when cloud resources are modified outside of your IaC workflow. This can happen through the console, CLI, or other automation. Over time, undetected drift leads to: * Unexpected behavior during the next `terragrunt apply` * Security and compliance gaps from untracked configuration changes * Increased difficulty troubleshooting production issues Drift Detection catches these changes early, before they compound into larger problems. Full Documentation For setup instructions and configuration options, see the [Gruntwork Drift Detection documentation](https://docs.gruntwork.io/2.0/docs/pipelines/concepts/drift-detection). # Installation > Get started with Terragrunt Scale - sign up and connect your infrastructure repositories. This page provides a brief overview of the setup process. ## Getting Started [Section titled “Getting Started”](#getting-started) 1. **Sign up** — Create an account at [app.gruntwork.io](https://app.gruntwork.io/signup/terragrunt-scale-free-tier). The rest of the steps listed here will be guided by the onboarding wizard that runs after sign-up. 2. **Connect your VCS** — Install the Gruntwork.io GitHub App or configure a GitLab machine user to connect to one or more Git repositories. 3. **Bootstrap your cloud resources** — Provision all the cloud resources you need to bootstrap your cloud environment for use with Terragrunt Scale. This involves running a single script, then merging a single pull request, generated by the onboarding wizard. 4. **Run your first pipeline** — Click a button in the wizard and have an automatically generated Pull Request (PR) created for you to review and merge. On creation, Pipelines will run a plan, with plan output commented on the pull request. On merge, Pipelines will apply the changes in the PR. Custom Installation Guide For detailed setup customization options including custom OIDC setups, environment-specific authentication, and advanced options, see the [Gruntwork Pipelines installation guide](https://docs.gruntwork.io/2.0/docs/pipelines/installation/addingnewrepo). This bypasses the automated wizard and has you manually provision all the requisite pieces of Terragrunt Scale yourself. You are encouraged to go through the onboarding wizard first to get familiar with the process. ## Configuration [Section titled “Configuration”](#configuration) Terragrunt Scale uses [configuration-as-code defined in HCL](https://docs.gruntwork.io/2.0/reference/pipelines/configurations-as-code/). You can configure settings at the repository, environment, or individual unit level, including: * Cloud provider authentication (e.g. `aws_oidc` with per-environment plan/apply IAM roles) * Environment definitions that filter which units a configuration applies to * Repository-level settings like the branch that deploys IaC (e.g. `main`) and environment variables to set for Terragrunt runs * Per-unit overrides for authentication and other settings ## Requirements [Section titled “Requirements”](#requirements) * A GitHub or GitLab repository you can push to * An AWS, GCP, or Azure account you can provision infrastructure in Free Tier Requirements The onboarding wizard for the free tier requires a GitHub repository and an AWS account. [Join the waitlist](https://form.typeform.com/to/c8xJ0vdx) to get notified when GitLab, Azure, and GCP are supported in the free tier. # Terragrunt Scale > GitOps infrastructure pipelines, drift detection, and automated updates for Terragrunt. No black boxes. Terragrunt Scale is a GitOps platform that extends Terragrunt with production-grade infrastructure automation. It runs entirely within your own CI/CD environment (GitHub Actions or GitLab CI), giving you full control with no black boxes. Free Tier Available Terragrunt Scale is free for up to 25 [units](/getting-started/terminology#unit). [Sign up](https://app.gruntwork.io) to get started today! ## What You Get [Section titled “What You Get”](#what-you-get) Terragrunt Scale includes three core products: * **[Pipelines](/terragrunt-scale/pipelines)** — CI/CD for Terragrunt, built by the creators. Plan on PR/MR, apply on merge, with blast radius minimization and OIDC authentication. * **[Drift Detection](/terragrunt-scale/drift-detection)** — Scheduled scans that compare your deployed infrastructure against your Terragrunt code, with automatic pull requests to remediate drift. * **[Patcher](/terragrunt-scale/patcher)** — Automated dependency updates that open PR/MRs when your OpenTofu/Terraform modules or Terragrunt units/stacks have new versions available, including handling of breaking changes. ## How It Works [Section titled “How It Works”](#how-it-works) Terragrunt Scale integrates directly with your version control system and CI/CD platform: 1. A GitHub App or GitLab machine user connects to your repositories. 2. Infrastructure changes trigger plan/apply workflows in your own CI/CD runners (GitHub Actions or GitLab CI). 3. OIDC handshakes acquire temporary, least-privilege credentials for each environment. 4. Terragrunt’s directed acyclic graph (DAG) ensures resources are created, updated, and destroyed in the correct order. All operations run inside your infrastructure and runners. Terragrunt Scale never has direct access to your cloud accounts or state files. ## Supported Platforms [Section titled “Supported Platforms”](#supported-platforms) * **VCS**: GitHub, GitLab, GitHub Enterprise, GitLab Self-Managed * **IaC**: Terragrunt, OpenTofu, Terraform * **Cloud**: AWS, GCP, Azure (and any other platform supported by OpenTofu/Terraform with custom authentication) Free Tier Platform Support The free tier currently only supports GitHub and AWS. [Join the waitlist](https://form.typeform.com/to/c8xJ0vdx) to get notified when GitLab, Azure, and GCP are supported in the free tier. ## Learn More [Section titled “Learn More”](#learn-more) * [Installation](/terragrunt-scale/installation) — Get started with Terragrunt Scale * [Full documentation](https://docs.gruntwork.io) — Detailed guides and reference on the Gruntwork docs site # Patcher > Automated dependency updates for your Terragrunt modules and configurations. Patcher is the dependency management component of Terragrunt Scale. It automates the process of keeping your infrastructure modules up to date, including handling breaking changes. ## How It Works [Section titled “How It Works”](#how-it-works) 1. Patcher scans your repositories for Terragrunt module/unit/stack references, and their current versions. 2. When new versions are available, it opens PRs with the updated version pins. 3. For breaking changes, Patcher applies code transformations (patches) to automate resolution of breaking changes, or generates documentation for changes that require manual intervention. ## Key Capabilities [Section titled “Key Capabilities”](#key-capabilities) * **Automated update PRs** — New module versions are surfaced as PRs on a configurable schedule, fitting into your existing review workflow. * **Breaking change handling** — Patcher doesn’t just bump versions. It applies code patches for breaking changes or clearly documents what manual steps are needed so you can safely stay up to date. * **Environment promotion** — Roll out updates to dev first, then staging, then production, with promotion workflows between environments. * **Stack awareness** — Detects updates within nested stacks and `terragrunt.stack.hcl` files, not just individual units. * **Grouping modes** — Combine related updates into a single PR to reduce noise, or keep them separate for granular review. ## Two Modes of Operation [Section titled “Two Modes of Operation”](#two-modes-of-operation) * **Ongoing updates** — A GitHub Action that continuously scans your repositories and opens PRs when new versions are available. This is the primary mode for day-to-day use. * **Legacy upgrades** — An interactive CLI mode for bringing significantly outdated repositories up to current versions. Useful for one-time catch-up migrations. ## Supported Sources [Section titled “Supported Sources”](#supported-sources) Patcher works with any IaC source that Terragrunt supports, including: * The Gruntwork IaC Library * Your own internal IaC * Open-source OpenTofu/Terraform modules * Open-source Terragrunt units/stacks Full Documentation For setup instructions, patching tutorials, and configuration reference, see the [Gruntwork Patcher documentation](https://docs.gruntwork.io/2.0/docs/patcher/concepts/). # Pipelines > DAG-aware CI/CD pipelines for Terragrunt with blast radius minimization and OIDC authentication. Pipelines is the CI/CD component of Terragrunt Scale. It automates `terragrunt plan` and `terragrunt apply` in your pull request workflow, with features designed to leverage Terragrunt’s dependency graph. ## How It Works [Section titled “How It Works”](#how-it-works) When you open a pull request with infrastructure changes: 1. Pipelines detects which Terragrunt units were modified (or potentially impacted by other changes in your repository). 2. It runs `terragrunt plan` for only the affected units, minimizing blast radius. 3. Plan results are posted as PR comments for review. 4. On merge, `terragrunt apply` runs in dependency order, respecting your DAG. All execution happens in your own CI/CD runners (GitHub Actions or GitLab CI). Pipelines never has direct access to your cloud accounts. ## Key Capabilities [Section titled “Key Capabilities”](#key-capabilities) * **DAG-aware execution** — Respects Terragrunt’s dependency graph for creates, updates, and destroys. Units are always run in the correct order. * **Blast radius minimization** — Only plans and applies units that were actually changed, rather than running against your entire stack. * **Least-privilege OIDC authentication** — Acquires temporary credentials via OIDC for each environment, scoped by read/write and segmented per environment. No long-lived secrets stored in CI. * **Concurrent runs with unified output** — Runs multiple independent units in parallel and aggregates all results into a single, well-structured PR comment, giving reviewers a complete picture of every change in one place. * **Multi-environment support** — Deploy changes across multiple environments in a single PR, with per-environment authentication and cross-environment dependencies supported. * **Configuration-as-code** — Pipeline behavior is defined in HCL and can be customized at the repository, environment, or individual unit level. See the [configuration reference](https://docs.gruntwork.io/2.0/reference/pipelines/configurations-as-code/) for details. ## Terragrunt Integration [Section titled “Terragrunt Integration”](#terragrunt-integration) Pipelines has native support for the complete Terragrunt lifecycle: * `terragrunt plan` / `terragrunt apply` / `terragrunt destroy` * `terragrunt run --all` for stack-wide operations * Full support for [Stacks](/features/stacks) and [Units](/features/units) Full Documentation For architecture details, configuration reference, and tutorials, see the [Gruntwork Pipelines documentation](https://docs.gruntwork.io/2.0/docs/pipelines/concepts/overview). # Troubleshooting > Get help with Terragrunt Scale - support resources, community, and contact information. ## Community Support [Section titled “Community Support”](#community-support) The [Terragrunt Community Discord Server](/tgs-discord) is the best place to ask questions, share tips, and get help from other Terragrunt Scale users and Gruntwork engineers. Enterprise Support [Terragrunt Enterprise Support](https://www.gruntwork.io/services/terragrunt) is available for organizations that need dedicated support, SLAs, and direct access to the Gruntwork engineering team. ## Resources [Section titled “Resources”](#resources) * [Gruntwork Documentation](https://docs.gruntwork.io) — Full product documentation for Pipelines, Patcher, and all Gruntwork products * [Terragrunt Community Discord](/tgs-discord) — Community chat and support * [Gruntwork Support](https://gruntwork.io/support) — Commercial support options * [Contact Gruntwork](https://gruntwork.io/contact) — Sales and partnership inquiries # Debugging > Debugging Terragrunt and OpenTofu/Terraform ## Support [Section titled “Support”](#support) If you are running into issues with Terragrunt, join the [Terragrunt Community Discord Server](/community/invite). Enterprise Support [Terragrunt Enterprise Support](https://www.gruntwork.io/services/terragrunt) is available for users who need additional support beyond the Terragrunt Community Discord Server. ## Logging [Section titled “Logging”](#logging) The easiest way to get more information for debugging an error you are experiencing in Terragrunt is to increase the log level. ```bash terragrunt run --log-level debug -- plan ``` Increasing the log level using the [`--log-level`](/reference/cli/global-flags/#log-level) flag will output more information about what Terragrunt is doing and why. This is useful information for your debugging, and for sharing with others if you want their help in understanding what went wrong in your run. Terragrunt has a fairly sophisticated logging system, and it can be helpful to read through the documentation on [Logging](/reference/logging/) to understand how it works. ## Telemetry [Section titled “Telemetry”](#telemetry) If you have [Docker](https://www.docker.com/) installed, and you want to gain deeper insight as to what Terragrunt is doing, you can also enable the [OpenTelemetry](https://opentelemetry.io/) integration in Terragrunt to send telemetry data to a locally running container to better visualize what Terragrunt is doing and when. First, run the following in a terminal: ```bash docker run --rm --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:1.54.0 ``` This spins up a local instance of [Jaeger](https://www.jaegertracing.io/), which is a distributed tracing system that can be used to see what Terragrunt is doing. Next, run the following in the terminal where you want to run Terragrunt: ```bash export TG_TELEMETRY_TRACE_EXPORTER=http export TG_TELEMETRY_TRACE_EXPORTER_HTTP_ENDPOINT=localhost:4318 export TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT=true ``` This tells Terragrunt that it should send telemetry data to the Jaeger instance running in the Docker container. Now you’ll want to do whatever you want to trace with Terragrunt by running Terragrunt normally. ```bash terragrunt run -- plan ``` Finally, visit the Jaeger UI at [http://localhost:16686](http://localhost:16686/) to see traces of your runs. Exporting traces You can export traces using the “Download Results” button in the UI and upload them using the “Upload” pane on the left. This can be useful if you want to share traces with others, though you should be mindful that you might have sensitive data in these traces you don’t want shared with untrusted parties. Sharing traces with other trusted members of your team can be a good way to visually analyze the results of runs in addition to reading through logs. Learn More About OpenTelemetry To learn more about Terragrunt’s integration with OpenTelemetry, see the dedicated [OpenTelemetry documentation](/troubleshooting/open-telemetry/). ## Debugging OpenTofu/Terraform Behavior [Section titled “Debugging OpenTofu/Terraform Behavior”](#debugging-opentofuterraform-behavior) Terragrunt and OpenTofu/Terraform usually play well together in helping you write scalable, reusable infrastructure. But how can you figure out what went wrong in the rare case that things *do* go wrong? Terragrunt provides a way to configure its logging level through the [`--log-level`](https://docs.terragrunt.com/reference/cli/global-flags/#log-level) flag. Additionally, Terragrunt provides an [`--inputs-debug`](https://docs.terragrunt.com/reference/cli/commands/run/#inputs-debug) flag that can generate a `terragrunt-debug.tfvars.json` file to help you understand what inputs it is setting when calling OpenTofu/Terraform. For example, you could use it like this to debug a `plan` that’s producing unexpected outcomes. ```shell terragrunt run --log-level debug --inputs-debug -- plan ``` Running this command will do two things for you: * Output a file named `terragrunt-debug.tfvars.json` to your current working directory (the same one containing your `terragrunt.hcl`) * Print instructions on how to invoke `tofu`/`terraform` against the generated file to reproduce exactly the same `tofu`/`terraform` output as you saw when invoking `terragrunt`. This will help you to determine where the problem’s root cause lies. Using those features is helpful when you want determine which of these three major areas is the root cause of your problem: 1. Misconfiguration of your infrastructure code. 2. An error in `terragrunt`. 3. An error in `tofu/terraform`. Consider this file structure for a fictional production environment where we have configured an application to deploy as many tasks as there are minimum number of machines in some cluster. * live * prod * app * vars.tf * main.tf * outputs.tf * terragrunt.hcl * ecs-cluster * outputs.tf The files contain this text (`app/main.tf` and `ecs-cluster/outputs.tf` omitted for brevity): app/vars.tf ```hcl variable "image_id" { type = string } variable "num_tasks" { type = number } # app/outputs.tf output "task_ids" { value = module.app_infra_module.task_ids } # app/terragrunt.hcl locals { image_id = "acme/myapp:1" } dependency "cluster" { config_path = "../ecs-cluster" } inputs = { image_id = locals.image_id num_tasks = dependency.cluster.outputs.cluster_min_size } ``` You perform a `terragrunt plan`, and find that `outputs.task_ids` has 7 elements, but you know that the cluster only has 4 VMs in it! What’s happening? Let’s figure it out. Run this: ```shell terragrunt plan --log-level debug --inputs-debug ``` After planning, you will see output like this in stderr: ```log [terragrunt] Variables passed to tofu/terraform are located in "~/live/prod/app/terragrunt-debug.tfvars" [terragrunt] Run this command to replicate how tofu/terraform was invoked: [terragrunt] tofu plan -var-file="~/live/prod/app/terragrunt-debug.tfvars.json" "~/live/prod/app" ``` Well we may have to do all that, but first let’s just take a look at `terragrunt-debug.tfvars.json` ```json { "image_id": "acme/myapp:1", "num_tasks": 7 } ``` So this gives us the clue — we expected `num_tasks` to be 4, not 7! Looking into `ecs-cluster/outputs.tf` we see this text: ecs-cluster/outputs.tf ```hcl output "cluster_min_size" { value = module.my_cluster_module.cluster_max_size } ``` Oops! It says `max` when it should be `min`. If we fix `ecs-cluster/outputs.tf` we should be golden! ## Additional OpenTofu/Terraform Debugging [Section titled “Additional OpenTofu/Terraform Debugging”](#additional-opentofuterraform-debugging) If you’re still having trouble, you may want to try adjusting the `TF_LOG` environment variables to instruct OpenTofu/Terraform to emit more detailed logs. ```bash TF_LOG=debug terragrunt run -- plan ``` You can learn more about OpenTofu/Terraform debug logging [here](https://opentofu.org/docs/internals/debugging/). # OpenTelemetry > Learn how to integrate Terragrunt with OpenTelemetry Terragrunt can be configured to emit telemetry in [OpenTelemetry](https://opentelemetry.io/) format in the form of traces and metrics. OpenTelemetry tracing is typically used in Terragrunt to analyze performance. For more details on analyzing and optimizing performance, read the dedicated [Performance documentation](/troubleshooting/performance). ## High-level overview [Section titled “High-level overview”](#high-level-overview) The following are concepts and technologies that are important to be aware of when using OpenTelemetry with Terragrunt. * [OpenTelemetry](https://opentelemetry.io/) * [Traces](https://opentelemetry.io/docs/concepts/signals/traces/) * [Metrics](https://opentelemetry.io/docs/concepts/signals/metrics/) * [Jaeger](https://www.jaegertracing.io/) Tracing configuration: * `TG_TELEMETRY_TRACE_EXPORTER` - traces exporter type to be used. Currently supported values are: * `none` - no trace exporting, default value. * `console` - to export traces to console as JSON * `otlpHttp` - to export traces to an OpenTelemetry collector over HTTP [otlptracehttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp) * `otlpGrpc` - to export traces over gRPC [otlptracegrpc](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc) * `http` - to export traces to a custom HTTP endpoint using [otlptracehttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp) * `TG_TELEMETRY_TRACE_EXPORTER_HTTP_ENDPOINT` - in case of `http` exporter, this is the endpoint to which traces will be sent. * `TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT` - if set to true, the exporter will not validate the server’s certificate, helpful for local traces collection. * `TRACEPARENT` - if set, the value will be used as a parent trace context, format `TRACEPARENT=00---`, example: `TRACEPARENT=00-xxx-yyy-01` Metrics configuration: * `TG_TELEMETRY_METRIC_EXPORTER` - metrics exporter type to be used. Currently supported values are: * `none` - no metric exporting, default value. * `console` - write metrics to console as JSONs. * `otlpHttp` - export metrics to an OpenTelemetry collector over HTTP [otlpmetrichttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp) * `grpcHttp` - export metrics to an OpenTelemetry collector over gRPC [otlpmetricgrpc](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc) * `TG_TELEMETRY_METRIC_EXPORTER_INSECURE_ENDPOINT` - if set to true, the exporter will not validate the server’s certificate, helpful for local metrics collection. Resource attributes: * `OTEL_SERVICE_NAME` - overrides the `service.name` resource attribute attached to every span and metric. Defaults to `terragrunt`. Useful for distinguishing multiple Terragrunt invocations in the same backend (e.g. per-environment or per-user). * `OTEL_RESOURCE_ATTRIBUTES` - additional resource attributes to attach to every span and metric, formatted as a comma-separated list of `key=value` pairs (e.g. `deployment.environment=local,service.namespace=tensho`). Values set here override Terragrunt’s defaults; if both `OTEL_SERVICE_NAME` and `service.name` in `OTEL_RESOURCE_ATTRIBUTES` are set, `OTEL_SERVICE_NAME` takes precedence per the [OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/resource/sdk/). ## Example configurations for trace collection [Section titled “Example configurations for trace collection”](#example-configurations-for-trace-collection) Collection of examples how to configure Terragrunt to emit traces and metrics in OpenTelemetry format. ## Example traces collection with Jaeger [Section titled “Example traces collection with Jaeger”](#example-traces-collection-with-jaeger) * Start a Jaeger instance with docker: ```bash docker run --rm --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:1.54.0 ``` * Verify that UI is available at * Define environment variables for Terragrunt to report traces to Jaeger: ```bash export TG_TELEMETRY_TRACE_EXPORTER=http export TG_TELEMETRY_TRACE_EXPORTER_HTTP_ENDPOINT=localhost:4318 export TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT=true ``` * Run terragrunt * Verify that traces are available in Jaeger UI ## Configurations to collect traces in Grafana Tempo [Section titled “Configurations to collect traces in Grafana Tempo”](#configurations-to-collect-traces-in-grafana-tempo) * Start a Grafana Tempo instance [example](https://grafana.com/docs/tempo/latest/getting-started/docker-example/) * Define environment variables for Terragrunt to report traces to Tempo: ```bash export TG_TELEMETRY_TRACE_EXPORTER=otlpHttp # Replace with your tempo instance export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 export TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT=true ``` * Run terragrunt * Check for traces in Tempo UI for service “terragrunt” ## Example traces collection in console [Section titled “Example traces collection in console”](#example-traces-collection-in-console) * Set env variable to enable telemetry: ```bash export TG_TELEMETRY_TRACE_EXPORTER=console ``` * Run terragrunt * Check produced traces in console, like: ```json {"Name":"run_bash","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"f91587247524593b","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:30.564217484Z","EndTime":"2024-02-08T12:32:31.570666395Z","Attributes":[{"Key":"command","Value":{"Type":"STRING","Value":"bash"}},{"Key":"args","Value":{"Type":"STRING","Value":"[-c sleep 1]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}} {"Name":"parse_config_file","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"d2823047fb469bdf","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:30.380054129Z","EndTime":"2024-02-08T12:32:31.570899286Z","Attributes":[{"Key":"config_path","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2/terragrunt.hcl"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}} {"Name":"run_terraform","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"152d873a18559f07","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:31.57161757Z","EndTime":"2024-02-08T12:32:31.688157882Z","Attributes":[{"Key":"command","Value":{"Type":"STRING","Value":"tofu"}},{"Key":"args","Value":{"Type":"STRING","Value":"[init]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}} {"Name":"run_terraform","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"29341bdb65f66b1e","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:31.688240673Z","EndTime":"2024-02-08T12:32:31.793377642Z","Attributes":[{"Key":"command","Value":{"Type":"STRING","Value":"tofu"}},{"Key":"args","Value":{"Type":"STRING","Value":"[apply -auto-approve -input=false]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}} {"Name":"run_module","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"8a01522bc65e0f1b","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:30.290680776Z","EndTime":"2024-02-08T12:32:31.793392803Z","Attributes":[{"Key":"path","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}},{"Key":"terraformCommand","Value":{"Type":"STRING","Value":"apply"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}} {"Name":"run --all apply","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"00000000000000000000000000000000","SpanID":"0000000000000000","TraceFlags":"00","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:26.388519019Z","EndTime":"2024-02-08T12:32:31.793405603Z","Attributes":[{"Key":"terraformCommand","Value":{"Type":"STRING","Value":"apply"}},{"Key":"args","Value":{"Type":"STRING","Value":"[apply]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":28,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}} ``` ## Collection of metrics with OpenTelemetry collector and Prometheus [Section titled “Collection of metrics with OpenTelemetry collector and Prometheus”](#collection-of-metrics-with-opentelemetry-collector-and-prometheus) * Start OpenTelemetry collector with Prometheus receiver. Example setup through `docker-compose.yml`: ```yaml version: '3' services: otel-collector: image: otel/opentelemetry-collector:0.94.0 volumes: - ./otel-collector-config.yaml:/etc/otelcol/config.yaml ports: - "4317:4317" # OTLP gRPC receiver - "4318:4318" # OTLP HTTP receiver - "8889:8889" # Prometheus exporter prometheus: image: prom/prometheus:v2.45.3 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090" depends_on: - otel-collector ``` OpenTelemetry collector configuration `otel-collector-config.yaml`: ```yaml receivers: otlp: protocols: grpc: http: processors: batch: exporters: prometheus: endpoint: "0.0.0.0:8889" # Prometheus exporter endpoint service: pipelines: metrics: receivers: [otlp] processors: [batch] exporters: [prometheus] ``` Prometheus configuration file `prometheus.yml`: ```yaml global: scrape_interval: 15s scrape_configs: - job_name: 'opentelemetry' scrape_interval: 5s static_configs: - targets: ['otel-collector:8889'] ``` * Confirm that Prometheus is available at * Define environment variables for Terragrunt to report metrics to OpenTelemetry collector: ```bash export TG_TELEMETRY_METRIC_EXPORTER=grpcHttp export TG_TELEMETRY_METRIC_EXPORTER_INSECURE_ENDPOINT=true export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 ``` * Run terragrunt * Verify that metrics are available in Prometheus UI Example configuration to export metrics to console: * Set env variable to enable telemetry: ```bash export TG_TELEMETRY_METRIC_EXPORTER=console ``` * Run terragrunt * In output will be printed messages like: ```json {"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-41-g7185318bb11b"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.1"}}],"ScopeMetrics":[]} {"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-41-g7185318bb11b"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.1"}}],"ScopeMetrics":[{"Scope":{"Name":"terragrunt","Version":"","SchemaURL":""},"Metrics":[{"Name":"run_bash_duration","Description":"","Unit":"","Data":{"DataPoints":[{"Attributes":[{"Key":"args","Value":{"Type":"STRING","Value":"[-c sleep 2]"}},{"Key":"command","Value":{"Type":"STRING","Value":"bash"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod3"}}],"StartTime":"2024-02-12T14:38:14.85578658Z","Time":"2024-02-12T14:38:17.853165589Z","Count":1,"Bounds":[0,5,10,25,50,75,100,250,500,750,1000,2500,5000,7500,10000],"BucketCounts":[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],"Min":2005,"Max":2005,"Sum":2005}],"Temporality":"CumulativeTemporality"}},{"Name":"run_bash_success_count","Description":"","Unit":"","Data":{"DataPoints":[{"Attributes":[],"StartTime":"2024-02-12T14:38:16.860878555Z","Time":"2024-02-12T14:38:17.853169359Z","Value":1}],"Temporality":"CumulativeTemporality","IsMonotonic":true}}]}]} ``` # Performance > Learn how to improve the performance of Terragrunt ## Easy Wins [Section titled “Easy Wins”](#easy-wins) Normally, it’s best practice to start by measuring performance before making any changes. This allows you to understand the impact of your changes, and to identify areas for improvement. However, given the nature of the problems that Terragrunt solves, there are some obvious wins that you can make without measuring performance, if you’re aware of the tradeoffs. ### Provider Cache Dir [Section titled “Provider Cache Dir”](#provider-cache-dir) One of the most expensive things that OpenTofu/Terraform does, from a bandwidth and disk utilization perspective, is download and install providers. These are large binary files that are downloaded from the internet, and not cached across units by default. If you’re using OpenTofu >= 1.10 and the latest version of Terragrunt, you’ll use the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature by default. This feature automatically configures OpenTofu to use its built-in provider caching mechanism by setting the `TF_PLUGIN_CACHE_DIR` environment variable to a central location on the filesystem, allowing reuse of downloaded providers across multiple Terragrunt runs. For most users at sensible scales, this is an automatic performance win that you don’t need to do anything to enable. ### Provider Cache Dir - Gotchas [Section titled “Provider Cache Dir - Gotchas”](#provider-cache-dir---gotchas) At very large scales, you might find that the filesystem lock contention between OpenTofu processes to synchronize access to the provider cache directory is a bottleneck. You might also find that you can’t use the provider cache directory because you are storing your provider cache in a shared NFS mount or are using Terraform or an older version of OpenTofu. In these scenarios, you can use the [Provider Cache Server](/features/caching/provider-cache-server) feature to improve performance. ### Provider Cache Server [Section titled “Provider Cache Server”](#provider-cache-server) You can significantly reduce the amount of time taken by Terragrunt runs by enabling the provider cache server, like this: ```shell terragrunt run --all plan --provider-cache ``` #### Provider Cache - Gotchas [Section titled “Provider Cache - Gotchas”](#provider-cache---gotchas) The provider cache server is a single server that is used by all Terragrunt runs being performed in a given Terragrunt invocation. You will see the most benefit if you are using it in a command that will perform many OpenTofu/Terraform operations, like with the `--all` flag and the `--graph` flag. When performing individual runs, like `terragrunt plan`, the provider cache server can be a net negative to performance, because starting and stopping the server might add more overhead than just downloading the providers (or using the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature). Whether this is the case depends on many factors, including network speed, the number of providers being downloaded, and whether or not the providers are already cached in the Terragrunt provider cache. When in doubt, [measure the performance](#measuring-performance) before and after enabling the provider cache server to see if it’s a net win for your use case. ### Fetching Output From State [Section titled “Fetching Output From State”](#fetching-output-from-state) Under the hood, Terragrunt dependency blocks leverage the OpenTofu/Terraform `output -json` command to fetch outputs from one unit and leverage them in another. The OpenTofu/Terraform `output -json` command does a bit more work than simply fetching output values from state, and a significant portion of that slowdown is loading providers, which it doesn’t really need in most cases. You can significantly improve the performance of dependency blocks by using the [`dependency-fetch-output-from-state`](/reference/experiments/active#dependency-fetch-output-from-state) experiment. When the experiment is active, Terragrunt will resolve outputs by directly fetching the backend state file from S3 and parse it directly, avoiding any overhead incurred by calling the `output -json` command of OpenTofu/Terraform. For example: ```bash terragrunt run --all plan --experiment=dependency-fetch-output-from-state ``` #### Fetching Output From State - Gotchas [Section titled “Fetching Output From State - Gotchas”](#fetching-output-from-state---gotchas) The first thing you need to be aware of when considering usage of the `dependency-fetch-output-from-state` experiment is that it only works for S3 backends. If you are using a different backend, this experiment won’t do anything. Next, you should be aware that there is no guarantee that OpenTofu/Terraform will maintain the existing schema of their state files, so there is also no guarantee that the flag will work as expected in future versions of OpenTofu/Terraform. We are coordinating with the OpenTofu team to improve the performance of the `output` command, and we hope that this flag will be unnecessary for most users in the future. Additionally, this experiment is **not compatible with OpenTofu state encryption**. When client-side state encryption is enabled, the state file in S3 is encrypted before upload and cannot be parsed directly by Terragrunt, resulting in a hard failure. If you encounter JSON parsing errors when using this experiment, check whether you have OpenTofu state encryption enabled and disable the experiment with `--no-dependency-fetch-output-from-state` if so. See [#1549](https://github.com/opentofu/opentofu/issues/1549) for more details. ## Measuring Performance [Section titled “Measuring Performance”](#measuring-performance) Before diving into any particular performance optimization, it’s important to first measure performance, and to make sure that you measure performance after any changes so that you understand the impact of your changes. To measure performance, you can use multiple tools, depending on your role. ### End User [Section titled “End User”](#end-user) As an end user, you’re advised to use the following tools to get a better understanding of the performance of Terragrunt. #### OpenTelemetry [Section titled “OpenTelemetry”](#opentelemetry) Use [OpenTelemetry](/troubleshooting/open-telemetry) to collect traces from Terragrunt runs so that you can analyze the performance of individual operations when using Terragrunt. This can be useful both to identify bottlenecks in Terragrunt, and to understand when performance changes can be attributed to integrations with other tools, like OpenTofu or Terraform. #### Benchmark Usage [Section titled “Benchmark Usage”](#benchmark-usage) Use benchmarking tools like [Hyperfine](https://github.com/sharkdp/hyperfine) to run benchmarks of your Terragrunt usage to compare the performance of different versions of Terragrunt, or with different configurations. You can use configurations like the `--warmup` flag to do some warmup runs before the actual benchmarking. This is useful to get a more accurate measurement of the performance of Terragrunt with cache populated, etc. Here’s an example of how to use Hyperfine to benchmark the performance of Terragrunt with two different configurations: ```shell hyperfine -w 3 -r 5 'terragrunt run --all plan' 'terragrunt run --all plan --experiment=dependency-fetch-output-from-state' ``` ### Terragrunt Developer [Section titled “Terragrunt Developer”](#terragrunt-developer) As a Terragrunt developer, you’re advised to use the following tools to improve the performance of Terragrunt when improving the codebase. #### Benchmark Tests [Section titled “Benchmark Tests”](#benchmark-tests) Use Benchmark tests to measure the performance of particular subroutines in Terragrunt. These benchmarks give you a good indication of the performance of a particular part of Terragrunt, and can help you identify areas for improvement. You can run benchmark tests like this: ```shell go test -bench=BenchmarkSomeFunction ``` You can also run benchmarks with different configurations, like the following for getting memory allocation information as well: ```shell go test -bench=BenchmarkSomeFunction -benchmem ``` You can learn more about benchmarking in Go by reading the [official documentation](https://pkg.go.dev/testing#hdr-Benchmarks). #### Profiling [Section titled “Profiling”](#profiling) Use profiling tools like [pprof](https://github.com/google/pprof) to get a more detailed view of the performance of Terragrunt. For example, you could use the following command to profile a particular test: ```shell go test -run 'SomeTest' -cpuprofile=cpu.prof -memprofile=mem.prof ``` You can then use the `go tool pprof` command to analyze the profile data: ```shell go tool pprof cpu.prof ``` It can be helpful to use the web interface to view the profile data using flame graphs, etc. ```shell go tool pprof -http=:8080 cpu.prof ``` You can learn more about profiling in Go by reading the [official documentation](https://pkg.go.dev/cmd/pprof).