Ansible Modules

What are Modules?

Modules are the units of work in Ansible. Each module performs a specific task, such as managing packages, services, files, or users. Ansible comes with thousands of modules organized into collections.

Module Categories

  • System: user, group, service, systemd, cron
  • Package Management: yum, apt, dnf, pip, npm
  • Files: copy, template, file, lineinfile, blockinfile
  • Database: mysql_db, postgresql_db, mongodb
  • Cloud: ec2, azure_rm, gcp_compute
  • Network: ios_command, junos_config, nxos_facts
  • Windows: win_service, win_package, win_file

Essential Modules

debug - Print Messages

- name: Print variable
  debug:
    var: ansible_hostname

- name: Print custom message
  debug:
    msg: "Server {{ inventory_hostname }} is {{ ansible_distribution }}"

copy - Copy Files

- name: Copy file
  copy:
    src: /local/path/file.txt
    dest: /remote/path/file.txt
    owner: root
    group: root
    mode: '0644'
    backup: yes

template - Deploy Jinja2 Templates

- name: Deploy configuration
  template:
    src: config.j2
    dest: /etc/app/config.conf
    validate: '/usr/sbin/configtest %s'

file - Manage Files and Directories

- name: Create directory
  file:
    path: /opt/myapp
    state: directory
    mode: '0755'

- name: Create symlink
  file:
    src: /opt/app/current
    dest: /usr/local/bin/app
    state: link

- name: Remove file
  file:
    path: /tmp/oldfile
    state: absent

lineinfile - Manage Lines in Files

- name: Ensure line exists
  lineinfile:
    path: /etc/hosts
    line: '192.168.1.100 myserver.local'

- name: Replace line matching regex
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?PermitRootLogin'
    line: 'PermitRootLogin no'

- name: Remove line
  lineinfile:
    path: /etc/config.conf
    regexp: '^OldSetting='
    state: absent

user - Manage Users

- name: Create user
  user:
    name: deploy
    uid: 1001
    group: wheel
    groups: docker
    append: yes
    shell: /bin/bash
    create_home: yes

- name: Remove user
  user:
    name: olduser
    state: absent
    remove: yes

service / systemd - Manage Services

- name: Start and enable service
  service:
    name: nginx
    state: started
    enabled: yes

- name: Restart service with systemd
  systemd:
    name: postgresql
    state: restarted
    daemon_reload: yes

yum / apt - Package Management

# RHEL/CentOS
- name: Install packages with yum
  yum:
    name:
      - httpd
      - mod_ssl
    state: present

# Debian/Ubuntu
- name: Install packages with apt
  apt:
    name:
      - apache2
      - libapache2-mod-ssl
    state: present
    update_cache: yes

command - Execute Commands

- name: Run command
  command: /usr/local/bin/script.sh
  args:
    chdir: /opt/app
    creates: /opt/app/initialized

- name: Command with changed_when
  command: echo "hello"
  register: result
  changed_when: false

shell - Execute Shell Commands

- name: Run shell script with pipes
  shell: cat /var/log/app.log | grep ERROR | wc -l
  register: error_count

- name: Multi-line shell script
  shell: |
    if [ -f /tmp/lock ]; then
      echo "locked"
      exit 1
    fi
    echo "unlocked"

git - Manage Git Repositories

- name: Clone repository
  git:
    repo: https://github.com/example/app.git
    dest: /opt/app
    version: main
    force: yes

- name: Update repository
  git:
    repo: https://github.com/example/app.git
    dest: /opt/app
    update: yes

get_url - Download Files

- name: Download file
  get_url:
    url: https://example.com/file.tar.gz
    dest: /tmp/file.tar.gz
    checksum: sha256:abc123...
    mode: '0644'

unarchive - Extract Archives

- name: Extract tar.gz
  unarchive:
    src: /tmp/app.tar.gz
    dest: /opt/app
    remote_src: yes

- name: Extract from control node
  unarchive:
    src: files/app.zip
    dest: /opt/app

mysql_db / mysql_user - MySQL Management

