> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fastfoundation.nimble.la/llms.txt
> Use this file to discover all available pages before exploring further.

# Terragrunt Guide

> Learn how Fast Foundation uses Terragrunt, including our custom before-hooks, secrets management system, and Fast Foundation-specific best practices

<Note>
  **New to Terragrunt?** We recommend starting with the <a href="https://terragrunt.gruntwork.io/docs/getting-started/quick-start" target="_blank" rel="noopener">official Terragrunt quick start</a> to learn the basics before diving into Fast Foundation’s implementation.
</Note>

<Note>
  💡 **Pro Tip**: Always run `terragrunt plan` before `apply` to review changes.
</Note>

<Warning>
  Before running any Terragrunt commands in Fast Foundation, review the [Gotchas section](#gotchas). It highlights common issues and pitfalls that are easier to avoid up front than to troubleshoot later.
</Warning>

# Overview

Fast Foundation uses **Terragrunt** as the orchestration layer for Terraform. Terragrunt provides configuration management, dependency handling, and integrates our **secrets management system** that keeps infrastructure secrets **consistent across environments**.

***

## Why Terraform + Terragrunt?

**Terraform** defines *what* to create (resource definitions).\
**Terragrunt** defines *how* to orchestrate it (configuration, dependencies, consistency checks).

<AccordionGroup>
  <Accordion title="Benefits in Fast Foundation">
    * **DRY principles** – Avoid repeating configuration across environments
    * **Dependency management** – Clear ordering of infrastructure components
    * **Remote state** – Centralized state using S3 (Simple Storage Service) and DynamoDB locks
    * **Environment promotion** – Move configurations cleanly between dev/staging/prod
    * **Custom secrets management** – Our system keeps secrets secure in SSM Parameter Store
  </Accordion>
</AccordionGroup>

***

## Repository Structure

### Infrastructure repository layout

<Note>
  For details about Organizational Units (OUs) and accounts, see the [**AWS Architecture**](/introduction-key-concepts/1-aws-architecture#organizational-units-ous-and-accounts) section.
</Note>

The repository mirrors your AWS Organization. At the top level you’ll see **Organizational Units (OUs)**; accounts live one or two levels deeper.

For example, to reach the **Workload Development** account, follow: **Workloads (OU) → Development (OU) → Workload Development (account)**.

<Note>
  As a rule of thumb: **OUs** start with **Capital letters**, while **accounts** use **lowercase**.
</Note>

Within an account, resources are **divided by environment and region**. Inside each **region** you’ll find:

* [shared config files](#shared-config-files), like `_account.hcl`, `_environment.hcl`,`_region.hcl`,`_service.hcl`, to set account, environment, region or service level configurations.
* resource types or groups folders (e.g., `ec2`, `clusters (eks/ecs)`, `lambda`, etc.)
* each resource unit contains a **Terragrunt file** (the “unit”) and an **inputs file** (the parameters).

```text theme={null}
fast-foundation-infrastructure/
├─ root.hcl                            # Common Terragrunt config shared across all units
├─ Infrastructure/                     # OU
├─ Networking/                         # OU
├─ Organization Management/            # Account
├─ Security/                           # OU
└─ Workloads/                          # OU
   └─ Development/                     # OU
      └─ Workload Development/    # Account
         ├─ _account.hcl               # Account-level configuration
         └─ development/
            ├─ _environment.hcl        # Environment-level configuration
            ├─ global/                 # Global resources in this env/account
            └─ us-east-1/              # Regional resources
               ├─ _region.hcl          # Region-level configuration
               └─ ec2/
                  └─ _service.hcl       # Service-level configuration
                  └─ my_ec2/
                     ├─ inputs.hcl      # Input parameters for this unit
                     └─ terragrunt.hcl  # Terragrunt unit (invokes the Terraform module)
```

## Shared Config Files

Fast Foundation relies on shared configuration files at different levels of the hierarchy. These files let you define variables once and automatically inherit them in all child units.

* **`root.hcl`** – global project-wide configuration (backend, provider setup, hooks)
* **`_account.hcl`** – account-specific settings like account IDs, tags, IAM roles
* **`_environment.hcl`** – environment-wide values like environment name, prefixes, or SSO roles
* **`_region.hcl`** – region-specific settings such as availability zones or region tags
* **`_service.hcl`** – service-specific settings such as a common security group rule for all ec2 instances

You bring these into a unit’s `terragrunt.hcl` using include blocks, which makes shared variables directly available in your local unit without redefining them.

```hcl theme={null}
include "root" {
  path   = find_in_parent_folders("root.hcl")
  expose = true
}
```

<Check>
  The `root.hcl` already imports shared configuration variables. Including `root` is enough to access variables in `_account.hcl`, `_environment.hcl`,`_region.hcl` and `_service.hcl` files.

  You can easily access variables with `include.root.account_vars`, `include.root.environment_vars`, `include.root.region_vars`,`include.root.service_vars`, inside your unit configuration.
</Check>

### What lives where?

* **`terragrunt.hcl`** – The “unit” file. It **invokes** a Terraform module, wires dependencies, configures remote state, and registers hooks.
* **`inputs.hcl`** – The **parameters** for that unit (names, ARNs, sizes, tags, etc.). These inputs should not contain hardcoded sensitive information since they are commited to Git. For sensitive data make reference to secrets stored in SSM Parameter Store.

This separation lets you reuse modules safely while keeping sensitive or mutable **inputs** out of the repository and **in sync across the team**.

***

## Secrets management – How sensitive information stays secure and consistent

All the secret parameters used in the units are not stored locally or pushed to any repository. Instead, Fast Foundation stores all of them in SSM Parameter Store as JSON, and provides an automated **secrets management** layer that replaces secrets references in the `inputs.hcl` file with the actual values stored in AWS.

This is what the **Secrets Management Automation** does for you:

* **On `init`**: Automatically creates the secret parameter in SSM Parameter Store if it does not already exist (it is created as an empty JSON object).
* **On `plan` & `apply`**: Replaces the references to the secrets in the `inputs.hcl` file with the actual secret values stored in SSM Parameter Store.

***

## Secrets Management

### Before-Hooks

<Note>
  In Fast Foundation, **before-hooks are already part of the standard Terragrunt setup**. You only need to add them if you’re creating a brand-new unit from scratch.
</Note>

Terragrunt **before-hooks** are commands or scripts that run **before** specific Terragrunt commands (e.g., `init`, `plan`, `apply`) to enforce prerequisites and prevent mistakes.

#### Why they matter

* ✅ Ensure AWS SSO sessions are valid before you run anything
* ✅ Make use of secrets stored in SSM Parameter Store when running a `plan` or `apply`
* ✅ Run validations (formatting, required files present, etc.) automatically

These hooks are the mechanism that powers the [secrets management system](#secrets-management-–-how-sensitive-information-stays-secure-and-consistent).

***

### What to add to your Unit if it contains sensitive inputs

If you are creating a new unit, and that unit has sensitive inputs that you don't want to commit into Git, you will need to add some blocks to its `terragrunt.hcl` file.\
The secrets-management scripts are already included in your repository.

You need to add the `before_hook` and `after_hook` sections, and the **locals parameters** described below to your `terragrunt.hcl`:

```hcl theme={null}
terraform {
  source = "..."

  before_hook "ensure_ssm_parameter_exists" {
    commands     = ["init"]
    execute      = [
      "bash", "-c", <<-EOT
        # Only run if SSM parameter is enabled
        if [ "${local.enable_ssm_parameter}" = "true" ]; then
          "${get_repo_root()}/_scripts/ensure-ssm-parameter.sh" \
            "/${path_relative_to_include("root")}" \
            "${include.root.locals.project_name}-${include.root.locals.account_vars.locals.account_name}" \
            "${include.root.locals.region_vars.locals.region}"
        fi
      EOT
    ]
  }

  after_hook "delete_resolved_inputs_file" {
    commands = ["plan", "apply", "destroy"]
    execute = [
      "bash", "-c", <<-EOT
        [ -f "${get_terragrunt_dir()}/inputs.resolved.hcl" ] && rm -f "${get_terragrunt_dir()}/inputs.resolved.hcl"
      EOT
    ]
  }
}

locals {
  enable_ssm_parameter = try(read_terragrunt_config("${get_terragrunt_dir()}/inputs.hcl").locals.enable_ssm_parameter, false)
  
  # Fetch SSM parameter (JSON format) and resolve ssm:// references in inputs.hcl
  resolved_inputs_file = run_cmd(
    "--terragrunt-quiet",
    "bash", "-c",
    <<-EOT
      if [[ "${local.enable_ssm_parameter}" = "true" && "${get_terraform_command()}" != "init" ]]; then
        TEMP_FILE="${get_terragrunt_dir()}/inputs.resolved.hcl"
        RESOLVE_SCRIPT="${get_repo_root()}/_scripts/resolve-ssm-refs.sh"
        INPUTS_FILE="${get_terragrunt_dir()}/inputs.hcl"
        SSM_PARAM_NAME="/${path_relative_to_include("root")}"
        AWS_PROFILE="${include.root.locals.project_name}-${include.root.locals.account_vars.locals.account_name}"
        AWS_REGION="${local.region_vars.region}"
        
        if ! "$RESOLVE_SCRIPT" "$INPUTS_FILE" "$SSM_PARAM_NAME" "$AWS_PROFILE" "$AWS_REGION" "$TEMP_FILE"; then
          exit 1
        fi
        echo "$TEMP_FILE"
      else
        echo "${get_terragrunt_dir()}/inputs.hcl"
      fi
    EOT
  )
  
  # Parse the resolved inputs file (with ssm:// references replaced if parameters are enabled)
  inputs = try(read_terragrunt_config(local.resolved_inputs_file).locals, {})
}
```

Then, you need to add an `enable_ssm_parameter` flag into your `inputs.hcl` file, and make reference to the secret parameters appending the prefix `ssm://` to the name of the key to reference in the secret stored in SSM Parameter Store:

```hcl theme={null}
locals {
  enable_ssm_parameter = true

  ## Example usage
  secret_token = "ssm://SECRET_TOKEN"
}

```

For the example above to work, the secret stored in SSM Parameter Store (in JSON format) should be:

```json theme={null}
{
  "SECRET_TOKEN": "my_secret_token_value"
}
```

***

### Behavior

Once the necessary changes are made to the `terragrunt.hcl` file and the `enable_ssm_parameter` flag is set to true in the `inputs.hcl` file, run `terragrunt init`.
This will create (if it does not already exist) a parameter in SSM Parameter Store. This parameter is stored in the same AWS Account that the Unit is included into, and its name will be the path to the Unit.
For example, if the parameter is created for the OpenSearch cluster located in `Security/Production/log-archive-production/production/us-east-1/logging/opensearch/fast-foundation-logging`, the parameter will live
in the `log-archive-production` account with the name `Security/Production/log-archive-production/production/us-east-1/logging/opensearch/fast-foundation-logging`.

The parameter is created as an empty JSON object. You will have to manually access the parameter and populate it with your sensitive information as key-value pairs:

```json theme={null}
{
  "SECRET_ONE": "first-secret-value",
  "SECRET_TWO": "second-secret-value",
  ...
}
```

When running terragrunt `plan` or `apply`, the secrets management script will replace the references made to the secret keys in the `inputs.hcl` file with the actual secret values. It will create a temporary
`inputs.resolved.hcl` file for this purpose that is automatically deleted once the command has finished running.

<Warning>
  Do not try to manually edit the contents of the `inputs.resolved.hcl`. This won't change the parameter values since the file is recreated on every terragrunt `plan` & `apply`.
</Warning>

***

## Best Practices

### Use unit folder names consistently

If possible, use the **folder name as the resource name**. This keeps the repository intuitive: the folder you are working in directly reflects the deployed resource.

* `ec2/my_app_server/` → contains inputs/terragrunt for `my_app_server` EC2 instance
* `eks/my_cluster/` → contains inputs/terragrunt for your `my_cluster` EKS cluster

This ensures resources are **discoverable, reusable, and follow consistent naming conventions**.

### Secrets Management

<Danger>
  Avoid these practices — they can compromise your infrastructure:

  * Hardcoding values in Terraform files
  * Reusing production secrets in development
</Danger>

***

## Gotchas

### Mock outputs

<Note>
  Mock outputs let `terragrunt init` and `terragrunt plan` run even when the dependency hasn’t been created yet. They’re placeholders — not the real values.
  For additional information, visit the [**official Terragrunt documentation**](https://terragrunt.gruntwork.io/docs/features/stacks/#unapplied-dependency-and-mock-outputs)
</Note>

In Terragrunt, **units can depend on outputs from other units**. For example: an EC2 unit may require the **VPC ID** exported from a networking unit.

But what happens if the dependency hasn’t been applied yet? That’s where **mock outputs** come in.

This helps during **initial setup** (so you don’t get a wall of errors before anything exists). However, it’s important to understand the trade-off:

* ✅ **Plans succeed early** because placeholders exist
* ⚠️ **Applies may fail later** if the real resource doesn’t exist yet

***

#### Key things to remember

* If something **works on plan but fails on apply** with errors like *“resource doesn’t exist”*, it probably used **mock outputs** during planning
* Always check whether dependencies (like VPCs, subnets, IAM roles) are actually applied before trusting a green `terragrunt plan`

***

### State lock issues

Terragrunt and Terraform use **DynamoDB locks** to prevent multiple people from applying changes at the same time on the same unit. If you see a state lock error, it usually means someone else is already running a deployment.

**What to do:**

* ⏳ Wait for the other deployment to finish.
* ✅ Verify you’re not overwriting someone else’s changes.
* 🚀 Apply your changes once you’re sure everything is okay.

***
