From charlesreid1

This article covers an example Ansible playbook to set up a Nginx server to serve pages over HTTPS.

The page is organized as follows:

  • Before You Begin: Vagrant setup
  • Part 1: Setting up nginx HTTP server
  • Part 2: Setting up nginx HTTPS server


Before you begin: Vagrant Setup

See Ansible/Nginx Playbook/Vagrant Setup for initial setup of Vagrant machines for testing this playbook.

Ansible Playbook Example 1: Nginx Server Playbook

Creating a simple playbook

The following simple playbook will set up an nginx web server on our fresh Ubuntu machine.

Here are the pieces in our playbook:

  • The playbook itself (YAML file)
  • nginx configuration file
  • nginx HTML templates
  • Create Ansible group webservers

The final directory structure for example 1 will look like this:

playbooks/
        Vagrantfile
        web-notls.yml
        hosts


The Playbook: YAML file

Here is a simple playbook for our secure nginx server:

web-notls.yml:

- name: Configure webserver with nginx
  hosts: webservers
  become: True
  tasks:
    - name: install nginx
      apt: name=nginx update_cache=yes

    - name: copy nginx config file
      copy: src=files/nginx.conf dest=/etc/nginx/sites-available/default

    - name: enable configuration
      file: >
        dest=/etc/nginx/sites-enabled/default
        src=/etc/nginx/sites-available/default
        state=link

    - name: copy index.html
      template: src=templates/index.html.j2 dest=/usr/share/nginx/html/index.html
        mode=0644

    - name: restart nginx
      service: name=nginx state=restarted

Required files: /etc/nginx/sites-available/default, /usr/share/nginx/html/index.html

YAML truth-y values: true, True, TRUE, yes, Yes, YES, on, On, ON, y, Y

YAML false-y values: false, False, FALSE, no, No, NO, off, Off, OFF, n, N

nginx http config file

Here is the corresponding nginx configuration file, which serves HTTP requests only. We put in files/nginx.conf:

files/nginx.conf:

server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        root /usr/share/nginx/html;
        index index.html index.htm;

        server_name localhost;

        location / {
                try_files $uri $uri/ =404;
        }
}

nginx index html page

Likewise, we want to create an index page for nginx to serve up, and we want to put template files into the playbook directory, in the templates subdirectory.

(NOTE: .j2 extension means it is a Jinja 2 template)

playbooks/templates/index.html.j2:

<html>
  <head>
    <title>Welcome to ansible</title>
  </head>
  <body>
  <h1>nginx, configured by Ansible</h1>
  <p>If you can see this, Ansible successfully installed nginx.</p>

  <p>Running on {{ inventory_hostname }}</p>
  </body>
</html>

Ansible hosts file

We will create a webservers Ansible group in the inventory file and refer to this group in the Ansible playbook.

In the playbooks/hosts file the "myvagrantbox" line is put under the heading [webservers]:

playbooks/hosts

[webservers]
myvagrantbox ansible_host=127.0.0.1 ansible_port=2222

Now test it out: ping the webservers group with a single command:

$ ansible webservers -m ping

Output:

testserver | success >> {
    "changed": false,
    "ping": "pong"
}

Running a simple playbook

The ansible-playbook command is used to execute playbooks:

ansible-playbook web-notls.yml

Alternatively, to run a playbook directly, use the shebang line:

#!/usr/bin/env ansible-playbook

Then execute it directly:

./web-notls.yml

Output

Here's the output from the playbook command:

$ ansible-playbook web-notls.yml

 _______________________________________
< PLAY [Configure webserver with nginx] >
 ---------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

 ________________________
< TASK [Gathering Facts] >
 ------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [myvagrantbox]
 ______________________
< TASK [install nginx] >
 ----------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

changed: [myvagrantbox]
 _______________________________
< TASK [copy nginx config file] >
 -------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

changed: [myvagrantbox]
 _____________________________
