$PATH
.tf
files.Within Terraform, projects are defined at the folder level. The top-level folder will generally house the HCL file with the required provider(s) and is where the workflow steps are performed. Folders within a directory structure underneath the top-level folder can be used to separate component HCL files based on type, location, or any operational or business need.
Terraform has a very procedural set of steps to follow in order to deploy a configuration to a device/controller. Also, under each step is a set of optional (or highly recommended) flags or switches to use.
terraform init
: Initializes the project folder by reading in the required providers from the top-level HCL file, downloads them, and adds them to the .terraform
folder within the project.terraform plan
: Reads in the top-level HCL file (and any HCL files nested within the project) to create a “graph” of the configuration and how it will interact with the infrastructure, ensuring that all configuration is applied in the correct order.terraform plan
, it is considered best practice to use an “outfile” by invoking the -out
switch with some filename (-out "plan.tf"
, for example). This creates a record of the steps to be followed when the apply
action is invoked based on the configuration in the HCL file at time the plan
action was run.plan
and apply
stages, creating a disparity between what was intended to run and what was actually applied.terraform apply
: Executes the steps determined by the Terraform configuration and the subsequent graph to instantiate the desired configuration on the end device/controller.-auto-approve
switch to automatically apply the configuration without answering yes
at the “Are you sure” prompt.terraform destroy
: Removes the configuration based on what is contained within the HCL file and the state file that was saved during the apply
stage.terraform fmt
: Will automatically align and format your Terraform files within your project directory. Unlike YAML-based tools, Terraform HCL will still work when the format is not exactly as intended; however, the fmt
command will help improve readability for the files if they are not formatted to spec.terraform graph
: Outputs the results of Terraform computing the configuration graph to a file with a .dot
extension, which can then be converted to other image formats. This output gives a visual representation of the computations and dependencies for the intended configuration and the order in which it must be applied to the end device/controller.All top-level HCL configuration files will follow a similar structure. For the sake of consistency, we’ll refer to this file as the main.tf
file.
At the top of the file, all required providers are declared in the terraform
block:
terraform {
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "> 2.5.0"
}
}
}
Notice the name that we’ve defined the provider as (aci
). While we are working with ACI, we could have used any locally significant name; however, it is best to use the provider’s preferred local name—usually, the first word of the provider’s resource types.
The provider configuration defines how Terraform will connect to the end device/controller, and it is placed directly underneath the required providers list. For our example, we’ll need the URL, username, and password of the ACI controller:
provider "aci" {
username = "admin"
password = "!v3G@!4@Y"
url = "https://sandboxapicdc.cisco.com"
insecure = true
}
The insecure
parameter is set because this controller is using a self-signed certificate. In a production environment, verification should occur as a best practice.
The final step is to add in the desired configuration we wish to apply to our ACI fabric. In this example, we’ll just configure a single tenant, using the following configuration:
resource "aci_tenant" "terraform_tenant" {
name = "CiscoU-Tenant-01"
description = "First CISCOU Demo Tenant"
}
The construction of the configuration blocks within Terraform will always follow the <TYPE> "<ACTION_NAME>" "<LOCAL_NAME>"
format. <TYPE>
is either an item to configure (resource
) or an item to read in (a data source or data
), the <ACTION_NAME>
is the name of the name of the resource defined by the provider (these are found through the docs pages for your provider), and the <LOCAL_NAME>
is a locally significant name of your choosing. We’ve also defined some arguments within the configuration block contained within the {}
, which includes the tenant name and a description.
Putting this configuration together, we’ll get something like this:
terraform {
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "> 2.5.0"
}
}
}
# Configure the provider with your Cisco APIC credentials.
provider "aci" {
username = "admin"
password = "!v3G@!4@Y"
url = "https://sandboxapicdc.cisco.com"
insecure = true
}
# Define desired ACI tenant
resource "aci_tenant" "terraform_tenant" {
name = "CiscoU-Tenant-01"
description = "CISCOU Demo Tenant"
}
Copy and paste this configuration into an IDE of your choice, and save it into a folder of your choosing, calling it main.tf
:
mkdir terraform-intro/ && mkdir terraform-intro/example_01
cd terraform-intro/example_01
This configuration will deploy just fine, but in order to make sure that configuration blocks are easier to read, there are some formatting changes that can be made. Let’s invoke terraform fmt
:
The changes are small, but you can see that the fmt
command aligns elements within a configuration block to add to readability. This approach allows you to focus on your desired intent and not the formatting of your configuration.
Now that the configuration is generated, let’s begin our workflow!
Now that the configuration has been created, it can be applied to the fabric. We’ll use steps detailed earlier to initialize (init
) Terraform, determine the changes to be made (plan
), and then apply the configuration (apply
).
Notice that because we did not use a “plan outfile”, Terraform ensured that we wanted to apply the configuration. Now let’s destroy the tenant (using terraform destroy
) and make a few changes to our main.tf
. Change the name of the tenant suffix from 01
to 02
so that the file looks like this:
terraform {
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "> 2.5.0"
}
}
}
# Configure the provider with your Cisco APIC credentials.
provider "aci" {
username = "admin"
password = "!v3G@!4@Y"
url = "https://sandboxapicdc.cisco.com"
insecure = true
}
# Define desired ACI tenant
resource "aci_tenant" "terraform_tenant" {
name = "CiscoU-Tenant-02"
description = "CISCOU Demo Tenant"
}
Let’s run through the same workflow, but let’s add the -out
switch to the plan step and tell Terraform to apply the steps created in the “plan” file:
Notice that Terraform did not prompt us to confirm the deployment, because it is deploying a configuration that has already been seen through the plan
step. Before moving on, let’s destroy the configuration, using terraform destroy
.
Let’s look at a more complex configuration. Again, don’t worry if you’re not experienced with ACI and its constructs.
In the following configuration, we are adding some network functionality to the tenant, including a context (VRF), a bridge domain, and a subnet. We could hardcode the values that we want for these configuration items (such as names, IP addresses, etc.), but we can also create Terraform configurations that separate the configuration from the values applied to the config using variables. We do this by creating a new “variables” HCL file (typically called variables.tf
), which contains the desired values, and then the main.tf
file is modified such that each resource block looks to a variable of a given name for its data. Let’s expand this concept to a larger configuration file (adding some network pieces to the overall tenant).
Let’s start by creating a new folder in which to place this configuration. (Do not nest this folder within the one containing the previous configuration; place them on the same folder depth.)
cd ../
mkdir example_02
cd example_02
Next, copy and paste this configuration into your IDE of choice and save it as main.tf
:
terraform {
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "> 2.5.0"
}
}
}
# Configure the provider with your Cisco APIC credentials.
provider "aci" {
username = var.user.username
password = var.user.password
url = var.user.url
insecure = true
}
# Define desired ACI tenant
resource "aci_tenant" "terraform_tenant" {
name = var.tenant
description = "CISCOU Demo Tenant"
}
# Tenant VRF (CTX)
resource "aci_vrf" "terraform_vrf" {
tenant_dn = aci_tenant.terraform_tenant.id
description = "CISCOU Demo Tenant VRF"
name = var.vrf
}
# Tenant CTX's BD
resource "aci_bridge_domain" "terraform_bd" {
tenant_dn = aci_tenant.terraform_tenant.id
relation_fv_rs_ctx = aci_vrf.terraform_vrf.id
description = "CISCOU Demo Tenant BD"
name = var.bd
}
# Tenant BD's subnet
resource "aci_subnet" "terraform_bd_subnet" {
parent_dn = aci_bridge_domain.terraform_bd.id
description = "CISCOU Demo Tenant Subnet"
ip = var.subnet
}
Without getting too far into the details, we’re adding more constructs, but we’re using variables to reference certain values (seen by the var
prefix). Let’s look at the corresponding variables.tf
file. Copy and paste this into a new file and save it in the same folder as the main.tf
from above:
variable "user" {
description = "Login information"
type = map
default = {
username = "admin"
password = "!v3G@!4@Y"
url = "https://sandboxapicdc.cisco.com"
}
}
variable "tenant" {
type = string
default = "CiscoU-Tenant-03"
}
variable "vrf" {
type = string
default = "CiscoU-Tenant-03-VRF"
}
variable "bd" {
type = string
default = "CiscoU-Tenant-03-BD"
}
variable "subnet" {
type = string
default = "10.10.100.1/24"
}
Looking at the variables file, we can see that the names of the variables (the values in quotes) align directly with the variables that are referenced in the main.tf
file. In the case of the tenant
, vrf
, bd
, and subnet
variables, there is only a single argument, allowing them to be referenced with a dotted double (var.<NAME>
). However, for the user
variable, a map is used, requiring a dotted triple to reference the appropriate argument (var.user.VALUE
).
When we perform the Terraform workflow, notice that we don’t need to specify which files to include in the plan and apply stages, because Terraform treats each folder as a project and will combine all *.tf
files automatically. Let’s apply this toward our infrastructure:
After we apply the configuration, you can choose whether to remove the configuration using the destroy action.
Note: We did not use terraform fmt
on the configuration file (though we could have). Terraform is not picky about whitespace within the HCL, but using the fmt
tool will make the configuration easier to read because all elements within a configuration block are aligned.
The final example we’ll do will illustrate the fact that Terraform is an end-state declarative tool. This means that we can write out configuration resources in any order, and Terraform will use its dependency graph that is created at runtime to ensure all configuration is applied in the order that is required by the end infrastructure. Let’s create a new project folder within the terraform-intro
directory for this new configuration:
cd ../
mkdir example_03/
cd example_03
We’ll start by taking the main.tf
folder from the previous step but rearranging the configuration elements:
terraform {
required_providers {
aci = {
source = "CiscoDevNet/aci"
version = "> 2.5.0"
}
}
}
# Configure the provider with your Cisco APIC credentials.
provider "aci" {
username = var.user.username
password = var.user.password
url = var.user.url
insecure = true
}
# Tenant CTX's BD
resource "aci_bridge_domain" "terraform_bd" {
tenant_dn = aci_tenant.terraform_tenant.id
relation_fv_rs_ctx = aci_vrf.terraform_vrf.id
description = "CISCOU Demo Tenant BD"
name = var.bd
}
# Tenant BD's subnet
resource "aci_subnet" "terraform_bd_subnet" {
parent_dn = aci_bridge_domain.terraform_bd.id
description = "CISCOU Demo Tenant Subnet"
ip = var.subnet
}
# Tenant VRF (CTX)
resource "aci_vrf" "terraform_vrf" {
tenant_dn = aci_tenant.terraform_tenant.id
description = "DCISCOU Demo Tenant VRF"
name = var.vrf
}
# Define desired ACI tenant
resource "aci_tenant" "terraform_tenant" {
name = var.tenant
description = "CISCOU Demo Tenant"
}
We have the same elements as the previous step, but in an order that if applied top-to-bottom would fail (the relationship is Tenant > VRF > BD > Subnet
). Our variables.tf
file will be the same as previous, with a few name changes:
variable "user" {
description = "Login information"
type = map
default = {
username = "admin"
password = "!v3G@!4@Y"
url = "https://sandboxapicdc.cisco.com"
}
}
variable "tenant" {
type = string
default = "CiscoU-Tenant-04"
}
variable "vrf" {
type = string
default = "CiscoU-Tenant-04-VRF"
}
variable "bd" {
type = string
default = "CiscoU-Tenant-04-BD"
}
variable "subnet" {
type = string
default = "10.100.100.1/24"
}
If we walk through the workflow as usual, the configuration will be applied, just as it was when it was in the “proper” order.
But why? We can inspect this using the terraform graph
command. By default, when we run terraform graph
, it will create a “dotfile,” which is a flat text file containing shapes, names, and relationships, similar to a “finite state machine” diagram. The output for this configuration will look similar to the following (you must have run terraform init
prior to running the graph
command):
terraform graph > terraform_dotfile.dot
cat terraform_dotfile.dot
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] aci_bridge_domain.terraform_bd (expand)" [label = "aci_bridge_domain.terraform_bd", shape = "box"]
"[root] aci_subnet.terraform_bd_subnet (expand)" [label = "aci_subnet.terraform_bd_subnet", shape = "box"]
"[root] aci_tenant.terraform_tenant (expand)" [label = "aci_tenant.terraform_tenant", shape = "box"]
"[root] aci_vrf.terraform_vrf (expand)" [label = "aci_vrf.terraform_vrf", shape = "box"]
"[root] provider[\"registry.terraform.io/ciscodevnet/aci\"]" [label = "provider[\"registry.terraform.io/ciscodevnet/aci\"]", shape = "diamond"]
"[root] var.bd" [label = "var.bd", shape = "note"]
"[root] var.subnet" [label = "var.subnet", shape = "note"]
"[root] var.tenant" [label = "var.tenant", shape = "note"]
"[root] var.user" [label = "var.user", shape = "note"]
"[root] var.vrf" [label = "var.vrf", shape = "note"]
"[root] aci_bridge_domain.terraform_bd (expand)" -> "[root] aci_vrf.terraform_vrf (expand)"
"[root] aci_bridge_domain.terraform_bd (expand)" -> "[root] var.bd"
"[root] aci_subnet.terraform_bd_subnet (expand)" -> "[root] aci_bridge_domain.terraform_bd (expand)"
"[root] aci_subnet.terraform_bd_subnet (expand)" -> "[root] var.subnet"
"[root] aci_tenant.terraform_tenant (expand)" -> "[root] provider[\"registry.terraform.io/ciscodevnet/aci\"]"
"[root] aci_tenant.terraform_tenant (expand)" -> "[root] var.tenant"
"[root] aci_vrf.terraform_vrf (expand)" -> "[root] aci_tenant.terraform_tenant (expand)"
"[root] aci_vrf.terraform_vrf (expand)" -> "[root] var.vrf"
"[root] provider[\"registry.terraform.io/ciscodevnet/aci\"] (close)" -> "[root] aci_subnet.terraform_bd_subnet (expand)"
"[root] provider[\"registry.terraform.io/ciscodevnet/aci\"]" -> "[root] var.user"
"[root] root" -> "[root] provider[\"registry.terraform.io/ciscodevnet/aci\"] (close)"
This is a bit hard to read, but luckily, we can translate this using graphviz
(this must be installed on your local machine, either manually or through a package manager such as Homebrew for macOS or apt
or yum
for various Linux distributions). Let’s rerun the graph command and translate it to a PNG file:
terraform graph | dot -Tpng > tf_graph.png
The resultant PNG output will look like this:
Without knowing much about ACI, we can discern the relationship that follows what was given before as Tenant > VRF > BD > Subnet
. Terraform will build this graph with every configuration to ensure that the items are applied in the order required for that device/controller. In this way, we can focus on the intent of the configuration rather than the individual steps and the order in which they should be placed.
Congratulations! You have successfully built Terraform configurations, learned some formatting tips and tricks, and seen how Terraform ensures the consistency of configuration application, regardless of which order you build the elements of that config.