ONLY FOR SELF STUDY, NO COMMERCIAL USAGE!!!
Chapter 3. Managing Variables and Facts
Managing Variables
Variables provide a convenient way to manage dynamic values for a given environment in your Ansible project. Examples of values that variables might contain include:
- Users to create
- Packages to install
- Services to restart
- Files to remove
- Archives to retrieve from the internet
Naming Variables
Variable names must start with a letter, and they can only contain letters, numbers, and underscores.
The following table illustrates the difference between invalid and valid variable names.
Table 3.1. Examples of Invalid and Valid Ansible Variable Names
Invalid variable names | Valid variable names |
---|---|
web server | web_server |
remote.file | remote_file |
1st file | file_1``file1 |
remoteserver$1 | remote_server_1 or remote_server1 |
Defining Variables
The following simplified list shows ways to define a variable, ordered from the lowest precedence to the highest:
-
Group variables defined in the inventory (lowest)
-
Group variables defined in files in a
group_vars
subdirectory in the same directory as the inventory or the playbook -
Host variables defined in the inventory
-
Host variables defined in files in a
host_vars
subdirectory in the same directory as the inventory or the playbook -
Host facts, discovered at runtime
-
Play variables in the playbook (
vars
andvars_files
)-
One common method is to place a variable in a
vars
block at the beginning of a play:- hosts: all vars: user: joe home: /home/joe or - hosts: all vars_files: - vars/users.yml
-
Using Variables in Playbooks
Variables are referenced by placing the variable name in double braces (
{{ }}
). Ansible substitutes the variable with its value when the task is executed.(quotes are mandatory if the variable is the first element to start)vars: user: joe n tasks: # This line will read: Creates the user joe - name: Creates the user {{ user }} user: # This line will create the user named Joe name: "{{ user }}"
-
-
Task variables
-
Extra variables defined on the command line (highest)
-
by using the
--extra-vars
or-e
option and specifying those variables -
[user@demo ~]$ ansible-navigator run main.yml -e "package=apache"
-
For the Directories to Populate Host and Group Variables
The following examples illustrate this approach in more detail. Consider a scenario where you need to manage two data centers, and the data center hosts are defined in the ~/project/inventory
inventory file:
[datacenter1]
demo1.example.com
demo2.example.com
[datacenter2]
demo3.example.com
demo4.example.com
[datacenters:children]
datacenter1
datacenter2
-
If you need to define a general value for all servers in both data centers, set a group variable for the
datacenters
host group:[admin@station project]$ cat ~/project/group_vars/datacenters package: httpd
-
If you need to define difference values for each data center, set a group variable for each data center host group:
[admin@station project]$ cat ~/project/group_vars/datacenter1 package: httpd [admin@station project]$ cat ~/project/group_vars/datacenter2 package: apache
-
If you need to define different values for each managed host in every data center, then define the variables in separate host variable files:
[admin@station project]$ cat ~/project/host_vars/demo1.example.com package: httpd [admin@station project]$ cat ~/project/host_vars/demo2.example.com package: apache [admin@station project]$ cat ~/project/host_vars/demo3.example.com package: mariadb-server [admin@station project]$ cat ~/project/host_vars/demo4.example.com package: mysql-server
The directory structure for the example project, project
, if it contained all the example files above, would appear as follows:
project
├── ansible.cfg
├── group_vars
│ ├── datacenters
│ ├── datacenters1
│ └── datacenters2
├── host_vars
│ ├── demo1.example.com
│ ├── demo2.example.com
│ ├── demo3.example.com
│ └── demo4.example.com
├── inventory
└── playbook.yml
Using Dictionaries as Variables
For example, consider the following snippet:
user1_first_name: Bob
user1_last_name: Jones
user1_home_dir: /users/bjones
user2_first_name: Anne
user2_last_name: Cook
user2_home_dir: /users/acook
This could be rewritten as a dictionary called users
:
users:
bjones:
first_name: Bob
last_name: Jones
home_dir: /users/bjones
acook:
first_name: Anne
last_name: Cook
home_dir: /users/acook
You can then use the following variables to access user data:
# Returns 'Bob'
users.bjones.first_name
# Returns '/users/acook'
users.acook.home_dir
Because the variable is defined as a Python dictionary, an alternative syntax is available.
# Returns 'Bob'
users['bjones']['first_name']
# Returns '/users/acook'
users['acook']['home_dir']
Capturing Command Output with Registered Variables
You can use the register
statement to capture the output of a command or other information about the execution of a module.
The following play demonstrates how to capture the output of a command for debugging purposes:
---
- name: Installs a package and prints the result
hosts: all
tasks:
- name: Install the package
ansible.builtin.dnf:
name: httpd
state: installed
register: install_result
- debug:
var: install_result
When you run the play, the debug
module dumps the value of the install_result
registered variable to the terminal.
[user@demo ~]$ ansible-navigator run playbook.yml -m stdout
PLAY [Installs a package and prints the result] ****************************
TASK [setup] ***************************************************************
ok: [demo.example.com]
TASK [Install the package] *************************************************
ok: [demo.example.com]
TASK [debug] ***************************************************************
ok: [demo.example.com] => {
"install_result": {
"changed": false,
"msg": "",
"rc": 0,
"results": [
"httpd-2.4.51-7.el9_0.x86_64 providing httpd is already installed"
]
}
}
PLAY RECAP *****************************************************************
demo.example.com : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
REFERENCES
How to build your inventory — Ansible Documentation
Using Variables — Ansible Documentation
Variable precedence: Where should I put a variable?
YAML Syntax — Ansible Documentation
Data-variable example
---
- name: Deploy and start Apache HTTPD service
hosts: webserver
vars:
web_pkg: httpd
firewall_pkg: firewalld
web_service: httpd
firewall_service: firewalld
rule: http
tasks:
- name: Required packages are installed and up to date
ansible.builtin.dnf:
name:
- "{{ web_pkg }}"
- "{{ firewall_pkg }}"
state: latest
- name: The {{ firewall_service }} service is started and enabled
ansible.builtin.service:
name: "{{ firewall_service }}"
enabled: true
state: started
- name: The {{ web_service }} service is started and enabled
ansible.builtin.service:
name: "{{ web_service }}"
enabled: true
state: started
- name: Web content is in place
ansible.builtin.copy:
content: "Example web content"
dest: /var/www/html/index.html
- name: The firewall port for {{ rule }} is open
ansible.posix.firewalld:
service: "{{ rule }}"
permanent: true
immediate: true
state: enabled
- name: Verify the Apache service
hosts: workstation
become: false
tasks:
- name: Ensure the webserver is reachable
ansible.builtin.uri:
url: http://servera.lab.example.com
status_code: 200
Managing Secret
- use the command-line tool named
ansible-vault
to create, edit, encrypt, decrypt, and view files. - Ansible Vault does not implement its own cryptographic functions but rather uses an external Python toolkit. Files are protected with symmetric encryption using AES256 with a password as the secret key. Note that the way this is done has not been formally audited by a third party.
Creating an Encrypted File
Use ansible-vault create *
filename*
—This command prompts for the new Vault password and then opens a file using the default editor, vi
. You can export the EDITOR
environment variable to specify a different default editor. To set the default editor to nano
, run the export EDITOR=nano
.
[student@demo ~]$ ansible-vault create secret.yml
New Vault password: redhat
Confirm New Vault password: redhat
Also can use a Vault password file to store the Vault password with param --vault-password-file=FILE_NAME
.
[student@demo ~]$ ansible-vault create --vault-password-file=vault-pass secret.yml
Viewing an Encrypted File
Use the ansible-vault view *
filename*
command to view an Ansible Vault-encrypted file without opening it for editing.
[student@demo ~]$ ansible-vault view secret1.yml
Vault password: secret
my_secret: "yJJvPqhsiusmmPPZdnjndkdnYNDjdj782meUZcw"
Editing an Existing Encrypted File
Use ansible-vault edit *
filename*
command. This command decrypts the file to a temporary file and allows you to edit it. When saved, it copies the content and removes the temporary file.
[student@demo ~]$ ansible-vault edit secret.yml
Vault password: redhat
Note
The edit
subcommand always rewrites the file, so you should only use it when making changes. This can have implications when the file is kept under version control. You should always use the view
subcommand to view the file’s contents without making changes.
Encrypting an Existing File
Use the ansible-vault encrypt *
filename*
command. This command can take the names of multiple files to be encrypted as arguments.
[student@demo ~]$ ansible-vault encrypt secret1.yml secret2.yml
New Vault password: redhat
Confirm New Vault password: redhat
Encryption successful
Use the --output=OUTPUT_FILE
option to save the encrypted file with a new name. You can only use one input file with the --output
option.
Decrypting an Existing File
Use the ansible-vault decrypt *
filename*
command. When decrypting a single file, you can use the --output
option to save the decrypted file under a different name.
[student@demo ~]$ ansible-vault decrypt secret1.yml --output=secret1-decrypted.yml
Vault password: redhat
Decryption successful
Changing the Password of an Encrypted File
Use the ansible-vault rekey *
filename*
command to change the password of an encrypted file. This command can rekey multiple data files at the same time. It prompts for the original password and then the new password.
[student@demo ~]$ ansible-vault rekey secret.yml
Vault password: redhat
New Vault password: RedHat
Confirm New Vault password: RedHat
Rekey successful
When using a Vault password file, use the --new-vault-password-file
option:
[student@demo ~]$ ansible-vault rekey \
> --new-vault-password-file=NEW_VAULT_PASSWORD_FILE secret.yml
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-navigator
command. If you do not provide the password, the playbook returns an error:
[student@demo ~]$ ansible-navigator run -m stdout test-secret.yml
ERROR! Attempting to decrypt but no vault secrets found
You can provide the Vault password using one of the following options:
- Prompt interactively
- Specify the Vault password file
- Use the
ANSIBLE_VAULT_PASSWORD_FILE
environment variable
To provide the Vault password interactively, use --playbook-artifact-enable false
(or --pae false
) and --vault-id @prompt
as illustrated in the following example:
[student@demo ~]$ ansible-navigator run -m stdout --pae false site.yml \
> --vault-id @prompt
Vault password (default): redhat
Or disable the playbook artifact from the following minimal ansible-navigator.yml
file:
ansible-navigator:
playbook-artifact:
enable: false
Instead of providing the Vault encryption password interactively, you can specify a file that stores the encryption password in plain text by using the --vault-password-file
option.
The password must be a string stored as a single line in the file. Because that file contains the sensitive plain text password, it is vital that it be protected through file permissions and other security measures.
[student@demo ~]$ ansible-navigator run -m stdout site.yml \
> --vault-password-file=vault-pw-file
You can also use the ANSIBLE_VAULT_PASSWORD_FILE
environment variable to specify the default location of the password file.
Use multiple pass for ansible navigator
To use multiple passwords, pass multiple --vault-id
or --vault-password-file
options to the ansible-navigator
command.
[student@demo ~]$ ansible-navigator run -m stdout --pae false site.yml \
> --vault-id one@prompt --vault-id two@prompt
Vault password (one):
Vault password (two):
...output omitted...
The Vault IDs one
and two
preceding @prompt
can be anything, and you can even omit them entirely. If you use the --vault-id *
id*
option when you encrypt a file with the ansible-vault
command, then the password for the matching ID is the first password tried when running the ansible-navigator
command. If it does not match, then ansible-navigator
tries the other passwords that you provided. The Vault ID @prompt
with no ID is actually shorthand for default@prompt
, which means to prompt for the password for Vault ID default
.
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. This ensures that the correct password is selected first when decrypting the vault-encrypted file, which is faster than forcing Ansible to try each of the Vault passwords that you provided until it finds the right one.
Recommended Practices for Variable File Management
Remember that the preferred way to manage group variables and host variables is to create directories at the playbook level. The group_vars
directory normally contains variable files with names that match the host groups to which they apply. The host_vars
directory normally contains variable files with names that match the hostnames of managed hosts to which they apply.
You can use subdirectories within the group_vars
or host_vars
directories for each host group or managed host. Those directories can contain multiple variable files, and all of those files are used by the host group or managed host.
In the following example project directory for the playbook.yml
playbook, members of the webservers
host group use variables in the group_vars/webservers/vars
file. The demo.example.com
host uses the variables in both the host_vars/demo.example.com/vars
and host_vars/demo.example.com/vault
files.:
.
├── ansible.cfg
├── group_vars
│ └── webservers
│ └── vars
├── host_vars
│ └── demo.example.com
│ ├── vars
│ └── vault
├── inventory
└── playbook.yml
If you do create subdirectories for each host group or managed host, most variables for demo.example.com
can be placed in the vars
file, but sensitive variables can be kept secret by placing them in the vault
file. You can use ansible-vault
to encrypt the vault
file and leave the vars
file as plain text.
Playbook variables (as opposed to inventory variables) can also be protected with Ansible Vault. You can place sensitive playbook variables in a separate file that is encrypted with Ansible Vault, then include that encrypted variables file in a playbook by using a vars_files
directive. This can be useful, because playbook variables take precedence over inventory variables.
Example:
secret.yml was protected by ansible-vault(pass: redhat)
[student@workstation data-secret]$ cat secret.yml
$ANSIBLE_VAULT;1.1;AES256
36333765303261353032373636386233653935656662613965386230646163396231303364623464
3935303532343139353032333235643831323237633364660a313661303031316263376335306136
66663436313830303766653034313261383962613764356233343163386163353239363633393031
3539626334303834370a666535646135303337323937393438366434333432653639303832663435
36363362626365383534646336363363323135306536623264346461336132323936626538383561
34373266626163623934373736366362326233353630613961313137393235616631646562343961
62633239653635376132663464656265346135636564303535306139363933623831623039346364
37323835333631623130316465366139646135376665303264336165303732363333316363343532
35373230646261303932303837313065353365636364653733386139306166366263666630373830
65303537366636613862346663373762633233633461656230643138633339356666343037626234
323334353065303465663630663738653765
[student@workstation data-secret]$ ansible-vault view secret.yml
Vault password:
username: ansibleuser1
pwhash: abcdefgh2345690009988767622341123453
Here is a playbookcreate_users.yml
which should contain one play (Create user accounts for all our servers
in the following example), which uses the variables defined in the secret.yml
encrypted file.
Configure the play to use the devservers
host group. Run this play as the devops
user on the remote managed host. Configure the play to create the ansibleuser1
user defined by the username
variable. Set the user’s password using the password hash stored in the pwhash
variable.
---
- name: Create user accounts for all our servers
hosts: devservers
become: true
remote_user: devops
vars_files:
- secret.yml
tasks:
- name: Creating user from secret.yml
ansible.builtin.user:
name: "{{ username }}"
password: "{{ pwhash }}"
Resolve any syntax errors in your playbook before you continue.
[student@workstation data-secret]$ ansible-navigator run -m stdout \
> --pae false create_users.yml --syntax-check --vault-id @prompt
Vault password (default): redhat
playbook: /home/student/data-secret/create_users.yml
Create a password file named vault-pass
that contains the password (redhat) and Change the permissions of the file to 0600
.
[student@workstation data-secret]$ echo 'redhat' > vault-pass
[student@workstation data-secret]$ chmod 0600 vault-pass
Run the Ansible Playbook to create the ansibleuser1
user on a remote system, using the Vault password in the vault-pass
file to decrypt the hashed password for that user. That password is stored as a variable in the secret.yml
Ansible Vault encrypted file.
[student@workstation data-secret]$ ansible-navigator run \
> -m stdout create_users.yml --vault-password-file=vault-pass
PLAY [Create user accounts for all our servers] ********************************
TASK [Gathering Facts] *********************************************************
ok: [servera.lab.example.com]
TASK [Creating users from secret.yml] ******************************************
changed: [servera.lab.example.com]
PLAY RECAP *********************************************************************
servera.lab.example.com : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Verify that the playbook ran correctly. The ansibleuser1
user should exist and have the correct password on the servera.lab.example.com
machine.
To make sure that SSH only tries to authenticate by password and not by using an SSH key, use the -o PreferredAuthentications=password
option when you log in.
[student@workstation data-secret]$ ssh -o PreferredAuthentications=password \
> ansibleuser1@servera.lab.example.com
ansibleuser1@servera.lab.example.com's password: redhat
...output omitted...
[ansibleuser1@servera ~]$ exit
logout
Connection to servera.lab.example.com closed.
References
Encrypting content with Ansible Vault — Ansible Documentation
Keep vaulted variables safely visible — Ansible Documentation
Managing Facts
Describing Ansible Facts
Ansible facts are variables that are automatically discovered by Ansible on a managed host.
Facts contain host-specific information that can be used just like regular variables in plays, conditionals, loops, or any other statement that depends on a value collected from a managed host.
Some facts gathered for a managed host might include:
- The host name
- The kernel version
- Network interface names
- Network interface IP addresses
- Operating system version
- Number of CPUs
- Available or free memory
- Size and free space of storage devices
You can even create custom facts, which are stored on the managed host and are unique to that system.
Facts are a convenient way to retrieve the state of a managed host and to determine what action to take based on that state. For example:
- Your play might restart a server by using a conditional task based on the value of a fact that was gathered, such as the status of a particular service.
- The play might customize a MySQL configuration file depending on the available memory that is reported by a fact.
- The IPv4 address used in a configuration file might be set based on the value of a fact.
Normally, every play runs the ansible.builtin.setup
module automatically to gather facts, before it performs its first task.
This is reported as the Gathering Facts
task in Ansible 2.3 and later, or simply as setup
in earlier versions of Ansible. By default, you do not need to have a task to run ansible.builtin.setup
in your play. It is normally run automatically for you.
One way to see what facts are gathered for your managed hosts is to run a short playbook that gathers facts and uses the ansible.builtin.debug
module to print the value of the ansible_facts
variable.
- name: Fact dump
hosts: all
tasks:
- name: Print all facts
ansible.builtin.debug:
var: ansible_facts
When you run the playbook, the facts are displayed in the job output(JSON format):
[user@demo ~]$ ansible-navigator run -m stdout facts.yml
PLAY [Fact dump] ***************************************************************
TASK [Gathering Facts] *********************************************************
ok: [demo1.example.com]
TASK [Print all facts] *********************************************************
ok: [demo1.example.com] => {
"ansible_facts": {
"all_ipv4_addresses": [
"10.30.0.178",
"172.25.250.10"
],
"all_ipv6_addresses": [
"fe80::8389:96fd:e53e:979",
"fe80::cb51:6814:6342:7bbc"
],
"ansible_local": {}
}
},
"apparmor": {
"status": "disabled"
},
"architecture": "x86_64",
"bios_date": "04/01/2014",
"bios_vendor": "SeaBIOS",
"bios_version": "1.13.0-2.module+el8.2.1+7284+aa32a2c4",
"board_asset_tag": "NA",
"board_name": "NA",
"board_serial": "NA",
"board_vendor": "NA",
"board_version": "NA",
"chassis_asset_tag": "NA",
"chassis_serial": "NA",
"chassis_vendor": "Red Hat",
"chassis_version": "RHEL 7.6.0 PC (i440FX + PIIX, 1996)",
"cmdline": {
"BOOT_IMAGE": "(hd0,gpt3)/vmlinuz-5.14.0-70.13.1.el9_0.x86_64",
"console": "ttyS0,115200n8",
"crashkernel": "1G-4G:192M,4G-64G:256M,64G-:512M",
"net.ifnames": "0",
"no_timer_check": true,
"root": "UUID=fb535add-9799-4a27-b8bc-e8259f39a767"
},
...output omitted...
The following table shows some facts that might be gathered from a managed node and which might be useful in a playbook:
Table 3.3. Examples of Ansible Facts
Fact | Variable |
---|---|
Short hostname | ansible_facts['hostname'] |
Fully qualified domain name | ansible_facts['fqdn'] |
Main IPv4 address (based on routing) | ansible_facts['default_ipv4']['address'] |
List of the names of all network interfaces | ansible_facts['interfaces'] |
Size of the /dev/vda1 disk partition | ansible_facts['devices']['vda']['partitions']['vda1']['size'] |
List of DNS servers | ansible_facts['dns']['nameservers'] |
Version of the currently running kernel | ansible_facts['kernel'] |
Note:
Remember that when a variable’s value is a dictionary, one of two syntaxes can be used to retrieve the value. To take two examples from the preceding table:
ansible_facts['default_ipv4']['address']
can also be writtenansible_facts.default_ipv4.address
When a fact is used in a playbook, Ansible dynamically substitutes the variable name for the fact with the corresponding value:
---
- hosts: all
tasks:
- name: Prints various Ansible facts
ansible.builtin.debug:
msg: >
The default IPv4 address of {{ ansible_facts.fqdn }}
is {{ ansible_facts.default_ipv4.address }}
The following output shows how Ansible was able to query the managed node and dynamically use the system information to update the variable. You can also use facts to create dynamic groups of hosts that match particular criteria.
[user@demo ~]$ ansible-navigator run -m stdout playbook.yml
PLAY [all] ***********************************************************************
TASK [Gathering Facts] *****************************************************
ok: [demo1.example.com]
TASK [Prints various Ansible facts] ****************************************
ok: [demo1.example.com] => {
"msg": "The default IPv4 address of demo1.example.com is 172.25.250.10\n"
}
PLAY RECAP *****************************************************************
demo1.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Ansible Facts Injected as Variables
Before Ansible 2.5, facts were always injected as individual variables prefixed with the string ansible_
instead of being part of the ansible_facts
variable. For example, the ansible_facts['distribution']
fact was called ansible_distribution
.
Many playbooks still use facts injected as variables instead of the new syntax, which uses the ansible_facts.*
namespace.
One reason why the Ansible community discourages injecting facts as variables is because it risks unexpected collisions between facts and variables. A fact has a very high precedence that overrides playbook and inventory host and group variables, so this can lead to unexpected side effects.
The following table shows some examples of facts with both the ansible_*
and ansible_facts.*
names.
Table 3.4. Comparison of Selected Ansible Fact Names
ansible_facts.* name | ansible_* name |
---|---|
ansible_facts['hostname'] | ansible_hostname |
ansible_facts['fqdn'] | ansible_fqdn |
ansible_facts['default_ipv4']['address'] | ansible_default_ipv4['address'] |
ansible_facts['interfaces'] | ansible_interfaces |
ansible_facts['devices']['vda']['partitions']['vda1']['size'] | ansible_devices['vda']['partitions']['vda1']['size'] |
ansible_facts['dns']['nameservers'] | ansible_dns['nameservers'] |
ansible_facts['kernel'] | ansible_kernel |
Important NOTE:
Currently, Ansible recognizes both the new fact-naming system (using ansible_facts
) and the earlier, pre-2.5 “facts injected as separate variables” naming system.
You can disable the ansible_
naming system by setting the inject_facts_as_vars
parameter in the [defaults]
section of the Ansible configuration file to false
. The default setting is currently true
.
If it is set to false
, you can only reference Ansible facts using the new ansible_facts.*
naming system. In that case, attempts to reference facts through the ansible_*
namespace results in an error.
Turning off Fact Gathering
Sometimes, you do not want to gather facts for your play. This might be for several reasons:
- You might not be using any facts and want to speed up the play, or reduce load caused by the play on the managed hosts.
- The managed hosts perhaps cannot run the
ansible.builtin.setup
module for some reason, or you need to install some prerequisite software before gathering facts.
To disable fact gathering for a play, set the gather_facts
keyword to false
:
---
- name: This play does not automatically gather any facts
hosts: large_datacenter
gather_facts: false
Even if gather_facts: false
is set for a play, you can manually gather facts at any time by running a task that uses the ansible.builtin.setup
module:
tasks:
- name: Manually gather facts
ansible.builtin.setup:
Gathering a Subset of Facts
All facts are gathered by default. You can configure the ansible.builtin.setup
module to only gather a subset of facts, instead of all facts. For example, to only gather hardware facts, set gather_subset
to hardware
:
- name: Collect only hardware facts
ansible.builtin.setup:
gather_subset:
- hardware
If you want to gather all facts except a certain subset, add an exclamation point (!) in front of the subset name. Add quotes around the string because in YAML the exclamation point cannot be used at the start of an unquoted string.
- name: Collect all facts except for hardware facts
ansible.builtin.setup:
gather_subset:
- "!hardware"
Visit https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html#parameter-gather_subset to view possible values for the gather_subset
parameter.
Creating Custom Facts
You can use custom facts to define certain values for managed hosts. Plays can use custom facts to populate configuration files or conditionally run tasks.
Custom facts are stored locally on each managed host. These facts are integrated into the list of standard facts gathered by the ansible.builtin.setup
module when it runs on the managed host.
You can statically define custom facts in an INI or JSON file, or you can generate them dynamically when you run a play. Dynamic custom facts are gathered via executable scripts, which generate JSON output.
By default, the ansible.builtin.setup
module loads custom facts from files and scripts in the etc/ansible/facts.d
directory of each managed host. The name of each file or script must end in .fact
for it to be used. Dynamic custom fact scripts must output JSON-formatted facts and must be executable.
The following example static custom facts file is written in INI format. An INI-formatted custom facts file contains a top level defined by a section, followed by the key-value pairs of the facts to define:
[packages]
web_package = httpd
db_package = mariadb-server
[users]
user1 = joe
user2 = jane
You can provide the same facts in JSON format. The following JSON facts are equivalent to the facts specified by the INI format in the preceding example. The JSON data could be stored in a static text file or printed to standard output by an executable script:
{
"packages": {
"web_package": "httpd",
"db_package": "mariadb-server"
},
"users": {
"user1": "joe",
"user2": "jane"
}
}
Note:
Custom fact files cannot be in YAML format like a playbook. JSON format is the closest equivalent.
The ansible.builtin.setup
module stores custom facts in the ansible_facts['ansible_local']
variable. Facts are organized based on the name of the file that defined them. For example, assume that the /etc/ansible/facts.d/custom.fact
file on the managed host produces the preceding custom facts. In that case, the value of ansible_facts['ansible_local']['custom']['users']['user1']
is joe
.
You can inspect the structure of your custom facts by gathering facts and using the ansible.builtin.debug
module to display the contents of the ansible_local
variable with a play similar to the following example:
- name: Custom fact testing
hosts: demo1.example.com
gather_facts: true
tasks:
- name: Display all facts in ansible_local
ansible.builtin.debug:
var: ansible_local
When you run the play, you might see output similar to the following example:
...output omitted...
TASK [Display all facts in ansible_local] *********************************
ok: [demo1.example.com] => {
"ansible_local": {
"custom": {
"packages": {
"db_package": "mariadb-server",
"web_package": "httpd"
},
"users": {
"user1": "joe",
"user2": "jane"
}
}
}
}
...output omitted...
You can use custom facts the same way as default facts in playbooks:
[user@demo ~]$ cat playbook.yml
---
- hosts: all
tasks:
- name: Prints various Ansible facts
ansible.builtin.debug:
msg: >
The package to install on {{ ansible_facts['fqdn'] }}
is {{ ansible_facts['ansible_local']['custom']['packages']['web_package'] }}
[user@demo ~]$ ansible-navigator run -m stdout playbook.yml
PLAY [all] ***********************************************************************
TASK [Gathering Facts] *****************************************************
ok: [demo1.example.com]
TASK [Prints various Ansible facts] ****************************************
ok: [demo1.example.com] => {
"msg": "The package to install on demo1.example.com is httpd"
}
PLAY RECAP *****************************************************************
demo1.example.com : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Creating Variables from Other Variables
Sometimes you might want to create a new variable that uses the value of a different variable. One reason to create a new variable is to minimize typing.
The previous example created custom facts on managed hosts. In that example, you can use the ansible_facts['ansible_local']['custom']
variable to reference those custom facts. That variable has the packages
and users
keys. If your play contains several references to those custom facts, then you might benefit from creating a new variable.
You can use the ansible.builtin.set_fact
module to create a new variable associated to the current host. For example, you might define the custom_host
variable and use the ansible_facts['ansible_local']['custom']
variable as its value.
- name: Set custom_host
ansible.builtin.set_fact:
custom_host: "{{ ansible_facts['ansible_local']['custom'] }}"
By defining this new variable, your play can use the shorter custom_host['packages']
and custom_host['users']
variables rather than the longer ansible_facts['ansible_local']['custom']['packages']
and ansible_facts['ansible_local']['custom']['users']
variables.
You might also use the ansible.builtin.set_fact
module to minimize typing for regular system facts or for registered variables. For example:
- name: Set vda_parts
ansible.builtin.set_fact:
vda_parts: "{{ ansible_facts['devices']['vda']['partitions'] }}"
After adding this task, your play can use the vda_parts['vda1']['size']
variable rather than the longer ansible_facts['devices']['vda']['partitions']['vda1']['size']
variable.
Using Magic Variables
Ansible sets some special variables automatically.
These magic variables can also be useful to get information specific to a particular managed host.
Magic variable names are reserved, so you should not define variables with these names.
Four of the most useful magic variables are:
-
hostvars
Contains the variables for managed hosts, and can be used to get the values for another managed host’s variables. It does not include the managed host’s facts if they have not yet been gathered for that host.
-
group_names
Lists all groups that the current managed host is in.
-
groups
Lists all groups and hosts in the inventory.
-
inventory_hostname
Contains the hostname for the current managed host as configured in the inventory. This might be different from the hostname reported by facts for various reasons.
One way to get insight into their values is to use the ansible.builtin.debug
module to display the contents of these variables.
For example, the following task causes every host that runs the play to print out a list of all network interfaces on the demo2.example.com
host. This task works as long as facts were gathered for demo2
earlier in the play or by a preceding play in the playbook. It uses the hostvars
magic variable to access the ansible_facts['interfaces']
fact for that host.
- name: Print list of network interfaces for demo2
ansible.builtin.debug:
var: hostvars['demo2.example.com']['ansible_facts']['interfaces']
You can use the same approach with regular variables, not only facts. Keep in mind that the preceding task is run by every host in the play, so it would be more efficient to use a different module to apply information gathered from one host to the configuration of each of those other managed hosts.
Remember that you can use the ansible.builtin.setup
module in a task to refresh gathered facts at any time. However, fact gathering does cause your playbook to take longer to run.
Several other magic variables are also available. For more information, see https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html
.
References
Ansible facts - Ansible Documentation
ansible.builtin.setup module - Gathers facts about remote hosts - Ansible Documentation
Special Variables - Ansible Documentation
Chapter 3 TEST
Instructions
-
In the working directory, create the
playbook.yml
playbook. In the playbook, start creating a play to install and configure the web server hosts with an Apache HTTP Server that has basic authentication enabled. Configure thewebserver
host group to contain the managed hosts for the play.Define the following play variables:
Variable Values firewall_pkg
firewalld
firewall_svc
firewalld
web_pkg
httpd
web_svc
httpd
ssl_pkg
mod_ssl
httpdconf_src
files/httpd.conf
httpdconf_dest
/etc/httpd/conf/httpd.conf
htaccess_src
files/.htaccess
secrets_dir
/etc/httpd/secrets
secrets_src
files/htpasswd
secrets_dest
"{{ secrets_dir }}/htpasswd"
web_root
/var/www/html
-
Add a
tasks
section to the play. Write a task that ensures the latest version of the necessary packages are installed. These packages are defined by thefirewall_pkg
,web_pkg
, andssl_pkg
variables. -
Add a second task to the play that ensures that the file specified by the
httpdconf_src
variable has been copied (with theansible.builtin.copy
module) to the location specified by thehttpdconf_dest
variable on the managed host. The file must be owned by theroot
user and theroot
group. Set0644
as the file permissions. -
Add a third task that uses the
ansible.builtin.file
module to create the directory specified by thesecrets_dir
variable on the managed host. This directory holds the password files used for the basic authentication of web services. The directory must be owned by theapache
user and theapache
group. Set0500
as the directory permissions. -
Add a fourth task that uses the
ansible.builtin.copy
module to add anhtpasswd
file, used for basic authentication of web users. The source should be defined by thesecrets_src
variable. The destination should be defined by thesecrets_dest
variable. The file must be owned by theapache
user and group. Set0400
as the file permissions. -
Add a fifth task that uses the
ansible.builtin.copy
module to create a.htaccess
file in the document root directory of the web server. Copy the file specified by thehtaccess_src
variable to{{ web_root }}/.htaccess
. The file must be owned by theapache
user and theapache
group. Set0400
as the file permissions. -
Add a sixth task that uses the
ansible.builtin.copy
module to create the web content file,index.html
, in the directory specified by theweb_root
variable. The file should contain the message*
HOSTNAME* (*
IPADDRESS*) has been customized by Ansible.
, whereHOSTNAME
is the fully qualified host name of the managed host andIPADDRESS
is its IPv4 IP address. Use thecontent
option with theansible.builtin.copy
module to specify the content of the file, and Ansible facts to specify the host name and IP address. -
Add a seventh task that uses the
ansible.builtin.service
module to enable and start the firewall service on the managed host. -
Add an eighth task that uses the
ansible.posix.firewalld
module to enable access to thehttps
service that is needed for users to access web services on the managed host. This firewall change should be permanent and should take place immediately. -
Add a final task that uses the
ansible.builtin.service
module to enable and start the web service on the managed host for all configuration changes to take effect. The name of the web service is defined by theweb_svc
variable. -
Define a second play in the
playbook.yml
file that uses theworkstation
machine as the managed host to test authentication to the web server. It does not need privilege escalation. Define a variable namedweb_user
with the valueguest
. -
Add a directive to the play that adds additional variables from a variable file named
vars/secret.yml
. This file contains a variable namedweb_pass
that specifies the password for the web user. You create this file later in the lab.Define the start of the task list.
-
Add two tasks to the second play.
The first task uses the
ansible.builtin.uri
module to request content fromhttps://serverb.lab.example.com
using basic authentication. Use theweb_user
andweb_pass
variables to authenticate to the web server. The task should verify a return HTTP status code of200
. Register the task result in a variable namedauth_test
.Note that the certificate presented by
serverb
is not trusted, so you need to avoid certificate validation.The second task uses the
ansible.builtin.debug
module to print the content returned from the web server, which is contained in theauth_test
variable. -
Create a
vars/secret.yml
file, encrypted with Ansible Vault. Use the passwordredhat
to encrypt it. It should set theweb_pass
variable toredhat
, which is the web user’s password.-
Create a subdirectory named
vars
in the working directory.[student@workstation data-review]$ mkdir vars
-
Create the encrypted variable file,
vars/secret.yml
, using Ansible Vault. Set the password for the encrypted file toredhat
.[student@workstation data-review]$ ansible-vault create vars/secret.yml New Vault password: redhat Confirm New Vault password: redhat
-
Add the following variable definition to the file:
web_pass: redhat
-
Save and close the file.
-
-
Run the
playbook.yml
playbook. Verify that content is successfully returned from the web server, and that it matches what was configured in an earlier task.
playbook.yml
---
- name: Webserver with variable test
hosts: webserver
vars:
firewall_pkg: firewalld
firewall_svc: firewalld
web_pkg: httpd
web_svc: httpd
ssl_pkg: mod_ssl
httpdconf_src: files/httpd.conf
httpdconf_dest: /etc/httpd/conf/httpd.conf
htaccess_src: files/.htaccess
secrets_dir: /etc/httpd/secrets
secrets_src: files/htpasswd
secrets_dest: "{{ secrets_dir }}/htpasswd"
web_root: /var/www/html
tasks:
- name: Installing latest packages for PKG
ansible.builtin.dnf:
name:
- "{{ firewall_pkg }}"
- "{{ web_pkg }}"
- "{{ ssl_pkg }}"
state: latest
- name: Copying FILES
ansible.builtin.copy:
src: "{{ httpdconf_src }}"
dest: "{{ httpdconf_dest }}"
owner: root
group: root
mode: 0644
- name: Creating Secret folder
ansible.builtin.file:
path: "{{ secrets_dir }}"
state: directory
owner: apache
group: apache
mode: 0500
- name: Coping secret files
ansible.builtin.copy:
src: "{{ secrets_src }}"
dest: "{{ secrets_dest }}"
owner: apache
group: apache
mode: 0400
- name: Coping htaccess files
ansible.builtin.copy:
src: "{{ htaccess_src }}"
dest: "{{ web_root }}/.htaccess"
owner: apache
group: apache
mode: 0400
- name: Coping webcontent files
ansible.builtin.copy:
dest: "{{ web_root }}/index.html"
content: >
{{ ansible_facts['fqdn'] }}({{ansible_facts['default_ipv4']['address']}}) has been customized by Ansible.
- name: Starting firewall service
ansible.builtin.service:
name: "{{ firewall_svc }}"
state: started
enabled: true
- name: Enable httpd access through firewall
ansible.posix.firewalld:
service: https
permanent: true
immediate: true
state: enabled
- name: Starting web service
ansible.builtin.service:
name: "{{ web_svc }}"
state: started
enabled: true
- name: Testing webservice
hosts: workstation
become: false
vars:
web_user: guest
vars_files:
- vars/secret.yml
tasks:
- name: Accessing the web service
ansible.builtin.uri:
url: https://serverb.lab.example.com
force_basic_auth: true
user: "{{ web_user }}"
password: "{{ web_pass }}"
validate_certs: false
return_content: true
status_code: 200
register: auth_test
- name: debug info
ansible.builtin.debug:
var: auth_test['content']
result:
[student@workstation data-review]$ ansible-navigator run -m stdout playbook.yml --pae false --vault-id @prompt
Vault password (default):
PLAY [Webserver with variable test] ******************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [serverb.lab.example.com]
TASK [Installing latest packages for PKG] ************************************************************
ok: [serverb.lab.example.com]
TASK [Copying FILES] *********************************************************************************
ok: [serverb.lab.example.com]
TASK [Creating Secret folder] ************************************************************************
ok: [serverb.lab.example.com]
TASK [Coping secret files] ***************************************************************************
ok: [serverb.lab.example.com]
TASK [Coping htaccess files] *************************************************************************
ok: [serverb.lab.example.com]
TASK [Coping webcontent files] ***********************************************************************
ok: [serverb.lab.example.com]
TASK [Starting firewall service] *********************************************************************
ok: [serverb.lab.example.com]
TASK [Enable httpd access through firewall] **********************************************************
ok: [serverb.lab.example.com]
TASK [Starting web service] **************************************************************************
ok: [serverb.lab.example.com]
PLAY [Testing webservice] ****************************************************************************
TASK [Gathering Facts] *******************************************************************************
ok: [workstation]
TASK [Accessing the web service] *********************************************************************
ok: [workstation]
TASK [debug info] ************************************************************************************
ok: [workstation] => {
"auth_test['content']": "\"serverb.lab.example.com\" (172.25.250.11) has been customized by Ansible.\n \n"
}
PLAY RECAP *******************************************************************************************
serverb.lab.example.com : ok=10 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
workstation : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
TO BE CONTINUED…