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:
.tffiles written in HCL (HashiCorp Configuration Language). This is the human-readable format and the one you should use 99% of the time..tf.jsonfiles 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, butterraform fmtwill 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:
| Type | Description | Example |
|---|---|---|
string | Text | "Hello-Terraform" |
number | Whole or fractional numbers | 10, 6.28 |
bool | Boolean | true, false |
list (tuple) | Ordered sequence | ["abc", 15, false] |
map (object) | Key-value pairs | {name = "Terraform", env = "dev"} |
null | No value | Tells 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_IDandAWS_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_stringresource to generate unique names. - Run
terraform validateto 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, andSubscription_id. Or use CI/CD pipeline variables. - Use modules heavily. The example reuses the Azure VM module from Chapter 7.
- Always run
terraform validateandterraform fmtbefore applying. - Define outputs in
outputs.tfso 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:
- Separate your files:
main.tf,variables.tf,terraform.tfvars,outputs.tf,providers.tf - Use input variables instead of hardcoded values
- Pass variable values through
terraform.tfvars - Use modules to keep code short and reusable
- Store state remotely in an encrypted location (GCS for GCP, S3 for AWS, Blob Storage for Azure)
- Keep credentials secure using environment variables or secrets managers
- Run
terraform fmtto format your code - Run
terraform validateto catch errors early - 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 styleterraform validatechecks syntax errorsterraform.tfvarsis the preferred way to pass variable values- Override files use the
_override.tforoverride.tfnaming 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.