DEV Community

Victor Modebe
Victor Modebe

Posted on • Edited on

Launching a VPC with Public subnet and Private subnet in AWS using Terraform

Image description

In this blog post I will launch a VPC and configure it with 2 subnets, one public and one private. A WORDPRESS EC2 instance will be created in the public subnet while a MYSQL instance will be created in the private subnet. I will create a Bastion host or jump box which will allow SSH connections to the MYSQL instance which is created in the private subnet. It is best practice when creating a multi-tier website with web server in public subnets and database server in private subnet so no one can have access to it since its private.

For this tutorial the wordpress will be a public facing application while the MYSQL will be the backend database server.
I will use visual studio code.

Long long ago, writing codes for all the infrastructure for testing, debugging and deploying would have consumed a good amount of valuable time. It is really difficult to tell which infrastructure as code tool was first created and released but in the early- to mid- 2000's Chef, Puppet and Ansible were first released. These tools allowed users to define and manage their infrastructure using configuration files, which enabled them to automate the provisioning and management of their infrastructure in a repeatable and predictable way. AWESOME!!! discovery right??? I read your mind LOL!!!

Over time, the concept of infrastructure as code has continued to evolve, and more specialized tools like Terraform, CloudFormation, and ARM Templates have been developed to help users manage their infrastructure more effectively. These tools provide a higher-level abstraction for defining infrastructure, and they often integrate with other tools and services to provide a more comprehensive infrastructure as code solution.

For this blog post we will use Terraform because it has several advantages over other IaC tool the major reason being that it is cloud agnostic which means it can be used to manage infrastructure on various cloud and on-premises platforms.
This makes it easy for organizations to use a single tool to manage their infrastructure across different environments, and avoid vendor lock-in.
you can read more about terraform here
Image description

In this blog post I will guide you to construct the pipeline described in the first paragraph of this blog.

Life these days is all about automation, which makes living much more easy. Productivity has increased by a large margin, tech companies delivery services much more faster as compared to the early years of software development. It will only get better and faster with Artificial Intelligence and Machine learning. What a future we have ahead of us.

Prerequisites to implement this pipeline

1. Some knowledge of AWS & Github.
2. You need to have some knowledge of JSON, arghh!!! me too, not to worry, I will simplify as much as possible. In this Pipeline, terraform is written in HCL (HashiCorp Configuration Language) & it is quite similar to JSON. Hence you will need basic knowledge of terraform
3. Basic knowledge of Docker is required
4. You will need to install AWS CLI & terraform

We are definitely making progress :) now I will list the steps required to automate the process described above using terraform. Watch and learn how will terraform bring to life the infrastructure we have described above with codes.

Steps to provision the infrastructure
1. Create a var.tf, provider.tf, main.tf, vpc.tf and output.tf files. this is where we will write our codes.
2. Create a provider inside provider.tf file.
3. Create an AWS key pair inside main.tf file.
4. Create a VPC (Virtual Private Cloud in AWS) inside vpc.tf file.
5. Create public and private subnet with map public ip on launch set to true inside vpc.tf file.
6. Create an Internet Gateway for Instances in the public subnet to access the Internet.
7. Create a route table that allows traffic to be routed through the Internet Gateway.
8. Create an association the route which will associate the route table to the public internet.
9. Create a Security Group and EC2 Instance for the Bastion Host.
10. Create a Security Group and EC2 Instance for the Wordpress .
11. Create a Security Group and EC2 Instance for the MYSQL Database.
12. Validate, plan & apply our Terraform configuration code.
13. Connect to the instances created and test SSH connection.
14. Create an outputs.tf file and input some code to display the resource id.
15. Launch a Bastion Host.
16. Final step configure Wordpress and upload some images.

Step 1
I will create a var.tf file where I will store all my variables. You can add any variable you want just remember to call the variable when you need it. which I will show in later steps.

#create variable, specify type and set to default
#create variable, specify type and set to default
variable "awsprops" {
  type = map(any)
  default = {
    region       = "us-east-1"
    ami          = "ami-0b0dcb5067f052a63"
    instancetype = "t2.micro"
    az1          = "us-east-1a"
    az2          = "us-east-1b"
  }
}

