Skip to content

Modules

First PublishedLast UpdatedByAtif Alam

Modules are the units of work in Ansible. Each task in a playbook calls a module, and Ansible ships with thousands of them. Modules are idempotent — they check the current state before making changes, so running them twice produces the same result.

- name: Install nginx
apt:
name: nginx
state: present
update_cache: true
- name: Install multiple packages
apt:
name:
- nginx
- curl
- git
- htop
state: present
- name: Remove a package
apt:
name: apache2
state: absent
- name: Upgrade all packages
apt:
upgrade: dist
update_cache: true
- name: Install nginx
yum:
name: nginx
state: present
- name: Install with dnf (Fedora/RHEL 8+)
dnf:
name: nginx
state: present

Automatically uses the right package manager for the OS:

- name: Install nginx (works on any distro)
package:
name: nginx
state: present
- name: Install Python packages
pip:
name:
- flask
- gunicorn
state: present
virtualenv: /opt/myapp/venv

Copy files from the control node to managed hosts:

- name: Copy nginx config
copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
backup: true # keep a backup of the original
- name: Write content directly
copy:
content: |
server {
listen 80;
server_name example.com;
}
dest: /etc/nginx/sites-available/default

Copy a Jinja2 template, rendering variables:

- name: Deploy app config
template:
src: templates/app.conf.j2
dest: /etc/app/config.yml
owner: deploy
mode: "0640"

See Variables and Templates for Jinja2 syntax.

Manage file/directory properties or create symlinks:

- name: Create a directory
file:
path: /opt/myapp
state: directory
owner: deploy
group: deploy
mode: "0755"
- name: Create a symlink
file:
src: /opt/myapp/current/public
dest: /var/www/html
state: link
- name: Delete a file
file:
path: /tmp/old-file.txt
state: absent

Ensure a specific line exists in a file (or is absent):

- name: Set timezone in config
lineinfile:
path: /etc/app/config.ini
regexp: '^TIMEZONE='
line: 'TIMEZONE=UTC'
- name: Remove a line
lineinfile:
path: /etc/crontab
regexp: '^.*old-job.*$'
state: absent

Insert or update a block of text in a file:

- name: Add SSH banner
blockinfile:
path: /etc/ssh/sshd_config
block: |
Banner /etc/ssh/banner.txt
MaxAuthTries 3
marker: "# {mark} ANSIBLE MANAGED BLOCK - SSH hardening"

Wraps rsync for syncing directories or large file sets to many hosts. Use it when you need to push an entire directory (e.g. app tree, static assets) rather than a single file. By default it runs on the control node and rsyncs to each host in turn; the source path is relative to the control node.

Key arguments: src (local path), dest (path on the managed host), delete (set to yes to mirror — remove files on the host that are not in source), rsync_opts for extra rsync flags. Use delegate_to: "{{ inventory_hostname }}" if you want the sync to run on each host (e.g. pull from a shared location) instead of from the control node.

- name: Sync app directory to hosts
synchronize:
src: app/
dest: /opt/myapp/
delete: yes
rsync_opts:
- "--exclude=.git"

Start, stop, restart, or enable services:

- name: Start and enable nginx
service:
name: nginx
state: started
enabled: true
- name: Restart nginx
service:
name: nginx
state: restarted
- name: Stop a service
service:
name: old-app
state: stopped
enabled: false

More control over systemd-specific features:

- name: Reload systemd daemon
systemd:
daemon_reload: true
- name: Enable and start a service
systemd:
name: myapp
state: started
enabled: true
- name: Create a group
group:
name: deploy
state: present
- name: Create a user
user:
name: deploy
group: deploy
groups: sudo,docker
append: true
shell: /bin/bash
create_home: true
- name: Add authorized SSH key
authorized_key:
user: deploy
key: "{{ lookup('file', 'files/deploy_key.pub') }}"
state: present
- name: Remove a user
user:
name: olduser
state: absent
remove: true # also remove home directory

Run a command (no shell features like pipes or redirects):

- name: Check app version
command: /opt/myapp/bin/myapp --version
register: app_version
changed_when: false

Run a command through the shell (supports pipes, redirects, env vars):

- name: Find large log files
shell: find /var/log -name "*.log" -size +100M | head -20
register: large_logs
changed_when: false

