Terraform - Place your EC2 instance in a private subnet
Tags: terraform, aws, security
Published: 2022-03-06 13:52:33

---
title: Terraform - Place your EC2 instance in a private subnet
published: true
description: How to create a VPC, private subnet and NAT gateway using terraform
tags: terraform,aws,security
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bi9usyyhphs6lj54eq7g.png
canonical_url: https://rhuaridh.co.uk/blog/aws-private-subnet.html
---

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f3mud2e0akr0tr8bjip5.png)

### 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.