Terragrunt dependencies: The power of “Terragrunt run-all” command
This article, will explore the power of Terragrunt’s dependencies feature and how it can help you manage your infrastructure as code more effectively. It will also cover the “terragrunt run-all” command and provide some hands-on examples of how to use it to deploy resources on Azure.
Introduction
As part of a recent project I worked on, I was required to develop and implement infrastructure as code (IaC) and applications deployment on Kubernetes to run on Azure infrastructure for the company’s SaaS application.
The effectiveness of the deployment process was important, guaranteeing it could become completely functional as quickly and seamlessly as possible. This would make building and destroying easier, faster, and smoother.
Integrating infrastructure as code (IaC)-(Terraform and Terragrunt) with Helm(Terraform Helm provider), and GitOps-(ArgoCD) provided a seamless deployment experience from top to bottom.
The “terragrunt run-all” command was especially useful, enabling us to deploy 12+ distinct cloud resources, each defined in separate Terraform modules, and essential company’s and third-party applications/operators on Kubernetes.
This powerful command simplified the end-to-end deployment process, starting from provisioning the first cloud resource to achieving a fully operational cloud infrastructure and applications running on a Kubernetes cluster, all with a single “terragrun run-all” command execution.
This approach also ensured proper deployment and configuration of critical third-party Kubernetes applications within the cluster. Combining Terraform, Terragrunt, Helm(TF-Provider) and GitOps-ArgoCD streamlined the deployment pipeline, reduced errors, simplified maintenance, and allowed us to efficiently deploy and destroy complex infrastructures, ultimately delivering a reliable and consistent environment for our organization super fast.
sample output from my "terragrunt run-all apply" command execution:
amit.karni:$ tg run-all apply
INFO[0000] The stack at /Users/amit.karni/Company/Company-config/azure/Project/tg-modules will be processed in the following order for command plan:
Group 1
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-firewall
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-resource_group
Group 2
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-vnet
Group 3
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-nsg
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-route-table
Group 4
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-vnet-peering
Group 5
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-dns
Group 6
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-mysql/company-app-sql-name
Group 7
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-storage-account(nfs)
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/for_each-azure-linux-vm
Group 8
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-aks/company-app-cluster-name
Group 9
- Module /Users/amit.karni/Company/Company-config/azure/Project/tg-modules/azure-helm/argocd-and-applications
Difficulties involved in managing infrastructure as code dependencies
Managing infrastructure as code (IaC) can quickly become a challenging task as the number of resources and dependencies in your environment grows. As a result, it is essential to have a tool that makes it easier to manage dependencies and ensures that resources are deployed in the correct order. Terragrunt is one such tool that offers a simple solution to this problem with its “dependencies” feature.
Terragrunt Dependencies: What They Are and How They Work
Terragrunt dependencies establish the deployment order of resources and can be defined between modules. By doing so, Terragrunt ensures that dependent modules are deployed before the ones relying on them. To define a dependency, specify the path to the dependent module in the “dependencies” block of the deploying module. For example, if “module-b” depends on “module-a,” the “terragrunt.hcl” file for “module-b” should include:
dependencies = [ "../module-a" ]
This informs Terragrunt that “module-a” should be deployed before “module-b.”
Using Terragrunt Dependencies with “terragrunt run-all” and Delivering Outputs
After defining dependencies, you can use the “terragrunt run-all” command to deploy all modules in the correct order. This command traverses the dependency graph, ensuring all dependencies are met. To use “terragrunt run-all”, navigate to your Terragrunt project’s root and run:
terragrunt run-all apply
This applies all Terraform configurations in your project in the correct order. The “run-all” command can also be used with other Terragrunt commands like “plan” and “destroy.”
Terragrunt also simplifies output delivery between dependent modules. The “output” command retrieves outputs from a specific module, and the “locals” block in the “terragrunt.hcl” file can access and store these outputs. By utilizing the “dependency” keyword within the “locals” block, you can pass outputs from one module to another, enhancing the modularity and reusability of your infrastructure code.
Hands-On Tutorial: Deploying 4 Resources on Azure
In this hands-on tutorial, we’ll explore how to deploy four resources to Azure Cloud using Terragrunt, a popular infrastructure provisioning tool. We’ll utilize Terragrunt’s dependency management and run-all
command to deploy everything at once.
Prerequisites
Before starting, you’ll need to have the following installed and configured:
Also, ensure you have an active Azure subscription and are logged in via Azure CLI:
az login
Project Structure
Our project will have the following structure:
.
├── resource_group
│ ├── main.tf
│ └── terragrunt.hcl
├── storage_account
│ ├── main.tf
│ └── terragrunt.hcl
├── virtual_network
│ ├── main.tf
│ └── terragrunt.hcl
└── virtual_machine
├── main.tf
└── terragrunt.hcl
We’ll create four resources: Resource Group, Storage Account, Virtual Network, and Virtual Machine. Each resource will have a main.tf
file containing the Terraform configuration and a terragrunt.hcl
file for Terragrunt settings.
Define Resources
Resource Group
In the resource_group
folder, create a main.tf
file with the following content:
resource "azurerm_resource_group" "example" {
name = var.resource_group_name
location = var.location
}
Now create a terragrunt.hcl
file in the same folder:
include {
path = find_in_parent_folders()
}
terraform {
source = "../resource_group"
}
inputs = {
resource_group_name = "myResourceGroup"
location = "East US"
}
Storage Account
In the storage_account
folder, create a main.tf
file with the following content:
resource "azurerm_storage_account" "example" {
name = var.storage_account_name
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"
}
Now create a terragrunt.hcl
file in the same folder:
include {
path = find_in_parent_folders()
}
terraform {
source = "../storage_account"
}
dependency "resource_group" {
config_path = "../resource_group"
}
inputs = {
resource_group_name = dependency.resource_group.outputs.resource_group_name
location = dependency.resource_group.outputs.location
storage_account_name = "mystorageaccount"
}
Virtual Network
In the virtual_network
folder, create a main.tf
file with the following content:
resource "azurerm_virtual_network" "example" {
name = var.virtual_network_name
resource_group_name = var.resource_group_name
location = var.location
address_space = ["10.0.0.0/16"]
}
Now create a terragrunt.hcl
file in the same folder:j
include {
path = find_in_parent_folders()
}
terraform {
source = "../virtual_network"
}
dependency "resource_group" {
config_path = "../resource_group"
}
inputs = {
resource_group_name = dependency.resource_group.outputs.resource_group_name
location = dependency.resource_group.outputs.location
virtual_network_name = "myVirtualNetwork"
}
Virtual Machine
In the virtual_machine
folder, create a main.tf
file with the following content:
resource "azurerm_virtual_machine" "example" {
name = var.virtual_machine_name
resource_group_name = var.resource_group_name
location = var.location
vm_size = "Standard_DS1_v2"
network_interface_ids = [azurerm_network_interface.example.id]
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "16.04-LTS"
version = "latest"
}
storage_os_disk {
name = "osdisk"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Standard_LRS"
}
os_profile {
computer_name = var.virtual_machine_name
admin_username = "ubuntu"
admin_password = "Password123!"
}
os_profile_linux_configuration {
disable_password_authentication = true
ssh_keys {
path = "/home/ubuntu/.ssh/authorized_keys"
key_data = var.ssh_public_key
}
}
}
resource "azurerm_network_interface" "example" {
name = "${var.virtual_machine_name}-nic"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "internal"
subnet_id = var.subnet_id
private_ip_address_allocation = "Dynamic"
}
}
Now create a terragrunt.hcl
file in the same folder:
include {
path = find_in_parent_folders()
}
terraform {
source = "../virtual_machine"
}
dependency "resource_group" {
config_path = "../resource_group"
}
dependency "virtual_network" {
config_path = "../virtual_network"
}
inputs = {
resource_group_name = dependency.resource_group.outputs.resource_group_name
location = dependency.resource_group.outputs.location
virtual_machine_name = "myVirtualMachine"
subnet_id = dependency.virtual_network.outputs.example_subnet_id
ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDc..."
}
Deploying Resources
With all resources defined, we can now use Terragrunt’s run-all
command to deploy everything at once. In the root directory, create a terragrunt.hcl
file with the following content:
locals {
common_vars = {
resource_group_name = "myResourceGroup"
location = "East US"
}
}
remote_state {
backend = "azurerm"
config = {
resource_group_name = local.common_vars.resource_group_name
storage_account_name = "mytfstatestorage"
container_name = "tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
}
}
This configures a remote state backend to store the Terraform state in Azure Blob Storage(Blob storage should be prepared beforehand)
Now, from the root directory, run the following command:
terragrunt run-all apply
Terragrunt will automatically determine the dependencies between resources and deploy them in the correct order. After the deployment is complete, you’ll have your four resources up and running in Azure.
To clean up the resources when you’re done, use the following command:
terragrunt run-all destroy
Conclusion
Terragrunt’s dependencies feature is a powerful tool for managing infrastructure as code. By defining dependencies between modules, you can ensure that resources are deployed in the correct order and avoid errors that arise from incorrect order or missing modules.
The “terragrunt run-all” command simplifies the process of deploying resources with dependencies, making it easier, faster, and more efficient to manage your infrastructure as code. With the examples provided in this article, you should now have a good understanding of how to use Terragrunt dependencies to deploy resources on Azure.
In this tutorial, we demonstrated how to deploy four resources to Azure Cloud using Terragrunt dependencies. We also showed how to use Terragrunt’s run-all
command to deploy everything at once. This powerful tool streamlines the deployment process and makes it easier to manage complex infrastructure environments.