DEV Community

Cover image for Terraform & Terragrunt to Create a VPC and its Components (Part II)
Stephane Noutsa for AWS Community Builders

Posted on

Terraform & Terragrunt to Create a VPC and its Components (Part II)

Terragrunt is a powerful open-source tool that serves as a wrapper around Terraform, providing enhanced features and simplifying the management of Terraform deployments. With Terragrunt, infrastructure as code (IaC) practitioners can achieve more effective and scalable infrastructure management in complex environments. By abstracting common Terraform tasks, Terragrunt facilitates the creation, deployment, and maintenance of infrastructure resources, enabling teams to efficiently manage infrastructure with consistency and ease.

In the previous article, we created Terraform building blocks which we'll use in this article to orchestrate the creation of a VPC with the components below.
This article assumes some familiarity with Terraform and Terragrunt.

  1. A VPC (obviously 😅)
  2. An Internet Gateway
  3. A route table for the public subnet (which we'll just call public route table)
  4. A public subnet
  5. An Elastic IP (EIP) for the NAT Gateway
  6. A NAT Gateway
  7. A route table for the private subnet (which we'll just call private route table)
  8. A private subnet
  9. A Network Access Control List (NACL)

Our Terragrunt project will have the following folder structure and files:

vpc-live/
  <environment>/
    <module_1>/
      terragrunt.hcl
    <module_2>/
      terragrunt.hcl
    ...
    <module_n>/
      terragrunt.hcl
  terragrunt.hcl
Enter fullscreen mode Exit fullscreen mode

Basically, we'll have a parent directory which we'll call vpc-live. This directory will contain a root terragrunt.hcl file and a directory for each environment we'll want to create our resources in (e.g dev, staging, prod). For our article, we'll only have a dev directory. This directory will contain directories that will represent the different specific resources we'll want to create.

Our final folder structure will be:

vpc-live/
  dev/
    elastic-ip/
      terragrunt.hcl
    internet-gateway/
      terragrunt.hcl
    nacl/
      terragrunt.hcl
    nat-gateway/
      terragrunt.hcl
    private-route-table/
      terragrunt.hcl
    private-subnet/
      terragrunt.hcl
    public-route-table/
      terragrunt.hcl
    public-subnet/
      terragrunt.hcl
    vpc/
      terragrunt.hcl
  terragrunt.hcl
Enter fullscreen mode Exit fullscreen mode

0. Root terragrunt.hcl file

Our root terragrunt.hcl file will contain the configuration for our remote Terraform state. We'll use an S3 bucket in AWS to store our Terraform state file, and the name of our S3 bucket must be unique for it to be successfully created. My S3 bucket is in the Paris region (eu-west-3).

vpc-live/terragrunt.hcl

generate "backend" {
  path      = "backend.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
terraform {
  backend "s3" {
    bucket         = "snk-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "eu-west-3"
    encrypt        = true
  }
}
EOF
}
Enter fullscreen mode Exit fullscreen mode

It should be noted that our module terragrunt.hcl files' Terraform source could either be the path to the local building block or the URL of the Git repository hosting our building block's code.
(We created the different building blocks in our previous article)

1. VPC

This module uses the VPC building block as its Terraform source.

vpc-live/dev/vpc/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_vpc_building_block_or_git_repo_url>
}

inputs = {
  vpc_cidr = "10.0.0.0/16"
  vpc_name = "dev-vpc"
  instance_tenancy = "default"
  enable_dns_support = true
  enable_dns_hostnames = true
  assign_generated_ipv6_cidr_block = false
  vpc_tags = {}
}
Enter fullscreen mode Exit fullscreen mode

The include block will include the root terragrunt.hcl file (with the backend configuration), and will substitute the key of our bucket configuration with the path to each module's terragrunt.hcl file.

The values passed in the inputs section are the variables that are defined in the building blocks.

For this module and the following modules, we won't be passing the variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION since such credentials (bar the AWS_REGION variable) are sensitive). You'll have to add them yourself since they're required.

NB: Please don't commit these credentials to version control systems as that is not secure. Ideally, you'll have a pipeline whose job runner will have the credentials configured as the default profile, or the runner will be able to assume a role that will allow it to create the resources.

2. Internet Gateway

This module uses the Internet Gateway building block as its Terraform source.

vpc-live/dev/internet-gateway/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_internet_gateway_building_block_or_git_repo_url>
}

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

inputs = {
  vpc_id = dependency.vpc.outputs.vpc_id
  name = "dev-igw"
  tags = {}
}
Enter fullscreen mode Exit fullscreen mode

The dependency block indicates that this module requires outputs (the VPC ID) from the VPC module which we created in step 1.

3. Public Route Table

This module uses the Route Table building block as its Terraform source.

vpc-live/dev/public-route-table/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_route_table_building_block_or_git_repo_url>
}

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

dependency "igw" {
  config_path = "../internet-gateway"
}

