Chapter 2: Terraform Core Concepts#
π― Learning Objectives#
- Understand Terraform providers, resources, and state
- Master the Terraform workflow (init, plan, apply, destroy)
- Learn about resource dependencies and the graph
- Understand Terraform’s lifecycle and how it manages resources
- Grasp the concept of idempotency in Terraform
2.1 Providers#
Providers are plugins that Terraform uses to interact with APIs of cloud providers and other services.
How Providers Work#
Terraform Core β Provider Plugin β API β Cloud Provider
(HCL) (Go) (REST) (AWS/GCP/Azure)Important Provider Concepts#
| Concept | Explanation |
|---|---|
| Source | Location where provider is downloaded from (e.g., hashicorp/aws) |
| Version | Provider version constraint (e.g., ~> 5.0) |
| Configuration | Provider-level settings (region, credentials, etc.) |
| Resources | What the provider can create/manage |
| Data Sources | What the provider can read/query |
Provider Example#
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "us-east-1"
# Optionally use a profile
# profile = "my-profile"
# Optionally assume a role
# assume_role {
# role_arn = "arn:aws:iam::ACCOUNT_ID:role/MyRole"
# session_name = "TerraformSession"
# }
}
provider "random" {
# No configuration needed for random provider
}Provider Version Constraints (Exam Critical)#
| Constraint | Meaning | Example |
|---|---|---|
= 5.0 |
Exact version | version = "= 5.0.0" |
~> 5.0 |
>= 5.0 and < 6.0 (minor updates allowed) | version = "~> 5.0" |
>= 5.0 |
Any version >= 5.0 | version = ">= 5.0" |
>= 5.0, < 5.10 |
Range | version = ">= 5.0, < 5.10" |
~> 5.4.0 |
>= 5.4.0 and < 5.5.0 (patch updates only) | version = "~> 5.4.0" |
Exam Tip: The
~>(pessimistic constraint) operator is frequently tested. Remember:~> 5.0means>= 5.0, < 6.0and~> 5.4.0means>= 5.4.0, < 5.5.0.
Provider Aliases (Multi-Region Deployments)#
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_instance" "east" {
# Uses default provider (us-east-1)
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
resource "aws_instance" "west" {
# Uses the aliased provider (us-west-2)
provider = aws.west
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}2.2 Resources#
Resources are the most important element in Terraform β they represent infrastructure objects like EC2 instances, S3 buckets, VPCs, etc.
Resource Syntax#
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "WebServer"
}
}| Part | Meaning | Example |
|---|---|---|
resource |
Keyword to declare a resource | resource |
"aws_instance" |
Resource type β what to create | "aws_instance", "aws_s3_bucket" |
"web_server" |
Local name β reference this resource in config | Used for referencing |
{ ... } |
Arguments β configuration for the resource | AMI, instance type, tags |
Referencing Resources#
# Reference attributes of another resource
resource "aws_eip" "my_eip" {
instance = aws_instance.web_server.id
domain = "vpc"
}
# Full syntax: RESOURCE_TYPE.LOCAL_NAME.ATTRIBUTE
output "instance_id" {
value = aws_instance.web_server.id
}Resource Attributes#
Resources have three kinds of attributes:
| Type | Description | Example |
|---|---|---|
| Arguments | You set these in config | ami, instance_type |
| Attributes | Returned by the provider, can be referenced | id, arn, public_ip |
| Read-only | Known only after creation | Known after apply |
2.3 Terraform State (tfstate)#
State is Terraform’s mapping between your configuration and the real-world infrastructure. It’s stored in a file called terraform.tfstate.
What State Contains#
{
"version": 4,
"terraform_version": "1.9.0",
"resources": [
{
"module": "",
"mode": "managed",
"type": "aws_instance",
"name": "web_server",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"attributes": {
"id": "i-0abcd1234efgh5678",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t2.micro",
"public_ip": "54.123.45.67",
"private_ip": "10.0.1.5",
"tags": {
"Name": "WebServer"
}
},
"dependencies": ["aws_security_group.web_sg"]
}
]
}
]
}Why State Matters (Exam Critical)#
- Mapping β Maps config resources to real-world resources (using resource
id) - Performance β Caches attribute values (no need to re-query API for every resource)
- Dependencies β Tracks resource dependencies for correct ordering
- Drift Detection β
terraform plancompares state vs config vs real-world to detect drift
State Commands#
# List all resources in state
terraform state list
# Show attributes of a specific resource
terraform state show aws_instance.web_server
# Move a resource in state (used for refactoring)
terraform state mv aws_instance.web aws_instance.web_server
# Remove a resource from state (NOT from real world)
terraform state rm aws_instance.web_server
# Pull state to local file
terraform state pull > backup.tfstate
# Push state (use with caution!)
terraform state push backup.tfstate2.4 The Terraform Workflow (Deep Dive)#
terraform init#
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β terraform init β
β β
β 1. Backend Initialization β
β βββ Configures where state is stored (local/S3/etc.) β
β β
β 2. Provider Installation β
β βββ Downloads provider plugins to .terraform/providers/ β
β β
β 3. Module Installation β
β βββ Downloads modules from registry β
β β
β 4. .terraform.lock.hcl creation β
β βββ Locks provider versions for reproducibility β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββterraform plan#
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β terraform plan β
β β
β Reads: Config (.tf files) + State (.tfstate) + Real infra (API) β
β β
β 1. Refresh state (optional: -refresh=false to skip) β
β βββ Queries providers to get current real-world state β
β β
β 2. Compare desired (config) vs actual (state + real world) β
β βββ Determines differences β
β β
β 3. Build execution plan β
β βββ Shows what will be created, updated, or destroyed β
β β
β Output: Plan file (can be saved with -out=plan.tfplan) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββterraform apply#
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β terraform apply β
β β
β If no plan file: Creates a plan & asks for confirmation β
β If plan file provided: Applies plan without confirmation β
β β
β 1. Execute changes in dependency order β
β βββ Creates resources first, then depends β
β β
β 2. Update state file β
β βββ Records new resource IDs and attributes β
β β
β 3. Show outputs β
β βββ Displays any defined output values β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββTerraform Workflow + AWS Infrastructure (Visual Overview)#
The diagram below shows the end-to-end flow β from developer commands to AWS resource provisioning to state management:
graph TB
subgraph "π¨βπ» Developer"
A["terraform init"] --> B["terraform plan -out=plan.tfplan"]
B --> C["review output"]
C --> D["terraform apply plan.tfplan"]
end
subgraph "βοΈ AWS Infrastructure"
D --> E["API Gateway REST API"]
D --> F["Lambda Function (Node.js)"]
D --> G["DynamoDB Table"]
E --> H["GET /items"]
E --> I["POST /items"]
E --> J["DELETE /items/{id}"]
F --> K["CRUD Operations"]
K --> G
end
subgraph "πΎ State Management"
D --> L["S3 Backend"]
L --> M["DynamoDB Locking"]
M --> N["terraform.tfstate"]
end
style A fill:#4a90d9,color:#fff
style B fill:#4a90d9,color:#fff
style D fill:#27ae60,color:#fff
style H fill:#f39c12,color:#fff
style I fill:#f39c12,color:#fff
style J fill:#f39c12,color:#fff
Terraform Lifecycle (Sequence Diagram)#
The diagram below shows the detailed sequence of interactions between the developer, Terraform, state storage, AWS API, and infrastructure over the full lifecycle:
sequenceDiagram
participant Dev as π¨βπ» Developer
participant TF as Terraform
participant State as State (S3/DynamoDB)
participant AWS as AWS API
participant Res as Infrastructure
Dev->>TF: terraform init
TF->>AWS: Initialize providers
AWS-->>TF: Provider ready
TF-->>Dev: Initialized
Dev->>TF: terraform plan
TF->>State: Read current state
State-->>TF: Existing state
TF->>AWS: Query resources
AWS-->>TF: Current infra
TF-->>Dev: Proposed changes
Dev->>TF: terraform apply
TF->>State: Lock state file
State-->>TF: Lock acquired
TF->>AWS: Create/Update resources
AWS->>Res: Provision
Res-->>AWS: Resource IDs
AWS-->>TF: Success
TF->>State: Write new state
TF->>State: Release lock
TF-->>Dev: Apply complete!
Dev->>TF: terraform destroy
TF->>State: Lock state
TF->>AWS: Delete resources
AWS-->>TF: Resources deleted
TF->>State: Clear state
TF-->>Dev: Destroy complete!
terraform destroy#
# Destroy everything managed by this configuration
terraform destroy
# Destroy specific resource
terraform destroy -target aws_instance.web_server
# Auto-approve (skip confirmation)
terraform destroy -auto-approve2.5 Resource Dependencies#
Terraform automatically builds a dependency graph from resource references.
Implicit Dependencies#
resource "aws_security_group" "web_sg" {
name = "web-sg"
description = "Allow HTTP and SSH"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
# Implicit dependency: aws_instance.web depends on aws_security_group.web_sg
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.web_sg.id]
# β Terraform sees this reference and adds an implicit dependency
}Explicit Dependencies (depends_on)#
Use depends_on when there’s a dependency that Terraform can’t infer automatically.
# Example: S3 bucket policy depends on a user
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
}
resource "aws_s3_bucket_policy" "data_policy" {
bucket = aws_s3_bucket.data.id
policy = data.aws_iam_policy_document.bucket_policy.json
}
resource "aws_iam_user" "data_user" {
name = "data-processor"
# Terraform cannot automatically detect this dependency
}
# Explicit dependency β use sparingly!
resource "aws_iam_user_policy" "data_user_policy" {
name = "s3-access"
user = aws_iam_user.data_user.name
policy = data.aws_iam_policy_document.s3_policy.json
depends_on = [
aws_s3_bucket.data,
aws_s3_bucket_policy.data_policy
]
}Important: Only use depends_on when you have to! Let Terraform infer dependencies automatically whenever possible.
2.6 Resource Lifecycle#
The lifecycle meta-argument controls how Terraform creates, updates, and destroys resources.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [ami, tags]
}
}Lifecycle Rules (Exam Critical)#
| Rule | Description | Use Case |
|---|---|---|
create_before_destroy |
Create new resource first, then destroy old | Zero-downtime deployments |
prevent_destroy |
Prevents accidental destruction of resource | Critical DBs, production resources |
ignore_changes |
Ignore specific attribute changes | AMI updates outside Terraform, auto-scaling tag changes |
Lifecycle Scenarios#
Scenario 1: prevent_destroy
resource "aws_db_instance" "production" {
engine = "postgres"
engine_version = "15.3"
instance_class = "db.r6g.large"
allocated_storage = 100
lifecycle {
prevent_destroy = true # terraform destroy will FAIL for this resource
}
}Scenario 2: create_before_destroy
resource "aws_launch_template" "web" {
name = "web-template"
image_id = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
lifecycle {
create_before_destroy = true
# New template created first, then old one destroyed
}
}Scenario 3: ignore_changes
resource "aws_autoscaling_group" "web_asg" {
name = "web-asg"
desired_capacity = 2
min_size = 1
max_size = 10
lifecycle {
ignore_changes = [
desired_capacity, # ASG may auto-scale, don't reset
tags, # Tags may be added by auto-scaling
]
}
}2.7 Refresh-Only Plans#
Refresh-only plans check for drift between your Terraform state and real-world infrastructure without suggesting any changes.
# See if state matches real infrastructure
terraform plan -refresh-only
# Update state to match real infrastructure
terraform apply -refresh-onlyWhen to use:
- After manual changes to infrastructure
- When you’ve imported resources
- Before refactoring your Terraform configuration
2.8 Idempotency#
Idempotency means running the same configuration multiple times produces the same result.
First run: terraform apply β Creates resources
Second run: terraform apply β No changes (idempotent!)
Third run: terraform apply β No changes (still idempotent!)Terraform achieves idempotency through:
- Desired state β You declare what you want
- State tracking β Terraform knows what exists
- Diff comparison β Config vs state vs real-world
- Incremental changes β Only changes what’s different
2.9 Lock File (.terraform.lock.hcl)#
The dependency lock file is created by terraform init and should be committed to version control.
# .terraform.lock.hcl
provider "registry.terraform.io/hashicorp/aws" {
version = "5.40.0"
constraints = "~> 5.0"
hashes = [
"h1:Z1JkR5Z4HJAtxrLZSuxCWQjHuA+eG82rNh0UCoMdfs=",
"h1:nO0d0R7jBCWAxjYJGqZ2R0aCx5+gw8e3g+nI0BMRpY=",
]
}Why it matters:
- Ensures everyone uses the same provider version
- Prevents accidental upgrades
- Must be committed to Git
- Use
terraform init -upgradeto update
2.10 Terraform Directory Structure#
A typical Terraform project:
my-terraform-project/
βββ main.tf # Main configuration (resources)
βββ variables.tf # Input variables
βββ outputs.tf # Output values
βββ terraform.tfvars # Variable values (DO NOT commit secrets!)
βββ terraform.tfvars.example # Example variable values (commit this)
βββ providers.tf # Provider configurations
βββ data.tf # Data sources
βββ locals.tf # Local values
βββ terraform.tfstate # State file (DO NOT commit!)
βββ terraform.tfstate.backup # Backup state (DO NOT commit!)
βββ .terraform/ # Provider binaries and modules (DO NOT commit!)
βββ .terraform.lock.hcl # Provider version lock (DO commit!)π Exam Tips#
- State is the source of truth β Not your config files alone
terraform initdoesn’t modify infrastructure β It sets up the environmentterraform planis read-only β It only shows what would happen- Providers translate HCL to API calls β They’re the bridge between Terraform and cloud providers
depends_oncreates explicit dependencies β Use it sparinglycreate_before_destroyis for zero-downtime β New resource created before old one is destroyedprevent_destroyblocks destruction β Prevents accidental deletionignore_changesignores specific attribute changes β Useful for auto-scaling, external modifications- Lock file ensures reproducibility across team β Check it into version control
- Never edit state files manually β Use
terraform statecommands instead
β Chapter 2 Quiz#
-
What Terraform command downloads provider plugins?
- a)
terraform plan - b)
terraform apply - c)
terraform init - d)
terraform get
- a)
-
Which lifecycle rule prevents a resource from being destroyed?
- a)
create_before_destroy - b)
prevent_destroy - c)
ignore_changes - d)
protect
- a)
-
What does the version constraint
~> 3.0mean?- a) Exactly 3.0
- b) >= 3.0 and < 4.0
- c) >= 3.0 and < 3.1
- d) Any version 3.0 or higher
-
True or False:
terraform plancan modify infrastructure. -
When should you use
depends_on?- a) For every resource
- b) Only when Terraform can’t infer the dependency
- c) Never
- d) For all resources in different providers
π Answers
- c β
terraform initdownloads providers and modules - b β
prevent_destroyprevents the resource from being destroyed - b β
~> 3.0means >= 3.0 and < 4.0 - False β
terraform planis read-only and never modifies infrastructure - b β Only use
depends_onwhen Terraform can’t automatically detect the dependency
Continue to β Chapter 3: HCL Configuration Language