...So we were given this task in the Altschool School of Cloud Engineering to setup an Nginx load balanced application using AWS, having learnt about Ansible, I decided to automate the task using Ansible's IaC capabilities.
Load balancing your applications help you attain high availability/up time. Imagine you were tasked to provision 10+ servers in various availability zones (AZ) using the AWS console, I bet that would be a pain and boring task which can also lead to inconsistencies and human error.
In this article, I will walk you through how we can provision a load balanced application using Ansible which makes it really efficient to provision your infrastructures consistently.
By the end of this article, we'll get the following setup and running with Ansible;
- VPC with 2 public and private subnets
- Security Groups to manage traffic within our VPC
- Internet Gateway
- NAT Gateway
- Two EC2 AMIs running with nginx server
- ALB to balance our requests in the AZs
Architecture Overview
image credit: AWS
Prerequisites:
- AWS CLI
- Boto3 Library
- IAM user role with EC2 & Elastic Load Balancing access (following the principle of least privilege)
- Ansible
- Ansible collection for AWS
ansible-galaxy collection install community.aws
- Optional - AWS Vault - you might want to also setup aws-vault for your IAM user role access key and secret key (recommended)
Let's get started;
Create/update your inventory file: hosts.yml
I will be using my local machine as the control node, a bastion host or Ansible tower can suffice.
---
localhost:
hosts:
127.0.0.1:
Playbook file: main.yml
---
- name: ansible aws alb
hosts: localhost
gather_facts: no
tasks:
1. AWS VPC: Think of a VPC as your own data centre where you will setup your traditional network and add subnets, but here, everything is virtual and AWS helps you with scaling without much thinkering on your part.
As a private network, we would use a private Classless Interdomain Routing (CIDR) block. As a rule of thumb, let's take note of the AWS rules for creating a best practice VPC;
- CIDR block size must be between /28 & /16 subnet mask.
- The CIDR block must not overlap with any existing CIDR block that's associated with the VPC.
- You cannot increase or decrease the size of an existing CIDR block.
- The first four and last IP addresses are not available for use, this is a very important point to note so you have enough addresses for the instances and resources you deploy.
- Use CIDR blocks from the RFC 1918 ranges.
Using this 10.0.0.0/16
CIDR block, we have enough networks and hosts per network.
N.B: I would only use IPv4 addresses in this article.
We'll use the register
to retrieve/reference variables.
- name: setup altschool vpc.
amazon.aws.ec2_vpc_net:
name: Altschool VPC
cidr_block: 10.0.0.0/16
region: us-east-1
tags:
module: ec2_vpc_net
this: altschool_vpc
register: vpc_id
2. SUBNETS FOR OUR VPC: A subnet is a range of IP addresses in your VPC, here, we will create four subnets; 2 public and 2 private. You can leverage this IP Subnet Tool to help you create your subnets.
Public Subnet: This subnet will have a direct route to an internet gateway as in our diagram, this is where we would launch our ALB later on. Make sure to create them in more than one AZs (ex: us-east-1a
& us-east-1b
) for high availability; this helps your load balancer to keep serving your users from the available zones when one server in an AZ goes down while you bring them back up.
# public subnet 1
- name: create Public 1A subnet
amazon.aws.ec2_vpc_subnet:
state: present
cidr: 10.0.1.0/24
region: us-east-1
az: us-east-1a
vpc_id: "{{ vpc_id.vpc.id }}"
tags:
Name: Public 1A
register: public_1a
# public subnet 2
- name: create Public 1B subnet
amazon.aws.ec2_vpc_subnet:
state: present
cidr: 10.0.2.0/24
region: us-east-1
az: us-east-1b
vpc_id: "{{ vpc_id.vpc.id }}"
tags:
Name: Public 1B
register: public_1b
Private Subnet: This subnet does not have a direct route to an internet gateway. Resources in a private subnet require a NAT device/gateway to access the public internet. We'll launch our ec2 instances in this subnet. Make sure to create them in different AZs for high availability.
# private subnet 1
- name: create Private 1A subnet
amazon.aws.ec2_vpc_subnet:
state: present
cidr: 10.0.3.0/24
region: us-east-1
az: us-east-1a
vpc_id: "{{ vpc_id.vpc.id }}"
tags:
Name: Private 1A
register: private_1a
# private subnet 2
- name: create Private 1B subnet
amazon.aws.ec2_vpc_subnet:
state: present
cidr: 10.0.4.0/24
region: us-east-1
az: us-east-1b
vpc_id: "{{ vpc_id.vpc.id }}"
tags:
Name: Private 1B
register: private_1b
3. INTERNET GATEWAY: The internate gateway allows communication between our VPC and the internet, here, we need to route internet-bound traffic to the internet gateway from the public subnet where our load balancer sits.
- name: create internet gateway
amazon.aws.ec2_vpc_igw:
vpc_id: "{{ vpc_id.vpc.id }}"
state: present
tags:
Name: Altschool igw
register: igw_id
4. NAT GATEWAY: We need a NAT device to allow our private ec2 instance access to donwload initial updates and install the neccessary components. You can decide to use a the AWS managed NAT gateway or choose to create your own NAT instance, here I'll be using a NAT gateway. NAT gateways are paid, make sure to review its pricing, also remember to shut it down when done configuring your instances, you can always attach a new NAT gateway later on.
- name: Create new nat gateway
amazon.aws.ec2_vpc_nat_gateway:
state: present
subnet_id: "{{ public_1a.subnet.id }}"
if_exist_do_not_create: yes
release_eip: true
wait: true
region: us-east-1
register: natgw_id
5. ROUTE TABLES: We need to create route tables for the internet gateway and NAT gateway. By default, AWS creates a route table for every VPC.
Internet Gateway Route Table: We create a router to route traffic from anywhere on the internet 0.0.0.0/0
to our internet gateway. This is going to be our main route table because we'll also associate our public subnets to it.
- name: create internet gateway route table
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc_id.vpc.id }}"
region: us-east-1
tags:
Name: Public Web
subnets:
- "{{ public_1a.subnet.id }}"
- "{{ public_1b.subnet.id }}"
routes:
- dest: 0.0.0.0/0
gateway_id: "{{ igw_id.gateway_id }}"
register: public_route_table
NAT Gateway Route Table: We also create another route table to route traffic from the internet to our NAT gateway and then associate our private subnets to it.
- name: create NAT gateway route table
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc_id.vpc.id }}"
region: us-east-1
tags:
Name: Private Web
subnets:
- "{{ private_1a.subnet.id }}"
- "{{ private_1b.subnet.id }}"
routes:
- dest: 0.0.0.0/0
gateway_id: "{{ natgw_id.nat_gateway_id }}"
register: private_route_table
6. SECURITY GROUPS: These are stateful traffic controllers that determine the traffic allowed to reach a certain resource attached to it. We'll create ALB & Instance security groups.
ALB Security Group: For this, we need to create an inbound rule to allow http traffic on port 80 for our load balancer, we will keep the default outbound rules as we would be deploying a static app.
- name: create ALB security group
amazon.aws.ec2_security_group:
name: alb-sg
description: allow internet access to ALB
vpc_id: "{{ vpc_id.vpc.id }}"
region: us-east-1
tags:
Name: Altschool alb sg
rules:
- proto: tcp
ports:
- 80
cidr_ip: 0.0.0.0/0
rule_desc: allow http traffic from anywhere
register: alb_sg
Instance Security Group: We also create a rule to allow inbound ssh traffic to our local machine IP (optional) and http traffic from the ALB security group.
- name: create private ec2 instance security group
amazon.aws.ec2_security_group:
name: altschool-ec2-sg
description: allow internet-facing ALB access
vpc_id: "{{ vpc_id.vpc.id }}"
region: us-east-1
tags:
Name: Altschool sg
rules:
- proto: tcp
ports:
- 22
# replace the IP with your host machine IP
cidr_ip: 197.xxx.xx.xx/32
rule_desc: allow ssh traffic from local machine
- proto: tcp
ports:
- 80
group_id: "{{ alb_sg.group_id }}"
rule_desc: allow traffic from ALB security group
register: ec2_sg
7. TARGET GROUP: We need to create a target group for our load balancer, we'll register our targets later on.
- ensure you already setup ansible community module for this.
- name: Create a target group with a default health check
community.aws.elb_target_group:
name: altschool-tg
protocol: http
port: 80
vpc_id: "{{ vpc_id.vpc.id }}"
state: present
8. APPLICATION LOAD BALANCER: Go ahead and configure the ALB in the public subnet.
- name: setup load balancer
amazon.aws.elb_application_lb:
name: altschool-alb
subnets:
- "{{ public_1a.subnet.id }}"
- "{{ public_1b.subnet.id }}"
security_groups:
- "{{ alb_sg.group_id }}"
region: us-east-1
listeners:
- Protocol: HTTP
Port: 80
DefaultActions:
- Type: forward
TargetGroupName: altschool-tg
state: present
register: altschool_lb
9. EC2 AMIs: We go ahead and launch our ec2 instances using a simple user data script to install Nginx and display the hostname
of our individual servers. Here, I used the AWS linux 2 AMI image ami-0b5eea76982371e91
you can use any distro of your choice, be sure to specify them appropriately.
Optional: Import a Key Pair. Before launching an EC2 instance, it is recommended to create a key pair or attach an existing key, this will enable you access your private instances later using their private IPs. Here I'll choose to import a key pair with the default name of each OS. In this case, we'll use ec2-user since we'll be using Amazon Linux 2 AMI. Make sure to create one beforehand and name it appropriately (eg: ec2-user) from the AWS console.
# change the user-name path accordingly
- name: import ec2 key pair
amazon.aws.ec2_key:
name: ec2-user
key_material: "{{ lookup('file', '/home/user-name/.ssh/id_rsa.pub') }}"
tags:
- ec2_create
- ec2_keypair
Write this script in a file and specify the location in the lookup file user_data.sh
:
#!/bin/bash
yum update -y
amazon-linux-extras install nginx1.12
systemctl start nginx
systemctl enable nginx
echo "<h1>This server has the IP: $(hostname -f)</h1>" > /usr/share/nginx/html/index.html
- name: create server1 ec2 instance with user data
amazon.aws.ec2_instance:
name: server1
region: us-east-1
key_name: ec2-user
instance_type: t2.micro
security_group: "{{ ec2_sg.group_id }}"
vpc_subnet_id: "{{ private_1a.subnet.id }}"
network:
assign_public_ip: false
delete_on_termination: true
image_id: ami-0b5eea76982371e91
user_data: "{{ lookup('file', 'user_data.sh') }}"
tags:
- ec2_create
register: server1
Repeat same for server 2
- name: create server2 ec2 instance with user data
amazon.aws.ec2_instance:
name: server2
region: us-east-1
key_name: ec2-user
instance_type: t2.micro
security_group: "{{ ec2_sg.group_id }}"
vpc_subnet_id: "{{ private_1b.subnet.id }}"
network:
assign_public_ip: false
delete_on_termination: true
image_id: ami-0b5eea76982371e91
user_data: "{{ lookup('file', 'user_data.sh') }}"
tags:
- ec2_create
register: server2
10. ATTACH INSTANCES TO TARGET GROUP Finally, we now attach our private instances to our load balancer target group.
# Register server1 & 2 instance targets to our target group
- name: Register targets
community.aws.elb_target:
target_group_name: altschool-tg
target_id:
- "{{ server1.instances[0].instance_id }}"
- "{{ server2.instances[0].instance_id }}"
state: present
Checkout the final playbook.
Go ahead and run the playbook.
N.B >> Using the Ansible --check
flag fails on some modules like amazon.aws.ec2_vpc_net
etc, doesn't mean they are wrong)
You should have similar results if there are no errors :)
Please leave a comment if you've got better ways to implement this, I'll be in the comment section. Ka emesia.