1. Home
  2. Blog
  3. DevOps

DevOps

Terraform Tutorial for Beginners: Infrastructure as Code From First File to Modules

Learn Terraform without a cloud account or a surprise bill. Start with local files to understand plan, apply and state, then move to real cloud resources, modules and a workflow that is safe for teams.

10 min read
Terraform tutorial for beginners: infrastructure as code from first file to modules

Key takeaways

  • Terraform creates and changes infrastructure from text files that you can keep in Git.
  • The core workflow is init, plan, apply, and plan shows changes before they happen.
  • The state file records what Terraform manages. Protect it, and never commit it to Git.
  • Use modules to reuse patterns, and remote state with locking when working in a team.

Clicking around a cloud console is fine for one server. It breaks down when you need the same setup in staging and production, or when someone asks who changed a firewall rule last March. This Terraform tutorial for beginners shows you a better way: describe your infrastructure in code, and let a tool make it real.

Most tutorials begin with a cloud account, which adds billing risk before you have learned anything. We start with Terraform’s local provider, which just creates files on your computer, so mistakes cost nothing. These examples and the sample output are standard illustrations that we did not execute for this article, and exact wording varies by version.

What is Terraform?

Terraform is an infrastructure as code (IaC) tool from HashiCorp. You write configuration files in HCL, the HashiCorp Configuration Language, describing the end result you want. Terraform then works out what to create, change or delete in order to get there.

It talks to platforms through plugins called providers. There are providers for AWS, Azure, Google Cloud, Kubernetes, GitHub, Cloudflare and many others, so you can use one workflow across them all.

One thing to know: in 2023, HashiCorp moved Terraform to the Business Source License. The community then created OpenTofu, an open-source fork that uses very similar commands and configuration. What you learn here largely applies to both.

Why infrastructure as code?

Question Manual console work Terraform
Who changed what? Hard to find out Git history
Can we copy staging to production? Careful clicking Reuse the code
What will this change do? You find out afterwards terraform plan shows you first
Can someone review it? Not easily A normal pull request
Disaster recovery Depends on people’s memory Recreate from code, plus data backups

Terraform is also declarative. You state “there should be three servers”, not “create a server, three times”. If two exist already, it adds one. A script that simply repeats its steps could end up creating three more.

Install Terraform

Install it using the official HashiCorp instructions for your operating system, and then confirm that it works:

terraform -version

Create an empty folder for the project. Terraform reads every .tf file in the current folder, so one folder equals one configuration.

Your first configuration

Create a file named main.tf:

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.5"
    }
  }
}

resource "local_file" "greeting" {
  filename = "${path.module}/hello.txt"
  content  = "Hello from Terraform\n"
}

Look at the resource block, as nearly everything you write follows this shape. local_file is the resource type, and greeting is your own name for it inside the configuration. Together, they form its address: local_file.greeting. The arguments inside describe what you want.

The workflow: init, plan, apply

terraform init

This prepares the folder and downloads the providers you asked for. Run it once at the start, and again whenever you add a provider or module. It also creates a .terraform.lock.hcl file, which records exact provider versions. Commit that file to Git.

terraform init

terraform plan

This compares your code with what exists and shows the planned changes, without touching anything:

terraform plan

# local_file.greeting will be created
+ resource "local_file" "greeting" {
    + content  = "Hello from Terraform\n"
    + filename = "./hello.txt"
  }

Plan: 1 to add, 0 to change, 0 to destroy.

Learn the symbols, because you will read a lot of plans:

Symbol Meaning
+ Create
~ Update in place
- Destroy
-/+ Destroy and recreate

terraform apply

This shows the plan again, asks for confirmation and makes the changes:

terraform apply

Type yes, and hello.txt appears. Now run terraform plan again. It should report no changes, because reality already matches your code. This property, where running it repeatedly causes no further change, is called idempotence.

Changing and destroying

Edit the content line and run terraform plan. Terraform spots the difference and proposes a change. For this resource, it replaces the file. Pay attention to replacements on real infrastructure, since recreating a database could mean losing data.

Next, delete hello.txt by hand and plan again. Terraform notices it is missing and offers to recreate it. A difference between your code and reality is called drift, and detecting it is one of the main benefits of the tool.

To remove everything this configuration manages, run:

terraform destroy

Variables and outputs

Hard-coded values stop you from reusing code. Input variables make a configuration flexible, and outputs report useful values back:

# variables.tf
variable "environment" {
  type        = string
  description = "Deployment environment"
  default     = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Use dev, staging or prod."
  }
}

