Configure PHP and Apache on DigitalOcean with Ansible
Setting up a webserver for a PHP site is relatively straightforward for a basic configuration. Install and configure Apache (or nginx), install the relevant PHP modules, add the code and off you go. There's loads of tutorials out there for that.
But what happens when you have a popular site which needs multiple servers to handle the load? Configuring each one individually is incredibly time consuming. Worse still, when you need to add a new module, or enable a module, or even add a new server to help take the load.
That is where configuration management tools like Ansible come in. There are others out there, but because Ansible playbooks (the jobs Ansible runs) are YAML files, they should afford a level of familiarity for most developers, as it's commonly used for configuration of applications.
What is Ansible?
Ansible is a tool which can be used to provision servers, manage configuration, deploy applications, configure continuous integration and continuous delivery (CI/CD), and automate and roll out security policies. That's a lot of things for one tool, but we're going to focus on the configuration side within this article.
Ansible is owned by Red Hat, but that doesn't tie you to using Red Hat, or the associated Linux distributions (CentOs, Fedora, RHEL) when using it. It works with Debian based Linux distributions, Windows, IBM z/OS and others. Basically, whatever is in your bit-barn, you can manage with Ansible, all from one place.
It is "agentless", and therefore only needs to have a single point for orchestration of the work. It communicates with other machines via OpenSSH, and uses existing credentials to access the infrastructure.
During this article we will:
- Created a single Ubuntu Linux host for Ansible
- Created two Ubuntu Linux web servers
- Attached them to a load balancer
- Configured a playbook for installing Apache and PHP, and installed additional modules
- Run the playbook on the web servers and confirm they are working
- Provision a third web server, apply the playbooks for configuration, and add it to the load balancer
DigitalOcean provide a comprehensive guide for creating droplets (servers), so to set up the Ansible host, create a standard, $5 droplet. This one only needs to be lightweight, as it will only be used for managing the servers. Once it has been set up, log in to the server, and install Ansible.
For Ubuntu versions 18.10 or lower:
$ sudo apt update $ sudo apt install software-properties-common $ sudo apt-add-repository --yes --update ppa:ansible/ansible $ sudo apt install ansible
For Ubuntu 20.04
$ sudo apt update $ sudo apt install -y ansible The following is optional, but recommended: $ sudo apt install -y ansible-lint
Using the same ssh key as the Ansible host, create two new droplets for the web servers. These are likely to need more resource than the Ansible host, so create two Standard 2vCPU, 2GB RAM droplets ($15 each).
Once they are up and running, we can add their IP addresses to the Ansible inventory. This is located (by default) at
/etc/ansible/hosts. This can be configured a few different ways. In this case, the hosts can point directly to the IP address of the droplet, or they can use a DNS entry which points to the droplet IP. Using DNS is preferable as it will allow new droplets to be created, and have the DNS switched to it for a record to be used, rather than always editing the hosts file.
By configuring the servers in a group (some text surrounded by ), a playbook can be run against the group rather than having to specify each server individually. A great time saver. This article will group the servers together as
webservers, with the
/etc/ansible/hosts file looking like:
[webservers] 10.106.0.4 10.106.0.3
To see the inventory within Ansible, run
ansible-inventory --list -y. The
--list flag produces a JSON formatted inventory script output, whereas adding the
-y flag flattens this into YAML for easier reading on a small terminal.
If that looks correct, then it's time to check the connection to the servers from Ansible. Run
ansible all -m ping. That will output whether the connection was a success or a failure. If it fails, try running
ansible all with a different username
ansible all -m ping -u root to see if that works. The user should be the one which the SSH key for login uses. As part of that command,
all refers to the servers to check. If you have multiple groups, it will check them all. For the web servers demo, the command
ansible webservers -m ping will check the connection to the servers grouped as web servers.
Load balancers and Firewalls. Oh, my!
Before we start configuring the servers, we want to configure them so that they aren't completely open to the world. This is where firewalls come in. They restrict access to the servers to certain locations and/or ports. or they can completely open them up to the world.
For a secured setup, the following rules are required:
- Access to the Ansible host should only be via SSH, and only from our own IP address
- Access to the web servers via SSH should only be from the Ansible host
- Access to the web servers via HTTP should only be from the load balancer
Once the firewall rules have been configured, re-run the ping command for the web servers
ansible webservers -m ping to ensure the communication channels are still available to the Ansible host.
Configuring a Playbook
With the servers up and running, and the firewall in place to restrict access to only essential channels, it's time to create a playbook. This is a series of steps which need to be performed on a machine to configure it as required. By using playbooks, you have a document of what steps were taken to put the machine to its current state. It allows adding and updating of components, modules, or other configuration to the servers in a consistent manner to keep things in line.
The playbook I created as part of this post is available on GitLab.
The playbook we are going to create will include:
- The playbook to install Apache, PHP, some PHP modules, and enable some Apache modules
- A template to overwrite the default Apache config
- A template to overwrite the default Apache vhost configuration
- A template to create a PHP file, which will output the result of
A playbook is broken down into two main sections:
- Hosts and users
Hosts and Users
This section determines which hosts are affected by the playbook, and which user they should run as. Examples of these usually start with:
--- - hosts:
- is important for the hosts, but the top `---` isn't required.
In the example I've added to GitLab, the hosts and users section looks like:
--- - hosts: webservers remote_user: gary become: true become_method: sudo
This lets Ansible know that the script is to affect all servers within the group
[webservers] in the inventory (
/etc/ansible/hosts); That it should connect as user 'gary', and should then run with escalated privileges via
Tasks are added under the
tasks: section (which aligns at the root with
hosts and the other root elements of the hosts and users part. Tasks each have a
name: and then the task definition.
The task defined below will install the latest version of Apache2 using
apt. When this task is running, Ansible will display
TASK [install Apache httpd server] in the terminal on the host.
When wanting to put a file (such as configuration file) on the remote server as part of the playbook, Jinja templates are used. These are basically text files, but have a level of templating built in for variable replacement (if required). These tasks get defined as:
Check the playbook
Once the playbook has been created, it is advisable to check it for syntactical correctness before Ansible runs it. This is done by running
ansible-lint webservers.yml. If there are issues, it will output the issues to be corrected before the playbook will run. Even if the linting is correct, if the contents of the playbook aren't what the end machine understands, it will still fail.
Once the playbook has been linted and is ready to run, it can be played using
ansible-playbook wevservers.yml. If the remote machine requires a password to escalate privileges, add the flag
Once the task has completed, we can verify it by running
ansible webservers -a "php -v". Each web server should provide the same information for both servers, as they are the same operating system and have had the same playbook executed.
Finally, for the initial configuration, we just need to point a DNS record at the load balancer so it can direct traffic to the configured servers. That will all depend upon the actual DNS provider, put remember to provide the public IP of the load balancer in the record, not one of the servers.
Adding more servers
Now that the servers are up and running as we would like, and we can see them working by loading the domain for them, we can add and fairly quickly configure a new server using the script we already have.
Create a new droplet in the DigitalOcean control panel. Once it is up and running, get the IP address and add that to the
[webservers]section of the
/etc/ansible/hosts file on the Ansible host. To configure the server, run the Ansible playbook again
ansible-playbook webservers.yml. It can be run on a single server if required using
ansible-playbook -l server-name-or-ip webservers.yml
The playbook will replay on all servers in the
[webservers] group. This is to ensure that all are up-to-date and configured the same. This has the benefit of allowing you to modify the playbook as time progresses, and apply all changes to all servers, without needing to remember which order the playbooks need to run in to keep things up-to-date.
Finally, add the new droplet to the load balancer group for it to take hold. To check this is working as the others were, remove the load balancer group from the other two servers.