Advanced Terraform Modules and Workspaces
Advanced Terraform Modules and Workspaces
Introduction
As you progress through your Terraform journey for Cisco infrastructure automation, you reach a point where simple, flat configuration files no longer scale. Managing dozens of resources across multiple environments with copy-pasted HCL blocks becomes error-prone, difficult to maintain, and nearly impossible to share across teams. This is where Terraform modules and supporting advanced techniques come into play.
In this lesson, we cover the advanced Terraform capabilities that take your automation from prototype-quality to production-grade. You will learn how to manage multiple Terraform versions with tfenv, enforce consistent formatting with terraform fmt, visualize your infrastructure as a dependency graph with terraform graph, interactively test expressions with terraform console, and most importantly, build reusable modules that abstract complexity and promote configuration re-use across your organization.
By the end of this lesson, you will be able to:
- Install and switch between Terraform versions using tfenv
- Format and lint your HCL files automatically
- Generate and interpret resource dependency graphs
- Use the Terraform console for interactive troubleshooting
- Design, build, and consume Terraform modules for Cisco infrastructure
Key Concepts
Terraform Version Management with tfenv
Terraform ships as a single, compiled binary written in Go. While installation is straightforward through package managers or a direct download to your system's $PATH, version management is entirely your responsibility. Different projects may require different Terraform versions, and running the wrong version can cause unexpected behavior or state file incompatibilities.
tfenv is a CLI tool that manages installed versions of Terraform in your executable path, similar in concept to pyenv for Python or rbenv for Ruby. It provides cross-platform support and works with all versions of Terraform. A key feature is its ability to automatically install the correct Terraform version based on project folder definitions, meaning each project directory can pin its own required version.
Formatting, Graphing, and Console
| Tool | Command | Purpose |
|---|---|---|
| terraform fmt | terraform fmt | Automatically formats .tf files for consistent readability |
| terraform graph | terraform graph | Outputs a visual dependency graph in DOT format |
| terraform console | terraform console | Interactive interrogation of variables, configurations, and state |
Terraform Modules
Just as Ansible uses roles to organize and reuse automation content, Terraform uses the concept of modules for simplification and abstraction of configuration. Every project directory is considered a "module" in the eyes of Terraform. Within a root module, all .tf files are analyzed and interpolated, including those in subfolders. This foundational concept is what makes the module system so powerful: you define a structure of desired state, abstract the underlying complexity, and feed only the required information to consumers of that module.
How It Works
The Terraform Workflow and Where Advanced Tools Fit
The standard Terraform workflow follows a predictable cycle:
- Write HCL -- You author your infrastructure definitions
- terraform plan -- Terraform calculates the changes needed
- terraform apply -- Terraform executes those changes
This cycle works well for simple configurations where errors are likely typos or syntactical issues. Single-level variable creation using maps and assignments makes troubleshooting straightforward. But as your configurations grow in complexity, you need tools to validate, visualize, and test your code before entering this cycle. That is exactly where the advanced tools discussed in this lesson fit in.
How terraform fmt Works
Terraform is based on Go, which brings well-defined structures, types, and syntax conventions. While whitespace is not a dealbreaker the way it is in Python, properly formatted code enables significantly easier readability. The terraform fmt command does the heavy lifting so you do not have to manually align blocks and arguments. One important detail: terraform fmt only impacts .tf files. It will not touch JSON files, variable definition files with other extensions, or any non-HCL content in your project directory.
How terraform graph Works
Terraform is an end-state declarative tool, meaning it builds a resource graph representing the relationships and dependencies among all resources. This resource graph is generated at all stages of the Terraform lifecycle and includes state information if a state file is present. The graph uses all files in the project folder to build its representation.
The native output of terraform graph is in DOT format, which is the standard syntax used by Graphviz. This DOT output can then be converted to a PNG image using the dot command-line tool, giving you a visual map of your entire infrastructure and how each resource depends on others.
How terraform console Works
The terraform console provides interactive interrogation of your Terraform environment. You can inspect:
- Variables -- Check the current values of your input and local variables
- Configurations -- Examine resource and data source definitions
- State -- Query the current state of deployed resources (if a state file is present)
You can also perform variable and data interpolations based on built-in functions within Terraform. This makes it invaluable for testing complex expressions before embedding them in your HCL. However, it is important to note that there is no Terraform REPL (Read-Eval-Print Loop) in the traditional programming sense. The console is specifically for interrogation and expression evaluation, not for executing arbitrary Terraform operations.
How Modules Work
A Terraform module is essentially a directory containing .tf files that define a set of related resources. When you call a module from your root configuration, you pass in variables, and the module produces resources and optionally returns outputs. The module encapsulates all of the implementation details, exposing only the variables you choose to make available.
Within the root module, Terraform recursively analyzes and interpolates all .tf files. When a child module is referenced, Terraform treats its directory as a self-contained unit, resolving its own internal variables and resource references independently before integrating the results into the overall resource graph.
Configuration Example
Managing Terraform Versions with tfenv
Install and use tfenv to switch between versions:
tfenv install 1.7.0
tfenv install 1.6.6
tfenv use 1.7.0
terraform version
You can also pin a version for a specific project by creating a .terraform-version file in your project directory. When you enter that directory, tfenv automatically selects the correct binary.
Formatting Your Configuration
Run the formatter against your project directory:
terraform fmt
This command rewrites all .tf files in the current directory to follow the canonical HCL style. Indentation, alignment of equals signs, and block structure are all corrected automatically. You can also run it recursively:
terraform fmt -recursive
Generating a Resource Graph
Generate a DOT-format graph and convert it to a visual PNG:
terraform graph
This outputs DOT syntax to standard output. To convert it into a viewable image:
terraform graph | dot -Tpng > infrastructure-graph.png
The resulting image shows every resource, data source, and provider as nodes in a directed graph, with edges representing dependencies. This is extremely useful for understanding complex configurations that span dozens of resources.
Using the Terraform Console
Launch the interactive console:
terraform console
Once inside, you can evaluate expressions:
> var.tenant_name
"prod"
> length(var.vlans)
5
> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"
This lets you test interpolations and function calls against your actual variable definitions and state without modifying any infrastructure.
Building a Terraform Module for Cisco ACI
A module directory structure for an ACI tenant configuration might look like this:
modules/
aci-tenant/
main.tf
variables.tf
outputs.tf
The variables.tf file defines the inputs the module accepts:
variable "tenant_name" {
type = string
description = "Name of the ACI tenant"
}
variable "vrf_name" {
type = string
description = "Name of the VRF within the tenant"
}
variable "bd_name" {
type = string
description = "Name of the bridge domain"
}
The main.tf file contains the resource definitions:
resource "aci_tenant" "this" {
name = var.tenant_name
}
resource "aci_vrf" "this" {
tenant_dn = aci_tenant.this.id
name = var.vrf_name
}
resource "aci_bridge_domain" "this" {
tenant_dn = aci_tenant.this.id
name = var.bd_name
relation_fv_rs_ctx = aci_vrf.this.id
}
To consume this module from your root configuration:
module "production_tenant" {
source = "./modules/aci-tenant"
tenant_name = "prod"
vrf_name = "prod_vrf"
bd_name = "web_servers"
}
Best Practice: By abstracting the ACI tenant, VRF, and bridge domain into a single module, you remove configuration complexity from the consumer. Teams only need to provide three simple variables instead of understanding the full ACI object model and its dependencies.
Real-World Application
When to Build Modules
Modules are appropriate in three primary scenarios drawn from production experience:
-
Simplify configuration -- Abstract the complexity and present only top-level variables and resources to module consumers. This reduces the cognitive load on operators who need to deploy infrastructure but do not need to understand every underlying parameter.
-
Prevent configuration issues -- By removing configuration options that should not be modified, you create an easier-to-interrogate automation layer. This acts as a guardrail, ensuring that only approved parameter combinations are possible.
-
Configuration re-use -- Common sets of automation routines can be packaged and shared across teams and organizations. A well-designed module for Cisco ACI tenant provisioning, for example, can be consumed by multiple business units without each team writing their own implementation.
Design Considerations
When designing modules for Cisco infrastructure automation, consider the following:
| Consideration | Recommendation |
|---|---|
| Variable scope | Expose only the parameters that consumers need to change |
| Default values | Provide sensible defaults for optional parameters |
| Output values | Return resource IDs and attributes that callers may need |
| Version pinning | Use tfenv with a .terraform-version file per project |
| Code formatting | Run terraform fmt as part of your CI/CD pipeline |
| Graph validation | Use terraform graph to verify dependency chains before applying |
Important: Always use
terraform consoleto validate complex expressions and variable interpolations before committing them to your module code. This saves significant debugging time compared to running the full plan-apply cycle to discover issues.
Multi-Environment Deployments
Combining modules with separate variable files enables consistent deployments across environments. You define your module once and instantiate it with different inputs for development, staging, and production. Each environment gets identical resource structures with environment-specific parameters such as naming conventions, IP addressing, and scale.
Summary
- tfenv solves Terraform version management by allowing multiple versions to coexist, with automatic version selection based on project directory configuration.
- terraform fmt enforces consistent HCL formatting across all
.tffiles, improving readability and reducing code review friction. - terraform graph generates DOT-format dependency graphs that can be visualized as PNG images, providing a clear picture of resource relationships.
- terraform console enables interactive testing of variables, expressions, and built-in functions against your configuration and state without modifying infrastructure.
- Terraform modules are the primary mechanism for simplifying, abstracting, and reusing infrastructure configuration, enabling teams to share standardized automation patterns across an organization.
Moving forward, practice building your own modules for common Cisco infrastructure patterns. Start with simple single-resource modules and gradually increase complexity as you become comfortable with variable passing, output values, and module composition. Combine these advanced techniques with version control and CI/CD pipelines to establish a mature infrastructure-as-code practice for your network automation workflows.