- name: Create database
  mysql_db:
    name: myapp_db
    state: present
    encoding: utf8mb4
    collation: utf8mb4_unicode_ci

- name: Create MySQL user
  mysql_user:
    name: myapp_user
    password: "{{ db_password }}"
    priv: 'myapp_db.*:ALL'
    host: localhost
    state: present

cron - Manage Cron Jobs

- name: Create cron job
  cron:
    name: "Daily backup"
    minute: "0"
    hour: "2"
    job: "/usr/local/bin/backup.sh"
    user: root

- name: Remove cron job
  cron:
    name: "Old backup"
    state: absent

authorized_key - Manage SSH Keys

- name: Add SSH public key
  authorized_key:
    user: deploy
    key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
    state: present

wait_for - Wait for Condition

- name: Wait for port to be available
  wait_for:
    host: "{{ inventory_hostname }}"
    port: 8080
    delay: 10
    timeout: 300
    state: started

- name: Wait for file to exist
  wait_for:
    path: /tmp/app.pid
    state: present

uri - HTTP Requests

- name: Check API endpoint
  uri:
    url: https://api.example.com/health
    method: GET
    return_content: yes
  register: health_check

- name: POST data to API
  uri:
    url: https://api.example.com/data
    method: POST
    body_format: json
    body:
      key: value
    headers:
      Authorization: "Bearer {{ api_token }}"

stat - Get File Status

- name: Check if file exists
  stat:
    path: /etc/app/config.conf
  register: config_file

- name: Use stat result
  debug:
    msg: "Config exists"
  when: config_file.stat.exists

set_fact - Define Variables

- name: Set custom fact
  set_fact:
    deployment_time: "{{ ansible_date_time.iso8601 }}"
    app_version: "2.0.1"

- name: Use fact
  debug:
    msg: "Deployed version {{ app_version }} at {{ deployment_time }}"

assert - Validate Conditions

- name: Assert conditions
  assert:
    that:
      - ansible_distribution == "Ubuntu"
      - ansible_distribution_major_version >= "20"
      - ansible_memtotal_mb >= 2048
    fail_msg: "System doesn't meet requirements"
    success_msg: "System meets all requirements"

Module Return Values

Modules return data that can be captured with register:

- name: Execute command
  command: uptime
  register: uptime_result

- name: Display return values
  debug:
    msg:
      - "Return code: {{ uptime_result.rc }}"
      - "Stdout: {{ uptime_result.stdout }}"
      - "Stderr: {{ uptime_result.stderr }}"
      - "Changed: {{ uptime_result.changed }}"

Module Documentation

# View module documentation
ansible-doc copy

# List all modules
ansible-doc -l

# Search for modules
ansible-doc -l | grep mysql

Common Module Parameters

Many modules support these common parameters:

  • state - Desired state (present, absent, started, stopped, etc.)
  • name - Name of the resource
  • become - Escalate privileges
  • when - Conditional execution
  • register - Save output
  • notify - Trigger handlers
  • tags - Tag for selective execution

Advanced Module Categories

Notification Modules

---
- name: Notification examples
  hosts: localhost

  tasks:
    - name: Send Slack notification
      slack:
        token: "{{ slack_token }}"
        channel: '#deployments'
        msg: "Deployment completed on {{ inventory_hostname }}"
        color: good
        icon_url: https://example.com/icon.png

    - name: Send email notification
      mail:
        host: smtp.example.com
        port: 587
        username: "{{ smtp_user }}"
        password: "{{ smtp_pass }}"
        to: ops@example.com
        subject: "Ansible Deployment Complete"
        body: "Deployment finished at {{ ansible_date_time.iso8601 }}"

    - name: Send webhook notification
      uri:
        url: https://hooks.example.com/webhook
        method: POST
        body_format: json
        body:
          event: deployment
          status: success
          timestamp: "{{ ansible_date_time.epoch }}"

    - name: Create ServiceNow ticket
      snow_record:
        username: "{{ snow_user }}"
        password: "{{ snow_pass }}"
        instance: dev12345
        state: present
        table: incident
        data:
          short_description: "Automated deployment notification"
          urgency: 2
          impact: 2

