Learning Terraform and AWS: Load Balancers, Target Groups, Security Groups and Launch Configurations

2018-06-22:
This is an incomplete entry. This is turning out to be a little more complex than I originally thought and rather than delay the publication further I will “publish what I have”. The date and this paragraph will get updated until this part of my series is finalized.

The 3rd part in my Virtual Private Cloud with Terraform series of posts.

Hopefully you've read part 1 and part 2 of this series. Thus far I have created a VPC and defined two sets of sub-nets to implement our application on, assigned Route Tables and Default Routes to the sub-nets and created NAT Gateways to separate the Database Servers from the Internet while retaining connectivity to the Internet.

Part 3 is really about configuring AWS so that it manages and scales our solution architecture in response to the demand experienced.

My goal is to place a Load Balancer in each Availability Zone and define Target Groups that it should route requests to. These Target Groups are populated by Instances of the WebServers. AWS Auto Scaling will add these WebServer Instances (Targets) to the Target Groups automatically.

Security Groups are associated with EC2 Compute Instances. Security Groups control network traffic to, and from, an EC2 Compute Instance. A Security Group can be associated with a set of zero or more EC2 Compute Instances, similarly an EC2 Compute Instance can be associated with one or more Security Groups (zero doesn't make sense - there would be no way to communicate with the instance).

For my purposes I want different Security Groups for my Load Balancer(s), Web Server(s) and my Database Server(s). Load Balancers should be accessible from anywhere, Web Servers only need to be accessible from the Load Balancer(s) and my Database Server(s) should only be accessible from my Web Server(s).

More specifically: I want the world to be able to access my Load Balancer(s) on ports 80 (HTTP) & 443 (HTTPS). I want the Load Balancer(s) to be able to access my Web Servers on ports 80 & 443. I want my Web Servers to be able to access my Database Server(s) on port 3306 (MySql).

I also want to be able to connect to all of these servers via SSH. This requires three additional security groups:

  1. SSH Access over the Internet from my PC
  2. Outbound SSH Access from WebServerPoolA & WebServerPoolB sub-nets
  3. Inbound SSH from WebServerPoolA & WebServerPoolB sub-nets into DatabasePoolA & DatabasePoolB sub-nets

The first Security Group needs to be able to derive my public IP address (shown in Provider & Security Groups sections below). This can be done by using the HTTP provider and then requesting my public IP Address from a server on the Internet.
To access the Database Server(s) via SSH I need two Security Groups. The first one specifically allows outbound connections to port 22 on the DatabasePoolA & DatabasePoolB sub-nets. This Security Group will be added to the WebServerPoolA & WebServerPoolB sub-nets. The second Security Group allows specifies that the first Security Group can connect to port 22.
I need to associate a Key Pair to the Launch Configuration in order to be able to SSH into the spawned Instances. You can use an existing Key Pair or create a new one.

If WebServer Instances get overloaded I want AWS to spawn additional instances ... to some maximum.

Launch Configurations and Auto Scaling Groups are used together to implement automated scaling of the solution as a work load changes. The Launch Configuration defines what an instance looks like and the Auto Scaling Group defines the minimum and maximum number of instances and the conditions to grow or shrink the group.

I am not going to scale the Database Servers. In a perfect world Database Servers would scale similarly to WebServers. For simplicity I'm just going to put a single database server into each availability zone and call it a day.

I do want to be notified when WebServer Instances are added/deleted though. I can use a SNS Topic to handle this. This is AWS Region specific - remember to configure your SNS Topic in the region you're using.

Variables

These values/definitions are used in other Terraform blocks.

Terraform


variable "Environment" {
default = "Production"
}
 
data "aws_ami" "AmazonLinuxAMI" {
most_recent = true
 
filter {
name = "name"
values = ["amzn-ami-hvm-*"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
 
owners = ["amazon"]
}
 
data "aws_acm_certificate" "LearningTerraformAndAWS" {
domain = "learningterraformandaws.myscratchpad.ca"
statuses = ["ISSUED"]
types = ["AMAZON_ISSUED"]
}
 
variable "webServerHostConfiguration" {
default = "webServerHostConfiguration.txt"
}
 
data "aws_vpc" "HoN_TF_VPC_Demo" {
filter {
name = "tag:Name"
values = ["HoN_TF_VPC_Demo"]
}
}
 
data "aws_subnet" "LoadBalancerPoolA" {
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
filter {
name = "tag:Name"
values = ["LoadBalancerPoolA"]
}
}
 
data "aws_subnet" "LoadBalancerPoolB" {
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
filter {
name = "tag:Name"
values = ["LoadBalancerPoolB"]
}
}
 
data "aws_subnet" "WebServerPoolA" {
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
filter {
name = "tag:Name"
values = ["WebServerPoolA"]
}
}
 
data "aws_subnet" "WebServerPoolB" {
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
filter {
name = "tag:Name"
values = ["WebServerPoolB"]
}
}
 
data "aws_subnet" "DatabasePoolA" {
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
filter {
name = "tag:Name"
values = ["DatabasePoolA"]
}
}
 
data "aws_subnet" "DatabasePoolB" {
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
filter {
name = "tag:Name"
values = ["DatabasePoolB"]
}
}
 
data "http" "ip" {
url = "http://icanhazip.com"
}
 
output "image_id" {
value = "${data.aws_ami.AmazonLinuxAMI.id}"
}

Load Balancers

Load Balancers have 3 parts:

  1. The Load Balancer itself
  2. Listeners - one per protocol
  3. Target Groups - where the Listeners direct requests

Additionally, because HTTPS will be supported I need to create and associate a Certificate.

In summary: I need a single Load Balancer with 2 listeners (HTTP & HTTPS) and I need it to send requests to a Target Group consisting of WebServers from both my Availability Zones.

AWS CLI


Terraform

resource "aws_lb" "HoN-TF-LoadBalancer" {
name = "HoN-TF-LoadBalancer"
internal = false
load_balancer_type = "application"
security_groups = ["${aws_security_group.LoadBalancers.id}"]
subnets = ["${data.aws_subnet.LoadBalancerPoolA.id}, ${data.aws_subnet.LoadBalancerPoolB.id}"]
 
enable_deletion_protection = false
 
tags {
Environment = "${var.Environment}"
}
}

Load Balancer Listeners

AWS CLI


Terraform

resource "aws_lb_listener" "HTTP-Listener" {
load_balancer_arn = "${aws_lb.HoN-TF-LoadBalancer.arn}"
port = "80"
protocol = "HTTP"
 
default_action {
target_group_arn = "${aws_lb_target_group.WebServerTargetGroup.arn}"
type = "forward"
}
}
 
resource "aws_lb_listener" "HTTPS-Listener" {
load_balancer_arn = "${aws_lb.HoN-TF-LoadBalancer.arn}"
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = "${data.aws_acm_certificate.LearningTerraformAndAWS.arn}"
 
default_action {
target_group_arn = "${aws_lb_target_group.WebServerTargetGroup.arn}"
type = "forward"
}
}

Target Groups

AWS CLI


Terraform

resource "aws_lb_target_group" "WebServerTargetGroup" {
name = "HoN-TF-TargetGroup"
port = 80
protocol = "HTTP"
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
target_type = "instance"
}

Security Groups

AWS CLI


Terraform

resource "aws_security_group" "LoadBalancers" {
name = "LoadBalancers"
description = "Allow HTTP and HTTPS inbound traffic"
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
 
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "HoN_TF_Demo_LoadBalancer_SG"
}
}
 
resource "aws_security_group" "WebServers" {
name = "WebServers"
description = "Allow inbound traffic from Load Balancers"
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
 
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = ["${aws_security_group.LoadBalancers.id}"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = ["${aws_security_group.LoadBalancers.id}"]
}
tags {
Name = "HoN_TF_Demo_WebServers_SG"
}
}
 
resource "aws_security_group" "DatabaseServers" {
name = "DatabaseServers"
description = "Allow inbound traffic from WebServers"
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
 
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = ["${aws_security_group.WebServers.id}"]
}
tags {
Name = "HoN_TF_Demo_DatabaseServers_SG"
}
}
 
resource "aws_security_group" "SSH-InternetAccess" {
name = "SSH-InternetAccess"
description = "Allow SSH from Internet"
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
 
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${chomp(data.http.ip.body)}/32"]
}
tags {
Name = "HoN_TF_Demo_Internet_SSHAccess_SG"
}
}
 
