Lesson 2 of 5

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

ToolCommandPurpose
terraform fmtterraform fmtAutomatically formats .tf files for consistent readability
terraform graphterraform graphOutputs a visual dependency graph in DOT format
terraform consoleterraform consoleInteractive 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:

  1. Write HCL -- You author your infrastructure definitions
  2. terraform plan -- Terraform calculates the changes needed
  3. 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:

ConsiderationRecommendation
Variable scopeExpose only the parameters that consumers need to change
Default valuesProvide sensible defaults for optional parameters
Output valuesReturn resource IDs and attributes that callers may need
Version pinningUse tfenv with a .terraform-version file per project
Code formattingRun terraform fmt as part of your CI/CD pipeline
Graph validationUse terraform graph to verify dependency chains before applying

Important: Always use terraform console to 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 .tf files, 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.