# key variable for refrencing 
variable "keyname" {
  default = "NV_R_key"
}

# base_path for refrencing 
variable "base_path" {
  default = "/Users/mac/Desktop/terraform-projects/"
}
Enter fullscreen mode Exit fullscreen mode

Step 2
I will create a provider. Notice how I called up region by using 'var.awsprops' which I defined in the variable file.

#create provider and specify provider name
provider "aws" {
  region = lookup(var.awsprops, "region")
}
Enter fullscreen mode Exit fullscreen mode

Step 3
I will create an AWS Key pair.
To authenticate with the remote host, you would need to have a copy of the corresponding public key, which you would typically add to your ~/.ssh/authorized_keys file on the client machine.

#this will create key with RSA algorithm and 4096 rsa bits
resource "tls_private_key" "private_key" {
  algorithm = "RSA"
  rsa_bits  = 4096
}
#this will create a key pair using the private key defined above  
resource "aws_key_pair" "Key-Pair" {
  key_name   = lookup(var.awsprops, "keyname")  # key name
  public_key = pls_private_key.private_key.public_key_openssh

 }

# this terraform resource will save private key at specified path.
resource "local_file" "storekey" {
  content = tls_private_key.private_key.private_key_pem
  filename = "${var.base_path}${var.key_name}.pem"
}
Enter fullscreen mode Exit fullscreen mode

Step 4
In this step we will be creating the backbone of this infrastructure called Virtual Private Cloud (VPC). We will use 10.0.0.0/16 CIDR block and enable DNS hostname, which is used to enable automatic of assignment of public DNS hostname to EC2 Instances.