< TASK [enable configuration] >
 -----------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ok: [myvagrantbox]
 ________________________
< TASK [copy index.html] >
 ------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

changed: [myvagrantbox]
 ______________________
< TASK [restart nginx] >
 ----------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

changed: [myvagrantbox]
 ____________
< PLAY RECAP >
 ------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

myvagrantbox               : ok=6    changed=4    unreachable=0    failed=0

Anatomy of example playbook

Let's examine the example playbook in detail.

Plays

A playbook is a list of plays.

Every play has:

  • a set of hosts to configure
  • a set of tasks to run on those hosts
  • the play is the thing that connects hosts to tasks

Optional play settings:

  • name - a comment that describes what the play is about
  • become - if true, Ansible will run each task by becoming the root user (useful for Ubuntu, where ssh as root is disabled by default)
  • vars - list of variables and values

In our example, the play is this entire section:

- name: Configure webserver with nginx
  hosts: webservers
  become: True
  tasks:
    - name: install nginx
      apt: name=nginx update_cache=yes

    - name: copy nginx config file
      copy: src=files/nginx.conf dest=/etc/nginx/sites-available/default

    - name: enable configuration
      file: >
        dest=/etc/nginx/sites-enabled/default
        src=/etc/nginx/sites-available/default
        state=link

    - name: copy index.html
      template: src=templates/index.html.j2
               dest=/usr/share/nginx/html/index.html mode=0644


    - name: restart nginx
      service: name=nginx state=restarted

Tasks

The tasks are the actions that are performed when the play is run.

The first task is to install nginx:

- name: install nginx
  apt: name=nginx update_cache=yes

This can also be written without the optional name parameter,

- apt: name=nginx update_cache=yes

Can also fold over multiple lines using >:

- name: install nginx
  apt: >
      name=nginx
      update_cache=yes

Actions in tasks are composed of modules.


Modules

There are lots of useful modules that come with Ansible that can be used from playbooks.

  • apt - installs/removes packages using aptitude package manager
  • copy - copies files from local machine to host
  • file - sets attributes of files/symlinks/directories
  • service - starts/stops/restarts a service
  • template - generates a file from a template and copies it to the hosts

Ansible Playbook Example 2: Secure Nginx Server Playbook

The second example introduces two new concepts:

  • variables
  • handlers

In example 2 we will generate a certificate on the machine that is running Ansible and distribute the certificate to the machines we're spinning up. (In this example we can use a self-signed certificate.)


The final directory structure for example 2 will look like this:

playbooks/
    ansible.cfg
    hosts
    Vagrantfile
    web-notls.yml
    web-tls.yml
    files/nginx.key
    files/nginx.crt
    files/nginx.conf
    templates/index.html.j2
    templates/nginx.conf.j2

The Playbook

Here is the YAML playbook file:

web-tls.yml

- name: Configure webserver with nginx and tls
  hosts: webservers
  become: True
  vars:
    key_file: /etc/nginx/ssl/nginx.key
    cert_file: /etc/nginx/ssl/nginx.crt
    conf_file: /etc/nginx/sites-available/default
    server_name: localhost
  tasks:
    - name: Install nginx
      apt: name=nginx update_cache=yes cache_valid_time=3600

    - name: create directories for ssl certificates
      file: path=/etc/nginx/ssl state=directory

    - name: copy TLS key
      copy: src=files/nginx.key dest={{ key_file }} owner=root mode=0600
      notify: restart nginx

    - name: copy TLS certificate
      copy: src=files/nginx.crt dest={{ cert_file }}
      notify: restart nginx

    - name: copy nginx config file
      template: src=templates/nginx.conf.j2 dest={{ conf_file }}
      notify: restart nginx

    - name: enable configuration
      file: dest=/etc/nginx/sites-enabled/default src={{ conf_file }} state=link
      notify: restart nginx

    - name: copy index.html
      template: src=templates/index.html.j2 dest=/usr/share/nginx/html/index.html
             mode=0644

  handlers:
    - name: restart nginx
      service: name=nginx state=restarted

