December 6, 2013

Day 6 - Ansible: Infrastructure Management Made Easy!

Written By: Derek Carter (@goozbach)
Edited By: Adam Compton (@comptona)

What is Ansible?

Ansible is most easily described by calling it a "configuration management tool", but it is so much more.

Ansible, written in Python, has an extensible, easy to modify architecture, which is built to do more than the common "configuration management" tasks.

Out of the box, Ansible has the following uses: - Normal configuration management tasks: - Create system files via templates - Manage software installation using yum, apt, gem, or the like. - Manage services or daemons (eg. start, stop, enable, disable) - Orchestration tasks: - Remove servers from load balancers - Disable monitoring/alerting - Deploy your code via git - Continuous integration: - Deploy code to QA servers - Run tests - Promote software to production if tests pass

Ansible is agentless. It uses the (most likely already running) ssh server on the target. This access uses either the Python library paramiko (the default) or the stock openssh clients.

An optional connection method is the "Accelerated Mode", which uses an ssh connection to initialize a ZeroMQ connection.

Ansible Architecture

Ansible architecture is simple. An inventory source (by default an INI-style file) defines hosts and the groups they may belong to. You can also use a dynamic inventory script to provide hosts/groups from an external source.

Ansible "Playbooks" are YAML formatted files which define "Plays". "Plays" are lists of "Tasks" which apply to hosts or host groups. "Tasks" define the Ansible "Modules" with the corresponding arguments to accomplish some purpose (eg template a file with the template module). The Ansible "Modules" are the workhorse of the system and do all the heavy lifting. An Ansible "Module" is most often a Python script, but it can be anything which prints JSON.

Getting Started

Prerequisites

The only things that a system needs as an Ansible node is the python-simplejson package, and a running OpenSSH server. Of course, if you properly setup ssh key-based authentication Ansible access becomes easier. Making sure you have proper DNS or host file setup is another way to make Ansible management easier.

Create Your Inventory File

After verifying that SSH access has properly been setup the first step is to create an inventory source.

The default inventory source is the file /etc/ansible/hosts, and is in the INI-file format. Groups use the INI group header syntax [groupname].

A simple host file example:

    # ungrouped hosts
    host1.example.com
    host2.example.com
    webinfra.example.com

    # webservers group
    [webservers]
    web1.example.com
    web2.example.com

    # databaseservers group
    [dbservers]
    db1.example.com
    db2.example.com

Verifying Ansible Connectivity

The easiest way to verify that Ansible can connect to managed nodes is the ping module.

    /usr/bin/ansible all -m ping

Ansible uses "Patterns" to decide which hosts or nodes to apply the current command or playbook. The pattern from our example above is all which is a special pattern which matches all hosts.

If you wanted to run the ping module against the database servers we previously defined we would use this command:

    /usr/bin/ansible dbservers -m ping

You can also use a wild card style syntax to match hosts across groups. For example:

    /usr/bin/ansible web*.example.com

This would match the following from our hosts file:

  • webinfra.example.com
  • web1.example.com
  • web2.example.com

Notice that it matches all hosts regardless of the group.

When using the command /usr/bin/ansible you are operating Ansible in "Ad-Hoc" mode. This is how you run "one-off" modules or commands against Ansible nodes.

One of the more useful things to run in ad-hoc mode is the setup module, which shows you the dynamically created variables which for use in playbooks.

This is what the setup module output looks like:


