DEV Community

Ioannis Moustakis for Spacelift

Posted on • Originally published at spacelift.io

Ansible Modules – How To Use Them Efficiently (Examples)

Image description

This article delves into Ansible modules, one of the core building blocks of Ansible. In this blog post, we will examine the purpose and usage of modules, along with information on how to build them and best practices.

If you are interested in other Ansible concepts, these Ansible tutorials posted on Spacelift’s blog might be helpful for you.

What Are Ansible Modules?
Modules represent distinct units of code, each one with specific functionality. Basically, they are standalone scripts written for a particular job and are used in tasks as their main functional layer.

We build Ansible modules to abstract complexity and provide end-users with an easier way to execute their automation tasks without needing all the details. This way, some of the cognitive load of more complex tasks is abstracted away from Ansible users by leveraging the appropriate modules.

Here’s an example of a task using the apt package manager module to install a specific version of Nginx.

- name: "Install Nginx to version {{ nginx_version }} with apt module"
   ansible.builtin.apt:
     name: "nginx={{ nginx_version }}"
     state: present
Enter fullscreen mode Exit fullscreen mode

Modules can be executed as well directly from the command line. Here’s an example of running the ping module against all the database hosts from the command line.

ansible databases -m ping
Enter fullscreen mode Exit fullscreen mode

Working With Ansible Modules

A well-designed module provides a predictable and well-defined interface that accepts arguments that make sense and are consistent with other modules. Modules take some arguments as input and return values in JSON format after execution.

Ansible modules should follow idempotency principles, which means that consecutive runs of the same module should have the same effect if nothing else changes. Well-designed modules detect if the current and desired state match and avoid making changes if that’s the case.

We can utilize handlers to control the flow execution of modules and tasks in a playbook. Modules can trigger additional downstream modules and tasks by notifying specific handlers.

As mentioned, modules return data structures in JSON data. We can store these return values in variables and use them in other tasks or display them to the console. Look at the common return values for all modules to get an idea.

For custom modules, the return values should be documented along with other useful information for the module. The command-line tool ansible-doc displays this information.

Here’s an example output of running the ansible-doc command.

ansible-doc apt
Enter fullscreen mode Exit fullscreen mode

Image description

In the latest versions of Ansible, most modules are part of collections, a distribution format that includes roles, modules, plugins, and playbooks. Many of the core modules we use extensively are part of the Ansible.Builtin collection. To find other available modules have a look at the Collection docs.

12 Useful & Common Ansible Modules

In this part, we explore some of the most used and helpful modules, and for each, we provide a working example. The modules in this list are picked based on their popularity within the Ansible community and functionality to perform everyday automation tasks.

Package Manager Modules yum & apt

The apt module is part of ansible-core and manages apt packages for Debian/Ubuntu Linux distributions. Here’s an example that updates the repository cache and updates the Nginx package to the latest version:

- name: Update the repository cache and update package "nginx" to latest version
  ansible.builtin.apt:
    name: nginx
    state: latest
    update_cache: yes
Enter fullscreen mode Exit fullscreen mode

The yum module is also part of ansible-core and manages packages with yum for RHEL/Centos/Fedora Linux distributions. Here’s the same example as above with the yum module:

- name: Update the repository cache and update package "nginx" to latest version
   ansible.builtin.yum:
     name: nginx
     state: latest
     update_cache: yes
Enter fullscreen mode Exit fullscreen mode

Service Module

The service module controls services on remote hosts and can leverage different init systems depending on their availability in a system. This module provides a nice abstraction layer for underlying service manager modules. Here’s an example of restarting the docker service:

- name: Restart docker service
   ansible.builtin.service:
     name: docker
     state: restarted
Enter fullscreen mode Exit fullscreen mode

File Module

The file module handles operations to files, symlinks, and directories. Here’s an example of using this module to create a directory with specific permissions:

- name: Create the directory "/etc/test" if it doesnt exist and set permissions
  ansible.builtin.file:
    path: /etc/test
    state: directory
    mode: '0750'
Enter fullscreen mode Exit fullscreen mode

Copy Module

The copy module copies files to the remote machine and handles file transfers or moves within a remote system. Here’s an example of copying a file to the remote machine with permissions set:

- name: Copy file with owner and permissions
  ansible.builtin.copy:
    src: /example_directory/test
    dest: /target_directory/test
    owner: joe
    group: admins
    mode: '0755'
Enter fullscreen mode Exit fullscreen mode

Template Module

The template module assists us to template files out to target hosts by leveraging the Jinja2 templating language. Here’s an example of using a template file and some set Ansible variables to generate an Nginx configuration file:

- name: Copy and template the Nginx configuration file to the host
  ansible.builtin.template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/sites-available/default
