Intermediate · Infrastructure as code with Terraform · topic 33 of 53 · 35 min
Modules, environments and Terraform in CI
Explanation
What a module is
Every folder of .tf files is a module. The folder where you run terraform is the root module. A root module can call other modules, called child modules, to reuse a group of resources. Think of a module as a function: variables are its parameters, outputs are its return values, and the resources inside are its body.
modules/vpc/
main.tf # the resources: VPC, subnets, route tables
variables.tf # inputs: name, cidr_block, azs, public_subnets ...
outputs.tf # outputs: vpc_id, public_subnet_ids ...
versions.tf # required_providers
README.mdCalling a module
module "vpc" {
source = "./modules/vpc" # a local path
name = "portal-${var.environment}"
cidr_block = "10.20.0.0/16"
azs = ["ap-south-1a", "ap-south-1b"]
}
module "network" {
source = "terraform-aws-modules/vpc/aws" # from the public Registry
version = "~> 5.0" # pin a version you have tested
# ... inputs ...
}
resource "aws_instance" "app" {
subnet_id = module.vpc.public_subnet_ids[0] # read a module output
# ...
}source can be a local path, a Git URL or a Registry address. Registry modules accept a version argument; always pin it, so an update cannot change your infrastructure without a review. The public Terraform Registry (registry.terraform.io) lists providers and community modules. Read a module's code before you trust it with production.
Structuring environments
You need dev, staging and production that are alike but separate. There are two common ways.
| Approach | How | Good | Watch out |
|---|---|---|---|
| Directory per environment | envs/dev, envs/staging, envs/prod, each a small root module calling shared modules, each with its own backend key | Clear, separate state and permissions per environment; prod can pin an older module version | Some repetition between folders |
| Workspaces | One folder; terraform workspace new staging; state is stored per workspace | Less code | Easy to apply to the wrong workspace; same backend and credentials for all environments |
Many teams choose a directory per environment for production systems, because a mistake in dev cannot touch prod state, and access can differ. Whatever you choose, keep one state per environment (and often per component, such as network and database) so the "blast radius" of a bad apply stays small.
Naming and tagging
Use one naming pattern, such as project-environment-component (portal-prod-db). Tag every resource with at least project, environment, owner and cost centre. Tags let finance split the cloud bill by team and let engineers find who owns a forgotten server. On AWS, the provider's default_tags setting adds tags to every resource automatically.
The dependency lock file
terraform init writes .terraform.lock.hcl, which records the exact provider versions and checksums chosen. Commit it, so every laptop and CI runner uses the same provider build. Run terraform init -upgrade on purpose when you want newer versions, and review the change.
Terraform in CI/CD
- On every pull request: terraform fmt -check, terraform validate, a linter such as tflint, and a security scanner such as Checkov.
- Run terraform plan and post the plan as a comment on the pull request, so reviewers see the real effect.
- After merge to main: apply, ideally the saved plan file. For production, require a manual approval step.
- Give CI short-lived cloud credentials (for example OIDC federation) instead of long-lived keys stored in the repo.
terraform fmt -check -recursive
terraform init -input=false
terraform validate
tflint
checkov -d .
terraform plan -input=false -out=tfplan
# after merge and approval:
terraform apply -input=false tfplanPolicy as code means writing rules that every plan must pass, such as "no S3 bucket may be public" or "every resource needs an owner tag". Checkov and tfsec-style scanners have many built-in rules. Open Policy Agent (OPA) lets you write your own rules in a language called Rego; HCP Terraform also supports policy checks.
Terraform and OpenTofu
In August 2023 HashiCorp changed Terraform's licence from the open-source MPL to the Business Source License (BSL 1.1), which limits some commercial uses. In response, the community created OpenTofu, an open-source fork now hosted by the Linux Foundation. Its command is tofu (tofu init, tofu plan, tofu apply), and it reads the same HCL files and providers. The two have since added some different features, so check each tool's documentation before switching. For learning, the ideas in this module apply to both.
Common mistakes
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Modules, environments and Terraform in CI · 1/7
In this lesson you will reuse Terraform code with modules and run Terraform safely as a team.
Prefer a video by a teacher? Find free videos on “terraform modules environments ci cd pipeline best practices” on YouTube ↗
In real life: One VPC module, three environments, at a fintech
A payments startup in Gurugram has a vpc module in its platform repo. envs/dev, envs/staging and envs/prod each call it with a different CIDR range and their own S3 state key. Pull requests run fmt, validate, tflint, Checkov and plan for every environment touched, and the plan appears as a PR comment. Merges apply dev and staging automatically; prod waits for a lead engineer to approve in the pipeline. Every resource carries project, environment, owner and cost-centre tags, so the monthly AWS bill is split by team.
envs/
dev/main.tf -> module "vpc" { source = "../../modules/vpc" cidr_block = "10.10.0.0/16" }
staging/main.tf -> module "vpc" { ... cidr_block = "10.20.0.0/16" }
prod/main.tf -> module "vpc" { ... cidr_block = "10.30.0.0/16" }
modules/
vpc/ -> main.tf, variables.tf, outputs.tfPractice (0/3 done)
Inside a root module, how do you read the output vpc_id of a module called "vpc"?
A tagging policy check. resources maps a resource name to its tags dict. required is a list of tag keys every resource must have. Write missing_tags(resources, required) that returns a dict mapping each non-compliant resource name to a sorted list of its missing tag keys. A tag whose value is an empty string counts as missing. Compliant resources are left out.
Design a reusable VPC module for AWS. Write its variables.tf (inputs with types, descriptions and sensible defaults where safe) and outputs.tf, and show how envs/prod/main.tf would call it. You do not need to write every resource in main.tf; list which resources it would contain.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Why infrastructure as codeInfrastructure as code with Terraform · same module
- Terraform for the cloudCloud Computing (AWS, Azure, Google Cloud) · Infrastructure as code
- IaC in a team: reviews, pipelines and policyCloud Computing (AWS, Azure, Google Cloud) · Infrastructure as code
- Why infrastructure as codeCloud Computing (AWS, Azure, Google Cloud) · Infrastructure as code
- GitOps and platform engineeringGitOps, platform engineering and DevOps careers
- GitHub ActionsCI/CD pipelines
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Why pin the version of a Registry module?
Answer: So an upstream update cannot change your infrastructure without review
Pinning means updates happen only when you change the version in a reviewed pull request.
What is a main risk of using workspaces for dev and prod?
Answer: It is easy to run apply in the wrong workspace, and all environments share one backend and credentials
Workspaces share code, backend and usually credentials, so a slip can hit prod.
What should happen to .terraform.lock.hcl?
Answer: Commit it to Git
The lock file pins exact provider versions and checksums for everyone, so it belongs in Git.
Which step usually runs on a pull request, before merge?
Answer: terraform plan, with the output shown to reviewers
Reviewers should see the plan before anything changes; apply comes after merge and approval.
What is OpenTofu?
Answer: An open-source fork of Terraform under the Linux Foundation, run with the tofu command
OpenTofu was forked after Terraform moved to the Business Source License in 2023.
Why should terraform.tfstate not be committed to Git?Related
Answer: It can contain secrets in plain text and it must be shared with locking, which Git cannot do
State can hold passwords and keys, and it needs locking to stop two applies at once. Use an encrypted remote backend.
How do you refer to the id of resource "aws_s3_bucket" "logs"?Related
Answer: aws_s3_bucket.logs.id
A resource address is TYPE.NAME, and you add the attribute after another dot.
From Terraform basics: HCL, providers, resources and the workflow
Your company has 200 resources that were created in the console. What is the usual way to bring them under Terraform?Related
Answer: Import them (import blocks or terraform import), then plan until it shows no changes
Importing adds existing resources to the state; a clean plan proves the code matches them.
What does terraform state rm aws_s3_bucket.legacy do to the real bucket?Related
Answer: Nothing: Terraform just stops tracking it
state rm only removes the record. The real resource keeps existing.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Infrastructure as code with Terraform”. The module exam comes next.
- Module 7 exam: Infrastructure as code with Terraform · 13 questions · 20 min, without help. 4 topics of this module still to finish first.
- Then the next module, Configuration management with Ansible, and YAML. 53 topics left in the course before the final exam.