3. Virtual Private Cloud

Classless Inter-Domain Routing (CIDR) block is a notation that allows you to specify a range of IPv4 addresses for the VPC. Visit cidr.xyz to check how many IP addresses you will have available for a certain CIDR block.

The first four IP addresses and the last IP address in each subnet CIDR block are reserved, not available for use. In a subnet with CIDR block 10.0.1.0/24 the following IP addresses are reserved:

  • 10.0.1.0/24 - Network address

  • 10.0.1.1/24 - Reserved by AWS for the VPC router

  • 10.0.1.2/24 - Reserved by AWS for IP address of DNS server

  • 10.0.1.3/24 - Reserved by AWS for future use

  • 10.0.1.255/24 - Network broadcast address

Let's create a network for our infrastructure. In terraform directory , create network directory with main.tf file.

terraform/network/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.62.0"
    }
  }

  required_version = ">= 1.0.8"
}

provider "aws" {
  region = "eu-central-1"
}

resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "TerraformWorkshopsVPC"
  }
}

Run terraform init and terraform apply commands to create a VPC, then go to VPC Dashboard on AWS Console to check what resources were created.

Along with a new VPC the following resources are created:

  • Main Route Table

  • Network Access Control List

  • Security Group

Create 4 subnets, 2 public and 2 private, 1 public and 1 private per 1 availability zone.

terraform/network/main.tf
@@ -20,3 +20,48 @@ resource "aws_vpc" "main" {
     Name = "TerraformWorkshopsVPC"
   }
 }
+
+data "aws_availability_zones" "available" {
+  state = "available"
+}
+
+resource "aws_subnet" "public_subnet_a" {
+  vpc_id            = aws_vpc.main.id
+  availability_zone = data.aws_availability_zones.available.names[0]
+  cidr_block        = "10.0.1.0/24"
+
+  tags = {
+    Name = "PublicSubnetA"
+  }
+}
+
+resource "aws_subnet" "public_subnet_b" {
+  vpc_id            = aws_vpc.main.id
+  availability_zone = data.aws_availability_zones.available.names[1]
+  cidr_block        = "10.0.2.0/24"
+
+  tags = {
+    Name = "PublicSubnetB"
+  }
+}
+
+resource "aws_subnet" "private_subnet_a" {
+  vpc_id            = aws_vpc.main.id
+  availability_zone = data.aws_availability_zones.available.names[0]
+  cidr_block        = "10.0.3.0/24"
+
+  tags = {
+    Name = "PrivateSubnetA"
+  }
+}
+
+resource "aws_subnet" "private_subnet_b" {
+  vpc_id            = aws_vpc.main.id
+  availability_zone = data.aws_availability_zones.available.names[1]
+  cidr_block        = "10.0.4.0/24"
+
+  tags = {
+    Name = "PrivateSubnetB"
+  }
+}

Apply changes and check subnet associations for the Main Route Table.

Subnets that are not explicitly associated with any route table are associated with the main route table. Never associate Internet Gateway with the Main Route Table to not expose your private resources by accident!

Let's explicitly make two subnets public. To do that we need to create a new Route Table and an Internet Gateway associated with it, and then associate subnets with the table.

terraform/network/main.tf
@@ -65,3 +65,30 @@ resource "aws_subnet" "private_subnet_b" {
     Name = "PrivateSubnetB"
   }
 }
+
+resource "aws_internet_gateway" "igw" {
+  vpc_id = aws_vpc.main.id
+
+  tags = {
+    "Name" = "TerraformWorkshopsInternetGateway"
+  }
+}
+
+resource "aws_route_table" "public_route" {
+  vpc_id = aws_vpc.main.id
+
+  route {
+    cidr_block = "0.0.0.0/0"
+    gateway_id = aws_internet_gateway.igw.id
+  }
+}
+
+resource "aws_route_table_association" "public_a_association" {
+  subnet_id      = aws_subnet.public_subnet_a.id
+  route_table_id = aws_route_table.public_route.id
+}
+
+resource "aws_route_table_association" "public_b_association" {
+  subnet_id      = aws_subnet.public_subnet_b.id
+  route_table_id = aws_route_table.public_route.id
+}

For now, the VPC configuration is ready. We need to test if it works correctly. For this purpose in terraform directory create webserver-cluster directory with main.tf file. At this point we will create a configuration required to test the VPC, later it will be transformed into a webserver cluster configuration.

terraform/webserver-cluster/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.62.0"
    }
  }

  required_version = ">= 1.0.8"
}

provider "aws" {
  region = "eu-central-1"
}

