Lesson 1 of 5

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:

FilePurpose
main.tfProvider configuration and core settings
tenant.tfTenant resource definitions
vrf.tfVRF resource definitions
bridge_domain.tfBridge domain resources
epg.tfEndpoint group resources
variables.tfVariable declarations (type, default, description)
variables.tfvarsVariable 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:

CategoryMaintained ByExamples
OfficialHashiCorpAWS, Azure, GCP
PartnerTechnology partnersACI, MSO, NX-OS, ASA
CommunityOpen-source contributorsVarious 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 .tf files with different .tfvars files, 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 plan reveals 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 init and 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.tf and overridden in .tfvars files make your code reusable across environments; sensitive values like credentials can be passed as TF_VAR_* environment variables.
  • The count and for_each meta-arguments eliminate repetitive resource blocks -- use count for simple numbered lists and for_each for key-based iteration over sets or maps.
  • Terraform state must be treated carefully: never edit terraform.tfstate directly, 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.