---
title: Units
description: Learn how Terragrunt units result in atomic deployments and immutable infrastructure.
slug: features/units
sidebar:
  label: Overview
  order: 1
---

import FileTree from '@components/vendored/starlight/FileTree.astro';
import { Aside } from '@astrojs/starlight/components';

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):

<FileTree>

- 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

</FileTree>

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

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

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`:

<FileTree>

- modules
  - app
    - main.tf
  - mysql
    - main.tf
  - vpc
    - main.tf

</FileTree>

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:

```hcl
# variables.tf
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:

<FileTree>

- 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

</FileTree>

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:

```hcl
# terragrunt.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"
}
```

<Aside type="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.
</Aside>

And `prod/app/terragrunt.hcl` may look like this:

```hcl
# terragrunt.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

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

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

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

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

<Aside type="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!
</Aside>

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:

    ``` hcl
    # terragrunt.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

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:

``` hcl
# terragrunt.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.

<Aside type="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
```
</Aside>

## 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:

<FileTree>

- 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

</FileTree>

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`:

```hcl
# prod/env.hcl
generate "provider" {
  path = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
provider "aws" {
  assume_role {
    role_arn = "arn:aws:iam::0123456789:role/terragrunt"
  }
}
EOF
}
```

This instructs Terragrunt to create the file `provider.tf` in the working directory where Terragrunt calls `tofu`/`terraform`
before it runs any of the OpenTofu/Terraform commands (e.g `plan`, `apply`, `validate`, etc). This allows you to inject this
provider configuration for any unit that includes the `env.hcl` file.
To include this in the child configurations (e.g `app/terragrunt.hcl`), you would update all the units to
include this configuration using the `include` block:

```hcl
# prod/app/terragrunt.hcl
include "env" {
  path = find_in_parent_folders("env.hcl")
}
```

The `include` block tells Terragrunt to use the exact same Terragrunt configuration from the `env.hcl` file
specified via the `path` parameter. It behaves exactly as if you had copy/pasted the OpenTofu/Terraform configuration from the
included file `generate` configuration into the child, but this approach is much easier to maintain!

## Further Reading

- [State Backend](/features/units/state-backend) — configure remote state for your units
- [Includes](/features/units/includes) — share common configuration across units
- [Extra Arguments](/features/units/extra-arguments) — pass additional CLI arguments to OpenTofu/Terraform
- [Hooks](/features/units/hooks) — run custom commands before or after OpenTofu/Terraform
- [Authentication](/features/units/authentication) — work with multiple AWS accounts and dynamic provider authentication
- [`terraform` block reference](/reference/hcl/blocks/#terraform) — full reference for the `terraform` block
- [Lock File Handling](/reference/lock-files) — details on how Terragrunt manages `.terraform.lock.hcl` files
