Ansible Cheatsheet

General

Ansible is an open-source automation tool that allows IT teams to automate the configuration, deployment, and management of systems and applications. It was developed by Michael DeHaan and is now part of Red Hat.

By using a simple and agentless architecture, it does not require any software or agent to be installed on the target systems. SSH (Secure Shell) or WinRM (Windows Remote Management) are used to connect to the target systems and execute tasks defined in YAML format.

Ansible provides a large number of pre-built modules that can be used to perform various tasks such as package installation, file management, user management, and network configuration. These modules can be used in playbooks, which are YAML files that define a series of tasks to be executed on one or more systems.

Ansible can be used to automate tasks on a wide range of systems and environments, from single servers to large-scale deployments both on-prem or in the cloud. It’s most proficient use cases are: IT operations, DevOps, and software development to automate repetitive tasks, reduce human error, and improve the overall efficiency of IT processes.

Setup Ansible Server

Install Ansible:

# Install ansible (for user).
python3 -m pip install --user ansible

# Upgrade ansible (for user).
python3 -m pip install --upgrade --user ansible

# Display ansible core version.
ansible --version

# Display ansible version.
python3 -m pip show ansible

Uninstall Ansible:

# Unsnstall ansible.
python3 -m pip uninstall ansible

Inventories

Inventory-file

An Ansible inventory file is a plaintext configuration file that lists the target hosts and their associated connection information, enabling Ansible to manage and automate tasks on those hosts.

# Hosts in group 'all'.
host1
host2

# Hosts in a group.
[group-name]
host1
host2

# Hosts in a group with variables defined.
[group-name]
host1 <var_1>=<value_1> <var_2>=<value_2> ... <var_n>=<value_n>
host2 <var_1>=<value_1> <var_2>=<value_2> ... <var_n>=<value_n>

Host Variables

Ansible host variables are user-defined data associated with specific hosts in an inventory file, allowing for customization and parameterization of tasks and playbooks on a per-host basis.

ansible_host                    # Hostname or IP-address
ansible_port                    # Port to connect (default 12)
ansible_user                    # User to use when connecting to the host
ansible_password                # The password to use to authenticate to the host
ansible_ssh_private_key_file    # Private key file used by ssh
ansible_ssh_common_args         # Appended to default command line for sftp, scp, and ssh
ansible_sftp_extra_args         # Appended to default sftp command line
ansible_scp_extra_args          # Appended to default scp command line
ansible_ssh_extra_args          # Appended to default ssh command line
ansible_ssh_pipelining          # Whether or not to use SSH pipelining
ansible_ssh_executable          # Overrides default behavior to use the system ssh
ansible_become                  # Force privilege escalation
ansible_become_method           # Set privilege escalation method
ansible_become_user             # Set user you become through privilege escalation
ansible_become_password         # Set privilege escalation password
ansible_become_exe              # Set executable for the escalation method selected
ansible_become_flags            # Set flags passed to the selected escalation method
ansible_shell_type              # Shell type of the target system
ansible_python_interpreter      # Host Python path
ansible_*_interpreter           # Replaces shebang of modules

Ping hosts

Check if Ansible can connect to all nodes in your inventory.

ansible all -m ping

Note: replace all with a specific group or host to limit the scope of the command.

Playbooks

Executing Playbooks

# Perform syntax verification before executing a plybook:
ansible-playbook --syntax-check playbook.yml
# Configure output verbosity of playbook execution:
ansible-playbook -v playbook.yml
# Execute a dry run:
ansible-playbook -C playbook.yml

Remote Users & Privileges

# Set the user that runs the tasks on the target:
remote_user: "remote-user"
# Set privilege escalation:
become: true
# Set privilege escalation method:
become_method: "sudo"
# Set the user used for privilege escalation:
become_user: "privileged-user"

Modules

Note: When possible, try to avoid the command, shell, and raw modules in playbooks. Because these take arbitrary commands, it is very easy to write non-idempotent playbooks with these modules.

Finding Modules

# List the modules available on the control node:
ansible-doc -l
# Display detailed documentation for a module:
ansible-doc module

Managing Variables