resource "aws_security_group" "public" {
  vpc_id = "???"

  ingress {
    description = "Allow SSH from everywhere"
    protocol    = "tcp"
    from_port   = 22
    to_port     = 22
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Allow outbound traffic on all ports"
    protocol    = "-1"
    from_port   = 0
    to_port     = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "private" {
  vpc_id = "???"

  ingress {
    description     = "Allow SSH from EC2 in public subnet"
    protocol        = "tcp"
    from_port       = 22
    to_port         = 22
    security_groups = [aws_security_group.public.id]
  }

  egress {
    description = "Allow outbound traffic on all ports"
    protocol    = "-1"
    from_port   = 0
    to_port     = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_key_pair" "my_ec2_key_pair" {
  key_name   = "my-ec2-key-pair"
  public_key = file("~/myEC2KeyPair.pub")
}

resource "aws_instance" "public" {
  ami                         = "ami-091f21ecba031b39a"
  instance_type               = "t2.micro"
  key_name                    = aws_key_pair.my_ec2_key_pair.key_name
  subnet_id                   = "???"
  vpc_security_group_ids      = [aws_security_group.public.id]
  associate_public_ip_address = true
}

resource "aws_instance" "private" {
  ami                    = "ami-091f21ecba031b39a"
  instance_type          = "t2.micro"
  key_name               = aws_key_pair.my_ec2_key_pair.key_name
  subnet_id              = "???"
  vpc_security_group_ids = [aws_security_group.private.id]
}

We are missing vpc_id and two subnet_id values. Create the following outputs for the network module:

terraform/network/outputs.tf
output "vpc_id" {
  description = "The VPC Id"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "Public Subnets' Ids"
  value       = [aws_subnet.public_subnet_a.id, aws_subnet.public_subnet_b.id]
}

output "private_subnet_ids" {
  description = "Private Subnets' Ids"
  value       = [aws_subnet.private_subnet_a.id, aws_subnet.private_subnet_b.id]
}

We need to get these values from the state file from the network directory. It can be done using terraform_remote_state.

terraform/webserver-cluster/main.tf
@@ -13,8 +13,20 @@ provider "aws" {
   region = "eu-central-1"
 }
 
+data "terraform_remote_state" "network" {
+  backend = "local"
+
+  config = {
+    "path" = "../network/terraform.tfstate"
+  }
+}
+
+locals {
+  vpc_id = data.terraform_remote_state.network.outputs.vpc_id
+}
+
 resource "aws_security_group" "public" {
-  vpc_id = "???"
+  vpc_id = local.vpc_id
 
   ingress {
     description = "Allow SSH from everywhere"
@@ -34,7 +46,7 @@ resource "aws_security_group" "public" {
 }
 
 resource "aws_security_group" "private" {
-  vpc_id = "???"
+  vpc_id = local.vpc_id
 
   ingress {
     description     = "Allow SSH from EC2 in public subnet"
@@ -62,7 +74,7 @@ resource "aws_instance" "public" {
   ami                         = "ami-091f21ecba031b39a"
   instance_type               = "t2.micro"
   key_name                    = aws_key_pair.my_ec2_key_pair.key_name
-  subnet_id                   = "???"
+  subnet_id                   = data.terraform_remote_state.network.outputs.public_subnet_ids[0]
   vpc_security_group_ids      = [aws_security_group.public.id]
   associate_public_ip_address = true
 }
@@ -71,6 +83,6 @@ resource "aws_instance" "private" {
   ami                    = "ami-091f21ecba031b39a"
   instance_type          = "t2.micro"
   key_name               = aws_key_pair.my_ec2_key_pair.key_name
-  subnet_id              = "???"
+  subnet_id              = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
   vpc_security_group_ids = [aws_security_group.private.id]
 }

Create outputs with IP addresses:

terraform/webserver-cluster/outputs.tf
output "public_ip_address" {
  value = aws_instance.public.public_ip
}

output "private_ip_address" {
  value = aws_instance.private.private_ip
}

Apply changes in network and then webserver-cluster directory.

To be able to connect via ssh from the EC2 instance in the public subnet to the EC2 instance in the private subnet copy your private key to EC2 instances using scp.

$ scp -i ~/myEC2KeyPair ~/myEC2KeyPair ubuntu@35.156.147.217:~/myEC2KeyPair

Connect via ssh with the EC2 instance in the public subnet and execute the following command to check whether it has a route to the Internet.

$ sudo apt-get update

Next, connect to the EC2 instance in the private subnet and perform the same test.

To connect securely to EC2 instances in private subnets use Bastion Hosts. Do not use the presented way. It's not secure. We did this only for quick testing, resources will be destroyed soon.

Last updated