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
NTPservers. - 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):
- Variables defined directly inside a playbook (vars: section)
- vars_prompt inside the playbook
- Variables loaded with vars_files: in a playbook
- host_vars/.yml
- group_vars/.yml
- Facts collected from the host (gather_facts)
- 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 🙂