Automating Web Server Configuration, Load Balancing, and Horizontal Scaling with Ansible

Automating Web Server Configuration, Load Balancing, and Horizontal Scaling with Ansible

ยท

14 min read

In this blog, we will explore how to design an architecture featuring a load balancer that efficiently distributes incoming traffic from clients to multiple systems simultaneously. We will also delve into the process of scaling, whether it's scaling in or scaling out, to accommodate increasing traffic that surpasses the capacity of individual servers.

In brief, Ansible, developed by RedHat, serves as a versatile configuration management tool. It facilitates the configuration of a wide range of tasks typically carried out by system administrators regularly. Notably, Ansible operates independently of agent software and doesn't require additional security infrastructure, making it straightforward to implement.

For effective use of Ansible, we require two essential components:

  1. Controller Node: This is where Ansible must be installed, and it serves as the central hub for managing the configuration of other systems.

  2. Target Node: These are the additional systems that need configuration, and they are managed by the Controller Node.

prerequisite:

  1. Ansible is based on Python, and its modules are essentially Python scripts. When we employ a module in our playbook or Ad-hoc commands, they are executed on the Target Nodes, even though they run from the Controller Node. Consequently, the Target Nodes need to have a Python interpreter installed in advance.

  2. Ansible operates behind the scenes using the SSH protocol to establish connections with the Target Nodes. Therefore, it's essential to ensure that SSHD (SSH Daemon) is activated on the Target Nodes.

Load Balancer:

Load balancing is like having multiple servers, but instead of just one person deciding which server to use, there's a smart traffic cop. This traffic cop (the load balancer) directs incoming requests, like website visits, to different servers. It does this to make sure no single server gets overwhelmed with too much traffic, ensuring that everything runs smoothly and quickly for all users. Think of it as distributing the workload so that no one server has to do all the heavy lifting.

Imagine you have a popular lemonade stand, and lots of thirsty customers want lemonade. You have three friends helping you make lemonade: Sam, Alex, and Casey.

Now, you're the boss (like a load balancer), and your job is to make sure no one gets tired or overwhelmed. So, when a customer comes, you look at your friends' energy levels. If one friend, let's say Sam, has made fewer lemonades and has more energy, you ask Sam to serve the customer.

If lots of customers arrive and Sam gets tired, you might ask Alex or Casey to help. This way, you're balancing the work among your friends, so no one gets exhausted, and the lemonade keeps flowing smoothly.

In the world of computers, this is similar to load balancing. You have multiple computers (like Sam, Alex, and Casey), and a load balancer (like you) distributes tasks (customer requests) to the computers, making sure they work together without anyone getting overwhelmed. HAProxy, in this case, is like your super-smart boss who manages everything perfectly!

Frontend:

  • Frontend IP: This is like the address of a friendly receptionist at a hotel. Clients (users) connect to this IP. It's the first point of contact.

  • Frontend Port: Think of this as the receptionist's phone extension. It's the specific line where clients can reach out. The Load Balancing Program operates here.

Backend:

  • Backend IP: These are like the hotel rooms where the web pages (applications) are hosted. Each room has an address (IP), and this is where the real action happens.

  • Backend Port: Imagine each room has its internal phone extension. The web server program runs on these extensions. If a room (backend) isn't registered with the receptionist (LB), it won't receive any calls (traffic).

Reverse Proxy:

  • Reverse Proxy: This is like the receptionist going above and beyond. When a client (user) asks for something, the receptionist (LB) doesn't just forward the request. It goes to the rooms (backends), gets what's needed, and brings it back to the client. So, to the client, it feels like they're directly talking to the room (webserver) even though the receptionist (LB) does all the behind-the-scenes work.

In essence, the front end is where clients start, the back end is where the real stuff is, and the reverse proxy is the super-smart receptionist making sure everything runs smoothly without clients even knowing there's a receptionist involved!

Implementation:

This is the Ansible_controller node which I configured in my local VM