Creating the TLS (SSL) certificate

The next step is to create the certificate that Ansible will distribute to our nodes. This step should be run once (and is optional, if you already have a certificate).

From the playbooks directory, put the certificate into a "files" folder that will hold files we want to copy to the nodes:

mkdir files

Now generate the SSL cert into that folder:

openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
    -subj /CN=localhost \
    -keyout files/nginx.key -out files/nginx.crt

This results in nginx.key and nginx.crt. 3650 = days til expiry = 10 years

Playbook Anatomy

Variables

The playbook defines a variables section that contains four variables:

vars:
  key_file: /etc/nginx/ssl/nginx.key
  cert_file: /etc/nginx/ssl/nginx.crt
  conf_file: /etc/nginx/sites-available/default
  server_name: localhost

Any valid YAML can be used as the value of a variable (lists and dictionaries, strings and booleans).

Variables are referenced both in tasks and in template files. The {{ braces }} notation is used to make a variable substitution.

Example: using variable in a task:

- name: copy TLS key
  copy: src=files/nginx.key dest={{ key_file }} owner=root mode=0600

Quoting

If you reference a variable right after specifying the module, the YAML parser will misinterpret the variable reference as the beginning of an inline dictionary.

WRONG:

- name: perform some task
  command: {{ myapp }} -a foo

You must quote the arguments instead.

RIGHT:

- name: perform some task
  command: "{{ myapp }} -a foo"

More Quoting

Arguments with colons cause similar problems, since the YAML parser will interpret the colon to indicate a yaml dictionary key-value split.

WRONG:

- name: show a debug message
  debug: msg="The debug module will print a message: neat, eh?"

If you quote the whole thing, that won't work either -

WRONG:

- name: show a debug message
  debug: "msg=The debug module will print a message: neat, eh?"

because the debug module's msg arg expected a quoted string, but didn't get one.

To do it right, use quotes around both:

RIGHT:

- name: show a debug message
  debug: "msg='The debug module will print a message: neat, eh?'"

Handlers and Notifiers

The playbook for the secure nginx node also added a handlers section:

handlers:
- name: restart nginx
  service: name=nginx state=restarted

A handler is a conditional task. It is a task that is run if it is notified (triggered) by another task.

To notify the handler, we create a notifier key for the appropriate task. Note that handlers have pitfalls - they make it hard to debug playbooks, multiple handler notifications will only run the handler once per play, and the order of handlers occurs as in the file, not in the order triggered.

Best practice: only use handlers for restarting services and for reboots.

Here is an example task that has a notify key added to it to trigger the restart nginx task:

- name: copy TLS key
  copy: src=files/nginx.key dest={{ key_file }} owner=root mode=0600
  notify: restart nginx

An nginx server would need to restart if:

  • The key changes
  • The cert changes
  • The config file changes
  • The sites-enabled directory contents change

Each of these tasks has a notify statement on it to ensure that Ansible will restart the nginx server in any of the above scenarios.

nginx configuration template

The nginx configuration file uses double-bracket template variables to make it easier to script deployment.

To do this, add another template to the templates folder:

templates/nginx.conf.j2

server {
        listen 80 default_server;
        listen [::]:80 default_server ipv6only=on;

        listen 443 ssl;

        root /usr/share/nginx/html;
        index index.html index.htm;

        server_name {{ server_name }};
        ssl_certificate {{ cert_file }};
        ssl_certificate_key {{ key_file }};

        location / {
                try_files $uri $uri/ =404;
        }
}

The three variables defined above are defined in the variables section of the playbook.

Note that you can also use template variables in the playbooks themselves.

Ansible hosts file

The Ansible hosts file will look the same as before:

[webservers]
myvagrantbox ansible_host=127.0.0.1 ansible_port=2222

Running the playbook

Run the playbook with the ansible-playbook command:

ansible-playbook web-tls.yml

Flags