Terraform Cert Guide Chapter 8: Understanding Terraform Configuration Files

Chapter 8 of Ravi Mishra’s book is one of those chapters that sounds basic on the surface but actually ties a lot of loose ends together. You’ve been writing Terraform code for seven chapters now, but this is where you stop and really understand the anatomy of a configuration file. What goes where, why it matters, and how the same patterns work across GCP, AWS, and Azure.

What Even Is a Terraform Configuration File?

A Terraform configuration file is a structured file that tells Terraform how to manage your infrastructure. That’s it. No magic.

You write it, Terraform reads it, and then it talks to cloud APIs on your behalf. Every cloud provider exposes their services through APIs, and Terraform knows how to call those APIs based on what you define in your config files.

Two formats exist:

  • .tf files written in HCL (HashiCorp Configuration Language). This is the human-readable format and the one you should use 99% of the time.
  • .tf.json files written in JSON. This is the machine-readable format, mostly used when config files are generated programmatically.

Both work. But if you’re writing by hand, stick with .tf.

The Basic Syntax

Every Terraform config file follows the same structure:

<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
  # Block body
  <IDENTIFIER> = <EXPRESSION>  # Argument
}

Let’s break that down with a real example:

resource "google_storage_bucket" "gcp-stg" {
  name     = "gcp-bucket"
  location = "US"

  versioning {
    enabled = true
  }
}

Four things to understand:

Blocks are containers. The block type (resource) tells Terraform what to do. The labels (google_storage_bucket and gcp-stg) identify the specific thing you’re working with. Everything between the curly braces is the block body.

Arguments assign values. name = "gcp-bucket" is an argument. The left side is the name, the right side is the value.

Identifiers are names for things. They can contain letters, digits, underscores, and hyphens. One rule: never start an identifier with a digit. Terraform will get confused and think it’s a number.

Expressions are values. Could be a string literal, a variable reference, or a combination of both.

One important thing the book emphasizes: Terraform is declarative. The order you write blocks doesn’t matter. Terraform figures out the correct execution order based on dependencies between resources. You don’t need to arrange things in any particular sequence.

Comments

Three ways to add comments:

  • # for single-line comments (this is the default and preferred style)
  • // also works for single-line comments, but terraform fmt will convert these to #
  • /* */ for multi-line block comments

Use #. Keep it simple.

The Override File Trick

Here’s something interesting that comes up on the exam. What happens when you have the same resource defined in two different files?

Normally, you get an error. Terraform will complain about duplicate resources, duplicate variables, duplicate outputs. Makes sense.

But Terraform has a special override mechanism. Files ending with _override.tf, override.tf, _override.tf.json, or override.tf.json are treated differently. Terraform reads them separately, then merges them with the main configuration, overwriting any matching arguments.

Example. You have this in main.tf:

resource "azurerm_resource_group" "rgname" {
  name     = "Terraform-rg"
  location = var.location
  tags = {
    "environment" = "development"
    "costcenter"  = "B3478"
  }
}

And this in override.tf:

resource "azurerm_resource_group" "rgname" {
  tags = {
    "environment" = "preprod"
    "costcenter"  = "C3478"
  }
}

When you run terraform plan, the tags from the override file win. The resource gets created with environment = "preprod" and costcenter = "C3478". Everything else (name, location) stays the same from main.tf.

This is useful when you have machine-generated configs and need to manually override specific values. Just make sure you comment your override files well so the next person understands why they exist.

JSON Configuration Syntax

The .tf.json format mirrors the HCL format but uses JSON structure. Here’s the same Azure resource group in JSON:

{
  "resource": {
    "azurerm_resource_group": {
      "rgname": {
        "//": "Create resource group in Azure",
        "name": "Terraform-rg",
        "location": "${var.location}",
        "tags": {
          "environment": "development",
          "costcenter": "B41892"
        }
      }
    }
  }
}

A few things to notice. Block types become JSON object keys. The nesting follows the number of labels: resource expects two labels (type and name), so it gets two levels of nesting. Comments use the "//" key, which Terraform ignores. Variable references use the "${...}" syntax inside strings.

You probably won’t write JSON configs by hand. But knowing the format is useful for the exam and for understanding auto-generated configs.

Data Types

Terraform supports these data types:

TypeDescriptionExample
stringText"Hello-Terraform"
numberWhole or fractional numbers10, 6.28
boolBooleantrue, false
list (tuple)Ordered sequence["abc", 15, false]
map (object)Key-value pairs{name = "Terraform", env = "dev"}
nullNo valueTells Terraform to skip the argument

String, number, and bool are called primitive types. Lists and maps are called complex types (or collection types, or structural types).

Style Conventions

