Modules are the verbs of Ansible. Use the everyday ones — file, copy, package, service, lineinfile — and understand why a real module beats the raw command module.
Why: every task calls a module — a small, focused unit that knows how to reach a desired state idempotently. ansible.builtin.* are the core modules shipped with Ansible. The fully-qualified name (collection.module) is the modern style; the short name (file, copy) still works. You describe the end state; the module figures out whether to act.
- name: Common file operations
hosts: local
tasks:
- name: Ensure a directory exists
ansible.builtin.file:
path: /tmp/app
state: directory
mode: "0755"
- name: Copy a file into place
ansible.builtin.copy:
src: ./app.conf
dest: /tmp/app/app.confWhy: the two most common server tasks — install software and run it. The package module installs from the OS package manager (works across distros); service (or systemd) ensures a service is started and enabled on boot. state: present/started express the end state, not the action. Note: these need a Linux host and root — run them on localhost in WSL or a Linux machine with become (next topic).
- name: Install and run nginx
hosts: local
become: true # run with sudo (needed to install/manage services)
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Start it and enable on boot
ansible.builtin.service:
name: nginx
state: started
enabled: trueWhy: most system changes need root. become: true makes Ansible run the task with sudo. Set it on a whole play or a single task. This keeps you logging in as a normal user while still managing the system — the same pattern as running sudo by hand, but declarative.
- name: A task that needs root
hosts: local
tasks:
- name: Write to a system path
ansible.builtin.copy:
dest: /etc/motd
content: "Managed by Ansible\n"
become: true # just this task runs as rootWhy: you often need to ensure a single line exists in a config file without replacing the whole file. lineinfile adds or changes one line idempotently, matching by regex. blockinfile manages a marked block. Both are how you tweak existing configuration safely.
- name: Ensure a config line is present
hosts: local
become: true
tasks:
- name: Disable SSH root login
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'Why: command and shell run raw commands, but they are NOT idempotent — Ansible cannot know what they do, so they report "changed" every run unless you guide it. Prefer a real module every time. When you must use them, add creates/removes (skip if a file already exists) or a when condition so they stay idempotent. shell allows pipes and redirection; command does not.
- name: Use command only when no module fits
hosts: local
tasks:
- name: Generate a file once (skip if it already exists)
ansible.builtin.command: "touch /tmp/initialized"
args:
creates: /tmp/initialized # makes it idempotent