I have launched 3 other instances in AWS Cloud for the Load Balancer( Ansible_LB )and the Backends(Backend A and Backend B)

Now it's time to configure all the Backends

Step1: Installing the httpd and php package

Step 2: Set up the web page - By default, httpd uses the /var/www/html directory to host web pages, so we need to place our webpage there

we added this code in the above file

Step3: Start the service

systemctl start httpd

// check the status as below

systemctl status httpd

Now let's connect to the ip_addresses of webservers

we need to make some necessary changes to this

Go to Security -> Edit Inbound rules

Type http://(Public IPv4 address) ๐Ÿ‘‡

However, when multiple backends are running, managing and providing the IP addresses of all those servers to clients becomes impractical. This is where the Load Balancer Program plays a crucial role โ€“ by offering a single, standardized IP address to clients.

Establish a Load Balancer using port number 8080 and the Round Robin Algorithm. To set up the Load Balancer, I'm utilizing a tool named HAProxy. My AWS EC2 instance, Ansible_LB is being configured as the Load Balancer

Step1: Install haproxy software/package

Step2: Set up the Configuration file of haproxy

By default main config file is /etc/haproxy/haproxy.cfg

otherwise, you can find the config files this way too

vim /etc/haproxy/haproxy.cfg

After modifying the configuration file, it's essential to restart the service; otherwise, the changes won't take effect

 systemctl restart haproxy

SERVER_ADDR 172.31.36.50 represents the private IP address of my Backend A, where my web server operates on port 80. To access my web application, you simply need to connect to the LB program (haproxy) on port 8080, as specified in the configuration file.

As a client, you can access the web application by navigating to the LB system's IP address, rather than individually connecting to each of the web servers. Please note that to access the application, you can use the following URL format:

http://Ip_of_LB:Frontend_Port

In my case, it would be:

http://13.233.37.240:8080

SERVER_ADDR 172.31.36.145 corresponds to the private IP address of Backend B, where the web server operates on port 80.

By simply refreshing the page, you can observe in real time how HAProxy efficiently distributes incoming traffic to various backend servers. HAProxy effectively performs load balancing, ensuring optimal resource utilization.

Now, let's explore the scenarios where Ansible can be incredibly useful. Imagine having to individually configure each system as a web server - a time-consuming and impractical task, especially if you're deploying numerous web servers for a single service.

Instead of manual setups, why not create a code script that automates the entire process in a single operation? This code can be easily shared with your team, allowing them to replicate the same configuration seamlessly.

In such cases, Ansible, with its playbook functionality, becomes an invaluable tool, streamlining complex setups effortlessly

Configuring the entire Load Balancer system, including the backends and scaling. To begin, start by creating an inventory.

What is an inventory?

An inventory is a list of hosts that Ansible will oversee. These hosts can be organized into groups for more efficient management.

By default, Ansible uses /etc/ansible/hosts as its inventory file, but you can create custom inventory files in your working directory.

However, it's common practice to define a custom location for inventory files in your Ansible configuration, and it's recommended to create a separate Ansible config file instead of modifying the main config file located at /etc/ansible/ansible.cfg.

# pwd
# ansible --version

// create a separate workspace

# mkdir /Ansible_LB_Project
# cd /Ansible_LB_Project/

//create a separate inventory file
# touch inventory.txt
//create a separate config file for ansible and the extension should be .cfg
# touch ansible.cfg

When you execute Ansible commands, the tool first looks for its configuration file within the current working directory. If it doesn't find a configuration file there, it defaults to searching in the standard location, which is /etc/ansible/ansible.cfg.

So, how does Ansible know where to find the inventory file? It's all defined within the Ansible configuration file. To inform Ansible that your inventory file is located in the current working directory, and not in the default location, you need to specify this in the ansible.cfg file of your current workspace. This way, you avoid making changes to the default configuration file and ensure that Ansible uses the correct inventory file for your specific project.

# vim /etc/ansible/ansible.cfg

To configure Ansible for your specific project, you should edit the newly created ansible.cfg file. Here's what you need to do in your workspace

