Ansible, Dns, Gluster, Low End Virtual, Mariadb, Mysql, Wordpress

Part 3: Utilizing Ansible to Set Up a Highly Available WordPress Site – 2024 Edition!

In this tutorial series, we’re setting up a highly available WordPress web site from scratch.

Part 1 – Introduction, Considerations, and Architecture

Part 2 – Ordering the VPSes

Part 3 – Ansible (this article)

Part 4 – Gluster

Part 5 – WordPress install

Part 6 – MariaDB Multi-Master

Part 7 – Round-Robin DNS, Let’s Encrypt, & Conclusion

In 2021, I just executed all the setup commands on both nodes. Now we have three nodes, and it’s time to take a more convenient approach.

We’re going to use Ansible, a fantastic tool for managing systems and applications. You could install it on node1, and use it to control all three nodes, but I have an existing Ansible master that I’m using. It really doesn’t matter.

To install ansible:

apt install ansible

On my Ansible master, I have a filesystem mounted at /ansible that contains everything I need. However, if you install Ansible from apt, it sets up various things in /etc/ansible, so we’ll use that.

Here is my /etc/ansible/hosts file:

[nodes]

node1.lowend.party

node2.lowend.party

node3.lowend.party

What the does is create a group called “nodes” that has our three nodes. Make sure these nodes are in DNS, or you can create /etc/hosts entries like these:

5.78.68.150 node1.lowend.party node1

5.78.91.194 node2.lowend.party node2

5.78.74.126 node3.lowend.party node3

In /etc/ansible/ansible.cfg you shouldn’t need to change anything.

Now here’s the main part, the playbook itself. In Ansible, a playbook is essentially a list of tasks. Other elements can also be included but for now, we’re mainly focusing on tasks. The name of our playbook is wp_nodes.yml. The extension .yml is an indicator that the file is in YAML format.

The playbook is as follows:

---

- name: setup HA WordPress Nodes

hosts: nodes

tasks:

- name: hostname

hostname: name={{ ansible_host }}

- name: /etc/hostname

lineinfile:

path=/etc/hostname line="{{ ansible_host }}" create=yes

- name: locale generation

locale_gen: name=en_US.UTF-8 state=present

- name: apt-get update

apt: update_cache=yes

- name: upgrade

apt: upgrade=dist

- name: apt packages

apt: name=nginx,mariadb-server,php-fpm,php-mysql,python3-pymysql,python3-pexpect,glusterfs-server,rsyslog,parted,certbot,python3-certbot-nginx

- name: Set timezone to US/Pacific

community.general.timezone:

name: US/Pacific

- name: /etc/profile mods

blockinfile:

path: /etc/profile

block: |

alias ll='ls -al'

set -o vi

- name: run mysql_secure_installation

expect:

command: mysql_secure_installation

responses:

'Enter current password for root': ''

'Switch to unix_socket authentication' : 'Y'

'Change the root password' : 'Y'

'Set root password': 'Y'

'New password': 'StrongPassword'

'Re-enter new password': 'StrongPassword'

'Remove anonymous users': 'Y'

'Disallow root login remotely': 'Y'

'Remove test database': 'Y'

'Reload privilege tables now': 'Y'

timeout: 1

- name: create wp database

mysql_db: login_user=root login_password="StrongPassword" name=wp state=present collation=utf8_general_ci

- name: create db user

mysql_user: login_user=root login_password="StrongPassword" name=wp password=ComplePassword priv=wp.*:ALL host=localhost

- name: nginx logrotate

copy: src=/ansible/src/nodes/nginx_web_dirs dest=/etc/logrotate.d owner=root group=root mode=0644 force=yes

- name: nginx www.lowend.party

copy: src=/ansible/src/nodes/www.lowend.party dest=/etc/nginx/sites-available owner=root group=root mode=0644 force=yes

- name: create sites-enabled symlink

file: src=/etc/nginx/sites-available/www.lowend.party dest=/etc/nginx/sites-enabled/www.lowend.party state=link

- name: nginx log dir

file: path=/var/log/nginx/www.lowend.party state=directory owner=www-data group=adm mode=0775

- name: nginx access log