inputs = {
  route_tables = [
    {
      name      = "dev-public-rt"
      vpc_id    = dependency.vpc.outputs.vpc_id
      is_igw_rt = true

      routes = [
        {
          cidr_block = "0.0.0.0/0"
          igw_id     = dependency.igw.outputs.igw_id
        }
      ]

      tags = {}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This module requires outputs from both the VPC and Internet Gateway modules.

4. Public Subnet

This module uses the Subnet building block as its Terraform source.

vpc-live/dev/public-subnet/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_subnet_building_block_or_git_repo_url>
}

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

dependency "public-route-table" {
  config_path = "../public-route-table"
}

inputs = {
  subnets = [
    {
      name                                = "dev-public-subnet"
      vpc_id                              = dependency.vpc.outputs.vpc_id
      cidr_block                          = "10.0.0.0/24"
      availability_zone                   = "eu-west-3a"
      map_public_ip_on_launch             = true
      private_dns_hostname_type_on_launch = "resource-name"
      is_public                           = true
      route_table_id                      = dependency.public-route-table.outputs.route_table_ids[0]
      tags                                = {}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This module requires outputs from both the VPC and Public Route Table modules.

5. Elastic IP (EIP)

This module will create an EIP which will be attached to the NAT Gateway that we'll create.
It uses the Elastic IP building block as its Terraform source.

vpc-live/dev/elastic-ip/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_elastic_ip_building_block_or_git_repo_url>
}

inputs = {
  in_vpc = true
  tags = {}
}
Enter fullscreen mode Exit fullscreen mode

This module does not depend on any other module, so it will actually be created before other modules which have dependencies.

6. NAT Gateway

This module uses the NAT Gateway building block as its Terraform source.

vpc-live/dev/nat-gateway/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_nat_gateway_building_block_or_git_repo_url>
}

dependency "eip" {
  config_path = "../elastic-ip"
}

dependency "public-subnet" {
  config_path = "../public-subnet"
}

inputs = {
  eip_id = dependency.eip.outputs.eip_id
  subnet_id = dependency.public-subnet.outputs.public_subnets[0]
  name = "dev-nat-gw"
  tags = {}
}
Enter fullscreen mode Exit fullscreen mode

This module requires outputs from both the Elastic IP and Public Subnet modules, given that the NAT Gateway needs to (a) have a public IP address and (b) be in a public subnet.

7. Private Route Table

This module uses the Route Table building block as its Terraform source.

vpc-live/dev/private-route-table/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_route_table_building_block_or_git_repo_url>
}

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

dependency "nat-gateway" {
  config_path = "../nat-gateway"
}

inputs = {
  route_tables = [
    {
      name      = "dev-private-rt"
      vpc_id    = dependency.vpc.outputs.vpc_id
      is_igw_rt = false

      routes = [
        {
          cidr_block = "0.0.0.0/0"
          nat_gw_id  = dependency.nat-gateway.outputs.nat_gw_id
        }
      ]

      tags = {}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This module requires outputs from both the VPC and NAT Gateway modules.

8. Private Subnet

This module uses the Subnet building block as its Terraform source.

vpc-live/dev/private-subnet/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_subnet_building_block_or_git_repo_url>
}

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

dependency "private-route-table" {
  config_path = "../private-route-table"
}

inputs = {
  subnets = [
    {
      name                                = "dev-private-subnet"
      vpc_id                              = dependency.vpc.outputs.vpc_id
      cidr_block                          = "10.0.1.0/24"
      availability_zone                   = "eu-west-3a"
      map_public_ip_on_launch             = false
      private_dns_hostname_type_on_launch = "resource-name"
      is_public                           = false
      route_table_id                      = dependency.private-route-table.outputs.route_table_ids[0]
      tags                                = {}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This module requires outputs from both the VPC and Private Route Table modules.

9. Network Access Control List (NACL)

This module uses the NACL building block as its Terraform source.

vpc-live/dev/nacl/terragrunt.hcl

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = <path_to_local_nacl_building_block_or_git_repo_url>
}

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

dependency "public-subnet" {
  config_path = "../public-subnet"
}

dependency "private-subnet" {
  config_path = "../private-subnet"
}

inputs = {
  nacls = [
    {
      name   = "open-public-nacl"
      vpc_id = dependency.vpc.outputs.vpc_id
      egress = [
        {
          protocol   = "-1"
          rule_no    = 100
          action     = "allow"
          cidr_block = "0.0.0.0/0"
          from_port  = 0
          to_port    = 0
        }
      ]
      ingress = [
        {
          protocol   = "-1"
          rule_no    = 100
          action     = "allow"
          cidr_block = "0.0.0.0/0"
          from_port  = 0
          to_port    = 0
        }
      ]
      subnet_id = dependency.public-subnet.outputs.public_subnets[0]
      tags      = {}
    },
    {
      name   = "open-private-nacl"
      vpc_id = dependency.vpc.outputs.vpc_id
      egress = [
        {
          protocol   = "-1"
          rule_no    = 100
          action     = "allow"
          cidr_block = "0.0.0.0/0"
          from_port  = 0
          to_port    = 0
        }
      ]
      ingress = [
        {
          protocol   = "-1"
          rule_no    = 100
          action     = "allow"
          cidr_block = "0.0.0.0/0"
          from_port  = 0
          to_port    = 0
        }
      ]
      subnet_id = dependency.private-subnet.outputs.private_subnets[0]
      tags      = {}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This module creates two NACLs: one for the public subnet and one for the private subnet. Their security rules allow traffic from and to all sources for simplicity, and that's not secure. You should modify the values for ingress and egress to only allow the traffic you desire, making the rules more secure.

The module requires outputs from the VPC, Public Subnet, and Private Subnet modules.

10. Creating the Resources

Terragrunt will allow us to orchestrate the creation of our VPC and its components through the modules we've created above.

First, we'll need to cd into our environment's directory from the terminal:

cd vpc-live/dev

Here, we can now run the following command to instruct Terragrunt to loop through all the module directories and create the different resources:

terragrunt run-all apply

We should then see a similar prompt in our terminal. Enter y to confirm the creation of all the resources:

terragrunt run-all apply

Conclusion

We've seen how to orchestrate the creation of a VPC and its components with Terragrunt, using basic Terraform building blocks.

We could create as many building blocks as we'd like with Terraform, then use Terragrunt to orchestrate the creation of an architecture that fits the needs of different projects (and in different environments) by simply reusing these building blocks. This will help us to keep our code DRY.

Happy coding!

Top comments (0)