# main.tf
resource "local_file" "config" {
  filename = "${path.module}/${var.environment}.txt"
  content  = "environment=${var.environment}\n"
}

# outputs.tf
output "config_path" {
  value = local_file.config.filename
}

Set values on the command line, or keep them in a file called terraform.tfvars, which is loaded automatically:

terraform apply -var="environment=staging"

Using local_file.config.filename in the output is also how resources refer to each other. Terraform reads those references to work out the correct order in which to create things.

Terraform state

After applying, you will find a file called terraform.tfstate. This is Terraform’s record of what it manages, mapping your code to real objects. Without it, Terraform would not know that local_file.greeting is that specific file.

Three rules matter here:

  1. Never commit state to Git. It can contain passwords and other secrets in plain text.
  2. Do not edit it by hand. Use commands such as terraform state list and terraform state mv.
  3. Use a remote backend for team projects. A shared backend stores state centrally and locks it, so two people cannot apply at once.

Here is a typical backend using Amazon S3, with a bucket you create in advance:

terraform {
  backend "s3" {
    bucket       = "my-company-terraform-state"
    key          = "shop/prod/terraform.tfstate"
    region       = "ap-south-1"
    encrypt      = true
    use_lockfile = true
  }
}

Tip: Start every project with a .gitignore that excludes .terraform/, *.tfstate, *.tfstate.* and any *.tfvars files holding secrets.

A real cloud resource

The workflow does not change when you move to a cloud. Here is an S3 bucket on AWS. Check pricing first, and destroy anything you create while practising:

provider "aws" {
  region = "ap-south-1"
}

resource "aws_s3_bucket" "reports" {
  bucket = "my-unique-reports-bucket-2026"

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

Terraform gets credentials from your AWS CLI setup or environment variables. Never write access keys into a .tf file.

Modules

A module is simply a folder of Terraform files that you can call with different inputs. It works much like a function, letting you define a pattern once and reuse it:

project/
  main.tf
  modules/
    reports_bucket/
      main.tf
      variables.tf
      outputs.tf
module "staging_reports" {
  source      = "./modules/reports_bucket"
  environment = "staging"
}

module "prod_reports" {
  source      = "./modules/reports_bucket"
  environment = "prod"
}

The public Terraform Registry also has ready-made modules, but read them before you rely on them, and pin the version. Do not build modules too early. Wait until you have repeated a pattern at least twice.

A safe team workflow

  1. Create a branch and change the .tf files. See our Git tutorial for beginners if you need a refresher.
  2. Run terraform fmt and terraform validate.
  3. Open a pull request, with the output of terraform plan attached for review.
  4. After approval, apply that saved plan from one controlled place, ideally a pipeline.

Saving the plan ensures you apply exactly what was reviewed:

terraform plan -out=tfplan
terraform apply tfplan

This fits naturally into CI/CD and GitHub Actions. Terraform usually provisions the cluster, and tools like Kubernetes then run applications on top of it.

Common mistakes

  • Applying without reading the plan. One -/+ on a database can be a very expensive surprise.
  • Keeping state on one laptop. Lose that laptop, and Terraform loses track of your infrastructure.
  • Making manual console changes. They cause drift, and the next apply may undo them.
  • Using unpinned versions. Pin provider and module versions so that upgrades happen on purpose.
  • Using one huge configuration. Splitting by environment or system limits the damage of any mistake.

Final thoughts

You have now seen the ideas behind nearly all Terraform work: declare resources, review the plan, apply it, and protect the state. Practise with the local provider until the workflow feels natural, then manage one small real resource in a free-tier account. Bring in remote state and modules once more than one person, or more than one environment, is involved.

Frequently asked questions

What is Terraform used for?

Terraform is used to create and manage infrastructure, such as servers, networks, databases and DNS, by describing it in code instead of configuring it by hand.

Is Terraform free?

The Terraform CLI is free to use under its Business Source License, though terms restrict some competing commercial uses. HCP Terraform has paid tiers, and OpenTofu is open source.

What is the difference between Terraform and Ansible?

Terraform mainly provisions infrastructure declaratively. Ansible mainly configures software on existing machines. Many teams use both, Terraform first and Ansible afterwards.

What is a Terraform state file?

It is a record mapping your configuration to real resources. Terraform uses it to work out changes. It can hold secrets, so store it securely and keep it out of Git.

Do I need to know programming to learn Terraform?

Not really. HCL is simple and declarative. Understanding the platform you manage, such as AWS networking, is usually harder than the language itself.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →