Introduction

This blog post is going to cover my initial journey into Terragrunt and deploying a simple S3 Bucket to AWS as per my previous examples as this is the simplest way to play with Terraform/grunt/whatever

Lets DO IT

Prerequisites

As this tutorial utilises AWS make sure you have setup a AWS account and ran aws configure with the correct credentials, this page is helpful for this kinda thing:

https://www.cyberciti.biz/faq/osx-installing-the-aws-command-line-interface-using-brew/

Install Terragrunt

You can install Terragrunt on macOS using Homebrew:

brew install terragrunt

Setup some terraform code

Firstly lets get some standard terraform code setup to create a S3 bucket.

Create the below file:

main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "b" {
  bucket = "my-tf-test-bucket"

  tags = {
    Name        = "My bucket"
    Environment = "Dev"
  }
}

Perform a terraform init, terraform plan

Your output should suggest its going to create 1 item which is your bucket!

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

Okay cool we know now this code is going to function as we expect, feel free to apply and destroy however I’m assuming as you’re reading this I assume you’re well aware of the basics for terraform (otherwise get your google hat on youngling)

Inside your folder create terragrunt.hcl

touch terragrunt.hcl

Create a directory for your environment:

mkdir -p environments/dev

Then create your src directory where your main.tf and providers will live:

mkdir src
touch src/main.tf
touch src/provider.tf

Inside srv/provider.tf add the following:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "us-east-1"
}

Notice this isn’t too disimilar to ouroriginal main.tf.

In the same vain take your resource for the s3 bucket and move it to srv/main.tf

resource "aws_s3_bucket" "b" {
  bucket = "my-tf-test-bucket"

  tags = {
    Name        = "My bucket"
    Environment = "Dev"
  }
}

In environments/dev, create a file named terragrunt.hcl with the below content:

environments/dev/terragrunt.hcl

include {
    path = find_in_parent_folders()
}

This smart little bit of code instructs Terragrunt to use any .hcl configuration files it finds in parent folders.

Add a terraform block to give Terragrunt a source reference:

include {
    path = find_in_parent_folders()
}

terraform {
    source = "../../src"
}

Now change to your environments/dev folder and run: terragrunt init

This sets up a bit of context, including environment variables, then runs terraform init.

run terragrunt plan and you’ll see your s3 bucket is going to get created:

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_s3_bucket.b will be created
  + resource "aws_s3_bucket" "b" {
      + acceleration_status         = (known after apply)
.
.
.
Plan: 1 to add, 0 to change, 0 to destroy.

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

In the root terragrunt.hcl let’s add a variable: terragrunt.hcl

inputs = {
    env_name = "develop"
}

adjust the srv/main.tf file to add this variable:

srv/main.tf

variable "env_name" {}

resource "aws_s3_bucket" "b" {
  bucket = "my-tf-${var.env_name}-bucket"

  tags = {
    Name        = "My ${var.env_name} bucket"
    Environment = "${var.env_name}"
  }
}

Now run your terragrunt plan again and you’ll see it would create a bucket named my-tf-develop-bucket

 # aws_s3_bucket.b will be created
  + resource "aws_s3_bucket" "b" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "my-tf-develop-bucket"

That’s cool we inherit a variable from the root directory but wouldn’t it make more sense to have it by more dynamic!

In the root terragrunt.hcl file, update the input to use path_relative_to_include(), and pass the value as the env_name variable:

inputs = {
    env_name = path_relative_to_include()
}

Running terragrunt plan again you can see its pulling in the folder names! How freaking cool is this:

 # aws_s3_bucket.b will be created
  + resource "aws_s3_bucket" "b" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "my-tf-environments/dev-bucket"

BUT its really not that nice to keep the “environments” part, in fact I hate it lets get rid of that bad boy!

Update the terragrunt.hcl to strip “environements/” from env_name:

locals {
    env_name = replace(path_relative_to_include(), "environments/", "")
}

inputs = {
    env_name = local.env_name
}

What the heck did I just do you ask? you added a locals block to create a local variable and used the built-in replace function to remove the unwanted parts of the relative path. Then, you updated the inputs block to use the local variable, makes total sense right?!

Running terragrunt plan again…

  # aws_s3_bucket.b will be created
  + resource "aws_s3_bucket" "b" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "my-tf-dev-bucket"

Now we’re sucking diesel !

It’s time!

terragrunt apply

and you’ll see its successfully created your bucket which you should also be able to view via the S3 AWS console:

(https://imgur.com/a/qL9xB6N)

Move your state to remote storage

Okay that’s cool we have now done some terragrunting and was it worth it? Not sure yet but lets also setup our remote state!

Since Terragrunt allows you to configure multiple environments, you should store state files in their own S3 buckets so they don’t overwrite each other.

In your root terragrunt.hcl, add a remote_state block that tells Terragrunt where to place your file in S3:

remote_state {
    backend = "s3"
    generate = {
        path = "backend.tf"
        if_exists = "overwrite_terragrunt"
    }
    config = {
        bucket = "lukayeh-terraform" # Amazon S3 bucket required

        key     = "envs/${local.env_name}/terraform.tfstate"
        region  = "eu-west-1"
        encrypt = true
        profile = "default" # Profile name required
    }
}

run terragrunt plan you might be asked Do you want to copy existing state to the new backend? to which I answered yes!

Once its completed, you should see in your s3 state bucket you have a folder called envs/ with a folder called dev thats NEAT!

Create a new environment

Now that you’ve configured your development environment, create another that reuses most of your work.

Under environments, create a folder named test. In it, create a file called terragrunt.hcl:

mkdir test && cd test

vi terragrunt.hcl

include {
    path = find_in_parent_folders()
}

terraform {
    source = "../../src"
}

Now notice run a terragrunt plan and the name of the bucket has magically changed!


  # aws_s3_bucket.b will be created
  + resource "aws_s3_bucket" "b" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "my-tf-test-bucket" << THIS OMG

Now, you’ve created two environments, dev and test, but they’re the same, other than their name.

In src/main.tf, add new a variable for us to play with:

variable "env_name" {}

variable "label" {default = "hairy_bikers"}

resource "aws_s3_bucket" "b" {
  bucket = "my-tf-${var.env_name}-bucket"

  tags = {
    Name        = "My ${var.env_name} bucket"
    Environment = "${var.env_name}"
    Labels      = "${var.label}"
  }
}

Running terragrunt plan again you’ll see the labels are being set from the src:

      + tags                        = {
          + "Environment" = "test"
          + "Labels"      = "hairy_bikers"
          + "Name"        = "My test bucket"
        }

In test/terragrunt.hcl, add values for your variable:

include {
    path = find_in_parent_folders()
}

terraform {
    source = "../../src"
}

inputs = {
    labels = "I'm overriding your stuff from test" 
}

Another plan and you’ll see the tags have now changed:

      + tags                        = {
          + "Environment" = "test"
          + "Labels"      = "I'm overriding your stuff from test"
          + "Name"        = "My test bucket"
        }

So now you have terragrunt code that functions very similarly to the workspaces tutorial I just ran but I can imagine you can see the similarities and I’ll let you decide which you prefer.

Code for this can be found here.

Acknowledgements

Huge credit to this post for helping me on this journey https://developer.newrelic.com/terraform/terragrunt-configuration/