Variables in Playbooks

Variable names must start with a letter, and they can only contain letters, numbers, and underscores.

# Define variables:
vars:
  user: luca
  home: /home/luca
# Define variables in external files:
vars_files:
  - vars/users.yml

# Reference variables (Example - create a user with username luca):
tasks:
  - name: Creates the user {{ user }}
    user:
      name: "{{ user }}"

Note: When a variable is used as the first element to start a value, quotes are mandatory!

Host- and Group Variables

Defining a host variable for a specific host:

[servers]
server.example.com  host_var_name=host_var_value

Defining a group variable for a specific host group:

[servers]
server1.example.com
server2.example.com

[servers:vars]
group_var_name=group_var_value

Best practice: It is recommended to define inventory variables using host_vars and group_vars directories, and not to define them directly in the inventory files.

project
├── ansible.cfg
├── group_vars
│   ├── datacenters
│   ├── datacenters1
│   └── datacenters2
├── host_vars
│   ├── demo1.example.com
│   ├── demo2.example.com
│   ├── demo3.example.com
├── inventory
└── playbook.yml

Overriding Variables from CLI

# Overriding inventory variables from CLI:
ansible-playbook playbook.yml -e "var_name=var_value"

Note: Variables set on the command line are called extra variables.

Using Arrays as Variables

users:
  bjones:
    first_name: Bob
    last_name: Jones
    home_dir: /users/bjones
  acook:
    first_name: Anne
    last_name: Cook
    home_dir: /users/acook
# Return 'Bob'
users['bjones']['first_name']

Capturing Command Output

---
- name: <play-name>
  hosts: all
  tasks:
    - name: Task name
      module:
        name: module-name
        ...
      register: install_result
    - debug: var=install_result

Managing Secrets

Ansible Vault, which is included with Ansible, can be used to encrypt and decrypt any structured data file used by Ansible.

Encrypted Files

Create an encrypted file.

# Create encrypted file using the prompt to provide the vault password:
ansible-vault create secret.yml
# Create encrypted file using a password file:
ansible-vault create --vault-password-file=VAULT_PASSWORD_FILE secret.yml

Modify an encrypted file.

# View an Ansible Vault- encrypted file without opening it for editing:
ansible-vault view secret.yml
# Edit an existing encrypted file:
ansible-vault edit secret.yml
# Change the password of an encrypted file using the prompt:
ansible-vault rekey secret.yml
# Change the password of an encrypted file using a password file:
ansible-vault rekey --new-vault-password-file=NEW_VAULT_PASSWORD_FILE secret.yml

Note: edit always rewrites the file –> only use it when making changes.

Encrypting or decrypting an existing file:

# Encrypting an existing file:
ansible-vault encrypt secret.yml
# Decrypt an existing file and save the decrypted file under a different name:
ansible-vault decrypt secret.yml --output=secret-decrypted.yml

Note: Use the --output=OUTPUT_FILE option to save the encrypted file with a new name (takes 1 input file).

Decrypt Ansible Vault secret

You can view the original value of an encrypted varibale using the debug module, given you have access to the vault password.

  1. cd into the directory of your playbook.
  2. Execute the following ad hoc command:
ansible localhost -m debug -a vars="VARIABLE_NAME" -e "@PATH/TO/VAR_FILE.yml" --vault-id dev@VAULT_PASSWORD_FILE

Note: If you don’t have a file with the password for the Ansible Vault, you can type it in the prompt with the following command:

ansible localhost -m debug -a vars="VARIABLE_NAME" -e "@defaults/main.yml" --vault-id @prompt
Vault password (default):

localhost | SUCCESS => {
    "my_variable": "my_secret_in_plaintext"
}

Playbooks and Ansible Vault

To run a playbook that accesses files encrypted with Ansible Vault, you need to provide the encryption password to the ansible-playbook command.

# Run a playbook with encrypted files using the prompt to provide the vault password:
ansible-playbook --vault-id @prompt playbook.yml
# Run a playbook with encrypted files using a password file:
ansible-playbook --vault-password-file=VAULT_PASSWORD_FILE playbook.yml

