Gallery

Contacts

405 W. Greenlawn Ave Lansing, Michigan 48910

contact@techjacksolutions.com

+1-616-320-4064

Terraform for Beginners: Infrastructure as Code in 2026
🌎
HashiCorp / Cloud Tools Hub

Terraform for Beginners: Infrastructure as Code in 2026

📅 June 29, 2026 👤 Tech Jacks Solutions Cloud Infrastructure Team ⏱ 18 min read 🏷 Guide · DevOps · IaC

Infrastructure should not be something you click together and hope to remember later. Terraform lets you write your cloud resources in plain text files: servers, databases, load balancers, DNS records, all of it. Those files go into git. They get reviewed in pull requests. They run in CI pipelines. They create identical environments every time.

When something breaks, you read the file. When you need a second environment, you copy the file. When someone asks "what's running in prod?", you show them the file.

This guide teaches Terraform from zero: how it thinks, how to write your first configuration, how to run it safely, and how to handle the one thing that trips up nearly every beginner: the state file. By the end you will have a mental model that makes every Terraform error message readable, and a working template you can adapt for AWS or Azure today.

🖥
Backend / Platform Engineers
Tired of "works on my machine" infra. Want repeatable, reviewable deployments.
Cloud Beginners
Have AWS/Azure access and want to stop clicking through consoles manually.
🎓
Cert Candidates
Studying for HashiCorp Terraform Associate or AWS DevOps certifications.
🔧
DevOps / SRE Transition
Moving from sysadmin or dev roles into infrastructure automation.
6,683 Public providers on Terraform Registry (June 2026)
1.15.6 Current stable Terraform (released June 10, 2026)
2014 Year Terraform first released by HashiCorp (July 28)
1.12.0 OpenTofu open-source fork (released May 14, 2026)
Before You Start: Prerequisites
  • A terminal (PowerShell, bash, or zsh) and basic command-line comfort
  • A cloud account: AWS free tier, Azure free, or GCP free trial works fine
  • Terraform CLI installed: terraform -version should return 1.x (download at developer.hashicorp.com/terraform/install)
  • Cloud credentials configured (AWS: ~/.aws/credentials; Azure: az login)
  • VS Code with the HashiCorp Terraform extension (recommended for HCL syntax highlighting)
Your Learning Path
1
Understand what IaC is and why Terraform exists
2
Learn providers, resources, and the state model
3
Write and apply your first HCL configuration
4
Understand state file security and remote backends
5
Use variables, outputs, and modules to scale
6
Choose between Terraform and OpenTofu for your team

What Is Infrastructure as Code and Why Does It Matter?

Infrastructure as Code (IaC) means your servers, databases, networks, and all the cloud plumbing that keeps your application alive are described in text files: not clicks, not bash scripts, not tribal knowledge. Every change is a commit. Every environment is reproducible. Every engineer on the team can read exactly what is deployed without logging into any console.

Before IaC, the dominant pattern was ClickOps: log into the AWS console, click through wizard screens, make guesses about settings, document the result in a wiki that would be out of date within a week. When the engineer who built it left, so did the knowledge of what was actually running. When the environment needed to be recreated for staging, disaster recovery, or after an accidental deletion, the process took days and produced something subtly different every time.

IaC solves this through declarative configuration: you describe the desired end-state, and the tool figures out how to get there. You do not say "run this API call to create a bucket, then run this other call to enable versioning." You say "I want a bucket named my-app-assets with versioning enabled," and Terraform handles the rest, including creating it if it does not exist, updating it if settings have changed, or leaving it alone if it already matches.

IaC vs Configuration Management: Two Different Problems

Terraform is often compared to tools like Ansible, Chef, or Puppet. They solve different problems. Terraform provisions infrastructure: it creates and manages cloud resources. Ansible configures software on existing machines: it installs packages, writes config files, starts services. Most real-world teams use both: Terraform to stand up the servers, then cloud-native tools or Ansible to configure what runs on them.

Terraform also differs from AWS CloudFormation or Azure Bicep. Those are cloud-specific. Terraform is multi-cloud by design: the same workflow provisions AWS resources, Azure resources, Cloudflare DNS records, GitHub repositories, and Datadog monitors. That breadth is why 6,683 providers exist on the Terraform Registry as of June 2026.

Declarative vs Imperative

