Table of Contents
- Core Terraform Workflow
- 📦 Code Examples
- The Terraform Lifecycle
- Step 1: Write Configuration
- Step 2: Initialize the Project
- Step 3: Format Your Code
- Step 4: Validate Configuration
- Step 5: Preview Changes (Plan)
- Step 6: Apply Changes
- Step 7: Inspect Your Infrastructure
- Step 8: Destroy Infrastructure
- Debugging Terraform
- Importing Existing Infrastructure
- State Operations
- Advanced CLI Operations
- The Complete Workflow Script
- Checkpoint Questions
- What's Next: Part 7 - Modules for Organization
- Series Navigation
- References
Core Terraform Workflow
You've learned HCL syntax, variables, and state management. Now let's talk about the actual workflow - the daily cycle you'll use every time you touch infrastructure.
This is Part 6 of 12 in our Terraform tutorial series. By the end, you'll know the commands you need, when to use them, and how to debug when things go sideways.
📦 Code Examples
Repository: terraform-hcl-tutorial-series This Part: Part 6 - Workflow Examples
Get the working example:
git clone https://github.com/khuongdo/terraform-hcl-tutorial-series.git
cd terraform-hcl-tutorial-series
git checkout part-06
cd examples/part-06-workflow/
# Practice the workflow
terraform init
terraform fmt
terraform validate
terraform plan
The Terraform Lifecycle
Every infrastructure change follows this pattern:
Write → Init → Format → Validate → Plan → Apply → Destroy
↑______________________________________________|
(Repeat for changes)
Each step has a job. Skip one and you'll find out why it exists the hard way.
| Step | What It Does | What Breaks If You Skip It |
|---|---|---|
| Write | Define what you want | Nothing to deploy |
| Init | Download providers, set up backend | Commands fail with "not initialized" |
| Format | Clean up spacing and indentation | Messy diffs, annoying code reviews |
| Validate | Check syntax before running | Errors during plan/apply |
| Plan | Preview changes | Surprises, accidental deletions |
| Apply | Actually create stuff | No infrastructure |
| Destroy | Tear it all down | Orphaned resources, surprise bills |
Let's walk through each one.
Step 1: Write Configuration
Start by writing what you want. Here's a basic EC2 instance setup:
# main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
tags = {
Name = "${var.project_name}-web-server"
Environment = var.environment
}
}
# variables.tf
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-east-1"
}
variable "ami_id" {
description = "AMI ID for EC2 instance"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "project_name" {
description = "Project name for resource tagging"
type = string
}
variable "environment" {
description = "Environment (dev/staging/prod)"
type = string
}
# outputs.tf
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "public_ip" {
description = "Public IP address of the instance"
value = aws_instance.web.public_ip
}
Step 2: Initialize the Project
Command: terraform init
This is always the first command you run. It:
- Downloads provider plugins (AWS, GCP, whatever you're using)
- Sets up the backend (where state gets stored)
- Creates the
.terraformdirectory
$ 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 been successfully initialized!
When to run init again:
- Added a new provider
- Changed backend configuration
- Fresh Git clone
- Deleted
.terraform/directory
terraform init is safe to run multiple times. If you're not sure whether you need it, just run it.
Step 3: Format Your Code
Command: terraform fmt
This auto-formats your .tf files to Terraform's standard style:
$ terraform fmt
main.tf
variables.tf
What it fixes:
- Indentation (2 spaces)
- Spacing around
= - Alignment
- Trailing whitespace
Before formatting:
resource "aws_instance" "web"{
ami="ami-12345"
instance_type = "t3.micro"
tags={
Name="web-server"
}
}
After terraform fmt:
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}
You can add this to a Git pre-commit hook so nobody pushes messy code:
#!/bin/bash
# .git/hooks/pre-commit
terraform fmt -check -recursive
if [ $? -ne 0 ]; then
echo "Error: Terraform files not formatted. Run 'terraform fmt'"
exit 1
fi
Step 4: Validate Configuration
Command: terraform validate
This checks for syntax errors and missing required fields without touching cloud APIs:
$ terraform validate
Success! The configuration is valid.
What it catches:
- Bad HCL syntax
- Missing required arguments
- Invalid resource references
- Type mismatches
Example validation failure:
resource "aws_instance" "web" {
ami = var.ami_id
# Oops, forgot instance_type
}
$ terraform validate
Error: Missing required argument
on main.tf line 10, in resource "aws_instance" "web":
10: resource "aws_instance" "web" {
The argument "instance_type" is required, but no definition was found.
terraform validate runs entirely locally - it never makes API calls. Use it early and often.
Step 5: Preview Changes (Plan)
Command: terraform plan
This is the most important step. It shows exactly what will happen before you change anything:
$ terraform plan -var-file="dev.tfvars" -out=tfplan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t3.micro"
+ id = (known after apply)
+ public_ip = (known after apply)
+ tags = {
+ "Environment" = "dev"
+ "Name" = "myapp-web-server"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ instance_id = (known after apply)
+ public_ip = (known after apply)
────────────────────────────────────────────────────────────────────
Saved the plan to: tfplan
Understanding plan symbols:
| Symbol | Meaning | Risk Level |
|---|---|---|
+ | Resource will be created | Low |
~ | Resource will be modified in-place | Medium |
-/+ | Resource will be destroyed and recreated | High |
- | Resource will be destroyed | Critical |
Save plans for safer applies:
# Generate plan file
terraform plan -out=tfplan
# Review the plan
terraform show tfplan
# Apply the exact plan
terraform apply tfplan
Why save plans?
Without -out, running terraform apply generates a new plan at apply time. If something changed between plan and apply (drift, another person's change), you might apply something different than what you reviewed.
Always use terraform plan -out=tfplan followed by terraform apply tfplan in production. Never run terraform apply without reviewing a saved plan first.
Step 6: Apply Changes
Command: terraform apply
This executes the plan and creates/modifies/destroys infrastructure:
# Interactive apply (asks for confirmation)
$ terraform apply -var-file="dev.tfvars"
# Auto-approve (use in CI/CD only)
$ terraform apply -var-file="dev.tfvars" -auto-approve
# Apply a saved plan (recommended)
$ terraform apply tfplan
Output:
aws_instance.web: Creating...
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Still creating... [20s elapsed]
aws_instance.web: Creation complete after 25s [id=i-0abcd1234efgh5678]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-0abcd1234efgh5678"
public_ip = "54.123.45.67"
What happens during apply:
- Terraform locks the state file (prevents other runs)
- Makes API calls to your cloud provider
- Updates
terraform.tfstatewith new resource IDs - Unlocks state file
- Shows outputs
When apply fails mid-execution:
If it crashes (network error, API limit, timeout), the state file reflects what actually got created. Fix the issue and re-run terraform apply - Terraform picks up where it left off.
Step 7: Inspect Your Infrastructure
After applying, check what you created:
# List all resources in state
$ terraform state list
aws_instance.web
# Show detailed resource attributes
$ terraform state show aws_instance.web
# aws_instance.web:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
id = "i-0abcd1234efgh5678"
public_ip = "54.123.45.67"
tags = {
"Environment" = "dev"
"Name" = "myapp-web-server"
}
}
# View outputs again
$ terraform output
instance_id = "i-0abcd1234efgh5678"
public_ip = "54.123.45.67"
# Get specific output value (for scripts)
$ terraform output -raw public_ip
54.123.45.67
Step 8: Destroy Infrastructure
Command: terraform destroy
When you're done (dev environment, testing, etc.), tear it down:
$ terraform destroy -var-file="dev.tfvars"
Terraform will perform the following actions:
# aws_instance.web will be destroyed
- resource "aws_instance" "web" {
- ami = "ami-0c55b159cbfafe1f0" -> null
- instance_type = "t3.micro" -> null
- id = "i-0abcd1234efgh5678" -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
aws_instance.web: Destroying... [id=i-0abcd1234efgh5678]
aws_instance.web: Destruction complete after 35s
Destroy complete! Resources: 1 destroyed.
Targeted destruction (specific resource):
# Destroy only the web instance
terraform destroy -target=aws_instance.web
terraform destroy is irreversible. Always run terraform plan -destroy first to preview what gets deleted. In production, protect critical resources with lifecycle { prevent_destroy = true }.
Debugging Terraform
When things break, Terraform provides detailed logging:
Enable Debug Logging
# Set log level (TRACE, DEBUG, INFO, WARN, ERROR)
export TF_LOG=DEBUG
# Save logs to file
export TF_LOG_PATH=terraform-debug.log
# Run Terraform command
terraform apply
# Disable logging
unset TF_LOG TF_LOG_PATH
Log levels:
| Level | Use Case |
|---|---|
TRACE | Maximum verbosity - shows every API call |
DEBUG | Detailed debugging info |
INFO | Standard operational messages |
WARN | Warning messages only |
ERROR | Errors only |
Common Debugging Scenarios
Problem: "Error: Provider configuration not present"
# Solution: Run init
terraform init
Problem: "Error: Inconsistent dependency lock file"
# Solution: Regenerate lock file
terraform init -upgrade
Problem: "Error: Backend initialization required"
# Solution: Migrate backend
terraform init -migrate-state
Problem: "Error: Resource already exists"
# Solution: Import existing resource
terraform import aws_instance.web i-1234567890abcdef0
Importing Existing Infrastructure
Got manually created resources? Import them into Terraform:
Step 1: Write the resource configuration
# main.tf
resource "aws_instance" "legacy" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "legacy-server"
}
}
Step 2: Import the resource
# Syntax: terraform import <resource_type>.<name> <cloud_resource_id>
$ terraform import aws_instance.legacy i-1234567890abcdef0
aws_instance.legacy: Importing from ID "i-1234567890abcdef0"...
aws_instance.legacy: Import prepared!
Prepared aws_instance for import
aws_instance.legacy: Refreshing state... [id=i-1234567890abcdef0]
Import successful!
Step 3: Verify the import
$ terraform plan
No changes. Your infrastructure matches the configuration.
If the plan shows changes, update your configuration to match the actual resource.
Use terraform show after import to see the full resource configuration, then copy it into your .tf files.
State Operations
Terraform state commands let you inspect and manipulate state without touching cloud resources:
List Resources
$ terraform state list
aws_instance.web
aws_security_group.web_sg
aws_vpc.main
Show Resource Details
$ terraform state show aws_instance.web
# aws_instance.web:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
id = "i-0abcd1234efgh5678"
instance_type = "t3.micro"
}
Move Resources (Refactoring)
When you rename resources in code, update state to match:
# Renamed in code: aws_instance.web → aws_instance.app_server
$ terraform state mv aws_instance.web aws_instance.app_server
Move "aws_instance.web" to "aws_instance.app_server"
Successfully moved 1 object(s).
Remove Resources from State
Remove a resource from Terraform management without destroying it:
$ terraform state rm aws_instance.web
Removed aws_instance.web
Successfully removed 1 resource instance(s).
The instance still exists in AWS, but Terraform no longer manages it.
Advanced CLI Operations
Refresh State
Sync Terraform state with actual cloud resources:
$ terraform refresh
When to use:
- Someone made manual changes in cloud console
- Checking for drift between state and reality
Note: Terraform automatically refreshes during plan and apply, so explicit refresh is rarely needed.
Replace Resources
Force replacement of a resource (destroy then recreate):
# Replace specific resource
$ terraform apply -replace=aws_instance.web
# Useful when:
# - Resource is corrupted
# - Need fresh instance ID
# - Testing disaster recovery
Targeted Apply
Apply changes to specific resources only:
# Only create/update the web instance
$ terraform apply -target=aws_instance.web
# Multiple targets
$ terraform apply -target=aws_instance.web -target=aws_security_group.web_sg
Using -target can create inconsistent state. Avoid in production unless recovering from partial failures.
The Complete Workflow Script
Here's a production-ready workflow script:
#!/bin/bash
# deploy.sh - Safe Terraform deployment script
set -e # Exit on error
ENVIRONMENT=${1:-dev}
VAR_FILE="${ENVIRONMENT}.tfvars"
echo "========================================="
echo "Deploying to: $ENVIRONMENT"
echo "========================================="
# Step 1: Initialize
echo "→ Initializing Terraform..."
terraform init
# Step 2: Format check
echo "→ Checking code formatting..."
terraform fmt -check -recursive
if [ $? -ne 0 ]; then
echo "❌ Code not formatted. Run: terraform fmt -recursive"
exit 1
fi
# Step 3: Validate
echo "→ Validating configuration..."
terraform validate
# Step 4: Generate plan
echo "→ Generating plan..."
terraform plan -var-file="$VAR_FILE" -out=tfplan
# Step 5: Show plan
echo "→ Plan generated. Review changes above."
read -p "Apply this plan? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "❌ Deployment cancelled."
rm tfplan
exit 0
fi
# Step 6: Apply plan
echo "→ Applying changes..."
terraform apply tfplan
# Cleanup
rm tfplan
echo "✅ Deployment complete!"
terraform output
Usage:
chmod +x deploy.sh
./deploy.sh dev # Deploy to dev
./deploy.sh staging # Deploy to staging
./deploy.sh prod # Deploy to production
Checkpoint Questions
Test your understanding:
What does
terraform initdo? Why must you run it beforeplanorapply?Difference between
terraform planandterraform apply? Can you apply without planning?What happens if you run
terraform applytwice with no code changes?How do you destroy only one specific resource without tearing down everything?
Why save plans with
-out=tfplan? What problem does it solve?When would you use
terraform import? Can you import resources that Terraform created?What's the difference between
terraform state rmandterraform destroy?
Click to reveal answers
terraform initdownloads provider plugins and initializes the backend. Required before any Terraform operations.planpreviews changes without making them;applyexecutes changes. You can apply without explicit plan, but it's risky (plan happens inline).Nothing changes. Terraform detects infrastructure matches desired state and performs no operations.
terraform destroy -target=resource_type.nameSaved plans ensure the changes you reviewed are exactly what gets applied (no drift between plan time and apply time).
Use
importto bring manually-created resources under Terraform management. Yes, you can import Terraform-created resources if you lost state.state rmremoves from Terraform tracking but leaves resource intact.destroydeletes the actual cloud resource.
What's Next: Part 7 - Modules for Organization
You've mastered the Terraform workflow, but what happens when your codebase grows to hundreds of resources? Copying the same VPC configuration across 10 projects? Managing dependencies between related resources?
In Part 7, we'll solve these problems with Terraform modules - reusable components that:
- Eliminate code duplication
- Enforce organizational standards
- Enable team collaboration
- Simplify complex infrastructure
You'll learn to:
- Create custom modules from scratch
- Use public modules from the Terraform Registry
- Pass variables and outputs between modules
- Version and publish internal modules
- Structure large Terraform projects
Preview snippet (Part 7):
# Instead of repeating VPC code...
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "${var.project}-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
enable_vpn_gateway = false
}
# Reference module outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.public_subnets[0]
}
See you in Part 7.
Series Navigation
- Part 1: Why Infrastructure as Code?
- Part 2: Setting Up Terraform (Coming soon)
- Part 3: Your First Cloud Resource (Coming soon)
- Part 4: HCL Fundamentals (Coming soon)
- Part 5: Variables, Outputs & State (Coming soon)
- Part 6: Core Terraform Workflow (You are here)
- Part 7: Modules for Organization (Coming soon)
- Part 8: Multi-Cloud Patterns (Coming soon)
- Part 9: State Management & Team Workflows (Coming soon)
- Part 10: Testing & Validation (Coming soon)
- Part 11: Security & Secrets Management (Coming soon)
- Part 12: Production Patterns & DevSecOps (Coming soon)
References
- Terraform CLI Documentation
- Terraform State Command Reference
- Debugging Terraform
- Import Command Documentation
Series navigation:
- Part 1: Why Infrastructure as Code?
- Part 2: Setting Up Terraform
- Part 3: Your First Cloud Resource
- Part 4: HCL Fundamentals
- Part 5: Variables, Outputs & State
- Part 6: Core Terraform Workflow (You are here)
- Part 7: Modules for Organization (Coming soon)
- Part 8: Multi-Cloud Patterns (Coming soon)
- Part 9: State Management & Team Workflows (Coming soon)
- Part 10: Testing & Validation (Coming soon)
- Part 11: Security & Secrets Management (Coming soon)
- Part 12: Production Patterns & DevSecOps (Coming soon)
Questions or feedback? Drop a comment below or connect on LinkedIn.