Best practice: Place secret variables in a seperate file under the host_vars or group_vars directroy:

.
├── ansible.cfg
├── group_vars
│   └── webservers
│       └── vars
├── host_vars
│   └── demo.example.com
│       ├── vars
│       └── vault
├── inventory
└── playbook.yml

Best practice: Sensitive playbook variables can be placed in a separate file which is encrypted with Ansible Vault and which is included in the playbook through a vars_files directive.

Best practice: If you are using multiple vault passwords with your playbook, make sure that each encrypted file is assigned a vault ID, and that you enter the matching password with that vault ID when running the playbook.

Managing Facts

Ansible facts are variables that are automatically discovered by Ansible on a managed host (host-specific information that can be used just like regular variables).

Dump all Host Facts

ansible all -m ansible.builtin.setup
- name: Fact dump
  hosts: all
  tasks:
    - name: Print all facts
      debug:
        var: ansible_facts

Note: replace all with a specific group or host to limit the scope of the command.

Useful Ansible Facts

ansible_facts['hostname']                                       # Short host name
ansible_facts['fqdn']                                           # Fully qualified domain name
ansible_facts['default_ipv4']['address']                        # Main IPv4 address (based on routing)
ansible_facts['interfaces']                                     # List of the names of all network interfaces
ansible_facts['devices']['vda']['partitions']['vda1']['size']   # Size of the /dev/ vda1 disk partition
ansible_facts['dns']['nameservers']                             # List of DNS servers
ansible_facts['kernel']                                         # Version of the currently running kernel

Before Ansible 2.5, facts were injected as individual variables prefixed with the string ansible_ instead of being part of the ansible_facts variable.

ansible_facts form Old fact variable form
ansible_facts['distribution'] ansible_distribution

Disable injection in ansible.cfg file:

INJECT_FACTS_AS_VARS:
  default: true
  description:
    - Facts are available inside the `ansible_facts` variable, this setting also pushes them as their own vars in the main namespace.
    - Unlike inside the `ansible_facts` dictionary, these will have an `ansible_` prefix.
  env:
    - name: ANSIBLE_INJECT_FACT_VARS
  ini:
    - key: inject_facts_as_vars
      section: defaults
  type: boolean
  version_added: "2.5"

Turn Off Fact Gathering

---
- name: <play-name>
  hosts: all
  gather_facts: false

Creating Custom Facts

By default, the setup module loads custom facts from files and scripts in each managed host’s /etc/ansible/facts.d directory.

  • Name of each file or script must end in .fact.
  • Custom fact files must be written in json or INI format.

Custom facts are stored by the setup module in the ansible_facts.ansible_local variable.

Using Magic Variables

hostvars              # Contains the variables for managed host
group_names           # Lists all groups the current managed host is in
groups                # Lists all groups and hosts in the inventory
inventory_hostname    # Contains the host name for the current managed host as configured in the inventory.
# Report on the contents of the hostvars variable for a particular host:
ansible localhost -m debug -a 'var=hostvars["localhost"]'

Task Control

Running Tasks in a Loop

---
# Simple loop
- name: <play-name>
  hosts: all

  tasks:
    - name: Put redundant tasks in a loop
      module:
        name: "{{ item }}"
        ...
      loop:
        - item_1
        - item_2

---
# Loop over a list of variables
- name: <play-name>
  hosts: all
  vars:
    loop_items:
      - item_1
      - item_2

  tasks:
    - name: Put redundant tasks in a loop
      module:
        name:  "{{ item }}"
        ...
      loop: "{{ loop_items }}"

---
# Loop over a list of hashes or dictionaries
- name: <play-name>
  hosts: all

  tasks:
    - name: Users exist and are in the correct groups
      user:
        name: "{{ item.name }}"
        state: present
        groups: "{{ item.group }}"
      loop:
        - name: jane
          groups: wheel
        - name: joe
          groups: root

# Loop over registered variables
---
tasks:
  - name: Looping Echo Task
    shell: "echo This is my item: {{ item }}"
    loop:
      - one
      - two
    register: echo_results
  - name: Show stdout from the previous task.
    debug:
      msg: "STDOUT from previous task: "
    loop: ""

