8i | 9i | 10g | 11g | 12c | 13c | 18c | 19c | 21c | 23c | Misc | PL/SQL | SQL | RAC | WebLogic | Linux

Home » Articles » Misc » Here

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.

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.

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:

Hope this helps. Regards Tim...

Back to the Top.