Ansible for DevOps

The authoritative guide on Ansible

Chapter 1 - Getting Started with Ansible

Ansible and Infrastructure Management

  • One of Ansible’s greatest strengths is its ability to run regular shell commands verbatim
  • Idempotence: is the ability to run an operation which produces the same
    result whether run once or multiple times.

Inventory files

  • Ansible uses an inventory file (basically, a list of servers) to communicate with your servers.
    • matches servers (IP addresses or domain names) to groups. Inventory files can do a lot more,
    • /etc/ansible/hosts (the default location for Ansible’s inventory file),

example is the group of servers you’re managing and www.example.com is the domain name (or IP address) of a server in that group

[example]
www.example.com
# www.example.com:2222 # with port

To ping the server, run the command below.

ansible example -m ping -u

Note: Ansible assumes you’re using passwordless (key-based) login for SSH. If you insist on using passwords, add the --ask-pass, -k, to ansible commands.

Run a command on a remote server:

ansible example -a "free -m" -u [username]

Chapter 2 - Local Infrastructure Development: Ansible and Vagrant

A sample ansible playbook in playbook.yml:

--- 
- hosts:all
  become: yes
  tasks:
  - name: Ensure NTP (for time synchronization) is installed.
    yum: name=ntp state=present
  - name: Ensure NTP is running.
    service: name=ntpd state=started enabled=yes

Vagrant is invisibly using its own Ansible inventory file (instead of the one we created earlier in /etc/ansible/hosts), which just defines the Vagrant VM.

To maintain idempotency and handle error conditions, you’ll have to do a lot more extra work with basic shell scripts than you do with Ansible.

Using the name function and/or adding comments to the YAML is a good practice to document your tasks.

Chapter 3 - Ad-Hoc Commands

But even if you only used Ansible for server management and running individual tasks against groups of servers, and didn’t use Ansible’s playbook functionality at all, you’d still have a great orchestration and deployment tool in Ansible!

An inventory file for multiple servers:

# Application servers
[app]
192.168.60.4
192.168.60.5

# Database server
[db]
192.168.60.6

# Group 'multi' with all servers
[multi:children]
app
db

# Variables that will be applied to all servers
[multi:vars]
ansible_ssh_user=vagrant
ansible_ssh_private_key_file=~/.vagrant.d/insecure_private_key

Use ansible with the -a argument ‘hostname’ to run hostname against all the servers: ansible multi -a "hostname". Ansible will run the commands in parallel.

If Ansible reports No hosts matched or returns some other inventory-re- lated error, try setting the ANSIBLE_HOSTS environment variable explicitly: export ANSIBLE_HOSTS=/etc/ansible/hosts

If you get an error like The authenticity of host '192.168.60.5' can't be established, you can set the environment variable ANSIBLE_HOST_KEY_CHECKING=False.

Add the argument -f 1 to tell Ansible to use only one fork (basically, to perform the command on each server in sequence).

A few other example commands:

  • ansible multi -a "df -h" to get free space
  • ansible multi -a "date" to get the time
  • ansible multi -b -m yum -a "name=ntp state=present" ensure ntp is present
  • ansible multi -b -m service -a "name=ntpd state=started enabled=yes" ensure ntpd is on and enabled
  • ansible [host-or-group] -m setup to get an exhaustive list of all the environment details
  • ansible app -b -m package -a "name=git state=present" ensure that the git package is present
  • ansible multi -m stat -a "path=/etc/environment" stat a file
  • ansible multi -m copy -a "src=/etc/hosts dest=/tmp/hosts" copy a file to servers
    • The src can be a file or a directory. If you include a trailing slash, only the contents of the directory will be copied into the dest.
  • ansible multi -b -m fetch -a "src=/etc/hosts dest=/tmp" to fetch a file from a server
  • ansible multi -m file -a "dest=/tmp/test mode=644 state=directory" create a directory
  • ansible multi -m file -a "src=/src/symlink dest=/dest/symlink owner=root group=root state=link" to create a symlink
  • ansible multi -m file -a "dest=/tmp/test state=absent" delete a file or directory

When you use Ansible’s modules instead of plain shell commands, you can use the powers of abstraction and idempotency offered by Ansible. Even if you’re running shell commands, you could wrap them in Ansible’s shell or command modules (like ansible multi -m shell -a "date"),

Users and groups