# vim ansible.cfg

The path should be specified as "./inventory" since my ansible.cfg and inventory file are located in the same directory. Alternatively, you can provide the complete path to your inventory file.

Now, proceed to configure the inventory.

# vim inventory.txt

In this setup, I'm utilizing key-based authentication, although password-based authentication is also an option.

  • ansible_user: This is the username used for establishing a connection to the host. Ensure that this user exists on the target node or host.

  • ansible_ssh_private_key_file: This parameter specifies the private key file used by SSH. It's particularly handy when working with multiple keys, and you prefer not to rely on an SSH agent.

# ansible all --list-hosts
# ansible all -m ping

Since the number of target nodes is restricted, you can manually verify whether my Controller node can establish remote SSH login connections to the Target nodes or not. To do this, we need to utilize the '-i' option to specify the location of the public key file. Without this option, SSH will attempt to locate the key in its default directory, which is typically '/root/.ssh/id_rsa'. In my particular setup, my public key is stored in the '/root/' directory.

All systems respond to pings from my CN (Controller Node). This confirms successful connectivity!

With the inventory now properly configured, it's time to execute some Ansible ad-hoc commands

// install httpd package in my Backend_A

# ansible 3.111.33.154 -m package -a "name=httpd state=present"

What does this error indicate? Upon closer inspection, you'll notice that in my Ansible inventory, I've set the ansible_user as 'ec2-user'. We're utilizing 'ec2-user' to log in to the remote host and perform configurations. However, it's worth noting that 'ec2-user' is a standard user with limited privileges

Although 'ec2-user' is a standard user, it can simulate root user privileges with the assistance of 'sudo.' We grant 'ec2-user' additional privileges beyond a typical user, and we achieve this through 'sudo,' a method that empowers 'ec2-user' with root-level capabilities. This process is commonly referred to as privilege escalation. Fortunately, AWS has already configured 'sudo' for 'ec2-user.' Now, our task is to instruct Ansible that, following the login as 'ec2-user,' we need to execute privilege escalation using 'sudo.' This step empowers 'ec2-user' to operate with the authority of the root user and complete the necessary configurations.

Running without privilege escalation:

# ansible 13.233.88.87 -a id

Running with privilege escalation using 'sudo':

# ansible 13.233.88.87 -a id --become --become-method=sudo --become-user=root

In the target node, we haven't set a password for escalating privileges. If we had, we would need to use the --ask-become-pass option, which would prompt us to provide the password.

It's worth noting that by default, the module_name is set as the 'command' module for Ansible. Therefore, whether you include -m command explicitly or not, it doesn't matter when you want to execute commands via Ansible ad-hoc tasks.

Instead of specifying privilege escalation options every time you run an Ansible command, it's a good practice to define them in the Ansible configuration file under the "privilege_escalation" section for convenience

With the connection-related settings in place, we can now proceed to configure the web server and create a playbook. Since there will be multiple tasks to execute, it's important to have a playbook to manage them.

# vim index.php
# vim web.yml
# ansible-playbook web.yml

Now, let's proceed with the configuration of HAProxy.

vim lb.yml

Record the current backend servers and open the 'lb_conf.txt' file for editing

Execute the 'ansible-playbook lb.yml' command to run the playbook and connect to the load balancer on port 8080

http://13.127.253.93:8080/

The load balancer (LB) is functioning flawlessly, and this time, we harnessed the power of Ansible and its automation capabilities to set up everything.

As we're configuring two web servers, having the registration hardcoded can be a hassle when new systems with new IP addresses come into play. Let's introduce some dynamism into the process instead of relying on hardcoded values. To achieve this, we can leverage the Jinja2 template

Jinja2 Tempalting Language

