The LAMP (Linux, Apache2, MySQL, PHP) stack has been a common choice for web applications of all kinds for decades. Its popularity makes it a great choice for automating it's deployment and configuration.
Choosing a LAMP stack
When installing a LAMP web server, the four core services to install are obvious. But a web server will commonly need more services to function properly, for example FTP to transfer files. The stack in this article chooses to install this additional software:
- PHPMyAdmin to provide a visual user interface to the MySQL database.
- logrotate to keep the size of log files in check
- sFTP through openssh-server to allow upload of web application contents
- certbot to automate SSL certificates
For simplicity, the playbook assumes that all hosts run a debian-like linux distribution (Debian, Ubuntu, Mint, ...).
A sample inventory
Before starting the playbook, we need an inventory defining some metadata about the host servers used to install the LAMP stacks on. Here is a sample inventory:
all:
hosts:
my_server: # nickname of the server
# SSH connection info for this server
ansible_host: 192.168.56.110
ansible_user: sysadmin
ansible_password: "123"
# variables to customize the LAMP stack install
vhost_domain: example.com
vhost_alt_domains: [www.example.com, app.example.com]
mysql_root_pass: "123"
ftp_user: my_user
ftp_pass: "123"
enable_ssl: true
ssl_email: admin@example.com
In addition to ansible's SSH connection variables, the sample inventory provides a lot of custom variables for the LAMP stack. Each of these will be explained together with the playbook section responsible for it. The playbook will be designed to use sane defaults if any variable is undefined.
Install required packages
The first step is to install the packages needed to run the stack. Ideally, this should be the first step and install all packages at once, as running multiple install steps may query the package cache unnecessarily and turn out to be fairly slow:
---
- name: LAMP stack setup
hosts: all
become: true
tasks:
- name: Install required packages
apt:
name:
- apache2
- php
- php-mysql
- php-xml
- php-mbstring
- php-zip
- php-curl
- php-bz2
- php-opcache
- libapache2-mod-php
- mariadb-server
- phpmyadmin
- logrotate
update_cache: true
state: present
The installation includes some common PHP extensions to avoid incompatibilities with common web software. You may need to add moer specialized extensions depending on the web application you plan to host.
Note the update_cache: true
line at the bottom, ensuring that the package cache is updated before installing the packages, so only the most recent versions are installed (even if the local package cache was outdated when running the playbook).
Configure apache2
The apache2 web server will be mostly functional out of the box, but we may need to alter the virtualhost to account for the desired domain(s) it should serve content for:
- name: Set primary domain for virtualhost
when: vhost_domain is defined
lineinfile:
path: /etc/apache2/sites-available/000-default.conf
regexp: ServerName
line: "ServerName {{ vhost_domain }}"
insertbefore: DocumentRoot
- name: Set alternative domains for virtualhost
when: vhost_alt_domains is defined
lineinfile:
path: /etc/apache2/sites-available/000-default.conf
regexp: ServerAlias
line: "ServerAlias {{ vhost_alt_domains | join(' ') }}"
insertbefore: DocumentRoot
The first step will set the ServerName
directive to the primary domain used for the virtualhost, using the variable vhost_domain
from the inventory. If the variable is missing, the virtualhost will not be assigned a ServerName
directive, effectively making it a catch-all for all incoming requests regardless of domain.
If one or more vhost_alt_domains
were specified for the server through the inventory, they are added to the virtualhost configuration through the ServerAlias
directive in the second step. The difference between the primary and alternative domains for a virtualhost is negligible for HTTP configurations, but when using SSL, the server will redirect incoming requests to the primary domain.
Lastly, we need to adjust the ServerTokens
directive in the apache2 security config:
- name: Adjust apache2 security config
copy:
dest: /etc/apache2/conf-available/security.conf
content: |
ServerTokens Prod
ServerSignature Off
TraceEnable Off
Setting this directive to Prod
ensures that the server will not expose the actual apache2 version or activated modules through headers in the HTTP response. Any web server facing untrusted connections (i.e. the internet) should use this setting to minimize the information an attacker may gain from fingerprinting, limiting their ability to exploit version-specific bugs.
Prepare MySQL and PHPMyAdmin
To enable PHPMyAdmin, we first need to link it's apache2 configuration into the conf-available
directory, then enable it and finally restart apache2:
- name: Link phpmyadmin config to apache2
file:
src: /etc/phpmyadmin/apache.conf
dest: /etc/apache2/conf-available/phpmyadmin.conf
state: link
- name: Enable phpmyadmin config in apache2
command: a2enconf phpmyadmin
- name: Restart apache2
service:
name: apache2
state: restarted
enabled: true
The web UI will be available at http://server_domain/phpmyadmin
.
The MySQL root user password is set using the mysql_root_pass
inventory variable:
- name: Set mysql root user password
command: mysqladmin -u root password "{{ mysql_root_pass }}"
- name: Start and enable mysql server
service:
name: mariadb
state: started
enabled: true
Finally, the mariadb service is enabled to run at boot and started.
Set up logrotate
If left unmaintained, log files can grow considerably in size and fill up all remaining disk space. To combat this issue, we set up logrotate to periodically move and compress log file contents:
- name: Configure logrotate for Apache2 and MySQL
copy:
dest: /etc/logrotate.d/lamp
content: |
/var/log/apache2/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 640 root adm
}
/var/log/mysql/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 640 mysql adm
}
The configuration will rotate log files from apache2 and mysql once per day, keep up to 14 previous versions and compress rotated logs. This also ensures that log file contents are deleted after 14 days, avoiding issues with privacy laws like GDPR.
Enable sFTP access
To enable users to access the webserver files over FTP, we set up a new user account from the ftp_user
inventory variable and set their password using ftp_pass
. The new user is then added to the sftp_users
group, which is used to identify them as an sftp-only user in the openssh-server
:
- name: Create sFTP user
user:
name: "{{ ftp_user }}"
password: "{{ ftp_pass | password_hash('sha512') }}"
shell: /usr/sbin/nologin
state: present
create_home: true
- name: Set up SFTP-only group
group:
name: sftp_users
state: present
- name: Add user to SFTP-only group
user:
name: "{{ ftp_user }}"
groups: sftp_users
append: yes
- name: Set permissions on /var/www/html for SFTP user
file:
path: /var/www/html
owner: root
group: sftp_users
mode: '0775'
state: directory
- name: Set correct permissions for chroot
file:
path: /var/www
owner: root
group: root
mode: '0755'
state: directory
- name: Configure SSH for SFTP-only access to /var/www
lineinfile:
path: /etc/ssh/sshd_config
line: |
Match Group sftp_users
ChrootDirectory /var/www
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
- name: Restart SSH
service:
name: sshd
state: restarted
Since the user has a login shell of /usr/sbin/nologin
and sftp_users
is jailed to /var/www
, they can only log in over sFTP (port 22) and access the web directory.
Automate SSL certificates
Web servers hosting websites available to the internet should use SSL to encrypt traffic to protect their visitors. Certbot is an obvious choice for automating SSL certificate retrieval and maintenance:
- name: Certbot SSL automation
when: enable_ssl is defined and enable_ssl and ssl_email is defined and vhost_domain is defined
block:
- name: Install certbot
apt:
name:
- certbot
- python3-certbot-apache
state: present
update_cache: yes
- name: Obtain and install SSL certificates
command: "certbot --apache -d {{ ([vhost_domain] + (vhost_alt_domains | default([]))) | join(' ') }} --noninteractive --agree-tos --email {{ ssl_email }}"
Note how the certbot installation and certificate retrieval are grouped into a block that only executes if enable_ssl
is true
and ssl_email
has been set, to prevent common errors.
The certbot package is installed in a separate step instead of adding it to the package installation in the first step as it is optional and especially local/testing servers have no need for it.
Since the certbot package sets up its own cronjob for certificate renewal, it is enough to retrieve a certificate for the vhost domain(s) from the playbook.