Deploying a static website with Ansible
![web server deployment](/sysadmin/sites/default/files/styles/full/public/2020-06/hand-4448892_1920%20Cropped.jpg?itok=XurNszkQ)
One of the best ways to get started with a new automation tool is to leverage it to simplify something you already do. Many sysadmins have their own personal web sites. Managing a static web site deployment is a great way to enter the world of Ansible and automated, reproducible infrastructure. In this article, I'll walk you through deploying a simple web site on NGINX by using Ansible.
While it might be tempting to use the official NGINX Ansible role for this, I encourage you to do it yourself first. It's a great way to learn, and you'll feel more confident in your Ansible abilities after solving some of these problems yourself before using upstream tools.
Setup
Before I start, let me provide an overview of the environment used for this article. I'm using a standard Ansible role setup and directory layout, as described in the docs.
$ ls
group_vars/ inventory.ini
roles/ site.yml
My inventory file contains a single host in a group called "webservers," and that group is assigned a single role in the site.yml
:
$ cat inventory.ini
[webservers]
nyc-webserver-1.example.com
$ cat site.yml
- hosts: webservers
roles:
- webserver
This article walks through the creation of the web server role. Before sitting down to write any type of tool, it's helpful to consider the overall goals. In this case, I want to write a role that:
- Installs and provides basic configuration for a web server (NGINX)
- Allows me to quickly provision web server configuration for one or more web sites, without having to muck around in the code
- Deploys the actual web site to the web server. I'll start with simple, file-based deployment and then move on to a model that leverages Git to pull the latest version of my site
Let's get started!
Install and configure a web server
The first thing that the automation should handle is the installation and configuration of NGINX, my chosen web server. The roles/webserver/tasks/main.yml
file shown below is a straightforward way to accomplish this:
---
- name: Install packages
package:
name: "{{ webserver_packages }}"
state: latest
- name: Add base NGINX configuration file
copy:
dest: /etc/nginx/nginx.conf
src: etc/nginx/nginx.conf
owner: root
group: root
mode: 0644
notify: Reload NGINX
Notice that I use the webserver_packages
variable to install packages, instead of explicitly specifying NGINX and other software. This variable makes it easier to add or remove packages from my role, without needing to edit the actual code. The webserver_packages
variable is defined in the roles/webserver/vars/main.yml
file:
---
webserver_packages:
- nginx
The second task copies a default NGINX configuration into place. The source of this configuration is the roles/webserver/files/etc/nginx/nginx.conf
file, which looks like a default NGINX configuration file with some minor changes, such as disabling server tokens:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
}
Notice that I didn't just put this file in roles/webserver/files/nginx.conf
. Instead, I matched the source path to the destination path on the server: roles/webserver/files/etc/nginx/nginx.conf
. While this makes your directory tree a bit larger, it's a great way to ensure that you know where your files and templates are going on your destination host. I've found this to be a valuable organizational tactic over the years.
Finally, you can see that the configuration task notifies the Restart NGINX handler, which is located in roles/webserver/handlers/main.yml
:
- name: Reload NGINX
service:
name: nginx
state: reloaded
Running this playbook (using ansible-playbook -i inventory.ini site.yml
) deploys a very basic NGINX configuration, but my server isn't doing anything useful yet. The next step is to configure server blocks to serve each of my web sites.
Deploy server blocks
NGINX uses server blocks to configure individual, named sites. Server blocks are analogous to virtual hosts in the Apache web server. To make my web sites accessible, I need a server block for each site. It might be tempting to set up an individual task to deploy each block, like so:
- name: Add server block for site1.example.com
copy:
src: etc/nginx/conf.d/site1.example.com
dest: /etc/nginx/conf.d/site1.example.com.conf
- name: Add server block for site2.example.com
copy:
src: etc/nginx/conf.d/site2.example.com
dest: /etc/nginx/conf.d/site2.example.com.conf
However, this requires that you make changes to the actual Ansible code every time you want to add a site. It also relies on individual, static config files for each server block. That seems excessive and isn't really in the spirit of writing reusable components that can be easily leveraged in other projects.
Ansible’s templates and loops provide a great way to accomplish this in a reusable manner. Instead of defining a separate task for each site’s config, I can loop through the contents of a variable and template out a configuration file for each server block:
- name: Add static site config files
template:
src: etc/nginx/conf.d/site.conf.j2
dest: "/etc/nginx/conf.d/{{ item.name }}.conf"
owner: root
group: root
mode: 0644
with_items: "{{ webserver_static_sites }}"
notify: Reload NGINX
This task uses the template module to create individual configuration files for each site defined in the webserver_static_sites
variable. This variable is defined in the group variables for the webserver group in group_vars/webservers.yml
:
webserver_static_sites:
- name: site1.example.com
root: /usr/share/nginx/site1.example.com
You can see that the webserver_static_sites
variable contains a list of dictionaries, each representing a single site. This all comes together once you take a look at the template used for the configuration file at roles/webserver/templates/etc/nginx/conf.d/site.conf.j2
:
server {
listen 80;
listen [::]:80;
server_name {{ item.name }};
root {{ item.root }};
index index.html
server_tokens off;
charset utf-8;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
Based on this template, you can see that the task loops through each entry in the webserver_static_sites
variable. The task then creates a configuration file for each one with the appropriate directives (the server_name and root) filled in.
In this case, I only have one site listed (site1.example.com). Running Ansible against this setup produces a single /etc/nginx/conf.d/site1.example.com.conf
file that looks like this (notice that server_name and root have been filled in from the template):
server {
listen 80;
listen [::]:80;
server_name site1.example.com;
root /usr/share/nginx/site1.example.com;
index index.html
server_tokens off;
charset utf-8;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
This seems like a lot of work for a single site! However, the real power of this reusable approach comes into play when I want to add multiple sites. For example, consider the modified group_vars/webserver.yml
file below:
webserver_static_sites:
- name: site1.example.com
root: /usr/share/nginx/site1.example.com
- name: site2.example.com
root: /usr/share/nginx/site2.example.com
- name: site3.example.com
root: /usr/share/nginx/site3.example.com
Without making any changes to my code, I add new sites to my web server by simply adding entries to the webserver_static_sites
variable. Subsequent Ansible runs deploy all of the configuration needed to host these sites, and I can now re-use this web server role in other projects.
You'll notice that I used a very basic NGINX configuration file for each site. However, this pattern can easily be extended to add more configuration directives. Give it a try with some of the more common ones that you might use in a web site config!
Deploy code
If you've been following along up until this point, you have an Ansible role that installs and configures NGINX with a base config and server blocks. While this is great, nobody wants to visit your site if it doesn't have any content. Next, I'll discuss two different ways that you might deploy content to your site.
The first method for deploying site content is to simply copy resources from your local machine to your web server by using the copy module:
- name: Copy site contents
copy:
dest: "{{ item.root }}/"
src: "usr/share/nginx/{{ item.name }}/"
owner: root
group: root
mode: 0755
with_items: "{{ webserver_static_sites }}"
The above task iterates over each site defined in the webserver_static_sites
variable and copies the site contents in roles/webserver/files/usr/share/nginx/{{ item.name }}
to the destination on the remote server. If you're just starting down this path, this is a solid beginning. However, this entangles your application code (your web site content) with your configuration management system (the Ansible repository). While you could move the source path outside the Ansible role's directory tree, this approach also assumes that all of your web site code is available (and up to date).
A better way would be to store your web code in a source code management system, such as Git, and have Ansible deploy your code from this repository. Such storage ensures that the code on your web server is always up to date, and it fully decouples your configuration management from the web site's code and resources. First, Git must be installed on the web server. The installation is easy thanks to the separation of packages into a variable in the roles/webserver/vars/main.yml
file:
webserver_packages:
- nginx
- git
Next, replace the copy task from the previous example with the task below. This task uses Ansible's Git module to check code out of a repository and place it into the appropriate destination directory on the web server:
- name: Clone git repositories
git:
repo: "{{ item.repository}}"
dest: "{{ item.root }}"
force: yes
with_items: "{{ webserver_static_sites }}"
This step creates an additional key (repository) in the dictionary for each web site. The group_vars/webserver.yml
file can be updated with this key, which allows Ansible to pull code from a remote Git repository:
webserver_static_sites:
- name: site1.example.com
root: /usr/share/nginx/site1.example.com
repository: https://github.com/acritelli/example-static-site.git
Note that if you have already deployed code using the copy module example, you will have to delete the directory on the web server. You need to do this because Git won't clone into a directory that isn't empty.
Final thoughts
Deploying a simple, static web site is a great way to dip your toes into Ansible and see immediate results. It's a task that some sysadmins take for granted; after all, many of us host private web servers and sites. However, taking the time to write your own Ansible role that allows for quick deployment of a web server is a task worth doing. It will help you sharpen your Ansible skills, shorten the time to deploy your server when the inevitable OS upgrade time comes, and allow you to write a reusable role that can be put to work in other parts of your infrastructure.
The code for this article is available on GitHub.
[ Need more on Ansible? Take a free technical overview course from Red Hat. Ansible Essentials: Simplicity in Automation Technical Overview. ]
![Author’s photo](/sysadmin/sites/default/files/styles/user_picture_square/public/pictures/2022-12/anthony_critelli.jpg?itok=ELMCxi9b)
Anthony Critelli
Anthony Critelli is a Linux systems engineer with interests in automation, containerization, tracing, and performance. He started his professional career as a network engineer and eventually made the switch to the Linux systems side of IT. He holds a B.S. and an M.S. More about me