Skip to main content

Terraform Tutorial — Providers, Resources, and Your First Infrastructure

Write your first Terraform configuration from scratch — installing Terraform, configuring the AWS provider, and provisioning real infrastructure.

26 Terms

Overview and What You Will Learn

Before Terraform, creating an EC2 instance at a company like Razorpay meant logging into the AWS console, clicking through seven different screens, filling in a form, and hoping the engineer who did it last time left a wiki page with the exact settings. The next time you needed the same instance — for a new environment, a disaster recovery test, a colleague who just joined — you started from scratch. You got slightly different results every time. You could not audit what changed. You could not reproduce it reliably.

Terraform replaces all of that with a text file. You describe the infrastructure you want in plain, readable code. Terraform figures out how to create it, in the right order, using the cloud provider's APIs. Every setting is visible in the file. Every change is a pull request. Every environment is reproducible in minutes.

In this lab you will build real AWS infrastructure from a blank directory using the exact workflow engineering teams use at production scale. By the end you will have:

  • Installed Terraform and configured AWS authentication
  • Written a complete multi-file Terraform configuration with providers, resources, variables, and outputs
  • Used terraform init, plan, and apply to create real AWS resources
  • Read and understood plan output — the most critical skill for working safely with Terraform
  • Made a live change and watched Terraform apply only what changed
  • Cleaned up all resources with terraform destroy

Why This Matters in Production

At Razorpay, every engineer who wants to create infrastructure opens a pull request with a Terraform configuration. Another engineer reviews the terraform plan output posted as a PR comment. If it looks right, they approve. The CI/CD pipeline applies. The infrastructure exists — and the code that created it is in Git history forever.

This workflow gives the team:

  • Reproducibility — any environment can be recreated from code in minutes, not days
  • Auditability — every infrastructure change has a commit, an author, a reviewer, and a timestamp
  • Safety — nobody can create infrastructure without another engineer seeing exactly what it does
  • Consistency — dev, staging, and prod are created from the same code with different variable values

Teams that adopt this workflow reduce environment setup time from days to under an hour and eliminate entire classes of production incidents caused by configuration drift between environments.

Core Principles

The Terraform Workflow
Bash
+------------------------------------------+
| Step 1 — Write (.tf files) |
| Describe what you want: |
| resource "aws_s3_bucket" "orders" { |
| bucket = "swiggy-orders-prod" |
| } |
+------------------------------------------+
|
v
+------------------------------------------+
| Step 2 — Init |
| terraform init |
| Downloads: hashicorp/aws v5.31.0 |
| Sets up state backend |
+------------------------------------------+
|
v
+------------------------------------------+
| Step 3 — Plan (safe preview) |
| terraform plan |
| + aws_s3_bucket.orders will be created |
| Plan: 1 to add, 0 to change, 0 to destroy|
+------------------------------------------+
|
v
+------------------------------------------+
| Step 4 — Apply (make it real) |
| terraform apply |
| aws_s3_bucket.orders: Creating... |
| aws_s3_bucket.orders: Creation complete |
+------------------------------------------+
|
v
+------------------------------------------+
| Step 5 — State (Terraform remembers) |
| terraform.tfstate records: |
| id = "swiggy-orders-prod" |
| arn = "arn:aws:s3:::swiggy-orders-prod" |
+------------------------------------------+
How Terraform Talks to AWS
◈ DIAGRAM
+------------------------------------------+
| Your .tf files |
| HCL describing what you want |
+------------------------------------------+
|
v
+------------------------------------------+
| Terraform Core |
| Reads config, builds dependency graph |
| Decides creation order automatically |
+------------------------------------------+
|
v
+------------------------------------------+
| AWS Provider Plugin |
| Knows how to call every AWS API |
| Handles auth, retries, error messages |
+------------------------------------------+
|
v
+------------------------------------------+
| AWS APIs (HTTPS) |
| EC2 API, S3 API, IAM API, RDS API... |
+------------------------------------------+
|
v
+------------------------------------------+
| Real AWS Resources |
| EC2 instances, S3 buckets, security |
| groups running in ap-south-1 |
+------------------------------------------+

Detailed Step-by-Step Practical Lab

Prerequisites

Before starting this lab you need:

  • An AWS account with IAM user or IAM role access
  • IAM permissions: EC2 full access, S3 full access (for this lab)
  • A terminal — macOS Terminal, Linux shell, or WSL on Windows
  • Git installed