Ansible harnesses Jinja2 templating to facilitate dynamic expressions, variable access, and various programming concepts, such as conditions and loops.

  1. Variables: To utilize variables in a template file, use the following syntax:

     {{ variable_name }}
    
    1. When Playbooks are executed, these variables are replaced with actual values defined in Ansible Playbooks.
  2. Comments: To include comments in a template file, use the following syntax:

     {# This is a comment #}
    
  3. Conditions: If you wish to employ conditional statements in a template file, the syntax is as follows:

     {% if <condition> %}
     - - - - - block - - - - -
     {% else %}
     - - - - - block - - - - -
     {% endif %}
    
  4. Loop: To incorporate loops into a template file, the syntax is as follows:

     {% for i in name %}
     {{ i }}
     {% endfor %}
    

Let's perform a brief test

Let's execute this playbook and observe the outcome!

Everything looks fine! Now, let's inspect the LB node.

In summary,When it comes to transferring files to a remote system using Ansible, we primarily employ the copy and template modules. The copy module is straightforward and static in nature; it simply transfers a file from the local host to a remote server. On the other hand, the template module also transfers a file to a remote server but offers the flexibility to utilize Jinja2 templates.

With Ansible, there's an additional feature: it examines the file for any syntax that matches the Jinja2 template syntax. If such syntax is found, Ansible detects it and, before transferring the file to the remote server, substitutes it with the corresponding values. These values can either be stored in a separate file or defined within the playbook as variables provided to Ansible. Subsequently, the modified file is transferred to the remote server.

Now, there's no need to manually embed my registration file; we can leverage the capabilities of the Jinja2 templating language. When we incorporate Jinja2 into a file, that file is referred to as a template file, and it should be given the ".j2" extension

Let's now modify the lb.yml file as well. Set the source to be 'lb_conf.j2' and change the module from 'copy' to 'template'.

Execute the lb.yml playbook

Proceed to the LB system and inspect the configuration file for HAProxy.

Observe the final two lines; this time, there is no manual input. It automatically registers all the IP addresses present in my web host group within the inventory.

Scaling:

Imagine our web service becoming incredibly popular, leading to a sudden surge in traffic. In such a scenario, it becomes imperative to deploy additional web servers with the same configuration. This process of adding more systems is known as horizontal scaling.

Horizontal scaling can be categorized into two types:

  1. Scaling Out: Increasing the number of systems.

  2. Scaling In: Reducing the number of systems.

To address the increased demand, I have just launched an additional system as part of our scaling efforts.

But how does Ansible determine that we've added another system and need to register it as a backend? Here's a quick tip: simply put the IP address of the new system here.

Run the playbook for configuring the webserver

You might encounter this type of error. Ansible consistently employs SSH for remote logins, and the problem arises when you connect to a system via SSH for the first time, as it will prompt you for this.

In all the previous instances, I performed manual remote logins on each target node and provided 'yes' as the input. That's why we didn't encounter errors when running Ansible commands.

To avoid these manual confirmations, we can disable host key checking in the Ansible.cfg file. By default, it is set to 'True,' so we need to change it to 'False'.

Once more, execute the 'web.yml' playbook

No errors related to host key verification failure, and the IP address 43.204.140.44 is configured as a web server.

Now, let's verify whether HAProxy has automatically registered it or not

This is the current file ๐Ÿ‘‡

Proceed to execute the playbook

# ansible-playbook lb.yml

Next, examine the haproxy.cfg file :

Before and after !! Observe that there is one additional IP, and it registers automatically.

Now, from the Windows command prompt, let's verify

curl http://43.204.140.44:8080

Whenever new traffic arrives, HAProxy routes it to a different backend each time.

Now, suppose the traffic decreases, and you believe that just two of your web servers can handle it. In this case, you can perform scaling by simply removing one of the backend IPs from your web host group and then running the 'lb.yml' playbook.

In summary, we are primarily managing host IPs within our inventory. Additionally, we can centralize other configuration details in our 'ansible.cfg' file.

We can significantly enhance the entire process , making it highly dynamic with minimal human involvement in some other time.

Thankyou.

My Github repo :

https://github.com/manogna-chinta/Ansible_LB_Project..git

ย