Terraform Cert Guide Chapter 3 Part 1: Providers, Resources, and Variables Explained
Chapter 3 of Ravi Mishra’s certification guide is where Terraform stops being theoretical and starts getting real. You installed it in Chapter 2, now you actually write configuration code. This first half covers three fundamentals: providers, resources, and input variables across Azure, AWS, and GCP.
Let me walk you through what the chapter covers and what actually matters.
Terraform Providers: How Terraform Knows Where to Go
Here is the first question Mishra raises: if you write a Terraform config to create a virtual network, how does Terraform know you mean Azure and not AWS or Google Cloud?
The answer is providers.
A provider is a plugin binary that gets downloaded when you run terraform init. It is the thing that actually talks to cloud APIs. The AzureRM provider talks to Azure. The AWS provider talks to AWS. The Google provider talks to GCP. Simple as that.
Every provider lives in the Terraform Registry, which is basically a package manager for infrastructure plugins. You can use official ones from HashiCorp, community-contributed ones, or even write your own.
Best practice: put your provider config in a file called providers.tf or required_providers.tf. You can technically put it in any .tf file, but keeping it separate makes your codebase easier to navigate.
AzureRM Provider
The Azure provider setup looks like this:
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "2.54.0"
}
}
}
provider "azurerm" {
features {}
subscription_id = "..."
client_id = "..."
client_secret = "..."
tenant_id = "..."
}
The key arguments are subscription_id, client_id, client_secret, and tenant_id. All of them can be sourced from environment variables (ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, etc.) instead of hardcoding them. And you should use environment variables. Never put secrets directly in your Terraform files.
The features {} block is required and a bit unique to Azure. It lets you control behavior for specific resources like Key Vault, VMs, and template deployments. For example, you can tell Terraform whether to recover soft-deleted key vaults or permanently delete a Log Analytics workspace on destroy.
Version Constraints
Mishra spends time on version pinning, and for good reason. Here is the cheat sheet:
>= 2.54.0means that version or newer= 2.54.0means exactly that version~> 2.54.0means any2.54.xrelease (this is the most useful one)>= 2.46, <= 2.54means anything in that range
The ~> operator is the one you will use the most. It lets you get patch updates while blocking major version changes that might break things.
Provider Aliases
What if you need to deploy resources across multiple Azure subscriptions? Use the alias argument:
provider "azurerm" {
features {}
}
provider "azurerm" {
features {}
alias = "nonprod_01_subscription"
}
resource "azurerm_resource_group" "example" {
provider = azurerm.nonprod_01_subscription
name = "example-resources"
location = "West Europe"
}
You can also mix different providers entirely. The book shows using azurerm and random together, where a random integer gets appended to a resource group name. Practical trick for avoiding naming collisions.
AWS Provider
Same concept, different arguments:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.35.0"
}
}
}
provider "aws" {
region = "us-east-1"
access_key = "..."
secret_key = "..."
}
Three main arguments: access_key, secret_key, and region. All sourceable from environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION). Same rule applies: do not hardcode credentials.
The alias and version arguments work exactly the same as Azure.
Google Cloud Provider
terraform {
required_version = ">= 1.0"
required_providers {
google = {
source = "hashicorp/google"
version = "3.63.0"
}
}
}
provider "google" {
credentials = file("account.json")
project = "my-google-project-id"
region = "europe-west2"
zone = "europe-west2-b"
}
Google uses a JSON credentials file instead of individual key/secret pairs. You also specify project, region, and zone. The zone is a nice addition since GCP is more zone-aware than Azure tends to be.
Everything else (alias, version constraints) works the same way.
Terraform Resources: The Actual Infrastructure Code
Providers tell Terraform where to go. Resources tell it what to build.
A resource block is the core building block of any Terraform config. It declares an infrastructure object you want to create, update, or destroy. The syntax is consistent across all clouds:
resource "<provider_resource_type>" "<local_name>" {
argument1 = "value1"
argument2 = "value2"
}
The resource type always starts with the provider name (aws_, azurerm_, google_). The local name is your internal reference, something you pick for referring to this resource elsewhere in your code.
Azure Resources
Mishra uses an Azure Public IP example:
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "West Europe"
}
resource "azurerm_public_ip" "azure-pip" {
name = "my-public-ip"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
allocation_method = "Dynamic"
}
Notice how the public IP references the resource group using azurerm_resource_group.example.location. That is how you chain resources together. Terraform figures out the dependency graph automatically.
AWS Resources
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}
Same pattern. The type is aws_instance, the local name is web. The arguments (ami, instance_type) are specific to EC2 instances.
Google Cloud Resources
resource "google_project" "my_project" {
name = "My Project"
project_id = "your-project-id"
org_id = "1234567"
}
resource "google_app_engine_application" "app" {
project = google_project.my_project.project_id
location_id = "us-central"
}
Same pattern again. The App Engine resource references the project resource for its project argument.
The key takeaway from the book here: the resource block syntax is identical across all providers. Only the arguments change based on what each cloud service expects.
Terraform Input Variables: Stop Hardcoding Everything
Hardcoding values in resource blocks works for demos, but it is terrible for real projects. Terraform variables let you parameterize your configs so you can reuse them across environments.
The syntax is var.<variable_name> when referencing, and a variable block for declaring.
Azure Variable Example
Here is the public IP example with variables:
# main.tf
resource "azurerm_resource_group" "example" {
name = var.rgname
location = var.rglocation
}
resource "azurerm_public_ip" "azure-pip" {
name = var.public_ip_name
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
allocation_method = var.allocation_method
idle_timeout_in_minutes = var.idle_timeout_in_minutes
tags = var.tags
}
Then you declare those variables in variables.tf:
variable "rgname" {
description = "(Required) Name of the Resource Group"
type = string
default = "example-rg"
}
variable "rglocation" {
description = "Resource Group location"
type = string
default = "West Europe"
}
variable "tags" {
description = "Tags to assign to the resource"
type = map(any)
default = {
environment = "Test"
Owner = "Azure-Terraform"
}
}
Each variable has a description, a type (string, number, map, etc.), and an optional default value.
Four Ways to Provide Variable Values
This is the part worth memorizing for the cert exam. Terraform accepts variable values from four sources:
- Environment variables with the
TF_VAR_prefix (e.g.,export TF_VAR_rgname=example-rg) - Variable definition files named
terraform.tfvars,terraform.tfvars.json, or anything ending in.auto.tfvars - Runtime prompts where Terraform asks you to type a value if it is missing
- Default values defined in the variable block itself
The precedence order matters: environment variables and runtime values override terraform.tfvars, which overrides default values.
If you use a custom filename like testing.tfvars, you need to explicitly tell Terraform about it: terraform apply -var-file="testing.tfvars".
AWS and GCP Variables
The variable syntax is exactly the same for AWS and GCP. The only difference is what you are parameterizing.
AWS EC2 example:
variable "ami" {
description = "Name of the AMI"
type = string
}
variable "instance_type" {
description = "Name of the instance type"
type = string
}
GCP App Engine example:
variable "myproject_name" {
description = "Name of the google project"
type = string
}
variable "project_id" {
description = "Name of the project ID"
type = string
}
Values go in terraform.tfvars the same way regardless of which cloud you target.
What to Take Away
Chapter 3 Part 1 boils down to three concepts:
- Providers connect Terraform to a cloud. They are plugins downloaded via
terraform init. Pin your versions. Use environment variables for credentials. Use aliases when you need multiple configurations of the same provider. - Resources are the actual infrastructure declarations. The syntax is the same across all clouds. Only the provider-specific arguments differ.
- Variables let you parameterize everything. Declare them in
variables.tf, provide values through environment variables,.tfvarsfiles, runtime input, or defaults.
If you are studying for the cert, make sure you know the version constraint operators (especially ~>) and the variable value precedence order. Those are the kinds of things that show up on the exam.
In Part 2, we will look at the other half of Chapter 3: Terraform outputs and data sources.
Previous: Chapter 2: Installation Guide
Next: Chapter 3 Part 2: Outputs and Data Sources
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.