file: path=/var/log/nginx/www.lowend.party/access.log state=touch owner=www-data group=adm mode=0664 state=touch

- name: nginx error log

file: path=/var/log/nginx/www.lowend.party/error.log state=touch owner=www-data group=adm mode=0664 state=touch

- name: nginx /web

file: path=/web state=directory owner=www-data group=adm mode=0775

- name: nginx /web/www.lowend.party

file: path=/web/www.lowend.party state=directory owner=www-data group=adm mode=0775

- name: restart nginx

service: name=nginx enabled=yes state=restarted

- name: gluster mount point

file: path=/gluster state=directory owner=root group=root mode=0777

Now, let’s break down the script step by step.

<!-- Start of Blog Post -->

<h2>Setting Up HA WordPress Nodes</h2>

<p>The <code>name:</code> parameter is just a descriptor for the playbook. The <code>hosts:</code> parameter informs us what group in /etc/ansible/hosts it will execute against.</p>

<h3>Tasks Include:</h3>

<ul>

<li>Setting the hostname with <code>hostname: name={{ ansible_host }}</code></li>

<li>Creating/editing /etc/hostname file with <code>lineinfile: path=/etc/hostname line="{{ ansible_host }}" create=yes</code></li>

</ul>

<h3>How Ansible Commands Work</h3>

<p>Each Ansible command takes advantage of one of Ansible’s modules. You don’t compose your own Bash or equivalent code, but instead pass arguments to Ansible modules, which do all the heavy lifting. This liberates you from having to concern yourself with syntax, logic, quoting, etc.</p>

<!-- End of Blog Post -->

Here we apply the hostname module, utilizing the Ansible variable “ansible_host”. This variable stands for node1.lowend.party, etc, and this module is employed to correctly define the system hostname.

Following this, we use the module “lineinfile” to ensure that the hostname situates in /etc/hostname (a feature mostly found in Debian systems), so that the system can retrieve its correct hostname at every startup.

- name: locale generation

locale_gen: name=en_US.UTF-8 state=present

Subsequently, we generate locales appropriate for our environment, ensuring that en_US.UTF-8 is chosen. Naturally, you may adjust this to your own requirements.

- name: Running apt-get update

apt: update_cache=yes

- name: Running upgrade

apt: upgrade=dist

- name: Installation of necessary apt packages

apt: name=nginx,mariadb-server,php-fpm,php-mysql,python3-pymysql,python3-pexpect,glusterfs-server,rsyslog,parted,certbot,python3-certbot-nginx

In this scenario, the Ansible apt module performs three core tasks:

  • Execution of apt-get update
  • Execution of apt-get upgrade
  • Installation of specific apt packages necessary for the application. The option to run rsyslog is subjective. Personally, I favor having traditional text logs instead of relying solely on journalctl, but ultimately, it’s your decision.

- name: Configuration of timezone to US/Pacific

community.general.timezone:

name: US/Pacific

Here we set the timezone. Flavor to your own taste.

 

- name: /etc/profile mods

blockinfile:

path: /etc/profile

block: |

alias ll='ls -al'

set -o vi

I left this in as an example, but it’s entirely optional. I like to have those two lines in my /etc/profile because no matter what user I’m working as, I want that alias and option set. What the blockinfile module does is make sure that the block (the two commands listed here) are present in the file specified, in this case /etc/profile.

 

- name: run mysql_secure_installation

expect:

command: mysql_secure_installation

responses:

'Enter current password for root': ''

'Switch to unix_socket authentication' : 'Y'

'Change the root password' : 'Y'

'Set root password': 'Y'

'New password': 'StrongPassword'

'Re-enter new password': 'StrongPassword'

'Remove anonymous users': 'Y'

'Disallow root login remotely': 'Y'

'Remove test database': 'Y'

'Reload privilege tables now': 'Y'

timeout: 1

While setting up MySQL (Actually MariaDB), running mysql_secure_installation is advised. However, instead of resorting to manual operation, we’ll implement Ansible’s expect module. This command facilitates prompting specified outputs and programmed responses. This segment will guide you through the mysql_secure_installation queries delivering the approporiate responses. Certainly, it’s wise to replace StrongPassword with an extremely secure password!

<a name="create wp database">

