Multi-Cloud, Multi-Environment Terraform Architecture: Terragrunt and Real-World Strategies


In my last article, I shared a folder structure to organize Terraform code for multi-cloud and multi-env setups. It keeps things clean and scalable. But there is still a lot of repetition like writing provider.tf
, backend.tf
, and variables again and again for each environment and cloud. As the project grows, it gets hard to manage. So in this article, I will show how Terragrunt helps remove that duplication and makes the setup simpler and easier to maintain.
In this article, I will take you beyond just the folder layout, demonstrating how to create real resources across multiple environments while keeping the code DRY (Don’t Repeat Yourself). To keep things cost-effective, I will deploy minimal resources in both Azure and AWS.
Along the way, I will cover strategies for:
Reducing code duplication and complexity using tools like Terragrunt to keep your Terraform code clean, maintainable, and DRY (Don’t Repeat Yourself)
Creating isolated environments with separate Terraform state files for each environment (dev, test, prod) and each cloud provider (AWS and Azure)
Using a single Git branch to safely manage multiple environments without requiring separate branches or repositories
⚠️ Note: I will cover the CI/CD setup for this project in the next article, as this post already covers a lot and I want to keep it focused and easy to follow.
Prerequisite:
Terraform installed
TFlint installed
Terragrunt installed to manage Terraform configurations across environments
AWS CLI installed and configured (
aws configure
)Azure CLI installed and authenticated (
az login
)Basic knowledge of Terraform syntax and module usage
Familiarity with AWS services and Azure services
Link to previous blog:
Code:
1. Strategy for this Project
Instead of creating separate branches for each environment which can cause code duplication, merge conflicts, and configuration drift. I will keep all environment-specific configurations in one branch. Each environment lives in its own folder (envs/dev
, envs/test
, envs/prod
) with isolated backend and provider settings. This ensures the Terraform state for each environment is separate and protected.
Why this approach?
Single Source of Truth: Infrastructure code remains consistent across environments, reducing errors and drift.
Simplified Collaboration: Teams can work on features and fixes without juggling multiple branches.
Safe Deployments: Changes are first applied and tested in dev, then promoted to test and finally prod, minimizing risks.
Easier Automation: CI/CD pipelines can be designed to deploy changes per environment folder, making automation straightforward and reliable.
I know you might be wondering:
How to Work with Single Branch and Deploy Changes Only to Dev?
Using a single branch for all your environments might raise the question: "What if I want to apply changes only to dev without affecting test or prod?"
Here’s how that works:
All your environment-specific configurations live in separate folders:
envs/dev/
,envs/test/
, andenvs/prod/
. Each folder has its own Terraform state and variables.When you want to make a change just for dev, you edit the files inside the
envs/dev/
folder (liketerraform.tfvars
orterragrunt.hcl
), or the shared modules those envs use if applicable.Running
terraform apply
(or Terragrunt commands) only in thedev
folder will apply changes to the dev environment’s infrastructure and state, leaving test and prod untouched.Changes for
test
andprod
environments can be promoted later by running the apply commands in their respective folders only after the dev changes have been verified.Create CI/CD pipelines and deployment scripts to target specific environment folders
Why I Don’t Use Terraform Workspaces?
Terraform workspaces let you keep different environment states (like dev, test, prod) in the same folder and switch between them using commands. This sounds useful, but for this project, I don’t use them because:
Clear Separation by Folder
I keep each environment in its own folder. This way, each environment’s files and state are separate and easy to find. It helps avoid mistakes, like changing the wrong environment by accident.More Control Over Storage
Some ways to save the state (called backends) don’t work well with workspaces. When each environment has its own folder, I can set the storage details exactly for that environment.Simpler Automation
In automation tools (like CI/CD), it’s easier to run commands in separate folders than to switch workspaces. This makes the automation simpler and less error-prone.Better for Big Projects
Workspaces work well for small projects with a few environments. But when you have many environments, clouds, and teams, using folders works better and is easier to manage.
2. Why Terragrunt & Terragrunt files:
Terragrunt is a wrapper around Terraform that makes managing multiple environments and clouds much easier by:
Reducing Code Duplication: The root
root.hcl
file holds common Terraform settings like providers, backend configuration, and shared variables so you don’t repeat these in every environment folder.Hierarchical Configuration: Each environment folder has its own
terragrunt.hcl
file that includes the rootroot.hcl
and overrides or adds settings specific to that environment, such as variables.Simplified Remote State Management: Terragrunt automates backend setup, locking, and state isolation per environment, helping avoid mistakes.
Dependency Management: You can declare dependencies between modules or stacks in your
terragrunt.hcl
files, ensuring correct deployment order.Convenient CLI: Terragrunt commands wrap Terraform commands, automatically applying the right config and environment variables.
Terragrunt Configuration Files in This Project
root.hcl
(at repo root): Contains shared configurations used by all environments, such as default provider settings, common variables, and a generic backend configuration.terragrunt.hcl
(in each env folder): These files inherit fromroot.hcl
and override specific settings per environment or cloud, like variable values or module inputs..tflint.hcl
: This file configures TFLint, a tool that helps catch errors and enforces best practices in Terraform code.
3. Folder Structure with Terragrunt
Integrating Terragrunt and modifying previous file structure by replacing backend.tf and providers.tf with terragrunt.hcl files:
multi-cloud-terraform/
├── aws/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│
├── azure/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│
├── modules/
│ ├── aws/
│ │ ├── compute/
│ │ │ ├── main.tf
│ │ │ ├── variables.tf
│ │ │ └── outputs.tf
│ └── azure/
│ └── storage/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── envs/
│ ├── dev/
│ │ ├── aws/
│ │ │ └── terragrunt.hcl
│ │ │
│ │ └── azure/
│ │ └── terragrunt.hcl
│ │
│ ├── test/
│ │ ├── aws/
│ │ │ └── terragrunt.hcl
│ │ │
│ │ └── azure/
│ │ └── terragrunt.hcl
│ │
│ └── prod/
│ ├── aws/
│ │ └── terragrunt.hcl
│ │
│ └── azure/
│ └── terragrunt.hcl
├── scripts/
│ ├── deploy.sh
│ ├── validate.sh
│ └── plan.sh
├── root.hcl
├── .tflint.hcl
├── .pre-commit-config.yaml
└── README.md
What each folder and file is for:
modules/: Reusable Terraform modules organized by cloud (
aws/
,azure/
).envs/: Environment-specific configurations per cloud (e.g.,
dev/aws/
,prod/azure/
), containsterragrunt.hcl
andterraform.tfvars
files for each environment-cloud combination.scripts/: Helper scripts for deployment, validation, planning, and automation.
terragrunt.hcl: Root Terragrunt configuration with shared/common settings.
.tflint.hcl: Configuration for Terraform linter to enforce best practices.
.pre-commit-config.yaml: Configuration for pre-commit hooks to run automated checks before code commits.
readme.md: Documentation including project overview, setup instructions, and usage guidelines.
4. How Terragrunt Works?
After understanding the folder structure, the real power of Terragrunt comes from how it eliminates repetition using a concept called configuration inheritance.
Let’s break this down with an example to see how Terragrunt helps manage shared configuration (like backends and providers) and environment-specific settings cleanly.
Root Terragrunt Configuration (root.hcl
)
The root-level file is placed at the root of repository. It contains:
Remote backend configuration
Provider configuration
Common variables used across environments
Tags or metadata (e.g., project name, owner)
Here’s an example of a root root.hcl
file for AWS:
remote_state {
backend = "s3"
config = {
bucket = "multi-cloud-multi-env-bucket" # Note: I am using the latest version of Terraform,
key = "${local.environment}/shared.tfstate" # which no longer requires using DynamoDB
region = local.default_aws_region # for state locking.
use_lockfile = true
}
}
generate "aws_provider" {
path = "provider_aws.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.default_aws_region}"
}
EOF
}
generate "azure_provider" {
path = "provider_azure.tf"
if_exists = "overwrite"
contents = <<EOF
provider "azurerm" {
features {}
subscription_id = "${local.azure_subscription_id}"
}
EOF
}
5. Running Terragrunt in Each Environment
Now that we have folder structure, modules, and Terragrunt configuration set up, here is how you can run Terraform using Terragrunt for each environment and cloud provider.
Add Your AWS Account ID and Azure subscription ID in root.hcl
Before running anything, make sure you have added your AWS Account ID and Azure Subscription ID in the locals
block of your root.hcl
.
Example snippet for root.hcl
locals:
locals {
project_name = "my-multi-cloud-project"
default_aws_region = "us-east-1"
default_azure_region = "eastus"
aws_account_id = "aws-account-id"
azure_subscription_id = "azure-subscription-id"
common_tags = {
Project = local.project_name
ManagedBy = "Terragrunt"
}
path_parts = split("/", replace(path_relative_to_include(), "envs/", ""))
environment = length(local.path_parts) > 0 ? local.path_parts[0] : "dev"
provider = length(local.path_parts) > 1 ? local.path_parts[1] : "aws"
}
Running Terragrunt Commands Per Environment and Cloud
Each environment (dev, test, prod) has its own folder under envs/
, and within each environment, separate folders for AWS and Azure exist. To work on a particular environment and cloud, navigate into the corresponding folder and run Terragrunt commands from there.
For example, to plan and apply infrastructure changes in Dev environment on AWS:
cd envs/dev/aws
terragrunt plan
terragrunt apply
Similarly, for Prod environment on Azure:
cd envs/prod/azure
terragrunt plan
terragrunt apply
After executing terragrunt commnds in all the environment below files got created in S3 backend:
Automation and Scripting
You can also automate this by creating simple shell scripts (like the ones in my scripts/
folder) to run Terragrunt commands for specific environments, helping reduce human errors.
TFlint warning
tflint is a Terraform linter tool used to analyze your Terraform code for potential errors, best practices, and policy violations before you apply your infrastructure changes. It helps catch issues early, improving code quality and preventing runtime errors.
In this case, when running tflint, the following warnings appeared in my Terraform code:
Missing
required_version
attribute:
Terraform recommends specifying arequired_version
block to define the Terraform version your code is compatible with. This helps ensure consistency across environments.Missing version constraint for provider
azurerm
:
Your Terraform configuration uses theazurerm
provider, but there is no version constraint specified underrequired_providers
. Defining provider versions helps avoid unexpected breaking changes when providers update.
After running terragrunt apply
command in test folder one and terragrunt destroy
in prod folder
Thank you for taking time to read my article. If I have overlooked any steps or missed any details, please don't hesitate to get in touch.
Feel free to reach out to me anytime Contact me
~ Palak Bhawsar ✨
Subscribe to my newsletter
Read articles from Palak Bhawsar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Palak Bhawsar
Palak Bhawsar
DevOps Engineer with 4+ years of experience in cloud automation, CI/CD, and infrastructure optimisation, I specialise in streamlining deployments, enhancing system resilience, and reducing operational costs.