#create a VPC and define the ip range
resource "aws_vpc" "prod_vpc" {
  cidr_block           = "10.0.0.0/16" #ip range for the vpc
  enable_dns_hostnames = true 

  tags = {
    Name = "Production_VPC"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5
In this step we will create subnets. One will be public and the other private. Here map_public_ip_on_launch is set to true which will automatically assign a public ip to the instance when launched.
In Terraform, the depends_on meta-argument can be used to specify that one resource depends on another resource. This means that the specified resource(s) will be created before the resource that specifies the depends_on argument.

#create public subnet
resource "aws_subnet" "prod_public_subnet" {
  depends_on = [aws_vpc.prod_vpc]

  vpc_id                  = aws_vpc.prod_vpc.id
  availability_zone       = lookup(var.awsprops, "az1")
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true #connect public subnet to the internet

  tags = {
    Name = "Public_Subnet_PROD"
  }
}

#create private subnet
resource "aws_subnet" "prod_private_subnet" {
  depends_on = [aws_vpc.prod_vpc] 

  vpc_id            = aws_vpc.prod_vpc.id
  availability_zone = lookup(var.awsprops, "az2")
  cidr_block        = "10.0.2.0/24"

  tags = {
    Name = "Private_Subnet_PROD"
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 6
In this step we will create the internet gateway. The internet gateway is a VPC component that enables instances in the VPC to connect to the public internet

#create an internet gateway for the vpc
resource "aws_internet_gateway" "prod_igw" {
  depends_on = [aws_vpc.prod_vpc]
  vpc_id = aws_vpc.prod_vpc.id
  tags = {
    Name = "prod-igw"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 7
Here we will create a route table. This will create a route in the VPC's main route table that allows traffic with a destination of 0.0.0.0/0 (i.e., all destinations) to be routed through the Internet Gateway. This will allow instances in the VPC to access the internet.

#create a route table which targets the internet gateway
resource "aws_route_table" "prod_igw_rt" {
  depends_on = [aws_vpc.prod_vpc,
  aws_internet_gateway.prod_igw]

  vpc_id = aws_vpc.prod_vpc.id

  #set default ipv4 route to use the internet gateway
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.prod_igw.id
  }
  tags = {
    Name = "Prod-Route-Table"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 8
In this step we will create route table associations which will associate the route table to the public subnet

#here we create the route table association to the Public subnet
resource "aws_route_table_association" "associate_prod_rt_public_subnet" {
  depends_on = [aws_subnet.prod_public_subnet,
    aws_route_table.prod_igw_rt]

  subnet_id      = aws_subnet.prod_public_subnet.id
  route_table_id = aws_route_table.prod_igw_rt.id
}
Enter fullscreen mode Exit fullscreen mode

Step 9
In this step we will create a security group for the bastion host and launch an EC2 instance for the Bastian host as well. A bastion host is a special server on a network specifically designed and configured to withstand attacks. It is typically used to control access to a private network from an external network, such as the internet.

We will launch a bastion host in the public subnet and from there we SSH into our instances located in the private subnet.

resource "aws_security_group" "bastion-sg" {
  depends_on = [aws_vpc.prod_vpc] #this means the vpc resource will be created before the bastion host sg

  name        = "Bastion_Host_SG"
  description = "Security Group for Bastion Host"
  vpc_id      = aws_vpc.prod_vpc.id

  ingress {
    description = "allow SSH"
    from_port   = 22
    to_port     = 22
    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"]
  }
}

# created bastion host ec2 instance
resource "aws_instance" "bastion_host" {
  ami                    = lookup(var.awsprops, "ami")
  instance_type          = lookup(var.awsprops, "instancetype")
  key_name               = var.keyname
  vpc_security_group_ids = [aws_security_group.bastion-sg.id]
  subnet_id              = aws_subnet.prod_public_subnet.id

  tags = {
    Name = "Bastion_host"
  }

  connection {
    user        = "ec2-user"
    private_key = file("/Users/mac/Desktop/terraform-projects/NV_R_key.pem")
    host        = aws_instance.bastion_host.public_ip

  }
}
Enter fullscreen mode Exit fullscreen mode

Step 10
Now we create a security group for our wordpress instance and also create resources for the wordpress instance.
In our security group we will allow port 80 for HTTP traffic from anywhere and for SSH we will allow port 22 from the bastion host security group. On the CIDR block we can input the bastion host's Ip but it will be bad practice because several conditions can cause it to change or we can use an elastic Ip (EIP) which will make it static. But for this tutorial we will add the bastion host's security group.

The 'user_data' argument can be used to specify data that should be passed to the instance when launched. We will install docker, pull Wordpress image from the docker hub while we launch a wordpress container with this image in the user_data script. Environment variables will be passed while launching wordpress so it automatically configures the MYSQL database.

resource "aws_security_group" "wordpress-sg" {
  depends_on = [aws_vpc.prod_vpc]

  name        = "Wordpress_SG"
  description = "Security Group for Wordpress EC2 Instance"
  vpc_id      = aws_vpc.prod_vpc.id

  #created an inbound rule for HTTP
  ingress {
    description = "Allow TCP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]

  }

  #created an inbound rule for PING
  ingress {
    description = "Allow PING"
    from_port   = 0
    to_port     = 0
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]

  }

  #created an inbound rule for SSH
  ingress {
    description     = "Allow SSH"
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    cidr_blocks     = ["0.0.0.0/0"]
    security_groups = [aws_security_group.bastion-sg.id]

  }

  #created an outbound rule for the wordpress
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = -1
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "Wordpress-SG"
  }

}

#created wordpress EC2 Instance
resource "aws_instance" "wordpress" {
  ami                    = lookup(var.awsprops, "ami")
  instance_type          = lookup(var.awsprops, "instancetype")
  vpc_security_group_ids = [aws_security_group.wordpress-sg.id]
  subnet_id              = aws_subnet.prod_public_subnet.id
  key_name               = var.keyname
  user_data              = <<EOF
            #! /bin/bash
            yum update
            yum install docker -y
            systemctl restart docker
            systemctl enable docker
            docker pull wordpress
            docker run --name wordpress -p 80:80 -e WORDPRESS_DB_HOST=${aws_instance.mysql.private_ip} \
            -e WORDPRESS_DB_USER=root -e WORDPRESS_DB_PASSWORD=root -e WORDPRESS_DB_NAME=wordpressdb -d wordpress
  EOF

  tags = {
    Name = "Wordpress_Instance"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 11
Now we will launch an EC2 instance for the MYSQL in the private subnet.
MySQL uses port 3306 as the default port for incoming connections. When defining egress we use protocol '-1' which is a placeholder or wildcard value or the specific protocol can be anything or not relevant/important. We are attaching the wordpress security group to mysql inbound traffic to only allow access it. We also added the bastion host security group which will only allow SSH access to the MSQL Instance.

resource "aws_security_group" "mysql-sg" {
  depends_on = [
    aws_vpc.prod_vpc,
  ]

  name        = "MYSQL_SG"
  description = "Security Group for MYSQL EC2 Instance"
  vpc_id      = aws_vpc.prod_vpc.id

  ingress {
    description     = "allow TCP"
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    cidr_blocks     = ["0.0.0.0/0"]
    security_groups = [aws_security_group.wordpress-sg.id]
  }

  ingress {
    description     = "allow SSH"
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    cidr_blocks     = ["0.0.0.0/0"]
    security_groups = [aws_security_group.bastion-sg.id]
  }

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

# created a mysql ec2 instance
resource "aws_instance" "mysql" {
  ami                    = lookup(var.awsprops, "ami")
  instance_type          = lookup(var.awsprops, "instancetype")
  key_name               = var.keyname
  vpc_security_group_ids = [aws_security_group.mysql-sg.id]
  subnet_id              = aws_subnet.prod_public_subnet.id
  user_data              = file("mysqlconfig.sh")

  tags = {
    Name = "MSQL-Instance"
  }
}
Enter fullscreen mode Exit fullscreen mode

From above We will pass a script in 'user_data' but this time we save the file locally and call it up with the file command. The file is a bash file hence it will be saved with suffix '.sh' ("mysqlconfig.sh") with configurations below. Make sure to input the latest version of mysql to avoid compatibility issues. The latest version when writing this blog is 8.0.31.

#! /bin/bash
yum update
yum install docker -y
systemctl restart docker
systemctl enable docker
docker pull mysql
docker run --name mysql -e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=wordpressdb -p 3306:3306 -d mysql:8.0.31
Enter fullscreen mode Exit fullscreen mode

Step 12
We have written code for our infrastructure using terraform, which took some time and was a bit technical. Now we will use some terraform commands to check if our code is valid, create an execution plan and finally apply changes based on the desired state defined in our configuration files.

#validate syntax and overall structure our our terraform configuration
terraform validate
Enter fullscreen mode Exit fullscreen mode
#generate execution plan and view changes made to our configurations
terraform plan
Enter fullscreen mode Exit fullscreen mode

Image description

#apply changes to infrastructure 
terraform apply
Enter fullscreen mode Exit fullscreen mode

If you are on the right track you should get the prompt below after the infrastructure has deployed on AWS

[Image description ]

Step 13
connect to the wordpress EC2 instance via ssh or via EC2 Instance connect from the AWS management console. Once you are connected follow the prompt to configure wordpress. You can also copy the IP address of the wordpress paste on your browser then configure Wordpress.

Image description

Step 14
You can optionally create an outputs.tf file. In Terraform, you can use output variables to display the values of resources created by a configuration.

output "us-east-1a-Public-IP" {
  value       = aws_instance.wordpress.public_ip
  description = "The Public IP address of the Web Server"
}

output "us-east-1b-Public-IP" {
  value       = aws_instance.bastion_host.public_ip
  description = "The Public IP address of Bastion Host"
}

output "us-east-1a-Private-IP" {
  value       = aws_instance.mysql.private_ip
  description = "The Private IP address of MySQL Database"
}

output "vpc_id" {
  value       = aws_vpc.prod_vpc.id
  description = "The Virtual Private Cloud(VPC) ID "
}

Enter fullscreen mode Exit fullscreen mode

You can then use the terraform output command to display the values of these output variables after running the terraform apply command.

$ terraform output prod_vpc_id
vpc-06578e6056e120939

$ terraform output us-east-1a-Public-IP
54.164.193.1
Enter fullscreen mode Exit fullscreen mode

FINAL STEP
This is the final view once wordpress has been configured, you can upload random images or type some text to your blog.

Image description

We have succeeded in creating a VPC with 1 Public & 1 Private Subnet using Terraform. We also deployed wordpress and mysql using docker while passing user data to the instance. Remember to type 'terraform destroy' to destroy all the infrastructure deployed to avoid incurring unnecessary charges.

I hope you learnt a lot from this article good luck on your cloud journey :)

You can get the full code from my GitHub repo below

Image description

Top comments (0)