Terraform Cert Guide Chapter 7 Part 2: Building Real Modules for AWS, Azure, and GCP
Part 1 of this chapter covered the theory: what modules are, where they come from, meta-arguments, and source types. Now it’s time to actually build stuff. Part 2 is where Ravi Mishra walks you through creating real Terraform modules for all three major cloud providers: Azure, AWS, and GCP.
The nice thing here? Once you understand the pattern for one provider, the others are basically the same structure with different resource names. The book makes this pretty clear by repeating the same workflow three times.
The Azure VM Module
The book starts with Azure. The example creates a virtual machine module that includes a NIC, KeyVault, and the VM itself. The module lives in a directory called azurerm-virtual-machine-module and follows the standard three-file structure you’ll see everywhere:
main.tffor the actual resource definitionsvariables.tffor input variablesoutputs.tffor what the module exposes back to the caller
Then there’s a separate directory called azurerm-virtual-machine-module-use-case that shows how to consume the module. This separation is important. The module is one thing, using the module is another thing.
The consumer side has a main.tf that calls the module like this:
module "terraform-vm" {
source = "github.com/PacktPublishing/HashiCorp-Infrastructure-Automation-Certification-Guide.git//chapter7/azurerm/azurerm-virtual-machine-module?ref=v0.0.1"
vm_name = var.vm_name
location = var.location
# ... other variables
}
The providers.tf file handles the backend configuration (storing state in Azure Blob Storage) and pins the provider version. One thing the book correctly flags: don’t hardcode your access_key in the config. Pass it through environment variables. I’ve seen people commit storage keys to Git repos in production. It’s not fun to clean up.
The outputs.tf in the consumer directory references module outputs using the module.<MODULE_NAME>.<OUTPUT_NAME> syntax:
output "vm_private_ip" {
value = module.terraform-vm.nic
}
output "vm_name" {
value = module.terraform-vm.vm_name
}
After running terraform init, terraform plan, and terraform apply, you get a working VM in Azure. The init step downloads the module from GitHub, the plan shows what will be created, and apply does the actual deployment.
The AWS VPC Module
Next up is AWS. The concept is identical, just different resources. This time the book creates a VPC with a subnet.
Same file structure in the module directory:
aws-vpc-subnet-module/
├── VERSION
├── main.tf
├── outputs.tf
└── variables.tf
The main.tf creates a VPC and a subnet:
resource "aws_vpc" "terraform-vpc" {
cidr_block = var.cidr_block
instance_tenancy = "default"
tags = {
Name = var.vpc_name
}
}
resource "aws_subnet" "terraform-subnet" {
vpc_id = aws_vpc.terraform-vpc.id
cidr_block = cidrsubnet(var.cidr_block, 8, 1)
tags = {
Name = var.subnet_name
}
}
Notice the cidrsubnet() function. It calculates a subnet CIDR from the parent VPC CIDR automatically. That’s a nice touch for making the module flexible without requiring the user to do subnet math manually.
Variables are straightforward: vpc_name, cidr_block (with a default of 10.0.0.0/16), and subnet_name. Outputs expose the vpc_id and vpc_cidr_block.
The consumer side points to a specific version of the module using a Git tag:
module "terraform-aws-vpc" {
source = "github.com/PacktPublishing/HashiCorp-Infrastructure-Automation-Certification-Guide.git//chapter7/aws/aws-vpc-subnet-module?ref=v1.0.0"
vpc_name = var.vpc_name
cidr_block = var.cidr_block
subnet_name = var.subnet_name
}
The terraform.tfvars file provides the actual values:
subnet_name = "Terraform-aws-subnet"
vpc_name = "Terraform-aws-vpc"
cidr_block = "10.0.0.0/16"
region = "us-east-1"
Run the Terraform workflow, and you get a VPC and subnet in AWS. Same pattern, different cloud.
The GCP Storage Bucket Module
For GCP, the book picks Google Cloud Storage as the example resource. Again, the structure is the same: module directory with main.tf, variables.tf, outputs.tf, and a separate use-case directory for consumption.
The module’s main.tf:
resource "google_storage_bucket" "gcp-stg" {
name = var.gcp_stg_name
location = var.gcp_location
force_destroy = var.force_destroy
storage_class = var.storage_class
project = var.project
labels = var.labels
versioning {
enabled = true
}
}
This one has more variables than the AWS example because GCS buckets have more configuration options: storage class (MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, ARCHIVE), force destroy behavior, labels as a map type, and project ID.
The outputs expose the bucket’s self_link, url, and name. After publishing with a v2.0.0 tag and consuming it, you get a storage bucket in GCP.
One important warning from the book: they included a GCP service account credential JSON file in the repo for demo purposes. Never do this in real life. Keep your credential files local or use environment variables. Pushing secrets to GitHub is one of those mistakes that can cost you real money if someone finds them.
The Pattern You Should See
If you step back and look at all three examples, the workflow is always the same:
- Write the module with
main.tf(resources),variables.tf(inputs),outputs.tf(what you expose) - Publish the module to a Git repository with a version tag
- Consume the module by referencing the source URL with a version ref
- Pass variables from the consumer to the module
- Read outputs using
module.<NAME>.<OUTPUT>syntax
The only things that change between Azure, AWS, and GCP are the resource types and their specific arguments. The module pattern itself is provider-agnostic.
Publishing Modules to Terraform Registry
The book wraps up by explaining how to share your modules with the broader community through Terraform Registry. This is basically the public marketplace for Terraform modules.
The key requirements to publish:
- Your module must be on a public GitHub repository
- The repo name must follow the format
terraform-<PROVIDER>-<NAME>(liketerraform-google-storage-bucketorterraform-aws-ec2-instance) - Include a repository description (the short one-liner on GitHub)
- Follow the standard module structure so the registry can auto-generate docs
- Use semantic versioning for release tags (like
v1.0.0or0.5.0)
The publishing process itself is simple: go to registry.terraform.io, sign in with GitHub, select your properly-named repo, and click publish. The registry picks up your tags, generates documentation from your variables and outputs, and makes the module available for anyone to use.
This is actually a nice way to contribute to the Terraform ecosystem. If you’ve written a module that solves a common problem, publishing it takes five minutes and could save someone else hours of work.
Chapter Summary
Chapter 7 Part 2 is really about one thing: practice. The theory from Part 1 gets applied to three real cloud providers, and you can see that modules are modules regardless of where you’re deploying.
Key takeaways:
- Module structure is always the same:
main.tf,variables.tf,outputs.tf - Version your modules with Git tags so consumers can pin to specific versions
- Don’t hardcode secrets in your config (storage keys, credential files, etc.)
- Module output syntax follows
module.<MODULE_NAME>.<OUTPUT_NAME> - Terraform Registry is how you share modules publicly, and it requires specific naming conventions
- The workflow is provider-agnostic: learn it once for Azure, apply the same pattern to AWS and GCP
If you’re prepping for the certification, make sure you understand the module consumption syntax, how versioning with Git tags works, and the requirements for publishing to Terraform Registry. Those are the kinds of things that show up in exam questions.
Previous: Chapter 7 Part 1: Module Sources
Next: Chapter 8: Configuration Files
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.