For your automation journey, you want to be as flexible as possible. For this reason, in Ansible we have a process known as decoupling. Here we are separating site-specific code from static code. Anything specific to a server or any type of managed device, such as an IP address, can be replaced with ansible variables. As a best practice, always aim to have playbooks to be flexible, and if you want to share with someone else, all you need to change is the variables. As you know, variables are defined in several places, and each place you define the variables, such as the inventory or play header, will have an order of precedence. So for larger playbooks, keep in mind where is the best place to hold your variables and not to keep your playbooks site-specific.
With Ansible, you can execute tasks and playbooks on multiple systems with a single command. With Ansible Tower, you can have very complex automation requirements with a push of a button. Every site will have variations, and Ansible uses variables to manage differences between systems. To represent the variations among those systems, you can create variables with standard YAML syntax, including lists and dictionaries.
Diagram: Ansible Variables.
Defining Ansible Variables
There are several places where you can define your variables. You can define these variables in your playbooks, inventory, reusable files or roles, or at the command line. During a playbook run, you can create variables by registering a task’s return value or value as a new variable. When defining variables in multiple places, those variables have variable precedence. After creating variables, you can use those variables in module arguments, such as conditional “when” clauses, templates, and loops. All of which are powerful constructs to have in your automation toolbox.
- Define variables:Vars: Section
If you are starting your automation journey, the simplest way to define variables is to put a vars section in your playbook with the names and values of variables. This allows you to define several configuration-related variables. So to define variables in plays, include a vars: section in the header of the play where the variables are needed. Variables defined in plays are only valid within that specific play and don’t have playbook scope. So if you need a variable in a different play, you need to define it again. It may be inconvenient to you and difficult to manage across large playbooks with multiple teams working on playbook development.
- Define variables: Set_fact Module
We also have the set_fact module. The set_fact is a module used anywhere in a play to set variables. Any variable set this way applies as a fact to the host in which it is set. The set_fact relates the variable to the host used in the play. Here you can dynamically set variables based on the result of any task in the playbook. So set_fact is dynamically defining variables. Keep in mind that setting variables this way will have a playbook scope.
- Define variables: Vars_files
You can also put variables into one or more files using the section called vars_files. This allows you to put variables in a file instead of directly in the playbook. What I like most about setting variables this way is that it allows you to separate variables that contain sensitive information. When you define variables in reusable variable files, the sensitive variables are separated from playbooks. This separation enables you to store your playbooks in, for example, source control software and even share the playbooks without the risk of exposing passwords or other sensitive and personal data. So, when you put variables in files, they are referenced in the playbook using vars_files. Use the var_files: to specify a list of files that include variables you want to include. This is convenient when you want to manage the variables independent of the place using them and is useful for security purposes.
- Define variables: Vars_Promt
So here, we can use vars_promt in the play header to prompt users for a variable value. This has a playbook scope. By default, the variable is flagged as private, so the user does not see anything while entering the variable. We can if we have set private to no here.
- Define variables: Defining variables at runtime
When you run your playbook, you can define variables by passing variables at the command line using the –extra-vars (or -e) argument.
- Define variables: Task Variables
Task variables are made from data discovered while executing tasks or in the fact-gathering phase of a play. These variables are host-specific and are added to the host’s host vars. Variables of this type can be discovered via gather_facts and fact modules, populated from task return data via the register task key, or defined directly by a task using the set_fact or add_host modules.
Diagram: Ansible Tutorial: Link to YouTube Video.
Facts / Variables
- Ansible Fact: System Variables
Ansible facts are a type of variable. You don’t define Ansible facts; they are discovered. Facts are system variables that contain information about the managed system. Each playbook starts with an implicit task to gather facts. This can also be disabled in the playhead. Gathering facts takes time, so if you are not going to use it, we can disable it. We can also gather facts manually by using the setup modules. All of the facts are stored in one big variable called ansible_facts. However, within the variable, we have the second-tier variables. All of the facts are categorized into different variables. These facts are variables, and you can use the facts in conditionals and when statements.
- Speeding up Fact Gathering
Fact collection can be slow as you may work against many hosts you can use and set up a fact cache. If fact-caching is enabled, Ansible will store facts in a cache for the first time; it connects to the host. This is added to your Ansible.cfg with the “fact_caching_timeout” value. So if your playbook does not reference any ansible facts, you can turn off fact-gathering for that play. Here we use the “gather_facts” clause in the play and the Ansible.cfg.
Ansible Variables are a key component of Automation, and they allow for dynamic play content and reusable plays across different sets of an inventory. Variable data, such as specific details on how to connect to a particular host in your inventory, can be included, along with an inventory, in a variety of ways. While Ansible can discover data about a system during the setup phase, however, not all of the data can be discovered. We have the option to define data with the inventory that will expand what Ansible has been to discover.
Diagram: Ansible Inventory: Link to YouTube Video.
Ansible Variables: The Inventory
- Host and group variables
You can store variable values related to a specific host or group in inventory. This allows you to add variables directly to the hosts and groups in your main inventory file. Adding more managed nodes to your Ansible inventory will likely want to store variables in separate host and group variable files.
- [host_var and group_var]
Ansible looks for host variable files in a directory called host_vars and group variable files in a directory called group_vars. Remember that Ansible expects these directories to be in either the directory that contains your playbooks or the directory adjacent to your inventory file. You can break things out even further if you want to go one step further. Ansible lets us define, for example, group_vars/production as a directory instead of a file.
Behavior Inventory Variables
Behavioral inventory parameters allow you to describe your machines with additional parameters in your inventory file. Such as, the ansible_connection parameter may be useful. By default, Ansible supports multiple means of transport, which is the mechanism Ansible uses to connect to the managed host. Here is a list of some common behavior inventory variables and the behaviors they intend to modify:
- ansible_host: This is the DNS name or the Docker container name that Ansible will initiate a connection to.
- ansible_port: This specifies the port number that Ansible will use to connect to the inventory host if it is not the default value of 22.
- ansible_user: This specifies the username Ansible will use to connect with the inventory host, regardless of the connection type.
Diagram: Ansible Automation Best Practices.
A Key Point: Ansible Inventory Scalability
You can use the inventory to put host and group variables if you don’t have too many hosts. However, as your environment gets larger, it will become more difficult to manage variables in the inventory. In this case, you need to find a more scalable approach to keep track of your host and group variables. Even though Ansible variables can hold Booleans, strings, lists, and dictionaries, in an inventory file, you can specify only Booleans and strings. Therefore we have a more scalable approach to keeping track of host and group variables: you can create a separate variable file for each host and group. You can create a separate variable file for each host and group. Ansible expects these variable files to be in YAML format. This allows you to break the inventory into multiple files.
Summary: Ways to Define Ansible Variables in Playbooks
There are different ways that variables can be defined.
- You can define variables in the play header using the vars: sections
- Also, the play header uses include vars_files:
- Using the set_fact modules in a place
- Also, in the command line with the -e key-value
- As inventory variables
- Using vars_promp to request values from the user while running the playbook
- Also, the Facts are discovered variables. They contain system properties.
Highlighting Ansible Conditionals
In a playbook, you may want to execute different tasks depending on the value of a fact, a variable, or the result of a previous task. You may want the value of some variables to depend on the value of other variables. You can do all of these things with conditionals. Ansible uses Jinja2 tests and filters in conditionals. Basic conditionals are used with the when clause. The simplest conditional statement applies to a single task. Create the Task, then add a when the statement that applies a test. Ansible evaluates the test for all hosts when running the Task or playbook. For example, if you are installing MySQL on multiple machines, some of which have SELinux enabled, you might have a task to configure SELinux to allow MySQL to run.
You would only want that Task to run on machines that have SELinux enabled: Sometimes, you want to execute or skip a task based on facts. With conditionals based on facts: You can install a certain package only when the operating system is a particular version. You can skip configuring a firewall on hosts with internal IP addresses. You can perform cleanup tasks only when a filesystem is getting full.
Ansible Conditionals and When Clause
You can also create conditionals based on variables defined in the playbooks or inventory. So we have playbook variables, registered variables, and facts that can all be used in conditions and ensure that tasks only run if specific conditions are true.
Handlers and When statement
There are several ways Ansible can be configured for conditional task execution. We have, for example, Handlers for conditional task execution. Are used when a task has changed something. Then we have a very powerful When statement. The When statement allows you to run tasks when specific conditions are true. You can also use the Register in combination with When statements.
Diagram: Automation Tutorial.
Using Handler for Conditional Task Execution
A handler is a task only executed when triggered by a task that has changed something. Handler is executed after all tasks in a play, so you need to organize the contents of your playbook accordingly. If any task fails after the Task called the handler, the handlers are not executed. We can use the “force-handler: to change this. Keep in mind that handlers are operational when something has changed. We have a force handler that allows you to force the handler to be started even if subsequent tasks are failing. Simply put, a handler is a special type of Task that is called only if something changes.
- Handlers in Pre and Post Tasks
Each task section in a playbook is handled separately; any handler notified in pre_tasks, tasks, or post_tasks is executed at the end of each section. As a result, it is possible to execute one handler several times in one play.
Blocks create logical groups of tasks. Blocks also offer ways to handle task errors, similar to exception handling in many programming languages. Blocks can also be used in error conditional handling. So you have to use a block to define the main Task to run and then rescue to define tasks that run if tasks defined in the block fail. You can use “Always to Define” tasks that will run regardless of the success or failure of the Block and Rescue.
- A Key Note: Ansible Loops
What happens, however, if you have a single task but need to run it against a list of data, for example, creating several user accounts, directories, or something more complex? As with any programming language, loops in Ansible provide an easier way of executing repetitive tasks using fewer lines of code in a playbook. Examples of commonly-used loops include changing ownership on several files and directories with the file module, creating multiple users with the user module, and repeating a polling step until a certain result is reached.
- A Final Point: Managing Failures with the Fail Module
Ansible looks at the exit status of a task to determine whether it has failed. When any task fails, Ansible aborts the rest of the playbook on that host and continues with the next host. We can change this with a few solutions. For example, we can use ignore_errors in a task to ignore failure. Force_handlers to force a handler that has been triggered to run even if another task fails. But remember, there needs to be a change. We can also use these Failed_When, which allows you to specify what to look for in command output to recognize a failure. You may have a playbook that is used to clean up resources, and you want the playbook to ignore every error and keep going on till the end, and then fail if there are errors. In this case, when using the fail module, the failing Task must have Ignore Errors set to yes.