Declarative IaC (Terraform, CloudFormation) says what you want. Imperative IaC (bash scripts, Ansible with complex conditionals) says how to get there. Declarative is safer for infrastructure because the tool handles idempotency: running it twice produces the same result as running it once.

How Terraform Works: Providers, Resources, and State

Three core concepts must click before anything else makes sense: providers, resources, and state.

Providers

A provider is a plugin that knows how to talk to a specific API. The AWS provider knows how to create EC2 instances, S3 buckets, and RDS databases. The Azure provider manages resource groups, VMs, and blob storage. Providers are published to the Terraform Registry. You declare which ones you need, and Terraform downloads them on terraform init:

# required_providers block: Terraform >= 1.7 / OpenTofu >= 1.6
terraform {
  required_version = ">= 1.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

Resources

A resource is a single infrastructure object. An S3 bucket is a resource. An EC2 instance is a resource. A security group is a resource. You declare resources with a type and a local name:

# resource "TYPE" "NAME" { ... }
resource "aws_s3_bucket" "app_assets" {
  bucket = "my-app-assets-2026"

  tags = {
    Environment = "production"
    Team        = "platform"
    ManagedBy   = "terraform"
  }
}

The type (aws_s3_bucket) comes from the provider. The local name (app_assets) is how you reference this resource elsewhere in your config. References look like aws_s3_bucket.app_assets.id: type dot name dot attribute.

State: The Most Important Concept

Terraform maintains a state file (terraform.tfstate) that records what it knows about your infrastructure. When you run terraform plan, Terraform compares three things: your configuration (what you want), the state (what it last saw), and the actual cloud resources (what currently exists). The plan shows exactly what changes it will make to close the gap.

State is what makes Terraform effective at managing change over time. It is also what makes it dangerous if mishandled. We cover state security in its own dedicated section.

Writing Your First Terraform Configuration

Terraform configurations are written in HashiCorp Configuration Language (HCL): a human-readable format designed specifically for infrastructure. Files use the .tf extension. Terraform loads all .tf files in a directory as a single configuration.

A minimal starter project has three files:

my-project/
├── main.tf          # Resource declarations
├── variables.tf     # Input variable definitions
└── outputs.tf       # Output value definitions

Here is a real-world starter: an AWS S3 bucket with versioning enabled and public access blocked. This is the kind of resource every cloud project needs, and a secure pattern to build from.

# main.tf: Terraform >= 1.7 / OpenTofu >= 1.6
terraform {
  required_version = ">= 1.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# Primary S3 bucket
resource "aws_s3_bucket" "app" {
  bucket = "${var.project_name}-${var.environment}-assets"

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

# Block all public access: secure default
resource "aws_s3_bucket_public_access_block" "app" {
  bucket = aws_s3_bucket.app.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Enable versioning
resource "aws_s3_bucket_versioning" "app" {
  bucket = aws_s3_bucket.app.id
  versioning_configuration {
    status = "Enabled"
  }
}

Notice how aws_s3_bucket_public_access_block references aws_s3_bucket.app.id. This is a dependency reference. Terraform uses these to build a dependency graph and create resources in the correct order automatically: you never need to specify order manually.

# variables.tf: Terraform >= 1.7 / OpenTofu >= 1.6
variable "project_name" {
  description = "Project name prefix for all resources"
  type        = string
}

variable "environment" {
  description = "Deployment environment (dev, staging, prod)"
  type        = string
  default     = "dev"

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

variable "aws_region" {
  description = "AWS region to deploy into"
  type        = string
  default     = "us-east-1"
}
# outputs.tf: Terraform >= 1.7 / OpenTofu >= 1.6
output "bucket_name" {
  description = "S3 bucket name"
  value       = aws_s3_bucket.app.bucket
}

output "bucket_arn" {
  description = "S3 bucket ARN for IAM policy attachment"
  value       = aws_s3_bucket.app.arn
}
HCL Syntax Quick Reference

String interpolation: "${var.project_name}-suffix"
Resource references: TYPE.NAME.ATTRIBUTE – e.g., aws_s3_bucket.app.id
Comments: # or // (single-line); /* ... */ (multi-line)
Auto-format: Run terraform fmt to normalize whitespace to the canonical style

Running Terraform: Plan, Apply, and Destroy

Every Terraform workflow uses the same four commands in sequence. Learn these deeply: they are the core loop you will run hundreds of times.

terraform init

Run this once per project, and again whenever you change provider versions or add modules. It downloads providers from the registry and initializes your backend:

terraform init

# Output:
# Initializing provider plugins...
# - Finding hashicorp/aws versions matching "~> 5.0"...
# - Installing hashicorp/aws v5.x.x...
# Terraform has been successfully initialized!

terraform plan

This is your dry run. Terraform reads your config, compares it to state, and shows exactly what it will create, update, or destroy before touching anything. Read the plan carefully before every apply:

terraform plan -var="project_name=myapp"

# Symbols in the diff:
# +  resource will be CREATED
# ~  resource will be UPDATED in-place
# -  resource will be DESTROYED
# -+ resource will be DESTROYED and recreated (replacement)

Save the plan to a file for exact reproducibility in CI:

terraform plan -var="project_name=myapp" -out=plan.tfplan
terraform apply plan.tfplan    # applies exactly what was planned

terraform apply

This executes the plan. Without -auto-approve, it shows the plan and prompts for confirmation. In production pipelines, save a plan file and apply it rather than using -auto-approve on ad-hoc applies:

terraform apply -var="project_name=myapp"

# After reviewing the plan, Terraform prompts:
# Do you want to perform these actions?
#   Enter a value: yes

# Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

terraform destroy

Destroys all resources managed by the configuration. This is non-reversible: S3 buckets with content, databases with data, VMs with their disks. Always confirm you are targeting the right environment:

terraform destroy -var="project_name=myapp"
# Always prompts for confirmation before destroying anything

Other Essential Commands

terraform validate      # Check syntax without connecting to cloud APIs
terraform fmt           # Auto-format all .tf files to canonical HCL style
terraform state list    # List all resources tracked in current state
terraform state show aws_s3_bucket.app  # Show full attributes of one resource
terraform import aws_s3_bucket.app existing-bucket-name  # Import existing resource
terraform output        # Print all output values from state
Environment Targeting Safety

Use separate state backends per environment (separate S3 buckets or Terraform Cloud workspaces for dev, staging, prod), not just different variable values. If all environments share one state file, a terraform destroy on "dev" can delete prod resources. Backend-level isolation is the only safe pattern.

Managing State Files Safely (Critical Security Note)

Critical Security Warning: State Files Contain Secrets

The terraform.tfstate file stores sensitive data in plaintext. Every attribute of every managed resource is recorded: database passwords, private keys, IAM access key secrets, and any other sensitive value your resources expose. This is a documented and recurring security incident pattern across the industry. Never commit terraform.tfstate to any repository. For team use: always use an encrypted remote backend.

By default, Terraform writes state to a local terraform.tfstate file. This is acceptable for learning on a local machine, but creates two problems in team environments: anyone with file access can read your secrets, and two engineers running terraform apply simultaneously will corrupt the state file.

Remote State with S3 and DynamoDB

The most common production setup on AWS uses S3 for storage and DynamoDB for state locking:

# backend.tf: Terraform >= 1.7 / OpenTofu >= 1.6
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-prod"
    key            = "myapp/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}

This stores state in S3 with server-side encryption via encrypt = true, and uses DynamoDB to prevent concurrent applies. The DynamoDB table stores a lock record with the key LockID, preventing two engineers from applying simultaneously.

One-Time Backend Bootstrap via AWS CLI

The S3 bucket and DynamoDB table must exist before Terraform can use them as a backend. Create them once with the AWS CLI v2:

# Create encrypted S3 state bucket
aws s3api create-bucket \
  --bucket my-terraform-state-prod \
  --region us-east-1

aws s3api put-bucket-encryption \
  --bucket my-terraform-state-prod \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms"}}]}'

# Create DynamoDB lock table (LockID string key: required by Terraform)
aws dynamodb create-table \
  --table-name terraform-state-lock \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

State Security Best Practices

Never commit terraform.tfstate to git
Add *.tfstate and *.tfstate.* to your .gitignore immediately. State contains plaintext secrets. A state file in a public repo has caused real credential compromises.
Use remote backend for all team work
Two engineers applying locally means state corruption. Remote backends with locking (S3+DynamoDB, Terraform Cloud, Azure Blob) prevent concurrent modifications. No exceptions.
Lock down state bucket access with IAM
Only CI/CD pipeline roles and senior engineers should have write access to the state bucket. Read Cloud IAM Explained for least-privilege patterns.
sensitive = true is display-only protection
Marking outputs sensitive = true hides them in terminal output but they remain in state as plaintext. For true secret protection, avoid putting secrets in state: use AWS Secrets Manager or HashiCorp Vault and reference ARNs instead.

Variables, Outputs, and Modules: Scaling Your Config

A flat main.tf file works for one environment. When you need to deploy the same infrastructure to dev, staging, and production, or share patterns across multiple projects, you need variables, outputs, and modules working together.

Variable Files for Multi-Environment Deployments

# environments/prod.tfvars
environment    = "prod"
aws_region     = "us-east-1"
project_name   = "myapp"

# Apply with environment-specific values:
terraform apply -var-file="environments/prod.tfvars"

For production secrets (database passwords, API keys), use environment variables injected by your CI/CD system rather than .tfvars files. Terraform reads TF_VAR_* environment variables automatically: TF_VAR_database_password=... sets the database_password variable without writing it to any file.

Outputs for Passing Data Between Stacks

Outputs expose resource attributes to other Terraform configurations or to your deployment pipeline. A common pattern: the "network" stack outputs VPC IDs and subnet IDs that the "app" stack reads as inputs. The data block (different from resource: it reads existing state rather than creating anything) is how you access another stack's outputs:

# In the network stack's outputs.tf:
output "vpc_id" {
  description = "VPC ID for dependent stacks"
  value       = aws_vpc.main.id
}

# In the app stack's data.tf, reads the network stack's state:
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state-prod"
    key    = "network/terraform.tfstate"
    region = "us-east-1"
  }
}
# Usage: data.terraform_remote_state.network.outputs.vpc_id

Modules: Reusable Infrastructure Blocks

A module is a directory of .tf files called like a function. Once you build a working S3 bucket configuration, VPC layout, or ECS cluster pattern, package it as a module and reuse it across projects:

# Calling a local module (in ./modules/s3-versioned-bucket/)
module "data_lake" {
  source      = "./modules/s3-versioned-bucket"
  bucket_name = "my-data-lake-2026"
  environment = var.environment
}

# Calling a public module from the Terraform Registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name             = "my-vpc"
  cidr             = "10.0.0.0/16"
  azs              = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets   = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
  enable_nat_gateway = true
}

Public registry modules like the AWS VPC module are battle-tested by thousands of teams. Check the registry before building custom modules from scratch: the solution very likely exists. Terraform integrates naturally with Kubernetes infrastructure: the kubernetes and helm providers manage cluster resources alongside cloud infra. See Terraform vs Pulumi if you want to compare IaC tool options before committing.

Terraform vs OpenTofu: Which Should You Learn?

On August 10, 2023, HashiCorp changed Terraform's license from the permissive Mozilla Public License 2.0 (MPL 2.0) to the Business Source License 1.1 (BSL/BUSL). The change restricts commercial use by entities offering "competitive services" to HashiCorp, primarily cloud platforms reselling Terraform as a managed service.

For individual engineers, startups, and companies not selling competing IaC platforms, the practical impact is minimal. Terraform remains free to use. But the license change alarmed the open-source community, and within weeks a fork was announced.

OpenTofu: The Open-Source Fork

OpenTofu launched as "OpenTF" in August 2023, was renamed OpenTofu, and was donated to the Linux Foundation (not CNCF: this is a persistent misconception). It forked from Terraform 1.5.6, the last version released under MPL 2.0. OpenTofu 1.12.0 was released May 14, 2026, fully open-source under MPL 2.0.

OpenTofu is syntax-compatible with Terraform for the vast majority of configurations. CLI commands are identical with a different binary name: tofu init, tofu plan, tofu apply. State files are compatible. Most providers work with both.

Terraform – Registry Provider Coverage 6,683 providers
OpenTofu – Compatible Terraform Registry access Most TF providers
Terraform – Enterprise tooling (Terraform Cloud) Full suite
OpenTofu – OSI-approved open-source (MPL 2.0) Fully open-source

Decision Guide: Terraform or OpenTofu?

For learning: Terraform. Documentation is more mature, tutorials are more abundant, and employers ask for Terraform experience by name. The BSL does not affect individual learning use.

For teams with compliance requirements: OpenTofu. If your legal or procurement process requires all software to be under an OSI-approved open-source license, OpenTofu (MPL 2.0) is the compliant path. BSL is not an OSI-approved license.

For teams already on Terraform: Stay unless you have a specific reason to migrate. The migration is low-risk but adds maintenance overhead without immediate benefit for most teams.

The HashiCorp Terraform Associate certification tests Terraform specifically: relevant if certification is on your path. Build your cloud networking foundation first at cloud networking basics if you are provisioning VPCs for the first time.

BSL Timeline Note

The BSL change date of August 10, 2023 is verified against official sources. Under BSL default terms, code transitions back to open-source 4 years after each release: versions from 2023 become open-source in 2027. New releases carry their own 4-year clock. Verify current terms at developer.hashicorp.com/terraform.

Terraform Knowledge Check
3 levels: Quick, Deep, and Mastery
1. What command downloads providers and initializes a Terraform project?
1. What problem does the DynamoDB table solve in an S3 remote backend?
1. OpenTofu is governed by which organization, and what is the common misconception?
Test Your Terraform Knowledge
1 of 5
0/5
Troubleshooting Common Terraform Errors

Terraform cannot find any .tf files in the current working directory. You are either in the wrong folder or have not created any configuration files yet.

Fix: Run ls *.tf to confirm you are in the right directory. Create at least one .tf file before running terraform init.

Terraform cannot authenticate with your cloud provider. Credentials are not configured or have expired. Never hardcode secrets in .tf files.

Fix for AWS: Run aws configure or export AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. For Azure, run az login.

Terraform is trying to create a resource that already exists but is not tracked in state. This happens when infrastructure was created manually outside of Terraform.

Fix: Use terraform import . to bring the existing resource into state management, then run terraform plan to confirm.

You changed your backend configuration and Terraform requires re-initialization. This happens when switching from local state to a remote backend such as S3.

Fix: Run terraform init -reconfigure. To migrate existing state to the new backend: terraform init -migrate-state.

Another Terraform process holds the state lock, or a previous run crashed without releasing it. Terraform uses state locking to prevent concurrent applies from corrupting infrastructure state.

Fix: Confirm no other apply is running. If the lock is genuinely stale, use Terraform's built-in state lock release command with the lock ID printed in the error message. Reference: developer.hashicorp.com/terraform/language/state/locking.

✓ Verified by Tech Jacks Solutions Cloud Infrastructure Team – June 29, 2026
Terraform® and HashiCorp® are registered trademarks of HashiCorp, Inc. OpenTofu™ is a trademark of the Linux Foundation. AWS® is a trademark of Amazon.com, Inc. Azure® is a trademark of Microsoft Corporation. Tech Jacks Solutions is not affiliated with or endorsed by any vendor. Pricing and version data verified June 2026; verify current details at official vendor documentation. Sources: HashiCorp Terraform Docs · OpenTofu Docs · Terraform Registry · endoflife.date.
Before You Use AI-Assisted Infrastructure Tooling

🔒 Your Privacy

Terraform and OpenTofu are local CLI tools: they do not send your configuration data to external servers beyond the cloud provider APIs you explicitly target. HashiCorp Terraform Cloud and Enterprise products process configuration and state server-side; review HashiCorp's data processing agreements for enterprise deployments.

AI coding assistants used to generate HCL configurations may process your code on third-party servers. Review the privacy policy of any AI assistant before using it with production infrastructure code or credentials. Never paste secrets, API keys, or credential values into AI prompts.

🧠 Using AI for Infrastructure Decisions

AI tools can help you write Terraform configurations, debug plan output, and understand HCL syntax. They can also generate plausible-sounding configurations with subtle errors, outdated syntax, or insecure defaults. Infrastructure errors can cause outages, data loss, or significant unexpected cloud costs. Always review AI-generated infra code carefully, run terraform plan before applying, and test in a non-production environment first.

If you are experiencing work-related distress, burnout, or on-call stress: 988 Lifeline · SAMHSA 1-800-662-4357 · Crisis Text Line: text HOME to 741741. AI systems can produce plausible-sounding but incorrect guidance. For mental health, medical, legal, or financial decisions, always consult a qualified professional.

⚖️ Your Rights & Our Transparency

This article was researched and written by the Tech Jacks Solutions Cloud Infrastructure Team using primary documentation from HashiCorp, OpenTofu, the Terraform Registry, and endoflife.date. No sponsored placement. Editorial positions reflect independent assessment.

Under GDPR and CCPA, you have rights to access, correct, and delete personal data we may hold. Contact privacy@techjacksolutions.com. This content may be subject to the EU AI Act's transparency requirements where applicable.

Accuracy standard: NIST AI Risk Management Framework. Version and pricing data verified June 2026; check official vendor documentation for current information.