Skip to content

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 NametypedefaultDependencyRequired
use_bulk_apiBooleanfalsenoneOptional
bulk_site_provisioningstring (full site path in Catalyst Center’s hierarchy, Eg “Global/USA/SF”)emptydepends on use_bulk_apiOptional

To enable bulk resource creation, set the use_bulk_api flag in your module configuration:

main.tf
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.

  • Catalyst Center Terraform Provider ≥ 0.4.3

  • nac-catalystcenter Terraform Module ≥ 0.3.0

Below is a table outlining which resources currently support bulk API mode. This list will expand as additional resources gain bulk support:

Non-Bulk ResourceBulk Resource EquivalentDescriptionDefault
catalystcenter_provision_devicecatalystcenter_provision_devicesUsed for wired device(s) non-fabric provisioningfalse
catalystcenter_fabric_devicecatalystcenter_fabric_devicesUsed for wired device(s) and WLC fabric provisioningfalse
catalystcenter_anycast_gatewaycatalystcenter_anycast_gatewaysUsed for anycast gateway provisioningfalse
No Equivalentcatalystcenter_provision_access_pointsUsed for Access Point(s) provisioningfalse
catalystcenter_fabric_l3_handoff_ip_transitscatalystcenter_fabric_l3_handoff_ip_transitsUsed for L3 handoff provisioningtrue
No Equivalentcatalystcenter_fabric_port_assignmentsUsed for fabric port-assignment provisioningtrue

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:

  • Set use_bulk_api = true in your module configuration of the terraform root main.tf file

2. Use terraform Plan to check planned changes

Section titled “2. Use terraform Plan to check planned changes”
  • Run terraform plan to 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 Plan
$ terraform plan
Terraform 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 name page, the Import syntax for the resource is provided
  • Follow the syntax to import the resource state
  • Example
Terraform state import CLI
# Anycast Gateways
terraform 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
Terraform state rm CLI
# Anycast Gateway
terraform 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 Device
terraform 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 Device
terraform 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 apply should show only updates and additions. No existing resources should be marked for destruction.
  • Example
Terraform apply
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: yes

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

Terminal window
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

main.tf
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"
}

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 NameSite Locationuse_bulk_api = trueExplanation
DEVICE-1Global/Poland/Krakow/Building_ABulk provisioned to Building_ADEVICE-1 and DEVICE-2 share the same building, so they are grouped into one Terraform provision_devices resource
DEVICE-2Global/Poland/Krakow/Building_ABulk provisioned to Building_ASame resource as DEVICE-1
DEVICE-3Global/Poland/Krakow/Building_A/Floor_1Provisioned as single resource to Floor_1DEVICE-3 belongs to a different site level and is therefore placed in its own provision_devices resource
DEVICE-4Global/Poland/Krakow/Building_A/Floor_2Bulk provisioned to Floor_2DEVICE-4 and DEVICE-5 share the same floor and are grouped into one Terraform provision_devices resource
DEVICE-5Global/Poland/Krakow/Building_A/Floor_2Bulk provisioned to Floor_2Same 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 NameSite Locationbulk_site_provisioning = “Global/Poland/Krakow”
DEVICE-1Global/Poland/Krakow/Building_ABulk-provisioned under Global/Poland/Krakow
DEVICE-2Global/Poland/Krakow/Building_ABulk-provisioned under Global/Poland/Krakow
DEVICE-3Global/Poland/Krakow/Building_A/Floor_1Bulk-provisioned under Global/Poland/Krakow
DEVICE-4Global/Poland/Krakow/Building_A/Floor_2Device is Bulk provisioned to “Global/Poland/Krakow”
DEVICE-5Global/Poland/Krakow/Building_A/Floor_2Device is Bulk provisioned to “Global/Poland/Krakow”
DEVICE-6Global/Poland/Warsaw/Building_BBulk-provisioned under Warsaw/Building_B (default behavior)
DEVICE-7Global/Poland/Warsaw/Building_BBulk-provisioned under Warsaw/Building_B (default behavior)
DEVICE-8Global/Poland/Warsaw/Building_B/Floor_3Bulk-provisioned individually under Warsaw/Building_B/Floor_3 (default behavior)