met_ansible_yet?

Ansible (ansible.com) is a tool of choice to automate anything you’d like for your servers. And guess what? “- What?” It’s written in Python. Hooray! :tada:

It took me waaay too long to realize it was THE tool I needed. But there are new things everyday in our industry, keeping up is hard. At Europython2017, I had the opportunity to witness how well Ansible spread within our community. Everyone seems to love it and I’m late to the party.

What you need to know right now : Ansible is dead simple to use and can do magic for you. Most of the time, you don’t have to hack a single thing. It’s like the phone marketplaces :phone:, if you want something, there are certainly apps for that. Hum, sorry, here, it’s called a module but it’s all the same.

On the Ansible website, you may notice something called “Ansible Tower” but let’s stick to the free part and the pure Ansible part.

Let’s take together a dead simple example to discover Ansible: nginx installation on a remote server.

Install

Before anything which may seem witty, we need to install it! You can see the official installation documentation for a more detailled approach. As I’m working on an ubuntu machine, the following examples assume you have a similar setup or you have the ability to “translate” the commands.

Via apt ubuntu :

sudo apt-get install software-properties-common
sudo apt-add-repository ppa:ansible/ansible
sudo apt-get update
sudo apt-get install ansible

:gift: That’s it.

First blood

Files we’re going to create can live almost anywhere on your machine. Let’s suppose you store them all in one place, maybe in your project root repository or in a folder inside it.

Let’s create a file with server lists called hosts:

[web]
192.168.33.10
192.168.33.11
192.168.33.12

[database]
192.168.33.13
192.168.33.14

It allows us to run recipes for groups and list servers within each group. Let’s start with a more simple “hosts” file containing only the address of one server of mine:

[web]
192.168.33.10

Ansible is about creating recipes to do stuff. It’s like cooking but with code. A recipe is called a playbook and is written with yaml

- name: Playbook to install nginx
  hosts: web
  remote_user: root

  tasks:
    - name: Install git
     (...)

remote_user being the user we want to use.

From here, we can define some tasks to perform. For example, we may want to install a few dependencies before nginx on our machine.

Ansible provide modules to ease such tasks. Modules can be found online on their website. This is one of the most important page of the Ansible website.

To install git for example, we can use the apt module which need name and update-cache as arguments. While we’re on it, let’s also add vim on our server to trick everyone because you know, nobody knows how to exit vim.

- name: Playbook to install nginx
  hosts: web
  remote_user: root

  tasks:
    - name: Install git
      apt: name=git update_cache=yes
    - name: Install Vim
      apt: name=vim

Here, we have our first playbook. We can execute it with the following command:

ansible-playbook -i hosts playbook.yml

Output should be similar to:

PLAY [Playbook to install nginx] ***********************************************

TASK [setup] ***************************************************
ok: [192.168.33.10]

TASK [Install git] ***************************************************
changed: [192.168.33.10]

TASK [Install Vim] ***************************************************
changed: [192.168.33.10]

PLAY RECAP *******************************************************************
192.168.33.10    : ok=3   changed=2   unreachable=0   failed=0

If we execute this playbook again, Ansible will check first if packages are installed and it will be a lot faster.

Let’s go deeper

We can save a few lines and installed our dependencies all in once. Let’s add some moar packages in this step.

- name: Playbook to install nginx
  hosts: web
  remote_user: root

  tasks:
    - name: Install deps
      apt: name= update_cache=yes state=latest
      with_items:
        - vim
        - htop
        - git

Hooray. I already like this. But you’re not going to impress a lot of people with this.

Deeeeeeeper: new user and ssh

Let’s add a task for user creation and let’s copy our ssh key to ease our server connections. We will need new modules: user and authorized_key.

- name: Playbook to install nginx
  hosts: web
  remote_user: root

  tasks:
    - name: Install deps
      apt: name= update_cache=yes state=latest
      with_items:
        - vim
        - htop
        - git
    - name: User creation
      user: name=adrien comment='My new user' shell=/usr/bin/bash
    - name: Adding SSH key
      authorized_key: user=adrien key=""

Modules are well documented and these lines come directly from the documentation. No panic if you feel a bit lost: stick to the Ansible documentation.

After executing our playbook, we may try to connect to the server with the adrien user and we should indeed connect without a hassle.

Are you even sorry?

We could be happy about what we did but have you paid attention carefully? We have specified multiple times our “user” variable (adrien). We are baaaaaad people. Let’s keep it DRY!

A tiny refactor to the rescue:

- name: Playbook to install nginx
  hosts: web
  remote_user: root
  vars:
    user: adrien

  tasks:
    - name: Install deps
      apt: name= update_cache=yes state=latest
      with_items:
        - vim
        - htop
        - git
    - name: User creation
      user: name= comment='My new user' shell=/usr/bin/bash
    - name: Adding ssh key
      authorized_key: user= key=""

I feel so much better now. Our recipe is easily reusable by anyone else which does’t wan’t to be called like me “adrien”. Only thing to do, update the variables part.

If you’re a bit into testing, you probably already wonder what would happen without the variable user being declared? Well, you can anticipate this in your recipe and skipp some part when irrelevant.

