Introduction

In the fast-paced world of networking, automation has become a crucial element in optimising efficiency and reducing manual effort. One prominent player in the networking space is F5 BIG-IP, a powerful application delivery controller. Pairing it with Ansible, a powerful automation tool, can unlock a whole new level of network automation capabilities. In this blog post, we will explore the benefits and techniques of automating F5 BIG-IP using Ansible.

Introducing Ansible for Network Automation

Ansible is an open-source automation tool that provides a simple and efficient way to automate IT infrastructure. With its declarative language and agentless architecture, Ansible makes it easy to automate F5 BIG-IP tasks. It allows network administrators to define the desired state of their infrastructure and automatically brings it into compliance.

Setting Up the Ansible Environment

Before diving into F5 BIG-IP automation with Ansible, it’s essential to set up the necessary environment. Following official Ansible documentation will guide you through the installation steps based on your platform.

Ansible Official Documentation

Testing Environment

  • F5 BIG-IP VM Appliance Version - 13.1.5.1
  • Ansible Controller OS - Red Hat Enterprise Linux release 9.3 (Plow)
  • Ansible Core Version - 2.15.6
  • Python Version - 3.11

Creating Ansible Playbooks for F5 BIG-IP Automation

Ansible playbooks are at the heart of automating F5 BIG-IP tasks. In this section, we will explore the structure and components of Ansible playbooks tailored specifically for F5 BIG-IP automation.

Ensure your controller is installed with F5 Ansible collection. More Info

ansible-galaxy collection install f5networks.f5_modules ./collection

To further enhance the reusability and modularity of F5 BIG-IP automation, Ansible roles can be employed. Create a Ansible role as f5-bigip (Any preferred role name can be used for Ansible).

ansible-galaxy init ./roles/f5-bigip

When you create the Ansible role, you’ll find the folder structure as follows.

❯ tree roles
roles
└── f5-bigip
    ├── defaults
    │   └── main.yml
    ├── files
    ├── handlers
    │   └── main.yml
    ├── meta
    │   └── main.yml
    ├── tasks
    │   └── main.yml
    ├── templates
    └── vars
        └── main.yml
9 directories, 5 files

We are now ready to start developing Ansible playbooks to automate F5 BIG-IP. The first thing we need to do is test connectivity between F5 and Ansible controller.We need to specify these connection parameters in order to establish a connection with the remote device. At the time of this writing, the F5 Ansible modules communicate almost exclusively over the REST API of the F5 device and this is accomplished by using a provider pattern.

In order to do this, update roles/vars/main.yml as below.

---
# vars file for f5-bigip
ansible_connection: local
f5_provider:
  password: {update the f5 user account here}
  server: {update the f5 user account here}
  user: {update the f5 user account here}
  validate_certs: False
  server_port: 443

The connection provider has now been set up, and now we need to create the Ansible role tasks.

In this Ansible role, we will cover the following workflow,

Validate the node, virtual server and pool existence → Create new nodes → Create pool → Add nodes to the created pool → Create virtual server → Attach pool to the virtual server.

To separate each task in the Ansible role, I have the following YAML files within the roles/tasks folder:

    ├── tasks
    │   ├── add_members_to_pool.yml
    │   ├── add_nodes.yml
    │   ├── add_virtual_server.yml
    │   ├── create_pool_list.yml
    │   ├── f5_gather_fact_nodes.yml
    │   ├── f5_gather_fact_vip_pool.yml
    │   └── main.yml

Validate Nodes Existence

Here, I have added the following tasks to f5_gather_fact_nodes.yml in order to validate nodes’ existence using the bigip_device_info module.

---
- name: Collect BIG-IP LTM Pool, Nodes and Virtual Servers Information
  bigip_device_info:
    gather_subset:
      - "nodes"
    provider: "{{ f5_provider }}"
  delegate_to: localhost
  register: info_out 