Cloud Modules - AWS

---
- name: AWS resource management
  hosts: localhost
  gather_facts: no

  tasks:
    - name: Launch EC2 instance
      ec2_instance:
        name: web-server-01
        key_name: my-keypair
        instance_type: t3.medium
        image_id: ami-0c55b159cbfafe1f0
        region: us-east-1
        vpc_subnet_id: subnet-12345
        security_groups:
          - sg-web
        tags:
          Environment: production
          Role: webserver
        wait: yes
      register: ec2

    - name: Create S3 bucket
      s3_bucket:
        name: my-app-bucket-{{ ansible_date_time.epoch }}
        region: us-east-1
        versioning: yes
        encryption: AES256
        tags:
          Environment: production

    - name: Create RDS database
      rds_instance:
        db_instance_identifier: myapp-db
        engine: postgres
        engine_version: "13.7"
        instance_class: db.t3.medium
        allocated_storage: 100
        master_username: dbadmin
        master_user_password: "{{ vault_db_password }}"
        vpc_security_group_ids:
          - sg-database
        multi_az: yes
        backup_retention_period: 7
        region: us-east-1

    - name: Create ELB
      elb_application_lb:
        name: myapp-alb
        subnets:
          - subnet-public-1
          - subnet-public-2
        security_groups:
          - sg-alb
        scheme: internet-facing
        listeners:
          - Protocol: HTTPS
            Port: 443
            DefaultActions:
              - Type: forward
                TargetGroupArn: "{{ target_group_arn }}"
        region: us-east-1

    - name: Create CloudWatch alarm
      cloudwatch_metric_alarm:
        name: high-cpu-alarm
        metric: CPUUtilization
        namespace: AWS/EC2
        statistic: Average
        comparison: '>='
        threshold: 80.0
        period: 300
        evaluation_periods: 2
        dimensions:
          InstanceId: "{{ ec2.instance_ids[0] }}"
        alarm_actions:
          - "{{ sns_topic_arn }}"
        region: us-east-1

Cloud Modules - Azure

---
- name: Azure resource management
  hosts: localhost
  gather_facts: no

  tasks:
    - name: Create resource group
      azure_rm_resourcegroup:
        name: production-rg
        location: eastus
        tags:
          Environment: production

    - name: Create virtual network
      azure_rm_virtualnetwork:
        resource_group: production-rg
        name: prod-vnet
        address_prefixes_cidr:
          - 10.0.0.0/16

    - name: Create subnet
      azure_rm_subnet:
        resource_group: production-rg
        virtual_network_name: prod-vnet
        name: web-subnet
        address_prefix_cidr: 10.0.1.0/24

    - name: Create public IP
      azure_rm_publicipaddress:
        resource_group: production-rg
        name: web-public-ip
        allocation_method: Static

    - name: Create VM
      azure_rm_virtualmachine:
        resource_group: production-rg
        name: web-vm-01
        vm_size: Standard_DS2_v2
        admin_username: azureuser
        ssh_password_enabled: false
        ssh_public_keys:
          - path: /home/azureuser/.ssh/authorized_keys
            key_data: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
        image:
          offer: UbuntuServer
          publisher: Canonical
          sku: 18.04-LTS
          version: latest
        tags:
          Environment: production
          Role: webserver

    - name: Create Azure SQL Database
      azure_rm_sqldatabase:
        resource_group: production-rg
        server_name: myapp-sql-server
        name: myapp-db
        sku:
          name: S1
          tier: Standard

Container Modules

