Terraform & Terragrunt to Create a VPC and its Components (Part II)
Tags: aws, devops, terraform, terragrunt
Published: 2023-11-03 00:47:31

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](https://dev.to/aws-builders/terraform-terragrunt-to-create-a-vpc-and-its-components-part-i-1hp7), 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](https://dev.to/aws-builders/terraform-terragrunt-to-create-a-vpc-and-its-components-part-i-1hp7))

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

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 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/emi4ljzrv6ww5dhp35uo.png)

**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!