Ansible Code Promotion in a Shared Hosting Environment

We are deploying our nifty web portal to two web and two app servers. The app servers will also act as alternate web servers, so they must be configered for both roles.

  • The machines are web01, web02, app01, and app02.

  • They are members of the groups webs and apps, respectively.

digraph ansiblecodepromotion {
    rankdir = "LR";
    node [shape = "box", color=blue];
    content [shape="folder", label="code and\ncontent"]
    roles [shape="folder"]
    groups [shape="folder"]
    nodes [shape="folder"]
    variables [shape="folder"]
    ansible [label="Configuration\nEnforcement"]

    subgraph cluster_0 {
        label = "webs";
        color = "orange";
        invis0 [color="transparent", fontcolor=transparent, shape=point]
        web01;
        web02;
        invis0 -> web01 [color=transparent];
        invis0 -> web02 [color=transparent];
    }

    subgraph cluster_1 {
        label = "apps";
        color = "orange";
        invis1 [color="transparent", fontcolor=transparent, shape=point];
        app01;
        app02;
        invis1 -> app01 [color=transparent];
        invis1 -> app02 [color=transparent];
    }

    content -> roles
    roles -> ansible
    groups -> ansible
    nodes -> ansible
    variables -> groups
    variables -> nodes
    ansible -> invis0 [label="web role", fontcolor="red", color="red"]
    ansible -> invis1 [label="web and app role", fontcolor="purple", color="purple"]
    ansible -> web01 [label="node config", fontcolor="blue", color="blue", style="dotted"]
    ansible -> web02 [label="node config", fontcolor="blue", color="blue", style="dotted"]
    ansible -> app01 [label="node config", fontcolor="blue", color="blue", style="dotted"]
    ansible -> app02 [label="node config", fontcolor="blue", color="blue", style="dotted"]
}

Structure of a Role

A sample role directory looks like this. Have the CDS (e.g. Jenkins) deploy to …/roles/<rolename>/src/.

Content

Purpose

…/tasks

Contains the main.yml that specifies state for the installation. This is the file that calls for templating configuration based on variables.

…/vars

Contains the main.yml that lists mock (or dummy) variables.

…/handlers

Contains the main.yml for triggered tasks. This is very convenient. For example, a change in httpd.conf should trigger an Apache2 restart.

…/templates

Contains the *.j2 files that become text files built with variable content. These are Jinja2 and the variables are in plain YAML. No tricks.

…/src

This is a home for all that copies to the host without templating. Putting everything in “templates” is not harmful as long as tasks/main.yml makes sense.

…/meta

Contains the main.yml that tracks dependencies among roles.

Sample Web Server Role

These tasks install Apache and push the configuration file. I can also add an application server role in this doc, but you probably get the point.

/etc/ansible/roles/webserver/tasks/main.yml
 1---
 2- name: Ensure Apache2 exists and is the current version
 3  yum: name=httpd state=latest
 4
 5- name: Build our configuration files from templates and variables
 6  template: src=/etc/ansible/roles/webserver/templates/{{ item }}.j2 dest=/etc/httpd/conf.d/{{item }} mode=0644 owner=www-data group=www-data setype=httpd_config_t backup=yes
 7  with_items:
 8    - portal.conf
 9    - ssl.conf
10  notify: "restart Apache"

This handler restarts Apache when triggered.

/etc/ansible/roles/webserver/handlers/main.yml
1- name: Restart and enable Apache
2  service: name=httpd state=restarted enabled=true
3  listen: "restart Apache"

This Apache configuration template serves a directory Ansible replaces {{portalpath}} with the content of that variable. See Ansible configuration below.

/etc/ansible/roles/webserver/templates/portal.conf.j2
 1Alias /portal {{ portalpath }}
 2# This is obviously not a portal.  I just wanted an illustrative Apache config.
 3<Directory {{ portalpath }}>
 4    AuthType None
 5    Options Indexes FollowSymLinks MultiViews
 6    IndexOptions +FancyIndexing +FoldersFirst +ScanHTMLTitles +TrackModified +DescriptionWidth=*
 7    AllowOverride None
 8    Order allow,deny
 9    Allow from all
10</Directory>

Playbooks

To apply these roles, we need a playbook to bind our instructions together.

/etc/ansible/playbooks/mywebsite.yml
1---
2# This file is a wrapper for the project
3- include: web.yml
4- include: app.yml

Notice how the web.yml installs the web server role to both the web servers and the application servers. This is for illustration.

/etc/ansible/playbooks/web.yml
1---
2# This file tells Ansible to apply the web server role
3- name: ensure web servers are deployed
4  hosts: webs apps
5  become: true
6  gather_facts: true
7  roles:
8    - web

Whereas the app role only applies to the application servers.

/etc/ansible/playbooks/app.yml
1---
2# This file tells Ansible to apply the application server role
3- name: ensure application servers are deployed
4  hosts: apps
5  become: true
6  gather_facts: true
7  roles:
8    - app

Ansible configuration

Our hosts file groups our nodes and declares them for Ansible.

/etc/ansible/hosts.ini
1[webs]
2web01
3web02
4
5[apps]
6app01
7app02

Our group variables file sets these variables for every member of webs.

/etc/ansible/group_vars/webs.yml
1portalpath: /var/www/html/portal/
2somelistvariable:
3  - value1
4  - value2

One of our nodes needs special treatment, so its variable file overwrites the value of portalpath.

/etc/ansible/host_vars/web02.yml
1portalpath: /var/www/html/site/

Run the Playbook

Now, we run this thing. The option flags are…

  • -k to prompt for an SSH password

  • -K to prompt for a sudo password (or press enter for them to be the same)

  • -D show the deltas imposed by Ansible on the target node.

1ansible-playbook -kKD /etc/ansible/playbooks/mywebsite.yml