Automating ACI using Ansible

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.