Note: Before Ansible 2.5, most playbooks used a different syntax for loops. Multiple loop keywords were provided with_items, with_files, with_sequence.

Running Tasks Conditionally

# Run task only when conditional_var is true:
---
- name: <play-name>
  hosts: all
  vars:
    conditional_var: true

  tasks:
    - name: Task name
      module:
        name: Module-name value
      when: conditional_var

# Run task only when conditional_var has a value
---
- name: <play-name>
  hosts: all
  vars:
    conditional_var: <variable-value>

  tasks:
    - name: "{{ conditional_var }} has a value"
      module:
        name: "{{ conditional_var }}"
      when: conditional_var is defined
conditional_var                         # Boolean variable is true (1, True, yes)
not conditional_var                     # Boolean variable is true (0, False, no)
conditional_var in conditional_list     # The value is present in the list

Note: The indentation of the when statement. Because the when statement is not a module variable, it must be placed outside the module by being indented at the top level of the task.

Testing Multiple Conditions

# Testing multiple conditions with or
when: conditional_var_1 == "<var_value_1>" or conditional_var_2 == "<var_value_2>"
# Testing multiple conditions with and
when: conditional_var_1 == "<var_value_1>" and conditional_var_2 == "<var_value_2>"
# Testing multiple conditions with lists (= and)
when:
  - conditional_var_1 == "<var_value_1>"
  - conditional_var_2 == "<var_value_2>"
# Testing multiple conditions
when: >
  ( ansible_distribution == "RedHat" and
    ansible_distribution_major_version == "7" )
  or
  ( ansible_distribution == "Fedora" and
    ansible_distribution_major_version == "28" )

Combining Tasks

---
# Run task for each item in a list but only when both properties of the item are true:
- name: <play-name>
  hosts: all
  vars:
    loop_items:
      - item_1
      - item_2

  tasks:
    - name: <task-name>
      module:
        name: <module-name-value>
        ...
      loop: "{{ loop_items }}"
      when: item.<proprty> == "<var_value_1>" and item.<property> > "<var_value_2>"

Note: When you use when with loop for a task, the when statement is checked for each item.

Implementing Handlers

Handlers can be considered as inactive tasks that only get triggered by other tasks when explicitly invoked using a notify statement.

---
# Run task which notifies a handler:
- name: <play-name>
  hosts: all

  tasks:
    - name: <task-name>
      module:
        name: <module-name-value>
        ...
      notify:
        - <handler-name>
        - <handler-name>

  handlers:
    - name: <handler-name>
      module:
        name: <module-name-value>
        ...
    - name: <handler-name>
      module:
        name: <module-name-value>
        ...

Note: Handlers run in the order specified by the handlers section of the play, not in order listed by the notify statement.

Handeling Task Failures

If a task fails, the play is aborted. This can be overridden by ignoring failed tasks: ignore_errors: true.

---
# Continue playbook execution even if a task fails:
- name: <play-name>
  hosts: all

  tasks:
    - name: <task-name>
      module:
        name: <module-name-value>
        ...
      ignore_errors: true

If a task fails, any notified handlers will not run. This can be overridden by forcing handlers to run: force_handlers: true.

---
# Force handlers to run if a task fails:
- name: <play-name>
  hosts: all
  force_handlers: true

  tasks:
    - name: <task-name>
      module:
        name: <module-name-value>
        ...
      notify: <handler-name>

   handlers:
    - name: <handler-name>
      module:
        name: <module-name-value>
        ...

Note: Handlers are notified when a task reports a changed result but not when it reports an ok/failed result.

You can use the failed_when to specify which conditions indicate that the task has failed.

---
# Set conditions for when a task should fail:
- name: <play-name>
  hosts: all

  tasks:
    - name: <task-name>
      module:
        name: <module-name-value>
        ...
      register: command_output
      failed_when: "'something went wrong' in command_output.stdout"