---
- name: Container management
  hosts: docker_hosts

  tasks:
    - name: Pull Docker image
      docker_image:
        name: nginx
        tag: latest
        source: pull

    - name: Run Docker container
      docker_container:
        name: web-app
        image: myapp:latest
        state: started
        restart_policy: unless-stopped
        ports:
          - "8080:80"
        volumes:
          - /data/app:/app/data
        env:
          DATABASE_HOST: db.example.com
          DATABASE_PORT: "5432"
        networks:
          - name: app-network
        labels:
          environment: production
          service: web

    - name: Build Docker image
      docker_image:
        name: myapp
        tag: "{{ app_version }}"
        build:
          path: /opt/app
          dockerfile: Dockerfile
          pull: yes
        source: build

    - name: Create Docker network
      docker_network:
        name: app-network
        driver: bridge
        ipam_config:
          - subnet: 172.20.0.0/16
            gateway: 172.20.0.1

    - name: Manage Docker Compose
      docker_compose:
        project_src: /opt/myapp
        state: present
        recreate: smart
        remove_orphans: yes

Monitoring & Logging Modules

---
- name: Monitoring and logging setup
  hosts: all

  tasks:
    - name: Install Datadog agent
      datadog_monitor:
        api_key: "{{ datadog_api_key }}"
        app_key: "{{ datadog_app_key }}"
        type: metric alert
        query: "avg(last_5m):avg:system.cpu.user{host:{{ inventory_hostname }}} > 90"
        name: "High CPU on {{ inventory_hostname }}"
        message: "CPU usage is above 90% @pagerduty"

    - name: Configure Prometheus node exporter
      systemd:
        name: node_exporter
        enabled: yes
        state: started
        daemon_reload: yes

    - name: Send log to Splunk
      splunk_data:
        host: splunk.example.com
        port: 8088
        token: "{{ splunk_token }}"
        event:
          event: "Deployment completed"
          host: "{{ inventory_hostname }}"
          source: ansible
          sourcetype: automation

    - name: Create New Relic deployment marker
      newrelic_deployment:
        token: "{{ newrelic_api_key }}"
        app_name: MyApplication
        user: ansible
        revision: "{{ git_commit_hash }}"
        changelog: "{{ git_commit_message }}"

Security & Secrets Modules

---
- name: Security and secrets management
  hosts: all

  tasks:
    - name: Fetch secret from HashiCorp Vault
      set_fact:
        db_password: "{{ lookup('hashi_vault', 'secret=secret/data/database:password') }}"
        api_key: "{{ lookup('hashi_vault', 'secret=secret/data/api:key') }}"

    - name: Fetch from AWS Secrets Manager
      set_fact:
        app_secret: "{{ lookup('aws_secret', 'production/app/credentials', region='us-east-1') }}"

    - name: Fetch from Azure Key Vault
      azure_rm_keyvaultsecret_info:
        vault_uri: https://myvault.vault.azure.net
        name: database-password
      register: azure_secret

    - name: Create SSL certificate
      openssl_certificate:
        path: /etc/ssl/certs/{{ ansible_fqdn }}.crt
        privatekey_path: /etc/ssl/private/{{ ansible_fqdn }}.key
        csr_path: /etc/ssl/csr/{{ ansible_fqdn }}.csr
        provider: selfsigned

    - name: Manage firewalld
      firewalld:
        service: https
        permanent: yes
        state: enabled
        immediate: yes

    - name: Configure SELinux
      selinux:
        policy: targeted
        state: enforcing

    - name: Set SELinux file context
      sefcontext:
        target: '/opt/app(/.*)?'
        setype: httpd_sys_content_t
        state: present

    - name: Apply SELinux context
      command: restorecon -Rv /opt/app

Module Collections

Ansible modules are organized into collections. Collections are distribution packages containing playbooks, roles, modules, and plugins.

Installing Collections

# Install from Ansible Galaxy
ansible-galaxy collection install amazon.aws

# Install specific version
ansible-galaxy collection install community.general:3.8.0

# Install from requirements file
ansible-galaxy collection install -r requirements.yml

# requirements.yml
---
collections:
  - name: community.general
    version: ">=3.0.0"
  - name: ansible.posix
  - name: amazon.aws
  - name: azure.azcollection
  - name: google.cloud

Using Collection Modules

---
- name: Use collection modules
  hosts: localhost

  tasks:
    # Fully Qualified Collection Name (FQCN)
    - name: Use AWS module with FQCN
      amazon.aws.ec2_instance:
        name: my-instance
        instance_type: t3.medium

    # With collections keyword
