yellow letter tiles

Intro to Terragrunt and Terraform

Terragrunt is a command line interface tool to make Terraform better or build a better infrastructure as code pipeline. Terragrunt is built on the concept DRY. As their website states, DRY stands for “Don’t Repeat Yourself”. Terragrunt can help with structuring your code directories where you can write the Terraform code once and apply the same code with different variables and different remote state locations for each environment. Another useful feature of Terragrunt is before and after hooks. If you are developer you know you’ll need this feature at some point. Let’s get started with intro to Terragrunt and Terraform.

Installing Terraform and Terragrunt

https://cloudly.engineer/2020/setup-infrastructure-as-code-environment/aws/

Terraform code: resource

Let’s define the anatomy of a Terraform resource.

resource "aws_instance" "this" {
  ami           = data.aws_ami.centos.id
  instance_type = "t3a.medium"
}

Let’s break down this small piece of code.

  1. resource (no quotes) is a reserved keyword; means create or to ensure this type of resource exists
  2. aws_instance” (double quotes with underscores) is the type of a resource you want to create. Here’s a list of them available today.Terraform attempts to always be up to date but it could be missing resource types or some features of a resource. Most of the time, it has all the core resource types and options available.
  3. this (double quotes with underscores) The last part of the first line is a name you want to give this resource for Terraform’s state file. My best practice is to always use “this” unless you have multiple of the same resource then be specific but don’t put the resource type in this name. That’s redundant nonsense.
  4. Within the braces it’s always one or more required or optional variables. They diff from resource to resource.

    If you want to learn more Terraform click here.

Terragrunt Code: Deploy Resources

Terraform code just defines our infrastructure as code, so then we need Terragrunt to help with the multiple environment deployment. Now the combination of these two will prevent us from repeating our code for however many AWS accounts we have. Below is my Terragrunt project for my AWS account “settings” repository.

└── settings
    ├── README.md
    ├── dev
    │   └── terragrunt.hcl
        └── inputs.yml
        └── vars.tf
    ├── qa
    │   └── terragrunt.hcl
        └── inputs.yml
        └── vars.tf
    ├── sec
    │   └── terragrunt.hcl
        └── inputs.yml
        └── vars.tf
    ├── prod
    │   └── terragrunt.hcl
        └── inputs.yml
        └── vars.tf
    └── terragrunt.hcl
    └── inputs.yml
    └── vars.tf

terragrunt.hcl The root terragrunt.hcl and environment terragrunt.hcl files are a must
inputs.yml This yml file will contain variables specific to that environment, such as AWS CLI profile name
vars.tf Terraform files for each environment and one common vars.tf for all deployments

This separation of projects allows each environments to have different Terraform versions of your code at the same time. Let’s continue to see what I mean.

Main ‘terragrunt.hcl’

remote_state {
  backend = "s3"
  config = {
    bucket  = "bucket-name-for-terraform-state"
    key     = "${path_relative_to_include()}/terraform.tfstate"
    region  = local.local_inputs.aws_region,
    profile = local.local_inputs.aws_cli_profile
    encrypt = true
  }
}

locals {
  local_inputs  = yamldecode(file("${get_terragrunt_dir()}/inputs.yml"))
  global_inputs = yamldecode(file("${get_terragrunt_dir()}/inputs.yml"))
}

inputs = merge(local.global_inputs, local.local_inputs)

remote state
I’ll be storing the Terraform state file in Amazon S3.

  1. key All of my environments/accounts Terraform state files will be stored in one AWS S3 bucket separated by environments using the directory names. The Terragrunt function path_relative_to_include() is going to help with that. But you can pass a different bucket for each environment through the local inputs.
  2. profile Since we’ll have several AWS accounts and profiles, this value will be dynamic and passed in from the environments input file.
  3. I think the rest are obvious.

locals

  1. local_inputs During Terraform plan or apply it grabs the variables for the environment to create Terraform files for that specific environment in the .terragrunt local cache directory.
  2. global_inputs contains variables that are common for all environments (if needed)

dev ‘terragrunt.hcl’

include {
  path = find_in_parent_folders()
}

terraform {
  source = "git@giturl.com:path/to/tf-modules/settings.git?ref=dev"
}

This says ‘hey go fetch the TF code from this URL but only the dev branch’. Also says ‘The Terraform backend configuration is in the main terragrunt.hcl file’. Next let’s init Terragrunt. If you haven’t already created the S3 bucket for your state file it will request to create.

I’ll be using aws cli profiles, this assumes you have already set this up.
AWS Permissions Required

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowS3ForTerraform",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketVersioning",
                "s3:CreateBucket"
            ],
            "Resource": "arn:aws:s3:::YOUR-TF-BUCKET-NAME-HERE"
        },
        {
            "Sid": "AllowDownloadNUploadtoPrefix",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::YOUR-TF-BUCKET-NAME-HERE/*"
        }
    ]
}

dev ‘inputs.yml’

aws_cli_profile: "your-env-aws-cli-profile-name"
aws_region: "us-east-1"
other_var: "other-value"

dev ‘vars.tf’

variable aws_account_alias {
  default = "acct-nickname-here"
}

variable aws_region {}

variable aws_cli_profile {}

variable other_var {}

As said before, this is environment specific values. Let’s initiate already!

cd settings/dev/
terragrunt init
Output

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

---------------------------------------------------------------------

Then ‘terragrunt apply’. Apply the configuration and verify.

Terragrunt cache

Don’t put terragrunt cache in git. Add the following to your .gitignore file for your Terragrunt repositories.

*.terraform*
*.terragrunt*

If you update your Terraform repository you’ll have to update your Terragrunt too to fetch the latest code. You can do that with this additional argument.

terragrunt init --terragrunt-source-update

An alternative design with Terragrunt and Terraform

In this alternative design structure you can have a main.tf file at the root of your project. This main.tf can contain all your main code in one place instead of having to create and manage several different git repositories and branches. See the structure example below. Here’s full working code example of this design.

├── README.md
├── dev
│   ├── inputs.yml
│   ├── terragrunt.hcl
│   └── vars.tf
├── prod
│   ├── inputs.yml
│   ├── terragrunt.hcl
│   └── vars.tf
├── qa
│   ├── inputs.yml
│   ├── terragrunt.hcl
│   └── vars.tf
├── sec
│   ├── inputs.yml
│   ├── terragrunt.hcl
│   └── vars.tf
├── terragrunt.hcl
├── main.tf
└── web-server-policy.tf
Posted in Aws

Published by

2 thoughts on “Intro to Terragrunt and Terraform

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.