LowEndBox - Cheap VPS, Hosting and Dedicated Server Deals

Setup a Highly Available Wordpress Site From Scratch, 2024 Edition! Part 3: Ansible

In this tutorial series, we are 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 meat of things, the playbook itself.  In Ansible, a playbook is a list of tasks (and other things, but we’re focused on tasks here) that will be performed.  We’re going to call our playbook wp_nodes.yml.  That .yml tells you that the file is in YaML format.

Here’s the playbook:

---
  - 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

Let’s break that down step by step.

  
---
  - name: setup HA WordPress Nodes
    hosts: nodes

The name: parameter is just a descripter for the playbook. The hosts: parameter tells us what group in /etc/ansible/hosts it will run against.

    tasks:
      - name: hostname
        hostname: name={{ ansible_host }}
      - name: /etc/hostname
        lineinfile:
          path=/etc/hostname line="{{ ansible_host }}" create=yes

Each Ansible command leverages one of Ansible’s modules.  You don’t write your own Bash or whatever code, but instead pass arguments to Ansible modules, which do all the work.  This frees you from having to worry about syntax, logic, quoting, etc.

Here we use the hostname module, and given it the Ansible variable “ansible_host”.  This variable will be node1.lowend.party, etc. and this module will set the system hostname appropriately.

Next we use the “lineinfile” module to make sure the hostname is put in /etc/hostname (a Debianism I believe) so the box will always pick up its correct hostname at boot.

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

Next, we generate the appropriate locales for our environment, making sure that en_US.UTF-8 is selected.  Obviously, suit to your own needs here.

      - 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

Here we’re using the Ansible apt module to do three things:

  • apt-get update
  • apt-get upgrade
  • Install some apt packages we’ll need.  Whether you want to run rsyslog is up to you.  I prefer to have traditional text logs instead of using journalctl for everything, but it’s up to you.
      - name: Set 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

When installing MySQL (MariaDB actually), it’s recommended to run mysql_secure_installation.  But rather than run that manually, we’ll use Ansible’s expect module.  You tell it what command to run, and then a list of what output to expect and what answers to give.  This section walks through the mysql_secure_installation questions and provides the appropriate answers.  Of course, you should change StrongPassword to a genuinely strong password!

      - 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=ComplexPassword priv=wp.*:ALL host=localhost 

Here we create a MariaDB database for WordPress, a user for that DB, and grant it appropriate permissions.  Note that even if you run this playbook multiple times, Ansible is smart enough to first check if the DB exists and not error out trying to recreate it, etc.

      - 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 

Here we’re distributing two local files from our Ansible master server to each node.  I store them in /ansible/src but you can store them anywhere.

See below for the content of these files.

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

We link /etc/nginx/sites-enabled/www.lowend.party to /etc/nginx/sites-available/www.lowend.party, which is standard Nginx setup on 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?).

      - 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

Finally, we restart the nginx service and create another directory we’ll need for gluster.

The /etc/logrotate.d/nginx_web_dirs file that we distribute to all nodes from /ansible/src/nginx_web_dirs looks like this:

/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 got a ton done with Ansible:

  • Setting hostname, locales, and timezone
  • Installing packages with apt
  • Setting up the database and securing it
  • Installing rsyslog and configuring it to rotate Nginx
  • Installing Nginx, configuring it, configuring the site, and setting up the site’s logs

Next time we’ll get GlusterFS up and running!

raindog308

No Comments

    Leave a Reply

    Some notes on commenting on LowEndBox:

    • Do not use LowEndBox for support issues. Go to your hosting provider and issue a ticket there. Coming here saying "my VPS is down, what do I do?!" will only have your comments removed.
    • Akismet is used for spam detection. Some comments may be held temporarily for manual approval.
    • Use <pre>...</pre> to quote the output from your terminal/console, or consider using a pastebin service.

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