Try to reserve the --limit option for running commands on single servers. One of the most common uses for Ansible’s ad-hoc commands in my day-to-day usage is user and group management.

  • ansible app -b -m group -a "name=admin state=present" add a group
  • ansible app -b -m user -a "name=johndoe group=admin createhome=yes" add a user
  • ansible app -b -m user -a "name=johndoe state=absent remove=yes" to delete a user

Running commands in the background

Use the following options to run a job in the background:

-B <seconds>: max job runtime.
-P <seconds>: the amount of time between polling

Logs

  • Operations that continuously monitor a file, like tail -f, won’t work via Ansible

  • It’s not a good idea to run a command that returns a huge amount of data via stdout via Ansible

  • ansible multi -b -a "tail /var/log/messages" view the first few lines of log files on the servers.

Cron jobs

It’s best to leave things be in the crontab itself, and always manage entries via ad-hoc commands or playbooks using Ansible’s cron module.

  • ansible multi -b -m cron -a "name='daily-cron-all-servers' hour=4 job='/path/to/daily-script.sh'" to enable a cron job

    • Ansible will assume * for all values you don’t specify (valid values are day, hour, minute, month, and weekday). You could also specify special time values like reboot, yearly, or monthly using special_time=[value]. You can also set the user the job will run under via user=[user], and create a backup of the current crontab by passing backup=yes. And a custom crontab location: cron_file=cron_file_name.
  • ansible multi -b -m cron -a "name='daily-cron-all-servers' state=absent" to remove a cronjob

Deploy a version-controlled application

  • ansible app -b -m git -a "repo=git://example.com/path/to/repo.git dest=/opt/myapp update=yes version=1.2.4" clone a repo

ControlPersist allows SSH connections to persist so frequent commands run over SSH don’t have to go through the initial handshake over and over again. Beginning in Ansible 1.3, Ansible defaulted to using native OpenSSH connections to connect to servers supporting ControlPersist.

Ansible’s Accelerated mode achieves greater performance for playbooks. Instead of connecting repeatedly via SSH, Ansi- ble connects via SSH initially, then uses the AES key used in the initial connection to communicate further commands and transfers via a separate port (5099). You can enable it for a playbook by adding the option accelerate: true to your playbook.

Chapter 4 - Ansible Playbooks

Playbooks (a list of instructions describing the steps to bring your server to a certain configuration state) that are then played on your servers. It is easy to convert shell scripts (or one-off shell commands) directly into Ansible plays.

A sample Ansible playbook:

---
- hosts: all

tasks:
    - name: Install Apache.
      command: yum install --quiet -y httpd httpd-devel
    - name: Copy configuration files.
      command: >
        cp httpd.conf /etc/httpd/conf/httpd.conf
    - command: >
      cp httpd-vhosts.conf /etc/httpd/conf/httpd-vhosts.conf
    - name: Start Apache and configure it to run at boot.
      command: service httpd start
    - command: chkconfig httpd on

Run it using ansible-playbook playbook.yml

--- 
- hosts:all
  become: yes
  tasks:
    - name: Install Apache.
      yum: name={{ item }} state=present
      with_items:
        - httpd
        - httpd-devel
    - name: Copy configuration files.
      copy:
        src: "{{ item.src }}"
        dest: "{{ item.dest }}"
        owner: root
        group: root
        mode: 0644
      with_items:
        - src: "httpd.conf"
          dest: "/etc/httpd/conf/httpd.conf"
        - src: "httpd-vhosts.conf"
          dest: "/etc/httpd/conf/httpd-vhosts.conf"
    - name: Make sure Apache is started now and at boot.
      service: name=httpd state=started enabled=yes

The greater-than sign (>) immediately following the command: module directive tells YAML “automatically quote the next set of indented lines as one long string, with each line separated by a space”.

Running the playbook with the --check option (see the next section below) verifies the configuration matches what’s defined in the playbook, without actually running the tasks on the server.

Ansible playbook command options:

  • --limit or -l limit the hosts or groups to run the playbook against
  • --inventory=PATH or -i define a custom inventory file
  • --verbose or -v verbose output
  • --extra-vars=VARS or -e extra vars in key=value format to be used in the playbook
  • --forks=NUM or -f number of forks to run the playbook using concurrently
  • --connection=TYPE -c e.g. ssh, local etc
  • --check dry run mode
  • --list-hosts to list the hosts targeted by the playbook.
  • --remote-user to connect as a specific remote user
  • --become-user to define the sudo user to become
  • --ask-become-pass to define the password to become sudo
  • --become to run all commands as sudo