web1.example.com | success >> {
 "ansible_facts": {
 "ansible_all_ipv4_addresses": [
 "10.0.2.15"
 ],
 "ansible_all_ipv6_addresses": [
 "fe80::a00:27ff:feb9:46f0"
 ],
 "ansible_architecture": "x86_64",
 "ansible_bios_date": "12/01/2006",
 "ansible_bios_version": "VirtualBox",
 "ansible_default_ipv4": {
 "address": "10.0.2.15",
 "alias": "eth0",
 "gateway": "10.0.2.2",
 "interface": "eth0",
 "macaddress": "08:00:27:b9:46:f0",
 "mtu": 1500,
 "netmask": "255.255.255.0",
 "network": "10.0.2.0",
 "type": "ether"
 },
 "ansible_default_ipv6": {},
 "ansible_devices": {
 "sda": {
 "holders": [],
 "host": "SATA controller: Intel Corporation 82801HM/HEM (ICH8M/ICH8M-E) SATA Controller [AHCI mode] (rev 02)",
 "model": "VBOX HARDDISK",
 "partitions": {
 "sda1": {
 "sectors": "1024000",
 "sectorsize": 512,
 "size": "500.00 MB",
 "start": "2048"
 },
 "sda2": {
 "sectors": "82860032",
 "sectorsize": 512,
 "size": "39.51 GB",
 "start": "1026048"
 }
 },
 "removable": "0",
 "rotational": "1",
 "scheduler_mode": "cfq",
 "sectors": "83886080",
 "sectorsize": "512",
 "size": "40.00 GB",
 "support_discard": "0",
 "vendor": "ATA"
 },
 "sr0": {
 "holders": [],
 "host": "IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)",
 "model": "CD-ROM",
 "partitions": {},
 "removable": "1",
 "rotational": "1",
 "scheduler_mode": "cfq",
 "sectors": "2097151",
 "sectorsize": "512",
 "size": "1024.00 MB",
 "support_discard": "0",
 "vendor": "VBOX"
 }
 },
 "ansible_distribution": "CentOS",
 "ansible_distribution_release": "Final",
 "ansible_distribution_version": "6.4",
 "ansible_domain": "vagrantup.com",
 "ansible_eth0": {
 "active": true,
 "device": "eth0",
 "ipv4": {
 "address": "10.0.2.15",
 "netmask": "255.255.255.0",
 "network": "10.0.2.0"
 },
 "ipv4_secondaries": [],
 "ipv6": [
 {
 "address": "fe80::a00:27ff:feb9:46f0",
 "prefix": "64",
 "scope": "link"
 }
 ],
 "macaddress": "08:00:27:b9:46:f0",
 "module": "e1000",
 "mtu": 1500,
 "promisc": false,
 "type": "ether"
 },
 "ansible_form_factor": "Other",
 "ansible_fqdn": "vagrant-centos-6_4.vagrantup.com",
 "ansible_hostname": "vagrant-centos-6_4",
 "ansible_interfaces": [
 "lo",
 "eth0"
 ],
 "ansible_kernel": "2.6.32-358.6.2.el6.x86_64",
 "ansible_lo": {
 "active": true,
 "device": "lo",
 "ipv4": {
 "address": "127.0.0.1",
 "netmask": "255.0.0.0",
 "network": "127.0.0.0"
 },
 "ipv4_secondaries": [],
 "ipv6": [
 {
 "address": "::1",
 "prefix": "128",
 "scope": "host"
 }
 ],
 "mtu": 16436,
 "promisc": false,
 "type": "loopback"
 },
 "ansible_machine": "x86_64",
 "ansible_memfree_mb": 452,
 "ansible_memtotal_mb": 996,
 "ansible_mounts": [
 {
 "device": "/dev/mapper/vg_compute01-lv_root",
 "fstype": "ext4",
 "mount": "/",
 "options": "rw",
 "size_available": 31202488320,
 "size_total": 34373152768
 },
 {
 "device": "/dev/sda1",
 "fstype": "ext4",
 "mount": "/boot",
 "options": "rw",
 "size_available": 426931200,
 "size_total": 507744256
 },
 {
 "device": "/dev/mapper/vg_compute01-lv_home",
 "fstype": "ext4",
 "mount": "/home",
 "options": "rw",
 "size_available": 3109019648,
 "size_total": 3352256512
 },
 {
 "device": "/vagrant",
 "fstype": "vboxsf",
 "mount": "/vagrant",
 "options": "uid=500,gid=500,rw",
 "size_available": 1362345984,
 "size_total": 246850482176
 }
 ],
 "ansible_os_family": "RedHat",
 "ansible_pkg_mgr": "yum",
 "ansible_processor": [
 "Intel(R) Core(TM) i7-3720QM CPU @ 2.60GHz"
 ],
 "ansible_processor_cores": 1,
 "ansible_processor_count": 1,
 "ansible_processor_threads_per_core": 1,
 "ansible_processor_vcpus": 1,
 "ansible_product_name": "VirtualBox",
 "ansible_product_serial": "0",
 "ansible_product_uuid": "D80F25AC-893F-4307-81D6-225A0D274010",
 "ansible_product_version": "1.2",
 "ansible_python_version": "2.6.6",
 "ansible_selinux": {
 "config_mode": "enforcing",
 "mode": "enforcing",
 "policyvers": 24,
 "status": "enabled",
 "type": "targeted"
 },
 "ansible_ssh_host_key_dsa_public": "AAAAB3NzaC1kc3MAAACBAPY3J6VaqhkMGNuMIKwYlqYOqzU/Mnp1k3p8pW4ABHXV6qQBdR9IygvK1VwdcO+OJJeVByfr6WxZszgeGVFjEByjE9ysVybe7pp5LsSKehJYYlnY1JRYBc6T6xvq4PBt2TUBxKDtYc0Wo+B6ieDmDQMlqV+MQyxa0E7nxLcBju/XAAAAFQDHEiCCQVAhA/zakoUAX7seyj4D6wAAAIEAgb08Hr84dSBDc50rIAHnVLoHu321NFuwF7k7tGXw0VileStISTT4sEBgruv0Roh1BheUR3oB1Toi0uBtRqphaVdccYdvvkbR+lWMJdwLmG5G48P8DD6a7940pb1x5anHITKON/0XWJzWUqyYWqvwaUHa5l8HJ8lldN2hGpuH1LsAAACAC9EYHjK3JGZZt9k48cCfFf+kcxDQ0FoJDdz4yJ8ZvYPPrU0yCa9DREKap8L5Vz7TnhHRZ2qipRa1toyZXpicS1v6JeS6rOMxEKcnDhX2HEEdKSoEJIYBwwy8CWdvn8gTvsDEmM5xvLBWozw8Sx5+JwBLR6X9YWC9hY9xZ6QYckk=",
 "ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAABIwAAAQEAnUqMarmVF5hlX3sH4ktAf36uFSTXKmtDvdo7SHOgkBCY6tItVR0rg35Bqdz2jTHHCSNljWRRTNDnvWUyerr43/vpc7UlacVlCGbnv8DTZ2Ktn5kh29cUCFbaWNZxcQPLKa9Rf2VefiPtn0CQp1R/hadK/ow3LHACNe8WkQPB3RqnrQn273QDYmCIscjPE8RC+v3pqw8adU/EUBmm10q8lBB5lYpZS4C0HBxdBnr40sVLLsH7WzKo+QvQN+p6UU1VOXrdNkQV3Krlrj1qJeLlxG56i+dgmRSHR8cmeiD2+w63zxKEwzqFeOs23pOGq1pOoldl75es4r9Ws4QufcHdqw==",
 "ansible_swapfree_mb": 3902,
 "ansible_swaptotal_mb": 3903,
 "ansible_system": "Linux",
 "ansible_system_vendor": "innotek GmbH",
 "ansible_user_id": "root",
 "ansible_userspace_architecture": "x86_64",
 "ansible_userspace_bits": "64",
 "ansible_virtualization_role": "guest",
 "ansible_virtualization_type": "virtualbox"
 },
 "changed": false
}

