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"modewritten as a quoted string is unambiguous:0644, not644, which would be interpreted as decimal and produce a mode you did not intend.validateruns the command before the file is moved into place, so a bad config never lands. nginx, sshd and visudo all support a validation flag.lineinfileis idempotent for one line; a set of lines belongs inblockinfileor, better, a template you own entirely.- A
state: touchon a file changes the mtime on every run unless you addmodification_time: preserveandaccess_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| Task | Module | Note |
|---|---|---|
| Simple install list | package | Works across families, no version pinning |
| Pinned version | apt or dnf | Version syntax differs per family |
| Add a repo | yum_repository, deb822_repository | Prefer a module over templating the file |
| Upgrade everything | package with state: latest | Rarely what you want on a running fleet |
| Remove | state: absent | Add 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.Related
Users, SSH keys and secure defaults Conditionals, loops and handlers in depth
Last refreshed 2026-09-18.