- name: Simplified usage
  hosts: localhost
  collections:
    - amazon.aws
    - community.general

  tasks:
    - name: Use module without FQCN
      ec2_instance:
        name: my-instance
        instance_type: t3.medium

Popular Collections

  • ansible.builtin: Core Ansible modules (file, copy, template, etc.)
  • community.general: General-purpose modules
  • ansible.posix: POSIX systems modules
  • amazon.aws: AWS cloud modules
  • azure.azcollection: Azure cloud modules
  • google.cloud: Google Cloud modules
  • kubernetes.core: Kubernetes management
  • community.docker: Docker container management
  • community.postgresql: PostgreSQL database
  • community.mysql: MySQL/MariaDB database

Custom Module Development

Simple Custom Module (Python)

#!/usr/bin/python
# library/custom_file_info.py

from ansible.module_utils.basic import AnsibleModule
import os

def main():
    module = AnsibleModule(
        argument_spec=dict(
            path=dict(type='str', required=True),
        ),
        supports_check_mode=True
    )

    path = module.params['path']

    if not os.path.exists(path):
        module.fail_json(msg=f"File {path} does not exist")

    stat_info = os.stat(path)

    result = {
        'changed': False,
        'path': path,
        'size': stat_info.st_size,
        'mode': oct(stat_info.st_mode)[-3:],
        'is_file': os.path.isfile(path),
        'is_directory': os.path.isdir(path),
    }

    module.exit_json(**result)

if __name__ == '__main__':
    main()

Using Custom Module

---
- name: Use custom module
  hosts: localhost

  tasks:
    - name: Get custom file info
      custom_file_info:
        path: /etc/hosts
      register: file_info

    - name: Display file info
      debug:
        msg: "File size: {{ file_info.size }} bytes, mode: {{ file_info.mode }}"

Advanced Custom Module with Change Detection

#!/usr/bin/python
# library/custom_config.py

from ansible.module_utils.basic import AnsibleModule
import json

def main():
    module = AnsibleModule(
        argument_spec=dict(
            path=dict(type='str', required=True),
            key=dict(type='str', required=True),
            value=dict(type='str', required=True),
            state=dict(type='str', default='present', choices=['present', 'absent']),
        ),
        supports_check_mode=True
    )

    path = module.params['path']
    key = module.params['key']
    value = module.params['value']
    state = module.params['state']

    # Read current config
    try:
        with open(path, 'r') as f:
            config = json.load(f)
    except FileNotFoundError:
        config = {}
    except json.JSONDecodeError:
        module.fail_json(msg=f"Invalid JSON in {path}")

    changed = False
    original_value = config.get(key)

    if state == 'present':
        if config.get(key) != value:
            config[key] = value
            changed = True
    elif state == 'absent':
        if key in config:
            del config[key]
            changed = True

    # Check mode - don't make changes
    if module.check_mode:
        module.exit_json(changed=changed, original_value=original_value)

    # Apply changes
    if changed:
        try:
            with open(path, 'w') as f:
                json.dump(config, f, indent=2)
        except IOError as e:
            module.fail_json(msg=f"Failed to write config: {str(e)}")

    module.exit_json(
        changed=changed,
        key=key,
        value=value if state == 'present' else None,
        original_value=original_value,
        config=config
    )

if __name__ == '__main__':
    main()

Advanced Module Patterns

Module Chaining with Loops