Playbook Parameters

  • hosts: all in a playbook means that the playbook will run against all hosts.
  • remote_user is used to set the user to connect as on the remote machine and overrides the inventory file
    • Sometimes you'll need to combine this with --ask-become-pass

Chapter 5 - Ansible Playbooks - Beyond the Basics

To notify multiple handlers from one task, use a list for the notify option:

- name: Rebuild application configuration.
  command: /opt/app/rebuild.sh
  notify:
    - restart apache
    - restart memcached

To have one handler notify another, add a notify option onto the handler—handlers are basically glorified tasks that can be called by the notify option

handlers:
  - name: restart apache
    service: name=apache2 state=restarted
    notify: restart memcached
  - name: restart memcached
    service: name=memcached state=restarted
  • Handlers will only be run if a task notifies the handler.
  • Handlers will run once, and only once, at the end of a play.

It’s recommended you use a task’s register option to store the environment variable in a variable Ansible can use later, for example.

-name: Add an environment variable to the remote user's shell.
lineinfile: "dest=~/.bash_profile regexp=^ENV_VAR= line=ENV_VAR=value"

-name: Get the value of the environment variable we just added. 
 shell: 'source ~/.bash_profile && echo $ENV_VAR'
 register: foo

-name: Print the value of the environment variable. 
 debug: msg="The variable is {{ foo.stdout }}"

Linux will also read global environment variables added to /etc/environment:

- name: Add a global environment variable.
  lineinfile: "dest=/etc/environment regexp=^ENV_VAR= \
  line=ENV_VAR=value"
  become: yes

You can also define environment variables per play:

- name: Download a file, using example-proxy as a proxy.
  get_url: url=http://www.example.com/file.tar.gz dest=~/Downloads/
  environment:
    http_proxy: http://example-proxy:80/

You can pass an environment in via a variable in your playbook’s vars section:

vars:
  var_proxy:
    http_proxy: http://example-proxy:80/
    https_proxy: https://example-proxy:443/
    [etc...]
tasks:
- name: Download a file, using example-proxy as a proxy.
  get_url: url=http://www.example.com/file.tar.gz dest=~/Downloads/
  environment: var_proxy

To set environment variables system wide using /etc/environment:

#Inthe'vars'sectionoftheplaybook(setto'absent'todisablepro\ xy):
proxy_state: present

#Inthe'tasks'sectionoftheplaybook: 
- name:Configuretheproxy.
  lineinfile:
    dest: /etc/environment
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
    state: "{{ proxy_state }}"
  with_items:
    - regexp: "^http_proxy="
      line: "http_proxy=http://example-proxy:80/"
    - regexp: "^https_proxy="
      line: "https_proxy=https://example-proxy:443/"
    - regexp: "^ftp_proxy="
      line: "ftp_proxy=http://example-proxy:80/"

To test environment variables: ansible test -m shell -a 'echo $TEST'

Variables

Can be passed in via the commandline using: --extra-vars "foo=bar"

You can set a vars section in the playbook:

vars:
    foo: bar

tasks:
    # Prints "Variable 'foo' is set to bar".
    - debug: msg="Variable 'foo' is set to {{ foo }}"

Or you can use var files in playbooks:

vars_files:
    - vars.yml
tasks:
    - debug: msg="Variable 'foo' is set to {{ foo }}"

Variables may also be added via Ansible inventory files, either inline with a host definition, or after a group. It is not recommended to use inventory files. Use group_vars and host_vars YAML variable files within a specific path instead.

Setting Variables

Ansible allows you to use register to store the output of a particular command in a variable at runtime.

Accessing

Definition:

foo_list:
  - one
  - two
  - three

Accessing:

foo[0]
foo|first

Or:

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

Ansible lets you define or override variables on a per-host or per-group basis. This is easiest to do in the inventory file.

Other variables that ansible provides

  • groups: A list of all group names in the inventory.
  • group_names: A list of all the groups of which the current host is a part.
  • inventory_hostname: The hostname of the current host, according to the inven-
    tory (this can differ from ansible_hostname, which is the hostname reported by
    the system).
  • inventory_hostname_short: The first part of inventory_hostname, up to the
    first period.
  • play_hosts: All hosts on which the current play will be run.

Facts