resource "aws_security_group" "SSH-OutboundSSH" {
name = "SSH-OutboundSSH"
description = "Allow SSH out to DatabaseServers"
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
 
egress {
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = ["${aws_security_group.DatabaseServers.id}"]
}
tags {
Name = "HoN_TF_Demo_SSHOutAccess_SG"
}
}
 
resource "aws_security_group" "SSH-InboundSSH" {
name = "SSH-InboundSSH"
description = "Allow SSH in to DatabaseServers"
vpc_id = "${data.aws_vpc.HoN_TF_VPC_Demo.id}"
 
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = ["${aws_security_group.SSH-OutboundSSH.id}"]
}
tags {
Name = "HoN_TF_Demo_SSHInAccess_SG"
}
}

Launch Configurations

Part of the Launch Configuration is configuring the Instance with the appropriate software and loading our application. The Software is installed into the instance by running the script referenced by user_data. Our application is pulled from an S3 bucket by the script. This requires that the WebServer Instance have read access (via the iam_instance_profile) to a S3 bucket containing the application.

AWS CLI


Terraform

resource "aws_launch_configuration" "WebServerLaunchConfiguration" {
name = "WebServerLaunchConfiguration"
image_id = "${data.aws_ami.AmazonLinuxAMI.id}"
security_groups = ["${aws_security_group.WebServers.id}", "${aws_security_group.SSH-InternetAccess.id}", "${aws_security_group.SSH-OutboundSSH.id}"]
key_name = "vpctestkp"
instance_type = "t2.micro"
 
iam_instance_profile = "HoN_LearningTerraformAndAWS"
user_data = "${file("${var.webServerHostConfiguration}")}"
}

Auto Scaling Groups

AWS CLI


Terraform


Placement Group

AWS CLI


Terraform

resource "aws_placement_group" "WebServers" {
name = "WebServersPlacementGroup"
strategy = "Spread"
}