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
, andraw
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.
cd
into the directory of your playbook.- 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 theansible_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
orINI
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 thenotify
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 anok
/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, withchanged_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, seeansible-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
...