How to deploy an Nginx web server on EC2 instances without public access using ansible and an application load balancer on AWS

Mathildaduku
11 min readJan 18, 2023

--

Hi guys, I’ll be showing you how to deploy an Nginx web server on private instances with ansible and use an application load balancer to route requests to the instances on AWS.

At the end of this article you should be able to:

  • Create a private EC2 instance on AWS
  • Create a bastion and NAT instance on AWS
  • Deploy an Nginx web server on private instances with ansible
  • Set up a load balancer to route requests to private instances

Creation of EC2 instances

I already have my VPC attached to an internet gateway, 2 private subnets, 2 public subnets, a public route table and a private route table already set up. You should too but no worries if you don’t, I have a tutorial for that already, check out my previous post here. Get it done in a few minutes then come back here let the fun begin.

We would be creating 4 instances; 2 private instances, 1 bastion instance to allow us have access to the private instances, 1 NAT(Network Address Translation) instance to allow the private instances send requests to the internet.

We will start out by creating a NAT instance, this would be used to allow the private instances connect to the internet

Click on services and search EC2.

Click Launch instances.

Input your desired name and search for a NAT instance in the community AMI. This is preconfigured to function almost like a NAT gateway for the private instances we would be creating along the line. It prevents the instances from being directly exposed to the internet while simultaneously helping the instances connect to the internet.

Select a pre- existing or new key pair that would allow you log into the instance.

In network settings, select the VPC created, choose one of the public subnet, enable auto-assign public IP. Create a new security group and give it a name. Add 2 security group rules to allow HTTP traffic from the private subnets. Select launch instance.

Next, we create the bastion instance, give it a name and select your desired OS, I’ll be choosing Ubuntu 22.04. Select your key pair. In network settings, select the VPC created, choose a public subnet, enable auto assign public IP. Create a new security group and give it a name. Leave everything as it is and launch instance.

Now we create the private instances. give a name, select your desired OS, I chose Ubuntu 22.04. Select your key pair. In network settings, select the VPC created, select a private subnet, disable auto-assign public IP. This would ensure our instance is truly “private”. Create a security group, give it a name and launch instance.

In creating the second private instance, we would replicate what we did for the first private instance BUT we would select a DIFFERENT private subnet which is in another availability zone. Select the security group created in the private subnet and launch instance.

We have our 4 instances up and running.

Edit private route table

Since our private instances cannot access the internet, we want to redirect any traffic from the private instances to the NAT instance.

Go to route tables under VPC and edit routes for the private route table. Add route, under destination, type 0.0.0.0/0, under target, type instance and select the NAT instance we created earlier and save changes.

Go to the instances and select the NAT instance, click actions, go to networking and select change source/destination check, tick stop and save. This ensures the NAT instance accepts the traffic from the private instances seamlessly.

Deploy Nginx web server on private instances

The bastion instance would enable us ssh into the private instances as they do not have a public IP and they are completely secluded. It is also going to act as the ansible control node for the target nodes which in this case are the 2 private instances.

Select the bastion instance and connect. Copy the command given by AWS and paste it in your terminal.

Ensure the key pair used to create the instance is in the same directory you want to run the command and also run chmod 400 keyPairName before hand.

Now we are inside the bastion server. Copy the content of the key pair used to create the private instances and paste it in a new file with the same name as the key pair.

Change the file permissions

chmod 400 keyPairName

Now we can easily ssh into any of the private instances using the command.

ssh userName@ipAddress -i keyPairName

This is essential for our ansible playbook to run smoothly.

Wow! Look how far you’ve come, you can create and connect instances on AWS, let’s take it further shall we?

I’ll be creating a file index.php, this will contain the code that will show the IP address of the instances.

nano index.php

The code is below

<!DOCTYPE html>
<html>
<body>
<h2> hello world this is my servers ip address! </h2>
<h1>
<?php
echo "hostname is:" .gethostname();
?>
</h1>
</body>
</html>

Next we create another file named “default” this contains the nginx configuration script.

nano default

The “default” file should contain the following code;

##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

# Default server configuration
#
server {
listen 80 default_server;
listen [::]:80 default_server;

# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;

root /var/www/html;

# Add index.php to the list if you are using PHP
index index.php index.html index.htm index.nginx-debian.html;

server_name _;

location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}

# pass PHP scripts to FastCGI server
#
location ~ \.php$ {
include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
location ~ /\.ht {
deny all;
}
}


# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
# listen 80;
# listen [::]:80;
#
# server_name example.com;
#
# root /var/www/example.com;
# index index.html;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}

So we are going to install ansible on the bastion instance to automate the installation of nginx on the private instances and also display the IP address of the instances.

sudo apt update
sudo apt upgrade
sudo apt install ansible

Next, we create the ansible configuration file ansible.cfg, it should contain the code below:

[defaults]
inventory = inventory
private_key_file = ~/private.pem

Create an inventory file that would contain the ip address of the private instances.

Create the ansible playbook main.yml, which would contain the following automation script to set up nginx on the servers and display their ip address.

---

- hosts: all
become: yes
tasks:

- name: Update and upgrade the servers
apt:
update_cache: yes
upgrade: yes

- name: Install nginx on the servers
apt:
name: nginx
state: latest

- name: Install PHP process manager
apt:
name: php8.1-fpm
state: latest

- name: Discard the nginx index file
file:
path: /var/www/html/index.nginx-debian.html
state: absent

- name: Discard the nginx default file
file:
path: /etc/nginx/sites-available/default
state: absent

- name: Copy the new nginx default file to the servers
copy:
src: default
dest: /etc/nginx/sites-available
owner: root
group: root
mode: 0744

- name: Add the php file to the servers
copy:
src: index.php
dest: /var/www/html
owner: root
group: root
mode: 0744

- name: Restart the nginx service
service:
name: nginx
state: restarted
enabled: yes

Run the playbook with the code

ansible-playbook main.yml -i inventory

Set up the load balancer

Go back to the AWS console and select target groups, then click create target group.

Choose instances and give the target group a name. Select the VPC we created and click next.

Choose only the private instances, select include as pending below, click create target group.

Select the target group and associate with a new load balancer. Now we create a load balancer. Give the load balancer a name.

Select the VPC we created, choose the two public subnets created.

Create new security group and you will be redirected to another page. Give the security group a name, select the VPC created.

Under Inbound rules, allow port 80(HTTP) and port 443(HTTPS) from anywhere (both IPv4 and IPv6) and create security group.

Going back to the load balancer, click the refresh icon next to security group and select the load balancer security group created.

Under listeners and routing, select the target group we created and create load balancer.

Head over to security groups and select the private security group created earlier and edit inbound rule.

Add rule, under type select HTTP, under source choose custom, search for load balancer and select the load balancer security group we created and save rules.

Go to the load balancer, copy the DNS name and paste in your browser, you should see your script running with the IP address of the server showing, and when you refresh the page, the IP address should change to that of the other private server. Pretty cool right?

Conclusion

In this article, we learnt how to create public and private EC2 instances on AWS, how to deploy an Nginx web server on private instances with ansible and how to set up a load balancer to route requests to private instances.

Thank you for reading!! Don’t forget to leave a comment and share to someone that would need this :)

--

--