Build and Deploy an Nginx Load Balancing Infrastructure using Ansible and Vagrant

Build and Deploy an Nginx Load Balancing Infrastructure using Ansible and Vagrant

ยท

7 min read

This article will walk you through building the infrastructure in the diagram below and deploying a simple python application. main_structure.jpeg

Before we begin, let's get a quick understanding of what load balancing is.

In a hypothetical situation, you are contracted to build a web application for a small pizza delivery business. You do good work and the client is satisfied. The business gets very popular quickly causing a huge increase in the number of visits to the website, but the website suddenly begins to malfunction. After troubleshooting, you find out that the downtime is a result of the server's inability to handle the traffic surge. How do you fix this?

This is where load balancing comes in. The solution to this problem is to increase the number of servers so that instead of user requests hitting just one over and over, there's another server to handle the load too. Load balancers are servers that forward internet traffic to multiple servers. They distribute workload evenly across multiple servers to maximize speed and performance and prevent downtime. User requests go to the load balancer which then decides how to distribute the load across the servers.

Now let's dive in!

Setup requirements for your local machine:

1) The first thing you need to do is create the virtual machines using vagrant. We will define three VMs for this infrastructure - the load balancer, server 1, and server 2. Create a Vagrantfile in your project directory.

$ touch Vagrantfile

We will use the ubuntu/bionic64 box for the three VMs. Put the following code in your Vagrantfile.

Vagrant.configure(2) do |config|

    config.vm.box = "ubuntu/bionic64"


    # configure loadbalancer
    config.vm.define "load-balancer" do |loadbalancer|
      loadbalancer.vm.network "private_network", ip: "192.168.33.11"
      loadbalancer.vm.hostname = "load-balancer"
      loadbalancer.vm.network "forwarded_port", guest: 80, host: 8080
      loadbalancer.vm.provider "virtualbox" do |vb| 
        vb.memory = 512
        vb.customize ["modifyvm", :id, "--audio", "none"]
      end

    end


    #configure web servers

    config.vm.define "server-1" do |server_1|
      server_1.vm.network "private_network", ip: "192.168.33.12"
      server_1.vm.hostname = "server-1"
      server_1.vm.provider "virtualbox" do |vb| 
        vb.memory = 512
        vb.customize ["modifyvm", :id, "--audio", "none"]
      end

    end   


    config.vm.define "server-2" do |server_2|
      server_2.vm.network "private_network", ip: "192.168.33.13"
      server_2.vm.hostname = "server-2"
      server_2.vm.provider "virtualbox" do |vb| 
        vb.memory = 512
        vb.customize ["modifyvm", :id, "--audio", "none"]
      end
   end


  # update the packages on the VMs
   config.vm.provision "shell", inline: <<-SHELL   
      apt-get update

    SHELL

end

2) Next, launch the VMs. Open a terminal in your present working directory and run

$ vagrant up

3) Since we are using Ansible for configuration management, we need to write Ansible manifests to configure the VMs. These manifest files are written in a data serialization language called YAML and are called Playbooks. Create a folder in the directory called ansible.

$ mkdir ansible

4) Create an ansible.cfg file within the ansible folder. The ansible.cfg file contains Ansible's default configuration.

$ touch ansible.cfg

Now let's specify some default settings in the file

[defaults]
gathering = smart

host_key_checking = false

inventory = ./hosts   # the location of your hosts file

5) Ansible needs to know which hosts to use when running a playbook. Hosts are declared and grouped in a file that is known as the inventory. Create a hosts file within the ansible folder and put the hosts from the Vagrantfile created earlier along with their IP addresses in it.

$ touch hosts

server-1 ansible_host=192.168.33.12  
server-2 ansible_host=192.168.33.13  

load-balancer ansible_host=192.168.33.11

Put the hosts into groups

[webservers]
server-1
server-2

[loadbalancer]
load-balancer

There is a default group in Ansible called all that contains every host. We will use this group to define variables that we want to apply to all available hosts

[all:vars]
ansible_user=vagrant 
ansible_ssh_pass=vagrant
ansible_python_interpreter=/usr/bin/python3

6) Now we create the playbooks.

First, create a general playbook that imports the other playbooks when referenced

$ touch site.yml
---
  - import_playbook: nginx_playbook.yml
  - import_playbook: webservers_playbook.yml

nginx_playbook.yml and webservers_playbook.yml will contain automation tasks for Nginx and the web servers respectively.

7) Create the webservers_playbook.yml file

$ touch nginx_playbook.yml
---
- hosts: all   #Apply these tasks to host group 'all'
  tasks:
  - name: Install pip
    apt: 
       name: python3-pip 
       state:  present
    become: yes    

  - name: Install Flask
    command: pip3 install -U Flask
    become: yes   

  - name: Create app  #Create a flask app in the home directory of the VMs
    become: yes
    copy:
      dest: "/home/vagrant/hello.py"
      content: |

        from flask import Flask

        app = Flask(__name__)


        @app.route('/')
        def index():

            return "Hello from IP" 

        if __name__ == '__main__':
                    app.run(debug = True, host='0.0.0.0', port='5000')


  - name: Get IP and insert in the app   # "IP" in the flask app should be the host name of the server
    shell: sed -i "s/IP/$(hostname)/g" /home/vagrant/hello.py
    become: yes

  - name: Create a service for the app   #Create a service to start the flask application automatically
    become: yes
    copy:
      dest: "/etc/systemd/system/app.service"
      mode: 0664 
      owner: vagrant 
      content: | 
        [Unit]
        Description=Flask application

        [Service]
        ExecStart=/usr/bin/python3 /home/vagrant/hello.py

        [Install]
        WantedBy=multi-user.target

  - name: Start app  #Start the application by running the service created earlier 
    become: yes
    systemd:   
      state: started 
      daemon_reload: yes 
      name: app.service
      enabled: yes

