Building a Private AWS VPC with Terraform + S3

Building a Private AWS VPC with Terraform + S3
Photo by Nikola Ancevski / Unsplash

Introduction

Terraform is an open-source infrastructure as code (IaC) tool developed by HashiCorp. It allows you to define and manage your infrastructure resources using a declarative configuration language. With Terraform, you can describe your desired infrastructure state in code, and Terraform will automatically provision and manage the resources needed to reach that state.

Here are some key reasons why you might need Terraform:

  1. Infrastructure Automation
  2. Cross-Platform Compatibility
  3. Infrastructure Versioning and Collaboration
  4. Infrastructure as Code (IaC) Best Practices
  5. Scalability and Consistency

Overall, Terraform simplifies infrastructure management, reduces manual effort, and provides a systematic approach to infrastructure provisioning. It is an essential tool for organizations adopting cloud computing, microservices architectures, or seeking to automate their infrastructure operations.

I'll write some Terraform codes for private AWS VPC creation in this article.

Requirements

Sample Code

I'll save all terraform states in an S3 bucket. If you will follow the article, please make sure you create the bucket just before the execution. Also, you need to make sure your AWS credentials are correct.

main.tf:

This is our main entry point for the terraform codes.  The following code block provides 3 main things.

  • s3 configuration for the state
  • Terraform provider plugin
  • Module source (All related tf files need to be in the VPC directory.)
terraform {
  required_version = ">= 0.12.0"
  backend  "s3" {
    bucket         = "habil-dev-bucket"
    key            = "prod/vpc/habil-dev.tfstate"
    region         = "eu-central-1"
  }
}

provider "aws" {
  version = "~> 4.0"
  region  = "eu-central-1"
}

module "vpc" {
  source = "./vpc"
}

vpc.tf:

This file contains the CIDR block and other DNS-specific settings.

resource "aws_vpc" "vpc" {
  cidr_block                       = "10.20.0.0/16"
  enable_dns_hostnames             = true
  enable_dns_support               = true
  assign_generated_ipv6_cidr_block = true

  tags = {
    Name        = "habil-dev-vpc"
    Environment = "dev"
  }
}

vpc_subnets.tf

This file contains public and private subnets.

// Public Subnets

resource "aws_subnet" "sn-1a-public-xlb" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.20.96.0/24"
  availability_zone = "eu-central-1a"

  tags = {
    Name        = "habil-dev-1a-public-xlb"
    CostCenter  = "habil-dev-1a-public-xlb"
  }
}

resource "aws_subnet" "sn-1b-public-xlb" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.20.97.0/24"
  availability_zone = "eu-central-1b"

  tags = {
    Name        = "habil-dev-1b-public-xlb"
    CostCenter  = "habil-dev-1b-public-xlb"
  }
}

resource "aws_subnet" "sn-1c-public-xlb" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.20.98.0/24"
  availability_zone = "eu-central-1c"

  tags = {
    Name        = "habil-dev-1c-public-xlb"
    CostCenter  = "habil-dev-1c-public-xlb"
  }
}

// Private Generic Subnets

resource "aws_subnet" "sn-1a-private-generic" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.20.104.0/21"
  availability_zone = "eu-central-1a"

  tags = {
    Name        = "habil-dev-1a-private-generic"
    CostCenter  = "habil-dev-1a-private-generic"
  }
}

resource "aws_subnet" "sn-1b-private-generic" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.20.112.0/21"
  availability_zone = "eu-central-1b"

  tags = {
    Name        = "habil-dev-1b-private-generic"
    CostCenter  = "habil-dev-1b-private-generic"
  }
}

resource "aws_subnet" "sn-1c-private-generic" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.20.120.0/21"
  availability_zone = "eu-central-1c"

  tags = {
    Name        = "habil-dev-1c-private-generic"
    CostCenter  = "habil-dev-1c-private-generic"
  }
}

vpc_route_tables.tf:

This file contains public and generic route tables.

// Public Route Table
resource "aws_route_table" "rt-1a-public-xlb" {

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

  tags = {
    Name = "habil-dev-1a-public-xlb-rt"
    CostCenter = "habil-dev-1a-public-xlb-rt"
  }

  vpc_id = aws_vpc.vpc.id
}

resource "aws_route_table" "rt-1b-public-xlb" {

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

  tags = {
    Name = "habil-dev-1b-public-xlb-rt"
    CostCenter = "habil-dev-1b-public-xlb-rt"
  }

  vpc_id = aws_vpc.vpc.id
}

resource "aws_route_table" "rt-1c-public-xlb" {

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

  tags = {
    Name = "habil-dev-1c-public-xlb-rt"
    CostCenter = "habil-dev-1c-public-xlb-rt"
  }

  vpc_id = aws_vpc.vpc.id
}

// Private Generic Route Table

resource "aws_route_table" "rt-1a-private-generic" {

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat-1a.id
  }

  tags = {
    Name = "habil-dev-1a-private-generic-rt"
    CostCenter = "habil-dev-1a-private-generic-rt"
  }

  vpc_id = aws_vpc.vpc.id
}

