Deploying a vSphere Kubernetes Service (VKS) Supervisor with Ansible
Table of Contents
Today’s blog post will focus on how to deploy a vSphere Kubernetes Service Supervisor Cluster using Infrastructure-as-Code (IaC) tools.
First, we will briefly compare Terraform and Ansible in this context, and then we will explore the actual automation workflow implemented with Ansible.
Terraform Considerations
Terraform is widely used for declarative infrastructure management, but certain limitations exist when working with vSphere Supervisor Clusters.
For example, the Tier-0 Gateway, a required parameter for a Supervisor Cluster, is not explicitly configurable in the Terraform provider. Testing environments with multiple Tier-0 Gateways showed that Terraform could not determine which gateway to use, causing deployment failures.
This highlights that while Terraform is powerful for many scenarios, it may not be the best choice when fine-grained control over vCenter or NSX configurations is required.
Implementing Automation with Ansible
To overcome these limitations, the deployment workflow was implemented with Ansible, which allows direct API interactions and full control over the Supervisor Cluster creation process.
Before creating playbooks, API calls against vCenter were tested using the vCenter Developer Center, confirming reliable communication with the vCenter API and providing a solid foundation for automation.
Ansible Playbooks
Two key Ansible playbooks were developed:
- One for deploying the Supervisor Cluster - createSV.yaml
- Another for creating the Namespaces within that cluster - createNS.yaml (loops task_seq_ns.yaml)
Both Playbooks collect their data from a supervisor manifest file: supervisor.yaml
supervisor.yaml
apiVersion: v1
kind: vSphereSupervisorCluster
metadata:
name: 'VKS-Cluster-01'
vsphere_url: 'https://vcenter.example.com'
vsphere_username: '<base64 encoded vsphere user>'
vsphere_password: '<base64 encoded vsphere password>'
spec:
clusterName: 'cluster-01'
storagePolicyName: 'Storage Policy'
tierZeroName: 'example-tier-0'
sizeHint: 'SMALL'
networks:
masterManagementNetwork:
addressRange:
subnetMask: '255.255.255.0'
startingAddress: '10.0.0.10'
gateway: '10.0.0.1'
addressCount: 5
network: 'network-1'
pods:
address: '10.60.0.0'
prefix: '20'
services:
address: '10.70.0.0'
prefix: '24'
ingress:
address: '10.80.0.0'
prefix: '24'
egress:
address: '10.90.0.0'
prefix: '24'
masterNTPServers:
- '10.0.0.1'
masterDNS:
- '10.0.0.1'
workerDNS:
- '10.0.0.1'
networkProvider: 'NSXT_CONTAINER_PLUGIN'
namespaces:
- name: 'namespace-1'
description: 'ansible-created-namespeace'
type: 'USER' #USER or GROUP
user: 'user-1'
domain: 'vsphere.local'
role: 'EDIT' #EDIT or VIEW
storageLimit: '10240'
- name: 'namespace-2'
description: 'ansible-created-namespeace'
type: 'user-2'
user: 'administrator'
domain: 'vsphere.local'
role: 'EDIT' #EDIT or VIEW
storageLimit: '10240'
- name: 'namespace-3'
description: 'ansible-created-namespeace'
type: 'USER' #USER or GROUP
user: 'user-3'
domain: 'vsphere.local'
role: 'EDIT' #EDIT or VIEW
storageLimit: '10240'
createSV.yaml
- name: Bootstrap vSphere Supervisor Cluster
hosts: localhost
vars_files:
- supervisor.yaml
tasks:
- name: Create vSphere Session
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/com/vmware/cis/session'
method: POST
url_username: '{{ metadata.vsphere_username | b64decode }}'
url_password: '{{ metadata.vsphere_password | b64decode }}'
force_basic_auth: true
validate_certs: false
register: session
- name: Show if session failed
ansible.builtin.fail:
msg: 'Login failed. Please provide the correct username + password'
when: session.status != 200
- name: Show status code
ansible.builtin.debug:
msg:
- 'Status_Code: {{ session.status }}'
- 'Session_ID: {{ session.json.value }}'
verbosity: 2
- name: Get Cluster ID
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/vcenter/cluster?filter.names={{ spec.clusterName }}'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: vsphere_cluster
- name: Set fact vSphere Cluster ID
ansible.builtin.set_fact:
vsphere_cluster_id: '{{ vsphere_cluster.json.value[0].cluster }}'
- name: Get Storage Policies
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/vcenter/storage/policies'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: vsphere_storage_policies
- name: Show Storage Policies
ansible.builtin.debug:
var: vsphere_storage_policies
verbosity: 2
- name: Get Storage Policy ID
ansible.builtin.set_fact:
vsphere_storage_policy_id: "{{ (vsphere_storage_policies.json.value | selectattr('name', 'equalto', spec.storagePolicyName ))[0].policy }}"
- name: Show vSphere Storage Policy ID
ansible.builtin.debug:
var: vsphere_storage_policy_id
verbosity: 2
- name: Get Virtual Distributed Switch ID
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespace-management/distributed-switch-compatibility?cluster={{ vsphere_cluster_id }}&compatible=true'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: vsphere_vds
- name: Show Virtual Distributed Switch ID
ansible.builtin.debug:
var: vsphere_vds.json[0].distributed_switch
verbosity: 2
- name: Set fact Virtual Distributed Switch ID
ansible.builtin.set_fact:
vsphere_vds_id: '{{ vsphere_vds.json[0].distributed_switch }}'
- name: Get NSX Edge Cluster ID
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespace-management/edge-cluster-compatibility?cluster={{ vsphere_cluster_id }}&compatible=true&distributed_switch={{ vsphere_vds_id | urlencode }}'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: nsx_edge_cluster
- name: Show NSX Edge Cluster ID
ansible.builtin.debug:
var: nsx_edge_cluster.json[0].edge_cluster
verbosity: 2
- name: Set fact NSX Edge Cluster ID
ansible.builtin.set_fact:
nsx_edge_cluster_id: '{{ nsx_edge_cluster.json[0].edge_cluster }}'
- name: Get Master Network Portgroup
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/vcenter/network?filter.names={{ spec.networks.masterManagementNetwork.network }}'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: vsphere_master_network_portgroup_response
- name: Show Master Network Portgroup ID
ansible.builtin.debug:
var: vsphere_master_network_portgroup_response.json.value[0].network
verbosity: 2
- name: Set fact Master Network Portgroup ID
ansible.builtin.set_fact:
vsphere_master_network_portgroup_id: '{{ vsphere_master_network_portgroup_response.json.value[0].network }}'
- name: Get Tier-0
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespace-management/nsx-tier0-gateways?distributed_switch={{ vsphere_vds_id | urlencode }}'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: nsx_tier_zero_gateways
- name: Set fact Tier-0 Gateway
ansible.builtin.set_fact:
nsx_tier_zero_gateway_id: "{{ (nsx_tier_zero_gateways.json | selectattr('display_name', 'equalto', spec.tierZeroName) | first).tier0_gateway }}"
- name: Show Tier-0 Gateway ID
ansible.builtin.debug:
var: nsx_tier_zero_gateway_id
verbosity: 2
- name: Get Supervisor Cluster
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespace-management/clusters/{{ vsphere_cluster_id }}'
method: GET
validate_certs: false
status_code:
- 200
- 404
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: supervisor_cluster
- name: Show Message if Supervisor Cluster already exists
ansible.builtin.debug:
msg: 'Supervisor Cluster already exists.'
when: supervisor_cluster.status == 200
- name: Create Supervisor Cluster
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespace-management/clusters/{{ vsphere_cluster_id }}?action=enable'
method: POST
validate_certs: false
status_code:
- 200
- 204
headers:
vmware-api-session-id: '{{ session.json.value }}'
body_format: json
body:
image_storage:
storage_policy: '{{ vsphere_storage_policy_id }}'
ncp_cluster_network_spec:
nsx_edge_cluster: '{{ nsx_edge_cluster_id }}'
nsx_tier0_gateway: '{{ nsx_tier_zero_gateway_id }}'
pod_cidrs:
- address: '{{ spec.networks.pods.address }}'
prefix: '{{ spec.networks.pods.prefix }}'
egress_cidrs:
- address: '{{ spec.networks.egress.address }}'
prefix: '{{ spec.networks.egress.prefix }}'
ingress_cidrs:
- address: '{{ spec.networks.ingress.address }}'
prefix: '{{ spec.networks.ingress.prefix }}'
cluster_distributed_switch: '{{ vsphere_vds_id }}'
master_management_network:
mode: 'STATICRANGE'
network: '{{ vsphere_master_network_portgroup_id }}'
address_range:
subnet_mask: '{{ spec.networks.masterManagementNetwork.addressRange.subnetMask }}'
starting_address: '{{ spec.networks.masterManagementNetwork.addressRange.startingAddress }}'
gateway: '{{ spec.networks.masterManagementNetwork.addressRange.gateway }}'
address_count: '{{ spec.networks.masterManagementNetwork.addressRange.addressCount }}'
master_NTP_servers: '{{ spec.networks.masterNTPServers }}'
ephemeral_storage_policy: '{{ vsphere_storage_policy_id }}'
service_cidr:
address: '{{ spec.networks.services.address }}'
prefix: '{{ spec.networks.services.prefix }}'
size_hint: '{{ spec.sizeHint }}'
master_DNS: '{{ spec.networks.masterDNS }}'
worker_DNS: '{{ spec.networks.workerDNS }}'
network_provider: '{{ spec.networks.networkProvider }}'
master_storage_policy: '{{ vsphere_storage_policy_id }}'
when: supervisor_cluster.status != 200
- name: Drop vSphere session
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/com/vmware/cis/session'
method: DELETE
url_username: '{{ metadata.vsphere_username | b64decode }}'
url_password: '{{ metadata.vsphere_password | b64decode }}'
force_basic_auth: true
validate_certs: false
status_code:
- 200
- 401
createNS.yaml
- name: Create Namespaces
hosts: localhost
vars_files:
- supervisor.yaml
tasks:
- name: Create vSphere Session
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/com/vmware/cis/session'
method: POST
url_username: '{{ metadata.vsphere_username | b64decode }}'
url_password: '{{ metadata.vsphere_password | b64decode }}'
force_basic_auth: true
validate_certs: false
register: session
- name: Show if session failed
ansible.builtin.fail:
msg: 'Login failed. Please provide the correct username + password'
when: session.status != 200
- name: Show status code
ansible.builtin.debug:
msg:
- 'Status_Code: {{ session.status }}'
- 'Session_ID: {{ session.json.value }}'
verbosity: 2
- name: Get Cluster ID
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/vcenter/cluster?filter.names={{ spec.clusterName }}'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: vsphere_cluster
- name: Set fact vSphere Cluster ID
ansible.builtin.set_fact:
vsphere_cluster_id: '{{ vsphere_cluster.json.value[0].cluster }}'
- name: Get Storage Policies
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/rest/vcenter/storage/policies'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: vsphere_storage_policies
- name: Show Storage Policies
ansible.builtin.debug:
var: vsphere_storage_policies
verbosity: 2
- name: Set fact Storage Policy ID
ansible.builtin.set_fact:
vsphere_storage_policy_id: "{{ (vsphere_storage_policies.json.value | selectattr('name', 'equalto', spec.storagePolicyName ))[0].policy }}"
- name: Show vSphere Storage Policy ID
ansible.builtin.debug:
var: vsphere_storage_policy_id
verbosity: 2
- name: Get Supervisor Cluster
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespace-management/clusters/{{ vsphere_cluster_id }}'
method: GET
validate_certs: false
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: supervisor_clusters
- name: Loop Namespaces
include_tasks: task_seq_ns.yaml
loop: '{{ spec.namespaces }}'
loop_control:
loop_var: item
task_seq_ns.yaml
- name: Get Namespace
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespaces/instances/{{ item.name }}'
method: GET
validate_certs: false
status_code:
- 200
- 404
headers:
vmware-api-session-id: '{{ session.json.value }}'
register: namespace
- name: Show Message if Namespace already exists
ansible.builtin.debug:
msg: 'Namespace: {{ item.name }} already exisiting.'
when: namespace.status == 200
- name: Create Namespace
ansible.builtin.uri:
url: '{{ metadata.vsphere_url }}/api/vcenter/namespaces/instances'
method: POST
validate_certs: false
status_code:
- 200
- 204
headers:
vmware-api-session-id: '{{ session.json.value }}'
body_format: json
body:
cluster: '{{ vsphere_cluster_id }}'
namespace: '{{ item.name }}'
description: '{{ item.description }}'
access_list:
- role: '{{ item.role }}'
subject_type: '{{ item.type }}'
subject: '{{ item.user }}'
domain: '{{ item.domain }}'
storage_specs:
- limit: '{{ item.storageLimit }}'
policy: '{{ vsphere_storage_policy_id }}'
when: namespace.status != 200
These playbooks were fully tested and verified to work as expected. This approach allows for repeatable and consistent deployments of Supervisor Clusters and Namespaces, entirely managed through code. You can find the official repository on github: vsphere-supervisor-as-code.
Conclusion
Using Ansible to automate the deployment of a vSphere Supervisor Cluster provides a flexible, reliable, and reproducible solution. While Terraform remains a strong tool for declarative infrastructure, its current provider limitations make Ansible the better choice for scenarios that require direct API control and precise configuration management.
This automation workflow ensures that Supervisor Clusters can be deployed consistently, laying the groundwork for fully codified management of Kubernetes infrastructure on vSphere.