Ansible

Introduction

  • push-based config management, also supports pull
  • copies a generated script to the managed host via ssh and then executes it
  • simple for small setups, powerful enough for complex situations
  • connects to several hosts in parallel (but largest scale only via pull mode)
  • master node only requires ansible, no other server (HTTP etc)
  • managed nodes need no client, only ssh and python2
  • written in python, uses yaml playbooks and Jinja2 templates
  • extensible with modules
  • less high-level abstraction than puppet (apt and yum modules instead of abstract package)

Commands

ansible all -m ping                         # test all hosts by invoking ping module
ansible some-host -m setup                  # display all gathered facts (system info)
ansible some-host -a uptime                 # run a given command
ansible some-host -s -a "tail some-file"    # run a given command with sudo
ansible some-host -s -m apt -a name=htop    # install given package using the apt module
ansible-playbook some-playbook.yml          # run a given playbook
ansible-playbook -C -D some-playbook.yml    # run in dry-run mode (--check) and show diff
ansible-playbook --step playbook.yml        # prompt before running each task
ansible-playbook --limit ahost playbook.yml # run playbook only on given host
ansible-playbook --syntax-check file.yml    # check the syntax of a given playbook
ansible-playbook --list-hosts playbook.yml  # list hosts affected by a given playbook
ansible-playbook --list-tasks playbook.yml  # list tasks defined in a given playbook
ansible-doc -l                              # list ansible commands with a short description
ansible-doc some-module                     # read documentation of a given module
ansible <command> -vvvv                     # run command with verbose output for debugging
export ANSIBLE_KEEP_REMOTE_FILES=1          # don't delete remote script files in ~/.ansible/

Configuration

Minimal setup

hosts:

hostname.example.com

playbook.yml:

- hosts: all
  remote_user: root
  tasks:
  - name: install htop
    apt: name=htop state=present

Command to run playbook

ansible-playbook -i hosts playbook.yml

Typical folder structure

├── ansible.cfg
├── handlers/
├── hosts
├── host_vars/
├── group_vars/
├── roles/
│   └── webserver/
│       ├── defaults/
│       ├── files/
│       ├── handlers/
│       ├── tasks/
│       ├── templates/
│       └── vars/
└── site.yml

Variable precedence

-e arg > role > play > host > group > defaults

ansible.cfg

Example ansible.cfg file to configure ansible

[defaults]
hostfile = hosts
sudo_flags = -HE
retry_files_enabled = False
gathering = smart
hash_behaviour = merge

where the hash_behaviour setting enables merging of hash variables for instance from host_vars and group_vars.

hosts

Example hosts inventory file

vagrant1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2222 ansible_ssh_user=vagrant ansible_ssh_private_key_file=/path_to_VM/.vagrant/machines/default/virtualbox/private_key

[server]                # group
vagrant1

[web]
vagrant1

[webserver:children]    # group of groups
web
server

Ansible also supports the concept of a dynamic inventory, i.e. an executable script that returns the hosts in JSON format.

Playbook Snippets

Variables

Prompt for variable values to be entered

vars_prompt:
    - name: user_password
      prompt: Please enter the your password
      private: yes                              # don't show what is being typed

Handlers

Handlers can be notified to restart services or trigger other actions.

Example handlers/services.yml to restart ssh service:

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

Include handler in site.yml:

- hosts: all
  roles:
    - role: remote-login
  handlers:
    - include: handlers/services.yml
      static: yes

Use notify to trigger a service restart in a playbook:

- name: sshd_config file
  copy: src=sshd_config dest=/etc/ssh/sshd_config
  notify: restart ssh

You may want to set handler_includes_static = yes in ansible.cfg to automatically include all handlers as static.

A meta module can be used to trigger the processing of all handlers at a specific moment:

- name: trigger all pending handler actions
  meta: flush_handlers

Templates

Variables

{{ ... }}

Comments are ignored and will not be shown in the deployed file

{# ... #}

Add header with the information that this file has been deployed by Ansible

# {{ ansible_managed }}

Fall back to default value if variable is not set

size = {{ custom_size | default(128) }}

or ignore the key if the value is missing

- name: install packages with pip
  pip: name= {{ item.name }} version={{ item.ver | default(omit) }}
  with_items: pip_packages

Code (loops, conditionals)

{% ... %}

for example

{% for dir in dirs if dir != '/' %}
{% if loop.first %}
folders = {{ dir }}{{ ',' if not loop.last }}
{% else %}
          {{ dir }}{{ ',' if not loop.last }}
{% endif %}
{% endfor %}

Add IP from eth0

{{ ansible_eth0['ipv4']['address'] }}

or equivalently

{{ ansible_eth0.ipv4.address }}

Use lookup to retrieve data from outside

{{ lookup('env', 'PATH') }}
{{ lookup('pipe', 'date') }}

Tags

You may add tags to selected items or roles

roles:
  - { role: webserver, tags: 'webserver' }
  - { role: mysqlserver, tags: [ 'dbserver', 'slowtask' ] }

and then only run tasks with a given tag.

ansible-playbook site.yml --tags "webserver,dbserver"
ansible-playbook site.yml --skip-tags "slowtask"

Package Management

Install packages with apt

- name: update apt cache
  apt: update_cache_yes cache_valid_time=3600
- name: install common tools with apt
  apt: pkg={{ item }}
  with_items:
    - git
    - htop

File Management

Copy a file with given owner and permissions

- name: copy site config
  copy:
    src: authorized_keys
    dest: /root/.ssh/authorized_keys
    owner: root
    mode: 0600

Copy a host-specific file if it exists, the default otherwise

- name: copy proper config
  copy: src={{ item }} dest=/etc/foo.conf
  with_first_found:
    - "foo.conf_{{ inventory_hostname}}"
    - foo.conf_default

Copy and unpack a compressed file to a given directory

- name: copy and extract archive
  unarchive: src=archive.tar.gz dest=/tmp

Create a directory

- name: create ~root/.ssh directory
  file: path=/root/.ssh state=directory

Create a symlink

- name: enable apache site
  file: >
    src=/etc/apache2/sites-available/site.conf
    dest=/etc/apache2/sites-enabled/site.conf
    state=link

Delete a file

- name: disable apache2 default config
  file: path=/etc/apache2/sites-enabled/default state=absent

Use stat for instance to check the existence of a file

- name: check if somefile exists
  stat: path=/path/to/somefile
  register: somefile
- name: run boostrap script (only if somefile does not exist)
  script: bootstrap.sh
  when: somefile.stat.exists == false

Miscellanea

Use wait_for to not continue until a port accepts a connection

- name: wait for webserver to start
  wait_for:
    port: 80
    state: started

or use until loops

- name: wait for web app
  shell: curl --head --silent http://localhost:80
  register: result
  until: result.stdout.find('200 OK') != -1
  retries: 10
  delay: 3

Use register to store output and debug to print it

- name: capture output of whoami
  command: whoami
  register: whoami_cmd
- debug: msg="Logged in as user {{ whoami_cmd.stdout }}"

Use when for conditionals

- shell: cat /etc/motd
  register: motd_contents
- shell: echo "motd contains the word hi"
  when: motd_contents.stdout.find('hi') != -1