Facts are variables derived from host system information. You can turn them off in a playbook by setting: gather_facts: no. Doing so can be helpful for deploys with significant numbers of servers. You can also manually add facts by adding INI or JSON files to /etc/ansible/facts.d/. It’s often better to build your playbooks in a way that doesn’t rely (or care about) specific details of individual hosts.

Ansible Vault

Ansible vault encrypts sensitive data so that it can be committed and stored alongside the rest of your repository.

You include and use the keys in the encrypted yaml file normally and Ansible decrypts them on the fly.

vars_files:
    - vars/api_key.yml # encrypted yaml file
  • Use --ask-vault-pass when running a playbook to have Ansible ask for the vault password at runtime
  • ansible-vault edit <file name 1> <file name 2> to edit the encrypted file
    • Additional commands are: rekey to change the password, create, view, or decrypt.

You can supply vault passwords via a password file located at: ∼/.ansible/vault_pass.txt and set strict perms (600). You will need to pass the location using --vault-password-file flag.

Variable Precedence and Sane Defaults

• Roles should provide sane default values via the role’s ‘defaults’ variables. They are the fallback variables.
• Playbooks should never define variables but include them via vars_files or via inventory (in that preference order).
• Only host or group specific variables should be defined inventories.
• Dynamic and static inventories should be kept to a minimum
• Command line variables (-e) should be avoided unless testing locally and the like

Jinja, Python built-ins, and Logic

  • Jinja2
    • types include integers, floats, lists, tuples, and booleans
    • math operations including addition, subtraction, multiplication and division, and comparisons
    • object foo is defined for an object check
      • undefined, iterable, and equalTo are also options
    • Logical operators: and, or, and not
  • Python built-ins: when, changed_when, and failed_when
    • you can also invoke pthon built-in library functions
  • register makes a play available to all plays once registered
  • when is a play conditional that checks a variable to decide if it should run: when: is_db_server. It can use the result of prior plays.
  • If you use the command or shell module without also using changed_when, Ansible will always report a change.
  • failed_when can be used to declare an actual failure and not a false positive
  • ignore_errors use to do just that

Delegation, Local Actions, and Pauses

  • delegate_to allows you to delegate any task to a specific machine including the host.
    • local_action is the easier method to delegate to localhost
    • --connection=local when wanting to run a playbook only against the local machine
  • wait_for can be used to pause your playbook execution to wait

Prompts

You can use prompts to collect sensitive and non-sensitive data. Options include: encrypt, confirm, salt_size, private, and default.

vars_prompt:
    - name: share_user
      prompt: "What is your network username?"
    - name: share_pass
      prompt: "What is your network password?"
      private: yes

Tags

Tags allow you to include or exclude particular roles, files, tasks, and plays. Invoke ansible-playbook using --tags or --skip-tags and give it a comma separated string of tags: "one,two".

Use tags for a larger playbook's individual roles and plays. However, generally avoid tags for individual tasks or includes.

Blocks

Blocks allow you to group related tasks together, using the same task parameters (with_items, become, etc) and allow you to handle errors inside of those blocks. r

The rescue syntax is like so:

tasks: 
    - block: 
        #  do something here
      rescue:
        # rescue work here
      always: 
        # does what it says 

It may be easier to use failed_when to define acceptable failure conditions.

Chapter 6 - Playbook Organization - Roles, Includes, and Imports

Imports

You can use the import_tasks directive to import tasks via a yaml file.

Ansible will retrieve variables from the global scope or you can specifically define them when calling import:

tasks:
  - import_tasks: user.yml
    vars:
      username: johndoe
      ssh_private_keys:
        - { src: /path/to/johndoe/key1, dest: id_rsa }
        - { src: /path/to/johndoe/key2, dest: id_rsa_2 }
  • Use import_tasks if the tasks can be inlined before the playbook runs
  • Use include_tasks if the tasks need to be more dynamic
    • As of Ansible ^2, includes are evaluated dynamically
  • Handlers can be imported just like tasks
  • Playbooks can be included in other playbooks at the top level of a playbook.

Most of the time it is best to start with a monolithic playbook and then seperate things out into smaller task, handler, and playbooks to accomplish the job.

Roles

Ansible automatically includes main.yml files inside of the role directories. The role directory structure is: role_name as the directory name for the role with sub-directories in that directory of tasks and meta.

--- 
- hosts:all
  roles:
    - role_name