ACI Automation with Terraform
ACI Automation with Terraform
Introduction
Managing a Cisco ACI fabric by hand works fine when you have a handful of tenants and bridge domains, but the moment your data center scales to dozens of applications and environments, manual APIC clicks become a bottleneck and a source of human error. Terraform gives you a way to define your entire ACI fabric configuration as code, apply it repeatably, and track every change through version control.
In this lesson you will learn how Terraform communicates with the APIC controller, how to structure a Terraform project for ACI, how to write resources that build tenants, VRFs, bridge domains, subnets, and EPGs, and how to use variables and iteration to keep your code clean. By the end you will be comfortable reading and writing Terraform configurations that automate a production ACI environment.
Key Concepts
Terraform Projects
A Terraform project is a collection of configuration files that live in a single directory. When you run Terraform, it operates against that directory and processes every .tf file inside it. Your intent -- what you want to provision -- can be split across multiple files or kept in one. The key point is that Terraform is declarative: you describe the desired end state and Terraform figures out the order of operations for you.
A common project layout separates concerns into individual files:
| File | Purpose |
|---|---|
main.tf | Provider configuration and core settings |
tenant.tf | Tenant resource definitions |
vrf.tf | VRF resource definitions |
bridge_domain.tf | Bridge domain resources |
epg.tf | Endpoint group resources |
variables.tf | Variable declarations (type, default, description) |
variables.tfvars | Variable value assignments |
When you manage multiple environments -- for example production and development -- you create separate projects (separate directories), each with its own set of files and its own state. This keeps environment configurations isolated from each other.
Terraform Providers
The Terraform binary on its own does not know anything about ACI or the APIC API. It relies on providers -- plugins that understand how to talk to a specific platform's API. The ACI provider translates your Terraform resource definitions into APIC REST API calls.
Providers fall into three categories:
| Category | Maintained By | Examples |
|---|---|---|
| Official | HashiCorp | AWS, Azure, GCP |
| Partner | Technology partners | ACI, MSO, NX-OS, ASA |
| Community | Open-source contributors | Various third-party integrations |
Each provider has a one-to-one relationship with its vendor platform. The ACI provider specifically communicates with the APIC and MSO REST APIs. Providers are installed automatically when you run terraform init.
Resources and Data Sources
A resource represents an infrastructure object that Terraform creates and manages. Each resource has a type (supplied by the provider) and a unique name that Terraform uses in its state file. Resources have required and optional attributes.
A data source lets you fetch information about objects that already exist so you can reference them elsewhere in your configuration. Data sources are read-only -- they do not create or modify anything.
Terraform State
Terraform is stateful. It maintains a file called terraform.tfstate in JSON format inside your working directory. This state file tracks every object Terraform has built and is the source of truth for what Terraform knows about your infrastructure. A backup is kept as terraform.tfstate.backup.
Warning: Never modify the state file directly. Terraform manages this file internally, and manual edits can corrupt your infrastructure tracking.
By default, state is stored locally, which makes team collaboration difficult. For production use, Terraform supports remote state backends such as AWS S3, AzureRM, and GCS. Remote backends enable collaboration and support state locking (for example, using DynamoDB with AWS S3) to prevent two engineers from modifying the same infrastructure simultaneously.
How It Works
Provider Authentication Flow
Before Terraform can manage any ACI objects, it must authenticate to the APIC controller. You configure this in a provider block that specifies the APIC URL, username, and password. When insecure is set to true, Terraform skips SSL certificate validation -- useful in lab environments with self-signed certificates.
Any time you change the provider configuration, you must re-run terraform init to reinitialize the provider plugin.
You declare the required provider version in a terraform block. This ensures everyone on your team uses the same provider release:
terraform {
required_version = ">= 1.6.0"
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "2.13.0"
}
}
}
Dependency Graph
Terraform automatically detects implicit dependencies between resources by analyzing attribute references. When one resource references another's ID, Terraform knows it must create the referenced resource first. It builds a Directed Acyclic Graph (DAG) that determines the correct creation, modification, and deletion order.
For example, a tenant must exist before a VRF can be created inside it, and a VRF must exist before a bridge domain can reference it. Terraform resolves this chain automatically. When automatic detection is not sufficient, you can set up explicit dependencies using the depends_on argument.
Configuration Example
Provider Block
The provider block connects Terraform to your APIC at a specific URL with credentials:
provider "aci" {
username = var.aci_username
password = var.aci_password
url = "https://172.31.2.31/"
insecure = true
}
The var.aci_username and var.aci_password references pull values from variables, which can be set securely as environment variables:
export TF_VAR_aci_username=admin
export TF_VAR_aci_password=Lab@123
Building a Complete ACI Tenant
The following configuration creates a full tenant structure -- tenant, VRF, application profile, bridge domain, subnet, and EPG -- using variables for all names:
variable "tenant_name" {
type = string
default = "PROD"
description = "Tenant name"
}
variable "vrf_name" {
default = "PROD-VRF"
}
variable "ap_name" {
default = "PROD-AP"
}
variable "bd_name" {
default = "web-bd"
}
variable "bd_subnet" {
default = "10.1.1.1/24"
}
resource "aci_tenant" "terraform_tenant" {
name = var.tenant_name
description = "Created with Terraform"
}
resource "aci_vrf" "terraform_vrf" {
tenant_dn = aci_tenant.terraform_tenant.id
name = var.vrf_name
description = "Created with Terraform"
}
resource "aci_application_profile" "terraform_ap" {
tenant_dn = aci_tenant.terraform_tenant.id
name = var.ap_name
}
resource "aci_bridge_domain" "web-bd" {
tenant_dn = aci_tenant.terraform_tenant.id
relation_fv_rs_ctx = aci_vrf.terraform_vrf.id
name = var.bd_name
}
resource "aci_subnet" "web_subnet" {
parent_dn = aci_bridge_domain.web-bd.id
ip = var.bd_subnet
}
resource "aci_application_epg" "prod_epg" {
application_profile_dn = aci_application_profile.terraform_ap.id
name = "prod-epg"
relation_fv_rs_bd = aci_bridge_domain.web-bd.id
pref_gr_memb = "include"
}
Notice how each resource references its parent by ID (for example, aci_tenant.terraform_tenant.id). Terraform uses these references to build the dependency graph and create objects in the correct order.
Overriding Defaults with tfvars
Variable defaults can be overridden using a terraform.tfvars file or any file ending in .auto.tfvars. Values set in these files take precedence over the defaults in variables.tf:
tenant_name = "ciscolive"
vrf_name = "ciscolive_vrf"
ap_name = "ciscolive_ap"
bd_name = "ciscolive_bd"
bd_subnet = "192.168.100.1/24"
Variable precedence follows this order: environment variables (TF_VAR_*) can be overridden by .tfvars files, which override the defaults in variables.tf. If no value is set anywhere, Terraform prompts the user for input at runtime.
Using aci_rest_managed for Unsupported Objects
When the ACI provider does not have a dedicated resource for a particular object, you can use the aci_rest_managed resource to manage it directly through REST API calls. This resource can also reconcile state information, so Terraform still tracks the object:
resource "aci_rest_managed" "rest_tenant" {
dn = "uni/tn-REST"
class_name = "fvTenant"
content = {
name = "REST"
descr = "Tenant built with REST"
}
}
Iteration with count
When you need to create multiple instances of the same object type, the count meta-argument eliminates repetitive code. It is index-based, meaning each instance is identified by its position in a list:
variable "bridge_domains" {
description = "BD names"
type = list(string)
default = ["prod", "dev", "qa"]
}
resource "aci_bridge_domain" "count_bd" {
count = length(var.bridge_domains)
tenant_dn = aci_tenant.count_tenant.id
relation_fv_rs_ctx = aci_vrf.count_vrf.id
description = "Created with Terraform"
name = "bd_${var.bridge_domains[count.index]}"
arp_flood = "yes"
}
This single block creates three bridge domains: bd_prod, bd_dev, and bd_qa.
Iteration with for_each
The for_each meta-argument is key-based rather than index-based. It iterates over a set or map, creating one instance per element. Each instance is identified by its key rather than a numeric index:
variable "bridge_domains" {
description = "BD names"
type = list(string)
default = ["prod-bd", "dev-bd", "qa-bd"]
}
resource "aci_bridge_domain" "for_each_bd" {
for_each = toset(var.bridge_domains)
tenant_dn = aci_tenant.tf_tenant.id
relation_fv_rs_ctx = aci_vrf.terraform_vrf.id
description = "Created with Terraform"
name = each.key
arp_flood = "yes"
}
When using a list with for_each, you must convert it using the toset() function. The each.key reference gives you the current element's value. This approach is generally preferred over count because adding or removing an item from the middle of the list does not cause Terraform to recreate unrelated resources.
Real-World Application
In production data centers, Terraform-driven ACI automation is used in several key scenarios:
-
Multi-environment provisioning -- Teams maintain separate Terraform projects for production, development, and QA. Each project uses the same
.tffiles with different.tfvarsfiles, ensuring consistent structure across environments while allowing different IP addressing and naming. -
Day-2 operations -- When a new application needs network resources, an engineer adds variable entries for the new bridge domains, subnets, and EPGs, then runs
terraform apply. Terraform calculates the difference between current state and desired state and makes only the necessary API calls. -
Drift detection -- Because Terraform tracks state, running
terraform planreveals any configuration drift where someone has made manual changes through the APIC GUI. This keeps your infrastructure aligned with your code. -
Collaboration at scale -- By storing the state file in a remote backend with state locking, multiple engineers can work on the same ACI fabric without overwriting each other's changes.
Best Practice: Always use variables rather than hard-coded values in your resource definitions. This makes your Terraform code reusable across environments and reduces the risk of errors when deploying similar configurations to multiple fabrics.
Summary
- The ACI provider translates Terraform resource definitions into APIC REST API calls; it is installed via
terraform initand requires the APIC URL, username, and password for authentication. - Terraform projects are declarative and directory-based -- you describe what you want, and Terraform determines the order of operations using a Directed Acyclic Graph built from implicit dependencies between resources.
- Variables defined in
variables.tfand overridden in.tfvarsfiles make your code reusable across environments; sensitive values like credentials can be passed asTF_VAR_*environment variables. - The
countandfor_eachmeta-arguments eliminate repetitive resource blocks -- usecountfor simple numbered lists andfor_eachfor key-based iteration over sets or maps. - Terraform state must be treated carefully: never edit
terraform.tfstatedirectly, and consider remote backends with state locking for team environments.
In the next lesson, we will explore how Terraform manages the full lifecycle of ACI objects -- planning changes, applying them, and handling updates and deletions across your fabric.