Enter fullscreen mode Exit fullscreen mode

Lineinfile & Blockinfile Modules

The lineinfile module adds, replaces, or ensures that a particular line exists in a file. It’s pretty common to use this module when we need to update a single line in configuration files.

- name: Add a line to a file if it doesnt exist
  ansible.builtin.lineinfile:
    path: /tmp/example_file
    line: "This line must exist in the file"
    state: present
Enter fullscreen mode Exit fullscreen mode

The blockinfile module inserts, updates, or removes a block of lines from a file. It has the same functionality as the previous module but is used when you want to manipulate multi-line text blocks.

- name: Add a block of config options at the end of the file if it doesn’t exist
  ansible.builtin.blockinfile:
    path: /etc/example_dictory/example.conf
    block: |
      feature1_enabled: true
      feature2_enabled: false
      feature2_enabled: true
    insertafter: EOF
Enter fullscreen mode Exit fullscreen mode

Cron Module

The cron module manages crontab entries and environment variables entries on remote hosts.

- name: Run daily DB backup script at 00:00
  ansible.builtin.cron:
    name: "Run daily DB backup script at 00:00"
    minute: "0"
    hour: "0"
    job: "/usr/local/bin/db_backup_script.sh > /var/log/db_backup_script.sh.log 2>&1"
Enter fullscreen mode Exit fullscreen mode

Wait_for Module

The wait_for module provides a way to stop the execution of plays and wait for conditions, amount of time to pass, ports to become open, processes to finish, files to be available, strings to exist in files, etc.

- name: Wait until a string is in the file before continuing
  ansible.builtin.wait_for:
    path: /tmp/example_file
    search_regex: "String exists, continue"
Enter fullscreen mode Exit fullscreen mode

Command & Shell Modules

The command and shell modules execute commands on remote nodes. Their main difference is that the command module bypasses the local shell, and consequently, variables like $HOSTNAME or $HOME aren’t available, and operations like “<”, “&” don’t work. If you need these features, you have to use the shell module.

On the other hand, the remote local environment won’t affect the command module, so its outcome is considered more predictable and secure.

Usually, it’s always preferred to use specialized Ansible modules to perform tasks instead of command and shell. There are cases, though, where you won’t be able to get the functionality that you need from specialized modules, and you will have to use one of these two. Use command and shell with care, and always try to check if there is a specialized module that can serve you better before relying on them.

- name: Execute a script in remote shell and capture the output to file
  ansible.builtin.shell: script.sh >> script_output.log
Enter fullscreen mode Exit fullscreen mode

Building Ansible Modules

For advanced users, there is always the option to develop custom modules if they have needs that can’t be satisfied by existing modules. Since modules always should return JSON data, they can be written in any programming language.

Before jumping into module development, ensure that a similar module doesn’t exist to avoid unnecessary work. Αdditionally, you might be able to combine different modules to achieve the functionality you need. In this case, you might be able to replicate the behavior you want by creating a role that leverages other modules. Another option is to use plugins to enhance Ansible’s basic functionality with logic and new features accessible to all modules.

Next, we will go through an example of creating a custom module that takes as input a string that represents an epoch timestamp and converts it to its human-readable equivalent of type datetime in Python. You can find the code for this tutorial on this repository.

First, let’s create a library directory on the top of our repository to place our custom module. Playbooks with a ./library directory relative to their YAML file can add custom ansible modules that can be recognized in the ansible module path. This way, we can group custom modules and their related playbooks.

We create our custom Python module epoch_converter.py inside the library directory. This simple module takes as input the argument epoch_timestamp and converts it to datetime type. We use another argument, state_changed, to simulate a change in the target system by this module.

library/epoch_converter.py

#!/usr/bin/python

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import datetime

DOCUMENTATION = r'''
---
module: epoch_converter

short_description: This module converts an epoch timestamp to human-readable date.

# If this is part of a collection, you need to use semantic versioning,
# i.e. the version is of the form "2.5.0" and not "2.4".
version_added: "1.0.0"

description: This module takes a string that represents a Unix epoch timestamp and displays its human-readable date equivalent.

options:
   epoch_timestamp:
       description: This is the string that represents a Unix epoch timestamp.
       required: true
       type: str
   state_changed:
       description: This string simulates a modification of the target's state.
       required: false
       type: bool

author:
   - Ioannis Moustakis (@Imoustak)
'''

EXAMPLES = r'''
# Convert an epoch timestamp
- name: Convert an epoch timestamp
 epoch_converter:
   epoch_timestamp: 1657382362
'''

RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
human_readable_date:
   description: The human-readable equivalent of the epoch timestamp input.
   type: str
   returned: always
   sample: '2022-07-09T17:59:22'