resource "aws_route_table" "rt-1b-private-generic" {

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat-1b.id
  }

  tags = {
    Name = "habil-dev-1b-private-generic-rt"
    CostCenter = "habil-dev-1b-private-generic-rt"
  }

  vpc_id = aws_vpc.vpc.id
}

resource "aws_route_table" "rt-1c-private-generic" {

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat-1c.id
  }

  tags = {
    Name = "habil-dev-1c-private-generic-rt"
    CostCenter = "habil-dev-1c-private-generic-rt"
  }

  vpc_id = aws_vpc.vpc.id
}

vpc_route_table_associations.tf

resource "aws_route_table_association" "rta-1a-public-xlb" {
  route_table_id = aws_route_table.rt-1a-public-xlb.id
  subnet_id = aws_subnet.sn-1a-public-xlb.id
}

resource "aws_route_table_association" "rta-1b-public-xlb" {
  route_table_id = aws_route_table.rt-1b-public-xlb.id
  subnet_id      = aws_subnet.sn-1b-public-xlb.id
}

resource "aws_route_table_association" "rta-1c-public-xlb" {
  route_table_id = aws_route_table.rt-1c-public-xlb.id
  subnet_id      = aws_subnet.sn-1c-public-xlb.id
}


// Private Generic Subnets

resource "aws_route_table_association" "rta-1a-private-generic" {
  route_table_id = aws_route_table.rt-1a-private-generic.id
  subnet_id      = aws_subnet.sn-1a-private-generic.id
}
resource "aws_route_table_association" "rta-1b-private-generic" {
  route_table_id = aws_route_table.rt-1b-private-generic.id
  subnet_id      = aws_subnet.sn-1b-private-generic.id
}
resource "aws_route_table_association" "rta-1c-private-generic" {
  route_table_id = aws_route_table.rt-1c-private-generic.id
  subnet_id      = aws_subnet.sn-1c-private-generic.id
}

vpc_nat_gateway.tf

Nat Gateway definitions.

resource "aws_nat_gateway" "nat-1a" {
  allocation_id = aws_eip.nat-1a.id
  subnet_id     = aws_subnet.sn-1a-public-xlb.id
  tags = {
    Name        = "habil-dev-1a-nat-gw"
    Environment = "dev"
    CostCenter  = "habil-dev-1a-nat-gw"
  }
}

resource "aws_nat_gateway" "nat-1b" {
  allocation_id = aws_eip.nat-1b.id

  subnet_id     = aws_subnet.sn-1b-public-xlb.id

  tags = {
    Name        = "habil-dev-1b-nat-gw"
    Environment = "dev"
    CostCenter  = "habil-dev-1b-nat-gw"
  }
}

resource "aws_nat_gateway" "nat-1c" {
  allocation_id = aws_eip.nat-1c.id

  subnet_id     = aws_subnet.sn-1c-public-xlb.id

  tags = {
    Name        = "habil-dev-1c-nat-gw"
    Environment = "dev"
    CostCenter  = "habil-dev-1c-nat-gw"
  }
}

vpc_igw.tf

Internet Gateway Definition.

resource "aws_internet_gateway" "gateway" {
  vpc_id = aws_vpc.vpc.id
  tags = {
    Name        = "habil-dev-igw"
    Environment = "dev"
    CostCenter  = "habil-dev-igw"
  }
}

vpc_eip.tf

Elastic IP definitions.

resource "aws_eip" "nat-1a" {
  vpc = true
  tags = {
    Name        = "habil-dev-nat-eip-1a"
    Environment = "dev"
    CostCenter  = "habil-dev-nat-eip-1a"
  }
}

resource "aws_eip" "nat-1b" {
  vpc = true
  tags = {
    Name        = "habil-dev-nat-eip-1b"
    Environment = "dev"
    CostCenter  = "habil-dev-nat-eip-1b"
  }
}

resource "aws_eip" "nat-1c" {
  vpc = true
  tags = {
    Name        = "habil-dev-nat-eip-1c"
    Environment = "dev"
    CostCenter  = "habil-dev-nat-eip-1c"
  }
}

Execution

The terraform init command initializes a working directory containing Terraform configuration files. This is the first command that should be run after writing a new Terraform configuration or cloning an existing one from version control. It is safe to run this command multiple times.

terraform init

The terraform plan command creates an execution plan, which lets you preview the changes that Terraform plans to make to your infrastructure. By default, when Terraform creates a plan it:

  • Reads the current state of any already-existing remote objects to make sure that the Terraform state is up-to-date.
  • Compares the current configuration to the prior state and noting any differences.
  • Proposes a set of change actions that should, if applied, make the remote objects match the configuration.
terraform plan

The terraform apply command executes the actions proposed in a Terraform plan.

terraform apply

Terraform will create a new VPC with provided configuration. Also will save the state into provided S3 bucket for you.  

If you want to destroy all your changes you need to execute following command.  

terraform destroy

Result

We have created a private AWS VPC with;

  • subnets
  • route tables
  • route table associations
  • nat gateway
  • Internet gateway
  • elastic IP

resources with our terraform files.

As always, you can find related files in my GitHub Repository:

habil-dev-blog/terraform at main · habil/habil-dev-blog
Contains materials used in blog posts. Contribute to habil/habil-dev-blog development by creating an account on GitHub.

See you in the next article.  👻