---
# Set conditions for when a task should fail using the fail module:
- name: <play-name>
  hosts: all

  tasks:
    - name: <task-name>
      module:
        name: <module-name-value>
        ...
      register: command_output
      ignore_errors: true

    - name: Report task failure
      fail:
        msg: "<custom-fail-message>"
      when: "'something went wrong' in command_output.stdout"

Note: the fail module allows a delayed failure.

you can use changed_when to control when a task reports that it has changed.

---
# Prevent a task from reporting it has changed:
- name: <play-name>
  hosts: all

  tasks:
    - name: <task-name>
      module:
        name: <module-name-value>
        ...
      changed_when: false

Note: Using the shell module is discouraged due to its lack of idempotency, with changed_when this change can be suppressed based on the output of the script.

Blocks and Error Handling

blocks are clauses that logically group tasks, and control how tasks are executed (conditionals can be applied to the whole block).

---
# Example of a block statement
- name: <play-name>
  hosts: all

  tasks:
    - name: <block-name>
      block:
        - name: <task-name>
          module:
            name: <module-name-value>
            ...
        - name: <task-name>
          module:
            name: <module-name-value>
            ...
      when: conditional_var == "some_value"

Blocks allow for error handling with rescue and always statements (See Try, Except, Finally in Python3).

---
# Example of error handling:
- name: <play-name>
  hosts: all

  tasks:
    - name: <block-name>
      block:
        - name: <task-name>
          module:
            name: <module-name-value>
            ...
      rescue:
        - name: <task-name>
          module:
            name: <module-name-value>
            ...
      always:
        - name: <task-name>
          module:
            name: <module-name-value>
            ...

File Management on Hosts

File Modules

The Files modules library includes modules allowing you to accomplish most tasks related to Linux file management, such as creating, copying, editing, and modifying permissions and other attributes of files.

blockinfile     # Insert, update, or remove a block of multiline text surrounded by customizable marker lines
copy            # Copy file from local or remote machine to a location on a managed host
fetch           # Fetch file from remote machines to control node and storing them in a file tree
file            # Set/creates/removes attributes of regular files, symlinks, hard links, and directories
lineinfile      # Insert line is in a file, or replace existing line using a back-reference regular expression
state           # Retrieve status information for a file
synchronize     # A wrapper around the rsync command to make common tasks quick and easy

Automation with Files

Ensure a file exists on managed hosts:

ansible all -m file -a "path=<file_path> owner=<user-name>, group=<group-name>, mode=<unix-permissions>, state=touch"
---
- name: <play-name>
  hosts: all

  tasks:
    - name: Touch a file and set permissions
      file:
        path: /path/to/file
        owner: <user-name>
        group: <group-name>
        mode: <unix-permissions>
        state: touch

Modify file attributes on managed hosts:

---
# Set SELinux file context:
- name: <play-name>
  hosts: all

  tasks:
    - name: Set the correct SELinux file type
      file:
        path: /path/to/file
        setype: <SELinux-type>

---
# Making SELinux file context changes persistent
- name: <play-name>
  hosts: all

  tasks:
    - name: Set the correct SELinux file type persistently
      sefcontext:
        target: /path/to/file
        setype: <SELinux-type>
        state: present

Note: File attribute parameters are available in multiple file management modules, see ansible-doc file.

Copy file to managed hosts:

ansible all -m copy -a "src=<file-path> dest=<remote-path>"
---
- name: <play-name>
  hosts: all
  become: true

  tasks:
    - name: Copy a file to managed hosts
      copy:
        src: <file-name>
        dest: /path/to/file

Remove a file on managed hosts:

ansible all -m file -a "dest=<remote-path>, state=absent"
---
# Remove a file from managed hosts
- name: <play-name>
  hosts: all

  tasks:
    - name: Copy a file to managed hosts
      file:
        dest: /path/to/file
        state: absent

Modify file content on managed hosts:

---
# Add a specific single line of text to an existing file:
- name: <play-name>
  hosts: all

  tasks:
    - name: Add single line of text to an existing file
      lineinfile:
        path: /path/to/file
        line: "<line-to-add>"
        state: present

