Skip to main content

Deploying a static website with Ansible

Learn Ansible by deploying a simple website using NGINX on Linux.
Image by Gerd Altmann from Pixabay

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

Topics:   Ansible   Automation  
Author’s photo

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

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.