Copy a local script to the remote host and execute it:

- name: Run setup script
script: scripts/setup.sh
args:
creates: /opt/myapp/.installed # skip if this file exists

Execute a command without the module system (useful for bootstrapping Python):

- name: Install Python on minimal hosts
raw: apt-get install -y python3
when: ansible_python_interpreter is not defined
ModuleUse When
commandSimple commands, no shell features needed
shellNeed pipes, redirects, or shell variables
scriptRunning a multi-line local script on remote hosts
rawBootstrapping (no Python on target yet)

Prefer specific modules (like apt, file, service) over command/shell whenever possible — specific modules are idempotent, command and shell are not.

- name: Download a file
get_url:
url: https://example.com/app-v2.tar.gz
dest: /tmp/app-v2.tar.gz
checksum: sha256:abc123...
- name: Extract archive
unarchive:
src: /tmp/app-v2.tar.gz
dest: /opt/myapp/
remote_src: true
- name: Schedule a backup
cron:
name: "Daily backup"
minute: "0"
hour: "2"
job: "/opt/scripts/backup.sh >> /var/log/backup.log 2>&1"
user: root
Terminal window
ansible-doc apt # show docs for the apt module
ansible-doc -l # list all available modules
ansible-doc -l | grep aws # find AWS modules
ansible-doc -s copy # show short snippet/example

Or browse the Ansible module index online.

When no built-in module fits your needs, you can write your own in Python. Custom modules follow a simple pattern: receive JSON input, do work, return JSON output.

Place custom modules in a library/ directory next to your playbook or role:

project/
playbooks/
site.yml
library/
my_custom_module.py # available to all playbooks
roles/
myapp/
library/
app_health.py # available only in this role
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
def run_module():
# Define accepted arguments
module_args = dict(
name=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['present', 'absent']),
force=dict(type='bool', default=False),
)
# Initialize the module
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
)
# Get parameters
name = module.params['name']
state = module.params['state']
# Initialize result
result = dict(
changed=False,
message='',
)
# Check mode — report what would change without doing it
if module.check_mode:
result['changed'] = True
module.exit_json(**result)
# Do the actual work
try:
if state == 'present':
# Create or ensure resource exists
result['changed'] = True
result['message'] = f'Resource {name} created'
else:
# Remove resource
result['changed'] = True
result['message'] = f'Resource {name} removed'
module.exit_json(**result)
except Exception as e:
module.fail_json(msg=f'Error: {str(e)}', **result)
def main():
run_module()
if __name__ == '__main__':
main()
- name: Use custom module
my_custom_module:
name: myresource
state: present
force: true
register: result
- debug:
var: result.message

Idempotency — Check current state before making changes:

# Check if resource already exists
current = get_current_state(name)
if current and state == 'present':
result['changed'] = False
result['message'] = f'{name} already exists'
module.exit_json(**result)

Check mode support — Return what would change without doing it:

module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True, # declare support
)
if module.check_mode:
result['changed'] = would_change(name, state)
module.exit_json(**result)

Running commands — Use module.run_command() instead of subprocess:

rc, stdout, stderr = module.run_command(['myapp', 'status', name])
if rc != 0:
module.fail_json(msg=f'Command failed: {stderr}')

Returning data — Include useful info in the result for register:

result['resource_id'] = resource.id
result['resource_url'] = resource.url
module.exit_json(**result)

For more advanced use cases — action plugins run on the control node (not the target) and can manipulate how modules are executed. These go in action_plugins/ and are less common.

SituationRecommendation
A built-in module almost worksUse command/shell with changed_when first
You need this logic in multiple playbooksWrite a custom module
Complex API interaction with idempotencyWrite a custom module
Simple one-off taskUse command or uri module instead

  • Prefer specific modules over command/shell — they’re idempotent and declarative.
  • Use copy for static files, template for files with variables, lineinfile/blockinfile for surgical edits. Use copy for single or few files; use synchronize for directory sync or large transfers.
  • service or systemd to manage services; apt/yum/package for packages.
  • Use command with changed_when: false for read-only checks.
  • Use ansible-doc <module> to look up any module’s arguments and examples.
  • Custom modules go in library/. Use AnsibleModule, support check mode, and be idempotent.