---
# Add a block of text to an existing file:
- name: <play-name>
  hosts: all

  tasks:
    - name: Add a block of text to an existing file
      blockinfile:
        path: /path/to/file
        block: |
          # BEGIN ANSIBLE MANAGED BLOCK
          <first-line-to-add>
          <Second-line-to-add>
          # END ANSIBLE MANAGED BLOCK
        state: present

Other automation examples with files:

---
# Retrieving the status of a file on managed hosts:
- name: <play-name>
  hosts: all

  tasks:
    - name: Retrieving the status of a file
      stat:
        path: /path/to/file
      register: result

    - name: Display the stat results
      debug:
        var: results

---
# Verify the checksum of a file on managed hosts:
- name: <play-name>
  hosts: all

  tasks:
    - name: Verify the checksum of a file
      stat:
        path: /path/to/file
        checksum_algorithm: <algorithm>
      register: result

    - name: Display the checksum
      debug:
        msg: "The checksum of the file is {{ result.stat.checksum }}"

---
# Sync files between control node and managed hosts:
- name: <play-name>
  hosts: all

  tasks:
    - name: Sync local file to remote files
      synchronize:
        src: <file-name>
        dest: /pat/to/file

---
# Retrieve SSH key from reference host
- name: <play-name>
  hosts: all

  tasks:
    - name: Retrieve SSH key from reference host
      fetch:
        src: "/home/{{ user }}/.ssh/id_rsa.pub"
        dest: "files/keys/{{ user }}.pub"

Jinja2 Templates

Variables and logic expressions are placed between tags, or delimiters.

{{ VARIABLE }}
{# COMMENT #}
{% EXPRESSION %}

Deploy a Jinja2 template to managed hosts:

---
- name: <play-name>
  hosts: all

  tasks:
    - name: <task-name>
      template:
        src: <source-template-location>
        dest: <destination-template-location>

Note: for more details on the template module, see ansible-doc template

Best practice: It is recommended to include a comment at the top of the template to indicate that the file should not be manually edited. To do this, use "Ansible managed" set in the ansible_managed directive (ansible.cfg).

# {{ ansible_managed }}

...

Jinja2 Control Structures

Using loops:

# For loop:
{% for item in loop_items %}
    {{ item }}
{% endfor %}

# For loop exmaple:
{# Run through all values of users, replacing myuser with each value, except when value is root #}
{% for myuser in users if not myuser == "root" %}
    User number {{ loop.index }} - {{ myuser }}
{% endfor %}

Using conditionals:

# If condition:
{% if <condition> %}
    {{ item }}
{% endfor %}

Jinja2 Filters and Tests

# Filters:
{{ <output> | to_json }}         # Formats the expression output using JSON
{{ <output> | to_yaml }}         # Formats the expression output using YAML

{{ <output> | to_nice_json }}    # Formats the expression output using human readable JSON
{{ <output> | to_nice_yaml }}    # Formats the expression output using human readable YAML

{{ <output> | from_json }}       # Parse the expression output using JSON
{{ <output> | from_yaml }}       # Parse the expression output using YAML

The expressions used with when clauses in Ansible Playbooks are Jinja2 expressions.

# Built-in Ansible tests used to test return values:
failed
changed
succeeded
skipped

Managing Large Projects

Selecting Hosts with Host Patterns

Host patterns are used to specify the hosts to target by a play or ad hoc command.

---
# Execute a play on a specific managed host:
- name: <play-name>
  hosts: <host-name>
  ...

---
# Execute a play on a specific managed host group:
- name: <play-name>
  hosts: <host-group-name>
  ...

Special groups to match managed hosts in an inventory:

---
# Execute a play on all managed hosts:
- name: <play-name>
  hosts: all
  ...

---
# Execute a play on all managed hosts which are not part of a host group:
- name: <play-name>
  hosts: ungrouped
  ...
---
# Execute a play on managed hosts with wildcards:
- name: <play-name>
  hosts: '*'
  ...

Best practice: It is recommended to enclose host patterns used on the command line in single quotes to protect them from unwanted shell expansion.

Lists:

---
# Execute a play on managed hosts with wildcards:
- name: <play-name>
  hosts: server1, server2, server3, group1, group2, 192.168.2.2
  ...