8i | 9i | 10g | 11g | 12c | 13c | 18c | 19c | 21c | 23c | Misc | PL/SQL | SQL | RAC | WebLogic | Linux
Ansible : Playbooks - First Steps
This article presents some examples of basic Ansible Playbooks, to give you a feel for how Ansible Playbooks work. The actual tasks being performed are not as important as the steps we go through to develop the playbooks.
A playbook is an ordered list of tasks, with each task being actions for Ansible to perform. This allows us to document administrative tasks in YAML format, save them in version control, and play them against multiple servers. This enables repeatable configuration and is the beginning of Infrastructure as Code.
- Useful Resources
- Creating a NGINX Playbook (Packages, Services, Firewall Rules)
- Creating a Patching Playbook (Packages, Reboots)
- Using Tags
- Managing Files
- Lists and Loops
- Manage Users and Groups
- Host Variables
- Group Variables
- Handlers
- Templates
- Roles
- Vault
Related articles.
Useful Resources
There is a vagrant build for the virtual machines used in these examples here.
There is a GitHub repository of the scripts used in the examples here.
There are videos covering these topics here.
- Ansible Playbooks : Introduction
- Ansible Playbooks : Lists and Loops
- Ansible Playbooks : Host and Group Variables
- Ansible Playbooks : Handlers
- Ansible Playbooks : Files and Templates
- Ansible Playbooks : Tags
- Ansible Playbooks : Users and Groups
- Ansible Playbooks : Roles
- Ansible Playbooks : Vault
Creating a NGINX Playbook (Packages, Services, Firewall Rules)
We want to use Ansible to build a standard approach to configuring NGINX servers. Our first attempt at a playbook contains two tasks. We install the NGINX package, then enable and start the NGINX service. We create a file called "configure_nginx.yml" with the following contents.
--- - name: Configure NGINX servers hosts: appservers become: true tasks: - name: Install NGINX package dnf: name: nginx state: present update_cache: yes - name: Enable and start NGINX service service: name: nginx enabled: yes state: started
We've given the playbook a name of "Configure NGINX servers". It is to be applied to all hosts in the "appservers" group. We allow escalated privileges by setting "become" to true. We then have two named tasks. The first uses the "dnf" module to install the "nginx" package. The second uses the "service" module to enable and start the "nginx" service.
We run the playbook using the ansible-playbook
command. If we needed a password for escalated privileges, we would add the --ask-become-pass
flag to the command line, which would prompt us for the password.
$ ansible-playbook configure_nginx.yml PLAY [Configure NGINX servers] ******************************************************************************************************************************************************************* TASK [Gathering Facts] *************************************************************************************************************************************************************************** ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Install NGINX package] ********************************************************************************************************************************************************************* changed: [appserver1.localdomain] changed: [appserver2.localdomain] TASK [Enable and start NGINX service] ************************************************************************************************************************************************************ changed: [appserver1.localdomain] changed: [appserver2.localdomain] PLAY RECAP *************************************************************************************************************************************************************************************** appserver1.localdomain : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 appserver2.localdomain : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
From the output we can see the "Install NGINX package" task resulted in changes on the two application servers. So did the "Enable and start NGINX service" task.
Some time later we remember we need to make sure the firewall is enable and running on these servers, and we need to make sure SSH and HTTPS traffic can get through the firewall. To do this we add new tasks into the existing playbook. The "firewalld" module allows us to amend the firewall, and the "service" module allows us to enable and start the "firewalld" service.
--- - name: Configure NGINX servers hosts: appservers become: true tasks: - name: Install NGINX package dnf: name: nginx state: present update_cache: yes - name: Enable and start NGINX service service: name: nginx enabled: yes state: started - name: Allow SSH traffic through the firewall firewalld: service: ssh permanent: yes state: enabled - name: Allow HTTPS traffic through the firewall firewalld: service: https permanent: yes state: enabled - name: Enable the firewall service: name: firewalld enabled: yes state: started
With the new tasks in place, we can run the entire playbook again.
$ ansible-playbook configure_nginx.yml PLAY [Configure NGINX servers] ***************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Install NGINX package] ******************************************************************************************************************* ok: [appserver1.localdomain] ok: [appserver2.localdomain] TASK [Enable and start NGINX service] ********************************************************************************************************** ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Allow SSH traffic through the firewall] ************************************************************************************************** ok: [appserver1.localdomain] ok: [appserver2.localdomain] TASK [Allow HTTPS traffic through the firewall] ************************************************************************************************ changed: [appserver1.localdomain] changed: [appserver2.localdomain] TASK [Enable the firewall] ********************************************************************************************************************* changed: [appserver2.localdomain] changed: [appserver1.localdomain] PLAY RECAP ************************************************************************************************************************************* appserver1.localdomain : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 appserver2.localdomain : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
This time there are no changes for the "Install NGINX package" and "Enable and start NGINX service" tasks, as they were done previously.
The "Allow SSH traffic through the firewall" task didn't require any changes, as port 22 is open by default on the firewall, but the "Allow HTTPS traffic through the firewall" task did require changes to the firewall configuration. So did the "Enable the firewall" task, as the firewall was disabled and stopped on the VM originally.
The fact we can incrementally improve a playbook, and rerun it without affecting previously applied tasks is one of benefits of Ansible. Rather than applying one-off changes, we can build them into a playbook, which acts as a permanent and consistent record of how the servers were configured.
Let's assume our company is going to start using Ubuntu for some application servers. For Ubuntu we would use the "apt" module to install packages, so the NGINX installation will be different on Oracle Linux and Ubuntu. We could write separate playbooks for each distribution, or we could combine them together, using "when" to determine which tasks apply to which distribution using Ansible "facts". Facts are just information about the remote system, which we can reference in our playbooks. We'll mention more about facts later.
In the following example the "Install NGINX package (DNF)" task uses the "dnf" module, and is only run for hosts with a distribution called "OracleLinux", "Red Hat Enterprise Linux" or "CentOS". The "Install NGINX package (APT)" task uses the "apt" module, and is only run for hosts with a distribution called "Ubuntu" or "Debian".
--- - name: Configure NGINX servers hosts: appservers become: true tasks: - name: Install NGINX package (DNF) dnf: name: nginx state: present update_cache: yes when: ansible_distribution in ["OracleLinux", "Red Hat Enterprise Linux", "CentOS"] - name: Install NGINX package (APT) apt: name: nginx state: present update_cache: yes when: ansible_distribution in ["Ubuntu", "Debian"] - name: Enable and start NGINX service service: name: nginx enabled: yes state: started - name: Allow SSH traffic through the firewall firewalld: service: ssh permanent: yes state: enabled - name: Allow HTTPS traffic through the firewall firewalld: service: https permanent: yes state: enabled - name: Enable the firewall service: name: firewalld enabled: yes state: started
When we run the playbook, the "Install NGINX package (DNF)" task is run, but the "Install NGINX package (APT)" task is skipped, because we don't have any Ubuntu servers yet.
$ ansible-playbook configure_nginx.yml PLAY [Configure NGINX servers] ***************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Install NGINX package (DNF)] ************************************************************************************************************* ok: [appserver1.localdomain] ok: [appserver2.localdomain] TASK [Install NGINX package (APT)] ************************************************************************************************************* skipping: [appserver1.localdomain] skipping: [appserver2.localdomain] TASK [Enable and start NGINX service] ********************************************************************************************************** ok: [appserver1.localdomain] ok: [appserver2.localdomain] TASK [Allow SSH traffic through the firewall] ************************************************************************************************** ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Allow HTTPS traffic through the firewall] ************************************************************************************************ ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Enable the firewall] ********************************************************************************************************************* ok: [appserver1.localdomain] ok: [appserver2.localdomain] PLAY RECAP ************************************************************************************************************************************* appserver1.localdomain : ok=6 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 appserver2.localdomain : ok=6 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 $
The output tells us a task was skipped for each server.
You can find more information, or facts, about the remote servers using the following Ansible commands. That these facts can be referred to in the playbook, like in your "when" criteria.
$ ansible database1.localdomain -m setup $ ansible database1.localdomain -m gather_facts
Creating a Patching Playbook (Packages, Reboots)
We want a playbook that will update all the packages on our database servers. We create a file called "update_database_packages.yml" with the following contents. We use the "dnf" module with a wildcard to update all installed packages to the latest version.
--- - name: Patch servers hosts: databases become: true tasks: - name: Update all packages dnf: name: "*" update_cache: yes state: latest
We run the playbook and it works OK. There are no changes, as the packages are already up to date.
$ ansible-playbook update_database_packages.yml PLAY [Patch servers] *************************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Update all packages] ********************************************************************************************************************* ok: [database1.localdomain] PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
To make sure we get all changes applied, including kernel changes, it might be sensible to reboot. We can do that by adding the a task based on the "reboot" module.
--- - name: Patch servers hosts: databases become: true tasks: - name: Update all packages dnf: name: "*" update_cache: yes state: latest - name: Reboot server reboot:
We run the playbook and this time we see a change associated with the "Reboot server" task.
$ ansible-playbook update_database_packages.yml PLAY [Patch servers] *************************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Update all packages] ********************************************************************************************************************* ok: [database1.localdomain] TASK [Reboot server] *************************************************************************************************************************** changed: [database1.localdomain] PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
Once again, we want to make the playbook work for multiple distributions, so we add a new task using the "apt" module, and limit the update tasks based on the underlying distribution.
--- - name: Patch servers hosts: databases become: true tasks: - name: Update all packages (DNF) dnf: name: "*" update_cache: yes state: latest when: ansible_distribution in ["OracleLinux", "Red Hat Enterprise Linux", "CentOS"] - name: Update all packages (APT) apt: name: "*" update_cache: yes state: latest when: ansible_distribution in ["Ubuntu", "Debian"] - name: Reboot server reboot:
As expected, the "Update all packages (DNF)" task runs, but the "Update all packages (APT)" task is skipped.
$ ansible-playbook update_database_packages.yml PLAY [Patch servers] *************************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Update all packages (DNF)] *************************************************************************************************************** ok: [database1.localdomain] TASK [Update all packages (APT)] *************************************************************************************************************** skipping: [database1.localdomain] TASK [Reboot server] *************************************************************************************************************************** changed: [database1.localdomain] PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 $
That works fine, but it has one annoying characteristic. Even if there are no package changes, we still get a reboot. We can fix that by registering the result of a task, and only performing an action if the task resulted in a change. In the following example we register the "dnf_update" and "apt_update" variables for their respective tasks, and limit the reboot task to only happen if one of these tasks has resulted in a change.
--- - name: Patch servers hosts: databases become: true tasks: - name: Update all packages (DNF) dnf: name: "*" update_cache: yes state: latest when: ansible_distribution in ["OracleLinux", "Red Hat Enterprise Linux", "CentOS"] register: dnf_update - name: Update all packages (APT) apt: name: "*" update_cache: yes state: latest when: ansible_distribution in ["Ubuntu", "Debian"] register: apt_update - name: Reboot server reboot: when: dnf_update.changed or apt_update.changed
When we run the playbook there are no package updates, so the reboot is skipped.
$ ansible-playbook update_database_packages.yml PLAY [Patch servers] *************************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Update all packages (DNF)] *************************************************************************************************************** ok: [database1.localdomain] TASK [Update all packages (APT)] *************************************************************************************************************** skipping: [database1.localdomain] TASK [Reboot server] *************************************************************************************************************************** skipping: [database1.localdomain] PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=2 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 $
Using Tags
While we are testing a playbook, we may want to limit which parts of it execute. We can control this with tags. We assign tags to the tasks, and then limit the play using those.
We create a file called "tags.yml" with the following contents. This is similar to the NGINX installation we used previously, but we have added tags to each task. These tags can be any text, but using the tag "all" means the task will always run.
--- - name: Using tags hosts: appservers become: true tasks: - name: Install NGINX package tags: nginx dnf: name: nginx state: present update_cache: yes - name: Enable and start NGINX service tags: nginx service: name: nginx enabled: yes state: started - name: Allow SSH traffic through the firewall tags: firewall,fwrule firewalld: service: ssh permanent: yes state: enabled - name: Allow HTTPS traffic through the firewall tags: firewall,fwrule firewalld: service: https permanent: yes state: enabled - name: Enable the firewall tags: firewall,fwservice service: name: firewalld enabled: yes state: started
We can list the tags using the --list-tags
flag.
$ ansible-playbook --list-tags tags.yml playbook: tags.yml play #1 (appservers): Using tags TAGS: [] TASK TAGS: [firewall, nginx, rule, service] $
The playbook will run as normal, but now we have the option of running only those tasks that match a specific tag, or group of tags.
$ ansible-playbook --tags "firewall" tags.yml $ ansible-playbook --tags "nginx" tags.yml $ ansible-playbook --tags "nginx,fwrule" tags.yml
Managing Files
Create a directory called "files" on the local server under your working directory, or under the role if the files are part of a role. This is where Ansible will look for files by default. Under that directory create a file called "defaut_page.html" will the following contents.
<html> <title>Default Page</title> <body> <p>This is the default page!</p> </body> </html>
We create a new playbook called "configure_nginx_2.yml" with the following contents. It's similar to one of the simpler NGINX playbooks, but we've added an extra task using the "copy" module to copy the default web page to the NGINX servers. The "src" is our source file, without the implied "files" directory. The "dest" is the location we want to copy it to on the servers. The ownership and permissions are also included.
--- - name: Configure NGINX servers hosts: appservers become: true tasks: - name: Install NGINX package dnf: name: nginx state: present update_cache: yes - name: Enable and start NGINX service service: name: nginx enabled: yes state: started - name: Allow SSH traffic through the firewall firewalld: service: ssh permanent: yes state: enabled - name: Allow HTTPS traffic through the firewall firewalld: service: https permanent: yes state: enabled - name: Enable the firewall service: name: firewalld enabled: yes state: started - name: Copy default web page copy: src: default_page.html dest: /usr/share/nginx/html/index.html owner: root group: root mode: 0644
When we run the playbook, we see the file is copied as required.
$ ansible-playbook configure_nginx_2.yml PLAY [Configure NGINX servers] ***************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [appserver1.localdomain] ok: [appserver2.localdomain] TASK [Install NGINX package] ******************************************************************************************************************* ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Enable and start NGINX service] ********************************************************************************************************** ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Allow SSH traffic through the firewall] ************************************************************************************************** ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Allow HTTPS traffic through the firewall] ************************************************************************************************ ok: [appserver1.localdomain] ok: [appserver2.localdomain] TASK [Enable the firewall] ********************************************************************************************************************* ok: [appserver1.localdomain] ok: [appserver2.localdomain] TASK [Copy default web page] ******************************************************************************************************************* changed: [appserver1.localdomain] changed: [appserver2.localdomain] PLAY RECAP ************************************************************************************************************************************* appserver1.localdomain : ok=7 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 appserver2.localdomain : ok=7 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
You may prefer to use templates if some customization of the files are necessary.
Lists and Loops
Sometimes we want to perform a task multiple times with different parameters. We could list each task separately, but that would make the playbook large and clumsy. Some modules allow us to specify parameters as lists. Even if they don't, we can use loops to simplify our playbooks.
In previous examples we've seen how to install packages using the "dnf" module. In this example we install multiple packages in a single task by specifying the package names using a list.
--- - name: Add basic packages hosts: all become: true tasks: - name: Install packages dnf: name: - zip - unzip - wget state: present update_cache: yes
In this example we set some firewall rules. The "port" parameter doesn't accept a list, but we can use "with_items" to supply a list of parameters.
--- - name: Configure firewall hosts: appservers become: true tasks: - name: Allow web through the firewall firewalld: port: "{{ item }}" permanent: yes state: enabled with_items: - 80/tcp - 443/tcp - 8080/tcp - 8443/tcp
We could make that more complicated by specifying the state for each port in the list.
--- - name: Configure firewall hosts: appservers become: true tasks: - name: Allow web through the firewall firewalld: port: "{{ item.port }}" permanent: yes state: "{{ item.state}}" with_items: - { port: 80/tcp, state: disabled } - { port: 443/tcp, state: enabled } - { port: 8080/tcp, state: disabled } - { port: 8443/tcp, state: disabled }
We could use a loop to do the same thing.
--- - name: Configure firewall hosts: appservers become: true tasks: - name: Allow web through the firewall firewalld: port: "{{ item.port }}" permanent: yes state: "{{ item.state}}" loop: - { port: 80/tcp, state: disabled } - { port: 443/tcp, state: enabled } - { port: 8080/tcp, state: disabled } - { port: 8443/tcp, state: disabled }
We may want to define the list as a variable at the top of the playbook, and refer to it later.
--- - name: Configure firewall hosts: appservers become: true vars: fwrules: - { port: 80/tcp, state: disabled } - { port: 443/tcp, state: enabled } - { port: 8080/tcp, state: disabled } - { port: 8443/tcp, state: disabled } tasks: - name: Allow web through the firewall firewalld: port: "{{ item.port }}" permanent: yes state: "{{ item.state}}" loop: "{{ fwrules }}"
This also works when using "with_items".
--- - name: Configure firewall hosts: appservers become: true vars: fwrules: - { port: 80/tcp, state: disabled } - { port: 443/tcp, state: enabled } - { port: 8080/tcp, state: disabled } - { port: 8443/tcp, state: disabled } tasks: - name: Allow web through the firewall firewalld: port: "{{ item.port }}" permanent: yes state: "{{ item.state}}" with_items: "{{ fwrules }}"
The is a lot more to know about loops, which you can read in the documentation here.
Manage Users and Groups
First we create a playboook called "groups_and_users.yml" with the following contents. We target all hosts in the "databases" group, and use the "group" module to create some common Linux groups.
--- - name: Create groups and users hosts: databases become: true tasks: - name: Create groups group: gid: "{{ item.group_id}}" name: "{{ item.group_name}}" state: present with_items: - { group_name: oinstall, group_id: 54321} - { group_name: dba, group_id: 54322} - { group_name: oper, group_id: 54323 }
We run the playbook and can see the groups were created.
$ ansible-playbook groups_and_users.yml PLAY [Create groups and users] ***************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Create groups] *************************************************************************************************************************** changed: [database1.localdomain] => (item={'group_name': 'oinstall', 'group_id': 54321}) changed: [database1.localdomain] => (item={'group_name': 'dba', 'group_id': 54322}) changed: [database1.localdomain] => (item={'group_name': 'oper', 'group_id': 54323}) PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
We edit the file and add a task using the "user" module to create a user. We specify the user id and name, but we also need a password. We must hash the password for it to be accepted. It's important to set the "update_password" flag to "user_created", or the password will be reset every time the playbook is run.
--- - name: Create groups and users hosts: databases become: true tasks: - name: Create groups group: gid: "{{ item.group_id}}" name: "{{ item.group_name}}" state: present with_items: - { group_name: oinstall, group_id: 54321} - { group_name: dba, group_id: 54322} - { group_name: oper, group_id: 54323 } - name: Create oracle user user: uid: 54321 name: oracle password: "{{ 'DummyPassword123' | password_hash('sha512', 'mysecretsalt') }}" groups: oinstall,dba,oper append: yes state: present update_password: on_create
When we run the playbook the user is created.
$ ansible-playbook groups_and_users.yml PLAY [Create groups and users] ***************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Create groups] *************************************************************************************************************************** ok: [database1.localdomain] => (item={'group_name': 'oinstall', 'group_id': 54321}) ok: [database1.localdomain] => (item={'group_name': 'dba', 'group_id': 54322}) ok: [database1.localdomain] => (item={'group_name': 'oper', 'group_id': 54323}) TASK [Create oracle user] ********************************************************************************************************************** changed: [database1.localdomain] PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
If we run the playbook again the user is unchanged.
$ ansible-playbook groups_and_users.yml PLAY [Create groups and users] ***************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Create groups] *************************************************************************************************************************** ok: [database1.localdomain] => (item={'group_name': 'oinstall', 'group_id': 54321}) ok: [database1.localdomain] => (item={'group_name': 'dba', 'group_id': 54322}) ok: [database1.localdomain] => (item={'group_name': 'oper', 'group_id': 54323}) TASK [Create oracle user] ********************************************************************************************************************** ok: [database1.localdomain] PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
Using a plain text password in a playbook is a security risk, so it is better to hash to password separately, and include the hashed password in the playbook, or as a variable. The simplest way to generate the password hash is to use Ansible to do it. The output from the "debug" module displays the hashed password.
$ ansible all -i localhost, -m debug -a "msg={{ 'DummyPassword123' | password_hash('sha512', 'mysecretsalt') }}" localhost | SUCCESS => { "msg": "$6$mysecretsalt$RP/rxvw0AG/pfo/SLr9LEkQuxGBsFlrfU01cWAcWMxOAA4leVi7j1Y2UzIIU1YyrqyWbpuiE/Ic7efvLJYzaE/" } $
We can now include the hashed password as a string, or as a variable.
--- - name: Create groups and users hosts: databases become: true tasks: - name: Create groups group: gid: "{{ item.group_id}}" name: "{{ item.group_name}}" state: present with_items: - { group_name: oinstall, group_id: 54321} - { group_name: dba, group_id: 54322} - { group_name: oper, group_id: 54323 } - name: Create oracle user user: uid: 54321 name: oracle password: "$6$mysecretsalt$RP/rxvw0AG/pfo/SLr9LEkQuxGBsFlrfU01cWAcWMxOAA4leVi7j1Y2UzIIU1YyrqyWbpuiE/Ic7efvLJYzaE/" groups: oinstall,dba,oper append: yes state: present update_password: on_create
Host Variables
Host variables are variables that have host-specific values. These allow us to write more generic playbooks and roles, to aid in code reusability. We create a directory called "host_vars" in our working directory.
mkdir -p host_vars
We create files in this directory to hold host variables. There is a separate file for each host, with the file name matching the host entry in the inventory, with a file extension of ".yml". For example, in our inventory we have a host called "database1.localdomain", so we create a file called "database1.localdomain.yml" with the following contents. These are variables with values specific to the "database1.localdomain" host.
hostname: database1.localdomain short_hostname: database1 ip_address: 192.168.56.103 packages: - zip - unzip - tar - cpio - tmux - oracle-database-preinstall-19c fwrules: - 22/tcp - 1521/tcp
In our working directory we create a playbook called "host_variables.yml" with the following content.
--- - name: Use host variables hosts: databases become: true tasks: - name: Add hosts entry lineinfile: state: present dest: /etc/hosts line: "{{ ip_address }} {{ hostname }} {{ short_hostname }}" - name: Install packages dnf: name: "{{ packages }}" update_cache: yes state: latest - name: Configure firewall firewalld: port: "{{ item }}" permanent: yes state: enabled with_items: "{{ fwrules }}"
The first task uses the "lineinfile" module to add an entry into the "/etc/hosts" file if it is not already present. The line is made up of the "ip_address", "hostname" and "short_hostname" variables from the "database1.localdomain.yml" file. The second task uses the "dnf" module to install the packages listed in the "packages" variable from the "database1.localdomain.yml" file. The third task uses the "firewalld" module to configure the firewall with the ports listed in the "fwrules" variable from the "database1.localdomain.yml" file.
The important point is the same playbook can install different packages and configure different firewall rules based on the variables defined for that host.
We run the playbook as normal. Most the tasks have already been completed previously, but we can see the "Add hosts entry" task resulted in a change.
$ ansible-playbook host_variables.yml PLAY [Use host variables] ********************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Add hosts entry] ************************************************************************************************************************* changed: [database1.localdomain] TASK [Install packages] ************************************************************************************************************************ ok: [database1.localdomain] TASK [Configure firewall] ********************************************************************************************************************** ok: [database1.localdomain] => (item=22/tcp) ok: [database1.localdomain] => (item=1521/tcp) PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
Ansible has lots of ways to supply variable values. You need to be aware of the Variable Precedence.
Group Variables
Group variables are similar to host variables, except they have group-specific values. These allow us to write more generic playbooks and roles, to aid in code reusability. We create a directory called "group_vars" in our working directory.
mkdir -p group_vars
We create files in this directory to hold group variables. There is a separate file for each group, with the file name matching the group entry in the inventory, with a file extension of ".yml". For example, in our inventory we have a group called "appservers", so we create a file called "appservers.yml" with the following contents. These are variables that apply to the whole "appservers" group.
ssh_users: "oracle tim"
In our working directory we create a playbook called "group_variables.yml" with the following content. We just message out the variable value using the "debug" module.
--- - name: Use group variables hosts: appservers tasks: - name: Show variable value debug: var: ssh_users
We run the playbook as normal, and see the same value appear for all hosts in the group.
$ ansible-playbook group_variables.yml PLAY [Use group variables] ********************************************************************************************************************* TASK [Gathering Facts] ************************************************************************************************************************* ok: [appserver2.localdomain] ok: [appserver1.localdomain] TASK [Show variable value] ********************************************************************************************************************* ok: [appserver1.localdomain] => { "ssh_users": "oracle tim" } ok: [appserver2.localdomain] => { "ssh_users": "oracle tim" } PLAY RECAP ************************************************************************************************************************************* appserver1.localdomain : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 appserver2.localdomain : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
Ansible has lots of ways to supply variable values. You need to be aware of the Variable Precedence.
Handlers
In a previous example we showed how to make one task dependent on the state of another. We register a variable for the task and check for the status of "changed" in the dependant task. In this example we add a line to the "nginx.conf" file if it is not already present. If we make a change we restart the NGINX service.
--- - name: Configure appservers hosts: appservers become: true tasks: - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# One silly a line added." register: nginx1 - name: Restart nginx service: name: nginx state: restarted when: nginx1.changed
That works fine for one item, but what if there are several tasks that need to trigger the same task later? We can't share the same variable, as we run the risk of overwriting the changed state in a later task. We could register a different variable for each task and alter the "when" in the final task like this.
--- - name: Configure appservers hosts: appservers become: true tasks: - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# One silly a line added." register: nginx1 - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# Another silly a line added." register: nginx2 - name: Restart nginx service: name: nginx state: restarted when: nginx1.changed or nginx2.changed
An alternative is to define a handler and notify that handler when it needs to run. It doesn't matter if it is notified one or many times during the lifespan of the playbook, it will only result in a single action. In this example we make two changes to the "nginx.conf" file, and each time notify the "Restart nginx" handler. The handler looks like a regualr task, but it is defined under "handlers".
--- - name: Configure appservers hosts: appservers become: true tasks: - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# One silly a line added." notify: Restart nginx - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# Another silly a line added." notify: Restart nginx handlers: - name: Restart nginx service: name: nginx state: restarted
A single task can notify multiple handlers. In this example the task notifies a list of handlers.
--- - name: Configure appservers hosts: appservers become: true tasks: - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# One silly a line added." notify: - Restart nginx - Restart firewall handlers: - name: Restart nginx service: name: nginx state: restarted - name: Restart firewall service: name: firewalld state: restarted
Alternatively, handlers can listen to generic topics, and tasks can raise those topics to notify multiple handlers.
--- - name: Configure appservers hosts: appservers become: true tasks: - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# One silly a line added." notify: "restart stuff" handlers: - name: Restart nginx service: name: nginx state: restarted listen: "restart stuff" - name: Restart firewall service: name: firewalld state: restarted listen: "restart stuff"
Handlers can be defined separately to the playbook. Create a directory called "handlers" under the working directory, or under the role if the handler is part of a role. Create a file in that directory called "main.yml" with the following contents.
- name: Restart nginx service: name: nginx state: restarted listen: "restart stuff" - name: Restart firewall service: name: firewalld state: restarted listen: "restart stuff"
Some sources suggest we can remove the handlers from the playbook and the they will be picked automatically from the handlers directory. In the version of Ansible we are using, the handlers file needs to be imported.
--- - name: Configure appservers hosts: appservers become: true tasks: - name: Amend nginx.conf lineinfile: state: present dest: /etc/nginx/nginx.conf line: "# One silly a line added." notify: "restart stuff" handlers: - import_tasks: handlers/main.yml
Templates
Templates are files we can customise using the Jinja2 templating language to provide host-specific variations of a common file. This is really useful for customising configuration files that require host-specific values.
Create a directory called "templates" in the working directory, or under the role if the template is part of a role.
mkdir templates
Create a file called "99-my-sshd-users.conf.j2" in the "templates" directory with the following contents. The ".j2" extension stands for Jinja2.
AllowUsers {{ ssh_users }}
Edit the "host_vars/database1.localdomain.yml" file, adding the new host variable "ssh_users" with a space separated list of users.
ssh_users: "oracle tim"
We create a playbook called "templates.yml" with the following contents. We process the template and copy it to create a new config file. If the state has changed, we run a handler to restart the "sshd" service.
--- - name: Configure databases hosts: databases become: true tasks: - name: Generate 99-my-sshd-users.conf file template: src: 99-my-sshd-users.conf.j2 dest: /etc/ssh/ssh_config.d/99-my-sshd-users.conf group: root owner: root mode: 0644 notify: Restart sshd handlers: - name: Restart sshd service: name: sshd state: restarted
We run the playbook and we can see the "Generate 99-my-sshd-users.conf file" task and "Restart sshd" handler ran.
$ ansible-playbook templates.yml PLAY [Configure databases] ********************************************************************************************************************* TASK [Gathering Facts] ************************************************************************************************************************* ok: [database1.localdomain] TASK [Generate 99-my-sshd-users.conf file] ***************************************************************************************************** changed: [database1.localdomain] RUNNING HANDLER [Restart sshd] ***************************************************************************************************************** changed: [database1.localdomain] PLAY RECAP ************************************************************************************************************************************* database1.localdomain : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 $
We can see the file is created on the host, and the host variable has been substituted into the file.
[root@database1 ssh_config.d]# cat 99-my-sshd-users.conf AllowUsers oracle tim [root@database1 ssh_config.d]#
Roles
Roles allow us to split Ansible playbooks into more manageable chunks, rather than having one monolithic playbook. They also aid in code reusability. You can read more about roles here.
Vault
Ansible Vault provides a simple way to encrypt secrets, so you don't expose sensitive data in your playbooks.
For more information see:
- Intro to playbooks
- ansible-playbook
- Index of all Modules
- Loops
- Ansible Playbooks : Introduction
- Ansible Playbooks : Lists and Loops
- Ansible Playbooks : Host and Group Variables
- Ansible Playbooks : Handlers
- Ansible Playbooks : Files and Templates
- Ansible Playbooks : Tags
- Ansible Playbooks : Users and Groups
- Ansible Playbooks : Roles
- Ansible Playbooks : Vault
- Ansible : First Steps
- Ansible : Roles - First Steps
- Ansible : Vault
- Ansible : All Articles
Hope this helps. Regards Tim...