- name: Set the query with replacing with {{ item.name }} variable
  set_fact:              
    node_query: "ansible_net_nodes[?name=='{{ item.name }}'].name"

- name: Set the node name 
  set_fact:
    node_name: 
        -  "{{ info_out.ansible_facts| json_query ( node_query ) | first }}"

- name: Print node name 
  ansible.builtin.debug:
    msg: 
     - "Node name {{ node_name[0] }} is already exist"

Validate Pool and Virtual Server Existence

The following tasks have been added to f5_gather_fact_vip_pool.yml to validate the existence of the pool and virtual server.

---
- name: Collect BIG-IP LTM Pool, Nodes and Virtual Servers Information
  bigip_device_info:
    gather_subset:
      - "ltm-pools"
      - "virtual-servers"
    provider: "{{ f5_provider }}"
  delegate_to: localhost
  register: info_out 

- name: Set the query with replacing with variable
  set_fact:              
    pool_query: "ansible_net_ltm_pools[?name=='{{ pool_name }}'].name"
    vip_name_query: "ansible_net_virtual_servers[?name=='{{ vip_name }}'].name"

- name: Set the fact with replacing with {{ pool_name }} and {{ vip_name }} variable
  set_fact:
    pool_name_status: 
      -  "{{ info_out.ansible_facts| json_query ( pool_query ) | first }}"
    virtual_ip_name: 
      -  "{{ info_out.ansible_facts| json_query ( vip_name_query ) | first }}"

- name: Print virtual server,and pool name
  ansible.builtin.debug:
    msg: 
     - "Pool name {{ pool_name_status }} is already exist"
     - "Virtual Server name {{ virtual_ip_name }} is already exist"

Create Multiple Nodes

The next step is to create the pool members in your BIG-IP configuration once the validation is complete. Here, I have added the following tasks to add_nodes.yml to create multiple nodes with a block and rescue controller. A rescue block specifies tasks that will run when an earlier task fails in a block. It is similar to the way many programming languages handle exceptions. Here again, I have used the bigip_device_info module for the second validation, and if the validation fails, the bigip_node module will create all pool members.

---
- block:
  - name: Collect BIG-IP LTM Nodes information
    bigip_device_info:
      gather_subset:
        - "nodes"
      provider: "{{ f5_provider }}"
    delegate_to: localhost
    register: info_out 

  - name: Set the query with replacing with {{ item.name }} variable
    set_fact:              
      node_query: "ansible_net_nodes[?name=='{{ item.name }}'].name"

  - name: Set the node name 
    set_fact:
      node_name: 
        -  "{{ info_out.ansible_facts| json_query ( node_query ) | first }}"

  - name: Print node name 
    ansible.builtin.debug:
      msg: "Node name {{ node_name[0] }} is already exist"

  rescue:
  - name: Create {{ item.name }} node in the F5
    bigip_node:
      host: "{{ item.host }}"
      name: "{{ item.name  }}"
      provider:  "{{ f5_provider }}"
    delegate_to: localhost
    when: node_name is undefined 

  - name: Print node {{ item.name }} status
    ansible.builtin.debug:
      msg: "The {{ item.name }} node has been created "

Create Pool List

Now, we have to create the pool. Here, I have also followed the same approach as the previous tasks and added the following task to the create_pool_list.yml and used the bigip_pool module to create the pool.

---
- block:
  - name: Collect BIG-IP LTM Pool information
    bigip_device_info:
      gather_subset:
        - "ltm-pools"
      provider: "{{ f5_provider }}"
    delegate_to: localhost
    register: info_out 

  - name: Set the query with replacing with {{ pool_name }} variable
    set_fact:              
      pool_query: "ansible_net_ltm_pools[?name=='{{ pool_name }}'].name"

  - name: Set the pool name 
    set_fact:
      pool_name_status: 
        -  "{{ info_out.ansible_facts| json_query ( pool_query ) | first }}"

  - name: Print pool name 
    ansible.builtin.debug:
      msg: "Node name {{ pool_name_status }} is already exist"

  rescue:

  - name: Create a {{ pool_name }} pool in F5
    bigip_pool:
      provider: "{{ f5_provider }}"
      lb_method: "{{ load_balancing_method }}"
      name: "{{ pool_name }}"
      slow_ramp_time: 120
    delegate_to: localhost

  - name: Print {{ pool_name }} pool status
    ansible.builtin.debug:
      msg: "The {{ pool_name }} pool has been created"

