Building a Custom VPC Infrastructure in AWS with Terraform.
Tags: aws, terraform, cloudcomputing, infrastructureascode
Published: 2024-01-16 04:19:55

**INTRODUCTION**
Amazon Web Services (AWS) provides a robust set of tools to manage cloud infrastructure, and Terraform enhances this capability by enabling Infrastructure as Code (IaC). In this article, we will explore how to use Terraform to create a custom **VPC (Virtual Private Cloud)** in AWS, complete with an **internet gateway**, a **route table**, **public** and **private subnets**, **security groups**, **network interfaces**, and **EC2 instances**.

**PREREQUISITES**
Before embarking on the journey of creating a custom VPC infrastructure in AWS using Terraform, ensure you have the following in place:

**1. AWS Account:**

- 
Create an AWS account if you don't have one already. Navigate to the AWS Console at `www.aws.amazon.com` to set up your account.

**2. AWS CLI and Credentials:**

- 
Install the AWS Command Line Interface (CLI) on your local machine. You can download it here.

- 
Configure your AWS credentials using the `aws configure` command.

- 
Ensure your credentials have the necessary permissions to create and manage resources.

**3.Terraform Installation:** 

Download and install the latest version of Terraform on your local machine.

**4. Text Editor or Integrated Development Environment (IDE):**

Choose a text editor or IDE of your preference for editing Terraform configuration files. Popular choices include VSCode, Sublime Text, or Atom.
_(For this demonstration, we will be using VSCode.)_

**5. Basic Understanding of AWS Services:**

- 
Familiarize yourself with basic AWS concepts, including **VPC**, **subnetting**, **internet gateways**, **security groups**, and **EC2 instances**. This foundational knowledge will help you design an infrastructure that meets your specific requirements.

**6. SSH Key Pair:**

- 
Generate an SSH key pair if you plan to access your EC2 instances. This key pair will be used when creating EC2 instances in your Terraform configuration.

**7. Knowledge of Terraform Basics:**

- 
Understand the fundamental concepts of Terraform, such as providers, resources, variables, and modules. Refer to the official Terraform documentation at `https://developer.hashicorp.com/terraform/docs` for guidance.

**8. Customization:**

- 
Modify the provided Terraform configuration according to your specific needs. Update variables like **cidr_block**, **availability_zone**, **key_name**, and **ami** with values suitable for your project.

**9. Security Considerations:**

- 
Be mindful of security best practices. Restrict inbound and outbound traffic in your security groups to only the necessary ports and IP ranges.

**Let dive right in!**

**STEP 1: CREATE YOUR WORKING DIRECTORY AND CONFIGURATION FILES**

- 
Go to your **Git Bash** interface and type this command `cd Desktop` to navigate to the location you want to create the folder. In this very instance, we are creating it at the **Desktop**.  

- Create a Directory (folder). Type the following command `mkdir <folder name>`to create the directory.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tr4b2znhh4jsn8c44t8k.png)

- Navigate to the directory you just created using the `cd command`.

- While in the directory, use the command `code .` to open the VScode IDE. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v1hu9ibhgiaurlumk3wx.png)



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gw2te1mtmv4zhuh450dt.png)

- 
Create'main.tf' and `provider.tf` files.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wbb3vev49ommq2r8k327.png)

**STEP 2: CONFIGURE AWS ACCESS AND PROVIDER**

- 
Run the following commands: `aws --version` to confirm your version of **aws-cli** and `aws configure` to provide your AWS credentials in your **VScode terminal**.

- The credentials: 

```
AWS Access Key ID [****************ZYAS]:
AWS Secret Access Key [****************6H9/]: 
Default region name [us-east-1]: 
Default output format [json]: 
```
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jqwnnnaf0gcpvnbhgdea.png)

- Go to the official Terraform site `https://registry.terraform.io/providers/hashicorp/aws/latest/docs` to get your Terraform codes to customize for your deployment.

- In the **provider.tf** file, paste the **AWS Provider** configuration code, then save.

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

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pyh5syorgvg5jr226l90.png)

- Then run the `terraform init` command to initialize Terraform. This command will create a folder containing terraform plugins and a `.terraform.lock.hcl` file, indicating that your environment is ready to apply your deployments.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lfg2mgykckfvczmy3t1b.png)

**STEP 3 DEFINE THE VPC, INTERNET GATEWAY, AND ROUTE TABLE**

- Go to the `main.tf` file and start declaring the resources you want provisioned using the **HashiCorp Configuration Language (HCL)**.