8) We need to create an Nginx configuration file that would tell the Nginx load balancer to balance the load between server-1 and server-2.

$ touch nginx.cfg

Write the configuration settings

upstream backend  {
  server 192.168.33.12:5000;  #server-1
  server 192.168.33.13:5000;  #server-2
}

server {
  listen 80;  #listen on port 80

  server_name web.app;
  location / {
    proxy_read_timeout 300s;
    proxy_pass  http://backend;  #pass all requests processed to the backend upstream servers
  }
}

9) Create the nginx_playbook.yml file to automate the load balancing tasks

$ touch nginx_playbook.yml

Put the following tasks in the nginx_playbook.yml file

---
- hosts: all
  tasks:
    - name: Ensure Nginx is installed and it is the latest version
      apt: 
         name: nginx 
         state: latest
      become: yes

    - name: Start Nginx
      service:
          name: nginx
          state: started
      become: yes

    - name: Copy the Nginx config file and restart nginx
      copy:
        src: ./nginx.cfg
        dest: /etc/nginx/sites-available/nginx.cfg
      become: yes

    - name: Create symlink
      file:
        src: /etc/nginx/sites-available/nginx.cfg
        dest: /etc/nginx/sites-enabled/default
        state: link
      become: yes

    - name: Restart nginx
      service:
        name: nginx
        state: restarted
      become: yes

10) The playbooks have been created, but to deploy the load balancer we need to provision Ansible in the Vagrantfile. We'll use the Ansible Provisioner for this.

Let's write the provisioning blocks

      loadbalancer.vm.provision "ansible" do |ansible|
        ansible.verbose = "v"
        ansible.playbook = "ansible/site.yml"
        ansible.become = true
        ansible.inventory_path = "ansible/hosts"
      end 

      server_1.vm.provision "ansible" do |ansible|
        ansible.verbose = "v"
        ansible.playbook = "ansible/site.yml"
        ansible.become = true
        ansible.inventory_path = "ansible/hosts"
      end

      server_2.vm.provision "ansible" do |ansible|
        ansible.verbose = "v"
        ansible.playbook = "ansible/site.yml"
        ansible.become = true
        ansible.inventory_path = "ansible/hosts"
      end

Your final Vagrantfile should look like this


Vagrant.configure(2) do |config|

    config.vm.box = "ubuntu/bionic64"


    # configure loadbalancer
    config.vm.define "load-balancer" do |loadbalancer|
      loadbalancer.vm.network "private_network", ip: "192.168.33.11"
      loadbalancer.vm.hostname = "load-balancer"
      loadbalancer.vm.network "forwarded_port", guest: 80, host: 8080
      loadbalancer.vm.provider "virtualbox" do |vb| 
        vb.memory = 512
        vb.customize ["modifyvm", :id, "--audio", "none"]
      end


      loadbalancer.vm.provision "ansible" do |ansible|
        ansible.verbose = "v"
        ansible.playbook = "ansible/site.yml"
        ansible.become = true
        ansible.inventory_path = "ansible/hosts"
      end 

    end


    #configure web servers

    config.vm.define "server-1" do |server_1|
      server_1.vm.network "private_network", ip: "192.168.33.12"
      server_1.vm.hostname = "server-1"
      server_1.vm.provider "virtualbox" do |vb| 
        vb.memory = 512
        vb.customize ["modifyvm", :id, "--audio", "none"]
      end


      server_1.vm.provision "ansible" do |ansible|
        ansible.verbose = "v"
        ansible.playbook = "ansible/site.yml"
        ansible.become = true
        ansible.inventory_path = "ansible/hosts"
      end

    end   


    config.vm.define "server-2" do |server_2|
      server_2.vm.network "private_network", ip: "192.168.33.13"
      server_2.vm.hostname = "server-2"
      server_2.vm.provider "virtualbox" do |vb| 
        vb.memory = 512
        vb.customize ["modifyvm", :id, "--audio", "none"]
      end


      server_2.vm.provision "ansible" do |ansible|
        ansible.verbose = "v"
        ansible.playbook = "ansible/site.yml"
        ansible.become = true
        ansible.inventory_path = "ansible/hosts"
      end
    end  

   # update the packages on the VMs
    config.vm.provision "shell", inline: <<-SHELL   
      apt-get update
    SHELL

end

11) Deploy the load balancer by running

$ vagrant up --provision

This will take a while.

Manual Tests

There are two ways to manually check that the load balancer is working correctly:

  • Open a web browser on your machine go to the load balancer's IP address

Screenshot from 2021-10-05 13-53-56.png

The application is first served from server-1, and when refreshed server-2 responds.

Screenshot from 2021-10-05 13-54-17.png

This alternating method of load balancing is called the Round Robin technique

  • Another way we can check that the load balancer works is to run the following command from the terminal
$ curl http://192.168.33.11

The response should alternate between server-1 and server-2

Screenshot from 2021-10-05 14-03-13.png

If these tests passed, congrats! you just deployed your first load balancer ๐ŸŽ‰

Conclusion

In this tutorial, you learnt what load balancing is and how to build and deploy an Nginx load balancer using Ansible for configuration management.

Keep learning! ๐Ÿค“

ย