Add Members to the Created Pool

Now that we have created the pool and the nodes for that pool. The next step is to add all created nodes to the pool as members. Here, I have made a separate task in add_members_to_pool.yml using the bigip_pool_member module.

---
- name: Add {{ item.name}} members to the {{ pool_name }} pool
  bigip_pool_member:
    provider: "{{ f5_provider }}"
    description: "webserver {{ item.name }}"
    host: "{{ item.host }}"
    name: "{{ item.name }}"
    pool: "{{ pool_name }}"
    port: "{{ service_port }}"
  delegate_to: localhost

Create Virtual Server and Attach the Pool

I have created separate tasks in the add_virtual_server.yml playbook to create the virtual server and attach the previously created pool to the virtual server. I used the second validation option as before and used the bigip_virtual_server module to create a virtual server in F5.

---
- block:
  - name: Collect BIG-IP LTM Virtual Servers information
    bigip_device_info:
      gather_subset:
        - "virtual-servers"
      provider: "{{ f5_provider }}"
    delegate_to: localhost
    register: info_out 

  - name: Set the query with replacing with {{ vip_name }} variable
    set_fact:              
      vip_name_query: "ansible_net_virtual_servers[?name=='{{ vip_name }}'].name"

  - name: Set the vip name 
    set_fact:
      virtual_ip_name: 
        -  "{{ info_out.ansible_facts| json_query ( vip_name_query ) | first }}"

  - name: Print virtual server name 
    ansible.builtin.debug:
      msg: "Virtual Server name {{ virtual_ip_name }} is already exist"

  rescue:
  - name: Create the {{ vip_name }} vip in F5
    bigip_virtual_server:
      provider: "{{ f5_provider }}"
      description: "{{ description }}"
      destination: "{{ virtual_ip }}"
      name: "{{ vip_name }}"
      pool: "{{ pool_name }}"
      port: "{{ vip_service_port }}"
      snat: Automap
      profiles:
        - http
        - clientssl
    delegate_to: localhost 

  - name: Print the {{ vip_name }} status
    ansible.builtin.debug:
      msg: "The {{ vip_name }} virtual server has been created"

Import Tasks to the main.yaml

Finally, all created playbooks have been imported to the main.yml file under the block and rescue controller. If the validation fails, recuse will execute all tasks under the rescue section and create the nodes, pools and virtual server accordingly. A loop has also been created for the member list within the relevant tasks.

---
- block:
  - name: Gather fact from F5 BIG-IP 
    ansible.builtin.include_tasks: f5_gather_fact_nodes.yml
    loop: "{{ member_list }}"

  - name: Gather fact from F5 BIG-IP 
    ansible.builtin.import_tasks: f5_gather_fact_vip_pool.yml

  rescue:
  - name: Create nodes
    ansible.builtin.include_tasks: add_nodes.yml
    loop: "{{ member_list }}"

  - name: Create a pool List
    ansible.builtin.import_tasks: create_pool_list.yml

  - name: Add members to the Pool
    ansible.builtin.include_tasks: add_members_to_pool.yml
    loop: "{{ member_list }}"

  - name: Create virtual server and attach the pool
    ansible.builtin.import_tasks: add_virtual_server.yml

Setup Default Variables

Now, you must pass all required variables in /roles/defaults/main.yml file as below. The following example uses the following values according to my testing environment. Consequently, you may need to modify it for your particular environment.

