How to Conditionally Copy Files and Execute Commands Only When Changes Occur in Ansible


11 views

When managing system configurations with Ansible, we often face situations where tasks execute unnecessarily during subsequent playbook runs. In this specific case with timezone configuration, two issues stand out:

1. The /etc/timezone file gets rewritten every time
2. dpkg-reconfigure runs regardless of whether the timezone actually changed

The original implementation uses simple copy and command modules:

- name: set timezone
  copy: 
    content: 'Europe/Berlin'
    dest: /etc/timezone
    owner: root
    group: root
    mode: 0644
    backup: yes

- name: update timezone
  command: dpkg-reconfigure --frontend noninteractive tzdata

We can improve this using Ansible's built-in idempotency features:

- name: set timezone
  copy:
    content: 'Europe/Berlin'
    dest: /etc/timezone
    owner: root
    group: root
    mode: 0644
    backup: yes
  register: timezone_file

- name: update timezone
  command: dpkg-reconfigure --frontend noninteractive tzdata
  when: timezone_file.changed

For more complex scenarios, consider these patterns:

# Using template module with checksum comparison
- name: configure timezone
  template:
    src: timezone.j2
    dest: /etc/timezone
    owner: root
    group: root
    mode: 0644
  register: tz_config
  notify: reconfigure tzdata

# Handler approach
handlers:
  - name: reconfigure tzdata
    command: dpkg-reconfigure --frontend noninteractive tzdata

Key principles to remember:

  • Always register task outputs when you need to make decisions
  • Use handlers for follow-up actions that should only run after changes
  • Consider using the stat module to check file contents before copying
  • For complex conditions, combine multiple registered variables

Here's a complete solution with additional robustness checks:

- name: check current timezone
  stat:
    path: /etc/timezone
  register: tz_stat

- name: set timezone if different
  copy:
    content: 'Europe/Berlin'
    dest: /etc/timezone
    owner: root
    group: root
    mode: 0644
  when: 
    - not tz_stat.stat.exists
    - or
    - tz_stat.stat.size == 0
    - '"Europe/Berlin" not in lookup("file", "/etc/timezone")'
  register: tz_updated

- name: reconfigure if timezone was updated
  command: dpkg-reconfigure --frontend noninteractive tzdata
  when: tz_updated.changed

When managing system configurations with Ansible, we often encounter scenarios where tasks execute unnecessarily, reporting changes even when the system is already in the desired state. The timezone configuration is a perfect example of this behavior.

- name: set timezone
  copy: content='Europe/Berlin'
        dest=/etc/timezone
        owner=root
        group=root
        mode=0644
        backup=yes

- name: update timezone
  command: dpkg-reconfigure --frontend noninteractive tzdata

The current implementation will always show as changed because:

  • The copy module doesn't check if the content is identical to the existing file
  • The dpkg-reconfigure command runs unconditionally after the copy

Here's how we can optimize this configuration:

- name: check current timezone
  stat:
    path: /etc/timezone
  register: timezone_file

- name: set timezone only if needed
  copy:
    content: 'Europe/Berlin'
    dest: /etc/timezone
    owner: root
    group: root
    mode: 0644
    backup: yes
  when: (timezone_file.stat.exists == False) or 
        (timezone_file.content|b64decode != 'Europe/Berlin')

- name: update timezone if changed
  command: dpkg-reconfigure --frontend noninteractive tzdata
  when: timezone_changed is defined and timezone_changed
  register: tz_update

For more complex scenarios, consider using handlers:

- name: set timezone with handler
  copy:
    content: 'Europe/Berlin'
    dest: /etc/timezone
    owner: root
    group: root
    mode: 0644
    backup: yes
  notify: reconfigure timezone

handlers:
  - name: reconfigure timezone
    command: dpkg-reconfigure --frontend noninteractive tzdata

This optimized approach provides several benefits:

  • Reduces unnecessary file writes
  • Minimizes system calls to dpkg-reconfigure
  • Provides more accurate change reporting
  • Improves overall playbook execution time

Always verify your changes with:

ansible-playbook playbook.yml --check --diff

This will show you exactly what would change without actually modifying the system.