The Problem
A regular problem in AWS is that everyone is too keen to get started. They use the default VPC provided and place everything inside the public subnet.
This might work for a basic web app. However, if we have an EC2 instance running a database or any other private resource then we need to find better ways to secure them.
The solution
We are going to look at using terraform to launch an AMI inside of a private subnet to stop external access.
We will utilise a NAT Gateway to allow the ec2 instance to connect outwards for security updates.
Create provider.tf
First up, let's create a provider.tf file to let terraform know we will be using the aws provider:
provider "aws" {
region = "${var.AWS_REGION}"
}
Create vars.tf
You will notice above that we're already using a variables. Let's create our vars.tf file to store these.
variable "AWS_REGION" {
default = "eu-west-1"
}
variable "AMI" {
type = map(string)
default = {
# For demo purposes only, we are using ubuntu for the web1 and db1 instances
eu-west-1 = "ami-08ca3fed11864d6bb" # Ubuntu 20.04 x86
}
}
variable "EC2_USER" {
default = "ubuntu"
}
variable "PUBLIC_KEY_PATH" {
default = "~/.ssh/id_rsa.pub" # Replace this with a path to your public key
}
Create vpc.tf
Next up, let's create our VPC that will contain one public subnet and one private subnet. We will create this inside of the eu-west-1a availability zone:
resource "aws_vpc" "prod-vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = "true"
enable_dns_hostnames = "true"
tags = {
Name = "prod-vpc"
}
}
resource "aws_subnet" "prod-subnet-public-1" {
vpc_id = "${aws_vpc.prod-vpc.id}"
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = "true" # This is what makes it a public subnet
availability_zone = "eu-west-1a"
tags = {
Name = "prod-subnet-public-1"
}
}
resource "aws_subnet" "prod-subnet-private-1" {
vpc_id = "${aws_vpc.prod-vpc.id}"
cidr_block = "10.0.2.0/24"
availability_zone = "eu-west-1a"
tags = {
Name = "prod-subnet-private-1"
}
}
Create network.tf
Now for the interesting part. We need to create the internet gateway, and the routes for subnets to communicate. Finally we will create the NAT Gateway.
# Add internet gateway
resource "aws_internet_gateway" "prod-igw" {
vpc_id = "${aws_vpc.prod-vpc.id}"
tags = {
Name = "prod-igw"
}
}
# Public routes
resource "aws_route_table" "prod-public-crt" {
vpc_id = "${aws_vpc.prod-vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.prod-igw.id}"
}
tags = {
Name = "prod-public-crt"
}
}
resource "aws_route_table_association" "prod-crta-public-subnet-1"{
subnet_id = "${aws_subnet.prod-subnet-public-1.id}"
route_table_id = "${aws_route_table.prod-public-crt.id}"
}
# Private routes
resource "aws_route_table" "prod-private-crt" {
vpc_id = "${aws_vpc.prod-vpc.id}"
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = "${aws_nat_gateway.prod-nat-gateway.id}"
}
tags = {
Name = "prod-private-crt"
}
}
resource "aws_route_table_association" "prod-crta-private-subnet-1"{
subnet_id = "${aws_subnet.prod-subnet-private-1.id}"
route_table_id = "${aws_route_table.prod-private-crt.id}"
}
# NAT Gateway to allow private subnet to connect out the way
resource "aws_eip" "nat_gateway" {
vpc = true
}
resource "aws_nat_gateway" "prod-nat-gateway" {
allocation_id = aws_eip.nat_gateway.id
subnet_id = "${aws_subnet.prod-subnet-public-1.id}"
tags = {
Name = "VPC Demo - NAT"
}
# To ensure proper ordering, add Internet Gateway as dependency
depends_on = [aws_internet_gateway.prod-igw]
}
Create security group
Then add security group. Please note this is very open, you should limit SSH access to your IP address.
# Security Group
resource "aws_security_group" "ssh-allowed" {
vpc_id = "${aws_vpc.prod-vpc.id}"
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
// Do not use this in production, should be limited to your own IP
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "ssh-allowed"
}
}
Create ec2.tf
Now to create our sample web app. We will use an ubuntu 20.04 AMI for demo purposes.
resource "aws_instance" "web1" {
ami = "${lookup(var.AMI, var.AWS_REGION)}"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.prod-subnet-public-1.id}"
vpc_security_group_ids = ["${aws_security_group.ssh-allowed.id}"]
key_name = "${aws_key_pair.ireland-region-key-pair.id}"
tags = {
Name: "My VPC Demo 2"
}
}
// Sends your public key to the instance
resource "aws_key_pair" "ireland-region-key-pair" {
key_name = "ireland-region-key-pair"
public_key = "${file(var.PUBLIC_KEY_PATH)}"
}
Create database.tf
We will also use a ubuntu 20.04 AMI to create a demo instance that will become our private database:
# This is just a mock example of a database to test out VPCs
resource "aws_instance" "db1" {
ami = "${lookup(var.AMI, var.AWS_REGION)}"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.prod-subnet-private-1.id}"
vpc_security_group_ids = ["${aws_security_group.ssh-allowed.id}"]
key_name = "${aws_key_pair.ireland-region-key-pair.id}"
tags = {
Name: "My VPC Demo DB"
}
}
Summary
Now that we have our terraform infrastructure, we can initialise it by running:
terraform init
Next, we can review and launch it by running:
terraform plan
terraform apply
Important: the above commands will cause AWS to start billing you.
Then, once we're done testing. We can run:
terraform destroy
By utilising terraform we now have better boilerplate to let us get started quicker in future. This will heavily reduce the chances of private servers being exposed to the world.
Top comments (0)