Step 1 — Install Terraform

Use tfenv to manage Terraform versions. It reads a .terraform-version file from your project and automatically switches to the correct version — the same way nvm works for Node.js.

Bash
brew install tfenv
# ── Linux ──────────────────────────────────────────────────────────────────
git clone https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# ── Install and activate Terraform 1.6.3 ──────────────────────────────────
tfenv install 1.6.3
tfenv use 1.6.3
# ── Verify the installation ────────────────────────────────────────────────
terraform version
# Terraform v1.6.3
# on linux_amd64
# ── Pin the version for your project ──────────────────────────────────────
# tfenv reads this file and auto-switches when you cd into the directory
echo "1.6.3" > .terraform-version
Tip

If you cannot use tfenv, download Terraform directly from https://developer.hashicorp.com/terraform/downloads and place the binary in your PATH. Always use the same version as your team — version mismatches cause state file compatibility issues.

Step 2 — Configure AWS Authentication

Terraform uses your AWS credentials to call the AWS APIs. Never put credentials in .tf files — they end up in Git history and get leaked.

Bash
# ── Option 1: Environment variables (recommended for local development) ───
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="ap-south-1" # Mumbai
# Verify credentials work before running Terraform
aws sts get-caller-identity
# {
# "UserId": "AIDAIOSFODNN7EXAMPLE",
# "Account": "123456789012",
# "Arn": "arn:aws:iam::123456789012:user/terraform-user"
# }
# ── Option 2: AWS named profile (multiple accounts) ────────────────────────
aws configure --profile razorpay-dev
# AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
# AWS Secret Access Key: xxxxxxxxxxxxxxxxxxxx
# Default region name: ap-south-1
# Default output format: json
export AWS_PROFILE="razorpay-dev"
# Terraform will now use this profile automatically
Step 3 — Create Your Project Directory

A well-organised Terraform directory separates concerns into different files. Terraform loads all .tf files in a directory together, so the separation is purely for readability.

Bash
mkdir terraform-first-lab && cd terraform-first-lab
# Standard Terraform file layout — each file has one job
touch versions.tf # Terraform version and provider version requirements
touch provider.tf # Provider configuration (region, default tags)
touch variables.tf # Input variable declarations
touch main.tf # Resource declarations — the actual infrastructure
touch outputs.tf # Output value declarations
# Pin the Terraform version for the whole team
echo "1.6.3" > .terraform-version
# Create .gitignore immediately — before any apply
cat > .gitignore << 'EOF'
# State files — NEVER commit to Git (contain sensitive values in plaintext)
terraform.tfstate
terraform.tfstate.backup
*.tfstate
*.tfstate.backup
# Working directory — regenerated by terraform init, never commit
.terraform/
# Variable files that may contain secrets
*.auto.tfvars
# Keep terraform.tfvars in Git only if it has no secrets
# Always commit terraform.tfvars.example as documentation
# Crash logs
crash.log
crash.*.log
EOF
Step 4 — Declare Terraform and Provider Requirements
HCL
# versions.tf
# This file tells Terraform three things:
# 1. What minimum version of Terraform CLI is required
# 2. Which provider plugins to download and from where
# 3. Where to store state (the backend — add this when working with a team)
terraform {
# Reject older Terraform versions that may not support features we use
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws" # download from registry.terraform.io/hashicorp/aws
version = "~> 5.0" # any 5.x version (>= 5.0.0, < 6.0.0)
# ~> allows patch updates, blocks major version jumps
}
}
# Add this backend block before your second engineer joins the project
# backend "s3" {
# bucket = "razorpay-terraform-state"
# key = "dev/first-lab/terraform.tfstate"
# region = "ap-south-1"
# dynamodb_table = "terraform-state-lock"
# encrypt = true
# }
}
HCL
# provider.tf
# Configures the AWS provider — tells it which region to use
# and how to authenticate (reads from environment variables automatically)
provider "aws" {
region = var.aws_region # use a variable, not a hardcoded string
# default_tags applies these tags to EVERY resource this provider creates
# Saves adding tags = {} on every single resource block
default_tags {
tags = {
ManagedBy = "terraform"
Repository = "github.com/razorpay/terraform-first-lab"
Environment = var.environment
}
}
}
Step 5 — Declare Input Variables

