Master the fundamentals of Terraform - the industry-standard Infrastructure as Code tool. Learn HCL syntax, core workflow, resource definitions, and deploy your first cloud infrastructure.
Terraform has become the de facto standard for Infrastructure as Code. Created by HashiCorp, it allows you to define, provision, and manage infrastructure across multiple cloud providers using a simple, declarative language called HCL (HashiCorp Configuration Language).
What makes Terraform special is its ability to work with virtually any cloud provider - AWS, Azure, GCP, Kubernetes, and hundreds more - using a consistent workflow. Write your configuration once, and Terraform figures out the exact API calls needed to make your infrastructure real.
In this lesson, you'll move from theory to practice. You'll write real Terraform code, understand the core workflow that millions of engineers use daily, and deploy actual cloud resources. By the end, you'll have the foundation to start building production infrastructure with confidence.
Every Terraform project follows the same four-step workflow. Understanding this cycle is essential for working effectively with Terraform.
Step 1: Write Configuration
Create .tf files that describe your infrastructure:
1# main.tf
2resource "aws_instance" "web" {
3 ami = "ami-0c55b159cbfafe1f0"
4 instance_type = "t2.micro"
5}**Step 2: Initialize (terraform init)**
Download providers and set up the working directory:
1$ terraform init
2
3Initializing the backend...
4Initializing provider plugins...
5- Finding hashicorp/aws versions matching "~> 5.0"...
6- Installing hashicorp/aws v5.31.0...
7
8Terraform has been successfully initialized!**Step 3: Plan (terraform plan)**
Preview what Terraform will do:
1$ terraform plan
2
3Terraform will perform the following actions:
4
5 # aws_instance.web will be created
6 + resource "aws_instance" "web" {
7 + ami = "ami-0c55b159cbfafe1f0"
8 + instance_type = "t2.micro"
9 + id = (known after apply)
10 ...
11 }
12
13Plan: 1 to add, 0 to change, 0 to destroy.**Step 4: Apply (terraform apply)**
Create the infrastructure:
1$ terraform apply
2
3aws_instance.web: Creating...
4aws_instance.web: Creation complete after 32s [id=i-0abc123def456]
5
6Apply complete! Resources: 1 added, 0 changed, 0 destroyed.**Bonus: Destroy (terraform destroy)**
Remove everything when done:
1$ terraform destroy
2# Removes all resources managed by this configurationThe Workflow Cycle:
1Write → Init → Plan → Apply → (Modify → Plan → Apply) → Destroy
2 ↑_______________________|After the initial setup, you'll spend most time in the Plan → Apply cycle as you iterate on your infrastructure.
HCL (HashiCorp Configuration Language) is Terraform's domain-specific language. It's designed to be human-readable while being machine-parseable.
Basic Structure:
1# Comments start with #
2# or can be multi-line
3/* like this
4 across multiple lines */
5
6# Blocks are the fundamental building unit
7block_type "label1" "label2" {
8 argument1 = "value1"
9 argument2 = 123
10
11 nested_block {
12 nested_argument = true
13 }
14}Arguments and Values:
1# Strings
2name = "my-server"
3description = "A web server"
4
5# Numbers
6count = 3
7port = 8080
8
9# Booleans
10enable_monitoring = true
11delete_on_terminate = false
12
13# Lists
14availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
15ports = [80, 443, 8080]
16
17# Maps
18tags = {
19 Name = "web-server"
20 Environment = "production"
21 Team = "platform"
22}
23
24# Multi-line strings (heredoc)
25user_data = <<-EOF
26 #!/bin/bash
27 apt-get update
28 apt-get install -y nginx
29EOFExpressions:
1# References to other resources
2subnet_id = aws_subnet.main.id
3
4# Interpolation
5name = "server-${var.environment}"
6
7# Conditional expressions
8instance_type = var.environment == "production" ? "t2.large" : "t2.micro"
9
10# For expressions
11subnets = [for s in aws_subnet.main : s.id]
12
13# Built-in functions
14timestamp = timestamp()
15upper_name = upper(var.name)Block Types You'll Use:
| Block | Purpose | Example |
|-------|---------|---------|
| terraform | Configure Terraform itself | Version constraints |
| provider | Configure cloud providers | AWS, Azure, GCP |
| resource | Define infrastructure | EC2, S3, VPC |
| variable | Input parameters | Instance type |
| output | Return values | IP address |
| data | Query existing resources | AMI lookup |
| module | Reusable components | VPC module |
| locals | Local computed values | Common tags |
Providers and resources are the core building blocks of any Terraform configuration.
Providers: The API Translators
Providers are plugins that tell Terraform how to communicate with external APIs:
1# Terraform block specifies required providers
2terraform {
3 required_version = ">= 1.0.0"
4
5 required_providers {
6 aws = {
7 source = "hashicorp/aws"
8 version = "~> 5.0"
9 }
10 random = {
11 source = "hashicorp/random"
12 version = "~> 3.0"
13 }
14 }
15}
16
17# Provider block configures the provider
18provider "aws" {
19 region = "us-east-1"
20
21 # Optional: assume role for cross-account access
22 # assume_role {
23 # role_arn = "arn:aws:iam::123456789012:role/TerraformRole"
24 # }
25
26 default_tags {
27 tags = {
28 ManagedBy = "Terraform"
29 }
30 }
31}
32
33# Multiple provider configurations (aliases)
34provider "aws" {
35 alias = "west"
36 region = "us-west-2"
37}Resources: Your Infrastructure
Resources are the most important element - they represent actual infrastructure:
1resource "aws_vpc" "main" {
2 cidr_block = "10.0.0.0/16"
3 enable_dns_hostnames = true
4 enable_dns_support = true
5
6 tags = {
7 Name = "main-vpc"
8 }
9}
10
11resource "aws_subnet" "public" {
12 vpc_id = aws_vpc.main.id
13 cidr_block = "10.0.1.0/24"
14 availability_zone = "us-east-1a"
15 map_public_ip_on_launch = true
16
17 tags = {
18 Name = "public-subnet"
19 }
20}
21
22resource "aws_instance" "web" {
23 ami = "ami-0c55b159cbfafe1f0"
24 instance_type = "t2.micro"
25 subnet_id = aws_subnet.public.id
26
27 tags = {
28 Name = "web-server"
29 }
30
31 # Use the west region provider
32 # provider = aws.west
33}Resource References:
Terraform automatically understands dependencies through references:
1# This creates implicit dependency
2resource "aws_instance" "web" {
3 subnet_id = aws_subnet.public.id # Depends on subnet
4}
5
6# Reference syntax: <RESOURCE_TYPE>.<NAME>.<ATTRIBUTE>
7# aws_vpc.main.id
8# aws_subnet.public.cidr_block
9# aws_instance.web.public_ipResource Behavior:
| Action | When |
|--------|------|
| Create | Resource doesn't exist |
| Update | Attribute changed (in-place) |
| Replace | Force-new attribute changed |
| Destroy | Resource removed from config |
Variables make your Terraform code reusable and flexible. Outputs expose useful information after deployment.
Input Variables:
1# variables.tf
2
3# Basic variable
4variable "environment" {
5 description = "Deployment environment"
6 type = string
7 default = "development"
8}
9
10# Variable with validation
11variable "instance_type" {
12 description = "EC2 instance type"
13 type = string
14 default = "t2.micro"
15
16 validation {
17 condition = contains(["t2.micro", "t2.small", "t2.medium"], var.instance_type)
18 error_message = "Instance type must be t2.micro, t2.small, or t2.medium."
19 }
20}
21
22# List variable
23variable "availability_zones" {
24 description = "List of AZs to use"
25 type = list(string)
26 default = ["us-east-1a", "us-east-1b"]
27}
28
29# Map variable
30variable "instance_tags" {
31 description = "Tags for instances"
32 type = map(string)
33 default = {
34 Project = "MyApp"
35 Team = "Platform"
36 }
37}
38
39# Sensitive variable (won't be shown in output)
40variable "database_password" {
41 description = "Password for the database"
42 type = string
43 sensitive = true
44}Using Variables:
1# main.tf
2resource "aws_instance" "web" {
3 ami = var.ami_id
4 instance_type = var.instance_type
5
6 tags = merge(var.instance_tags, {
7 Name = "web-${var.environment}"
8 Environment = var.environment
9 })
10}Setting Variable Values:
1# 1. terraform.tfvars file (auto-loaded)
2environment = "production"
3instance_type = "t2.small"
4
5# 2. Command line
6terraform apply -var="environment=production"
7
8# 3. Environment variables
9export TF_VAR_environment="production"
10terraform apply
11
12# 4. .tfvars file (explicitly specified)
13terraform apply -var-file="production.tfvars"Outputs:
Outputs expose values after apply:
1# outputs.tf
2
3output "instance_id" {
4 description = "ID of the EC2 instance"
5 value = aws_instance.web.id
6}
7
8output "instance_public_ip" {
9 description = "Public IP of the EC2 instance"
10 value = aws_instance.web.public_ip
11}
12
13output "instance_private_ip" {
14 description = "Private IP of the EC2 instance"
15 value = aws_instance.web.private_ip
16 sensitive = true # Hide from normal output
17}
18
19# Complex output
20output "instance_info" {
21 description = "All instance information"
22 value = {
23 id = aws_instance.web.id
24 public_ip = aws_instance.web.public_ip
25 private_ip = aws_instance.web.private_ip
26 }
27}Using Outputs:
1# View all outputs after apply
2terraform output
3
4# View specific output
5terraform output instance_public_ip
6
7# Get output as JSON (for scripts)
8terraform output -jsonData sources allow Terraform to fetch information from external sources or existing infrastructure.
Why Data Sources?
Instead of hardcoding values like AMI IDs, you can query them dynamically:
1# BAD: Hardcoded AMI (breaks when outdated)
2resource "aws_instance" "web" {
3 ami = "ami-0c55b159cbfafe1f0" # This will become outdated!
4}
5
6# GOOD: Dynamic AMI lookup
7data "aws_ami" "ubuntu" {
8 most_recent = true
9 owners = ["099720109477"] # Canonical
10
11 filter {
12 name = "name"
13 values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
14 }
15}
16
17resource "aws_instance" "web" {
18 ami = data.aws_ami.ubuntu.id # Always latest Ubuntu 22.04
19}Common Data Sources:
1# Get current AWS account info
2data "aws_caller_identity" "current" {}
3
4output "account_id" {
5 value = data.aws_caller_identity.current.account_id
6}
7
8# Get current region
9data "aws_region" "current" {}
10
11# Look up existing VPC by tag
12data "aws_vpc" "existing" {
13 filter {
14 name = "tag:Name"
15 values = ["main-vpc"]
16 }
17}
18
19resource "aws_subnet" "new" {
20 vpc_id = data.aws_vpc.existing.id
21 cidr_block = "10.0.100.0/24"
22}
23
24# Look up availability zones
25data "aws_availability_zones" "available" {
26 state = "available"
27}
28
29resource "aws_subnet" "multi_az" {
30 count = length(data.aws_availability_zones.available.names)
31 vpc_id = aws_vpc.main.id
32 cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index)
33 availability_zone = data.aws_availability_zones.available.names[count.index]
34}
35
36# Look up IAM policy
37data "aws_iam_policy" "admin" {
38 name = "AdministratorAccess"
39}
40
41# Look up secrets from AWS Secrets Manager
42data "aws_secretsmanager_secret_version" "db_password" {
43 secret_id = "my-database-password"
44}
45
46resource "aws_db_instance" "main" {
47 password = data.aws_secretsmanager_secret_version.db_password.secret_string
48}Data Source vs Resource:
| Aspect | Data Source | Resource |
|--------|-------------|----------|
| Action | Read only | Create/Update/Delete |
| Syntax | data "type" "name" | resource "type" "name" |
| State | Refreshed every plan | Managed by Terraform |
| Purpose | Query existing | Create new |
Let's put everything together in a complete, working Terraform configuration that deploys a web server.
Project Structure:
1my-web-server/
2├── main.tf # Main configuration
3├── variables.tf # Variable definitions
4├── outputs.tf # Output definitions
5├── terraform.tfvars # Variable values
6└── .gitignore # Ignore state filesmain.tf:
1terraform {
2 required_version = ">= 1.0.0"
3
4 required_providers {
5 aws = {
6 source = "hashicorp/aws"
7 version = "~> 5.0"
8 }
9 }
10}
11
12provider "aws" {
13 region = var.region
14}
15
16# Get latest Amazon Linux 2 AMI
17data "aws_ami" "amazon_linux" {
18 most_recent = true
19 owners = ["amazon"]
20
21 filter {
22 name = "name"
23 values = ["amzn2-ami-hvm-*-x86_64-gp2"]
24 }
25}
26
27# Security group for web server
28resource "aws_security_group" "web" {
29 name = "${var.project_name}-web-sg"
30 description = "Security group for web server"
31
32 ingress {
33 description = "HTTP"
34 from_port = 80
35 to_port = 80
36 protocol = "tcp"
37 cidr_blocks = ["0.0.0.0/0"]
38 }
39
40 ingress {
41 description = "SSH"
42 from_port = 22
43 to_port = 22
44 protocol = "tcp"
45 cidr_blocks = [var.ssh_cidr]
46 }
47
48 egress {
49 from_port = 0
50 to_port = 0
51 protocol = "-1"
52 cidr_blocks = ["0.0.0.0/0"]
53 }
54
55 tags = var.common_tags
56}
57
58# EC2 instance
59resource "aws_instance" "web" {
60 ami = data.aws_ami.amazon_linux.id
61 instance_type = var.instance_type
62 vpc_security_group_ids = [aws_security_group.web.id]
63
64 user_data = <<-EOF
65 #!/bin/bash
66 yum update -y
67 yum install -y httpd
68 systemctl start httpd
69 systemctl enable httpd
70 echo "<h1>Hello from Terraform!</h1>" > /var/www/html/index.html
71 EOF
72
73 tags = merge(var.common_tags, {
74 Name = "${var.project_name}-web-server"
75 })
76}variables.tf:
1variable "region" {
2 description = "AWS region"
3 type = string
4 default = "us-east-1"
5}
6
7variable "project_name" {
8 description = "Name of the project"
9 type = string
10}
11
12variable "instance_type" {
13 description = "EC2 instance type"
14 type = string
15 default = "t2.micro"
16}
17
18variable "ssh_cidr" {
19 description = "CIDR block for SSH access"
20 type = string
21 default = "0.0.0.0/0"
22}
23
24variable "common_tags" {
25 description = "Common tags for all resources"
26 type = map(string)
27 default = {}
28}outputs.tf:
1output "instance_id" {
2 description = "ID of the EC2 instance"
3 value = aws_instance.web.id
4}
5
6output "public_ip" {
7 description = "Public IP address"
8 value = aws_instance.web.public_ip
9}
10
11output "website_url" {
12 description = "URL of the website"
13 value = "http://${aws_instance.web.public_ip}"
14}terraform.tfvars:
1project_name = "my-first-terraform"
2
3common_tags = {
4 Environment = "learning"
5 ManagedBy = "Terraform"
6 Owner = "your-name"
7}Deploy:
1terraform init
2terraform plan
3terraform apply
4
5# Visit the website_url output in your browser!
6# When done:
7terraform destroyA fully working Terraform configuration that deploys an EC2 web server with proper security, variables, and outputs.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Terraform Basics - Interactive Demo</title>
7 <style>
8 * { margin: 0; padding: 0; box-sizing: border-box; }
9 body {
10 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11 background: linear-gradient(135deg, #623aa2 0%, #9d4edd 100%);
12 min-height: 100vh;
13 padding: 20px;
14 }
15 .container {
16 max-width: 1200px;
17 margin: 0 auto;
18 background: white;
19 border-radius: 15px;
20 box-shadow: 0 20px 60px rgba(0,0,0,0.3);
21 overflow: hidden;
22 }
23 header {
24 background: linear-gradient(135deg, #623aa2 0%, #9d4edd 100%);
25 color: white;
26 padding: 30px;
27 text-align: center;
28 }
29 h1 { font-size: 2rem; margin-bottom: 10px; }
30 .content { padding: 30px; }
31 .section { margin-bottom: 40px; }
32 .section-title {
33 color: #623aa2;
34 font-size: 1.5rem;
35 margin-bottom: 20px;
36 padding-bottom: 10px;
37 border-bottom: 3px solid #623aa2;
38 }
39
40 /* Workflow */
41 .workflow {
42 display: flex;
43 justify-content: space-between;
44 align-items: center;
45 background: #f8f9fa;
46 padding: 30px;
47 border-radius: 10px;
48 }
49 .workflow-step {
50 text-align: center;
51 flex: 1;
52 }
53 .step-icon {
54 width: 70px;
55 height: 70px;
56 margin: 0 auto 15px;
57 background: linear-gradient(135deg, #623aa2 0%, #9d4edd 100%);
58 border-radius: 50%;
59 display: flex;
60 align-items: center;
61 justify-content: center;
62 font-size: 1.8rem;
63 color: white;
64 }
65 .step-title { font-weight: bold; color: #623aa2; }
66 .step-cmd {
67 font-family: monospace;
68 font-size: 0.8rem;
69 background: #1e293b;
70 color: #22d3ee;
71 padding: 5px 10px;
72 border-radius: 4px;
73 margin-top: 10px;
74 }
75 .workflow-arrow {
76 font-size: 2rem;
77 color: #623aa2;
78 }
79
80 /* Code Block */
81 .code-block {
82 background: #1e293b;
83 border-radius: 10px;
84 overflow: hidden;
85 }
86 .code-header {
87 background: #0f172a;
88 padding: 10px 20px;
89 display: flex;
90 align-items: center;
91 gap: 10px;
92 }
93 .code-dots { display: flex; gap: 5px; }
94 .dot { width: 12px; height: 12px; border-radius: 50%; }
95 .dot.red { background: #ef4444; }
96 .dot.yellow { background: #f59e0b; }
97 .dot.green { background: #10b981; }
98 .code-title { color: #94a3b8; font-size: 0.85rem; }
99 .code-body {
100 padding: 20px;
101 color: #e2e8f0;
102 font-family: 'Courier New', monospace;
103 font-size: 0.85rem;
104 overflow-x: auto;
105 white-space: pre;
106 }
107 .keyword { color: #c084fc; }
108 .string { color: #34d399; }
109 .comment { color: #64748b; }
110 .property { color: #60a5fa; }
111
112 /* Syntax Reference */
113 .syntax-grid {
114 display: grid;
115 grid-template-columns: repeat(3, 1fr);
116 gap: 15px;
117 }
118 .syntax-card {
119 background: #f8f9fa;
120 border-radius: 8px;
121 padding: 15px;
122 }
123 .syntax-card h4 {
124 color: #623aa2;
125 margin-bottom: 10px;
126 }
127 .syntax-card code {
128 display: block;
129 background: #1e293b;
130 color: #22d3ee;
131 padding: 10px;
132 border-radius: 4px;
133 font-size: 0.8rem;
134 white-space: pre;
135 }
136
137 @media (max-width: 768px) {
138 .workflow { flex-direction: column; gap: 20px; }
139 .workflow-arrow { transform: rotate(90deg); }
140 .syntax-grid { grid-template-columns: 1fr; }
141 }
142 </style>
143</head>
144<body>
145 <div class="container">
146 <header>
147 <h1>⚡ Terraform Basics</h1>
148 <p>The Industry-Standard Infrastructure as Code Tool</p>
149 </header>
150
151 <div class="content">
152 <!-- Workflow -->
153 <div class="section">
154 <h2 class="section-title">Terraform Workflow</h2>
155 <div class="workflow">
156 <div class="workflow-step">
157 <div class="step-icon">📝</div>
158 <div class="step-title">Write</div>
159 <div>Create .tf files</div>
160 </div>
161 <div class="workflow-arrow">→</div>
162 <div class="workflow-step">
163 <div class="step-icon">📦</div>
164 <div class="step-title">Init</div>
165 <div class="step-cmd">terraform init</div>
166 </div>
167 <div class="workflow-arrow">→</div>
168 <div class="workflow-step">
169 <div class="step-icon">🔍</div>
170 <div class="step-title">Plan</div>
171 <div class="step-cmd">terraform plan</div>
172 </div>
173 <div class="workflow-arrow">→</div>
174 <div class="workflow-step">
175 <div class="step-icon">🚀</div>
176 <div class="step-title">Apply</div>
177 <div class="step-cmd">terraform apply</div>
178 </div>
179 </div>
180 </div>
181
182 <!-- HCL Example -->
183 <div class="section">
184 <h2 class="section-title">HCL Configuration Example</h2>
185 <div class="code-block">
186 <div class="code-header">
187 <div class="code-dots">
188 <span class="dot red"></span>
189 <span class="dot yellow"></span>
190 <span class="dot green"></span>
191 </div>
192 <span class="code-title">main.tf</span>
193 </div>
194 <div class="code-body"><span class="comment"># Configure the AWS Provider</span>
195<span class="keyword">provider</span> <span class="string">"aws"</span> {
196 <span class="property">region</span> = <span class="string">"us-east-1"</span>
197}
198
199<span class="comment"># Create a web server</span>
200<span class="keyword">resource</span> <span class="string">"aws_instance"</span> <span class="string">"web"</span> {
201 <span class="property">ami</span> = <span class="string">"ami-0c55b159cbfafe1f0"</span>
202 <span class="property">instance_type</span> = <span class="keyword">var</span>.<span class="property">instance_type</span>
203
204 <span class="property">tags</span> = {
205 <span class="property">Name</span> = <span class="string">"WebServer"</span>
206 <span class="property">ManagedBy</span> = <span class="string">"Terraform"</span>
207 }
208}
209
210<span class="comment"># Output the public IP</span>
211<span class="keyword">output</span> <span class="string">"public_ip"</span> {
212 <span class="property">value</span> = <span class="keyword">aws_instance</span>.<span class="property">web</span>.<span class="property">public_ip</span>
213}</div>
214 </div>
215 </div>
216
217 <!-- HCL Syntax Reference -->
218 <div class="section">
219 <h2 class="section-title">HCL Syntax Reference</h2>
220 <div class="syntax-grid">
221 <div class="syntax-card">
222 <h4>Strings</h4>
223 <code>name = "my-server"</code>
224 </div>
225 <div class="syntax-card">
226 <h4>Numbers</h4>
227 <code>port = 8080
228count = 3</code>
229 </div>
230 <div class="syntax-card">
231 <h4>Booleans</h4>
232 <code>enabled = true
233delete = false</code>
234 </div>
235 <div class="syntax-card">
236 <h4>Lists</h4>
237 <code>zones = ["us-east-1a",
238 "us-east-1b"]</code>
239 </div>
240 <div class="syntax-card">
241 <h4>Maps</h4>
242 <code>tags = {
243 Name = "web"
244 Env = "prod"
245}</code>
246 </div>
247 <div class="syntax-card">
248 <h4>References</h4>
249 <code>subnet_id = aws_subnet.main.id
250ami = var.ami_id</code>
251 </div>
252 </div>
253 </div>
254 </div>
255 </div>
256</body>
257</html>This demonstration provides a comprehensive overview of Terraform basics:
Terraform Workflow:
HCL Syntax:
Key Concepts:
Great job mastering Terraform basics! You now understand the core workflow and can write basic configurations.
In our next lesson, "Terraform Providers & Resources," you'll dive deeper into the vast provider ecosystem. You'll learn to work with multiple providers, understand resource lifecycles, and deploy more complex infrastructure components.
Get ready to expand your Terraform toolkit!
Your progress is saved in this browser, on this device only.