Automating ACI using Ansible
This section contains several examples automating Application Centric Infrastructure (ACI) with Ansible. Whilst this example is not part of the Nexus-as-Code data model, it offers more seasoned Ansible users several comprehensive examples to provision ACI resources.
The repository can be found here: https://github.com/datacenter/Ansible-recipes-for-ACI
This repository contains a series of Ansible playbooks you can use with Cisco ACI. The playbooks make use of native ACI Ansible modules as much as possible. They are kept as simple as possible to make them easy to understand and reuse in your environment. Example playbooks you can find here include:
- Complete configuration of interface, interface policies, VLAN pools, domains
- Comprehensive multi-tier tenant configuration
- Different types of L3outs
- Configuration of common features such as Network Time Protocol (NTP), Domain Name Services (DNS), Border Gateway Protocol (BGP) Route Reflector
The Cisco ACI Ansible collection
Ansible interacts with ACI by using the ACI REST API over HTTP/HTTPS. Two methods for authenticating against ACI fabrics exist.
- Username/password combination
- Signature-based transactions (with per-user X.509 certificate)
Each authentication method has direct consequences over the client to APIC connection behaviour. This section will explore some of the theory as to how this is handled, and what the implications are when writing playbooks.
As of Ansible 2.9+, vendor-specific modules moved out of Ansible core in to collections
. The Cisco ACI collection is required with Ansible 2.9+ to automate Cisco ACI. This is fully open-source and can be found at the following location: https://github.com/CiscoDevNet/ansible-aci. The collection has also been upstreamed for easier installation at https://galaxy.ansible.com/cisco/aci. This allows for simple installation by entering ansible-galaxy collection install --upgrade cisco.aci
This section explores some of differences between authentication mechanisms. In order to measure the execution time of each task in a playbook, configure callbacks_enabled = profile tasks
in ansible.cfg
. Note that this requires ansible.posix.
Consider the following playbook:
---
- name: create N tenants with a loop
hosts: dot34
connection: local
vars:
aci_creds: &aci_login
hostname: '{{ inventory_hostname }}'
username: '{{ ansible_user }}'
password: '{{ ansible_password }}'
validate_certs: no
use_proxy: no
myState: "present"
tasks:
- name: tenant loop
aci_tenant:
<<: *aci_login
tenant: "{{ 'tenantLoopUser-%02x' | format(item) }}"
description: performance testing
state: '{{ myState }}'
loop: "{{ range(1,6) }}"
This playbook creates 5 distinct tenants. Each loop item results in one unique aaaLogin
sequence to APIC. Each tenant creation also results in two GET
requests. One before creating anything to determine whether the tenant already exists. And one after creating the tenant to check if the create operation succeeded.
PLAY [create N tenants with a loop]
TASK [tenant loop hostname={{ inventory_hostname }}, username={{ ansible_user }}, password={{ ansible_password }}, validate_certs=False, use_proxy=False, tenant={{ 'tenantLoopUser-%02x' | format(item) }}, description=performance testing, state={{ myState }}] ***
Thursday 06 April 2023 08:17:23 +0000 (0:00:00.018) 0:00:00.018 ********
changed: [10.48.168.3] => (item=1)
changed: [10.48.168.3] => (item=2)
changed: [10.48.168.3] => (item=3)
changed: [10.48.168.3] => (item=4)
changed: [10.48.168.3] => (item=5)
PLAY RECAP
10.61.124.34 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Thursday 06 April 2023 08:17:26 +0000 (0:00:03.227) 0:00:03.246 ********
===============================================================================
Packet capture showing the post
requests:
Packet capture showing the get
requests:
In summary:
- Each call to an Ansible ACI module results in one
aaaLogin
- Each create or delete operation results in two
GET
requests - This behaviour is by design and is not controllable by configuration settings
Using signature-based authentication when using Ansible with ACI is recommended. But what are signature-based transactions?
- Local users in APIC can carry an optional X.509 certificate (public key) as an attribute
- Using the corresponding private key, clients can sign every API request with SHA-256. A SHA-256 digest prevents man-in-the-middle tampering attempts
More information about how to set-up a local user on APIC and certificate creation for signature-based authentication can be found here.
ACI Ansible modules can perform this task automatically when instructed to do so. When using signature-based transactions, the aaaLogin
method is no longer requred because each transaction carries a unique signature derived from the user's private key. This is sufficient to validate the identity of the client and authenticate that client.
Consider the following playbook:
---
- name: create N tenants with a loop
hosts: dot34
connection: local
vars:
aci_creds: &aci_login
hostname: '{{ inventory_hostname }}'
username: '{{ ansible_user }}'
validate_certs: no
use_proxy: no
private_key: key.pem
myState: "present"
tasks:
- name: tenant loop
aci_tenant:
<<: *aci_login
tenant: "{{ 'tenantLoopUser-%02x' | format(item) }}"
description: performance testing
state: '{{ myState }}'
loop: "{{ range(1,6) }}"
Each request contains a unique, cryptographically secure signature. Without the need for an aaaLogin
for each post
, the performance gain is significant. The greater the key-length, the longer it takes to calculate a unique sha-256 hash. Performance improvements will therefore vary with different key lengths. Comparing with username/password authentication, results up to 4x the execution speed have been observed.
Additional improvements
When the speed at which a playbook executes is extremely significant, additional improvements can be considered to reduce the runtime. With the aci_rest
module it is possible to send raw JSON or XML directly to APIC's REST API. This allows the user to consolidate the request into a single post
.
An example that posts the content of completeTenant.xml
is shown below:
---
- hosts: dot34
gather_facts: no
connection: local
vars:
tasks:
- name: create complete tenant in one push operation
aci_rest:
hostname: '{{ inventory_hostname }}'
username: '{{ ansible_user }}'
private_key: key.pem
validate_certs: no
path: /api/mo/uni.xml
method: post
content: "{{ lookup('file', 'completeTenant.xml') }}"
delegate_to: localhost
More information about the aci_rest
module can be found here: https://docs.ansible.com/ansible/latest/collections/cisco/aci/aci_rest_module.html
The benefit of this approach is that the entire payload is contained in a single post
. This will increase performance significantly, at the trade-off of having to construct the configuration in XML or JSON format.
This approach is used by theaci_bulk_static_binding_to_epg_module Ansible module. Static path bindings may be frequently configured in large amounts, which typically will increase the playbook's runtime. This module precomputes the payload before sending it to APIC's REST API. The merged configuration is then sent in a single post
operation. An example of how to use this module can be found here: Ansible recipes for static bindings in bulk.