Infrastructure as code stopped being optional years ago, and in 2026 Terraform remains the most widely adopted way to declare cloud resources in plain text and let a tool build them for you. This Terraform tutorial walks you through provisioning real, working AWS infrastructure — a custom network, a firewall, a Linux web server, and a remote state backend — using Terraform CLI 1.15.6 and the official AWS provider v6.49.0. By the end you will have a complete, reproducible project you can terraform apply to stand up a live web server and terraform destroy to tear it down in seconds.
This guide is written for people who have used the AWS Console at least once but have never written a single line of HCL (HashiCorp Configuration Language). Every command, file, and expected output is shown in full. We cover the modern 2026 workflow: the BUSL-licensed Terraform CLI, the renamed HCP Terraform service, native S3 state locking, modules, variables, and the security practices that keep a junior mistake from costing you a cloud bill. Follow the thirteen steps in order and you will not get lost.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
Why Learn Terraform in 2026
Terraform turns infrastructure into version-controlled text. Instead of clicking through the AWS Console and forgetting what you did, you write a declarative description of the resources you want, and Terraform figures out the API calls needed to make reality match that description. The same configuration that builds your staging environment builds production, byte for byte. That reproducibility is why Terraform sits at the center of most cloud platform teams, and why infrastructure-as-code fluency has become a baseline expectation for cloud, DevOps, and platform engineering roles.
Three things changed the Terraform landscape recently, and you should understand them before you start. First, HashiCorp relicensed Terraform from the open-source MPL to the Business Source License (BUSL) 1.1 in 2023. The CLI is still free to download and use for the vast majority of users, but it is no longer technically open source. Second, that license change spawned OpenTofu, a community fork now governed by the Linux Foundation that remains under the original MPL license and stays largely command-compatible with Terraform. Third, HashiCorp itself is now part of IBM following the acquisition that closed in 2025, which has reassured many enterprises about long-term stewardship.
For learning purposes the differences barely matter: the HCL you write in this tutorial works in both Terraform and OpenTofu, and the terraform and tofu commands accept the same subcommands. We use the HashiCorp Terraform CLI here because it is what most job postings name, but every code block below runs unchanged under OpenTofu if you prefer the MPL-licensed fork. The hosted SaaS product, formerly Terraform Cloud, is now branded HCP Terraform; we mention it where remote state and team collaboration come up.
What You Will Build: Project Overview
The complete working project in this tutorial provisions a small but realistic slice of AWS infrastructure. It is intentionally non-trivial — not a single resource, but a network with a server inside it, the way real applications are deployed. Here is the full resource map you will create:
| Resource | AWS service | Purpose |
|---|---|---|
| VPC (10.0.0.0/16) | Amazon VPC | Isolated private network for the project |
| Public subnet | Amazon VPC | Subnet with a route to the internet |
| Internet gateway + route table | Amazon VPC | Outbound and inbound internet access |
| Security group | Amazon VPC | Firewall allowing HTTP (80) and SSH (22) |
| EC2 instance (t3.micro) | Amazon EC2 | Linux server running an Nginx welcome page |
| S3 bucket | Amazon S3 | Remote backend storing Terraform state |
| State lock (S3 native / DynamoDB) | Amazon S3 or DynamoDB | Prevents concurrent applies corrupting state |
Most of these resources fall inside the AWS Free Tier if your account is eligible, and the t3.micro instance plus minimal S3 usage costs cents per hour even if it does not. Crucially, the final step tears everything down with one command, so you never leave billable resources running by accident. The entire codebase is roughly six short files, and we print each one in full so you can copy the complete working project end to end.
Prerequisites and Required Versions
Pin your versions before you write any HCL. Terraform configurations are sensitive to provider versions, and a mismatch is the single most common reason a tutorial “works on my machine” but fails on yours. The table below lists exactly what this tutorial was validated against in June 2026.
| Tool | Version used here | How to check |
|---|---|---|
| Terraform CLI | 1.15.6 (latest stable, June 2026) | terraform version |
| AWS provider (hashicorp/aws) | 6.49.0 | Listed in versions.tf |
| AWS CLI | v2 (latest) | aws --version |
| An AWS account | Free Tier eligible recommended | Console login |
| Terminal | bash, zsh, or PowerShell | — |
| Code editor | VS Code + HashiCorp Terraform extension | Optional but recommended |
You also need an AWS Identity and Access Management (IAM) user or role with permissions to create VPCs, EC2 instances, and S3 buckets. For a learning project, an IAM user with the AdministratorAccess policy is the fastest path, but in any shared or production account you should scope permissions down to only what the project needs. Never use your AWS account root credentials for Terraform. We configure credentials in Step 2.
Step 1: Install Terraform CLI 1.15.6
Install the Terraform binary for your operating system. On macOS with Homebrew, the HashiCorp tap gives you the official build. On Linux, add the HashiCorp APT repository. On Windows, use Chocolatey or download the zip from the official releases page. The commands below cover the three common paths.
# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
# Ubuntu / Debian
wget -O- https://apt.releases.hashicorp.com/gpg |
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg]
https://apt.releases.hashicorp.com $(lsb_release -cs) main" |
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# Windows (Chocolatey)
choco install terraform
Confirm the install and the version. You should see 1.15.6 or newer. If you would rather use the open-source OpenTofu fork, install it from opentofu.org and substitute tofu for terraform in every command that follows — the configuration files are identical.
$ terraform version
Terraform v1.15.6
on darwin_arm64
If terraform: command not found appears, your shell cannot see the binary on its PATH. Close and reopen the terminal so the new path is loaded, or, on a manual zip install, move the binary into /usr/local/bin (macOS/Linux) or a folder that is already on your Windows PATH.
Step 2: Configure AWS Credentials
Terraform’s AWS provider authenticates using the standard AWS SDK credential chain, which means it reads the same credentials the AWS CLI uses. Install the AWS CLI v2, then run aws configure and paste the access key ID and secret access key from an IAM user you created in the AWS Console under IAM → Users → Security credentials.
$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-east-1
Default output format [None]: json
# Verify the credentials work
$ aws sts get-caller-identity
{
"UserId": "AIDAEXAMPLE",
"Account": "123456789012",
"Arn": "arn:aws:iam::123456789012:user/terraform"
}
A successful get-caller-identity call confirms Terraform will be able to authenticate. Never hard-code these keys inside your .tf files — doing so is the number-one way developers accidentally leak credentials to public Git repositories. Terraform reads them from the environment or the credentials file automatically, so they never need to appear in your code. For extra safety, consider exporting them as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables in a session you can close.
Step 3: Create the Project and Provider Block
Make a project directory and create your first file, versions.tf. This file pins both the Terraform CLI and the AWS provider to specific version constraints, which is the practice that prevents surprise breakage when a new provider release ships. The ~> 6.0 constraint allows any 6.x version but blocks an automatic jump to a hypothetical 7.0 with breaking changes.
mkdir terraform-aws-tutorial && cd terraform-aws-tutorial
# versions.tf
terraform {
required_version = ">= 1.15.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "terraform-aws-tutorial"
ManagedBy = "Terraform"
}
}
}
The default_tags block is a quietly powerful feature: every resource this provider creates automatically gets tagged ManagedBy = Terraform, which makes it trivial to find and audit Terraform-managed resources in the AWS Console later. We reference var.aws_region, a variable we define in the next step, so the region is not hard-coded.
Step 4: Define Input Variables
Variables parameterize your configuration so the same code can target different regions, instance sizes, or environments without edits. Create variables.tf. Each variable gets a type, a description, and where sensible a default. Typed variables catch mistakes early — pass a string where a number is expected and Terraform refuses to plan.
# variables.tf
variable "aws_region" {
description = "AWS region to deploy into"
type = string
default = "us-east-1"
}
variable "instance_type" {
description = "EC2 instance type for the web server"
type = string
default = "t3.micro"
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "allowed_ssh_cidr" {
description = "CIDR allowed to SSH to the instance"
type = string
default = "0.0.0.0/0"
}
variable "project_name" {
description = "Name prefix for resources"
type = string
default = "ti-tutorial"
}
Note the allowed_ssh_cidr default of 0.0.0.0/0, which opens SSH to the entire internet. That is fine for a throwaway learning instance but dangerous in production. In the pitfalls section we discuss overriding it with your own IP. A real best practice is to set a terraform.tfvars file (and add it to .gitignore) with values like allowed_ssh_cidr = "203.0.113.4/32" so only your machine can reach port 22.
Step 5: Build the VPC and Networking
Now the real infrastructure. Create network.tf. This file declares the VPC, a public subnet, an internet gateway, and a route table that sends all outbound traffic through that gateway. Each resource references another by attribute — the subnet references aws_vpc.main.id — and Terraform uses those references to build a dependency graph and create resources in the correct order automatically.
# network.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "${var.project_name}-vpc" }
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = { Name = "${var.project_name}-igw" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
availability_zone = "${var.aws_region}a"
tags = { Name = "${var.project_name}-public-subnet" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = { Name = "${var.project_name}-public-rt" }
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
You never have to tell Terraform that the gateway must exist before the route, or the VPC before the subnet. Because the route table references aws_internet_gateway.gw.id, Terraform infers the ordering. This implicit dependency graph is the heart of how Terraform works, and it is why you write what you want rather than how to build it step by step.
Step 6: Add a Security Group Firewall
A security group is AWS’s instance-level firewall. Create security.tf to allow inbound HTTP on port 80 from anywhere and SSH on port 22 from your allowed CIDR, while permitting all outbound traffic so the instance can reach the internet to install packages.
# security.tf
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Allow HTTP and SSH"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH from allowed CIDR"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.allowed_ssh_cidr]
}
egress {
description = "All outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "${var.project_name}-web-sg" }
}
The protocol = "-1" in the egress rule means “all protocols.” Security groups are stateful, so you do not need a matching inbound rule for return traffic — if the instance initiates an outbound connection, the response is allowed back automatically. This is a frequent point of confusion for people coming from traditional stateless firewalls.
Step 7: Launch the EC2 Web Server
Time to create the server. Create compute.tf. We use a data source to dynamically look up the latest Amazon Linux 2023 AMI rather than hard-coding an image ID that changes per region and over time. The user_data script runs on first boot to install and start Nginx with a custom welcome page.
# compute.tf
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
dnf install -y nginx
systemctl enable --now nginx
echo "<h1>Deployed with Terraform 1.15.6</h1>" > /usr/share/nginx/html/index.html
EOF
tags = { Name = "${var.project_name}-web" }
}
The data "aws_ami" block queries AWS at plan time and always resolves to the newest matching image, so your configuration stays current without manual edits. The heredoc <<-EOF syntax lets you embed a multi-line shell script directly in HCL. On Amazon Linux 2023 the package manager is dnf, not yum — a detail that trips up people copying older tutorials.
Step 8: Define Outputs
Outputs surface useful values after an apply, such as the public IP you need to visit the site. Create outputs.tf. Outputs are also how one Terraform configuration exposes data to another via remote state, making them essential for larger multi-module setups.
# outputs.tf
output "instance_public_ip" {
description = "Public IP of the web server"
value = aws_instance.web.public_ip
}
output "website_url" {
description = "URL to open in a browser"
value = "http://${aws_instance.web.public_ip}"
}
output "vpc_id" {
description = "ID of the created VPC"
value = aws_vpc.main.id
}
After you apply, Terraform prints these values and stores them in state. You can re-display them any time with terraform output, or fetch a single value with terraform output -raw website_url — handy for piping into a script or a curl health check.
Step 9: Initialize the Working Directory
With all files written, run terraform init. This downloads the AWS provider plugin into a local .terraform directory, writes a .terraform.lock.hcl dependency lock file, and prepares the backend. You must run init once per project and again whenever you change provider versions or backend configuration.
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 6.0"...
- Installing hashicorp/aws v6.49.0...
- Installed hashicorp/aws v6.49.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record
the provider selections it made above.
Terraform has been successfully initialized!
Commit the .terraform.lock.hcl file to Git. It pins the exact provider version and checksums across your whole team, guaranteeing everyone uses the same provider build. Do not commit the .terraform directory or any *.tfstate files — add them to .gitignore. A minimal ignore file: .terraform/, *.tfstate, *.tfstate.*, and terraform.tfvars.
Step 10: Format, Validate, and Plan
Before applying anything, run the three safety commands. terraform fmt rewrites your files into canonical style, terraform validate checks syntax and internal consistency, and terraform plan shows exactly what will change without touching AWS. The plan is your last chance to catch a mistake before real resources are created.
$ terraform fmt
network.tf
security.tf
$ terraform validate
Success! The configuration is valid.
$ terraform plan
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0abcdef1234567890"
+ instance_type = "t3.micro"
+ public_ip = (known after apply)
...
}
Plan: 8 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_public_ip = (known after apply)
+ website_url = (known after apply)
Read the summary line: Plan: 8 to add, 0 to change, 0 to destroy. Anything showing “to destroy” that you did not expect is a red flag — stop and investigate. Making terraform plan a habit, and reading every plan carefully, is the discipline that separates engineers who trust Terraform from those who fear it.
Step 11: Apply and Visit Your Live Server
Run terraform apply. Terraform shows the same plan and asks for confirmation; type yes. It then makes the AWS API calls in dependency order and prints your outputs when finished. The whole process usually takes one to two minutes, most of which is AWS booting the instance.
$ terraform apply
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 2s [id=vpc-0a1b2c3d]
aws_instance.web: Creating...
aws_instance.web: Creation complete after 32s [id=i-0f9e8d7c]
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
instance_public_ip = "54.81.123.45"
website_url = "http://54.81.123.45"
Give the instance about 60 seconds to run its boot script, then open the website_url in a browser or run curl $(terraform output -raw website_url). You should see “Deployed with Terraform 1.15.6.” Congratulations — you just provisioned a complete network and a live web server entirely from code. If the page does not load immediately, wait a moment; user_data installs Nginx after the instance reports as created.
Step 12: Move State to a Remote S3 Backend
So far your state lives in a local terraform.tfstate file. That is fine solo, but the moment a second person runs Terraform you risk corrupting state or two people applying at once. The standard fix is a remote backend in S3, with state locking to serialize concurrent runs. First create the bucket (one-time, manually or with a tiny separate config), then add a backend block.
# Create the state bucket once (names must be globally unique)
aws s3api create-bucket --bucket ti-tutorial-tfstate-12345 --region us-east-1
aws s3api put-bucket-versioning --bucket ti-tutorial-tfstate-12345
--versioning-configuration Status=Enabled
# backend.tf
terraform {
backend "s3" {
bucket = "ti-tutorial-tfstate-12345"
key = "tutorial/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
Recent Terraform versions support native S3 state locking via use_lockfile = true, which writes a lock object directly in the bucket and removes the long-standing requirement for a separate DynamoDB table. If you are on an older CLI or prefer the classic approach, drop use_lockfile and add dynamodb_table = "terraform-locks" instead, pointing at a DynamoDB table with a LockID partition key. After adding the backend block, run terraform init again and answer yes when prompted to copy existing state into S3.
$ terraform init
Initializing the backend...
Do you want to copy existing state to the new backend?
Enter a value: yes
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Versioning on the bucket means every state change is retained, so you can recover from accidental corruption. Always encrypt = true — state files frequently contain secrets like database passwords in plaintext. For teams that would rather not manage backend infrastructure at all, HCP Terraform offers managed remote state and a free tier suitable for small teams.
Step 13: Destroy Everything Cleanly
The final step is the one that keeps your AWS bill at zero: tear it all down. terraform destroy reads state, builds a reverse-dependency plan, and removes every managed resource. It asks for confirmation just like apply.
$ terraform destroy
Plan: 0 to add, 0 to change, 8 to destroy.
Do you really want to destroy all resources?
Enter a value: yes
aws_instance.web: Destroying... [id=i-0f9e8d7c]
aws_instance.web: Destruction complete after 28s
aws_vpc.main: Destroying... [id=vpc-0a1b2c3d]
aws_vpc.main: Destruction complete after 1s
Destroy complete! Resources: 8 destroyed.
This reproducible build-and-destroy loop is what makes Terraform so powerful for development and testing: spin up a full environment, test against it, and delete it minutes later for pennies. The S3 state bucket you created manually survives the destroy (it was never managed by this config), so delete it separately with aws s3 rb s3://ti-tutorial-tfstate-12345 --force if you are completely finished.
Refactor Into a Reusable Module
Once the flat project works, the natural next step is packaging it as a module. A module is just a directory of .tf files with defined inputs and outputs that other configurations can call. Modules are how teams standardize a “web server” or “VPC” pattern and reuse it across dozens of projects without copy-paste.
# Calling a local module from a root configuration
module "web_server" {
source = "./modules/web-server"
project_name = "prod-app"
instance_type = "t3.small"
aws_region = "us-east-1"
}
output "prod_url" {
value = module.web_server.website_url
}
To convert this tutorial into a module, move network.tf, security.tf, compute.tf, variables.tf, and outputs.tf into modules/web-server/, then call it from a thin root configuration like the one above. The public Terraform Registry hosts thousands of community and verified modules — the official AWS VPC module alone has tens of millions of downloads — so you rarely have to build common patterns from scratch.
Common Pitfalls to Avoid
These mistakes catch nearly every Terraform beginner. Internalize them now and skip hours of debugging later.
- Committing state to Git. The
terraform.tfstatefile often contains secrets in plaintext and changes on every apply. Never commit it; use a remote backend and a.gitignorefrom day one. - Hard-coding credentials. Putting an access key inside a
providerblock leaks it the instant you push to a public repo. Bots scan GitHub for AWS keys within seconds. Always use the credential chain. - Editing infrastructure in the AWS Console. Manual changes create “drift” between reality and state. Your next
planwill try to undo them. Treat Terraform as the single source of truth. - Skipping
terraform plan. Applying blind is how people accidentally destroy a database. Read every plan, especially the destroy count, before typingyes. - Not pinning provider versions. Without a
~> 6.0constraint, a future provider release can break your config on the nextinit. Pin versions and commit the lock file. - Opening SSH to 0.0.0.0/0 in production. Convenient for a tutorial, but it exposes port 22 to the entire internet. Scope
allowed_ssh_cidrto your own IP with a/32mask.
Troubleshooting Common Terraform Errors
When something goes wrong, the error message usually points straight at the fix. Here are the eight you are most likely to hit, with causes and resolutions.
| Error | Likely cause | Fix |
|---|---|---|
No valid credential sources found | AWS CLI not configured | Run aws configure, verify with aws sts get-caller-identity |
UnauthorizedOperation | IAM user lacks permissions | Attach a policy granting EC2/VPC actions to the IAM user |
InvalidAMIID.NotFound | Hard-coded AMI from another region | Use the data "aws_ami" lookup as shown in Step 7 |
Error acquiring the state lock | A previous run crashed holding the lock | Investigate, then terraform force-unlock LOCK_ID |
BucketAlreadyExists | S3 bucket names are globally unique | Pick a more unique bucket name with a random suffix |
Provider produced inconsistent final plan | Provider version bug or drift | Run terraform refresh, upgrade the provider, re-plan |
Reference to undeclared resource | Typo in a resource address | Check the resource name spelling against its resource block |
| Website not loading after apply | user_data still installing Nginx | Wait 60 seconds; confirm the security group allows port 80 |
For deeper debugging, set the TF_LOG=DEBUG environment variable before running a command to see every API call Terraform makes. It is verbose but invaluable when a resource refuses to create and the top-level message is unclear. Redirect it to a file with TF_LOG=DEBUG terraform apply 2> debug.log so you can search through it later.
Advanced Tips for Production Terraform
Once the basics click, these practices separate a hobby setup from a production-grade workflow that a team can rely on.
- Separate state per environment. Use distinct state keys (or workspaces) for dev, staging, and production so a mistake in one never touches another.
- Run plan in CI. Wire
terraform planinto a pull-request check so reviewers see infrastructure changes before merge, and gateapplybehind approval. - Use
for_eachovercount. When creating multiple similar resources,for_eachkeys them by a stable map key, so removing one item does not force-recreate the others the way positionalcountindexes do. - Adopt a linter and policy-as-code. Tools like
tflintcatch provider-specific mistakes, and Sentinel or Open Policy Agent enforce rules such as “no public S3 buckets” automatically. - Lock with the dependency lock file. Commit
.terraform.lock.hcland runterraform providers lockfor multiple platforms so CI and laptops resolve identical provider hashes. - Prefer data sources over hard-coded IDs. Looking up AMIs, VPCs, and availability zones dynamically keeps configurations portable across regions and accounts.
If you want managed remote state, run history, and policy enforcement without building it yourself, HCP Terraform (the service formerly called Terraform Cloud) provides all three with a free tier suitable for small teams. For organizations committed to a fully open-source toolchain, OpenTofu plus an open backend such as S3 covers the same ground under the MPL license.
Terraform vs OpenTofu: Which Should You Use?
Because the 2023 license change is recent enough to still cause confusion, it is worth a clear summary. Both tools share the same HCL syntax, the same providers from the Terraform Registry, and nearly identical commands. The decision usually comes down to licensing philosophy and which features your organization needs.
| Factor | Terraform (HashiCorp) | OpenTofu |
|---|---|---|
| License | BUSL 1.1 (source-available) | MPL 2.0 (open source) |
| Governance | HashiCorp (part of IBM) | Linux Foundation |
| CLI command | terraform | tofu |
| HCL compatibility | Native | Compatible |
| Provider registry | HashiCorp Registry | OpenTofu Registry (mirrors providers) |
| Hosted SaaS | HCP Terraform | Third-party / self-hosted |
For learning, job interviews, and most corporate environments, HashiCorp Terraform remains the default and is what employers most often name. If your company has a strict open-source-only policy, or you simply prefer Linux Foundation governance, OpenTofu is a drop-in alternative and everything in this tutorial works under it. You can even switch later with minimal friction because the state and configuration formats are interoperable.
Frequently Asked Questions
Is Terraform free to use in 2026?
The Terraform CLI is free to download and use for the overwhelming majority of users. Since 2023 it has been licensed under the Business Source License (BUSL) 1.1, which restricts only the narrow case of building a competing commercial product that hosts Terraform. Normal individual and corporate infrastructure work is unaffected. If you need a fully open-source MPL license, use OpenTofu instead.
Do I need to know a programming language first?
No. Terraform uses HCL, a declarative configuration language, not a general-purpose programming language. You describe the desired end state rather than writing loops and logic to build it. Basic familiarity with the command line and the cloud platform you are targeting (AWS here) is far more important than prior coding experience.
What is the difference between terraform plan and apply?
terraform plan is a dry run: it shows exactly what Terraform would create, change, or destroy without touching your cloud account. terraform apply executes those changes after you confirm. Always read the plan before applying — it is your safety net against accidental destruction.
Where should I store Terraform state for a team?
Use a remote backend such as an encrypted, versioned S3 bucket with state locking, as shown in Step 12, or a managed service like HCP Terraform. Never keep team state in a local file or commit it to Git, because concurrent edits can corrupt it and it often contains secrets.
Will this tutorial cost me money on AWS?
Most resources here fall within the AWS Free Tier for eligible accounts, and even outside it a t3.micro running for an hour costs only pennies. Because Step 13 destroys everything with one command, your ongoing cost returns to zero as soon as you tear the project down. Always run terraform destroy when you finish experimenting.
Can I use this same workflow for Azure or Google Cloud?
Yes. The Terraform workflow — init, plan, apply, destroy — is identical across providers. You swap the hashicorp/aws provider for hashicorp/azurerm or hashicorp/google and use that provider’s resource types. The core concepts of state, variables, outputs, and modules carry over unchanged, which is a major reason Terraform is so widely adopted.
What is the latest Terraform version?
As of June 2026, the latest stable HashiCorp Terraform CLI release is 1.15.6, published June 4, 2026, and the latest official AWS provider is hashicorp/aws v6.49.0.49.0. Pin to these or newer in your versions.tf, and commit the resulting .terraform.lock.hcl so your whole team resolves the same builds.
How do I fix a “state lock” error?
A lock error usually means a previous run crashed without releasing the lock. First make sure no one else is mid-apply, then run terraform force-unlock LOCK_ID using the ID printed in the error. With native S3 locking enabled via use_lockfile, the lock is a small object in your state bucket that the unlock command clears.
Related Coverage
- Docker Tutorial: Build a Production Stack in 13 Steps [2026]
- Kubernetes Tutorial: Build a Cluster in 13 Steps [2026]
- Docker vs Podman 2026: 71K Stars, $0 vs $24/mo [Tested]
- ArgoCD vs Flux 2026: 23K vs 8K Stars, UI Gap [Tested]
- Cloudflare Workers vs Lambda 2026: 240x Cold Start Gap [Tested]
- Cloud Computing 2026: The Complete Hub
For official references, see the HashiCorp Terraform documentation, the CLI install guide, the AWS provider on the Terraform Registry, the S3 backend documentation, and the OpenTofu project site.


