10 best Ansible modules for infrastructure as code
10 (plus a bonus) Ansible automation modules that anyone—from a beginner to a power user—can leverage to transform their network infrastructure to code.
In 2017, I emailed several colleagues about a new discovery: Ansible modules. I had stumbled upon Ansible, a new open-source network automation tool from Red Hat. I thought it could help us solve some particularly complex and large-scale problems.
Was I ever right! Since then, I’ve gone on to write hundreds of playbooks to automate everything from enterprise networks to compute and storage, and even entire clouds.
Below are my top 10 (plus a bonus) recommended Ansible modules that anyone—from a beginner to a power user—can leverage to transform their infrastructure to code.
But first, what is infrastructure as code when it comes to Ansible modules?
Infrastructure as code is a modern approach to managing networks, compute, storage, load-balancing, firewalls, and appliances (like BlueCat Address Manager). Unlike traditional infrastructure management, it relies on a computer programming approach instead of a command-line or graphical user interface.
This matters because all modern—or, at the very least, next-generation—infrastructure comes equipped with a REST API. The ability to program infrastructure using a REST API allows for the maturity of the software development lifecycle. This offers numerous advantages, including:
- version and source control;
- a central source of truth; and
- the ability to build, release, and monitor using Docker container images in a continuously integrated (CI) and continuously delivered (CD) pipeline.
Note: It’s easy to try the latest Ansible automation platform for free. To proceed with these recommended modules, you will need to have Ansible installed using their pip package manager. Their complete installs package can be found in their helpful Installing Ansible guide. In addition, while there is an Ansible config file, you typically don’t need to modify it as a beginner.
No. 10: Vars prompt
One of the first steps in any Ansible playbook is to establish secure connectivity to a target host. Regardless of the connection type—be it SSH, HTTPS, or WinRM—the playbook needs to authenticate with a set of credentials. The first mistake engineers tend to make—often to expedite development or out of pure ignorance—is to hard-code their credentials into their group_vars or directly in their playbook.
---
# Network provider
ansible_connection: network_cli
ansible_network_os: ios
ansible_user: admin
ansible_ssh_pass: MyPa$$W0rd123
The problem with this approach is that infrastructure as code should be stored in a version or source control system like Git. However, this means you are storing highly secure credentials in plain text for all to see inside the Git repository. Not good.
Instead, we can use the vars_prompt module to prompt for input from the user.
This has several advantages, including not hard-coding your credentials. It also allows for collaboration, as every user can provide their own credentials at run-time.
# IOS
- hosts: IOS
vars_prompt:
- name: IOS_User_Prompt
prompt: “Enter Cisco IOS Username”
private: no
- name: IOS_Password_Prompt
prompt: “Enter IOS Password”
private: yes
# Register Username and Password
- set_fact:
ios_user: “{{ IOS_User_Prompt }}”
ios_password: “{{ IOS_Password_Prompt }}”
---
# Network provider:
ansible_connection: network_cli
ansible_network_os: ios
ansible_user: “{{ hostvars[inventory_hostname][‘ios_user’] }}”
ansible_ssh_pass: “{{ hostvars[inventory_hostname][‘ios_password’] }}”
For further reading, Ansible’s interactive prompts guide is helpful.
No. 9: Shell
The ability to interact with the Linux host the playbook is running on opens up operating system-level commands and control from within a playbook. Any Linux command you can think of can be executed from inside your playbook. I like to use this ability to automate the Git actions at the end of a playbook.
---
# Git Add / Commit / Push
- hosts: localhost
tasks:
- set_fact:
git_message: “Automated Release Update”
- name: Git - Add
shell: “git add ../*”
register: gitadd
- name: Git - Commit
shell: “git commit -am “{{ git_message }}”
register: git commit
- name: Git - Push
shell: “git push”
register: git push
For further reading, Ansible’s guide to execute shell commands on targets is helpful.
No. 8: Set fact
A key component of infrastructure as code is the ability to work with variables. The ability to set facts, or create your own variables, is extremely powerful and a key differentiator from the legacy manual methodologies of days past. In the previous example, the git_message variable was defined, or set as a fact, with a hard-coded string (“Automated Release Update”).
It’s important to note that you are not limited to just hard-coded values. You can set facts from other facts or data retrieved from devices. In the following example, I am parsing JSON and creating a variable called device_entity with the results from the JSON_Query.
# Set Device Fact
- name: Set Device Fact
set_fact:
device_entity: “{{ device_id.json.queryResponse.entityID[0][‘$’] }}”
For further reading, Ansible documentation on set host facts from a task, using variables, and filters and JSON_query is helpful.
No. 7: Register
Continuing with the theme of variables, the ability to register data into a variable is slightly different than setting a one-time fact. For example, when you iterate over a loop, if you use set_fact, only the data from the last iteration of the loop would end up inside your variable. This can often lead to confusion and false positive development issues. Namely, where is the rest of the data from all the other loops?
Register has you covered. It allows you to, well, register the results from each loop into a dynamic flexible variable. For example, you can get the networks from the BlueCat Address Manager REST API registered into a variable.
# GET IP Network #
# Replace BAM with your BAM IP or Hostname #
- name: GET Network Details
uri:
url: http://BAM/Services/REST/v1/searchByObjectTypes?count=100&start=0&types=IP4Network&keyword=*
method: GET
headers:
Authorization: "{{ token }}"
Cache-Control: no-cache
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Accept: application/json
return_content: yes
validate_certs: no
force_basic_auth: yes
follow_redirects: yes
register: network
To create a variable inside another task from received data, you also need to register it. Here is a simple example using the ios_command module where I want to register the response into a variable.
# Run show mac address-table and register results #
- name: show mac address-table
ios_command:
commands:
- show mac address-table
register: mac_table_raw
The key difference here is that set_fact is a stand-alone task all on its own that creates a variable while register creates a variable with the results of another task. Pay close attention to this if you are new to Ansible.
For further reading, Ansible’s guide on registering variables, as well as my BlueCat Address Manager Network Facts playbook on GitHub, are helpful.
No. 6: Copy
The ability to orchestrate and automate file manipulation is a powerful capability. If you read the documentation for this module, it may seem like copying a source file to a local or remote destination is all this module can do. So, how did it make it so high up on this list?
Sure, you can copy files, as seen in this example:
- name: Copy file with owner and permissions
ansible.builtin.copy:
src: /srv/myfiles/foo.conf
dest: /etc/foo.conf
owner: foo
group: foo
mode: '0644'
But what I like to do with the copy module is create files from the variables the set_fact and register modules have created.
A good example of this, and the next step in the BlueCat Address Manager Network Facts playbook, is to take the registered variable and create a nice (pretty) JSON file from the data. I am using the Ansible filter | to_nice_json to achieve this.
# Create JSON file with Ansible Facts #
- name: Create raw JSON file
copy:
content: |
{{ network | to_nice_json }}
dest: ../documentation/BAM/Network_Facts_Nice.json
Which is now in the Git repository and looks like this:
"json": [
{
"id": 100899,
"name": "sample",
"properties": "CIDR=10.10.88.0/24|allowDuplicateHost=disable|inheritAllowDuplicateHost=true|pingBeforeAssign=disable|inheritPingBeforeAssign=true|gateway=10.10.88.1|inheritDefaultDomains=true|defaultView=100893|inheritDefaultView=true|inheritDNSRestrictions=true|",
"type": "IP4Network"
}
]
And since I am partial to reading YAML, I also create another file using the | to_nice_yaml filter.
# Create YAML file with Ansible Facts #
- name: Create raw YAML
copy:
content: |
{{ network | to_nice_yaml }}
dest: ../documentation/BAM/Network_Facts.yml
Which, respectively, looks like this:
json:
- id: 100899
name: sample
properties: CIDR=10.10.88.0/24|allowDuplicateHost=disable|inheritAllowDuplicateHost=true|pingBeforeAssign=disable|inheritPingBeforeAssign=true|gateway=10.10.88.1|inheritDefaultDomains=true|defaultView=100893|inheritDefaultView=true|inheritDNSRestrictions=true|
type: IP4Network
- id: 100907
name: sample02
properties: CIDR=10.10.100.0/24|allowDuplicateHost=disable|inheritAllowDuplicateHost=true|pingBeforeAssign=disable|inheritPingBeforeAssign=true|gateway=10.10.100.1|inheritDefaultDomains=true|defaultView=100893|inheritDefaultView=true|inheritDNSRestrictions=true|
type: IP4Network
For further reading, the Ansible guides on copy files to remote locations and playbook filters are helpful.
No. 5: Command and configuration
The most sought-after network automation capability is, obviously, making configuration changes and running orchestrated commands at scale. Almost every IT platform available now has either Ansible or vendor-provided command and configuration modules to programmatically alter configurations.
Command modules—for example on a Cisco Catalyst platform running Internetwork Operating System (OS)—have ios_command modules that allow you to execute privileged exec commands at scale. A good example of this, and a non-disruptive yet highly valuable playbook, could be to perform a copy running-config startup-config command writing the memory. This is usually added to the end of any playbooks that have altered the configuration of the device and when you need to save the changes to memory.
# Copy Run Start
- name: Write Mem
ios_command:
commands:
- “copy run start\n\n”
Configuration modules go a step further and enter the configuration terminal level (conf t) on the platform and issue configuration commands. For example, if we wanted to configure an IP helper-address on a switch virtual interface (SVI):
# Configure IP Helper Address on Interface VLAN 10
- name: Update SVI IP Helpers
ios_config:
lines:
- ip helper-address 192.168.1.1
parents: interface vlan 10
The examples above are simple but that is kind of the point. You can run any command or configuration you want, at scale, in any order you want, on basically any platform available.
The BlueCat Ansible module extends Ansible to allow for command and configuration within the BlueCat platform.
For further reading, Ansible’s guides to run commands on remote devices running Cisco IOS and manage Cisco IOS configuration sections are helpful.
No. 4: Vault
The ability to prompt for inputs such as credentials is very useful. However, it does come with limitations and will only get you so far. After all, this is supposed to be automation, right? But prompts require human interaction.
If you are ever going to evolve and containerize your simple Ansible playbooks into full-fledged CI/CD in a fully automated pipeline, you will need to re-factor any vars_prompt credential handling.
Fortunately, there is a way that allows you to securely hard code your credential strings inside what is known as an Ansible vault. It is very versatile in what it can encrypt. My particular use case is to encrypt my credentials so I can store them safely inside my Git repository.
ansible-vault encrypt_string 'string' --name 'variable_name'
After running this command, you will be prompted for a secret. You can either provide it at playbook runtime to decrypt the strings or—and this is the key to full automation—you can store the key inside a plain text file available to the playbook. This allows for hands-free automation and containerization in which you can use Docker mount to mount the decryption file to unlock the credentials.
Ultimately, your strings will look like this:
ansible_ssh_pass: !vault |
$ANSIBLE_VAULT;1.1;AES256
{{ a huge string of numbers here }}
For further reading, Ansible’s guides on Ansible vault and encrypting content with Ansible vault are helpful.
No. 3: Facts and information
The Ansible modules that made my top three may come as a bit of a surprise to you. The ability to gather facts and information—you may be wondering how this seemingly simple capability is higher on the list than configuration modules.
But think about how often the network is in a steady-state that you want to document, monitor, validate, and understand versus how often you are affecting change on the network.
Also, I believe that digital information is the new oil and worth a lot. This is particularly true for important information from the network such as the state of DNS. Enterprises can actually profit from these resulting digital insights as well, either with operational excellence, commoditizing the data, or adjusting business flows.
These Ansible modules are also safe and the best entry point for enterprises and individuals to start their infrastructure-as-code automation journey. Depending on the platform, these modules go by a variety of different names. But they all achieve the same end: Collecting stateful facts and information and returning the data as structured JSON.
A note on platforms
Windows and Linux platforms use the setup module while VMware has a variety of _info modules (guest_info, host_info, etc.). Cisco has devoted ios_facts, nxos_facts, and even asa_facts modules. In the cloud, Azure and AWS have dozens of _info modules.
BlueCat has implemented a collection of RESTful get_ modules that achieve the same goals. For example, get_configurations.yml returns an Address Manager entity for each configuration in Address Manager. The BlueCat Ansible module includes a series of standalone playbooks.
Using these simple Ansible modules, fully automated, fully scaled, real-time, network-state documentation can be stored in a real enterprise-grade source of truth.
For further reading, Ansible’s guides on gather facts about remote hosts (Windows and Linux), collect facts from remote devices running Cisco IOS, get facts about NX-OS switches, collect facts from remote devices running Cisco Adaptive Security Appliance (ASA), Microsoft Azure, and Amazon Web Services; Ansible’s collection of community content on VMware; and my blog post about infrastructure as code, are helpful.
No. 2: Template
The most difficult decision I had to make was if the template module was my favorite module or not. It was close. But this extremely powerful module, which opens Ansible up to a Python-powered templating language, Jinja2, ended up with the silver medal.
Jinja2 lets you create templates—just like a Microsoft PowerPoint or Word template or any other cookie-cutter template—that, in turn, create other file types. Jinja2 lets you apply logic, such as If, Else, End If, and even For Loops. And it allows you to plug in variable-data to plug dynamic data into your files.
Using the template module in both directions
I’ve used the template module in both directions—both for template data I want to send to a device, such as a configuration, as well as for template data I’ve received from a device (typically the JSON).
For example, first I can generate a full IOS configuration for my network core using a Jinja2 template:
# Generate Core Config
- name: Generate Core Config
template:
src: CampusCoreCFG.j2
dest: ../../intended_config/CAMPUS/CORE/{{ inventory_hostname }}.cfg
In which, for example, the VLAN configuration stanzas look like this in the CampusCoreCFG.j2 file:
{% if host_vlans is defined %}
{% for host_vlan in host_vlans %}
vlan {{ host_vlan }}
Name {{ host_vlans[host_vlan].name }}
{% endfor %}
{% endif %}
Which look like this in the Core.cfg file generated by the template:
vlan 2
name BLUE_VLAN
vlan 3
name RED_VLAN
vlan 6
name GREEN_VLAN
Now, using the earlier ios_config module, we can simply push the templated intended configuration file to the device:
# Push Templated Config to Core
- name: Push Config to Core
ios_config:
src: ../../intended_config/CAMPUS/CORE/{{ inventory_hostname }}.cfg
Conversely, with received facts, I also pass the received data through a Jinja2 template to create both a comma-separated value (CSV) spreadsheet file and a markdown file with the JSON data. Yes, using the same template!
# Gather Ansible Facts about Core
- name: Gather Ansible Facts about Core
ios_facts:
gather_subset:
- all
# Create CSV and Markdown file with results #
- name: Create CSV and Markdown
template:
src: ../../../roles/campus_core/templates/CiscoCoreFacts.j2
dest: ../../../FACTS/CAMPUS/CORE/{{ inventory_hostname }}/IOS_facts.{{ item }}
loop:
- csv
- md
The benefits of templating
As you can see, the template uses the Ansible loop iteration and generates either a .csv or .md file depending on the value. Some basic logic and valid output file type formatting (be careful with your markdown, as it’s very strict) result in two files: the CSV and the markdown.
Templating is fast—much faster than using the lineinfile module. It also allows you to keep the logical complexity inside the templates in Pythonic syntax, which is easier to write and maintain than inside the Ansible playbook as YAML.
For further reading, Ansible’s guides on templating (Jinja2) and template a file out to a target host, as well as my blog post about moving to Jinja2 for automated documentation, are helpful.
No. 1: URI
My top Ansible module is the URI module. I chose this module because I firmly believe the future of IT and the backbone of infrastructure as code is the REST API. Any modern appliance, including BlueCat Address Manager, as well as all next-generation network infrastructure and the Cloud, is driven by the RESTful API.
Typically, I will get any given API CRUD (Create, Read, Update, Delete) activity in Postman first to get it working. Once I have my API authentication, headers, and body working in Postman, I port over the code into the Ansible URI module to take that single request and automatically scale it up.
Demonstrating this approach using the BlueCat Address Manager REST API
We did a demonstration of this approach, live, using the BlueCat Address Manager REST API:
Authentication is handled with a preliminary REST API call to get a valid token:
# Get RAW Token #
# Replace "BAM" with your BAM IP or Hostname #
- name: Get Token
uri:
url: http://BAM/Services/REST/v1/login?username=*7B*7B__;JSU!!LGx5ln-YXE48mET8!L5I4_u6rikbdtDBGeHhHAb3_nMZlH3ihGuUavBe9Ay2b7ikHSn5NXyjjL52yTlmPwD9alQ$ username }}&password={{ password }}
headers:
Accept-Encoding: gzip, deflate, br
Accept: application/json
Content-Type: application/x-www-form-urlencoded
User-Agent: Jakarta Commons-HttpClient/3.1
host: cloudsso.cisco.com
Connection: keep-alive
Content-Length: 0
method: GET
return_content: yes
validate_certs: no
force_basic_auth: yes
follow_redirects: yes
body_format: json
register: rawtoken
Once we have a token, we can then pass it along to the BlueCat Address Manager APIs. In this example, I use the searchByObjectType API:
# GET IP Network #
# Replace BAM with your BAM IP or Hostname #
- name: GET Network Details
uri:
url: http://BAM/Services/REST/v1/searchByObjectTypes?count=100&start=0&types=IP4Network&keyword=*__;Kg!!LGx5ln-YXE48mET8!L5I4_u6rikbdtDBGeHhHAb3_nMZlH3ihGuUavBe9Ay2b7ikHSn5NXyjjL52yTlmMiflD1A$
method: GET
headers:
Authorization: "{{ token }}"
Cache-Control: no-cache
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Accept: application/json
return_content: yes
validate_certs: no
force_basic_auth: yes
follow_redirects: yes
register: network
You can also learn how to use Ansible and Postman or Gateway to get network information from BlueCat Address Manager. Here’s a screenshot from BlueCat Address Manager Network Facts, my Ansible playbook on GitHub that uses the BlueCat Address Manager API.
So far, I have used this module to automate:
- BlueCat Address Manager
- Cisco.com
- Serial Number 2 Information API
- Recommended Software Release
- Product Security Incident Response Team (PSIRT)
- Cisco DCNM
- Cisco Catalyst 3850 / 9300
- Cisco NXOS
- Cisco Umbrella
- Cisco Security Manager
- F5
Cisco APIs
Cisco has several APIs to get information returned in JSON format. Using the Ansible URI module, you can access these APIs, query the JSON output, and create formatted CSV files. You can even check against Cisco’s PSIRT REST API for security vulnerabilities.
My Cisco Services APIs Ansible Playbooks is an open-source package available on GitHub. These playbooks capture serial and process identification numbers and send them to Cisco.com’s APIs, transforming the response into business-ready documents.
Anything with a RESTful API can be automated! For further reading, Ansible’s guide on interacts with webservices, RedHat’s article on using Ansible to interact with web endpoints, are helpful.
Bonus: Debug
Debug allows you to interact with your terminal during playbook execution and is extremely handy for troubleshooting or development. You can print messages to yourself as a task in your playbook by using debug to send messages to yourself (“Your playbook got here!”). I like to print variables to the screen to check their contents.
I can add this debug as a task, for example, to print to the screen the registered variable network from the URI module calls.
# GET IP Network #
# Replace BAM with your BAM IP or Hostname #
- name: GET Network Details
uri:
url: http://BAM/Services/REST/v1/searchByObjectTypes?count=100&start=0&types=IP4Network&keyword=*__;Kg!!LGx5ln-YXE48mET8!L5I4_u6rikbdtDBGeHhHAb3_nMZlH3ihGuUavBe9Ay2b7ikHSn5NXyjjL52yTlmMiflD1A$
method: GET
headers:
Authorization: "{{ token }}"
Cache-Control: no-cache
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Accept: application/json
return_content: yes
validate_certs: no
force_basic_auth: yes
follow_redirects: yes
register: network
- debug:
msg=”{{ network }}”
(You could also filter this | to_nice_json.)
For further reading, Ansible’s guide on print statements during execution is helpful.
Building an automated solution with Ansible really is as simple as picking and choosing the right modules and then organizing and orchestrating the tasks you want these Ansible modules to perform. They are all very easy to use and well-documented. There are many real-world examples available on GitHub and other places, my blog included.
Now that you have these 11 Ansible modules, I hope that you have a sense of the type of network automation and orchestration available to you. I wish you the best of success with your journey into infrastructure as code. I also hope you find these Ansible modules useful, and that if you find more good stuff, that you share it with the rest of our networking community in Network VIP on Slack.
You can also read more from me about other network automation tools and eight network automation tasks for anyone to get started.