---
- name: Complex module chaining
  hosts: webservers

  vars:
    applications:
      - name: app1
        port: 8080
        replicas: 3
      - name: app2
        port: 8081
        replicas: 5

  tasks:
    # Create users for each app
    - name: Create application users
      user:
        name: "{{ item.name }}"
        system: yes
        create_home: yes
        home: "/opt/{{ item.name }}"
      loop: "{{ applications }}"

    # Create directories
    - name: Create app directories
      file:
        path: "{{ item.1 }}"
        state: directory
        owner: "{{ item.0.name }}"
        mode: '0755'
      loop: "{{ applications | subelements('paths', skip_missing=True) | default([]) }}"
      vars:
        paths:
          - /opt/{{ item.0.name }}/data
          - /opt/{{ item.0.name }}/logs
          - /opt/{{ item.0.name }}/config

    # Deploy configurations
    - name: Deploy app configs
      template:
        src: "{{ item.name }}/config.j2"
        dest: "/opt/{{ item.name }}/config/app.conf"
        owner: "{{ item.name }}"
        mode: '0600'
        validate: 'python3 -c "import json; json.load(open(\"%s\"))"'
      loop: "{{ applications }}"
      notify: restart applications

    # Create systemd services
    - name: Create systemd service files
      template:
        src: app.service.j2
        dest: "/etc/systemd/system/{{ item.name }}.service"
      loop: "{{ applications }}"
      notify:
        - reload systemd
        - restart applications

    # Configure firewall rules
    - name: Allow application ports
      firewalld:
        port: "{{ item.port }}/tcp"
        permanent: yes
        state: enabled
        immediate: yes
      loop: "{{ applications }}"

  handlers:
    - name: reload systemd
      systemd:
        daemon_reload: yes

    - name: restart applications
      systemd:
        name: "{{ item.name }}"
        state: restarted
        enabled: yes
      loop: "{{ applications }}"

Conditional Module Execution

---
- name: Conditional module execution
  hosts: all

  tasks:
    - name: Gather package facts
      package_facts:
        manager: auto

    - name: Install package only if not present
      package:
        name: nginx
        state: present
      when: "'nginx' not in ansible_facts.packages"

    - name: Update only if package exists
      package:
        name: nginx
        state: latest
      when: "'nginx' in ansible_facts.packages"

    - name: OS-specific package installation
      package:
        name: "{{ item }}"
        state: present
      loop: "{{ packages[ansible_os_family] }}"
      vars:
        packages:
          Debian:
            - apache2
            - libapache2-mod-php
          RedHat:
            - httpd
            - php

    - name: Version-specific configuration
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin prohibit-password'
      when:
        - ansible_distribution == 'Ubuntu'
        - ansible_distribution_major_version | int >= 20

Error Handling and Retry Logic

---
- name: Advanced error handling
  hosts: all

  tasks:
    - name: Download with retry
      get_url:
        url: https://example.com/package.tar.gz
        dest: /tmp/package.tar.gz
        checksum: sha256:abc123...
      register: download_result
      retries: 5
      delay: 10
      until: download_result is succeeded

    - name: Service with failure handling
      block:
        - name: Stop service
          service:
            name: myapp
            state: stopped

        - name: Deploy new version
          copy:
            src: /tmp/newapp
            dest: /opt/app/
            backup: yes
          register: deploy

        - name: Start service
          service:
            name: myapp
            state: started

        - name: Health check
          uri:
            url: http://localhost:8080/health
            status_code: 200
          retries: 5
          delay: 5
          register: health

      rescue:
        - name: Rollback deployment
          copy:
            src: "{{ deploy.backup_file }}"
            dest: /opt/app/
            remote_src: yes
          when: deploy.backup_file is defined

        - name: Restart with old version
          service:
            name: myapp
            state: restarted

        - name: Send failure alert
          mail:
            subject: "Deployment failed on {{ inventory_hostname }}"
            body: "Deployment failed, rolled back to previous version"

      always:
        - name: Clean temporary files
          file:
            path: /tmp/newapp
            state: absent

        - name: Log deployment attempt
          lineinfile:
            path: /var/log/deployments.log
            line: "{{ ansible_date_time.iso8601 }} - Deployment attempt: {{ 'success' if health is succeeded else 'failed' }}"
            create: yes

Parallel Execution with Async

---
- name: Parallel module execution
  hosts: webservers

  tasks:
    # Start all updates in parallel
    - name: Start package updates (async)
      yum:
        name: '*'
        state: latest
        security: yes
      async: 1800  # 30 minutes max
      poll: 0
      register: update_jobs

    # Do other work while updates run
    - name: Check application status
      uri:
        url: http://{{ inventory_hostname }}:8080/health
      register: health_check

    - name: Backup configuration files
      copy:
        src: /etc/myapp/
        dest: /backup/myapp-{{ ansible_date_time.date }}/
        remote_src: yes

    # Check async task status
    - name: Wait for package updates to complete
      async_status:
        jid: "{{ update_jobs.ansible_job_id }}"
      register: job_result
      until: job_result.finished
      retries: 180
      delay: 10

    - name: Verify update success
      assert:
        that:
          - job_result.finished
          - job_result.failed == false
        fail_msg: "Package updates failed"