Variables make your configuration reusable across environments. Instead of hardcoding "ap-south-1" in ten places, declare it once as a variable and reference it everywhere with var.aws_region.

HCL
# variables.tf
variable "aws_region" {
description = "AWS region to deploy infrastructure into"
type = string
default = "ap-south-1" # Mumbai — change to your nearest region
# Validation catches typos before Terraform makes any API calls
validation {
condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]$", var.aws_region))
error_message = "Must be a valid AWS region code like ap-south-1 or us-east-1."
}
}
variable "environment" {
description = "Deployment environment — used in resource names and tags"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "project" {
description = "Project name — used as prefix in all resource names and tags"
type = string
default = "razorpay-api"
}
variable "instance_type" {
description = "EC2 instance type for the web server"
type = string
default = "t3.micro" # smallest — appropriate for a learning lab
}

Create a terraform.tfvars file to set values for this environment:

HCL
# terraform.tfvars — auto-loaded by Terraform on every plan and apply
# Override variable defaults here for the dev environment
# Do NOT put secrets (passwords, API keys) in this file
aws_region = "ap-south-1"
environment = "dev"
project = "razorpay-api"
instance_type = "t3.micro"
Step 6 — Write Your Infrastructure Resources
HCL
# main.tf
# All AWS resources go here.
# Resources can be written in any order — Terraform builds the dependency
# graph automatically from references between resources.
# ── Locals: computed values used throughout this configuration ────────────
locals {
# Single source of truth for the naming prefix
# razorpay-api-dev — every resource name starts with this
name_prefix = "${var.project}-${var.environment}"
# Extra tags merged onto resources alongside provider default_tags
common_tags = {
Project = var.project
}
}
# ── Data Source: latest Amazon Linux 2023 AMI ────────────────────────────────
# Data sources READ existing information — they never create anything
# This finds the current Amazon Linux 2023 AMI in your region automatically
# The AMI ID changes whenever Amazon releases a security patch
data "aws_ami" "amazon_linux" {
most_recent = true # get newest matching AMI
owners = ["amazon"] # official Amazon AMIs only — not community
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"] # Amazon Linux 2023 naming pattern
}
filter {
name = "virtualization-type"
values = ["hvm"] # hardware virtual machine — required for t3 instances
}
}
# ── Data Source: current AWS account ID ───────────────────────────────────
# S3 bucket names are globally unique across all AWS accounts
# Including the account ID in the name guarantees no naming conflicts
data "aws_caller_identity" "current" {}
# ── S3 Bucket: application asset storage ──────────────────────────────────
resource "aws_s3_bucket" "app_assets" {
# Globally unique name — account ID suffix eliminates collision risk
bucket = "${local.name_prefix}-assets-${data.aws_caller_identity.current.account_id}"
tags = local.common_tags
}
# Block all public access — application data is never public
# AWS exposes this as a separate API from the bucket itself, so it is a
# separate resource block
resource "aws_s3_bucket_public_access_block" "app_assets" {
bucket = aws_s3_bucket.app_assets.id # reference creates implicit dependency
# Terraform will create the bucket first
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Enable versioning — recover accidentally deleted or overwritten objects
resource "aws_s3_bucket_versioning" "app_assets" {
bucket = aws_s3_bucket.app_assets.id
versioning_configuration {
status = "Enabled"
}
}
# Enable server-side encryption — all objects encrypted at rest with AES-256
resource "aws_s3_bucket_server_side_encryption_configuration" "app_assets" {
bucket = aws_s3_bucket.app_assets.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256" # SSE-S3 — free, no extra KMS charges
}
}
}
# ── Security Group: control network access to the EC2 instance ────────────
resource "aws_security_group" "web" {
name = "${local.name_prefix}-web-sg"
description = "Security group for the ${var.environment} web server"
# Allow HTTPS inbound from anywhere
ingress {
description = "HTTPS from internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow HTTP inbound — for redirect to HTTPS
ingress {
description = "HTTP from internet — redirect to HTTPS"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow all outbound — instance needs internet access to download packages
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1" # -1 means all protocols
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
# ── EC2 Instance: the web server ──────────────────────────────────────────
resource "aws_instance" "web" {
# AMI from the data source — always the latest patched Amazon Linux 2023
ami = data.aws_ami.amazon_linux.id
# Instance type from the variable — t3.micro for dev, larger for prod
instance_type = var.instance_type
# Terraform creates the security group first because we reference its .id
vpc_security_group_ids = [aws_security_group.web.id]
# user_data: shell script that runs once when the instance first boots
user_data = <<-EOF
#!/bin/bash
dnf update -y
# Install nginx web server
dnf install -y nginx
systemctl start nginx
systemctl enable nginx
# Write a simple health check response page
echo "<h1>${var.project} — ${var.environment}</h1>" > /usr/share/nginx/html/index.html
EOF
# Require IMDSv2 — blocks SSRF attacks that steal instance credentials
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-server"
})
}
Step 7 — Declare Outputs

Outputs print useful values after apply and make attributes available to other Terraform configurations.

HCL
# outputs.tf
output "s3_bucket_name" {
description = "S3 bucket name — set as APP_ASSETS_BUCKET env var in your application"
value = aws_s3_bucket.app_assets.id
}
output "s3_bucket_arn" {
description = "S3 bucket ARN — use in IAM policy resource fields"
value = aws_s3_bucket.app_assets.arn
}
output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "Public IP address of the web server"
value = aws_instance.web.public_ip
}
output "instance_public_dns" {
description = "Public DNS name of the web server"
value = aws_instance.web.public_dns
}
output "ami_id_used" {
description = "AMI ID selected by the data source — useful for auditing"
value = data.aws_ami.amazon_linux.id
}
Step 8 — terraform init: Download Providers