mysql_db: login_user=root login_password="StrongPassword" name=wp state=present collation=utf8_general_ci

</a>

<a name="create db user">

mysql_user: login_user=root login_password="StrongPassword" name=wp password=ComplexPassword priv=wp.*:ALL host=localhost

</a>

We proceed by generating a MariaDB database for WordPress, coupled with a user for this DB, and then allot the required permissions. Attention should be paid to the fact that despite running this playbook numerous times, Ansible is clever enough to verify the existence of the DB and it prevents errors from attempting to recreate it.

<a name="nginx logrotate">

copy: src=/ansible/src/nodes/nginx_web_dirs dest=/etc/logrotate.d owner=root group=root mode=0644 force=yes

</a>

<a name="nginx www.lowend.party">

copy: src=/ansible/src/nodes/www.lowend.party dest=/etc/nginx/sites-available owner=root group=root mode=0644 force=yes

</a>

Here we’re distributing two local files from our Ansible master server to each node. These files are stored in /ansible/src, however, you can keep them wherever convenient.

The content of these files can be seen below.

- name: create sites-enabled symlink

file: src=/etc/nginx/sites-available/www.lowend.party dest=/etc/nginx/sites-enabled/www.lowend.party state=link

Here, we’re linking /etc/nginx/sites-enabled/www.lowend.party to /etc/nginx/sites-available/www.lowend.party, which is an ordinary Nginx setup in Debian.

- name: nginx log dir

file: path=/var/log/nginx/www.lowend.party state=directory owner=www-data group=adm mode=0775

- name: nginx access log

file: path=/var/log/nginx/www.lowend.party/access.log state=touch owner=www-data group=adm mode=0664 state=touch

- name: nginx error log

file: path=/var/log/nginx/www.lowend.party/error.log state=touch owner=www-data group=adm mode=0664 state=touch

We create a directory and the access.log and error.logs for Nginx. I prefer to have each site in its own directory.

- name: nginx /web

file: path=/web state=directory owner=www-data group=adm mode=0775

- name: nginx /web/www.lowend.party

file: path=/web/www.lowend.party state=directory owner=www-data group=adm mode=0775

I organize my web roots in /web (yes, right off the root directory – why not?).

Following the above steps, we proceed to restart the nginx service and establish another directory required for gluster.

The contents of the /etc/logrotate.d/nginx_web_dirs file distributed across all nodes from /ansible/src/nginx_web_dirs are as presented below:

    

/var/log/nginx/*/*.log {

daily

missingok

rotate 14

compress

delaycompress

notifempty

create 0640 www-data adm

sharedscripts

prerotate

if [ -d /etc/logrotate.d/httpd-prerotate ]; then

run-parts /etc/logrotate.d/httpd-prerotate;

fi

endscript

postrotate

invoke-rc.d nginx rotate >/dev/null 2>&1

endscript

}

You only need this if you’re using rsyslog.

The Nginx file that is put in /etc/nginx/sites-available/www.lowend.party looks like this:

server {

server_name www.lowend.party;

access_log /var/log/nginx/www.lowend.party/access.log;

error_log /var/log/nginx/www.lowend.party/error.log;

location ~ .php$ {

fastcgi_split_path_info ^(.+.php)(.*)$;

include /etc/nginx/fastcgi_params;

fastcgi_pass unix:/run/php/php8.2-fpm.sock;

fastcgi_index index.php;

fastcgi_param SCRIPT_FILENAME /web/www.lowend.party$fastcgi_script_name;

}

location / {

root /web/www.lowend.party;

index index.php index.html;

try_files $uri $uri/ /index.php;

if (!-e $request_filename) {

rewrite . /index.php last;

}

}

}

This is a pretty standard Nginx file. As you can see, we’re using PHP-FPM, which is a package we installed above.

Wow, we’ve accomplished a lot with Ansible:

  • Configuring hostname, locales, and timezone
  • Implementing packages via apt
  • Establishing the database and ensuring its safekeeping
  • Employing rsyslog and manipulating it to rotate Nginx
  • Utilizing Nginx, adjusting it, organizing the site, and managing the site’s logs

In our next session, we’ll have GlusterFS operational!

Leave a Reply

Your email address will not be published. Required fields are marked *