```
# Create a VPC
resource "aws_vpc" "main-vpc" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "default"

  tags = {
    Name = "main-vpc"
  }
}

# Create an Internet Gateway
resource "aws_internet_gateway" "classgw" {
  vpc_id = aws_vpc.main-vpc.id

  tags = {
    Name = "classgw"
  }
}

# Create a Route Table
resource "aws_route_table" "classRT" {
  vpc_id = aws_vpc.main-vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.classgw.id
  }
}

```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6niikbb17owrir3h682h.png)

- 
Run the commands `terraform init`, `terraform validate`, `terraform plan` and `terraform apply` in sequence to provision the resources. You can choose to do this in batches, as we are doing here, or run the commands after declaring all your resources.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8e6i5esunty0zz7ecpaa.png)

- **Output of `terraform plan` command**

```
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_internet_gateway.classgw will be created        
  + resource "aws_internet_gateway" "classgw" {
      + arn      = (known after apply)
      + id       = (known after apply)
      + owner_id = (known after apply)
      + tags     = {
          + "Name" = "classgw"
        }
      + tags_all = {
          + "Name" = "classgw"
        }
      + vpc_id   = (known after apply)
    }

  # aws_route_table.classRT will be created
  + resource "aws_route_table" "classRT" {
      + arn              = (known after apply)
      + id               = (known after apply)
      + owner_id         = (known after apply)
      + propagating_vgws = (known after apply)
      + route            = [
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "0.0.0.0/0"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = (known after apply)
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = ""
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
      + tags_all         = (known after apply)
      + vpc_id           = (known after apply)
    }

  # aws_vpc.main-vpc will be created
  + resource "aws_vpc" "main-vpc" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.0.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_dns_hostnames                 = (known after apply)
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "main-vpc"
        }
      + tags_all                             = {
          + "Name" = "main-vpc"
        }
    }

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

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

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
```

- **Output of `terraform plan` command**

```
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_internet_gateway.classgw will be created
  + resource "aws_internet_gateway" "classgw" {
      + arn      = (known after apply)
      + id       = (known after apply)
      + owner_id = (known after apply)
      + tags     = {
          + "Name" = "classgw"
        }
      + tags_all = {
          + "Name" = "classgw"
        }
      + vpc_id   = (known after apply)
    }

  # aws_route_table.classRT will be created
  + resource "aws_route_table" "classRT" {
      + arn              = (known after apply)
      + id               = (known after apply)
      + owner_id         = (known after apply)
      + propagating_vgws = (known after apply)
      + route            = [
          + {
              + carrier_gateway_id         = ""
              + cidr_block                 = "0.0.0.0/0"
              + core_network_arn           = ""
              + destination_prefix_list_id = ""
              + egress_only_gateway_id     = ""
              + gateway_id                 = (known after apply)
              + ipv6_cidr_block            = ""
              + local_gateway_id           = ""
              + nat_gateway_id             = ""
              + network_interface_id       = ""
              + transit_gateway_id         = ""
              + vpc_endpoint_id            = ""
              + vpc_peering_connection_id  = ""
            },
        ]
      + tags_all         = (known after apply)
      + vpc_id           = (known after apply)
    }

  # aws_vpc.main-vpc will be created
  + resource "aws_vpc" "main-vpc" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.0.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_dns_hostnames                 = (known after apply)
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "main-vpc"
        }
      + tags_all                             = {
          + "Name" = "main-vpc"
        }
    }

Plan: 3 to add, 0 to change, 0 to destroy.
aws_vpc.main-vpc: Creating...
aws_vpc.main-vpc: Creation complete after 5s [id=vpc-07733c710c0a92868]
aws_internet_gateway.classgw: Creating...
aws_internet_gateway.classgw: Creation complete after 2s [id=igw-08480f88ccaf9b886]
aws_route_table.classRT: Creating...
aws_route_table.classRT: Creation complete after 2s [id=rtb-0bed4113ba3664e06]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
```

- We will repeat the above steps until we finish provisioning our resources.

- **The provisioned resources in AWS management console**


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d47kppz9om85h2cl441z.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ta59qop1hqjfj39nbzbu.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bgwol6gypbf2bsm89eun.png)

**STEP 4: CREATE PUBLIC AND PRIVATE SUBNETS**

- On the `main.tf` file, go ahead and declare the commands for the subnets to provision both **public** and **private subnets**

