DEV Community

Cover image for Terraform - Place your EC2 instance in a private subnet
Rhuaridh
Rhuaridh

Posted on • Originally published at rhuaridh.co.uk

Terraform - Place your EC2 instance in a private subnet

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.

AWS Network Diagram showing private subnet and NAT gateway

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

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

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

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

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

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

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

Summary

Now that we have our terraform infrastructure, we can initialise it by running:

terraform init
Enter fullscreen mode Exit fullscreen mode

Next, we can review and launch it by running:

terraform plan
terraform apply
Enter fullscreen mode Exit fullscreen mode

Important: the above commands will cause AWS to start billing you.

Then, once we're done testing. We can run:

terraform destroy
Enter fullscreen mode Exit fullscreen mode

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)