Creating an Ansible Playbook for Keeping NTP Client Settings

Reading Time: 6 minutes

This article shows how to create an Ansible playbook to maintain a consistent chrony configuration across multiple servers.

Are you new to Ansible? Don’t worry about that!
We’ve written an article explaining what Ansible is and what you need to do to use Ansible for the first time. Click here to read the article 🙂

Accurate timekeeping is one of the most critical configurations required for reliable communication between network-based computers. Computers can utilize the Network Time Protocol (NTP) to synchronize their time to an upstream time server or a pool of servers to maintain accurate time.

First and foremost: What is chrony?

Chrony is an implementation of the Network Time Protocol (NTP). Many Linux distributions have it installed by default. We can use Chrony:

  • to synchronize the system clock with NTP servers.
  • to synchronize the system clock with a reference clock, for example, a GPS receiver.
  • to synchronize the system clock with a manual time input.
  • as an NTPv4(RFC 5905) server or peer providing a time service to other computers on the network.

Chrony consists of chronyd, a daemon that runs in user space, and chronyc, a command-line program which can be used to monitor the performance of chronyd and to change various operating parameters when it is running. The default configuration file for chronyd is /etc/chrony.conf.

Creating the Inventory

Before going forward, our environment is composed of:

  • One Control VM (A CentOS 9 VM that runs Ansible).
  • Six Managed VMs (Red Hat 8 VMs that Ansible manages).

Let’s create a directory to store the inventory file:

mkdir -p /root/ansible/inventory

Create the hosts.ini file:

touch /root/ansible/inventory/hosts.ini

And then, add the hosts to hosts.ini. In this case, for instance, our inventory file has some groups

[hpc2_login_nodes]    # This is a group
hpc2-login

[hpc2_head_nodes]     # This is a group
hpc2-head

[hpc2_compute_nodes]   # This is a group
hpc2-node[01:06]

[all:vars]             # This is a variable section
ansible_user=root      # This is a variable applies to all hosts

Creating the Template

Let’s create a directory to store the template file:

mkdir -p /root/ansible/templates

Create the template for chrony configuration (the template content must be written using the Jinja2 format):

touch /root/ansible/templates/chrony.conf.j2

And then, add the following content to the chrony.conf.j2:

#+++++++++++++++++++++
# Managed by Ansible #
#+++++++++++++++++++++
{#
Template for chrony.conf
Variables:
  chrony_pool      - NTP pool or server
  chrony_driftfile - path to driftfile
  chrony_makestep  - makestep value
  chrony_keyfile   - path to keyfile
  chrony_leapsectz - leap second timezone
  chrony_logdir    - log directory
#}
pool {{ chrony_pool }} iburst
driftfile {{ chrony_driftfile }}
makestep {{ chrony_makestep }}
rtcsync
keyfile {{ chrony_keyfile }}
leapsectz {{ chrony_leapsectz }}
logdir {{ chrony_logdir }}

Note: The template contains many variables (in red). For example, “chrony_pool” is a variable. All variables will be defined in a YAML file and will be used by the Playbook to generate the final chrony.conf file!

Creating the Variables File

Ansible can get variables from many places. If the same variable exists in multiple places, it follows a priority order (precedence) to decide which one wins. Basically (there is a complete list in the Ansible documentation):

From highest priority → lowest (it means that if you have declared a variable named test inside the playbook and the same variable in the main.yml file, the variable inside the playbook “wins” because it will be preferable):

  1. Variables defined directly inside a playbook (vars: section)
  2. vars_prompt inside the playbook
  3. Variables loaded with vars_files: in a playbook
  4. host_vars/.yml
  5. group_vars/.yml
  6. Facts collected from the host (gather_facts)
  7. Role defaults (roles/*/defaults/main.yml) (lowest priority)

Let’s create a directory to store the variables file. In this case, we’re using the “group_vars” dir inside the inventory dir that was previously created:

mkdir -p /root/ansible/inventory/group_vars

Create a var file all.yml (the file extension can be .yaml or .yml):

touch /root/ansible/inventory/group_vars/all.yml

And then, add the following content – as we can see, there are all variables defined on the template chrony.conf.j2 and their values 😉

Note: Adjust the variable values according to your environment. Being honest, the only variable that you must change is the “chrony_pool” 😉

chrony_pool: "192.168.255.3"
chrony_driftfile: "/var/lib/chrony/drift"
chrony_makestep: "1.0 3"
chrony_keyfile: "/etc/chrony.keys"
chrony_leapsectz: "right/UTC"
chrony_logdir: "/var/log/chrony"

Creating the Playbook

Let’s create a directory to store the playbook files:

mkdir -p /root/ansible/playbooks

Inside the playbooks directory, generate the playbook file chrony.yml:

touch /root/ansible/playbooks/chrony.yml

And then, add the following content:

---
- name: Deploy chrony configuration and validate (auto-fix)
  hosts: all
  become: yes

  tasks:
    - name: Deploy chrony.conf from template
      template:
        src: ../templates/chrony.conf.j2     # Here is the template file
        dest: /etc/chrony.conf
        owner: root
        group: root
        mode: '0644'
      notify: Restart chronyd

    - name: Pause to allow chronyd restart
      pause:
        seconds: 3

    - name: Check if chrony is synchronized
      shell: chronyc sources | grep '\^\*'
      register: chrony_sync
      failed_when: false      # don't fail here
      changed_when: false

    - name: Restart chronyd if not synchronized
      service:
        name: chronyd
        state: restarted
      when: chrony_sync.rc != 0

    - name: Wait and re-check synchronization after fix
      shell: chronyc sources | grep '\^\*'
      register: chrony_sync_after
      when: chrony_sync.rc != 0
      failed_when: false
      changed_when: false

    - name: Final validation — fail if still not synced
      fail:
        msg: "Chrony is still NOT synchronized after auto-fix"
      when: (chrony_sync_after is defined) and ((chrony_sync_after.rc | default(0)) != 0)

    - name: Success message
      debug:
        msg: "Chrony synchronized successfully"
      when: (chrony_sync_after is defined) and ((chrony_sync_after.rc | default(0)) != 0)

  handlers:
    - name: Restart chronyd
      service:
        name: chronyd
        state: restarted

When we run the playbook, the chrony.conf file will be dynamically created and sent to all nodes!

Running the Playbook

To run the playbook:

ansilble-playbook -i inventory/hosts.ini playbooks/chrony.yml

Output’s example:

PLAY [Deploy chrony configuration and validate (auto-fix)] ******************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [hpc2-login]
ok: [hpc2-node01]
ok: [hpc2-node03]
ok: [hpc2-head]
ok: [hpc2-node02]
ok: [hpc2-node04]
ok: [hpc2-node05]
ok: [hpc2-node06]

TASK [Deploy chrony.conf from template] *************************************************************************************************************************************
ok: [hpc2-node01]
ok: [hpc2-node02]
ok: [hpc2-login]
ok: [hpc2-head]
ok: [hpc2-node03]
ok: [hpc2-node04]
ok: [hpc2-node06]
ok: [hpc2-node05]

TASK [Pause to allow chronyd restart] ***************************************************************************************************************************************
Pausing for 3 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [hpc2-login]

TASK [Check if chrony is synchronized] **************************************************************************************************************************************
ok: [hpc2-login]
ok: [hpc2-node02]
ok: [hpc2-head]
ok: [hpc2-node03]
ok: [hpc2-node01]
ok: [hpc2-node04]
ok: [hpc2-node05]
ok: [hpc2-node06]

TASK [Restart chronyd if not synchronized] **********************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]
skipping: [hpc2-node01]
skipping: [hpc2-node02]
skipping: [hpc2-node03]
skipping: [hpc2-node04]
skipping: [hpc2-node05]
skipping: [hpc2-node06]

TASK [Wait and re-check synchronization after fix] **************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]
skipping: [hpc2-node01]
skipping: [hpc2-node02]
skipping: [hpc2-node03]
skipping: [hpc2-node04]
skipping: [hpc2-node05]
skipping: [hpc2-node06]

TASK [Final validation — fail if still not synced] **************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]
skipping: [hpc2-node01]
skipping: [hpc2-node02]
skipping: [hpc2-node03]
skipping: [hpc2-node04]
skipping: [hpc2-node05]
skipping: [hpc2-node06]

TASK [Success message] ******************************************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]
skipping: [hpc2-node01]
skipping: [hpc2-node02]
skipping: [hpc2-node03]
skipping: [hpc2-node04]
skipping: [hpc2-node05]
skipping: [hpc2-node06]

PLAY RECAP ******************************************************************************************************************************************************************
hpc2-head                  :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-login                 :ok=4 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node01                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node02                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node03                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node04                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node05                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node06                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0

Note: The playbook has many “plays”, each with tasks to achieve a specific purpose (or maintain a desired state)!

Let’s take a look at the “PLAY RECAP” result of the Playbook:

ok=3 changed=0

That means:

— It checked the state of each item.
— It found that the system already matches the desired state.
— It did not re-run or reinstall anything.
— No redundant action, no disruption — safe by design.

Now, let’s do a test:

  • For testing purposes, I’ll delete all chrony.conf configuration file for all managed hosts.
  • Additionally, I’ll stop the chronyd service too.
  • And afterward, I’ll run the Playbook again. Let’s analyze the results:
PLAY [Deploy chrony configuration and validate (auto-fix)] ******************************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************************************************************************
ok: [hpc2-node01]
ok: [hpc2-login]
ok: [hpc2-head]
ok: [hpc2-node03]
ok: [hpc2-node02]
ok: [hpc2-node06]
ok: [hpc2-node04]
ok: [hpc2-node05]

TASK [Deploy chrony.conf from template] *************************************************************************************************************************************
ok: [hpc2-login]
ok: [hpc2-head]
changed: [hpc2-node02]
changed: [hpc2-node03]
changed: [hpc2-node01]
changed: [hpc2-node04]
changed: [hpc2-node05]
changed: [hpc2-node06]

TASK [Pause to allow chronyd restart] ***************************************************************************************************************************************
Pausing for 3 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [hpc2-login]

TASK [Check if chrony is synchronized] **************************************************************************************************************************************
ok: [hpc2-head]
ok: [hpc2-node03]
ok: [hpc2-node02]
ok: [hpc2-node01]
ok: [hpc2-login]
ok: [hpc2-node04]
ok: [hpc2-node06]
ok: [hpc2-node05]

TASK [Restart chronyd if not synchronized] **********************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]
changed: [hpc2-node02]
changed: [hpc2-node04]
changed: [hpc2-node03]
changed: [hpc2-node01]
changed: [hpc2-node05]
changed: [hpc2-node06]

TASK [Wait and re-check synchronization after fix] **************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]
ok: [hpc2-node01]
ok: [hpc2-node04]
ok: [hpc2-node02]
ok: [hpc2-node03]
ok: [hpc2-node05]
ok: [hpc2-node06]

TASK [Final validation — fail if still not synced] **************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]
fatal: [hpc2-node01]: FAILED! => {"changed": false, "msg": "Chrony is still NOT synchronized after auto-fix"}
fatal: [hpc2-node02]: FAILED! => {"changed": false, "msg": "Chrony is still NOT synchronized after auto-fix"}
fatal: [hpc2-node03]: FAILED! => {"changed": false, "msg": "Chrony is still NOT synchronized after auto-fix"}
fatal: [hpc2-node04]: FAILED! => {"changed": false, "msg": "Chrony is still NOT synchronized after auto-fix"}
fatal: [hpc2-node05]: FAILED! => {"changed": false, "msg": "Chrony is still NOT synchronized after auto-fix"}
fatal: [hpc2-node06]: FAILED! => {"changed": false, "msg": "Chrony is still NOT synchronized after auto-fix"}

TASK [Success message] ******************************************************************************************************************************************************
skipping: [hpc2-login]
skipping: [hpc2-head]

PLAY RECAP ******************************************************************************************************************************************************************
hpc2-head                  :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-login                 :ok=4 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node01                :ok=5changed=2 unreachable=0 failed=1 skipped=0    rescued=0    ignored=0
hpc2-node02                :ok=5changed=2 unreachable=0 failed=1 skipped=0    rescued=0    ignored=0
hpc2-node03                :ok=5changed=2 unreachable=0 failed=1 skipped=0    rescued=0    ignored=0
hpc2-node04                :ok=5changed=2 unreachable=0 failed=1 skipped=0    rescued=0    ignored=0
hpc2-node05                :ok=5changed=2 unreachable=0 failed=1 skipped=0    rescued=0    ignored=0
hpc2-node06                : ok=5    changed=2    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

As we can see in the previous section, at the “PLAY RECAP” section, we can confirm that changes occurred and failed too. The failure was just to keep the NTP sync okay – it’s normal because after restarting the chronyd service, it takes some seconds to sync the date and time details from the NTP server.

If we run the Playbook after a few seconds, everything will be fine 🙂

PLAY RECAP ******************************************************************************************************************************************************************
hpc2-head                  :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-login                 :ok=4 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node01                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node02                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node03                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node04                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node05                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0
hpc2-node06                :ok=3 changed=0    unreachable=0    failed=0skipped=4 rescued=0    ignored=0

So, as we can see, Ansible works to keep the “desired state” (in simple words, to keep everything working fine)!

That’s it for now 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *