Photo by Stillness InMotion on Unsplash
Using Terragrunt to deploy to Azure
This is the guide I wish I had
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.
If you're looking for an intro to Terragrunt, the Terragrunt docs 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 book.
The demo
Here's a repository I used to create a demo to deploy to Azure.
Here's a backup link in case the fancy card stops working ๐๐ฝ.
The directory structure looks like this.
.
โโโ 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.
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 lastvs-enterprise
- A folder for my Visual Studio Enterprise subscriptionvs-enterprise/staging
- A folder for mystaging
environmentvs-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.
.
โโโ 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.
.
โโโ 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.
# 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.
# 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.
# 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.
# 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 likesubscription.hcl
don't follow the default name, they will be ignored with this statement. Later, we'll create aterragrunt.hcl
file at the repository's root. ๐ When we create aterragrunt.hcl
file in the root of the repository, then thisinclude
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. You won't be able to consume
locals
defined in the parent directories like thesubscription.hcl
orenv.hcl
. However, you can force this behavior by loading them explicitly:
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. The problem is that we can't write proper Terraform inside of a Terragrunt configuration. We could create a
resource_group.tf
next to theterragrunt.hcl
file and the resource group would be created, but there's no way to refer to the attributes of that resource or do adepends_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
anddependency
blocks Thedependencies
block is only required to specify the order of dependencies for when you useterragrunt run-all
. Thedependency
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 callingdependency.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 also applied to me when I wanted to use data
blocks since you can't use data
blocks in your Terragrunt configuration. In this example, I am not using data elements.
# 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.
# 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.
# 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.
# 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, we could define the backend configuration in one place.
As promised, remember this from our terragrunt.hcl
file?
# 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.
# 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 thisterragrunt.hcl
file will be run from. It always runs from the context of the leaf folder (the module folder). So, when we deploy ourspoke-vnet
module with Terragrunt, it will load the parentterragrunt.hcl
file. Then, it will try to load subscription locals from a Terragrunt config file calledsubscription.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 thesubscription.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 oflocation
for the Azure region. We won't have to specifylocation = 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...
โฏ 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. 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
.
โฏ 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. ๐