Terraform Roadmap
Terraform is a tool for describing cloud and platform infrastructure in files and applying it with one repeatable workflow. Platform engineers, DevOps and cloud engineers, and developers who own their own infrastructure use it to build networks, clusters, databases, DNS and access rules across AWS, Azure, Google Cloud and many other services.
This roadmap starts with why infrastructure is written as code and ends with pipelines, policy and governance. In between you learn the language, providers, variables, state, modules, testing and security scanning. State and modules get the most attention, because they cause most real-world problems and most interview questions. Each topic is marked core, recommended or optional, so you can see what to learn first.
By the end you will be able to design a small multi-environment setup, review a plan critically and explain how you would keep it safe in a team. Practise in study plan and keep track of your progress in progress page. The commands work with OpenTofu too, which is covered where it differs.
- Level
- Beginner to advanced
- Time
- 10-14 weeks at about 8 hours a week
- Scope
- 57 topics in 9 stages
- Before you start
- Basic use of a terminal, covered in Shell and Bash Roadmap and Linux Roadmap; A free-tier account with one cloud provider; the AWS Roadmap roadmap is a good match; Familiarity with networks, servers and storage, see Cloud Computing Roadmap; Version control habits from Git and GitHub Roadmap
What you will be able to do
- Write and read HCL, and explain how Terraform plans and applies changes.
- Use providers, variables, locals, outputs and data sources to build reusable configurations.
- Store state remotely with locking and repair it safely with state commands and import.
- Design modules with clean interfaces and use published ones with pinned versions.
- Test configurations with terraform test and scan them with linters and policy tools.
- Run Terraform from a CI pipeline with review, approval and short-lived credentials.
- Explain trade-offs such as workspaces against directories, and count against for_each.
The roadmap at a glance
- Stage 1Infrastructure as Code and First StepsBeginner · 1 week · 7 topics
- Stage 2The Language: Providers, Resources and VariablesBeginner · 2 weeks · 9 topics
- Stage 3Meta Arguments, Expressions and Resource LifecycleIntermediate · 2 weeks · 7 topics
- Stage 4Terraform CLI CommandsIntermediate · 1 week · 6 topics
- Stage 5State and Importing Existing InfrastructureIntermediate · 2 weeks · 7 topics
- Stage 6ModulesIntermediate · 2 weeks · 4 topics
- Stage 7Provisioners, Workspaces and ScalingIntermediate · 1-2 weeks · 7 topics
- Stage 8Testing, Security and Cost ControlAdvanced · 2 weeks · 6 topics
- Stage 9Automation: CI/CD and HCP TerraformAdvanced · 2 weeks · 4 topics
Stage 1 Infrastructure as Code and First Steps
Goal. Explain why infrastructure is written as code, install Terraform and run a first plan, apply and destroy cycle.
What is Infrastructure as Code? Core
Infrastructure as Code (IaC) means describing servers, networks, databases and permissions in text files that a tool reads and turns into real resources. The files live in version control, so every change has an author, a diff, a review and a history you can roll back.
Without it, infrastructure is built by clicking in a console. That is fast once, but nobody can repeat it, review it or explain why a setting changed. IaC gives you repeatable environments, a shared source of truth and safer changes. It also makes disaster recovery more realistic, because a lost environment can be rebuilt from the same files.
Practice. Write down five things in a cloud console you have clicked through, and describe what a file for each would need to contain.
CaC vs IaC Recommended
Configuration as Code (CaC) is the neighbouring idea. IaC provisions the resources themselves: a virtual machine, a subnet, a bucket. CaC configures what runs inside them: packages, users, service files and application settings. Tools such as Ansible, Chef and Puppet sit mostly on the CaC side, while Terraform, OpenTofu, Pulumi and CloudFormation sit on the IaC side.
The line is blurry, and many teams use both. A common split is Terraform to create the instances and network, then an image build tool or Ansible to prepare the software. Increasingly, containers and managed services shrink the CaC part, because the image carries its own configuration.
Practice. Pick a small web service and list which parts would be IaC and which would be CaC.
What is Terraform? Core
Terraform is a tool from HashiCorp that reads configuration written in HashiCorp Configuration Language and creates, changes or deletes the matching resources through provider plugins. It is cloud-neutral: the same workflow drives AWS, Azure, Google Cloud, Kubernetes, GitHub, Cloudflare, Datadog and hundreds of other services.
It is declarative. You state what should exist, not the steps to get there. Terraform compares your files with its record of what it built and with what the provider reports, then shows a plan of the differences. You approve the plan, and it applies the changes in dependency order.
Licensing changed in 2023 to the Business Source License, and the community responded with OpenTofu, an open-source fork run under the Linux Foundation. The language and workflow are nearly identical, so this roadmap applies to both.
Practice. Read the first page of the Terraform documentation and write three sentences describing the plan and apply workflow in your own words.
Benefits of Terraform and When to Use? Core
The main benefits are a single workflow across many providers, a readable language, a plan step that previews changes, a large registry of providers and modules, and state that tracks what exists. Because it is declarative, running it twice with no changes does nothing.
Use Terraform for long-lived infrastructure that several people change: networks, clusters, databases, DNS, identity and access, and the platform pieces under applications. It is less suited to configuring software inside machines, to one-off scripts, or to resources that change many times a day under application control.
Be honest about the costs too. State needs care, provider upgrades can change behaviour, and a mistake in a shared configuration can delete real data. A good team treats Terraform code like production code.
Practice. Name one resource in a project you know that suits Terraform and one that does not, with a reason for each.
Installing Terraform and Version Management Core
Install Terraform from a package manager such as Homebrew or your Linux distribution's repository, or download the single binary and put it on your path. Check it with terraform version. OpenTofu installs the same way and uses the tofu command.
Different projects need different versions, and an upgrade can alter behaviour. Version management means pinning the version for each project. Tools such as tfenv or asdf switch versions per directory, and a .terraform-version file records the choice. Inside the configuration, required_version makes Terraform refuse to run with an incompatible binary:
terraform {
required_version = ">= 1.6.0, < 2.0.0"
}Use the same version on laptops and in CI, or you will get puzzling plan differences.
Practice. Install Terraform, print its version and add a required_version constraint to an empty configuration.
Basic Syntax and HashiCorp Configuration Language (HCL) Core
What is HCL? It is the configuration language Terraform uses, designed to be easy to read and write for people and still machine-friendly. Files end in .tf. The building blocks are blocks, arguments and expressions.
A block has a type, optional labels and a body in braces. An argument assigns a value to a name. Expressions can reference other objects, call functions and use conditionals.
resource "aws_s3_bucket" "logs" {
bucket = "example-logs-12345"
tags = {
team = "platform"
}
}Here resource is the block type, aws_s3_bucket the resource type and logs the local name. Other objects refer to it as aws_s3_bucket.logs. Comments use # or // for single lines and /* */ for blocks. Terraform loads every .tf file in a directory as one configuration, so file names are only for your organisation. JSON syntax with .tf.json also exists, mainly for generated code.
Practice. Write a configuration with one resource of your choice and label every part of it: block type, labels, arguments and references.
Project Initialization and Deployment Workflow Core
Project initialization starts with a directory holding your .tf files. The usual layout has main.tf for resources, variables.tf, outputs.tf and versions.tf for provider and Terraform constraints. This is a convention, not a rule.
The core deployment workflow has four steps. terraform init downloads providers and modules and configures the backend. terraform plan shows what would change. terraform apply makes the change after you confirm. terraform destroy removes everything the configuration manages.
Always read the plan. It marks creations with +, deletions with -, in-place updates with ~ and replacements with -/+. Replacement of a database, for example, is a signal to stop and check. Commit the lock file .terraform.lock.hcl, and add the .terraform directory and any local state files to .gitignore.
Practice. Initialise a directory, run plan and apply for a harmless resource such as a random_pet, then run destroy as clean up.
Stage 2 The Language: Providers, Resources and Variables
Goal. Configure providers, declare resources and make configurations reusable with variables, locals, outputs and data sources.
Providers and Configuring Providers Core
A provider is a plugin that knows how to talk to one API and exposes its resource types and data sources. The aws provider offers aws_instance, the google provider offers google_compute_instance, and so on. Terraform downloads providers during init from the registry or a mirror.
You declare which providers you need and their versions in required_providers, then configure each with a provider block, where the region, project or endpoint goes:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-1"
}The ~> operator allows only the rightmost version part to rise, so ~> 5.0 accepts 5.x but not 6.0. Aliases let you configure the same provider twice, for example two regions, and pass the right one to a resource with the provider meta argument.
Practice. Configure the AWS or another provider with a pinned version and a second aliased configuration for another region.
Authentication and Provider Credentials Core
Authentication tells the provider who you are. The golden rule is to keep credentials out of your .tf files, because those go into version control. Providers read standard sources instead: environment variables such as AWS_ACCESS_KEY_ID, shared credential files, cloud CLI logins, or an instance or workload identity.
For people, short-lived credentials from single sign-on are best. For pipelines, prefer identity federation, where the CI system proves its identity with a token and the cloud issues temporary access. This removes long-lived keys entirely. For AWS, the provider can also assume a role through an assume_role block, which suits separate accounts per environment.
If a secret does end up in a file, rotate it, because deleting a commit does not undo exposure.
Practice. Set up your provider to authenticate through environment variables or a CLI login with no secrets in the code.
Resources and Resource Behavior Core
A resource block declares one infrastructure object. Terraform tracks it in state under its address, such as aws_instance.web. Arguments configure it, and attributes, some known only after creation, can be read by other blocks.
Resource behavior depends on the kind of change. Some arguments can be updated in place. Others force replacement, meaning Terraform destroys the object and creates a new one. The plan shows which applies, and the provider documentation marks arguments that force a new resource. Terraform builds a dependency graph from references: if a subnet refers to a VPC's id, the VPC is created first.
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
subnet_id = aws_subnet.main.id
}Resources that nothing refers to are still created. Order is by references, not by position in the file.
Practice. Create a VPC and subnet where the subnet references the VPC, then read the plan to see the order Terraform chose.
Variables and Input Variables Core
Input variables are the parameters of a configuration. You declare them in variable blocks and use them as var.name. A variable can have a description, a default, a type and a sensitive flag. Without a default it is required, and Terraform prompts for it or fails in automation.
variable "instance_count" {
type = number
description = "How many web servers to run"
default = 2
}Set values in several ways. In order of increasing precedence: defaults, environment variables, terraform.tfvars and *.auto.tfvars files, then -var-file and -var on the command line. A good habit is that the root configuration has few, clearly described variables and each environment supplies its own values.
Practice. Declare three variables with descriptions and set them using a tfvars file and a command line flag.
Type Constraints and Validation Rules Core
Type constraints stop wrong input early. The primitive types are string, number and bool. Collections are list(type), set(type) and map(type). Structural types are object({...}) and tuple([...]), and any lets Terraform infer the type. Objects can mark attributes as optional(type, default), which is useful for module inputs.
Validation rules add logic the type cannot express:
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be dev, staging or prod."
}
}The error appears at plan time with your message. Use validation for allowed values, name formats and sensible ranges. A variable validation can refer only to the variable itself in older versions, and to other objects in recent ones, so check your version.
Practice. Write an object-typed variable with an optional attribute and a validation rule, then test it with a bad value.
Variable Definition File and Environment Variables Core
A variable definition file is a plain list of assignments, usually named terraform.tfvars or prod.tfvars:
environment = "prod"
instance_count = 4
tags = { team = "platform" }Terraform loads terraform.tfvars and any *.auto.tfvars automatically. Other names need -var-file=prod.tfvars. This is the usual way to keep one codebase with a file per environment.
Environment variables with the TF_VAR_ prefix also set variables, for example TF_VAR_environment=prod. They are handy in CI, where the pipeline injects values. Other environment variables control Terraform itself: TF_LOG=debug turns on logging, TF_IN_AUTOMATION adjusts output for pipelines, and TF_CLI_ARGS adds default flags. Do not put secrets in tfvars files you commit.
Practice. Create dev.tfvars and prod.tfvars and run plan against each, then repeat using TF_VAR_ environment variables.
Local Values Core
A local value gives a name to an expression so you can reuse it. Declare them in a locals block and refer to them as local.name:
locals {
name_prefix = "${var.project}-${var.environment}"
common_tags = {
project = var.project
environment = var.environment
}
}Use locals to avoid repeating the same expression, to compute a value from several variables or to give a clear name to something awkward. Do not overuse them. If a local is used once and the expression is already clear, it only adds a lookup for the reader. Unlike variables, locals cannot be set from outside, so they are internal to the module.
Practice. Refactor a configuration so that a common tags map and name prefix are defined once as locals.
Outputs and Sensitive Outputs Core
Outputs publish values from a configuration. At the root they are printed after apply and can be read with terraform output. In a child module they are how the parent reads results. Output Syntax is a block with a value, and optionally description and sensitive:
output "web_ip" {
value = aws_instance.web.public_ip
description = "Public address of the web server"
}Setting sensitive = true hides the value in the CLI output. This is only display masking. The value is still stored in the state file in plain text, and anyone who can read the state can read it. Treat sensitive outputs as hidden from logs, not as secure. Use terraform output -json for scripts, and -raw for a single string with no quotes.
Practice. Add outputs for two resource attributes, mark one as sensitive and read both with terraform output.
Data Sources Core
A data source reads information that exists outside your configuration, or outside the current state, without managing it. It is declared with a data block and read with the data. prefix.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-amd64-server-*"]
}
}Typical uses are looking up the latest machine image, finding an existing VPC by tag, reading the current account id, or reading a remote state output. Data sources are read during plan when their arguments are known. If they depend on something not yet created, the read is deferred to apply, which can make the plan less precise. Prefer explicit identifiers over fragile lookups for critical values.
Practice. Use a data source to look up the current region or account and an existing resource, and print them as outputs.
Stage 3 Meta Arguments, Expressions and Resource Lifecycle
Goal. Create many similar resources, control ordering and protect critical resources with lifecycle rules and checks.
Meta Arguments Core
Meta arguments work on any resource type and change how Terraform treats the block rather than configuring the remote object. The main ones are count, for_each, depends_on, provider and lifecycle. Modules accept most of them too, apart from lifecycle.
Learn them as a set. count and for_each decide how many instances exist. depends_on adds ordering Terraform cannot see. provider picks an aliased provider configuration. lifecycle adjusts creation, update and deletion rules. The sections below cover each one.
Practice. List the five meta arguments from memory and write one sentence on the purpose of each.
count Core
count creates several instances of a resource from a number. Inside the block count.index gives the position, and the resource becomes a list.
resource "aws_instance" "web" {
count = 3
ami = var.ami_id
instance_type = "t3.micro"
tags = { Name = "web-${count.index}" }
}You refer to one with aws_instance.web[0]. The weakness is identity by position. If you remove the first of three, the others shift and Terraform wants to destroy and recreate them. So count suits identical resources or an on/off switch: count = var.enabled ? 1 : 0. For items that have their own identity, use for_each.
Practice. Create three identical resources with count, then remove the first one from a list-driven setup and observe the plan.
for_each Core
for_each takes a map or a set of strings and creates one instance per element. Inside, each.key and each.value identify the element, and instances are addressed by key, such as aws_s3_bucket.logs["audit"].
resource "aws_iam_user" "team" {
for_each = toset(["asha", "ben", "chen"])
name = each.key
}Because identity is the key, adding or removing one element changes only that instance. This is why for_each is usually preferred over count. The keys must be known at plan time, so you cannot build them from values only known after apply. Convert lists with toset, and use maps when each instance needs several settings. The same meta argument works on modules.
Practice. Convert a count-based configuration to for_each over a map and compare how each handles removing the middle item.
Expressions, Functions and Dynamic Blocks Core
Expressions are how values are computed. You will use string templates ("${var.name}-app"), conditionals (cond ? a : b), for expressions to transform collections, and splat expressions such as aws_instance.web[*].id.
locals {
upper_names = [for n in var.names : upper(n)]
by_name = { for u in var.users : u.name => u }
}Built-in functions cover strings (format, join, split), collections (merge, lookup, flatten, concat), numbers, encoding (jsonencode, base64encode), files (file, templatefile) and networking (cidrsubnet). Test them in terraform console. A dynamic block generates repeated nested blocks, such as ingress rules, from a collection. Use it only where the nested block count truly varies, as it hurts readability.
Practice. Use terraform console to try cidrsubnet, merge and a for expression, then use a dynamic block for security group rules.
depends_on Recommended
Terraform infers order from references. Sometimes a dependency exists that no attribute reveals, for example a policy that must be attached before an instance starts using the permission. depends_on states it explicitly:
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = "t3.micro"
depends_on = [aws_iam_role_policy.app]
}Use it sparingly. Explicit dependencies make Terraform treat the whole target as a dependency, which can cause more updates and can defer data source reads. If you can express the relationship through a real reference, do that and leave a comment when you must use depends_on.
Practice. Find a case where an implicit reference is missing and add a depends_on with a comment explaining the hidden relationship.
Resource Lifecycle and the lifecycle Block Core
The lifecycle block adjusts what Terraform does when changing a resource. The common settings:
create_before_destroybuilds the replacement first, then removes the old one, which avoids downtime for things like certificates or launch templates.prevent_destroymakes any plan that would destroy the resource fail. It is a safety net for databases and buckets, though it does not stop someone deleting the block itself.ignore_changeslists attributes Terraform should leave alone, such as a tag set by another system or a desired count managed by an autoscaler.replace_triggered_byforces replacement when another resource or attribute changes.
Resource lifecycle also has the order in which Terraform creates, updates and destroys objects, driven by the graph. Using these settings hides drift on purpose, so document why each one is there.
Practice. Add prevent_destroy to a resource, try to destroy it and read the error, then add ignore_changes for a tag.
Preconditions, Postconditions and Checks Recommended
Preconditions and postconditions let a resource, data source or output assert what must be true. A precondition is evaluated before the object is created or read, and a postcondition after.
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
postcondition {
condition = self.public_ip != ""
error_message = "The instance must have a public address."
}
}
}They stop a bad apply when assumptions break, such as a looked-up image being in the wrong architecture. A separate check block, added in Terraform 1.5, runs assertions that only warn and never block, which suits ongoing health tests like an endpoint answering. Variable validation covers input, and these cover the world around it.
Practice. Add a precondition on a data source result and a check block that only warns, and trigger each.
Stage 4 Terraform CLI Commands
Goal. Use the everyday commands with confidence, including formatting, inspection and targeted replacement.
terraform fmt and Format & Validate Core
Format & Validate are the two cheap checks you run before every commit. terraform fmt rewrites files into the canonical style: two-space indentation and aligned equals signs. Use terraform fmt -recursive for a whole repository, and -check in CI to fail when files are not formatted.
terraform validate checks that the configuration is syntactically correct and internally consistent: valid references, correct argument names and types. It does not contact the provider API or check credentials, so a passing validate does not mean apply will work. It needs init first so provider schemas are available. Wire both into a pre-commit hook so errors are caught before review.
Practice. Add terraform fmt -check and terraform validate to a pre-commit hook.
terraform validate Core
Beyond formatting, terraform validate is worth knowing in detail. It catches typos in attribute names, references to missing variables, wrong types, and invalid block structure. With -json it prints machine-readable diagnostics that editors and CI can annotate.
What it cannot catch: values that only fail at the provider, such as an invalid instance type, a name that is already taken, or insufficient permissions. Those appear at plan or apply. A good pipeline therefore runs validate, then plan, and treats plan output as the real check.
Practice. Introduce three kinds of mistake into a file and note which ones validate finds and which only plan finds.
terraform plan Core
terraform plan refreshes its view of real objects, compares it with your configuration and prints the actions it would take. It changes nothing. Save the result with terraform plan -out=tfplan, then apply exactly that file with terraform apply tfplan. This guarantees that what was reviewed is what runs, which matters in pipelines.
Useful options: -var and -var-file for inputs, -target to limit to specific resources (an emergency tool, not a habit), -refresh-only to update state with real values, -destroy to preview teardown, -detailed-exitcode so scripts can tell whether changes exist (exit code 2), and -parallelism for concurrency. Read replacements and deletes closely.
Practice. Save a plan to a file, show it with terraform show, and apply that exact file.
terraform apply and the -replace option in apply Core
terraform apply shows a plan and asks for the word yes. In automation you pass a saved plan file, which skips the prompt, or use -auto-approve for plans you have already reviewed.
Sometimes a resource is unhealthy and you want it rebuilt even though the configuration has not changed. The -replace option in apply does this: terraform apply -replace="aws_instance.web". It plans a destroy and create for that one address. It replaces the older terraform taint command, because the replacement is visible in the plan and nothing is marked in state beforehand.
If apply fails halfway, state still records what succeeded. Fix the cause and run plan again. Do not delete state to make errors go away.
Practice. Rebuild a single resource using -replace and confirm in the plan that only that resource changes.
terraform destroy and Clean Up Core
terraform destroy plans and executes removal of every resource in the configuration. It is the same as terraform apply -destroy. Always run terraform plan -destroy first in anything shared.
Clean up matters for cost and tidiness. Learning projects leave behind NAT gateways, load balancers and databases that bill by the hour, so destroy at the end of each session. Resources protected by prevent_destroy will stop the run, and some resources refuse deletion while they hold data, such as a non-empty bucket without force_destroy. To remove only some resources, delete their blocks and apply, rather than using -target as routine. After a destroy, check the cloud console for stragglers created outside Terraform.
Practice. Destroy a practice project and then check the cloud console for anything left over.
Inspecting with graph, show, output and list Recommended
Several commands help you understand a configuration. terraform graph prints the dependency graph in DOT format, which Graphviz can render, useful when ordering surprises you. terraform show renders state, or a plan you saved earlier, in a readable form, and with -json for tools. terraform output reads outputs from state. terraform console evaluates expressions interactively.
For state inspection there is terraform state list, which prints every resource address. The list and show subcommands are the first things to use when you are lost. Other helpful commands are terraform providers for provider requirements and terraform version for versions.
Practice. Render the dependency graph of a small project to an image and compare it with your expectations.
Stage 5 State and Importing Existing Infrastructure
Goal. Understand what state is, store it safely, repair it when needed and bring existing resources under management.
State Core
State is Terraform's record of what it manages. It is a JSON file mapping each resource address in your configuration to the real object's id and attributes. Without it, Terraform could not tell that aws_instance.web already exists, and every plan would try to create it again.
State also caches attribute values for speed and records dependency metadata. Never edit it by hand. It may contain secrets such as database passwords, so protect it like a credential. By default it is stored locally as terraform.tfstate, which suits experiments but not teams. Real projects store it remotely with locking.
Practice. Open a local state file after an apply and find the id and attributes of one resource.
Remote State and Backends Core
Remote state means storing state in a shared backend. Common choices are an S3 bucket, Azure Storage, a Google Cloud Storage bucket or HCP Terraform. A backend block selects it:
terraform {
backend "s3" {
bucket = "example-tfstate"
key = "network/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true
}
}Enable versioning on the bucket so you can recover old state, encrypt it and limit who can read it. Backend blocks cannot use variables, so supply changing values with -backend-config during init. Other configurations can read outputs of a remote state using the terraform_remote_state data source, though many teams prefer passing explicit values or using data sources to avoid coupling.
Practice. Create a storage bucket for state with versioning on, point a project at it and migrate local state with terraform init.
State Locking and Releasing a Stuck Lock Core
State locking stops two runs from changing state at once, which could corrupt it. When a backend supports locking, Terraform takes a lock before any operation that writes state and releases it after. S3 historically used a DynamoDB table for locks, and newer versions offer a lock file in the bucket with use_lockfile. HCP Terraform, Azure Storage and Google Cloud Storage lock natively.
If a run crashes or loses its connection, the lock can remain. Then terraform force-unlock LOCK_ID removes it, where the id is shown in the error. Use it only after confirming that no other run is active. Forcing a lock off during a live apply is how state gets damaged. Never disable locking with -lock=false as a habit.
Practice. Simulate a held lock by starting an apply in one terminal and a plan in another, and read the lock error.
Inspect and Modify State: state list, show, mv, rm, pull and push Core
The terraform state commands let you inspect and carefully change state. Inspect / Modify State starts with read-only commands: state list for addresses and state show ADDRESS for one resource's attributes.
Modification commands: state mv renames or moves a resource address, for example when you refactor into a module, so Terraform does not destroy and recreate it. state rm removes a resource from state without deleting the real object, so Terraform forgets it. state pull prints the current remote state, and state push uploads a local file, which is risky and rarely needed. state replace-provider changes which provider source an existing state refers to, used when a provider moved registry namespaces.
Take a copy first with terraform state pull > backup.tfstate. In current versions, prefer the declarative moved and removed blocks, which are reviewable in code and apply on every workspace.
Practice. Move a resource into a module using terraform state mv, then repeat the idea using a moved block instead.
Import Existing Resources Core
Import brings an object that already exists into state. Older versions used the CLI command terraform import ADDRESS ID, which only updated state and left you to write matching configuration. Since version 1.5 you can use an import block, which is planned and reviewed like any change:
import {
to = aws_s3_bucket.legacy
id = "my-existing-bucket"
}Run terraform plan -generate-config-out=generated.tf to have Terraform draft the resource block for you, then clean it up. After import, the plan should show no changes. If it shows differences, adjust the configuration until it matches. Import does not create code for dependants, so do it one resource at a time and test often.
Practice. Create a bucket in the console, import it with an import block and make the plan show no changes.
Best Practices for State and Sensitive Data Core
Best practices for state: use remote storage with locking, enable versioning and encryption, give each environment or component its own state, restrict access tightly and never commit state to git. Run Terraform from a pipeline with a single identity so only that identity needs write access to state.
Sensitive data in state is the hard part. Marking variables or outputs sensitive hides them in output, but the value is still in the state file. Reduce exposure by not putting secrets in resources when possible: have the application read them from a secrets manager at run time, or use generated passwords that the cloud service stores. Recent Terraform versions add ephemeral values and write-only arguments that are never stored in state, which suit secrets where providers support them.
Practice. Audit a state file for secrets and decide how each one could be kept out of state.
Splitting Large State and Splitting State Files Recommended
A single huge state is slow to plan and risky, since every change touches the same blast radius and the same lock. Splitting State Files means dividing infrastructure into separate root configurations, each with its own state. Typical splits are by layer (network, data, application), by environment and by team ownership.
Rules of thumb: separate things that change at different speeds, keep related resources that change together in one state and avoid circular dependencies between states. Lower layers publish outputs, such as a VPC id, that higher layers read through remote state or a lookup. Splitting an existing state uses state mv with -state-out or import blocks, then removal from the old one. Do it in small steps with backups.
Practice. Plan how you would split a network, database and application stack into three states and list what each exports.
Stage 6 Modules
Goal. Package configuration into reusable modules, use published ones safely and design clean module interfaces.
Modules, Root vs Child Modules and Creating Local Modules Core
A module is a directory of .tf files. Every configuration is a module: the directory where you run Terraform is the root module, and any module it calls is a child module. Root vs child modules differ in role, not in format.
Creating local modules means putting a directory in your repository, for example modules/network, with its own variables, resources and outputs. The caller uses a module block:
module "network" {
source = "./modules/network"
cidr_block = "10.0.0.0/16"
name = "demo"
}Inside the caller, outputs are read as module.network.vpc_id. Run terraform init again after adding a module so it is loaded. Start by extracting repeated groups of resources, and give modules a narrow purpose such as a network, a service or a database, not a catch-all.
Practice. Extract a VPC with subnets into a local module with variables and outputs, and call it twice with different names.
Module Inputs, Outputs and Interfaces Core
A module's variables and outputs are its public interface. Keep it small and stable. Provide sensible defaults, document each variable, validate inputs and expose only the outputs callers need, such as ids and endpoints.
Avoid passing dozens of booleans; model optional features as an object variable with optional attributes or as separate modules. Do not read caller state from inside a module. Pass everything as inputs so the module is testable alone. Outputs of a child module are visible only to the parent, and the root must re-export them if you want them printed. Provider configuration should usually come from the caller, with the module declaring only which providers it needs.
Practice. Review a module you wrote and cut its interface to the smallest set of inputs and outputs that still works.
Terraform Registry and Published Modules Usage Core
The Terraform Registry hosts public providers and modules. Published modules cover common patterns, such as an AWS VPC or an EKS cluster, and use a short source address:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "demo"
cidr = "10.0.0.0/16"
}Published modules usage deserves care. Read the module's code and inputs, pin a version, check how actively it is maintained and understand which resources it creates and what they cost. A popular module saves days but also hides decisions. Sources can also be Git URLs, with ?ref= for a tag, or private registries such as the one in HCP Terraform. Upgrade deliberately, reading release notes and reviewing the plan.
Practice. Use a public VPC module with a pinned version, read its code and list three resources it creates that you did not expect.
Versioning and Module Best Practices Core
Module versioning follows semantic versioning: a major bump for breaking changes, a minor for new features and a patch for fixes. Publish by tagging a Git repository, such as v1.2.0, and have callers pin a version range. Local path modules have no versions, so treat changes to them as changes to every caller.
Modules best practices:
- Keep one clear purpose per module and prefer composition over deep nesting.
- Do not hard-code regions, accounts or names; take them as inputs.
- Include a short README, examples and tests.
- Avoid provisioners and provider blocks inside reusable modules.
- Use
movedblocks when renaming resources so callers upgrade without replacement.
Be wary of thin wrappers that only rename another module's variables. If a module adds no policy or defaults, call the underlying resources directly.
Practice. Tag a version of a module, call it with a pinned ref, then make a breaking change and release a new major version.
Stage 7 Provisioners, Workspaces and Scaling
Goal. Know when provisioners are a last resort, manage several environments and keep large setups fast and organised.
Provisioners and Why They Are a Last Resort Recommended
Provisioners run scripts on a resource after creation or before destruction. Terraform cannot model what a script does, so it cannot show it in a plan or detect drift. For that reason HashiCorp documents them as a last resort.
Prefer alternatives: bake software into machine images, pass boot-time configuration through user_data or cloud-init, use a configuration tool, or use managed services. Provisioners still appear in existing code and in rare bootstrapping cases, so you must read them. If a provisioner fails, the resource is marked tainted by default, and the next apply replaces it. A on_failure = continue setting changes that, at the risk of half-built machines.
Practice. Replace a remote-exec provisioner in an example with user_data and list what you gained.
local-exec, remote-exec and file provisioners Optional
There are three built-in provisioners. With local-exec, a command executes on whichever machine is running Terraform, for instance to call a script or write a file. With remote-exec, commands execute on the new resource itself over SSH or WinRM, so a connection block is required. With file, a file or directory is copied from your machine onto the resource.
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo ${self.private_ip} >> inventory.txt"
}
}Network reachability and credentials make remote provisioners fragile in pipelines. Many teams ban them. Know what each does, and what you would use instead: an image pipeline, cloud-init or a configuration management run.
Practice. Write a local-exec provisioner that records an instance IP into a file, then replace it with an output.
Custom Provisioners and Creation / Destroy Time Optional
Custom provisioners are plugins, but support for third-party provisioners has been removed in recent versions, so you should not plan on writing one. The vendor-specific ones such as chef and puppet were removed for being obsolete, and teams run those tools through other means.
Provisioners run at creation time by default. Setting when = destroy runs a command at destroy time, before the resource is removed, which can deregister a host or drain connections. Destroy-time provisioners are fragile: if they fail, the destroy fails, and they cannot refer to other resources. The null_resource and newer terraform_data resources are sometimes used to hold provisioners, with triggers_replace re-running them when an input changes.
Practice. Use a terraform_data resource with a trigger to run a local command only when a variable changes.
Template Files and the templatefile Function Recommended
Template files generate text such as scripts, configuration and policy documents from variables. The templatefile function reads a file and substitutes values:
user_data = templatefile("${path.module}/boot.sh.tftpl", {
app_port = var.app_port
env_name = var.environment
})Inside the template, ${app_port} inserts a value and %{ for ... } loops produce repeated lines. This replaced the old template_file data source, which is deprecated and archived. For JSON documents such as IAM policies, prefer jsonencode on an HCL object, which prevents quoting mistakes. Keep templates small, and test the rendered output with terraform console.
Practice. Render a boot script from a template with a loop over a list of packages and check it in terraform console.
Workspaces Recommended
Workspaces let one configuration keep several separate states. The default workspace is called default. Commands: terraform workspace new staging, select, list and delete. Inside code, terraform.workspace returns the current name, which can pick a size or a name suffix.
Workspaces are convenient for short-lived copies, like a test environment per branch. They are a weak fit for long-lived dev, staging and production, because the same backend credentials, the same code and no visible difference in the directory make mistakes easy. Many teams prefer a directory or variable file per environment, with separate state and separate access. HCP Terraform uses the word workspace differently: there it is a full project unit with its own variables, state and run history.
Practice. Create two CLI workspaces, give each a different instance size using terraform.workspace and inspect their states.
Terragrunt Optional
Terragrunt is a thin wrapper from Gruntwork that reduces repetition across many Terraform configurations. It lets you define backend settings and common inputs once in a parent file, and each environment directory inherits them with a short terragrunt.hcl. It also orchestrates dependencies between configurations and can run commands across a whole tree with run-all.
It is useful when you have dozens of root modules and copy the same backend and provider code everywhere. The cost is another tool, another file format and one more thing to learn and upgrade. Small teams are often better served by plain Terraform plus a clear layout. Know its purpose, since interviewers may ask how you keep DRY across environments.
Practice. Describe how you would lay out dev, staging and prod directories with and without Terragrunt and compare the duplication.
Scaling Terraform and Parallelism Recommended
Scaling Terraform is about people and runtime. For runtime, large states are slow because each plan refreshes every resource. Split states, use -target only for emergencies and consider -refresh=false when you are sure nothing drifted. Parallelism controls how many operations run at once, ten by default; raise it with -parallelism=20 if the provider and API rate limits allow, and lower it when you see throttling.
For people: use a clear repository layout, shared modules with versions, remote state with locking, pull request plans, and a pipeline as the only way to apply. Define ownership so each state has an owning team. Add naming and tagging standards through modules and policy checks. When many teams contribute, a platform team often publishes approved modules.
Practice. Time a plan on a large configuration, then change parallelism and compare the result.
Stage 8 Testing, Security and Cost Control
Goal. Test modules at several levels, scan for misconfigurations and manage secrets and cost before changes ship.
Testing and Unit Testing with terraform test Core
Testing infrastructure code has layers, as with application code. Native testing arrived in Terraform 1.6 with terraform test and files ending in .tftest.hcl. Each file contains run blocks that execute plan or apply, then assert on results.
run "bucket_name_is_prefixed" {
command = plan
variables {
name = "demo"
}
assert {
condition = startswith(aws_s3_bucket.this.bucket, "demo-")
error_message = "Bucket name must start with the project name."
}
}Testing modules this way is cheap. Unit testing in this style uses command = plan, so no real resources are created. Mock providers, added in version 1.7, let you run tests with no cloud account at all. Use plan-time tests for logic: naming, conditionals, validation and counts.
Practice. Write a tftest.hcl file for a module with one plan-based assertion and one failing case.
Integration Testing Recommended
Integration testing creates real resources, checks them and destroys them. With native testing you use command = apply, and Terraform destroys the resources at the end of the test. Go-based Terratest remains common: a Go test applies a configuration, queries the real system with SDK or HTTP calls, then runs destroy.
Integration tests catch what plans cannot: permissions, API limits, and resources that only work in combination. They cost money and take minutes, so run them for modules on pull requests or nightly, in a separate sandbox account with budget alerts. Always make destroy reliable, with unique name suffixes so parallel runs do not clash.
Practice. Write a test that applies a module in a sandbox account, checks one real property and destroys it.
Contract Testing and End to End Testing Optional
Contract testing checks that a module keeps its promises to callers: the variables it accepts, the outputs it returns and their types. A simple form is a test that calls the module as a consumer would and asserts on its outputs, so a change that removes an output fails before release. This matters most for shared modules with many users.
End to End Testing deploys a whole environment, such as network, database and application, and verifies the behaviour that a user sees, for example an HTTP request through the load balancer returning the expected page. These tests are slow and expensive, so keep few of them and run them before releases. Use cheaper tests for most changes and keep e2e for the critical paths.
Practice. Write a consumer-style test that asserts on a shared module output and one e2e check against a staging environment.
Linting and Scanning: TFLint, Checkov, Trivy, KICS and Terrascan Core
Static tools read your code without running it. TFLint is a linter that finds provider-specific mistakes, such as invalid instance types, deprecated syntax and unused declarations. The security scanners look for risky settings: Checkov, Trivy (which absorbed the older tfsec), KICS and Terrascan each ship policies for things like public buckets, open security groups, missing encryption and over-wide IAM permissions.
Pick one or two, not all. Run them locally through pre-commit and again in CI, and fail the build on high severity findings. Tune carefully: suppress a finding only with an inline comment that gives the reason. Scanners cannot see what happens at run time, so they complement plan review, not replace it. Overlap among them is large, so the main difference is policy coverage and how well they fit your pipeline.
Practice. Run TFLint and one scanner on a sample project, fix two findings and suppress one with a written reason.
Infracost and Cost Checks Recommended
Infracost reads a plan and estimates the monthly cost of the resources, and shows the difference a change would make. Running it on pull requests puts a cost comment next to the code review, which helps catch an accidental large instance type or an extra NAT gateway before it is deployed.
Estimates depend on usage you provide, such as requests or data transfer, so treat figures as approximate. Pair them with budgets and alerts in the cloud account, and with tags that let you attribute spend to teams. Cost policy can also be enforced: for example, fail the check if a change adds more than a threshold per month.
Practice. Run a cost estimate for a plan, change an instance size and see how the difference is reported.
Security, Secret Management and Compliance with Sentinel Core
Security in Terraform has several layers. Give the pipeline least-privilege credentials, separate accounts for environments and review IAM changes carefully. Protect state. Scan code. Pin provider and module versions and commit the lock file, so a malicious or broken release is not pulled silently.
Secret management means keeping secrets out of code and out of state where possible. Read them from a secrets manager such as AWS Secrets Manager or HashiCorp Vault at run time through data sources only when necessary, and prefer letting applications fetch their own at start. Never put secrets in tfvars committed to git.
Compliance / Sentinel: Sentinel is HashiCorp's policy-as-code language in HCP Terraform and Terraform Enterprise, running between plan and apply. Open Policy Agent with Rego is a common alternative and is also supported there. Policies enforce rules such as mandatory tags, approved regions or no public storage.
Practice. Write a policy in words (or code) that blocks public buckets and name the stage in the pipeline where it runs.
Stage 9 Automation: CI/CD and HCP Terraform
Goal. Run Terraform safely from pipelines, choose between a managed service and a self-run pipeline and keep governance in place.
CI / CD Integration Core
CI / CD integration means nobody applies from a laptop. The usual flow: a pull request runs format, validate, lint, scan and plan, and posts the plan as a comment. Reviewers approve. After merging to the main branch, a pipeline job runs apply, ideally with the saved plan from the reviewed commit or with a fresh plan and a manual approval for production.
Practical points: use short-lived federated credentials, store state remotely with locking, pin versions, cache provider downloads, run one apply at a time per state and run plan on a schedule to detect drift. Keep the destructive step behind approval. Make plan output readable, and fail the job if the plan contains unexpected deletions.
Practice. Sketch the pipeline stages for a repository from pull request to production apply, with approvals marked.
GitHub Actions, GitLab CI, Jenkins and Circle CI Recommended
Any CI system can run Terraform. The steps are the same, so learn the pattern and the syntax differences. In GitHub Actions use a workflow file with hashicorp/setup-terraform and OpenID Connect to the cloud, plan on pull_request and apply on push to main. GitLab CI uses .gitlab-ci.yml stages and has a built-in Terraform state backend and merge request widget. Jenkins uses a Jenkinsfile and needs agents with Terraform installed and a secure credential store. Circle CI has orbs for Terraform and caches.
jobs:
plan:
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- run: terraform plan -input=false -out=tfplanPass -input=false so jobs never hang waiting for a prompt, and use environments or protections to gate production.
Practice. Create a minimal pipeline in your preferred CI system that runs fmt, validate and plan on every pull request.
HCP Terraform, VCS Integration and Run Tasks Recommended
What and when to use HCP? HCP Terraform, formerly called Terraform Cloud, is HashiCorp's managed service. It offers remote state with locking, remote runs, a private registry, variable sets, access control, audit logs and policy checks. Terraform Enterprise is the self-hosted version. Use it when you want these features without building a pipeline yourself, or need governance.
VCS integration connects a workspace to a repository. A pull request triggers a speculative plan, and a merge triggers a run. Run tasks call external services, such as scanners or cost tools, at set points in a run and can block it. Other features include drift detection, no-code provisioning and teams with permissions. Alternatives are Spacelift, env0, Atlantis and plain CI.
Practice. Connect a small repository to an HCP Terraform workspace and watch a speculative plan appear on a pull request.
Enterprise Features and Governance Optional
Enterprise Features in the paid tiers add things larger organisations need: policy enforcement, single sign-on, team and project hierarchies, agents that run inside private networks, audit trails, drift detection, cost estimation and support. Governance is the practice behind them: who may change what, what must be checked first and how changes are recorded.
At small scale you can approximate much of this with a good repository setup, protected branches, CODEOWNERS and a pipeline. The questions to ask are about risk: is it acceptable for any engineer to apply to production, and how would you prove who changed a firewall rule last month? Answer those first, then pick tooling.
Practice. Write down who may approve a production apply in your current or imagined team and how that is enforced.
Projects to build
Build these in order. Each one should end up in a public repository with a short README that explains your decisions.
- Static website bucketBeginner
Create a storage bucket configured for website hosting with a variable for the name, an output for the address and tags from a local value. Use destroy to clean up afterwards.
Resources · Variables · Outputs
- Network moduleIntermediate
Build a local module that creates a VPC with public and private subnets across two zones, driven by for_each. Call it for dev and prod with different address ranges and publish the subnet ids as outputs.
Modules · for_each · Networking
- Remote state and environmentsIntermediate
Move a project to a remote backend with locking, then split it into network and application states. Read the network outputs from the application configuration and explain the trade-offs of your split.
State · Backends · Import
- Tested and scanned moduleAdvanced
Add terraform test files with mock providers, TFLint and a security scanner to a module. Include a deliberately bad example and show the scanner catching it.
Testing · Scanning · Module design
- Pipeline with approvalsAdvanced
Create a CI workflow that formats, validates, scans and plans on pull requests, posts the plan and applies on merge after a manual approval, using federated credentials instead of stored keys.
CI/CD · Security · Governance
Interview questions
Try to answer each question out loud before you open the answer.
What does terraform init do, and when must you run it again?Junior
It prepares the working directory. It downloads providers, fetches modules and configures the backend, and it writes the dependency lock file. Run it again after adding or changing a provider, a module source or version, or the backend block. It is safe to repeat, and in CI I run it with -input=false on every job because the directory starts empty.
Explain the difference between plan and apply.Junior
Plan compares configuration, state and real objects, and prints the actions needed without changing anything. Apply performs those actions. In automation I save the plan with -out and apply that file, so exactly what was reviewed is what runs, even if the code or the world changes in between.
What is state and why does it matter?Junior
State maps resource addresses in the configuration to real objects and stores their attributes. Terraform needs it to know what it already created, to compute diffs and to order deletions. It can hold secrets, so it needs encryption and restricted access. Shared work requires remote state with locking and versioning.
Count or for_each: how do you choose?Mid
I use count for identical copies or an on/off switch, since instances are identified by position. For anything with identity I use for_each, because instances are identified by key. Removing the middle item of a count list shifts the rest and causes replacements, while for_each only touches the removed key.
A teammate edited a resource in the console. What happens on the next plan?Mid
That is drift. The plan refreshes the real attributes, sees the difference from the configuration and proposes to revert it. If the change was intended, I update the code to match. If another system owns the attribute, I add it to ignore_changes. I also find out why the console edit was possible and tighten access.
How would you bring an existing resource under Terraform management?Mid
I write an import block with the target address and the real id, run plan, optionally generating configuration with -generate-config-out, and adjust the code until the plan shows no changes. Then I apply, which records it in state. Before version 1.5 the equivalent was the terraform import command, which did not write code.
How do you store state safely for a team?Mid
A remote backend such as an encrypted, versioned bucket with locking, or HCP Terraform. Access is limited to the pipeline identity and a few admins. State is split by environment and component to reduce blast radius. I never commit it to git, and I keep secrets out of resources where I can.
What are the risks of provisioners?Mid
Terraform cannot predict or track what a script does, so plans are blind to it and drift goes unseen. They need network access and credentials, fail halfway and taint resources. I prefer images, cloud-init or user data and configuration tools, and I use provisioners only when nothing else fits.
How do you refactor a resource into a module without recreating it?Senior
I use a moved block mapping the old address to the new one inside the module, then run plan and confirm it shows a move and no destroy. The older option is terraform state mv, which changes state directly and is not reviewable. The moved block is committed in code and works for every environment.
Workspaces or separate directories for environments?Senior
CLI workspaces share code and backend configuration, which is convenient for temporary copies but makes it easy to apply to production by mistake. For long-lived environments I prefer separate directories or pipelines with separate state, credentials and variable files, sharing modules. The environments then differ visibly in the repository and in access control.
How would you design a safe CI pipeline for Terraform?Senior
Pull requests run fmt, validate, linting, security scans, cost estimate and plan, with the plan posted for review. Merge triggers apply, with approval for production, using the saved plan. Credentials are short-lived through federation. State locking prevents concurrent runs, and a scheduled plan detects drift.
How do you handle secrets in Terraform?Senior
I keep them out of code and tfvars. Applications should fetch secrets from a secrets manager at run time. Where Terraform must set one, I know it ends up in state unless the provider supports ephemeral or write-only values, so the state is encrypted and tightly controlled. Sensitive flags only hide values in output.
What happens if apply fails halfway?Mid
Terraform writes to state what it did create, so state matches reality up to the failure. The remaining changes are not applied. I read the error, fix the cause, such as a quota or permission, and run plan again, which resumes from the current position. Resources that failed in a provisioner are tainted and replaced.
How do you test Terraform code?Senior
Layers: format, validate and lint on every commit, security scans, plan-based unit tests with terraform test and mock providers, and integration tests that apply in a sandbox account and then destroy. Contract tests protect module interfaces. End to end tests are few and run before releases.
Terraform or OpenTofu?Mid
They share syntax and workflow, since OpenTofu forked from Terraform in 2023 after the licence change to the Business Source License. I would check licence terms, provider and module compatibility, feature differences and what the team and tooling already support. For most day-to-day work the skills transfer directly.
How do you prevent accidental deletion of a database?Senior
Layers of protection: prevent_destroy in the lifecycle block, the provider's own deletion protection flag, a plan review that flags destroys, policy checks that block them in CI, backups and restricted production credentials. No single one is enough, because someone can remove the block that holds the setting.
Common mistakes
- Committing state files or secrets to git; use a remote backend and ignore local state.
- Skipping the plan review and applying with auto-approve; read every deletion and replacement.
- Using count for items with identity, which causes replacement when the list changes; use for_each.
- Leaving provider and module versions unpinned; use constraints and commit the lock file.
- Putting everything in one state; split by layer, environment and ownership.
- Relying on sensitive = true to protect secrets; it hides output only and the state still has the value.
- Editing state by hand or deleting it to clear an error; use state commands, moved blocks and imports.
- Reaching for provisioners first; use images, user data or managed services.
- Forgetting to destroy practice resources and paying for them; run destroy and check the console.
- Writing modules that wrap one resource with no added value; call the resource directly.
Ready-for-interview checklist
- I can explain what Infrastructure as Code is and when Terraform fits.
- I can write a configuration with providers, resources, variables and outputs from scratch.
- I can describe the init, plan, apply and destroy workflow and read a plan.
- I can choose between count and for_each and explain why.
- I can use lifecycle rules and explain the risk of each.
- I can set up remote state with locking and explain why.
- I can import an existing resource and make the plan clean.
- I can refactor with moved blocks without recreating resources.
- I can write a module with a small interface and pin published modules.
- I can explain why provisioners are a last resort.
- I can write a terraform test file and run a security scanner.
- I can describe a safe CI pipeline for plan and apply.
- I can explain how secrets reach state and how to limit that.
- I can compare workspaces, directories and Terragrunt for environments.
Related roadmaps
- AWS RoadmapCore services: IAM, EC2, S3, VPC, RDS, Lambda and more.
- Cloud Computing RoadmapService models, networking, storage, cost and multi-cloud.
- Docker RoadmapImages, containers, networking, volumes, Compose and security.
- Kubernetes RoadmapPods, deployments, services, config, scaling and operations.
- Git and GitHub RoadmapCommits, branches, rebasing, pull requests, Actions and recovery.
- Linux RoadmapFilesystem, processes, permissions, networking and troubleshooting.
FAQ
How long does it take to learn Terraform?
The basics, meaning providers, resources, variables and the plan and apply cycle, take one to two weeks with some cloud knowledge. Comfort with state, modules and pipelines usually takes two to three months of regular practice. Interview readiness is realistic in about ten to twelve weeks.
Which should I learn first, Terraform or a cloud provider?
Learn basic cloud concepts and one provider first, since Terraform only automates what you already understand. Spend a few weeks on AWS Roadmap or another cloud with the console, then start Terraform and rebuild what you made by hand.
Is Terraform still worth learning after the licence change?
Yes. It remains the most widely used tool of its kind, and OpenTofu uses the same language and workflow, so the skills carry across. Tools such as Pulumi and CloudFormation are alternatives, but job listings still ask for Terraform often.
Do I need to know a programming language for Terraform?
Not for the core. HCL is a configuration language with expressions, and you will use loops and conditionals in a limited way. Scripting skills help in pipelines and for testing, and Go helps if you write providers or Terratest tests.
Terraform or Ansible?
They solve different problems. Terraform provisions infrastructure and tracks it in state. Ansible configures what runs on machines and runs tasks in order. Many teams use both, with Terraform creating servers and Ansible or images preparing them.