The book lists several formatting rules, but honestly you don’t need to memorize them. Here’s why: terraform fmt -recursive does it all for you.

That said, the important ones to know:

  • Two spaces per indentation level (not tabs, not four spaces)
  • Align the equals signs for consecutive arguments at the same nesting level
  • Put arguments at the top of a block, nested blocks below, separated by a blank line
  • Put meta-arguments (like count, lifecycle) either first or last in the block, separated from regular arguments

Just write your code, run terraform fmt, and move on.

Writing Config Files for GCP

The book walks through a practical GCP example with a storage bucket and VPC. The file structure follows a standard pattern:

gcp-files/
├── main.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
└── variables.tf

The providers.tf file contains your terraform block with version constraints and the provider configuration:

terraform {
  required_version = ">= 1.0"
  required_providers {
    google = {
      version = "~> 3.0"
    }
  }
}

provider "google" {
  project = var.project_name
  region  = var.gcp_region
  zone    = var.zone
}

Key takeaways from the GCP section:

  • Never hardcode values. Use input variables for everything. Pass values through terraform.tfvars.
  • Keep credentials out of your code. Use environment variables for GCP credentials or add the credentials file to .gitignore.
  • Use modules. The example references the storage bucket module from Chapter 7. Modules keep your config short and reusable.
  • Store state remotely. Use a Google Cloud Storage bucket for your state file. It contains sensitive information, so encrypt it.
  • Use data sources when you need to reference existing resources. Use locals for values you reuse within the code.

Writing Config Files for AWS

Same structure, different provider. The AWS example creates a VPC (using a module) and an S3 bucket:

module "terraform-aws-vpc" {
  source      = "github.com/PacktPublishing/..."
  vpc_name    = var.vpc_name
  cidr_block  = var.cidr_block
  subnet_name = var.subnet_name
}

resource "aws_s3_bucket" "s3_bucket" {
  bucket = var.bucket_name
  acl    = var.bucket_acl
  tags   = var.custom_tags
}

The pattern is identical to GCP. Separate files, input variables everywhere, modules where possible.

AWS-specific best practices:

  • Store state in S3 with encryption enabled.
  • Use environment variables for AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Or store them in a CI/CD pipeline or AWS Secrets Manager.
  • S3 bucket names must be globally unique. The book suggests using Terraform’s random_string resource to generate unique names.
  • Run terraform validate to catch syntax errors before you plan or apply.

Writing Config Files for Azure

By the third cloud, the pattern should feel familiar. The Azure example provisions a VM with a KeyVault module and a storage account:

azure-files/
├── main.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
└── variables.tf

Azure-specific best practices:

  • Store state in Azure Blob Storage with encryption.
  • Authenticate using environment variables for Client_id, Client_secret, Tenant_id, and Subscription_id. Or use CI/CD pipeline variables.
  • Use modules heavily. The example reuses the Azure VM module from Chapter 7.
  • Always run terraform validate and terraform fmt before applying.
  • Define outputs in outputs.tf so you can verify what got created (VM ID, VM name, private IP, etc.).

The Pattern Across All Clouds

Here’s what Chapter 8 is really telling you. The exact same approach works everywhere:

  1. Separate your files: main.tf, variables.tf, terraform.tfvars, outputs.tf, providers.tf
  2. Use input variables instead of hardcoded values
  3. Pass variable values through terraform.tfvars
  4. Use modules to keep code short and reusable
  5. Store state remotely in an encrypted location (GCS for GCP, S3 for AWS, Blob Storage for Azure)
  6. Keep credentials secure using environment variables or secrets managers
  7. Run terraform fmt to format your code
  8. Run terraform validate to catch errors early
  9. Define outputs to verify what was created

The cloud-specific stuff (provider blocks, resource types, authentication methods) changes, but the structure and the principles stay the same. That’s the whole point of Terraform being cloud-agnostic.

Quick Exam Tips

A few things from this chapter that are likely to show up on the certification exam:

  • Two spaces per indentation level
  • # is the default comment style
  • terraform validate checks syntax errors
  • terraform.tfvars is the preferred way to pass variable values
  • Override files use the _override.tf or override.tf naming convention
  • Terraform is declarative, so block order doesn’t matter

Previous: Chapter 7 Part 2: Building Real Modules | Next: Chapter 9: Terraform Stacks

This is part of a series retelling “HashiCorp Infrastructure Automation Certification Guide” by Ravi Mishra (Packt, 2021). For the full text, grab the book - ISBN: 978-1-80056-597-5.

About

About BookGrill.net

BookGrill.net is a technology book review site for developers, engineers, and anyone who builds things with code. We cover books on software engineering, AI and machine learning, cybersecurity, systems design, and the culture of technology.

Know More