Terraform: Core Concepts
Master the fundamentals of Infrastructure as Code, from installation to your first deployment in 30 minutes.
Getting Started
Before writing any infrastructure code, you need a few things in place. The good news: most of these are free and take only minutes to set up.
Prerequisites
| Requirement | Why You Need It | How to Get It |
|---|---|---|
| Command line basics | Terraform runs from the terminal | Practice with cd, ls, mkdir |
| Cloud account | A place to deploy infrastructure | AWS/Azure/GCP free tier |
| Text editor | Writing configuration files | VS Code with HCL extension |
| Terraform CLI | The tool itself | terraform.io/downloads |
Verifying Your Installation
After installing Terraform, confirm it works:
terraform version
# Terraform v1.7.0 on linux_amd64
Terraform Crash Course: Zero to Hero in 30 Minutes
This hands-on crash course takes you from zero knowledge to deploying real cloud infrastructure. By the end, you will understand the complete Terraform workflow.
Step 1: Your First Terraform File (5 minutes)
Every Terraform project starts with a configuration file. Create a new directory and file:
mkdir terraform-tutorial && cd terraform-tutorial
touch main.tf
Add this configuration to main.tf. Notice how it reads almost like a description of what you want:
terraform {
required_version = ">= 1.5"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" {
region = "us-east-1" # Uses AWS_ACCESS_KEY_ID env var
}
resource "aws_s3_bucket" "my_first_bucket" {
bucket = "my-unique-bucket-12345" # Must be globally unique
tags = { Name = "My First Terraform Bucket" }
}
What is happening here:
terraform {}block configures Terraform itself and declares required providersprovider "aws"tells Terraform how to connect to AWSresourcedeclares infrastructure you want to create
Step 2: The Terraform Workflow
Terraform follows a predictable three-step workflow. Understanding this workflow is essential because you will repeat it hundreds of times.
| Command | What It Does | When to Use |
|---|---|---|
terraform init |
Downloads providers, sets up backend | Once per project, or after adding providers |
terraform plan |
Shows what will change (dry run) | Before every apply, to review changes |
terraform apply |
Creates/updates real infrastructure | When you are ready to make changes |
terraform destroy |
Removes all managed resources | Cleanup, or tearing down environments |
# Step 1: Initialize (downloads the AWS provider)
terraform init
# Step 2: Preview changes (nothing is created yet)
terraform plan
# Step 3: Apply changes (type 'yes' to confirm)
terraform apply
Step 3: Modifying Infrastructure
The real power of Terraform shows when you change things. Add versioning to your bucket:
resource "aws_s3_bucket_versioning" "versioning" {
bucket = aws_s3_bucket.my_first_bucket.id
versioning_configuration { status = "Enabled" }
}
Run terraform plan again. Terraform shows exactly what will change - in this case, one resource added. This predictability is why teams trust Terraform with production infrastructure.
Step 4: Understanding State
After applying, check your directory. You will see a terraform.tfstate file. This is Terraform’s memory of what it created.
terraform state list # See managed resources
terraform state show aws_s3_bucket.my_first_bucket # Details
Why state matters: Terraform compares your configuration against state to determine what changes are needed. Without state, Terraform would try to create everything fresh each time.
Step 5: Clean Up
When you are done experimenting, remove everything:
terraform destroy # Type 'yes' to confirm
What you accomplished:
- Wrote Infrastructure as Code
- Created real cloud resources
- Modified existing infrastructure safely
- Understood state management
- Cleaned up automatically
Core Concepts
Now that you have hands-on experience, let’s understand the principles that make Terraform work.
Declarative vs Imperative: Why It Matters
Consider the following approaches to creating a web server:
| Imperative (Scripts) | Declarative (Terraform) |
|---|---|
| “Create a VM. If it fails, retry. Then install nginx. Configure the firewall…” | “I want a VM with nginx and these firewall rules” |
| You specify every step | You specify the end state |
| Order matters | Terraform figures out the order |
| Partial failures leave mess | Terraform tracks what succeeded |
Terraform’s declarative approach means you describe what you want, not how to get there. This eliminates entire categories of bugs.
Why Infrastructure as Code Changes Everything
When infrastructure becomes code, you gain powerful capabilities:
- Version Control: Track every change through git history
- Code Review: Infrastructure changes go through pull requests
- Rollback: Return to any previous state instantly
- Reusability: Package patterns into modules
- Testing: Validate before deploying
How Terraform Works Under the Hood
Understanding Terraform’s internals helps you write better configurations and debug issues faster.
Dependency Graphs: Automatic Ordering
Consider the following scenario: You need a database, a web server that connects to it, and a load balancer in front of the web server. These must be created in order - you cannot connect to a database that does not exist yet.
Terraform automatically figures out this ordering by building a dependency graph. When you reference one resource from another (like database.endpoint), Terraform knows to create the database first.
What this means for you:
- You do not need to specify creation order
- Terraform creates independent resources in parallel (faster deployments)
- Circular dependencies are caught before anything is created
The Plan-Apply Cycle
When you run terraform plan, Terraform performs a three-way comparison:
- Your Configuration: What you wrote in
.tffiles - Current State: What Terraform remembers from the last apply
- Real Infrastructure: What actually exists in the cloud (via API calls)
This comparison produces one of five actions for each resource:
| Action | Symbol | Meaning |
|---|---|---|
| Create | + |
Resource does not exist, will be created |
| Update | ~ |
Resource exists but configuration changed |
| Replace | -/+ |
Must destroy and recreate (e.g., changing instance type) |
| Delete | - |
Resource in state but not in configuration |
| No-op | (none) | Everything matches, no action needed |
Practical tip: Always review the plan output before applying. The symbols make it easy to spot unexpected changes.
Providers: Connecting to Cloud Services
Providers are plugins that let Terraform communicate with cloud platforms and services. Think of them as translators between Terraform’s configuration language and each platform’s API.
How Providers Work
When you declare a provider, Terraform:
- Downloads the provider plugin during
terraform init - Uses it to translate your configuration into API calls
- Handles authentication, retries, and rate limiting automatically
# Tell Terraform which providers you need
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
azure = { source = "hashicorp/azurerm", version = "~> 3.0" }
}
}
Commonly Used Providers
| Provider | Use Case | Example Resources |
|---|---|---|
aws |
Amazon Web Services | EC2 instances, S3 buckets, RDS databases |
azurerm |
Microsoft Azure | VMs, Storage accounts, SQL databases |
google |
Google Cloud | Compute instances, GCS buckets, Cloud SQL |
kubernetes |
K8s clusters | Deployments, Services, ConfigMaps |
random |
Generate random values | Random IDs, passwords, pet names |
Provider Authentication
Terraform supports multiple authentication methods. Choose based on your security requirements:
| Method | Best For | Security Level |
|---|---|---|
| Environment variables | CI/CD pipelines | Good |
| Shared credentials file | Local development | Moderate |
| IAM roles (assume role) | Cross-account access | Excellent |
| Instance profiles | EC2-based automation | Excellent |
provider "aws" {
region = "us-east-1"
# Option 1: Use environment variables (recommended for CI/CD)
# Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
# Option 2: Assume a role (recommended for cross-account)
assume_role {
role_arn = "arn:aws:iam::123456789:role/TerraformRole"
}
# Apply default tags to all resources
default_tags {
tags = { ManagedBy = "Terraform" }
}
}
Security tip: Never hardcode credentials in Terraform files. Use environment variables or IAM roles instead.
Resource Lifecycles
Every resource in Terraform goes through a lifecycle: creation, updates, and eventual deletion. Understanding this lifecycle helps you handle special cases.
The Lifecycle Meta-Argument
Sometimes you need to customize how Terraform handles resource changes. The lifecycle block lets you do this:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true # Create new before destroying old
prevent_destroy = true # Block accidental deletion
ignore_changes = [tags] # Ignore external tag changes
}
}
When to Use Each Lifecycle Option
| Option | Use Case | Example |
|---|---|---|
create_before_destroy |
Zero-downtime updates | Load balancers, DNS records |
prevent_destroy |
Critical resources | Production databases, S3 buckets with data |
ignore_changes |
Externally managed attributes | Tags set by other tools, autoscaling counts |
replace_triggered_by |
Force replacement | When AMI changes, replace instance |
Variables and Types
Variables make your Terraform code reusable. Instead of hardcoding values, you define variables that can change between environments.
Variable Types at a Glance
| Type | Example | Use Case |
|---|---|---|
string |
"us-east-1" |
Regions, names, IDs |
number |
3 |
Counts, sizes, ports |
bool |
true |
Feature flags |
list(string) |
["a", "b"] |
Multiple values of same type |
map(string) |
{env = "prod"} |
Key-value pairs |
object({...}) |
Complex structures | Grouped configuration |
Defining Variables with Validation
Catch errors before they reach production by validating inputs:
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_type" {
type = string
default = "t3.micro"
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "Only t3 instance types are allowed."
}
}
Variable Precedence
Variables can be set in multiple places. Terraform uses them in this order (later overrides earlier):
- Default values in variable definitions
- Environment variables (
TF_VAR_name) terraform.tfvarsfile*.auto.tfvarsfiles (alphabetical order)-var-filecommand line flag-varcommand line flag
Practical tip: Use terraform.tfvars for environment-specific values and keep it out of version control for sensitive data.
Using Variables in Practice
Here is a complete example showing variables in action:
# variables.tf
variable "region" {
description = "AWS region"
default = "us-west-2"
}
variable "bucket_name" {
description = "Name for the S3 bucket"
type = string # Required - no default
}
# main.tf
provider "aws" {
region = var.region
}
resource "aws_s3_bucket" "data" {
bucket = var.bucket_name
}
# terraform.tfvars
bucket_name = "my-company-data-bucket"
When you run terraform apply, Terraform uses the values from terraform.tfvars automatically.