DEV Community

Cover image for Exploring GCP With Terraform: Adding Terragrunt
Robert Nemet
Robert Nemet

Posted on • Originally published at rnemet.dev on

Exploring GCP With Terraform: Adding Terragrunt

Finally, this is the last step in making the Terraform project manageable. I'll add Terragrunt to the project. Terragrunt is a thin wrapper for Terraform that provides extra tools for working with multiple Terraform modules. It's an excellent tool for making your Terraform code DRY and reusable.

I'm starting with the same project as in the previous post. The current state of code is available here in the branch add_modules.

Prerequisites

Well, you need to install Terragrunt. You can find installation instructions here. That is all.

Adding Terragrunt to the back-office network and vms modules

I'm starting with the back-office module. What I do in this module will be easily copied to other modules.

Notice that common code in back-office/vms and back-office/network is related to the provider and state file configurations. This code is common for all modules. The only thing that differs is the backend.prefix. Its value differs from module to module.

So, the first thing is to make a global configuration on the dev level. To do that, I'm creating a terragrunt.hcl file in the dev folder:

generate "provider" {
    path = "provider.tf"
    if_exists = "overwrite_terragrunt"
    contents = <<EOF
provider "google" {
  project = var.project_id
  region  = var.region
  zone    = var.zone
}

terraform {
  required_version = ">=1.5.7"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "4.77.0"
    }
  }
}
EOF
}

remote_state {
    backend = "gcs"
    generate = {
        path = "backend.tf"
        if_exists = "overwrite_terragrunt"
    }
    config = {
        bucket = "terraform-states-network-playground-382512"
        prefix = "terraform/state/dev/${path_relative_to_include()}"
    }
}

