Cisco ACI - Deployment With Terraform - Advanced

Hello and welcome back to my blog! In this post we’ll see how to automate your Cisco ACI deployment using Terraform 😊

If you didn’t read the previous blog post, I strongly recommend you to check it out in order to understand better this post. In the “Cisco ACI - Deployment with Terraform - Basic” post I introduced Terraform and showed you how to create a basic Tenant with 1 VRF, 1 Application Profile, 1 EPG and 1 BD using Terraform.

Let’s scale it! 😉

Tools

In this lab I’m using:

  • Visual Studio
  • WSL (Windows Subsystem for Linux, it allows you to install a complete Ubuntu terminal environment in minutes on your Windows machine)
  • Cisco ACI Sandbox

Tenant

In this post we’re going to create a single Tenant with the following objects:

  • Tenant: THETECHGUY
  • VRF1: PROD_VRF
  • VRF2: DEMO_VRF
  • Application Profile1: PROD_APP
  • Application Profile2: DEMO_APP
  • EndPoint Group1: PROD_EPG1
  • EndPoint Group2: PROD_EPG2
  • EndPoint Group3: PROD_EPG3
  • EndPoint Group4: DEMO_EPG1
  • Bridge Domain1: PROD_BD
  • Bridge Domain2: DEMO_BD
  • Bridge Domain PROD Subnet: 192.168.100.254/24
  • Bridge Domain DEMO Subnet: 192.168.200.254/24

Moreover, we’ll create the following Access Policies:

  • VLAN Pool: Physical_VLAN-Pool
  • VLAN Pool range: From VLAN-2 to VLAN-100 [static allocation]
  • Physical Domain: Physical_Dom
  • Attachable Access Entity Profile: Physical_AAEP

Obviously you can choose different names 😊

The Cisco ACI sandbox is available here:

With all these information, we can proceed with our lab.

Coding

If you are new to Terraform and its structure, I strongly recommend you to read my previous post called “Cisco ACI - Deployment with Terraform - Basic” , here you can find some useful information.
As always, here is the Cisco ACI Provider documentation for other customizations and ideas.

Let’s start! 😊

First, create three different files:

  • “main.tf”: Here is where our code is located
  • “variables.tf”: Here is where our variables are declared
  • “terraform.tfvars”: Here is where our variables are defined

Let’s review the variable.tf file:

# Local variables
locals {
    username = "admin"
    password = "!v3G@!4@Y"
    url = "https://sandboxapicdc.cisco.com"
    phy_domain = "PHY_DOM_TECH"
    tenant = "THETECHGUY"
}

# EndPoint Group Map
variable "epg_map" {
    type = map (object( {
        bd = string
        app = string
    }
    ))
}

# Bridge Domain Map
variable "bd_map" {
    type = map (object( {
        bd_subnet = string
        vrf = string
    }
    ))
}

# Application Profile Set
variable "app_set" {
    type = set (string)
}

# VRF Set
variable "vrf_set" {
    type = set (string)
}

As you can see, there are 5 different “blocks”:

  • “locals” âž¡ here you can store the local variables in order to hide them from the main.tf file (it’s more secure)
  • “epg_map” âž¡ This is the EPG Map, here we’re going to defined the BD and Application Profile for each EPG (into “variables.tf”)
  • “bd_map” âž¡ This is the BD Map, here we’re going to defined the BD Subnet and VRF for each BD (into “variables.tf”)
  • “app_set” âž¡ This is the Application Profile Set, here we’re going to put all the Application Profiles that have to be created (into “variables.tf”)
  • “vrf_set” âž¡ This is the VRF Set, here we’re going to put all the VRFs that have to be created (into “variables.tf”)

Let’s review the terraform.tfvars file:

# Bridge Domain Map
bd_map = {
    "PROD_BD" = {
        bd_subnet = "192.168.100.254/24"
        vrf = "PROD_VRF"
    }
    "DEMO_BD" = {
        bd_subnet = "192.168.200.254/24"
        vrf = "DEMO_VRF"
    }
}

# EndPoint Group Map
epg_map = {
    "PROD_EPG1" = {
        bd = "PROD_BD"
        app = "PROD_APP"
    }
    "PROD_EPG2" = {
        bd = "PROD_BD"
        app = "PROD_APP"
    }
    "PROD_EPG3" = {
        bd = "PROD_BD"
        app = "PROD_APP"
    }
    "DEMO_EPG1" = {
        bd = "DEMO_BD"
        app = "DEMO_APP"
    }
}

# Application Profile Set
app_set = [
    "PROD_APP",
    "DEMO_APP"
]

# VRF Set
vrf_set = [
    "PROD_VRF",
    "DEMO_VRF"
]

It’s important to understand the differences between a Map and a Set:

  • map: are a collection of string keys and string values. There is always a “Key” and one or more “Value” associated to the key. Let’s see the EPG Map, here is a piece of map:
"PROD_EPG1" = {
    bd = "PROD_BD"
    app = "PROD_APP"
}  

The “PROD_EPG1” is the Key, “PROD_BD” and “PROD_APP” are the values (respectively “bd” and “app”).

  • set: a collection of unique values that do not have any secondary identifiers or ordering.
vrf_set = [
    "PROD_VRF",
    "DEMO_VRF"
]

The “vrf_set” is the name of the set, “PROD_VRF” and “DEMO_VRF” are the string values inside it.

Let’s review the main.tf file:

# Provider Declaration
terraform {
  required_providers {
    aci = {
      source = "ciscodevnet/aci"
    }
  }
}

# Provider Configuration
provider "aci" {
  username = local.username
  password = local.password
  url = local.url
}

# Tenant
resource "aci_tenant" "prod-tenant" {
  name = local.tenant
  description = "Production Tenant"
}

