# Using Terragrunt to deploy to Azure

I finally had the chance to play with Terragrunt. Unfortunately, I didn't find much content for using Terragrunt for Azure deployments. So, here it is. The guide I wish I had.

# What this guide is not

* An introduction to Terraform
    
* An introduction to Terragrunt
    
* An introduction to Azure
    

Instead, this guide will cover how to set up an opinionated Terragrunt "live" repository so that you can deploy across multiple subscriptions, regions, resource groups, and application environments.

If you're looking for an overview of Terraform and how to use it with Azure, [there's a great lab by HashiCorp that's free](https://learn.hashicorp.com/collections/terraform/azure-get-started).

If you're looking for an intro to Terragrunt, the [Terragrunt docs](https://terragrunt.gruntwork.io/docs/getting-started/quick-start/) is a great place to start.

📕 Also, Terragrunt makes it easier to implement some of the ideas called out in the [Terraform Up and Running](https://www.amazon.com/Terraform-Running-Writing-Infrastructure-Code/dp/1492046906/ref=asc_df_1492046906/?tag=hyprod-20&linkCode=df0&hvadid=385609353592&hvpos=&hvnetw=g&hvrand=3123620425578758399&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=9011814&hvtargid=pla-556939758690&psc=1&tag=&ref=&adgrpid=77500930054&hvpone=&hvptwo=&hvadid=385609353592&hvpos=&hvnetw=g&hvrand=3123620425578758399&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=9011814&hvtargid=pla-556939758690) book.

# The demo

Here's a repository I used to create a demo to deploy to Azure.

[![fgauna12/terragrunt-azure-example - GitHub](https://gh-card.dev/repos/fgauna12/terragrunt-azure-example.svg?fullname= align="left")](https://github.com/fgauna12/terragrunt-azure-example)

[Here's a backup link](https://github.com/fgauna12/terragrunt-azure-example) in case the fancy card stops working 👆🏽.

The directory structure looks like this.

```plaintext
 .
├── terragrunt.hcl
└── vs-enterprise 
    ├── staging 
    │   ├── eastus
    │   │   ├── region.hcl
    │   │   └── spoke-vnet
    │   │       └── terragrunt.hcl
    │   ├── env.hcl
    │   ├── global
    │   │   ├── region.hcl
    │   │   └── resource_groups
    │   │       ├── terragrunt.hcl
    │   └── westus
    │       ├── region.hcl
    │       └── spoke-vnet
    │           └── terragrunt.hcl
    └── subscription.hcl
```

This example is heavily inspired by the [AWS terragrunt demo](https://github.com/gruntwork-io/terragrunt-infrastructure-live-example).

So, if you want to follow along, *clone the repository*, then I'll walk you through customizing to your scenario.

# Okay, about the file structure

For me, the folder/file structure is one of the most appealing aspects of Terragrunt. I like that someone has put thought into and shared their experience on promoting Terraform across multiple environments for many large-scale applications.

* `terragrunt.hcl` - We'll talk about this last
    
* `vs-enterprise` - A folder for my Visual Studio Enterprise subscription
    
* `vs-enterprise/staging` - A folder for my `staging` environment
    
* `vs-enterprise/staging/eastus` - A folder for resources in the East US region
    

Let's walk through all the folders.

## First, the subscriptions 🔑

Some organizations have separate subscriptions per workload and per environment. Some organizations just have a "non-production" and a "production subscription."

Therefore, you will want to create as many folders as you have subscriptions. As you'll see later, this will help you switch subscriptions by simply changing the directory from which you run `terragrunt apply`. I like it because it is more obvious to see which subscription I am deploying to.

So, if you have a "non-production" and "production," then you're going to have these folders in your repository.

```plaintext
.
├── azsub-non-production
└── azsub-production
```

If you have a more complex subscription strategy, like subscription per app and environment, then you're going to have more folders.

```plaintext
.
├── azsub-fabrikam-app-dev
├── azsub-fabrikam-app-prod
└── azsub-fabrikam-app-test
```

**Please note:** I pretend to use a fancy naming convention for my subscription. If you have messy subscription names, I think it's ok to have messy subscription folders.

Lastly, you'll want to create a `subscription.hcl` file in each subscription folder. In here, add the following contents. Make sure to add the `subscription_id` to each subscription file.

```plaintext
# vs-enterprise/subscription.hcl
locals {
    subscription_id = "<< your subscription id >>"
}
```

## Next, the environment 📦

Like always, it depends on how many environments your applications will have. You could have just "staging" or "production" environments, or you could have "dev", "test", "prod."

I create a `shared` or `management` folder for resources that don't necessarily correspond to an environment. For example, you might have a Log Analytics workspace that's shared across *all* non-prod environments.

In my example, I am using a single environment - **staging**.

But then, in each environment folder, create an `env.hcl` file. Make sure to change the `environment` for each `env.hcl` file.

```plaintext
# vs-enterprise/staging/env.hcl
locals {
  environment = "staging"
}
```

## The region 🌎

Depending on your architecture, create a folder for each region. I make a `global` folder for resources that don't necessarily correspond to a region.

Examples of global resources are resource groups, Azure Front Door, Azure DNS, Azure Traffic Manager, Log Analytics workspace, etc. If you're a person that doesn't like to have resources from different regions inside of the same resource group, then you don't have to consider it a "global" resource.

For my example, if I am deploying to *multiple* regions, then my folders could be:

* East - A spoke virtual network
    
* West - Another spoke virtual network
    
* Global - Create the resource groups necessary for the virtual networks. More on dependencies later.
    

In each region folder, create a `region.hcl` file.

```plaintext
# vs-enterprise/staging/eastus/region.hcl
locals {
  location = "eastus"
}
```

But what about the `global` folder? You'll still want to set a region. Most Azure global resources still require that you pick a region, for example, resource groups and Azure WAN. So, pick a "default" region for all of these global resources.

## Finally, the Terragrunt file 📄

You will have to break up your system architecture into smaller pieces. This is one of the strengths of Terraform/Terragrunt. So, find "seams" in your architecture where you break things up into "modules." This takes some experience and practice and time.

Once you have the modules, you create the Terragrunt configuration. So, in my example, I will be deploying a virtual network.

So, I created a directory called `spoke-vnet` under the East and West US regions. Then, I created a `Terragrunt.hcl` file.

```plaintext
# vs-enterprise/staging/eastus/spoke-vnet/terragrunt.hcl
terraform {
  source = "tfr:///Azure/vnet/azurerm//?version=2.6.0"
}

include {
  path = find_in_parent_folders()
}

dependencies {
  paths = ["../../global/resource_groups"]
}

dependency "resource_groups" {
  config_path = "../../global/resource_groups"

  mock_outputs = {
    vnet_resource_group_name = "rg-terragrunt-mock-001"
  }
  mock_outputs_merge_with_state = true
}

locals {
  env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  environment = local.env_vars.locals.environment

  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
  location = local.region_vars.locals.location
}

inputs = {
  vnet_name           = "vnet-spoke-${local.environment}-${local.location}-001"
  resource_group_name = dependency.resource_groups.outputs.vnet_resource_group_name
  address_space       = ["10.0.0.0/16"]
  subnet_prefixes     = ["10.0.1.0/26", "10.0.2.0/24", "10.0.3.0/24", "10.0.4.0/24"]
  subnet_names        = ["AzureBastionSubnet", "Management", "Tools", "Workloads"]
  location            = local.location

  tags = {
    environment = local.environment
  }
}
```

Then, I created another `Terragrunt.hcl` file in my `westus` folder. The contents are mostly the same with the exception of the address spaces (`inputs.address_space` and `inputs.subnet_prefixes`).

A couple of important things to call out:

* **The source** - Under the `terraform` block, there's the opportunity to configure the source of the Terraform module. In my example, I use a public Terraform module to create a virtual network.
    
* **The include** - We're saying to include the **first** `terragrunt.hcl` file in the parent directories. Because our other Terragrunt files like `subscription.hcl` don't follow the default name, they will be ignored with this statement. Later, we'll create a `terragrunt.hcl` file at the repository's root. 😊 When we create a `terragrunt.hcl` file in the root of the repository, then this `include` statement will discover it.
    
* **The inputs** - The inputs are the input variables passed into the module. Notice how I am using `locals` to build some inputs.
    
* **The locals** - Much like proper Terraform, you can have locals. These variables are truly local to the file. <mark>You won't be able to consume </mark> `locals` defined in the parent directories like the `subscription.hcl` or `env.hcl`. However, you can *force* this behavior by loading them explicitly:
    

```plaintext
locals {
    env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
    environment = local.env_vars.locals.environment
}
```

* **The dependencies** - Remember how we never created a resource group for our virtual network? Well, we want to use Terraform to create the resource group as well. <mark>The problem is that we can't write proper Terraform inside of a Terragrunt configuration.</mark> We *could* create a `resource_group.tf` next to the `terragrunt.hcl` file and the resource group would be created, but there's no way to refer to the attributes of that resource or do a `depends_on` from Terragrunt file to Terraform configuration. Instead, the annoying part is that we'll have to create a separate module for just the resource groups to develop an inter-module dependency. Notice that there are two kinds of blocks: `dependencies` and `dependency` blocks The `dependencies` block is only required to specify the order of dependencies for when you use `terragrunt run-all`. The `dependency` block declares the actual dependency and refers to outputs from the depending module. For example, to get the name of the resource group that hasn't been created, we refer to the resource group module by calling `dependency.resource_groups.outputs.vnet_resource_group_name`.
    

## Alright, fine, let's create the `resource-group` module. 😬

Create `resource-groups` folder. I placed mine under the `global` region. Here, you will define the resource groups that you want to create and deploy your infrastructure into.

**Note:** This pattern of creating a module <mark>also applied to me when I wanted to use </mark> `data` blocks since you can't use `data` blocks in your Terragrunt configuration. In this example, I am not using data elements.

```plaintext
# vs-enterprise/staging/global/resource_groups/resource_group.tf
resource "azurerm_resource_group" "vnet_resource_group" {
  name     = var.vnet_resource_group_name
  location = var.location
}
```

Then you need some variables.

```plaintext
# vs-enterprise/staging/global/resource_groups/variables.tf
variable "location" {
  type  = string
  description = "The Azure Region to use"
}

variable vnet_resource_group_name {
  type        = string
  description = "The name of the vnet resource group"
}
```

And, some outputs so that you can create inter-module dependencies.

```plaintext
# vs-enterprise/staging/global/resource_groups/outputs.tf
output vnet_resource_group_name {
  value = var.vnet_resource_group_name
  depends_on = [azurerm_resource_group.vnet_resource_group]
}
```

Lastly, a Terragrunt configuration file.

```plaintext
# vs-enterprise/staging/global/resource_groups/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

inputs = {
  vnet_resource_group_name = "rg-example-vnet"
}
```

# Dude, what about the backend? And... the provider?

Yes, I know. We haven't talked about where the Terraform state will be stored. Bad news. This part is a little complicated.

To define a Terraform backend, we *could* go into each `terragrunt.hcl` file for each component, environment, subscription, and region. But, that would mean copying and pasting code. Instead, <mark>we could define the backend configuration in one place.</mark>

As promised, remember this from our `terragrunt.hcl` file?

```plaintext
# vs-enterprise/staging/eastus/spoke-vnet/terragrunt.hcl
include {
  path = find_in_parent_folders()
}
```

Well, Terragrunt will be looking for another `terragrunt.hcl` file in the parent directories. So let's define it at the *root* of your repository.

```plaintext
# terragrunt.hcl
locals {
  # Automatically load subscription variables
  subscription_vars = read_terragrunt_config(find_in_parent_folders("subscription.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Automatically load environment-level variables
  environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  
  location              = local.region_vars.locals.location
  environment       = local.environment_vars.locals.environment
  subscription_id   = local.subscription_vars.locals.subscription_id
}

# Generate Azure providers
generate "versions" {
  path      = "versions_override.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
    terraform {
      required_providers {
        azurerm = {
          source = "hashicorp/azurerm"
          version = "2.95.0"
        }
      }
    }

    provider "azurerm" {
        features {}
        subscription_id = "${local.subscription_id}"
    }
EOF
}

remote_state {
    backend = "azurerm"
    config = {
        subscription_id = "${local.subscription_id}"
        key = "${path_relative_to_include()}/terraform.tfstate"
        resource_group_name = "rg-terragrunt-example-001"
        storage_account_name = "stterragruntexample001"
        container_name = "environment-states"
    }
    generate = {
        path      = "backend.tf"
        if_exists = "overwrite_terragrunt"
    }
}

# Configure root level variables that all resources can inherit. This is especially helpful with multi-subscription configs
# where terraform_remote_state data sources are placed directly into the modules.
inputs = merge(
  local.subscription_vars.locals,
  local.region_vars.locals,
  local.environment_vars.locals
)
```

Did you shed a tear? 😢 Yes, it's ugly and it looks complicated. I'll walk you through it.

* **The locals** - The key to understanding the `locals` is the context where this `terragrunt.hcl` file will be run from. It **always** runs from the context of the leaf folder (the module folder). So, when we deploy our `spoke-vnet` module with Terragrunt, it will load the parent `terragrunt.hcl` file. Then, it will try to load *subscription* locals from a Terragrunt config file called `subscription.hcl` located in one of the parent directories.
    
* **The generate block** - Essentially, you will generate a block to specify the provider for all the leaf Terraform modules. This means one place to update the provider version! Notice how you can use `local` references to select which subscription you want to deploy to. Depending on which folder the "leaf folder" is located in, it's going to use a different subscription.
    
* **The remote state** - Here we tell Terragrunt to generate backend configuration and place it in the `backend.tf` file for each leaf folder.
    
* **Input magic** - Remember how we were adding `locals` blocks in the `subscription.hcl` and other non-default Terragrunt files? Essentially, we will be reading all of these local variables and merging them as inputs. So, let's say that *all* of your Terraform modules had an input variable of `location` for the Azure region. We won't have to specify `location = eastus` a million times with this input magic.
    

Almost there! 🙏🏾 We just need a few things on the Azure side.

# Wait ✋🏽, some prep work

We defined a storage account for our backend and we haven't created it yet.

First, create a resource group for the Terraform backend.

* Name: `rg-terragrunt-example-001`
    
* Location: `eastus`
    

Next, create the storage account that will hold the Terraform backend. You will also need a storage account container.

* Name: `stterragruntexample001`
    
* Location: `eastus`
    
* Redunancy: `LRS`
    
* Container Name: `environment-states`
    

Also, remember that our code is creating the resource group for the virtual network? Well, if we do a `terragrunt run-all plan`, it will fail since no resource groups exist yet. So, let's create a "mock" resource group.

* Name: `rg-terragrunt-mock-001`
    
* Location: `eastus`
    

In reality, this "mocking" resource group will only be used when new infrastructure is being created for the first time. This is unique to Azure because, in AWS, resource groups are not required to create resources. This is also unique to Terragrunt for Azure because we can't add raw Terraform code in a Terragrunt configuration file. I guess the only way to avoid this issue is to create resource groups from *within* the modules themselves. But, creating resource groups within modules doesn't sound effective either since we would have less control over the *tags* at the resource group level.

Lastly, in that storage account, create a private container called `environment-states`. This is where the Terraform states will go. Don't worry; environments will be separated through sub-folders.

# Ready to run it?

This part is so cool. Set the current directory of your shell to the region folder. Then run `terragrunt run-all plan`.

Terragrunt will find all the `terragrunt.hcl` files and then run `terragrunt plan` from each folder *(It won't work from the root of the repository)*.

Oh my...

```plaintext
❯ terragrunt run-all plan
INFO[0000] The stack at /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise will be processed in the following order for command plan:
Group 1
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/global/resource_groups

Group 2
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/eastus/spoke-vnet
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/westus/spoke-vnet


Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.vnet_resource_group will be created
  + resource "azurerm_resource_group" "vnet_resource_group" {
      + id       = (known after apply)
      + location = "eastus"
      + name     = "rg-example-vnet"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vnet_resource_group_name = "rg-example-vnet"

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_subnet.subnet[0] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.1.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "AzureBastionSubnet"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_subnet.subnet[1] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.2.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Management"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_subnet.subnet[2] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.3.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Tools"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_subnet.subnet[3] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.4.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Workloads"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_virtual_network.vnet will be created
  + resource "azurerm_virtual_network" "vnet" {
      + address_space         = [
          + "10.0.0.0/16",
        ]
      + dns_servers           = []
      + guid                  = (known after apply)
      + id                    = (known after apply)
      + location              = "eastus"
      + name                  = "vnet-spoke-staging-westus-001"
      + resource_group_name   = "rg-terragrunt-mock-001"
      + subnet                = (known after apply)
      + tags                  = {
          + "environment" = "staging"
        }
      + vm_protection_enabled = false
    }

Plan: 5 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vnet_address_space = [
      + "10.0.0.0/16",
    ]
  + vnet_id            = (known after apply)
  + vnet_location      = "eastus"
  + vnet_name          = "vnet-spoke-staging-westus-001"
  + vnet_subnets       = [
      + (known after apply),
      + (known after apply),
      + (known after apply),
      + (known after apply),
    ]

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_subnet.subnet[0] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.1.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "AzureBastionSubnet"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_subnet.subnet[1] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.2.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Management"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_subnet.subnet[2] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.3.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Tools"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_subnet.subnet[3] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.4.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Workloads"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_virtual_network.vnet will be created
  + resource "azurerm_virtual_network" "vnet" {
      + address_space         = [
          + "10.0.0.0/16",
        ]
      + dns_servers           = []
      + guid                  = (known after apply)
      + id                    = (known after apply)
      + location              = "eastus"
      + name                  = "vnet-spoke-staging-eastus-001"
      + resource_group_name   = "rg-terragrunt-mock-001"
      + subnet                = (known after apply)
      + tags                  = {
          + "environment" = "staging"
        }
      + vm_protection_enabled = false
    }

Plan: 5 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vnet_address_space = [
      + "10.0.0.0/16",
    ]
  + vnet_id            = (known after apply)
  + vnet_location      = "eastus"
  + vnet_name          = "vnet-spoke-staging-eastus-001"
  + vnet_subnets       = [
      + (known after apply),
      + (known after apply),
      + (known after apply),
      + (known after apply),
    ]

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
```

## But, wait, should you use `run-all` all the time?

No, the Terragrunt team comments that [they originally designed this capability to stand up an entire infrastructure stack from scratch](https://github.com/gruntwork-io/terragrunt/issues/720#issuecomment-497888756). Once the infrastructure is up, they recommend using `terragrunt plan` and `apply` at the individual modules.

# Alright, do it. Apply. 🤯

I love this part. Run `terragrunt run-all apply -auto-approve`.

```plaintext
❯ terragrunt run-all apply -auto-approve
INFO[0000] The stack at /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise will be processed in the following order for command apply:
Group 1
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/global/resource_groups

Group 2
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/eastus/spoke-vnet
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/westus/spoke-vnet

Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) y
azurerm_resource_group.vnet_resource_group: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply":

  # azurerm_resource_group.vnet_resource_group has changed
  ~ resource "azurerm_resource_group" "vnet_resource_group" {
        id       = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet"
        name     = "rg-example-vnet"
      + tags     = {}
        # (1 unchanged attribute hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────

No changes. Your infrastructure matches the configuration.

Your configuration already matches the changes detected above. If you'd like
to update the Terraform state to match, create and apply a refresh-only plan:
  terraform apply -refresh-only

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

vnet_resource_group_name = "rg-example-vnet"
azurerm_virtual_network.vnet: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001]
azurerm_virtual_network.vnet: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001]
azurerm_subnet.subnet[3]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads]
azurerm_subnet.subnet[2]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools]
azurerm_subnet.subnet[0]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet]
azurerm_subnet.subnet[1]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management]
azurerm_subnet.subnet[3]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads]
azurerm_subnet.subnet[2]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools]
azurerm_subnet.subnet[0]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet]
azurerm_subnet.subnet[1]: Refreshing state... [id=/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply":

  # azurerm_subnet.subnet[0] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet"
        name                                           = "AzureBastionSubnet"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[1] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management"
        name                                           = "Management"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[2] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools"
        name                                           = "Tools"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[3] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads"
        name                                           = "Workloads"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_virtual_network.vnet has changed
  ~ resource "azurerm_virtual_network" "vnet" {
        id                      = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001"
        name                    = "vnet-spoke-staging-westus-001"
      ~ subnet                  = [
          + {
              + address_prefix = "10.1.1.0/26"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet"
              + name           = "AzureBastionSubnet"
              + security_group = ""
            },
          + {
              + address_prefix = "10.1.2.0/24"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management"
              + name           = "Management"
              + security_group = ""
            },
          + {
              + address_prefix = "10.1.3.0/24"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools"
              + name           = "Tools"
              + security_group = ""
            },
          + {
              + address_prefix = "10.1.4.0/24"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads"
              + name           = "Workloads"
              + security_group = ""
            },
        ]
        tags                    = {
            "environment" = "staging"
        }
        # (7 unchanged attributes hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────

No changes. Your infrastructure matches the configuration.

Your configuration already matches the changes detected above. If you'd like
to update the Terraform state to match, create and apply a refresh-only plan:
  terraform apply -refresh-only

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

vnet_address_space = tolist([
  "10.1.0.0/16",
])
vnet_id = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001"
vnet_location = "eastus"
vnet_name = "vnet-spoke-staging-westus-001"
vnet_subnets = [
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet",
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management",
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools",
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads",
]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply":

  # azurerm_subnet.subnet[0] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet"
        name                                           = "AzureBastionSubnet"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[1] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management"
        name                                           = "Management"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[2] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools"
        name                                           = "Tools"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[3] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads"
        name                                           = "Workloads"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_virtual_network.vnet has changed
  ~ resource "azurerm_virtual_network" "vnet" {
        id                      = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001"
        name                    = "vnet-spoke-staging-eastus-001"
      ~ subnet                  = [
          + {
              + address_prefix = "10.0.1.0/26"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet"
              + name           = "AzureBastionSubnet"
              + security_group = ""
            },
          + {
              + address_prefix = "10.0.2.0/24"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management"
              + name           = "Management"
              + security_group = ""
            },
          + {
              + address_prefix = "10.0.3.0/24"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools"
              + name           = "Tools"
              + security_group = ""
            },
          + {
              + address_prefix = "10.0.4.0/24"
              + id             = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads"
              + name           = "Workloads"
              + security_group = ""
            },
        ]
        tags                    = {
            "environment" = "staging"
        }
        # (7 unchanged attributes hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────

No changes. Your infrastructure matches the configuration.

Your configuration already matches the changes detected above. If you'd like
to update the Terraform state to match, create and apply a refresh-only plan:
  terraform apply -refresh-only

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

vnet_address_space = tolist([
  "10.0.0.0/16",
])
vnet_id = "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001"
vnet_location = "eastus"
vnet_name = "vnet-spoke-staging-eastus-001"
vnet_subnets = [
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet",
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management",
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools",
  "/subscriptions/<< redacted subscription id >>/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads",
]
```

That's it! All resources were created. You'll notice that the output looks like someone iterated through all the folders and ran `terraform apply`.

# Summary 🙏🏾

As I was learning Terragrunt, I wanted to like it. I am starting to, and I think I will end up liking it more. There were some quirks that are unique when using Azure. For example, AWS regions are king and therefore the AWS Terraform Provider has to be configured with the region. In Azure, subscriptions are king and it's wise to configure the Azure Terraform provider with the subscription ID. Also, Azure resource groups are mandatory to deploy resources and you have to make some weird decisions around *when* you create your resource groups and whether you pass them as inputs to your infrastructure modules. Lastly, don't forget that you might have to make a `data` module to query Azure AD for some *Object IDs*.

If you followed, this guide, I hope you found it useful. Leave a comment below, especially if you have some tips and tricks too. I would love to hear from you. 😊