Run terraform init from the project directory. This downloads the AWS provider plugin, sets up the backend, and creates the lock file.

Bash
terraform init
# Initializing the backend...
#
# Initializing provider plugins...
# - Finding hashicorp/aws versions matching "~> 5.0"...
# - Installing hashicorp/aws v5.31.0...
# - Installed hashicorp/aws v5.31.0 (signed by HashiCorp)
#
# Terraform has created a lock file .terraform.lock.hcl to record the provider
# selections it made above. Include this file in your version control repository
# so that Terraform can guarantee to make the same selections by default when
# you run "terraform init" in the future.
#
# Terraform has been successfully initialized!
# Commit the lock file — never commit the .terraform/ directory
git add .terraform.lock.hcl
Remember

Commit .terraform.lock.hcl to Git. This file records the exact provider version and cryptographic hash — it ensures every engineer and CI/CD pipeline downloads the identical provider binary. Without it, a silent provider upgrade could change behaviour between teammates.

Step 9 — terraform validate: Check for Errors Before Plan

Validate checks syntax and internal consistency without making any API calls. Run it before every plan.

Bash
terraform validate
# Success! The configuration is valid.
# Common errors you will see as you learn:
# Error: Unsupported argument
# An argument named "buckett" is not expected here. Did you mean "bucket"?
#
# Error: Reference to undeclared resource
# A resource "aws_s3_bucket" "app_aseets" has not been declared.
Step 10 — terraform fmt: Format Your Code

terraform fmt normalises all .tf files to the canonical Terraform style — consistent indentation, aligned equals signs, sorted block arguments.

Bash
# Format all .tf files in place
terraform fmt
# See what would change without modifying files
terraform fmt -diff
# Use in CI to reject unformatted code — exits non-zero if any file needs formatting
terraform fmt -check
Step 11 — terraform plan: Preview Every Change Before It Happens

This is the most important step. Read every line of the plan before applying. Nothing changes when you run plan — it is entirely read-only.

Bash
terraform plan
# Terraform will perform the following actions:
#
# # aws_instance.web will be created
# + resource "aws_instance" "web" {
# + ami = "ami-0f5ee92e2d63afc18" <- from data source
# + instance_type = "t3.micro"
# + id = (known after apply) <- AWS assigns this
# + public_ip = (known after apply)
# + vpc_security_group_ids = (known after apply) <- after SG is created
# + tags = {
# + "Environment" = "dev"
# + "ManagedBy" = "terraform"
# + "Name" = "razorpay-api-dev-web-server"
# + "Project" = "razorpay-api"
# }
# }
#
# # aws_s3_bucket.app_assets will be created
# + resource "aws_s3_bucket" "app_assets" {
# + bucket = "razorpay-api-dev-assets-123456789012"
# + id = (known after apply)
# + arn = (known after apply)
# }
#
# # aws_security_group.web will be created
# + resource "aws_security_group" "web" { ... }
#
# Plan: 6 to add, 0 to change, 0 to destroy.