```
# Create public subnets
resource "aws_subnet" "main_pubs1" {
  vpc_id            = aws_vpc.main-vpc.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "us-east-1a"

  tags = {
    Name = "main_pubs1"
  }
}

resource "aws_subnet" "main_pubs2" {
  vpc_id            = aws_vpc.main-vpc.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1b"

  tags = {
    Name = "main_pubs2"
  }
}

# Create private subnets
resource "aws_subnet" "main_privs1" {
  vpc_id            = aws_vpc.main-vpc.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = "us-east-1c"

  tags = {
    Name = "main_privs1"
  }
}

resource "aws_subnet" "main_privs2" {
  vpc_id            = aws_vpc.main-vpc.id
  cidr_block        = "10.0.4.0/24"
  availability_zone = "us-east-1d"

  tags = {
    Name = "main_privs2"
  }
}

```
In this demonstration, we have two public and two private subnets.

**STEP 5: ASSOCIATE SUBNETS WITH THE ROUTE TABLE**

```
# Associate public subnets with the route table
resource "aws_route_table_association" "a" {
  subnet_id      = aws_subnet.main_pubs1.id
  route_table_id = aws_route_table.classRT.id
}

resource "aws_route_table_association" "b" {
  subnet_id      = aws_subnet.main_pubs2.id
  route_table_id = aws_route_table.classRT.id
}
```

**STEP 6:  CREATE A SECURITY GROUP**

```
# Create a security group
resource "aws_security_group" "class_SG" {
  name        = "class_SG"
  description = "Allow SSH, HTTP, HTTPS inbound traffic"
  vpc_id      = aws_vpc.main-vpc.id

  ingress {
    description = "SSH from VPC"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  ingress {
    description = "HTTPS from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTP from VPC"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
     from_port   = 0
     to_port     = 0
     protocol    = "-1"
     cidr_blocks = ["0.0.0.0/0"]
   }

  tags = {
    Name = "allow_traffic"
  }
}
```

**STEP 7: CREATE NETWORK INTERFACES**

- Create network interfaces for the two instances.
```
# Create network interfaces
resource "aws_network_interface" "class_NI" {
  subnet_id       = aws_subnet.main_pubs1.id
  private_ips     = ["10.0.1.50"]
  security_groups = [aws_security_group.class_SG.id]
}

resource "aws_network_interface" "main-NIC" {
  subnet_id       = aws_subnet.main_pubs2.id
  private_ips     = ["10.0.2.20"]
  security_groups = [aws_security_group.class_SG.id]
}
```
**STEP 8: ALLOCATE ELASTIC IPS AND ASSOCIATE WITH NETWORK INTERFACES** 

```
# Create Elastic IPs
resource "aws_eip" "class_EIP" {
  vpc                       = true
  network_interface         = aws_network_interface.class_NI.id
  associate_with_private_ip = "10.0.1.50"
}

resource "aws_eip" "class_EIP1" {
  vpc                       = true
  network_interface         = aws_network_interface.main-NIC.id
  associate_with_private_ip = "10.0.2.20"
}

```

**STEP 9: LAUNCH TWO EC2 INSTANCES**

- One in a Public Subnet and the other in a Private Subnet.
```
# Create EC2 instances
resource "aws_instance" "class_instance" {
  ami           = "ami-0005e0cfe09cc9050"
  instance_type = "t2.micro"
  key_name      = "Don-KP"
  network_interface {
    network_interface_id = aws_network_interface.class_NI.id
    device_index         = 0
  }

  tags = {
    Name = "hello1"
  }
}

resource "aws_instance" "class_instance222" {
  ami           = "ami-0005e0cfe09cc9050"
  instance_type = "t2.micro"
  key_name      = "Don-KP"
  network_interface {
    network_interface_id = aws_network_interface.main-NIC.id
    device_index         = 0
  }

  tags = {
    Name = "hello2"
  }
}

```

**Resources Created**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tm7twvmu19mruefgrzg3.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wikd8l2dt6kyddp0qxsm.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8p10i8qr8nzd673ip05s.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3u3uz8r71pku5kxp9wm4.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/77f4nmzaqpyor7n1ti94.png)

**STEP 10: DESTROY**

- Use the command `terraform destroy` to clean-up the resources provisioned to have a clean slate and avoid paying for resources not used for production. 


**CONCLUSION**
In this tutorial, we've walked through the process of creating a custom VPC infrastructure in AWS using Terraform. This modular approach allows for easy maintenance, scalability, and reproducibility of your cloud environment. By leveraging Terraform, you gain the benefits of Infrastructure as Code, making it simpler to manage and collaborate on complex AWS architectures.

**Thank you for your time!**