Terragrunt Quick Start

Terragrunt is a tool that wraps around Terraform and allows reusing of Terraform code. It’s an open source project hosted in Github. In this quick start guide, we’ll deploy a simple Terraform code with Terragrunt.

We will be reusing the famous terraform-aws-ec2-instance module from Github.

Now, let’s start.

Download Terragrunt

Terragrunt’s latest binaries can be found in their Github releases page. If you’re using a linux machine you can use the code below to install the latest available now. As of writing this 0.36.7 is the latest one.

				
					
# install Terragrunt in linux/amd64:
sudo curl -L https://github.com/gruntwork-io/terragrunt/releases/download/v0.36.7/terragrunt_linux_amd64 -o /usr/bin/terragrunt
sudo chmod +x /usr/bin/terragrunt
				
			

Terragrunt.hcl

Now let’s start with a simple terragrunt.hcl file. This is the main entrypoint for Terragrunt.

				
					terraform {
  source = "tfr://registry.terraform.io/terraform-aws-modules/ec2-instance/aws?version=3.5.0"
}

inputs = {

  name                   = "single-instance"
  ami                    = "ami-ebd02392"
  instance_type          = "t2.micro"
  key_name               = "user1"
  monitoring             = true
  vpc_security_group_ids = ["sg-12345678"]
  subnet_id              = "subnet-eddcdzz4"

}

generate "provider" {
  path      = "tg_provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region  = "ap-southeast-2"
}
EOF
}
				
			

Okay, so what’s happening here? Let’s dive-in a little.

We’re specifying a Terraform’s module source. With the `terraform {}` resource block in the file above. This block then pulls the specified terraform code for us to use. 

The `inputs` block sets the module’s input variable values. These variables are required by the module we’re using (ec2-instance).

The `generator` block generates files. In this instance, we’re instructing the block to generate a provider file named tg_provider.tf.

AWS credentials for CLI

Make sure you’ve added your AWS credentials to your environment variables. (yeah, these creds are dummy, they won’t work.) Follow this guide for more in-depth tutorial for AWS creds.

				
					# example AWS credentials bash
export AWS_ACCESS_KEY_ID="ASIATIYSATIYSRMJ69"
export AWS_SECRET_ACCESS_KEY="ldIcH0wkaRuRkFubXycH0wkaRuRkFuab"
export AWS_SESSION_TOKEN="cH0wkcH0wkaRuRkFuabUtMaRuRkFuabUcH0wkaRuRkFuabUtMtMcH0wkaRuRkFuabUtM"
				
			

Terragrunt init

As we discussed earlier, Terragrunt is just a wrapper around Terraform, so it has supports Terraform’s commands.

Let’s run `terragrunt init`.

				
					bash-5.1# terragrunt init
WARN[0000] No double-slash (//) found in source URL /terraform-aws-modules/ec2-instance/aws. Relative paths in downloaded Terraform code may not work. 

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v4.10.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
				
			

Great! Terragrunt is now initialized, which means it has downloaded the terraform module, providers and all other dependencies. Let’s check out what it downloaded. 

				
					bash-5.1# ls -l .terragrunt-cache/7WEVFcxAwAkGvCNkkW1D4bxEnc0/SZPFnGYD7NZ2XqjMVV_h8Yq28Fg/
total 104
-rw-r--r--    1 root     root         21809 Apr 20 11:34 CHANGELOG.md
-rw-r--r--    1 root     root         10173 Apr 20 11:34 LICENSE
-rw-r--r--    1 root     root         19813 Apr 20 11:34 README.md
-rw-r--r--    1 root     root          4075 Apr 20 11:34 UPGRADE-3.0.md
drwxr-xr-x    4 root     root          4096 Apr 20 11:34 examples
-rw-r--r--    1 root     root         11774 Apr 20 11:34 main.tf
-rw-r--r--    1 root     root          3782 Apr 20 11:34 outputs.tf
-rw-r--r--    1 root     root           568 Apr 20 11:49 terragrunt.hcl
-rw-r--r--    1 root     root            97 Apr 20 11:49 tg_provider.tf
-rw-r--r--    1 root     root          9861 Apr 20 11:34 variables.tf
-rw-r--r--    1 root     root           151 Apr 20 11:34 versions.tf
				
			

As we can see in the snippet above, terragrunt has downloaded the modules inside a .terragrunt-cache folder (and its unique subfolders). When we run terragrunt plan next, it executes all of its commands from inside this path.

Terragrunt plan

Let’s run a `terragrunt plan` and see what happens.

				
					WARN[0000] No double-slash (//) found in source URL /terraform-aws-modules/ec2-instance/aws. Relative paths in downloaded Terraform code may not work. 

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:

  # aws_instance.this[0] will be created
  + resource "aws_instance" "this" {
      + ami                                  = "ami-ebd02392"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = "user1"
      + monitoring                           = true
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = "subnet-eddcdzz4"
      + tags                                 = {
          + "Name" = "single-instance"
        }
      + tags_all                             = {
          + "Name" = "single-instance"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + user_data_replace_on_change          = false
      + volume_tags                          = {
          + "Name" = "single-instance"
        }
      + vpc_security_group_ids               = [
          + "sg-12345678",
        ]

      + capacity_reservation_specification {
          + capacity_reservation_preference = (known after apply)

          + capacity_reservation_target {
              + capacity_reservation_id = (known after apply)
            }
        }

      + credit_specification {}

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + tags                  = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + enclave_options {
          + enabled = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      + metadata_options {
          + http_endpoint               = "enabled"
          + http_put_response_hop_limit = 1
          + http_tokens                 = "optional"
          + instance_metadata_tags      = "disabled"
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      + root_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + tags                  = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + timeouts {}
    }

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

Changes to Outputs:
  + arn                                = (known after apply)
  + capacity_reservation_specification = (known after apply)
  + id                                 = (known after apply)
  + instance_state                     = (known after apply)
  + ipv6_addresses                     = (known after apply)
  + outpost_arn                        = (known after apply)
  + password_data                      = (known after apply)
  + primary_network_interface_id       = (known after apply)
  + private_dns                        = (known after apply)
  + private_ip                         = (known after apply)
  + public_dns                         = (known after apply)
  + public_ip                          = (known after apply)
  + tags_all                           = {
      + "Name" = "single-instance"
    }

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

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.
				
			

And that’s it! We have successfully generated a plan using terragrunt. At this point, if you were to apply this, it would fail because we have supplied fake inputs to the terragrunt.hcl file. You will need to update the inputs with real values for it to apply successfully.

Conclusion

This was a quick intro to Terragrunt. It has far more capabilities than we discussed here. This was just a tip of the iceberg. Checkout Terragrunt’s official documentation here.

Adopting Cloud? Let's Chat!

We are passionate cloud consultants and bring years of experience doing it the right way. 

Book a quick 30 min chat with us and let us help you get the most of your cloud infrastructure requirements.

Some related content