Plan symbols — memorise these before you ever run terraform apply:

◈ DIAGRAM
+ Resource will be CREATED (brand new)
~ Resource will be UPDATED in-place (no downtime, no recreation)
-/+ Resource will be DESTROYED then RECREATED (check why — data loss risk)
- Resource will be DESTROYED (irreversible — read carefully)
<= Data source will be READ (read-only, completely safe)

Save the plan to a file for production CI/CD — this guarantees exactly what was reviewed is what gets applied:

Bash
# Save plan as a binary file
terraform plan -out=tfplan.binary
# Show the saved plan in human-readable form
terraform show tfplan.binary
# Apply exactly this saved plan — no re-evaluation
terraform apply tfplan.binary
Step 12 — terraform apply: Create the Infrastructure
Bash
terraform apply
# Terraform shows the full plan again and asks for confirmation
# Enter a value: yes
# aws_s3_bucket.app_assets: Creating...
# aws_security_group.web: Creating... <- parallel — no dependency
# aws_s3_bucket.app_assets: Creation complete after 2s
# aws_s3_bucket_public_access_block.app_assets: Creating... <- depends on bucket
# aws_security_group.web: Creation complete after 4s
# aws_instance.web: Creating... <- depends on security group
# aws_s3_bucket_public_access_block.app_assets: Creation complete after 1s
# aws_s3_bucket_versioning.app_assets: Creating...
# aws_s3_bucket_versioning.app_assets: Creation complete after 1s
# aws_s3_bucket_server_side_encryption_configuration.app_assets: Creating...
# aws_s3_bucket_server_side_encryption_configuration.app_assets: Creation complete
# aws_instance.web: Still creating... [10s elapsed]
# aws_instance.web: Still creating... [20s elapsed]
# aws_instance.web: Creation complete after 23s [id=i-0a1b2c3d4e5f]
#
# Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
#
# Outputs:
# ami_id_used = "ami-0f5ee92e2d63afc18"
# instance_id = "i-0a1b2c3d4e5f"
# instance_public_dns = "ec2-13-126-x-x.ap-south-1.compute.amazonaws.com"
# instance_public_ip = "13.126.x.x"
# s3_bucket_arn = "arn:aws:s3:::razorpay-api-dev-assets-123456789012"
# s3_bucket_name = "razorpay-api-dev-assets-123456789012"

Notice Terraform creates resources in parallel wherever dependencies allow — the S3 bucket and security group start at the same time. Once the security group exists, the EC2 instance starts. Terraform manages all ordering automatically from the dependency graph — you never write "create X before Y" anywhere.

Step 13 — Verify and Inspect State
Bash
# List every resource Terraform is now tracking
terraform state list
# data.aws_ami.amazon_linux
# data.aws_caller_identity.current
# aws_instance.web
# aws_s3_bucket.app_assets
# aws_s3_bucket_public_access_block.app_assets
# aws_s3_bucket_server_side_encryption_configuration.app_assets
# aws_s3_bucket_versioning.app_assets
# aws_security_group.web
# Inspect every stored attribute of one resource
terraform state show aws_instance.web
# Print all output values
terraform output
# Print one output value as raw text — for use in shell scripts
terraform output -raw instance_public_ip
# Test the web server
curl http://$(terraform output -raw instance_public_ip)/
# <h1>razorpay-api — dev</h1>
Step 14 — Make a Change and Apply the Minimum Diff
HCL
# In variables.tf — change the default instance type
variable "instance_type" {
default = "t3.small" # was t3.micro
}
Bash
terraform plan
# ~ aws_instance.web will be updated in-place
# ~ instance_type = "t3.micro" -> "t3.small"
#
# Plan: 0 to add, 1 to change, 0 to destroy.
# This is an in-place update — the instance is NOT recreated
terraform apply
# aws_instance.web: Modifying... [id=i-0a1b2c3d4e5f]
# aws_instance.web: Modifications complete after 9s
# Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Terraform calculated that only the instance type needed to change — everything else stayed untouched. This is Terraform's declarative model: you describe the desired end state, Terraform figures out the minimum set of changes to get there.