- name: Playbook to install nginx
  hosts: web
  remote_user: root
  vars:

  tasks:
    - name: Install deps
      apt: name= update_cache=yes state=latest
      with_items:
        - vim
        - htop
        - git
    - name: User creation
      when: user is defined
      user: name= comment='My new user' shell=/usr/bin/bash
    - name: Adding ssh key
      when: user is defined
      authorized_key: user= key=""

If you execute the above playbook, both tasks “User creation” and “Adding ssk key” will be ignored ("skipping" on the ouput of Ansible).

Promote everyone

Our noob user adrien does not have superuser rights, let’s add him to the “sudoers” group. In order to perform this, we’re going to create a template and use our first jinja file!

Create templates/sudoers.j2:

 ALL=(ALL:ALL) NOPASSWSD: ALL

And within our playbook :

- name: Playbook to install nginx
  hosts: web
  remote_user: root
  vars:

  tasks:
    - name: Install deps
      apt: name= update_cache=yes state=latest
      with_items:
        - vim
        - htop
        - git
    - name: User creation
      when: user is defined
      user: name= comment='My new user' shell=/usr/bin/bash
    - name: Adding ssh key
      when: user is defined
      authorized_key: user= key=""
    - name: "" becomes a sudoer
      when: user is defined
      template: src=templates/sudoers.j2 dest=/etc/suoders.d/-sudoer validate='visudo -cf %s'

Hooray, a step closer to our initial goal.

Roles, everywhere

The next step to get nginx is to install and setup nginx. This step seems obvious. We want to install nginx via ansible. It is more than obvious that at some point in our playbook tasks, we’re going to need to install nginx. We could add a new tasks right away but let’s take a step back and look at what we have done so far.

We are still very far from our goal and yet the playbook is getting longer and longer. What about reusability?

Ansible provides the concept of roles. One role is responsible for a specific task and can be reused for different playbook.

Let’s create a new folder roles/utils/tasks and a file main.yml in it. Copy/paste the content of your first task in it:

- name: Install deps
  apt: name= update_cache=yes state=latest
  with_items:
    - vim
    - htop
    - git

Let’s create another repository for our user role roles/user/tasks and the according file main.yml:

- name: User creation
  when: user is defined
  user: name= comment='My new user' shell=/usr/bin/bash
- name: Adding ssh key
  when: user is defined
  authorized_key: user= key=""
- name: "" becomes a sudoer
  when: user is defined
  template: src=templates/sudoers.j2 dest=/etc/suoders.d/-sudoer validate='visudo -cf %s'

Our playbook becomes:

- name: Playbook to install nginx
  hosts: web
  remote_user: root
  vars:

  roles:
    - utils
    - user

You are probably wondering if it makes sense to write such stupid roles. We’re not the first users of Ansible and it must have been done a Jeelion times. That’s true. That’s why you can go waste a few precious minutes on galaxy.ansible.com to discover roles written by the community.

Keep in mind that these role are to be used at your own risk. Ensure you understand the content of the roles you want to copy/use. If it can be a bit dangerous to reuse direclty roles from there, it can at least be a great source of inspiration.

nginx role, finally!!!

Now that our playbook is split into differents roles, let’s add the nginx one:

- name: Playbook to install nginx
  hosts: web
  remote_user: root
  vars:

  roles:
    - utils
    - user
    - nginx

Create roles/nginx/tasks/main.yml :

- name: Install
  become: yes
  apt: name=nginx state=latest

- name: Start
  become: yes
  service: name=nginx state=started enabled=true

become: yes allows us to ensure the command try to be executed as an admin.

Executing this playbook, you should get nginx install on your machine. If you go to your server URL, you should get the nginx homepage.

It is quite common to hide this page. Let’s delete the default config and end our ansible discovery for today.

What we need is to delete some files and restart nginx. My first guess is that we’re going to need 2 tasks:

  • One to delete the default config
  • One to restart nginx

If you think about it, every modification to the nginx configuration may require a nginx restart. I’m so lazy I won’t let a chance to my future self to write twice the same task. It allows me to introduce you the last bit of Ansible before I let you go: handlers!!

Every task can notify a handler. Handlers will be executed once all tasks have been completed if they have been notified. In our case, it ensures we specify the restart command once, we notify the handler, and nginx will restart only once after all the changes we need to make on our nginx conf.

Let’s rewrite our nginx role:

- name: Install
  become: yes
  apt: name=nginx state=latest

- name: Start
  become: yes
  service: name=nginx state=started enabled=true

- name: Delete defaut config
  become: yes
  file: path=/etc/nginx/sites-enabled/default state=absent
  notify: reload nginx

Now we need the file roles/nginx/handlers/main.yml:

- name: reload nginx
  action: service name=nginx state=reloaded

Now, nginx homepage shouldn’t be visible anymore.

We could carry on all night and setup everything we need to replicate a production server but I need to go to sleep. We’ve seen everything you need to know to rock with Ansible: tasks, roles, templates and handlers. Have fun, happy coding!

Cheers :beers:


Adrien Brunet

Dev and Beer Lover