Providers & resources
What we’re building
Section titled “What we’re building”One tiny resource on each cloud, in plain Terraform — no Terragrunt yet. An S3 bucket on AWS, a Cloud Storage bucket on GCP, and a resource group on Azure. Each is nearly free, quick to destroy, and enough to exercise the whole loop: declare a provider, declare a resource, then init, plan, and apply.
We’re using bare Terraform on purpose. Seeing three almost-identical provider blocks by hand is what makes Module 3’s Terragrunt feel like a relief rather than a mystery.
A provider is the plugin that translates HCL into a cloud’s API — aws, google, azurerm. You pin its version in a required_providers block so a colleague (or a CI run six months from now) gets the same behavior you did, and you configure it (region, project, subscription) in a provider block. A resource is one thing that provider manages: a bucket, a network, a database.
The init/plan/apply loop is the heartbeat of Terraform. init downloads the pinned providers; plan diffs your config against reality and shows exactly what will change; apply makes it so. You’ll run that loop thousands of times — learn to trust the plan, and never apply one you haven’t read.
Pros & cons
Section titled “Pros & cons”Pinning providers with ~> vs. taking whatever’s latest:
- Pros: Reproducible.
~> 6.0accepts 6.x bug fixes but never a breaking 7.0, soapplydoes the same thing on every machine and in CI. - Cons: You own the upgrades. Bumping a major means reading an upgrade guide and re-testing — but an unpinned provider that silently jumps a major is far worse.
Plain Terraform per cloud (this lesson) vs. jumping straight to Terragrunt:
- Pros: You learn the engine directly — providers, state, the plan — with nothing between you and the API. When something breaks later, you know which layer to blame.
- Cons: It’s repetitive. Three provider blocks, three backends, three copies of the same settings. That repetition is the problem Terragrunt exists to solve, and you’ll feel it by the end of this module.
Set it up
Section titled “Set it up”These are throwaway experiments, so keep them out of the real tree — a scratch/ directory you’ll delete before Module 4 builds the actual modules.
1. scratch/aws/main.tf — the AWS provider + an S3 bucket
Section titled “1. scratch/aws/main.tf — the AWS provider + an S3 bucket”Bucket names are globally unique, so we suffix a random string:
terraform { required_providers { aws = { source = "hashicorp/aws", version = "~> 6.0" } random = { source = "hashicorp/random", version = "~> 3.6" } }}
provider "aws" { region = "us-east-1"}
resource "random_id" "suffix" { byte_length = 4}
resource "aws_s3_bucket" "first" { bucket = "clouddeploy-first-${random_id.suffix.hex}"}2. scratch/gcp/main.tf — the Google provider + a Cloud Storage bucket
Section titled “2. scratch/gcp/main.tf — the Google provider + a Cloud Storage bucket”terraform { required_providers { google = { source = "hashicorp/google", version = "~> 6.0" } random = { source = "hashicorp/random", version = "~> 3.6" } }}
provider "google" { project = "YOUR_GCP_PROJECT_ID" region = "us-central1"}
resource "random_id" "suffix" { byte_length = 4}
resource "google_storage_bucket" "first" { name = "clouddeploy-first-${random_id.suffix.hex}" location = "US-CENTRAL1" uniform_bucket_level_access = true force_destroy = true}3. scratch/azure/main.tf — the azurerm provider + a resource group
Section titled “3. scratch/azure/main.tf — the azurerm provider + a resource group”The azurerm v4 provider requires a features {} block and a subscription. The provider reads ARM_SUBSCRIPTION_ID from the environment, so you don’t hardcode it:
terraform { required_providers { azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" } }}
provider "azurerm" { features {} # subscription_id comes from the ARM_SUBSCRIPTION_ID env var, # or set it here explicitly.}
resource "azurerm_resource_group" "first" { name = "clouddeploy-first" location = "eastus"}export ARM_SUBSCRIPTION_ID="$(az account show --query id -o tsv)"Every one of these authenticates with the CLI credentials you set up in Module 1 — no keys in these files.
Verify
Section titled “Verify”Run the loop in each directory. AWS shown; GCP and Azure are identical commands:
cd scratch/awsterraform init # Terraform has been successfully initialized!terraform plan # Plan: 2 to add, 0 to change, 0 to destroy.terraform apply # type 'yes' when promptedA successful apply ends with:
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.Now confirm the resources really exist, straight from the cloud CLIs:
aws s3 ls | grep clouddeploy-firstgcloud storage buckets list --filter="name:clouddeploy-first"az group show --name clouddeploy-first --query name -o tsvThen tear them down so nothing lingers on the bill:
terraform destroy # Destroy complete! Resources: 2 destroyed.You’re done when all three clouds went from plan → apply → a CLI-confirmed resource → clean destroy.
Check your understanding:
- What’s the difference between a
required_providersblock and aproviderblock? - Why does
~> 6.0protect you where a bare “use the latest” would eventually bite? - What does
terraform planshow you, and why should you neverapplya plan you haven’t read? - None of these files contains a cloud credential — so how does each provider authenticate?
You’ve configured all three providers, created and destroyed a first resource on each cloud, and run the init/plan/apply loop. You also felt the repetition — three provider blocks that differ only in the details. Before we fix that with Terragrunt, we need to understand the thing Terraform tracks between applies: state. Next up, state, variables, and outputs.