
Introduction
Ansible is a radically simple IT automation tool. It handles configuration management, application deployment, cloud provisioning, ad-hoc task execution, network automation, and multi-node orchestration. Ansible makes complex changes like zero-downtime rolling updates with load balancers easy.
VMware has Ansible modules for NSX-T and NSX-T Advanced Load balancer (AVI Networks)
This blog assumes you have some basic knowledge of Ansible and NSX-T. I will not go through the whole process of installing Ansible and NSX-T
This is part 2, where I will cover NSX-T Ansible examples.
Part 1 Covers basic configuration and AVI Ansible examples. (can be found here and here)
I am using NSX-T 3.1.2 and AVI 20.1.5.
For Ansible I am using Linux (Ubuntu 20.04 LTS)
The goal of this blog
The goal of this blog is to show an example of how easy it is to use Ansible with NSX-T.
I am only doing a simple task with Ansible to show you how easy it is.
Installation of Ansible roles/collections
Please refer to part 1 (can be found here and here)
Logical overview
Let me describe how I have set up NSX-T.
The setup is pretty straightforward. T0 router is preconfigured with ECMP and BGP to the external routers (This can also be done with ansible if you want, but I will not cover this in this post.)
T1 with T1-SR with four segments connected to it.
Basic Setup
I will describe some small basics in what names and subnets I am using.
I am using the following networks:
Network Name (Segment) | Ip subnet | T1 router | Description/Security rules |
Web-Segment | 10.4.203.0/27 | t1-poc | Any to tcp port 80 |
App-Segment | 10.4.203.32/27 | t1-poc | web to app tcp port 8080 |
poc-vip-segment | 10.4.203.96/27 | t1-poc | AVI SE will use this segment (in Part 3) |
DB-Segment | 10.4.203.64/27 | t1-poc | app to db service mysql |
Lets test Ansible
Let the fun begin 🙂
In the examples I am using I will create:
– T1
– Segments
– Security groups (membership is done with tags.)
– Security rules
I am using a var file with all the definitions I need. The file is named ‘build_topology_vars.yml’
{
"nsx_manager": "10.0.27.31",
"nsx_username": "admin",
"nsx_password": "VMware1!VMware1!",
"validate_certs": "false",
"state": "present",
"tier1_gateways": [
{
"display_name": "t1-poc",
"tier0_display_name": "bf-poc-nxt-tier0",
"route_advertisement_types": ["TIER1_CONNECTED"],
"failover_mode": "PREEMPTIVE",
"tags": [
{
"tag": "ansible",
"scope": "demo"
}
],
"locale_services": [
{
"state": "present",
"id": "t1-poc-sr",
"route_redistribution_config": {
"redistribution_rules": [
"route_redistribution_types": ["TIER1_CONNECTED"]
]
},
"edge_cluster_info": {
"edge_cluster_display_name": "Poc-Edge-T0-only-cluster",
},
"preferred_edge_nodes_info": [
{
"edge_cluster_display_name": "Poc-Edge-T0-only-cluster",
"edge_node_display_name": "bf-poc-nxtedge01"
},
{
"edge_cluster_display_name": "Poc-Edge-T0-only-cluster",
"edge_node_display_name": "bf-poc-nxtedge02"
}
],
}
]
}
],
"segments": [
{
"display_name": "Web-Segment",
"tier1_display_name": "t1-poc",
"tz": "TZ-Poc-Overlay",
"domain_name": "mylab.net",
"subnets": [
{
"gateway_address": "10.4.203.1/27"
}
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
},
{
"tag": "web",
"scope": "east"
}
]
},
{
"display_name": "App-Segment",
"tier1_display_name": "t1-poc",
"tz": "TZ-Poc-Overlay",
"domain_name": "mylab.net",
"subnets": [
{
"gateway_address": "10.4.203.33/27"
}
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
},
{
"tag": "app",
"scope": "east"
}
]
},
{
"display_name": "poc-vip-segment",
"tier1_display_name": "t1-poc",
"tz": "TZ-Poc-Overlay",
"domain_name": "mylab.net",
"subnets": [
{
"gateway_address": "10.4.203.97/27"
}
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
},
{
"tag": "app",
"scope": "east"
}
]
},
{
"display_name": "DB-Segment",
"tier1_display_name": "t1-poc",
"tz": "TZ-Poc-Overlay",
"domain_name": "mylab.net",
"subnets": [
{
"gateway_address": "10.4.203.65/27"
}
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
},
{
"tag": "db",
"scope": "east"
}
]
}
],
# Note: 'group' is a reserved key. Cant use it here.
"mygroups": [
{
"display_name": "web-VMs",
"domain_id": "default",
"expression": [
{
"member_type": "VirtualMachine",
"value": "web",
"key": "Tag",
"operator": "EQUALS",
"resource_type": "Condition"
}
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
},
{
"tag": "web",
"scope": "east"
}
]
},
{
"display_name": "app-VMs",
"domain_id": "default",
"expression": [
{
"member_type": "VirtualMachine",
"value": "app",
"key": "Tag",
"operator": "EQUALS",
"resource_type": "Condition"
}
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
},
{
"tag": "app",
"scope": "east"
}
]
},
{
"display_name": "db-VMs",
"domain_id": "default",
"expression": [
{
"member_type": "VirtualMachine",
"value": "db",
"key": "Tag",
"operator": "EQUALS",
"resource_type": "Condition"
}
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
},
{
"tag": "db",
"scope": "east"
}
]
}
],
"security_policies" : [
{
"display_name": "world-to-web",
"domain_id": "default",
"category": "Application",
"rules": [
{
"display_name": "http",
"description": "Rule for http port",
"sequence_number": 1,
"source_groups": [
"any"
],
"destination_groups": [
"/infra/domains/default/groups/web-VMs"
],
"services": [
"/infra/services/HTTP"
],
"action": "ALLOW",
"scope": [
"/infra/domains/default/groups/web-VMs"
],
},
{
"display_name": "Catch-All",
"description": "Catch All rule",
"sequence_number": 2,
"source_groups": [
"any"
],
"destination_groups": [
"/infra/domains/default/groups/web-VMs"
],
"services": [
"any"
],
"action": "DROP",
"scope": [
"/infra/domains/default/groups/web-VMs"
],
},
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
}
]
},
{
"display_name": "web-to-app",
"domain_id": "default",
"category": "Application",
"rules": [
{
"display_name": "app-port",
"description": "Rule for Application port",
"sequence_number": 1,
"source_groups": [
"/infra/domains/default/groups/web-VMs"
],
"destination_groups": [
"/infra/domains/default/groups/app-VMs"
],
"services": [
"any"
],
"service_entries": [
{
"l4_protocol": "TCP",
"destination_ports": [
"8080"
],
"resource_type": "L4PortSetServiceEntry",
},
],
"action": "ALLOW",
"scope": [
"/infra/domains/default/groups/app-VMs",
"/infra/domains/default/groups/web-VMs"
],
},
{
"display_name": "Catch-All",
"description": "Catch All rule",
"sequence_number": 2,
"source_groups": [
"any"
],
"destination_groups": [
"/infra/domains/default/groups/app-VMs"
],
"services": [
"any"
],
"action": "DROP",
"scope": [
"/infra/domains/default/groups/app-VMs",
],
},
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
}
]
},
{
"display_name": "app-to-db",
"domain_id": "default",
"category": "Application",
"rules": [
{
"display_name": "MySQL",
"description": "Rule for Application port",
"sequence_number": 1,
"source_groups": [
"/infra/domains/default/groups/app-VMs"
],
"destination_groups": [
"/infra/domains/default/groups/db-VMs"
],
"services": [
"/infra/services/MySQL"
],
"action": "ALLOW",
"scope": [
"/infra/domains/default/groups/db-VMs",
"/infra/domains/default/groups/app-VMs"
],
},
{
"display_name": "Catch-All",
"description": "Catch All rule",
"sequence_number": 2,
"source_groups": [
"any"
],
"destination_groups": [
"/infra/domains/default/groups/db-VMs"
],
"services": [
"any"
],
"action": "DROP",
"scope": [
"/infra/domains/default/groups/db-VMs",
],
},
],
"tags": [
{
"tag": "ansible",
"scope": "demo"
}
]
}
]
}
Creating T1
Ansible code to create the T1 and connect it to the T0. The file is named ’02_create_t1_gateway.yml’
---
- hosts: localhost
become: no
vars_files:
- build_topology_vars.yml
collections:
- vmware.ansible_for_nsxt
tasks:
- name: Modify Tier1
nsxt_policy_tier1:
hostname: "{{ nsx_manager }}"
username: "{{ nsx_username }}"
password: "{{ nsx_password }}"
validate_certs: "{{ validate_certs }}"
state: "{{ state }}"
failover_mode: "{{ item.failover_mode }}"
display_name: "{{ item.display_name }}"
tier0_display_name: "{{ item.tier0_display_name }}"
route_advertisement_types: "{{ item.route_advertisement_types }}"
locale_services: "{{ item.locale_services }}"
tags: "{{ item.tags }}"
with_items:
- "{{ tier1_gateways }}"
Creating segments
Ansible code for creating the segments. The file is named ’03_create_segments.yml’
---
- hosts: localhost
become: no
vars_files:
- build_topology_vars.yml
collections:
- vmware.ansible_for_nsxt
tasks:
- name: Modify Segment
nsxt_policy_segment:
hostname: "{{ nsx_manager }}"
username: "{{ nsx_username }}"
password: "{{ nsx_password }}"
state: "{{ state }}"
validate_certs: "{{ validate_certs }}"
display_name: "{{ item.display_name }}"
tier1_id: "{{ item.tier1_display_name }}"
domain_name: "{{ item.domain_name }}"
transport_zone_display_name: "{{ item.tz }}"
subnets: "{{ item.subnets }}"
tags: "{{ item.tags }}"
with_items:
- "{{ segments }}"
Creating groups
Group membership is done with tags
"member_type": "VirtualMachine",
"value": "web",
"key": "Tag",
"operator": "EQUALS",
"resource_type": "Condition"
Ansible code for creating the security group. The file is ’04_create_groups.yml’
---
- hosts: localhost
become: no
vars_files:
- build_topology_vars.yml
collections:
- vmware.ansible_for_nsxt
tasks:
- name: Modify Groups
nsxt_policy_group:
hostname: "{{ nsx_manager }}"
username: "{{ nsx_username }}"
password: "{{ nsx_password }}"
state: "{{ state }}"
validate_certs: "{{ validate_certs }}"
domain_id: "{{ item.domain_id }}"
display_name: "{{ item.display_name }}"
expression: "{{ item.expression }}"
tags: "{{ item.tags }}"
with_items:
- "{{ mygroups }}"
Creating security rules
Ansible code for creating the security rules. The file is ’05_create_security_policy.yml’
---
- hosts: localhost
become: no
vars_files:
- build_topology_vars.yml
collections:
- vmware.ansible_for_nsxt
tasks:
- name: Modify Security Policy
nsxt_policy_security_policy:
hostname: "{{ nsx_manager }}"
username: "{{ nsx_username }}"
password: "{{ nsx_password }}"
validate_certs: "{{ validate_certs }}"
state: "{{ state }}"
display_name: "{{ item.display_name }}"
domain_id: "{{ item.domain_id }}"
category: "{{ item.category }}"
rules: "{{ item.rules }}"
tags: "{{ item.tags }}"
with_items:
- "{{ security_policies }}"
Running the Ansible playbooks
Running the code:
$ ansible-playbook 02_create_t1_gateway.yml 03_create_segments.yml 04_create_groups.yml 05_create_security_policy.yml
PLAY [localhost] *********************************************************************************
TASK [Gathering Facts] ***************************************************************************
ok: [localhost]
TASK [Modify Tier1] ******************************************************************************
changed: [localhost] => (item={'display_name': 't1-poc', 'tier0_display_name': 'bf-poc-nxt-tier0', 'route_advertisement_types': ['TIER1_CONNECTED'], 'failover_mode': 'PREEMPTIVE', 'tags': [{'tag': 'ansible', 'scope': 'demo'}], 'locale_services': [{'state': 'present', 'id': 't1-poc-sr', 'route_redistribution_config': {'redistribution_rules': [{'route_redistribution_types': ['TIER1_CONNECTED']}]}, 'edge_cluster_info': {'edge_cluster_display_name': 'Poc-Edge-T0-only-cluster'}, 'preferred_edge_nodes_info': [{'edge_cluster_display_name': 'Poc-Edge-T0-only-cluster', 'edge_node_display_name': 'bf-poc-nxtedge01'}, {'edge_cluster_display_name': 'Poc-Edge-T0-only-cluster', 'edge_node_display_name': 'bf-poc-nxtedge02'}]}]})
PLAY RECAP ***************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
PLAY [localhost] *********************************************************************************
TASK [Gathering Facts] ***************************************************************************
ok: [localhost]
TASK [Modify Segment] ****************************************************************************
changed: [localhost] => (item={'display_name': 'Web-Segment', 'tier1_display_name': 't1-poc', 'tz': 'TZ-Poc-Overlay', 'domain_name': 'mylab.net', 'subnets': [{'gateway_address': '10.4.203.1/27'}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}, {'tag': 'web', 'scope': 'east'}]})
changed: [localhost] => (item={'display_name': 'App-Segment', 'tier1_display_name': 't1-poc', 'tz': 'TZ-Poc-Overlay', 'domain_name': 'mylab.net', 'subnets': [{'gateway_address': '10.4.203.33/27'}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}, {'tag': 'app', 'scope': 'east'}]})
changed: [localhost] => (item={'display_name': 'poc-vip-segment', 'tier1_display_name': 't1-poc', 'tz': 'TZ-Poc-Overlay', 'domain_name': 'mylab.net', 'subnets': [{'gateway_address': '10.4.203.97/27'}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}, {'tag': 'app', 'scope': 'east'}]})
changed: [localhost] => (item={'display_name': 'DB-Segment', 'tier1_display_name': 't1-poc', 'tz': 'TZ-Poc-Overlay', 'domain_name': 'mylab.net', 'subnets': [{'gateway_address': '10.4.203.65/27'}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}, {'tag': 'db', 'scope': 'east'}]})
PLAY RECAP **************************************************************************************
localhost : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
PLAY [localhost] *********************************************************************************
TASK [Gathering Facts] ***************************************************************************
ok: [localhost]
TASK [Modify Groups] *****************************************************************************
changed: [localhost] => (item={'display_name': 'web-VMs', 'domain_id': 'default', 'expression': [{'member_type': 'VirtualMachine', 'value': 'web', 'key': 'Tag', 'operator': 'EQUALS', 'resource_type': 'Condition'}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}, {'tag': 'web', 'scope': 'east'}]})
changed: [localhost] => (item={'display_name': 'app-VMs', 'domain_id': 'default', 'expression': [{'member_type': 'VirtualMachine', 'value': 'app', 'key': 'Tag', 'operator': 'EQUALS', 'resource_type': 'Condition'}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}, {'tag': 'app', 'scope': 'east'}]})
changed: [localhost] => (item={'display_name': 'db-VMs', 'domain_id': 'default', 'expression': [{'member_type': 'VirtualMachine', 'value': 'db', 'key': 'Tag', 'operator': 'EQUALS', 'resource_type': 'Condition'}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}, {'tag': 'db', 'scope': 'east'}]})
PLAY RECAP ***************************************************************************************
localhost : ok=6 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
PLAY [localhost] *********************************************************************************
TASK [Gathering Facts] ***************************************************************************
ok: [localhost]
TASK [Modify Security Policy] ********************************************************************
changed: [localhost] => (item={'display_name': 'world-to-web', 'domain_id': 'default', 'category': 'Application', 'rules': [{'display_name': 'http', 'description': 'Rule for http port', 'sequence_number': 1, 'source_groups': ['any'], 'destination_groups': ['/infra/domains/default/groups/web-VMs'], 'services': ['/infra/services/HTTP'], 'action': 'ALLOW', 'scope': ['/infra/domains/default/groups/web-VMs']}, {'display_name': 'Catch-All', 'description': 'Catch All rule', 'sequence_number': 2, 'source_groups': ['any'], 'destination_groups': ['/infra/domains/default/groups/web-VMs'], 'services': ['any'], 'action': 'DROP', 'scope': ['/infra/domains/default/groups/web-VMs']}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}]})
changed: [localhost] => (item={'display_name': 'web-to-app', 'domain_id': 'default', 'category': 'Application', 'rules': [{'display_name': 'app-port', 'description': 'Rule for Application port', 'sequence_number': 1, 'source_groups': ['/infra/domains/default/groups/web-VMs'], 'destination_groups': ['/infra/domains/default/groups/app-VMs'], 'services': ['any'], 'service_entries': [{'l4_protocol': 'TCP', 'destination_ports': ['8080'], 'resource_type': 'L4PortSetServiceEntry'}], 'action': 'ALLOW', 'scope': ['/infra/domains/default/groups/app-VMs', '/infra/domains/default/groups/web-VMs']}, {'display_name': 'Catch-All', 'description': 'Catch All rule', 'sequence_number': 2, 'source_groups': ['any'], 'destination_groups': ['/infra/domains/default/groups/app-VMs'], 'services': ['any'], 'action': 'DROP', 'scope': ['/infra/domains/default/groups/app-VMs']}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}]})
changed: [localhost] => (item={'display_name': 'app-to-db', 'domain_id': 'default', 'category': 'Application', 'rules': [{'display_name': 'MySQL', 'description': 'Rule for Application port', 'sequence_number': 1, 'source_groups': ['/infra/domains/default/groups/app-VMs'], 'destination_groups': ['/infra/domains/default/groups/db-VMs'], 'services': ['/infra/services/MySQL'], 'action': 'ALLOW', 'scope': ['/infra/domains/default/groups/db-VMs', '/infra/domains/default/groups/app-VMs']}, {'display_name': 'Catch-All', 'description': 'Catch All rule', 'sequence_number': 2, 'source_groups': ['any'], 'destination_groups': ['/infra/domains/default/groups/db-VMs'], 'services': ['any'], 'action': 'DROP', 'scope': ['/infra/domains/default/groups/db-VMs']}], 'tags': [{'tag': 'ansible', 'scope': 'demo'}]})
PLAY RECAP ***************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Verify the deployment
In NSX-T it will look like this
On NSX-T edge the route will show up as a t1c (T1 connected)
T0-SR
bf-poc-nxtedge01(tier0_sr)> get route | find 10.4.203.
t1c> * 10.4.203.0/27 [3/0] via 100.64.96.9, downlink-634, 00:18:30
t1c> * 10.4.203.32/27 [3/0] via 100.64.96.9, downlink-634, 00:18:26
t1c> * 10.4.203.64/27 [3/0] via 100.64.96.9, downlink-634, 00:18:19
t1c> * 10.4.203.96/27 [3/0] via 100.64.96.9, downlink-634, 00:18:23
Conclusion
That concludes part 2.
As expected it works very well. Ansible is a great tool for automation and it works very well for NSX-T
If you have any thoughts or suggestions on this, please let me know in the comments!