Skip to main content
New to Terragrunt? We recommend starting with the official Terragrunt quick start to learn the basics before diving into Fast Foundation’s implementation.
💡 Pro Tip: Always run terragrunt plan before apply to review changes.
Before running any Terragrunt commands in Fast Foundation, review the Gotchas section. It highlights common issues and pitfalls that are easier to avoid up front than to troubleshoot later.

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).
  • 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

Repository Structure

Infrastructure repository layout

For details about Organizational Units (OUs) and accounts, see the AWS Architecture section.
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).
As a rule of thumb: OUs start with Capital letters, while accounts use lowercase.
Within an account, resources are divided by environment and region. Inside each region you’ll find:
  • 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).
fast-foundation-infrastructure/
├─ root.hcl                            # Common Terragrunt config shared across all units
├─ _modules/                          
├─ 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.
include "root" {
  path   = find_in_parent_folders("root.hcl")
  expose = true
}
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.

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 Fundation 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

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.
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.

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:
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:
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:
{
  "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:
{
  "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.
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.

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

Avoid these practices — they can compromise your infrastructure:
  • Hardcoding values in Terraform files
  • Reusing production secrets in development

Gotchas

Mock outputs

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
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.