terraform {
    extra_arguments "common" {
        commands = get_terraform_commands_that_need_vars()

        arguments = [
            "-var-file=${path_relative_from_include()}/common.tfvars",
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

This file has three sections. The first section is generate "provider". This section will generate a provider.tf file in each module. Suppose it already exists and is managed by Terragrunt. In that case, it will be re-generated if there are any changes to the configuration. This file will contain provider configuration.

The second section is remote_state. This section will generate a backend.tf file in each module. This file will contain state file configuration. Notice that prefix is set to terraform/state/dev/${path_relative_to_include()}. This setting will store the state file in the terraform/state/dev folder, and each module will have its subfolder. That is important because each module will have its state file.

The third section is terraform. This section will add extra arguments to Terraform commands. In this case, it will add -var-file=${path_relative_from_include()}/common.tfvars. So, when I run any Terraform command requiring variables, Terragrunt will add this argument. That allows me to have one common.tfvars file in the dev folder, and all modules will use it.

Now, I need to add the common.tfvars file to the dev folder:

project_id = "network-playground-382512"
region = "europe-west1"
zone = "europe-west1-b"
Enter fullscreen mode Exit fullscreen mode

I need to add a terragrunt.hcl file to each module to use this. I'm adding it to the back-office module:

include {
    path = find_in_parent_folders()
}
Enter fullscreen mode Exit fullscreen mode

Now, I can remove the provider.tf and terraform.tfvars files from the back-office module. This time, I'll be running terragrunt plain instead of terraform plan in the back-office module:

terragrunt plan
Acquiring state lock. This may take a few moments...
google_service_account.back_office_fw_sa: Refreshing state... [id=projects/network-playground-382512/serviceAccounts/back-office@network-playground-382512.iam.gserviceaccount.com]
module.back-office.module.vpc.google_compute_network.network: Refreshing state... [id=projects/network-playground-382512/global/networks/back-office]
module.back-office.module.subnets.google_compute_subnetwork.subnetwork["us-central1/back-office-private"]: Refreshing state... [id=projects/network-playground-382512/regions/us-central1/subnetworks/back-office-private]
module.back-office.module.subnets.google_compute_subnetwork.subnetwork["us-central1/back-office"]: Refreshing state... [id=projects/network-playground-382512/regions/us-central1/subnetworks/back-office]
module.back-office.module.firewall_rules.google_compute_firewall.rules_ingress_egress["back-office-icmp"]: Refreshing state... [id=projects/network-playground-382512/global/firewalls/back-office-icmp]
module.back-office.module.firewall_rules.google_compute_firewall.rules_ingress_egress["back-office-iap"]: Refreshing state... [id=projects/network-playground-382512/global/firewalls/back-office-iap]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
Enter fullscreen mode Exit fullscreen mode

Running the terragrunt plan in the back-office/network module will generate two files: provider.tf and backend.tf, according to the terragrunt.hcl file in the dev folder.

For the back-office/vms module, I need to add the terragrunt.hcl file to the back-office/vms folder:

include "root" {
  path = find_in_parent_folders()
}

dependencies {
  paths = ["../network"]
}

dependency "network" {
  config_path = "../network"
}

inputs = {
  vpc_back_office_id = dependency.network.outputs.vpc_back_office_id
  vpc_back_office_subnetwork = dependency.network.outputs.vpc_back_office_subnetwork
  back_office_fw_sa = dependency.network.outputs.back_office_fw_sa
}
Enter fullscreen mode Exit fullscreen mode

Now, this terragrunt.hcl not only includes files from parent folders but also defines dependency on the ../network module. That means that the back-office/vms module depends on the back-office/network module. A network needs to be created before virtual machines. That is important because the back-office/vms module needs to know some outputs from the back-office/network module. The section inputs defines inputs for the back-office/vms module. These inputs are outputs from the back-office/network module. The last thing allows me to remove in addition to provider.tf and terraform.tfvars, and inputs.tf files from the back office/vms module.

I need to add additional variables to the variables.tf file in the back-office/vms:

variable "back_office_fw_sa" {
  description = "back office firewall service account"
  type        = string
}

variable "vpc_back_office_id" {
  description = "back office vpc id"
  type        = string
}

variable "vpc_back_office_subnetwork" {
  description = "back office vpc subnetwork"
  type        = any
}
Enter fullscreen mode Exit fullscreen mode

Running terragrunt plan should give me no changes needed. Also, I can now run terragrunt apply instead of terraform apply in back-office/vms module.

What I did in the back-office module, I need to do in the service and strorage modules. Simple. Remember to properly define dependencies and inputs in other vms modules.

When it comes to the peering module, I need to add the terragrunt.hcl file to the peering folder:

include "root" {
  path = find_in_parent_folders()
}

dependencies {
  paths = ["../back-office/network", "../services/network", "../storage/network"]
}

dependency "back-office" {
  config_path = "../back-office/network"
}

dependency "services" {
  config_path = "../services/network"
}

dependency "storage" {
  config_path = "../storage/network"
}

inputs = {
  vpc_back_office = dependency.back-office.outputs.vpc_back_office
  vpc_services = dependency.services.outputs.vpc_services
  vpc_storage = dependency.storage.outputs.vpc_storage
}
Enter fullscreen mode Exit fullscreen mode

That will ensure that the peering module will be created after the back-office, services, and storage modules. Also, it will provide inputs for the peering module. These inputs are outputs from back-office, services, and storage modules.

Interesting would be generating a graphical representation of the dependencies:

terragrunt graph-dependencies | dot -Tpng > dependencies.png
Enter fullscreen mode Exit fullscreen mode

dependencies

The terragrunt graph-dependencies command will generate a graph of the dependencies between modules. The dot -Tpng > dependencies.png command will convert the graph to a PNG file, that is graphviz. That is a great way to visualize dependencies between modules.

Try from dev folder terragrunt run-all plan. The Terragrunt will split the modules into groups. In each group, it will run plan, but first, it will run it in network modules because they are dependencies for other modules. Then, it will run plan in parallel for other modules in the other group.

Conclusion

Adding Terragrunt to the project is relatively easy. It requires some changes to the project structure and the code. But it is worth it. It makes the project more manageable: code is DRY, and common code is generated in one place. I can run Terraform commands from the root folder and do not need to visit each module, etc...

References

Top comments (0)