# Pool Information
load_balancing_method: ratio-member
pool_name: test-pool

# Member Information
service_port: 80
member_list:
  - host: 10.1.1.1
    name: app01
  - host: 10.1.1.2
    name: app02
  - host: 10.1.1.5
    name: app03

# Virtual Server Information
description: test-vip
virtual_ip: 192.168.1.2
vip_name: test_vip
vip_service_port: 80

Create main.yml to call the Ansible Role

Your Ansible role is now referenced through the main playbook below. In this example, I renamed the main playbooks as f5.yaml.

---
- name: Create F5 LTM Rule
  hosts: localhost
  connection: local
  collections:
    - f5networks.f5_modules
  roles: 
    - f5-bigip

Update your Inventory and ansible.cfg

You can update your inventory and ansible.cfg file according to your Ansible environment. I prefer to have separate configurations for each test I do in my environment. According to this configuration, it refers to the current working directory.

ansible.cfg File

[defaults]
INVENTORY = inventory
command_warnings = False
collections_paths = ./collections/ansible_collections
ansible_python_interpreter= /usr/bin/python3.11
roles_path = ./roles

Inventory File

localhost

Run the Ansible Role

Our Ansible role is now ready to run. In this case, we will run it from the top-level Ansible directory.

❯ tree f5_bigip_automation_with_ansible
f5_bigip_automation_with_ansible
├── README.md
├── ansible.cfg
├── collections
│   └── requirements.yml
├── f5.yml
├── inventory
└── roles
    └── f5-bigip
        ├── defaults
        │   └── main.yml
        ├── files
        ├── handlers
        │   └── main.yml
        ├── meta
        │   └── main.yml
        ├── tasks
        │   ├── add_members_to_pool.yml
        │   ├── add_nodes.yml
        │   ├── add_virtual_server.yml
        │   ├── create_pool_list.yml
        │   ├── f5_gather_fact_nodes.yml
        │   ├── f5_gather_fact_vip_pool.yml
        │   └── main.yml
        ├── templates
        └── vars
            └── main.yml

11 directories, 16 files

Please refer to the following command.

ansible-playbook f5.yml

Below is an example of the output you should see if you followed the above steps correctly.

[dhananjak@ansible-core ansible_projects]$ ansible-playbook f5.yml 

PLAY [Create F5 LTM Rule] *******************************************************************************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Gather fact from F5 BIG-IP] ************************************************************************************************************************************************************************************
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/f5_gather_fact_nodes.yml for localhost => (item={'host': '10.1.1.1', 'name': 'app01'})
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/f5_gather_fact_nodes.yml for localhost => (item={'host': '10.1.1.2', 'name': 'app02'})
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/f5_gather_fact_nodes.yml for localhost => (item={'host': '10.1.1.5', 'name': 'app03'})

TASK [f5-bigip : Collect BIG-IP LTM Pool, Nodes and Virtual Servers Information] ************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the query with replacing with app01 variable] **************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the node name] *********************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: No first item, sequence was empty.. No first item, sequence was empty.\n\nThe error appears to be in '/home/dhananjak/ansible_projects/roles/f5-bigip/tasks/f5_gather_fact_nodes.yml': line 14, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- name: Set the node name\n  ^ here\n"}

TASK [f5-bigip : Create nodes] **************************************************************************************************************************************************************************************************
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_nodes.yml for localhost => (item={'host': '10.1.1.1', 'name': 'app01'})
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_nodes.yml for localhost => (item={'host': '10.1.1.2', 'name': 'app02'})
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_nodes.yml for localhost => (item={'host': '10.1.1.5', 'name': 'app03'})

TASK [f5-bigip : Collect BIG-IP LTM Nodes information] **************************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the query with replacing with app01 variable] **************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the node name] *********************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: No first item, sequence was empty.. No first item, sequence was empty.\n\nThe error appears to be in '/home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_nodes.yml': line 15, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: Set the node name\n    ^ here\n"}