original_timestamp:
   description: The original epoch timestamp input.
   type: str
   returned: always
   sample: '16573823622'

'''

from ansible.module_utils.basic import AnsibleModule


def run_module():
   # define available arguments/parameters a user can pass to the module
   module_args = dict(
       epoch_timestamp=dict(type='str', required=True),
       state_changed=dict(type='bool', required=False)
   )

   # seed the result dict in the object
   # we primarily care about changed and state
   # changed is if this module effectively modified the target
   # state will include any data that you want your module to pass back
   # for consumption, for example, in a subsequent task
   result = dict(
       changed=False,
       human_readable_date='',
       original_timestamp=''
   )

   # the AnsibleModule object will be our abstraction working with Ansible
   # this includes instantiation, a couple of common attr would be the
   # args/params passed to the execution, as well as if the module
   # supports check mode
   module = AnsibleModule(
       argument_spec=module_args,
       supports_check_mode=True
   )

   # if the user is working with this module in only check mode we do not
   # want to make any changes to the environment, just return the current
   # state with no modifications
   if module.check_mode:
       module.exit_json(**result)

   # manipulate or modify the state as needed (this is going to be the
   # part where your module will do what it needs to do)
   result['original_timestamp'] = module.params['epoch_timestamp']
   result['human_readable_date'] = datetime.datetime.fromtimestamp(int(module.params['epoch_timestamp']))

   # use whatever logic you need to determine whether or not this module
   # made any modifications to your target
   if module.params['state_changed']:
       result['changed'] = True

   # during the execution of the module, if there is an exception or a
   # conditional state that effectively causes a failure, run
   # AnsibleModule.fail_json() to pass in the message and the result
   if module.params['epoch_timestamp'] == 'fail':
       module.fail_json(msg='You requested this to fail', **result)

   # in the event of a successful module execution, you will want to
   # simple AnsibleModule.exit_json(), passing the key/value results
   module.exit_json(**result)


def main():
   run_module()


if __name__ == '__main__':
   main()

Enter fullscreen mode Exit fullscreen mode

To test our module, let’s create a test_custom_module.ymlplaybook in the same directory as our library directory.

test_custom_module.yml

- name: Test my new module
  hosts: localhost
  tasks:
  - name: Run the new module
    epoch_converter:
      epoch_timestamp: '1657382362'
      state_changed: yes
    register: show_output
  - name: Show Output
    debug:
      msg: '{{ show_output }}'
Enter fullscreen mode Exit fullscreen mode

Last stop, let’s execute the playbook to test our custom module. Since we opted to set the state_changed argument, we expect the task state to appear as changed and displayed in yellow.

Image description

If you wish to contribute to an existing Ansible collection or create and publish a new one with your custom modules, look at Distributing collections and Ansible Community Guide, where you can find information on how to configure and distribute Ansible content.

Ansible Modules Best Practices
Use specialized modules over shell or command: Although It might be tempting to use the shell or command module often, it’s considered a best practice to leverage more specific modules for each job. Specialized modules are typically recommended because they implement the concept of desired state and idempotency, have been tested, and fulfill basic standards, like error handling.

Specify arguments when it makes sense: Some module arguments have default values that can be omitted. To be more transparent and explicit, we can opt to specify some of these arguments like the state in our playbook definitions.

Prefer multi-tasks in a module over loops: The most efficient way of defining a list of similar tasks, like installing packages, is to use multiple tasks in a single module.

- name: Install Docker dependencies
  ansible.builtin.apt:
     name:
       - curl
       - ca-certificates
       - gnupg2
       - lsb-release
     state: latest
Enter fullscreen mode Exit fullscreen mode

The above method should be preferred over the loop or defining multiple separate tasks using the same module.

Custom modules should be simple and tackle a specific job: If you decide to build your own module, focus on solving a particular problem. Each module should have a concise functionality, be as simple as possible, and perform one thing well. If what you try to achieve goes beyond the scope of a single module, consider developing a new collection.

Custom modules should have predictable parameters: Try to enable others to use your module by defining a transparent and predictable user interface. The arguments should be well-scoped and understandable, and their structures should be as simple as possible. Follow the typical convention of parameter names in lowercase and use underscores as the word separator.

Document and test your custom modules: Every custom module should include examples, explicitly document dependencies, and describe return responses. New modules should be tested thoroughly before releasing. You can create roles and playbooks to test your custom modules and different test cases.

Key Points

We deep-dived into Ansible modules and examined their use and functionality in detail. We discussed best practices and showed practical examples of leveraging the most commonly-used modules. Lastly, we went through a complete example of developing a custom module.

Thank you for reading, and I hope you enjoyed this article as much as I did.

Top comments (0)