Learning Ops Tooling, Part Three - More Ansible

Learning Ops Tooling, Part Three - More Ansible

In the last part we looked at setting up an Ansible inventory file and running a simple module against it using the command line. In this post I want to take things a bit further and define a playbook that will install all the pre-requisites for my blog engine, which by the way is Ghost.

Defining a playbook file

A playbook is Ansible's way of defining what needs to be run against your servers. It comprises of one or more plays that themselves comprise of one or more tasks, which each make use of a module. As mentioned in the previous post Ansible comes with a lot of predefined modules that can all be viewed here for accomplishing all manor of jobs.

Lets start by creating a file called playbook.yml. All Ansible playbooks are written in a language called YAML. Which despite what people think doesn't stand for "Yet Another Markup Language" but rather "Yaml Aint A Markup Language".

The first line of any YAML file should be three hyphens --- to denote that the file is in-fact YAML.

It's within this file that we will tell Ansible what to install on our server. But before we can do that we need to tell it what servers to run on and as what user. We can do that by adding the following code at the start:

---
- hosts: ghost
  become: true
  
  tasks:

The hyphen before "hosts" is YAMLs way of defining an item in an array. The root array in this file denotes the plays.

The "hosts" line says that this play should be run against the ghost host, which is the name of the server as we defined it in our inventory file. The "become" command tells Ansible to escalate privileges for running the playbook. This is followed by the task section where we include what tasks we want to run in this play.

Installing the Dependencies

The next thing we need to do is to tell Ansible what dependencies we want installing. The first one we will start with NGINX as you can't really have a website without a web server.

Installing nginx

Ubuntu comes with a command line tool called apt-get pre installed on it and Ansible has a module that will allow you to run apt-get on the servers defined in the play:

- name: Install nginx
  apt:
    name: nginx
    state: latest
    update_cache: yes

Here we are giving the task a name and then telling it run the apt module installing the latest (state) version of ngnix and to update the apt-get cache at the same time. There are more options you can define against the apt task that are detailed in the modules website.

Finally we need to reset nginx to a default status and that means deleting the default website. I won't go into detail about setting up nginx at this point, as quite frankly I've still got a lot to learn, but to do this you need to delete the config file from /etc/nginx/sites-available/ and the sym linked file from /etc/nginx/sites-enabled.

Luckily Ansible also has a module for interacting with files:

- name: delete default site-available
  file:
    state: absent
    path: /etc/nginx/sites-available/default

- name: disable default site-available
  file:
    state: absent
    path: /etc/nginx/sites-enabled/default

And that's it. nginx is installed and reset to a default state.

Installing MySql

MySql is slightly trickier to install in that it needs a couple of packages installing from apt-get and a few more tasks to set it up. It also needs a bit extra work in order to make it immutable so that this script can be run multiple times against the same server without failing.

Firstly we need to pull down MySql and the python-mysqldb package:

- name: Install mysql
  apt:
    name: '{{ item }}'
    state: latest
  with_items:
    - mysql-server
    - python-mysqldb

You'll notice we are doing something a little bit different here. Where we define the "name" we are making use of an Ansible variable, defined by the {{ }}. We are then later telling it to run this command twice using the with_items block. This is just Ansible's way of defining a foreach loop.

Next we need to set the password for the root account:

- name: update mysql root password for all root accounts
  mysql_user:
    name: root
    host: '{{ item }}'
    password: password
    state: present
  with_items:
    - "{{ ansible_hostname }}"
    - 127.0.0.1
    - ::1
    - localhost

Again, there is a specific module for handling mysql users and here we are using to set the password for each of the hosts. However there is a problem with this task, in that if you were to run this script again it would fail because the password has already been changed. To avoid this we also need to copy across a mysql options file again defining the user and the password:

- name: copy .my.cnf file with root password credentials (so its immutable)
  template: 
    src: 'templates/.my.cnf'
    dest: '/root/.my.cnf'
    owner: root
    mode: 0600

Here we are using the template module that will copy a file from the local machine onto the playbook server. All that's contained in the local file is:

[client]
user=root
password=password  

And then finally we just need to delete the temp database and remove anonymous access to mysql:

- name: ensure anonymous users are not in the database
  mysql_user:
    name: ''
    host: '{{ item }}'
    state: absent
  with_items:
    - localhost
    - $inventory_hostname

- name: remove the test database
  mysql_db: 
    name: test
    state: absent

Installing Node

Ghost is a node application so we need to install that too. There's not much to cover here as we've already gone over it all before. Other than the fact we need to register another apt key using the apt_key module:

- name: add nodesource keys
  apt_key:
    url: https://deb.nodesource.com/gpgkey/nodesource.gpg.key
    state: present

- name: add nodesource apt sources
  apt_repository:
    repo: '{{ item }}'
    state: present
  with_items:
    - 'deb https://deb.nodesource.com/node_6.x xenial main'
    - 'deb-src https://deb.nodesource.com/node_6.x xenial main'

- name: install node
  apt:
    name: 'node'
    state: latest
    update_cache: yes

And that's it. We now have all the dependencies installed for the blog.

Bonus

Explaining the state option

You'll have noticed that a number of tasks defined in the above script ask for a "state" rather than just to be installed. This is because of the way Ansible works, in that rather than just being a script to install things on your server, it checks the state before making any changes.

For example if we look at the task to install node:

- name: install node
  apt:
    name: 'node'
    state: latest
    update_cache: yes

Ansible will actually check to see if the latest version of node is installed before trying to install it. This means we can run the script as many times as we like and if node is installed it will do nothing. But if it isn't, or it's been removed Ansible will (re)install it.

The full playbook is in gist.

Comments