Step 15 — terraform destroy: Clean Up All Resources
Bash
# Preview what will be destroyed before committing
terraform plan -destroy
# Destroy all resources — type "yes" when prompted
terraform destroy
# aws_s3_bucket_server_side_encryption_configuration.app_assets: Destroying...
# aws_s3_bucket_versioning.app_assets: Destroying...
# aws_s3_bucket_public_access_block.app_assets: Destroying...
# aws_instance.web: Destroying...
# aws_s3_bucket.app_assets: Destroying...
# aws_instance.web: Destruction complete after 32s
# aws_s3_bucket.app_assets: Destruction complete after 1s
# aws_security_group.web: Destroying...
# aws_security_group.web: Destruction complete after 1s
#
# Destroy complete! Resources: 6 destroyed.
Security

Always run terraform destroy after finishing labs to avoid unexpected AWS charges. A t3.micro instance left running costs roughly $8 per month. Set a billing alert in the AWS console as a safety net.

Production Best Practices and Common Pitfalls

  • Pin provider versions with ~> MAJOR.MINOR. An unpinned provider (version = ">= 5.0") can auto-upgrade to a breaking major version. The ~> operator allows patch updates while blocking major jumps — ~> 5.0 means any version from 5.0.0 up to but not including 6.0.0.

  • Use default_tags in the provider block. Declare shared tags once in the provider's default_tags block. They apply automatically to every AWS resource the provider creates — you cannot forget them on a resource.

  • Never hardcode resource names. Use a locals name prefix: "${var.project}-${var.environment}". This ensures dev, staging, and prod resources all have distinct, recognisable names without duplicating logic across files.

  • Use data sources for AMI IDs — never hardcode them. A hardcoded AMI ID eventually becomes outdated or unavailable in another region. The data "aws_ami" data source always returns the current patched image.

  • Add a remote backend before your team grows. Set up S3 + DynamoDB locking as soon as a second engineer joins. Adding it later requires a state migration. Start remote from day one.

  • Commit .terraform.lock.hcl, never .terraform/. The lock file records exact provider versions and cryptographic hashes — commit it so every engineer and pipeline is on identical providers. The .terraform/ directory is the downloaded binary — large, regenerated by terraform init, never commit it.

  • Read every -/+ in the plan. Replace actions destroy the existing resource and create a new one — which causes downtime or data loss for stateful resources like RDS. Always understand why a replacement is happening before you apply.

Quick Reference and Troubleshooting Commands

Command What It Does
terraform init Download providers, configure backend, install modules
terraform init -upgrade Upgrade providers to latest allowed version
terraform validate Check syntax and internal consistency — no API calls
terraform fmt Format all .tf files to standard style
terraform fmt -check Check formatting without changes — for CI pipelines
terraform plan Preview changes — nothing is modified
terraform plan -out=tfplan Save plan to binary file for later apply
terraform apply Apply changes — prompts for confirmation
terraform apply tfplan Apply a saved plan file — no re-evaluation
terraform apply -auto-approve Apply without confirmation — CI/CD only, never prod manually
terraform destroy Destroy all managed resources — prompts for confirmation
terraform state list List all resources tracked in state
terraform state show <addr> Show all stored attributes of one resource
terraform output Print all output values
terraform output -raw <name> Print one output value without quotes — for shell scripts
terraform console Interactive HCL expression evaluator
Error Root Cause Fix
No configuration files found in directory Running from wrong directory cd to the directory containing .tf files
Provider hashicorp/aws not installed Did not run terraform init Run terraform init
No valid credential sources found for AWS Provider Missing AWS credentials Export AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
BucketAlreadyExists S3 bucket names are globally unique Add account ID suffix using data.aws_caller_identity.current.account_id
InvalidAMIID.NotFound AMI filter matches nothing Check filter pattern and verify owners = ["amazon"]
Error: Invalid function argument Wrong type passed to a function Read the error — it shows expected vs actual type
Error: Cycle Two resources reference each other circularly Remove the circular reference
Plan shows -/+ unexpectedly A change forces resource replacement Read the (forces replacement) annotation — check provider docs for that argument

Resources

Explore More in Terraform Fundamentals and Core Workflow

All 4 Topics

Frequently Asked Questions

Is Terraform Tutorial — Providers, Resources, and Your First Infrastructure free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Terraform Tutorial — Providers, Resources, and Your First Infrastructure topic cover?

Write your first Terraform configuration from scratch — installing Terraform, configuring the AWS provider, and provisioning real infrastructure.