Ansible Playbooks

The other method of running Ansible is by creating a playbook. Ansible playbooks are simple YAML syntax files which look something like this:

    ---
    - hosts: all
      remote_user: root
      tasks:
        - name: keep vim-enhanced package up to date
          yum: name=vim-enhanced state=latest

This defines which hosts the playbook will attempt to configure the remote user, used for SSH, and makes sure the package vim-enhanced is the latest version using the yum Ansible module. You would use this playbook by putting it in a file (lets say /etc/ansible/playbooks/vim.yml), and applying or running the playbook by using the command:

    /usr/bin/ansible-playbook /etc/ansible/playbooks/vim.yml

You will see some output like this:

    PLAY [all] ******************************************************************** 

    GATHERING FACTS *************************************************************** 
    ok: [foo.example.com]

    TASK: [keep vim-enhanced package up to date] ********************************** 
    ok: [foo.example.com]

    PLAY RECAP ******************************************************************** 
    foo.example.com                : ok=2    changed=0    unreachable=0    failed=0

Now that we have this good start, let's add a bit of complexity by creating and using some variables. Ansible variables come from a myriad of sources (from highest precedence to lowest):

  • "Facts" derived from the setup module.
  • Passed from the command line using the -e switch. (these variables always win)
  • Set in playbook.
  • Role variables. (More on roles later)
  • Variables passed from inventory.
  • Host variables. (from /etc/ansible/host_vars/<HOSTNAME>)
  • Group variables. (from /etc/ansible/group_vars/<GROUPNAME>)
  • Site default variables. ( from /etc/ansible/group_vars/all)
  • Role "default" variables.

Variable files are defined in YAML files as well. The contents of /etc/ansible/group_vars/all could look something like this:

    ---
    apache_service: httpd

To use a variable in a playbook or a template use this syntax:

        - name: restart apache service
          service: name={{ apache_service }} state=restarted

This task would allow you to define the name of the apache service based on the system or group.

Ansible Work flows

You now know enough about the format of the Ansible playbooks to start using Ansible in earnest. As mentioned before, Ansible is a pretty flexible tool and can be used in many different work flows. And of course, the modules and tasks you'd use for each can overlap.

Common Configuration Management Tasks

Ansible can be used in a configuration management work flow. Some of the common tasks done in this work flow include:

Create a user account:

    - name: create foo user
      user: name=foo state=present password=<SALTED_HASH>

More options for the user module are found in its documentation

