Bulk API Terraform Resources
Bulk API with Terraform for Cisco Catalyst Center
Section titled “Bulk API with Terraform for Cisco Catalyst Center”Overview
As organizations scale their network infrastructure, efficient resource provisioning becomes vital. Traditionally, Terraform creates resources one at a time, which could resulting in significant delays and increased exposure to API rate limiting, especially when managing large environments.
Bulk API functionality addresses this challenge by enabling Terraform to create resources in batches (i.e., as lists), instead of sending API calls for each resource individually. This approach reduces overall execution time and minimizes the risk of hitting rate limits.
Key Benefits
- Accelerated Resource Creation: Batch processing reduces time to provision large sets of resources.
- Lower API Rate Limit Impact: Fewer calls are made, which helps avoid throttling.
- Simplified State Management: Terraform updates its state only after asynchronous tasks complete successfully.
- Consistent Deployments: Bulk operations reduce the risk of partial resource creation and ensure state consistency.
Bulk resource provisioning introduces two new flags:
| Flag Name | type | default | Dependency | Required |
|---|---|---|---|---|
| use_bulk_api | Boolean | false | none | Optional |
| bulk_site_provisioning | string (full site path in Catalyst Center’s hierarchy, Eg “Global/USA/SF”) | empty | depends on use_bulk_api | Optional |
Using the “use_bulk_api” Flag
Section titled “Using the “use_bulk_api” Flag”To enable bulk resource creation, set the use_bulk_api flag in your module configuration:
module "catalyst_center" { source = "netascode/nac-catalystcenter/catalystcenter" version = "0.3.0" yaml_directories = ["data/"] templates_directories = ["data/templates/"] use_bulk_api = true}Default behavior: Resources are created one at a time (non-bulk).
Bulk mode: Setting use_bulk_api = true enables bulk resource creation for supported resources.
Use_bulk_api requires:
Section titled “Use_bulk_api requires:”Catalyst Center Terraform Provider ≥ 0.4.3
nac-catalystcenter Terraform Module ≥ 0.3.0
Supported Resources
Section titled “Supported Resources”Below is a table outlining which resources currently support bulk API mode. This list will expand as additional resources gain bulk support:
Bulk Resource Table
Section titled “Bulk Resource Table”| Non-Bulk Resource | Bulk Resource Equivalent | Description | Default |
|---|---|---|---|
| catalystcenter_provision_device | catalystcenter_provision_devices | Used for wired device(s) non-fabric provisioning | false |
| catalystcenter_fabric_device | catalystcenter_fabric_devices | Used for wired device(s) and WLC fabric provisioning | false |
| catalystcenter_anycast_gateway | catalystcenter_anycast_gateways | Used for anycast gateway provisioning | false |
| No Equivalent | catalystcenter_provision_access_points | Used for Access Point(s) provisioning | false |
| catalystcenter_fabric_l3_handoff_ip_transits | catalystcenter_fabric_l3_handoff_ip_transits | Used for L3 handoff provisioning | true |
| No Equivalent | catalystcenter_fabric_port_assignments | Used for fabric port-assignment provisioning | true |
Migration Path: Moving from Non-Bulk to Bulk
Section titled “Migration Path: Moving from Non-Bulk to Bulk”If you are currently using individual (non-bulk) resources, migrating to bulk API mode is as follows:
1. Enable Bulk API Mode
Section titled “1. Enable Bulk API Mode”- Set
use_bulk_api = truein your module configuration of the terraform rootmain.tffile
2. Use terraform Plan to check planned changes
Section titled “2. Use terraform Plan to check planned changes”- Run
terraform planto identify which existing resources will be replaced by the new bulk resources. - These will show the resources that must be removed from state and the resources that needs to be imported
- Resources that must be removed have the word “will be destroyed”
- Resources that needs to be created have the word “will be created”
- Example
$ terraform planTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create - destroy
Terraform will perform the following actions:
# module.catalyst_center.catalystcenter_anycast_gateway.anycast_gateway["AP_IPPOOL"] will be destroyed # (because key ["AP_IPPOOL"] is not in for_each map) - resource "catalystcenter_anycast_gateway" "anycast_gateway" { - auto_generate_vlan_name = false -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - id = "7cf3eb66-82da-4b5e-9ee9-326bbb2519d4" -> null - ip_pool_name = "AP_IPPOOL" -> null - pool_type = "FABRIC_AP" -> null - traffic_type = "DATA" -> null - virtual_network_name = "INFRA_VN" -> null - vlan_id = 1025 -> null - vlan_name = "AP_VLAN" -> null }
# module.catalyst_center.catalystcenter_anycast_gateway.anycast_gateway["BYOD-IPPool"] will be destroyed # (because key ["BYOD-IPPool"] is not in for_each map) - resource "catalystcenter_anycast_gateway" "anycast_gateway" { - auto_generate_vlan_name = true -> null - critical_pool = false -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - id = "8d5f3c11-6443-403e-a87c-15e57e64d22f" -> null - intra_subnet_routing_enabled = false -> null - ip_directed_broadcast = false -> null - ip_pool_name = "BYOD-IPPool" -> null - l2_flooding_enabled = false -> null - multiple_ip_to_mac_addresses = false -> null - traffic_type = "DATA" -> null - virtual_network_name = "BYOD" -> null - vlan_id = 1024 -> null - vlan_name = "192_168_103_0-BYOD" -> null - wireless_pool = false -> null }
# module.catalyst_center.catalystcenter_anycast_gateways.anycast_gateways["Global/Poland/Krakow"] will be created + resource "catalystcenter_anycast_gateways" "anycast_gateways" { + anycast_gateways = [ + { + auto_generate_vlan_name = false + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + ip_pool_name = "AP_IPPOOL" + pool_type = "FABRIC_AP" + traffic_type = "DATA" + virtual_network_name = "INFRA_VN" + vlan_id = (known after apply) + vlan_name = "AP_VLAN" }, + { + auto_generate_vlan_name = true + critical_pool = false + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + intra_subnet_routing_enabled = false + ip_directed_broadcast = false + ip_pool_name = "BYOD-IPPool" + l2_flooding_enabled = false + multiple_ip_to_mac_addresses = false + traffic_type = "DATA" + virtual_network_name = "BYOD" + vlan_id = (known after apply) + vlan_name = (known after apply) + wireless_pool = false },
] + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) }
# module.catalyst_center.catalystcenter_fabric_device.border_device["BR10"] will be destroyed # (because key ["BR10"] is not in for_each map) - resource "catalystcenter_fabric_device" "border_device" { - border_types = [ - "LAYER_3", ] -> null - default_exit = true -> null - device_roles = [ - "BORDER_NODE", - "CONTROL_PLANE_NODE", ] -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - id = "6969bdd3-49dc-4215-aa2b-63472c3931be" -> null - import_external_routes = false -> null - local_autonomous_system_number = "65001" -> null - network_device_id = "e7869917-cf34-44ed-998a-e72ef9866eeb" -> null }
# module.catalyst_center.catalystcenter_fabric_device.edge_device["EDGE01"] will be destroyed # (because key ["EDGE01"] is not in for_each map) - resource "catalystcenter_fabric_device" "edge_device" { - device_roles = [ - "EDGE_NODE", ] -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - id = "e94c6c16-0966-443a-85bf-7d4524cae18c" -> null - network_device_id = "7ef492ca-b008-479a-9de4-7e40438c7d10" -> null }
# module.catalyst_center.catalystcenter_fabric_device.wireless_controller["C9800-KRK-WLC1"] will be destroyed # (because key ["C9800-KRK-WLC1"] is not in for_each map) - resource "catalystcenter_fabric_device" "wireless_controller" { - device_roles = [ - "WIRELESS_CONTROLLER_NODE", ] -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - id = "6a30660d-7428-44d2-adfe-5c3a0f9cd609" -> null - network_device_id = "29998034-a675-43db-9bcb-17a9d5d580f8" -> null }
# module.catalyst_center.catalystcenter_fabric_devices.fabric_devices["Global/Poland/Krakow"] will be created + resource "catalystcenter_fabric_devices" "fabric_devices" { + fabric_devices = [ + { + border_types = [ + "LAYER_3", ] + default_exit = true + device_roles = [ + "BORDER_NODE", + "CONTROL_PLANE_NODE", ] + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + import_external_routes = false + local_autonomous_system_number = "65001" + network_device_id = "e7869917-cf34-44ed-998a-e72ef9866eeb" }, + { + device_roles = [ + "EDGE_NODE", ] + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + network_device_id = "7ef492ca-b008-479a-9de4-7e40438c7d10" }, + { + device_roles = [ + "WIRELESS_CONTROLLER_NODE", ] + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + network_device_id = "29998034-a675-43db-9bcb-17a9d5d580f8" }, ] + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) }
# module.catalyst_center.catalystcenter_fabric_devices.fabric_devices["Global/Poland/Warsaw"] will be created + resource "catalystcenter_fabric_devices" "fabric_devices" { + fabric_devices = [ + { + border_types = [ + "LAYER_3", ] + default_exit = true + device_roles = [ + "BORDER_NODE", + "CONTROL_PLANE_NODE", + "EDGE_NODE", ] + fabric_id = "81890279-6fb0-4a07-8765-62ac3cb858be" + id = (known after apply) + import_external_routes = false + local_autonomous_system_number = "65001" + network_device_id = "28a9f0f0-2834-4f12-8409-26d34a7f5bbb" }, + { + device_roles = [ + "WIRELESS_CONTROLLER_NODE", ] + fabric_id = "81890279-6fb0-4a07-8765-62ac3cb858be" + id = (known after apply) + network_device_id = "c0cd8517-5120-42ad-982b-a62ba039be77" }, ] + fabric_id = "81890279-6fb0-4a07-8765-62ac3cb858be" + id = (known after apply) }
# module.catalyst_center.catalystcenter_provision_device.provision_device["BR10"] will be destroyed # (because key ["BR10"] is not in for_each map) - resource "catalystcenter_provision_device" "provision_device" { - id = "6969bdd3-49dc-4215-aa2b-63472c3931be" -> null - network_device_id = "e7869917-cf34-44ed-998a-e72ef9866eeb" -> null - reprovision = false -> null - site_id = "6431e42a-8dc3-48d6-a991-8295af04bed5" -> null }
# module.catalyst_center.catalystcenter_provision_device.provision_device["EDGE01"] will be destroyed # (because key ["EDGE01"] is not in for_each map) - resource "catalystcenter_provision_device" "provision_device" { - id = "e94c6c16-0966-443a-85bf-7d4524cae18c" -> null - network_device_id = "7ef492ca-b008-479a-9de4-7e40438c7d10" -> null - reprovision = false -> null - site_id = "6431e42a-8dc3-48d6-a991-8295af04bed5" -> null }
# module.catalyst_center.catalystcenter_provision_devices.provision_devices["Global/Poland/Krakow/Bld A"] will be created + resource "catalystcenter_provision_devices" "provision_devices" { + id = (known after apply) + provision_devices = [ + { + id = (known after apply) + network_device_id = "7ef492ca-b008-479a-9de4-7e40438c7d10" + reprovision = false + site_id = "6431e42a-8dc3-48d6-a991-8295af04bed5" }, + { + id = (known after apply) + network_device_id = "e7869917-cf34-44ed-998a-e72ef9866eeb" + reprovision = false + site_id = "6431e42a-8dc3-48d6-a991-8295af04bed5" }, ] + site_id = "6431e42a-8dc3-48d6-a991-8295af04bed5" }3. Import “Bulk Resource” using Brownfield Import
Section titled “3. Import “Bulk Resource” using Brownfield Import”- Navigate to the Catalyst Center terraform Provider documentation and select the target provider version
- Filter the documentation using the “bulk resource” name
- On the bottom of the
resource namepage, theImport syntaxfor the resource is provided - Follow the syntax to import the resource state
- Example
# Anycast Gatewaysterraform import 'module.catalyst_center.catalystcenter_anycast_gateways.anycast_gateways["Global/Poland/Krakow"]' "05bb4d28-9887-43a4-817a-5653d07c3bc5"
#Fabric Devices
terraform import 'module.catalyst_center.catalystcenter_fabric_devices.fabric_devices["Global/Poland/Krakow"]' "05bb4d28-9887-43a4-817a-5653d07c3bc5"
#provison Devices
terraform import 'module.catalyst_center.catalystcenter_provision_devices.provision_devices["Global/Poland/Krakow/Bld A"]' "6431e42a-8dc3-48d6-a991-8295af04bed5"4. Use terraform command to remove the “to be destroyed resources”
Section titled “4. Use terraform command to remove the “to be destroyed resources””- You must remove the old non-bulk resources from the state so Terraform does not try to delete them during the next apply.
- Use Terraform commands to remove individual resources which are planned to be destroyed from the state file.
- Example
# Anycast Gatewayterraform state rm 'module.catalyst_center.catalystcenter_anycast_gateway.anycast_gateway["AP_IPPOOL"]'terraform state rm 'module.catalyst_center.catalystcenter_anycast_gateway.anycast_gateway["BYOD-IPPool"]'
# Fabric Deviceterraform state rm 'module.catalyst_center.catalystcenter_fabric_device.border_device["BR10"]'terraform state rm 'module.catalyst_center.catalystcenter_fabric_device.edge_device["EDGE01"]'terraform state rm 'module.catalyst_center.catalystcenter_fabric_device.wireless_controller["C9800-KRK-WLC1"]'
# Provison Deviceterraform state rm 'module.catalyst_center.catalystcenter_provision_device.provision_device["BR10"]'terraform state rm 'module.catalyst_center.catalystcenter_provision_device.provision_device["EDGE01"]'5. Use terraform command to do a terraform apply
Section titled “5. Use terraform command to do a terraform apply”- Use Terraform commands to apply and update the imported resources
- After completing previous steps,
terraform applyshould show only updates and additions. No existing resources should be marked for destruction. - Example
Terraform will perform the following actions:
# module.catalyst_center.catalystcenter_anycast_gateways.anycast_gateways["Global/Poland/Krakow"] will be updated in-place ~ resource "catalystcenter_anycast_gateways" "anycast_gateways" { ~ anycast_gateways = [ - { - auto_generate_vlan_name = false -> null - critical_pool = false -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - group_based_policy_enforcement_enabled = true -> null - id = "8d5f3c11-6443-403e-a87c-15e57e64d22f" -> null - intra_subnet_routing_enabled = false -> null - ip_directed_broadcast = false -> null - ip_pool_name = "BYOD-IPPool" -> null - l2_flooding_enabled = false -> null - multiple_ip_to_mac_addresses = false -> null - supplicant_based_extended_node_onboarding = false -> null - traffic_type = "DATA" -> null - virtual_network_name = "BYOD" -> null - vlan_id = 1024 -> null - vlan_name = "192_168_103_0-BYOD" -> null - wireless_pool = false -> null }, - { - auto_generate_vlan_name = false -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - group_based_policy_enforcement_enabled = false -> null - id = "7cf3eb66-82da-4b5e-9ee9-326bbb2519d4" -> null - ip_pool_name = "AP_IPPOOL" -> null - pool_type = "FABRIC_AP" -> null - supplicant_based_extended_node_onboarding = false -> null - traffic_type = "DATA" -> null - virtual_network_name = "INFRA_VN" -> null - vlan_id = 1025 -> null - vlan_name = "AP_VLAN" -> null }, + { + auto_generate_vlan_name = false + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + ip_pool_name = "AP_IPPOOL" + pool_type = "FABRIC_AP" + traffic_type = "DATA" + virtual_network_name = "INFRA_VN" + vlan_id = 1022 + vlan_name = "AP_VLAN" }, + { + auto_generate_vlan_name = true + critical_pool = false + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + intra_subnet_routing_enabled = false + ip_directed_broadcast = false + ip_pool_name = "BYOD-IPPool" + l2_flooding_enabled = false + multiple_ip_to_mac_addresses = false + traffic_type = "DATA" + virtual_network_name = "BYOD" + vlan_id = 1021 + vlan_name = "192_168_102_0-Printers" + wireless_pool = false }, ] id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" }
# module.catalyst_center.catalystcenter_fabric_devices.fabric_devices["Global/Poland/Krakow"] will be updated in-place ~ resource "catalystcenter_fabric_devices" "fabric_devices" { ~ fabric_devices = [ - { - border_priority = 10 -> null - border_types = [ - "LAYER_3", ] -> null - default_exit = true -> null - device_roles = [ - "BORDER_NODE", - "CONTROL_PLANE_NODE", ] -> null - fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" -> null - id = "6969bdd3-49dc-4215-aa2b-63472c3931be" -> null - import_external_routes = false -> null - local_autonomous_system_number = "65001" -> null - network_device_id = "e7869917-cf34-44ed-998a-e72ef9866eeb" -> null - prepend_autonomous_system_count = 0 -> null }, + { + border_types = [ + "LAYER_3", ] + default_exit = true + device_roles = [ + "BORDER_NODE", + "CONTROL_PLANE_NODE", ] + fabric_id = "05bb4d28-9887-43a4-817a-5653d07c3bc5" + id = (known after apply) + import_external_routes = false + local_autonomous_system_number = "65001" + network_device_id = "e7869917-cf34-44ed-998a-e72ef9866eeb" }, ] id = "05bb4d28-9887-43a4-817a-5653d07c3bc5"
}
Plan: 0 to add, 2 to change, 0 to destroy. Enter a value: yesTroubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Import Syntax: Wrong Import syntax used Solution: - Ensure the correct import statement syntax is used
Client Warning: After the final terraform apply step is completed, you may see CLI warning about failure to update vlanName. This is expected
│ Failed to configure object (PUT), got error: HTTP Request failed: StatusCode 400, {"response":{"errorCode":"NCHS20297","message":"Bad│ Request","detail":"Anycast Gateway update failed in SDA Fabric for payload element 0. vlanName cannot be updated."},"version":"1.0"}││ (and one more similar warning elsewhere)Using the “bulk_site_provisioning” Flag
Section titled “Using the “bulk_site_provisioning” Flag”The bulk_site_provisioning flag is an optional setting used to control how devices are grouped for bulk provisioning. It determines which site-level Terraform provision_devices resource, all eligible devices will be associated with.
Before using this flag, you must enable use_bulk_api = true
module "catalyst_center" { source = "netascode/nac-catalystcenter/catalystcenter" version = "0.3.0" yaml_directories = ["data/"] templates_directories = ["data/templates/"] use_bulk_api = true bulk_site_provisioning = "Global/Poland/Krakow"}What bulk_site_provisioning Does
Section titled “What bulk_site_provisioning Does”When use_bulk_api = true is enabled without setting bulk_site_provisioning, devices will only be bulk-provisioned if they share the exact same level in the Catalyst Center site hierarchy (e.g., the same building or the same floor).
The table below illustrates how devices are grouped when bulk_site_provisioning is not set:
| Device Name | Site Location | use_bulk_api = true | Explanation |
|---|---|---|---|
| DEVICE-1 | Global/Poland/Krakow/Building_A | Bulk provisioned to Building_A | DEVICE-1 and DEVICE-2 share the same building, so they are grouped into one Terraform provision_devices resource |
| DEVICE-2 | Global/Poland/Krakow/Building_A | Bulk provisioned to Building_A | Same resource as DEVICE-1 |
| DEVICE-3 | Global/Poland/Krakow/Building_A/Floor_1 | Provisioned as single resource to Floor_1 | DEVICE-3 belongs to a different site level and is therefore placed in its own provision_devices resource |
| DEVICE-4 | Global/Poland/Krakow/Building_A/Floor_2 | Bulk provisioned to Floor_2 | DEVICE-4 and DEVICE-5 share the same floor and are grouped into one Terraform provision_devices resource |
| DEVICE-5 | Global/Poland/Krakow/Building_A/Floor_2 | Bulk provisioned to Floor_2 | Same resource as DEVICE-4 |
The second table illustrates the behavior when bulk_site_provisioning is set to "Global/Poland/Krakow". With this setting enabled, all devices located anywhere under the Krakow site hierarchy are grouped into a single provision_devices resource, regardless of their building or floor.
Devices outside those specified in the bulk_site_provisioning Krakow site hierarchy (Global/Poland/Krakow) continue to follow the default bulk-provisioning behavior.
| Device Name | Site Location | bulk_site_provisioning = “Global/Poland/Krakow” |
|---|---|---|
| DEVICE-1 | Global/Poland/Krakow/Building_A | Bulk-provisioned under Global/Poland/Krakow |
| DEVICE-2 | Global/Poland/Krakow/Building_A | Bulk-provisioned under Global/Poland/Krakow |
| DEVICE-3 | Global/Poland/Krakow/Building_A/Floor_1 | Bulk-provisioned under Global/Poland/Krakow |
| DEVICE-4 | Global/Poland/Krakow/Building_A/Floor_2 | Device is Bulk provisioned to “Global/Poland/Krakow” |
| DEVICE-5 | Global/Poland/Krakow/Building_A/Floor_2 | Device is Bulk provisioned to “Global/Poland/Krakow” |
| DEVICE-6 | Global/Poland/Warsaw/Building_B | Bulk-provisioned under Warsaw/Building_B (default behavior) |
| DEVICE-7 | Global/Poland/Warsaw/Building_B | Bulk-provisioned under Warsaw/Building_B (default behavior) |
| DEVICE-8 | Global/Poland/Warsaw/Building_B/Floor_3 | Bulk-provisioned individually under Warsaw/Building_B/Floor_3 (default behavior) |