# VRF
resource "aci_vrf" "prod-vrf" {
  tenant_dn  = aci_tenant.prod-tenant.id
  for_each = var.vrf_set
  name = each.value
  ip_data_plane_learning = "enabled"
  knw_mcast_act = "permit"
  pc_enf_dir = "ingress"
  pc_enf_pref = "enforced"
}

# Application Profile
resource "aci_application_profile" "prod-app" {
  for_each = var.app_set
  name = each.value
  tenant_dn = aci_tenant.prod-tenant.id
}

# EPGx LOOP EXTERNAL VARIABLES
resource "aci_application_epg" "prod-epg" {
  for_each = var.epg_map
  name = each.key
  relation_fv_rs_bd = aci_bridge_domain.prod-bd[each.value.bd].id
  application_profile_dn = aci_application_profile.prod-app[each.value.app].id
  pc_enf_pref = "unenforced"
  pref_gr_memb = "include"
  prio = "unspecified"
}

# BDx LOOP EXTERNAL VARIABLES
resource "aci_bridge_domain" "prod-bd" {
  for_each = var.bd_map
  name = each.key
  arp_flood = "yes"
  unicast_route = "yes"
  unk_mac_ucast_act = "flood"
  relation_fv_rs_ctx = aci_vrf.prod-vrf[each.value.vrf].id
  tenant_dn  = aci_tenant.prod-tenant.id
}

# Subnet
resource "aci_subnet" "prod-bdsubnet" {
  for_each = var.bd_map
  ip = each.value.bd_subnet
  parent_dn = aci_bridge_domain.prod-bd[each.key].id
  scope = [ "public" ]
  ctrl = ["unspecified"]
}

# Physical Domain
# Pay attention that the Physical Domain created does not have any VLAN Pool associated.
resource "aci_physical_domain" "prod-domain" {
  name  = local.phy_domain
}

# EPG to Domain
resource "aci_epg_to_domain" "prod-epg_to_domain" {
  for_each = var.epg_map
  application_epg_dn = aci_application_epg.prod-epg[each.key].id
  tdn = aci_physical_domain.prod-domain.id
}

# VLAN Pool
resource "aci_vlan_pool" "Physical_VLAN-Pool" {
  name = "Physical_VLAN-Pool"
  alloc_mode = "static"
}
resource "aci_ranges" "prod-vlanpool-range" {
  vlan_pool_dn = aci_vlan_pool.Physical_VLAN-Pool.id
  from = "vlan-2"
  to = "vlan-1000"
  alloc_mode = "static"
}

# Physical Domain
resource "aci_physical_domain" "Physical_Dom" {
  name = "Physical_Dom"
  relation_infra_rs_vlan_ns = aci_vlan_pool.Physical_VLAN-Pool.id
}

# AAEP
resource "aci_attachable_access_entity_profile" "Physical_AAEP" {
  name = "Physical_AAEP"
}

# AAEP and Domain Association
resource "aci_aaep_to_domain" "aaep_to_domain" {
  attachable_access_entity_profile_dn = aci_attachable_access_entity_profile.Physical_AAEP.id
  domain_dn = aci_physical_domain.Physical_Dom.id
}

Let’s review how “set” has been used in our code:

# VRF
resource "aci_vrf" "prod-vrf" {
  tenant_dn  = aci_tenant.prod-tenant.id
  for_each = var.vrf_set
  name = each.value
}

We called the “vrf_set” by using the command “for_each = var.vrf_set”.
“vrf_set” is composed by 2 different VRFs: DEMO_VRF and PROD_VRF:

vrf_set = [
    "PROD_VRF",
    "DEMO_VRF"
]

The resource will iterate the set values and create both VRFs.

Let’s review how “map” has been used in our code:

# EPGx LOOP EXTERNAL VARIABLES
resource "aci_application_epg" "prod-epg" {
  for_each = var.epg_map
  name = each.key
  relation_fv_rs_bd = aci_bridge_domain.prod-bd[each.value.bd].id
  application_profile_dn = aci_application_profile.prod-app[each.value.app].id
  pc_enf_pref = "unenforced"
  pref_gr_memb = "include"
  prio = "unspecified"
}

We called the “epg_map” by using the command “for_each = var.epg_map”.
“epg_map” is composed by several keys that contain several values:

epg_map = {
    "PROD_EPG1" = {
        bd = "PROD_BD"
        app = "PROD_APP"
    }
    "PROD_EPG2" = {
        bd = "PROD_BD"
        app = "PROD_APP"
    }
    "PROD_EPG3" = {
        bd = "PROD_BD"
        app = "PROD_APP"
    }
    "DEMO_EPG1" = {
        bd = "DEMO_BD"
        app = "DEMO_APP"
    }
}

The name of the EPG in the resource match the “epg_map” Key values:

name = each.key

The name of the BD and Application Profile that are associated to the EPG are specified by using:

relation_fv_rs_bd = aci_bridge_domain.prod-bd[each.value.bd].id
application_profile_dn = aci_application_profile.prod-app[each.value.app].id

It matches the value called “bd” and the value called “app” in the “epg_map”.

Perfect! Now that we prepared our code, we can run it:

terraform init

01

terraform plan

02

terraform apply -auto-approve

03

Here is the GUI output from Tenant tab: 04

05

Now that you learn how to scale you Cisco ACI fabric, good luck and enjoy your free time! 😉

Here is my GitHub page with this example
Here is my GitHub page for Terraform (WIP)

DISCLAIMER

I take no responsibility for any damage you may do running the scripts I provide here. Use it at your own risk, always test before running in production.

Thanks for your time I hope that you’re enjoying my blog!
If you have some questions, please drop me a message through social networks!😊
👈 You can find the relative icons here on the left of the page

Riccardo