Module Performance Optimization

Batch Operations

---
- name: Efficient module usage
  hosts: all

  tasks:
    # BAD: Multiple task calls
    - name: Install package 1
      yum:
        name: package1
        state: present

    - name: Install package 2
      yum:
        name: package2
        state: present

    # GOOD: Single task with list
    - name: Install multiple packages efficiently
      yum:
        name:
          - package1
          - package2
          - package3
          - package4
        state: present

    # GOOD: Use with_items for complex operations
    - name: Create multiple users efficiently
      user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        groups: "{{ item.groups }}"
      loop:
        - { name: user1, uid: 1001, groups: wheel }
        - { name: user2, uid: 1002, groups: developers }
        - { name: user3, uid: 1003, groups: operators }

Minimizing Fact Gathering

---
- name: Optimize fact gathering
  hosts: all
  gather_facts: no

  tasks:
    # Only gather specific facts
    - name: Gather minimal facts
      setup:
        filter:
          - ansible_distribution
          - ansible_distribution_major_version
          - ansible_default_ipv4

    - name: Use gathered facts
      debug:
        msg: "Running on {{ ansible_distribution }} {{ ansible_distribution_major_version }}"

Module Testing

Testing Custom Modules

# test_custom_module.yml
---
- name: Test custom module
  hosts: localhost
  gather_facts: no

  tasks:
    - name: Test module with valid input
      custom_config:
        path: /tmp/test_config.json
        key: test_key
        value: test_value
        state: present
      register: result

    - name: Verify result
      assert:
        that:
          - result.changed == true
          - result.key == 'test_key'
          - result.value == 'test_value'

    - name: Test idempotency
      custom_config:
        path: /tmp/test_config.json
        key: test_key
        value: test_value
        state: present
      register: result

    - name: Verify no change on second run
      assert:
        that:
          - result.changed == false

    - name: Test removal
      custom_config:
        path: /tmp/test_config.json
        key: test_key
        value: test_value
        state: absent
      register: result

    - name: Verify removal
      assert:
        that:
          - result.changed == true

    - name: Cleanup
      file:
        path: /tmp/test_config.json
        state: absent

Module Debugging

Debug Module Execution

# Run with maximum verbosity
ansible-playbook playbook.yml -vvvv

# Debug specific module
ANSIBLE_KEEP_REMOTE_FILES=1 ansible-playbook playbook.yml

# Check what module would do (without executing)
ansible-playbook playbook.yml --check

# See differences
ansible-playbook playbook.yml --check --diff

# Step through tasks
ansible-playbook playbook.yml --step

# List all tasks
ansible-playbook playbook.yml --list-tasks

# Start at specific task
ansible-playbook playbook.yml --start-at-task="Install packages"

Best Practices

  • Prefer dedicated modules: Use specific modules over command or shell
  • Use FQCN: Always use Fully Qualified Collection Names for clarity
  • Check return values: Use return values for conditional logic
  • Implement idempotency: Use changed_when and creates parameters
  • Handle errors gracefully: Use block/rescue/always for error handling
  • Validate changes: Use validate parameter when available
  • Use check mode: Test with --check before running
  • Batch operations: Install multiple packages in one task
  • Use async for long tasks: Don't block on long-running operations
  • Cache facts: Enable fact caching for better performance
  • Document custom modules: Include DOCUMENTATION, EXAMPLES, RETURN
  • Test custom modules: Write tests for custom module behavior
  • Keep modules simple: One module should do one thing well
  • Use module defaults: Set defaults in ansible.cfg for repeated parameters
  • Leverage collections: Organize related modules into collections

Next Steps