Recently, I had to re-visit an old architectural challenge in AWS that I came across a couple of years ago but couldn’t quite remember how to resolve: EC2 instances which have no public IP address (residing in a private subnet) which can accept requests from (and serve responses back to) the internet.

The motivation for this particular topology is security focused: The EC2 instances cannot be directly addressed via the internet due to the absence of a public IP, but can serve (web, in this example) content over the internet using the load balancer.

A bastion server is included in this example, for the purposes of getting SSH access to the EC2 webservers, in the private subnet.

Code

I decided to create the whole environment in AWS using terraform to revisit this challenge and the code can be found here, on GitHub

Environment Diagram

When the terraform code is deployed it creates an environment in AWS which looks like this (click to enlarge):

AWS Architecture Diagram (click to expand)

Deployed Resources

The example terraform code will deploy the following resources:

  • 1 x VPC
  • 4 x Subnets
    • 2 x Private Subnets
    • 2 x Public Subnets
  • 1 x Internet Gateway
    • 1 x Routing Table for the Public Subnets
  • 1 x NAT Gateway
    • 1 x Routing Table for the Private Subnets
  • 2 x Security Groups
    • 1 x Load Balancer Security Group
    • 1 x EC2 Instance Security Group
  • 2 x (t3.micro) EC2 webserver instances
    • User data script will install and configure the apache web server
  • 1 x (t3.micro) EC2 bastion instance
  • 1 x Application Load Balancer (ALB)
    • 1 x ALB Listener (configured to listen on port 80 for http)
    • 1 x Target Group (containing the EC2 instances which will listen on port 80)

The isolation of the EC2 instances is achieved by the creation of two public and two private subnets. The public subnets do not contain any EC2 instances, but are critical to this design. It is therefore necessary to have an “empty” public subnet for every private subnet which contains EC2 instances.

The EC2 instances sit inside the Application Load Balancer’s target group, but the Load Balancer’s network mappings point to the empty public subnets.

Load Balancer Network Mappings (click to expand)

Requests come into the load balancer (via the internet gateway) on port 80 and are then routed to the EC2 instances. Return requests are then routed back out to the requestor via the NAT gateway

Post Install (user-data) Script

For the purposes of this demonstration, I wrote a very simple post instal (user-data) script which configures the EC2 instances to run an apache web server, with a very simple index.html page which displays the hostname of each EC2 instance:

#!/bin/bash

yum install -y httpd
systemctl start httpd
systemctl enable httpd

echo "<html><h1>$(hostname)</h1></html>" > /var/www/html/index.html

Terraform Outputs

The terraform code will provide a single output, which is the URL of the Application Load Balancer. When the page is refreshed, the page should alternately display the hostnames of each instance.