install a package

    - name: install foo package
      yum: name=foo state=latest

or

    - name: install foo package via `apt`
      apt: name=foo state=installed

There are additional packaging modules as well.

Enable and start a service

    - name: enable foo service
      service: name=foo enabled=yes

    - name: start foo service
      service: name=foo state=started

Additional service documentation

Creating a file from a template

There are two steps to of a template, the task and the template itself. The task looks like this:

    - name: template the file /etc/motd
      template: src=motd.j2 dest=/etc/motd owner=root group=wheel mode=0644

The template module gets most of its arguments from the file module

The template file syntax is Jinja2 and a simple template for the motd task above would look like this:

    Welcome to host: {{ ansible_hostname }}
    Please be kind

Running tasks on changes

If you want to run a task when a template file changes or another task creates a change, you use a handler:

    tasks:
      - name: template apache config
        template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
        notify:
          - restart apache

You then create a handler in the playbook:

    handlers:
      - name: restart apache
        service: name=httpd state=restarted

Notice, you define any module as you would with a normal task.

Orchestration work flow

Ansible can also use some features to do normal orchestration tasks such as deploying code from Git. The real power however, comes from the ability to delegate a single task to a different server.

A good example is removing a server from a load balancer prior to deploying code:

    tasks:
      - name: remove host from LB
        shell: /usr/local/bin/remove_host {{ ansible_hostname }}
        delegate_to: loadbalancer.example.com

      - name: deploy code
        git: repo=http://git.example.com/project/foo.git dest=/var/www/html/
        notify:
          - restart apache

      - name: add host to LB
        shell: /usr/local/bin/add_host {{ ansible_hostname }}
        delegate_to: loadbalancer.example.com

Continuous Integration Work flow

Using Ansible as part of a continuous integration system is a powerful work flow. Ansible can easily use the same playbooks against different environments by simply changing the inventory source.

To run a site-wide playbook against production you would use the default command syntax:

    /usr/bin/ansible-playbook /etc/ansible/playbooks/site.yml

To run the same site-wide playbook against the development environment you would change the hosts file using the -i switch:

    /usr/bin/ansible-playbook -i /etc/ansible/hosts-dev /etc/ansible/playbooks/site.yml

Advanced Playbook Syntax

There are some advanced syntax options for tasks which allow for better control and administration.

Tags

Tags allow for selective execution of tasks.

    tasks:
      - name: template apache config
        template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
        tags:
          - templates
          - apache
        notify:
          - restart apache

      - name: template the file /etc/motd
        template: src=motd.j2 dest=/etc/motd owner=root group=wheel mode=0644
        tags:
          - motd

To run just the motd tasks you would run ansible-playbook this way:

    /usr/bin/ansible-playbooks /etc/ansible/playbooks/site.yml --tags=motd

Conditional Execution

You may also want to skip tasks based on some criteria, this is done with the when parameter.

      - name: template the file /foo.txt
        template: src=foo.j2 dest=/foo.txt owner=root group=wheel mode=0644
        when: ansible_hostname == "foo.example.com"

The above task will only run on the host foo.example.com.

Loops

You can also loop over items in a single task. Some modules (such as yum and apt) can also do smart grouping with a list of items.

    - name: install packages loop
      yum: name={{ item }} state=present
      with_items:
        - vim-enhanced
        - screen
        - nano
        - mlocate

Graduate-Level Study

Ansible has more advanced features which are a bit out of the scope of this article.

Ansible Roles

Ansible roles are used for organization and code-reuse. They allow for a more modular and easier to read playbook.

Accelerated Mode and Asynchronous Mode

SSH connections do add some overhead. To properly scale to a large number of hosts, Ansible has a "Accelerated Mode" which starts a transient daemon for a lighter-weight communication channel.

Other Features

  • Python API

You can use the provided Python API to add Ansible features into your own projects.

  • Custom Modules

Ansible modules can be written in any language. They only need to be able to output JSON.

If you use Python however, there are some shortcuts you can use to get some basic features.

  • Callbacks

Callbacks allow for custom logging or notifications for tasks

  • Connection Plugins

Connection plugins allow for custom communication channels.

In Closing

Ansible's power and ease of use makes it a good candidate for your environment without the overhead or complexity of other tools. Also because of the extensible nature of the system it can be placed in the work flow which best fits your requirements.

Spend a few minutes and give Ansible a shot.

1 comment:

  1. Slight correction. The most recent versions of Ansible do not use ZeroMQ for 'accelerated' mode. In older versions there was a similar 'fireball' mode that used ZeroMQ, but it's no longer needed. More info here:

    http://www.ansibleworks.com/docs/playbooks_acceleration.html

    ReplyDelete