TASK [f5-bigip : Create app01 node in the F5] ***********************************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Print node app01 status] ***************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "The app01 node has been created "
}

TASK [f5-bigip : Collect BIG-IP LTM Nodes information] **************************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the query with replacing with app02 variable] **************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the node name] *********************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: No first item, sequence was empty.. No first item, sequence was empty.\n\nThe error appears to be in '/home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_nodes.yml': line 15, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: Set the node name\n    ^ here\n"}

TASK [f5-bigip : Create app02 node in the F5] ***********************************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Print node app02 status] ***************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "The app02 node has been created "
}

TASK [f5-bigip : Collect BIG-IP LTM Nodes information] **************************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the query with replacing with app03 variable] **************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the node name] *********************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: No first item, sequence was empty.. No first item, sequence was empty.\n\nThe error appears to be in '/home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_nodes.yml': line 15, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: Set the node name\n    ^ here\n"}

TASK [f5-bigip : Create app03 node in the F5] ***********************************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Print node app03 status] ***************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "The app03 node has been created "
}

TASK [f5-bigip : Collect BIG-IP LTM Pool information] ***************************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the query with replacing with test-pool variable] **********************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the pool name] *********************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: No first item, sequence was empty.. No first item, sequence was empty.\n\nThe error appears to be in '/home/dhananjak/ansible_projects/roles/f5-bigip/tasks/create_pool_list.yml': line 15, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: Set the pool name\n    ^ here\n"}

TASK [f5-bigip : Create a test-pool pool in F5] *********************************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Print test-pool pool status] ***********************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "The test-pool pool has been created"
}

TASK [f5-bigip : Add members to the Pool] ***************************************************************************************************************************************************************************************
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_members_to_pool.yml for localhost => (item={'host': '10.1.1.1', 'name': 'app01'})
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_members_to_pool.yml for localhost => (item={'host': '10.1.1.2', 'name': 'app02'})
included: /home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_members_to_pool.yml for localhost => (item={'host': '10.1.1.5', 'name': 'app03'})

TASK [f5-bigip : Add app01 members to the test-pool pool] ***********************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Add app02 members to the test-pool pool] ***********************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Add app03 members to the test-pool pool] ***********************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Collect BIG-IP LTM Virtual Servers information] ****************************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the query with replacing with test_vip variable] ***********************************************************************************************************************************************************
ok: [localhost]

TASK [f5-bigip : Set the vip name] **********************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: No first item, sequence was empty.. No first item, sequence was empty.\n\nThe error appears to be in '/home/dhananjak/ansible_projects/roles/f5-bigip/tasks/add_virtual_server.yml': line 15, column 5, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n  - name: Set the vip name\n    ^ here\n"}

TASK [f5-bigip : Create the test_vip vip in F5] *********************************************************************************************************************************************************************************
changed: [localhost]

TASK [f5-bigip : Print the test_vip status] *************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": "The test_vip virtual server has been created"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************
localhost                  : ok=35   changed=8    unreachable=0    failed=0    skipped=0    rescued=6    ignored=0   

[dhananjak@ansible-core ansible_projects]$ 

F5 Big-IP Web Interface

Nodes

f5_nodes

Pool

f5_pool

Virtual Server

f5_vip

Pool Members

f5_pool_members

Network Map

f5_network_map

Conclusion

In today’s rapidly evolving networking landscape, automation is no longer a luxury but a necessity. By leveraging Ansible for F5 BIG-IP automation, network administrators can unlock the full potential of their infrastructure. From reducing manual effort to ensuring consistency and scalability, this powerful combination empowers technical professionals to focus on strategic initiatives rather than mundane operational tasks. Embrace F5 BIG-IP automation with Ansible and witness the transformation of your network infrastructure

Github Repo

https://github.com/DhananjaK/f5_bigip_automation_with_ansible

Reference