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

Stéphane Noutsa
7 min readJul 4, 2023
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

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

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
}

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 = {}
}

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 = {}
}

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 = {}
}
]
}

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 = {}
}
]
}

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 = {}
}

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 = {}
}

This module requires outputs from both the Elastic IP and Public Subnet modules, given that the NAT Gateway needs (a) to have a public IP address and (b) to 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 = {}
}
]
}

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 = {}
}
]
}

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 = {}
}
]
}

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 apply-all

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

terragrunt apply-all

You should then see the outputs as each resource is created until it’s all done!

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!

--

--

Stéphane Noutsa

DevOps Engineer | 4x AWS | Terraform | Ansible | Docker | Kubernetes