Network Automation: Start of the journey – Automating MikroTik Switches with Ansible
Together with Fabio Bertagna, we share our experiences and insights on the journey toward automating our network infrastructure – from concept to implementation.
While more and more devices are configured using Ansible, certain types in data centers and server rooms remain mostly untouched by automation: network devices. We won’t delve into the reasons why, but in our case, like in many other companies, switches and firewalls were mostly configured manually until recently. This blog post sheds light on our journey towards achieving a fully automated network infrastructure and the lessons we learned along the way.
It all started back in January 2023, when our team decided to develop an OPNsense Collection in order to configure our firewall using Ansible rather than make changes manually (read more about this journey here). Naturally, when the task of evaluating new switches entered our sprint, we prioritized choosing a product that could also be configured with Ansible.
Evaluation and planning
During our evaluation, we considered two main brands:
On the one hand, we looked at a major player in the market – Cisco. On the other hand, we aimed to find a product that offered the best price–performance ratio. We reviewed multiple products and eventually identified a lesser-known, but promising brand: MikroTik.
Shortly after, we decided to prioritize price–performance ratio over market share and began planning our new network stack using MikroTik devices.
On the hardware side we envisioned three distribution switches and seven access switches on multiple floors in our office using LAG.
On the software side, our goal was to configure elements such as switch ports, link aggregations, VLANs, and trunks directly in NetBox, ensuring these configurations could be automatically applied via Ansible.
As our distribution switches, we chose the CRS518-16XS-2XQ-RM, and as our access switches, we selected the CRS354-48G-4S+2Q+RM.
Here you can see a rough overview of our planned topology:
Since MikroTik used the same OS on both distribution and access switches, we were able to use the same Ansible code for both device types.
Implementing an automated network config management
One of our main goals with automation was to have a single source of truth for our network. While the reasons behind this are worth exploring, our focus here is to demonstrate how we achieved said goal.
Back in 2018, Puzzle adopted NetBox as its IP Address Management (IPAM) solution. Their GitHub ‚About‘ section makes an appealing case: „The premier source of truth powering network automation“. Since then, NetBox has evolved impressively and is able to model most of the networks we encounter in our daily operations. And for cases where native functionality falls short, the flexibility to develop plugins using Django comes in handy.
With NetBox as our data source, we leveraged its GraphQL interface and built our new solution in three steps:
- Gather the network inventory for Ansible from NetBox
- Gather device data from NetBox using GraphQL
- Configure the switches using Ansible with GraphQL data.
This three-step process ensures a single, centralized source of truth for our network configuration, minimizing errors and keeping things consistent. Let’s break down each step in more detail.
Building the inventory
Our first step involved adding new switches to NetBox. One way to accomplish this is by creating a new device type and manually configuring properties such as port count, speeds, and other attributes. However, we recommend leveraging this device type library, which includes many common devices, saving time and effort.
Such a device in NetBox could look like this:
NetBox provides many options to model interfaces and networks as precisely as desired – such as LAGs, bridge interfaces, virtual inter-
faces, different speeds as well as 802.1Q configurations, and many more. This is what enables us to implement efficient network configuration automation in the first place. Here you can see an example of a modelled device:
From the Ansible side, we built the inventory using the netbox.netbox.nb_inventory NetBox inventory plugin.
This can be done, for example, by defining the following inventory:
---
# set NETBOX_API and NETBOX_TOKEN env vars
plugin: netbox.netbox.nb_inventory
validate_certs: true
config_context: false
query_filters:
- tag: network-demo
Resulting in an Ansible inventory that could look like this:
$ ansible-inventory --list -i ./demo_inventory.yml
{
"_meta": {
"hostvars": {
"demo-switch": {
"custom_fields": {},
"device_roles": [
"access_switch"
],
"device_types": [
"crs518-16xs-2xq-rm"
],
"is_virtual": false,
"local_context_data": [
null
],
"locations": [],
"manufacturers": [
"mikrotik"
],
"platforms": [
"routeros"
],
"regions": [],
"serial": "",
"services": [],
"site_groups": [],
"sites": [
"sesamstrasse-5"
],
"status": {
"label": "Planned",
"value": "planned"
},
"tags": [
"network-demo"
]
}
}
},
"all": {
"children": [
"ungrouped"
]
},
"ungrouped": {
"hosts": [
"demo-switch"
]
}
}
Consult the nb_inventory Documentation for further details on the supported features of this plugin, such as grouping and filtering.
Getting device data using GraphQL
The next step involved gathering any device-related data that is relevant for the configuration using Ansible. Leveraging the GraphQL endpoint provided by NetBox, we can do that from within an Ansible run using the ansible.builtin.uri module. To request device-specific data we used a Jinja template, which takes the devices inventory_hostname and builds a GraphQL query that can then be sent to NetBox. Here is the GraphQL query template we ended up using:
query {
vlans: vlan_list{
vid
comment: name
tagged: interfaces_as_tagged (filters: {device: "{{ inventory_hostname }}"}){
name
type
bridge{
name
}
}
untagged: interfaces_as_untagged (filters: {device: "{{ inventory_hostname }}"}){
name
type
bridge{
name
}
}
}
device: device_list(filters: { name: { exact: "{{ inventory_hostname }}" } }) {
name
role {
name
}
platform {
name
}
primary_ip4 {
address
}
interfaces {
name
type
mode
speed
enabled
ip_addresses {
address
}
bridge {
name
}
lag {
name
}
member_interfaces{
name
}
bridge_ports: bridge_interfaces {
name
type
mode
bridge {
name
}
tagged_vlans {
vid
name
}
untagged_vlan {
vid
name
}
}
tagged_vlans {
vid
name
}
untagged_vlan {
vid
name
}
}
}
}
At this point we have all device-specific data available in Ansible. It is also worth noting that up until now, our approach was not specific to MikroTik or RouterOS and could be used for automated network configurations using any networking platform.
Configuring RouterOS using Ansible
Having retrieved and processed the necessary data, our final stage involved applying this information to configure the network devices themselves. It’s important to reiterate that while the previous steps are platform-agnostic and applicable to automating configuration management for various network devices, this final step is specific to RouterOS. Because our infrastructure uses MikroTik devices, we implemented a RouterOS-specific configuration process.
To achieve this, we leveraged the community.routeros Ansible collection to create a dedicated Ansible role for MikroTik switch configuration. While a detailed breakdown of the role’s inner workings is beyond the scope of this post, it encompasses a wide range of configuration tasks essential for our network infrastructure. These include:
configuring interface bonds for link aggregation
- setting interface speeds to optimize bandwidth utilization
- establishing MAC bridges for Layer 2 connectivity
- defining and managing VLANs for network segmentation
- assigning IP addresses for device management and intercommunication
- configuring logging for monitoring and troubleshooting
To illustrate how we interact with RouterOS using the community.routeros collection, here are a few examples of how its modules can be used:
- Setting interface speed and disabling auto-negotiation using
api_modify: This ensures consistent link speeds and avoids potential negotiation issues.
- name: Set interface speed and disable autonegotiation
community.routeros.api_modify:
hostname: "{{ ansible_host }}"
username: "{{ router_os_api_user.username }}"
password: "{{ router_os_api_user.password }}"
path: /interface/ethernet
data:
default-name: ether1
speed: 1G-baseT-full
auto-negotiation: "no"
- Setting DNS servers using
commandwith change detection: This example demonstrates how to use thecommandmodule withchanged_whento ensure idempotency. The task only changes when the DNS servers are different from the desired configuration.
- name: Setup dns
community.routeros.command:
commands:
- "/ip dns print"
- "/ip dns set servers=8.8.8.8"
register: dns_command
changed_when: >
'servers: 8.8.8.8' not in dns_command.stdout[0]
These examples show the versatility of the community.routeros collection, allowing us to manage various aspects of our MikroTik switches within our Ansible playbooks. The dedicated Ansible role enables us to consistently and reliably apply our desired configurations across all MikroTik switches, ensuring uniformity and reducing the risk of human error.
Outlook
In the next blog post, we’ll discuss how we connected our new OPNsense instance to the network and used Ansible to configure the majority of the settings.