DEV Community

Cover image for Building a Custom VPC Infrastructure in AWS with Terraform.
Bernard Chika Uwaezuoke
Bernard Chika Uwaezuoke

Posted on • Edited on

Building a Custom VPC Infrastructure in AWS with Terraform.

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

  • 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

Image description

  • Create'main.tf' and provider.tf files.

Image description

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]: 
Enter fullscreen mode Exit fullscreen mode

Image description

  • 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"
}
Enter fullscreen mode Exit fullscreen mode

Image description

  • 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

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

Enter fullscreen mode Exit fullscreen mode

Image description

  • 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

  • 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.
Enter fullscreen mode Exit fullscreen mode
  • 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.
Enter fullscreen mode Exit fullscreen mode
  • We will repeat the above steps until we finish provisioning our resources.

  • The provisioned resources in AWS management console

Image description

Image description

Image description

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"
  }
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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"
}

Enter fullscreen mode Exit fullscreen mode

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"
  }
}

Enter fullscreen mode Exit fullscreen mode

Resources Created

Image description

Image description

Image description

Image description

Image description

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!

Top comments (0)