Managing files, packages and services

copy, template and file, lineinfile and blockinfile, the package abstraction, systemd and service management, and handler-driven restarts.

Files and directories

- name: A directory tree with explicit ownership
  ansible.builtin.file:
    path: "{{ item.path }}"
    state: directory
    owner: "{{ item.owner | default('root') }}"
    group: "{{ item.group | default('root') }}"
    mode: "{{ item.mode }}"
  loop:
    - { path: /srv/app, mode: "0755" }
    - { path: /srv/app/shared, owner: app, group: app, mode: "2775" }   # setgid
    - { path: /var/log/app, owner: app, group: app, mode: "0750" }

- name: Copy a small static file, changing only when the content changes
  ansible.builtin.copy:
    src: motd
    dest: /etc/motd
    owner: root
    group: root
    mode: "0644"

- name: Generate a file from a template
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    mode: "0644"
    backup: true
    validate: nginx -t -c %s

- name: Set one setting without rewriting the whole file
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?PermitRootLogin'
    line: 'PermitRootLogin no'
    validate: /usr/sbin/sshd -t -f %s

- name: Add a managed block rather than a single line
  ansible.builtin.blockinfile:
    path: /etc/hosts
    marker: "# {mark} ANSIBLE MANAGED BLOCK"
    block: |
      10.0.0.10  app.internal
      10.0.0.11  db.internal"
  • mode written as a quoted string is unambiguous: 0644, not 644, which would be interpreted as decimal and produce a mode you did not intend.
  • validate runs the command before the file is moved into place, so a bad config never lands. nginx, sshd and visudo all support a validation flag.
  • lineinfile is idempotent for one line; a set of lines belongs in blockinfile or, better, a template you own entirely.
  • A state: touch on a file changes the mtime on every run unless you add modification_time: preserve and access_time: preserve.

Packages across distributions

# the generic module picks the right backend per host
- name: Install base packages
  ansible.builtin.package:
    name:
      - curl
      - ca-certificates
      - chrony
    state: present

# when you need a version or options the generic module cannot express
- name: Install the application package on Debian
  ansible.builtin.apt:
    name: app={{ app_version }}
    state: present
    update_cache: true
    cache_valid_time: 3600        # do not refresh if it was refreshed recently
  when: ansible_os_family == "Debian"

- name: Install from a repository on RHEL
  ansible.builtin.dnf:
    name: app-{{ app_version }}
    state: present
    enablerepo: epel

- name: Add a third-party repository the supported way
  ansible.builtin.deb822_repository:
    name: docker
    types: [deb]
    uris: https://download.docker.com/linux/ubuntu
    suites: ["{{ ansible_distribution_release }}"]
    components: [stable]
    signed_by: https://download.docker.com/linux/ubuntu/gpg
TaskModuleNote
Simple install listpackageWorks across families, no version pinning
Pinned versionapt or dnfVersion syntax differs per family
Add a repoyum_repository, deb822_repositoryPrefer a module over templating the file
Upgrade everythingpackage with state: latestRarely what you want on a running fleet
Removestate: absentAdd autoremove deliberately

Services and restarts

- name: Deploy the unit file first
  ansible.builtin.template:
    src: app.service.j2
    dest: /etc/systemd/system/app.service
    mode: "0644"
  notify:
    - Reload systemd
    - Restart app

- name: Ensure the service is enabled and running
  ansible.builtin.systemd:
    name: app
    enabled: true
    state: started
    daemon_reload: true          # safe and idempotent

handlers:
  - name: Reload systemd
    ansible.builtin.systemd:
      daemon_reload: true

  - name: Restart app
    ansible.builtin.systemd:
      name: app
      state: restarted
⚠️
A restart is not a health check. Add a task after the restart that polls the service endpoint until it responds, or a broken deploy reports success while the service is crash-looping. A handler can do this with until and retries.

FAQ

Why does my file task always report changed?
Usually the mode is written unquoted, the source content genuinely differs, or a task writes a timestamp. Compare the rendered file between two runs with --diff to see the exact difference.
Should I manage whole config files or single lines?
A file your role owns entirely should be a template. For a file owned by a package, lineinfile or a drop-in directory is less brittle than replacing it and losing upstream updates.

Users, SSH keys and secure defaults Conditionals, loops and handlers in depth

Last refreshed 2026-09-18.