Introduction
Welcome to the final part of our hands-on Terraform series! You've installed Terraform, configured AWS, and written complete infrastructure configuration files. Now comes the most exciting partβactually deploying real infrastructure to AWS and managing its entire lifecycle.
In this comprehensive guide, you'll execute every Terraform command, understand the detailed output, see your infrastructure come to life, and learn how to safely modify and destroy it. By the end, you'll have practical, hands-on experience with the complete Terraform workflow.
π― What You'll Learn in Part 3:
- terraform init: Initialize your project and download providers
 - terraform validate: Verify configuration syntax
 - terraform fmt: Format code consistently
 - terraform plan: Preview infrastructure changes before applying
 - terraform apply: Create real AWS infrastructure
 - terraform output: Query and display output values
 - terraform show: Inspect current infrastructure state
 - terraform state: Understand and manage state files
 - Modifying infrastructure: Make changes and re-apply
 - terraform destroy: Safely remove all infrastructure
 - Understanding state files: Why they're critical and how they work
 - Troubleshooting: Common issues and solutions
 - Best practices: Production-ready workflows
 
Prerequisites:
- Completed Part 1 (Terraform and AWS CLI installed)
 - Completed Part 2 (All .tf configuration files created)
 - Terminal open in your 
terraform-practicedirectory - AWS credentials configured
 - All six configuration files from Part 2
 
Series Progress:
- Part 1: β Installation and AWS setup (completed)
 - Part 2: β Configuration files and resources (completed)
 - Part 3: π Terraform workflow and state management (current)
 
β οΈ Important: This tutorial creates real AWS resources. While we use free-tier-eligible resources (t2.micro), ensure you destroy resources after completion to avoid any potential charges. We'll cover the destroy process at the end.
Understanding the Terraform Workflow
Before executing commands, let's understand the complete Terraform workflow and how each command fits together:
| Step | Command | Purpose | Frequency | 
|---|---|---|---|
| 1 | terraform init | Initialize directory, download providers | Once per project, or when providers change | 
| 2 | terraform validate | Check configuration syntax | After writing/modifying configuration | 
| 3 | terraform fmt | Format code consistently | Before committing changes | 
| 4 | terraform plan | Preview changes before applying | Every time before apply | 
| 5 | terraform apply | Create/modify infrastructure | When ready to execute changes | 
| 6 | terraform output | Display output values | After apply, when needed | 
| 7 | terraform show | Inspect current state | When troubleshooting | 
| 8 | terraform destroy | Remove all infrastructure | When tearing down environment | 
The Core Workflow Loop
Most of your time will be spent in this cycle:
Write/Modify Configuration β Plan β Review Changes β Apply β Verify
                β                                                 β
                βββββββββββββββββββββββββββββββββββββββββββββββββββ
Step 1: terraform init
The first command initializes your Terraform working directory. This must be run before any other Terraform commands.
terraform init
What this command does:
- Reads your configuration files (especially 
main.tf) - Downloads required provider plugins (AWS provider ~5.x)
 - Initializes backend for state storage (local by default)
 - Creates 
.terraformdirectory with downloaded plugins - Creates 
.terraform.lock.hclfile to lock provider versions 
Expected output:
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.80.0...
- Installed hashicorp/aws v5.80.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!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Understanding the Output
Section 1: Backend Initialization
Initializing the backend...
- Backend: Where Terraform stores state (tracking of resources)
 - Default: Local filesystem (
terraform.tfstatefile) - Production: Often remote (S3, Terraform Cloud, etc.)
 
Section 2: Provider Plugin Installation
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.80.0...
- Installed hashicorp/aws v5.80.0 (signed by HashiCorp)
| Component | Explanation | 
|---|---|
| Finding versions | Terraform queries the registry for available versions matching ~> 5.0 | 
| Installing v5.80.0 | Downloads the AWS provider binary (~450 MB) | 
| signed by HashiCorp | Cryptographic signature verified (security measure) | 
Section 3: Lock File Creation
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above.
Purpose of .terraform.lock.hcl:
- Records exact provider versions used
 - Ensures team members use same versions
 - Should be committed to version control (Git)
 - Prevents unexpected provider updates breaking things
 
Section 4: Success Confirmation
Terraform has been successfully initialized!
- You're now ready to use Terraform commands
 - The 
.terraform/directory now contains AWS provider plugin - State backend is ready to track resources
 
What Was Created?
After terraform init, your directory contains:
terraform-practice/
βββ .terraform/                    # Provider plugins (don't commit to Git)
β   βββ providers/
β       βββ registry.terraform.io/
β           βββ hashicorp/
β               βββ aws/
β                   βββ 5.80.0/
β                       βββ linux_amd64/
β                           βββ terraform-provider-aws_v5.80.0_x5
βββ .terraform.lock.hcl           # Version lock file (commit to Git)
βββ main.tf
βββ variables.tf
βββ data.tf
βββ security.tf
βββ ec2.tf
βββ outputs.tf
π‘ Git Best Practice: Add .terraform/ to your .gitignore file. Never commit provider plugins to Gitβthey're large and platform-specific. Always commit .terraform.lock.hcl to ensure consistent provider versions across your team.
Step 2: terraform validate
Validate checks your configuration syntax without accessing any remote services. It's a quick way to catch errors before attempting to deploy.
terraform validate
What this command does:
- Parses all 
.tffiles in the directory - Checks HCL syntax for errors
 - Validates resource configurations
 - Checks for required attributes
 - Verifies variable and output references
 
Expected output (success):
Success! The configuration is valid.
What this confirms:
- All 
.tffiles have valid HCL syntax - Resource blocks are properly structured
 - Variable references are correct
 - No missing required attributes
 - Resource dependencies can be resolved
 
Common Validation Errors
If you have syntax errors, you'll see specific error messages:
Example Error 1: Missing Required Attribute
Error: Missing required argument
  on ec2.tf line 2, in resource "aws_instance" "terraform_instance":
   2: resource "aws_instance" "terraform_instance" {
The argument "ami" is required, but no definition was found.
How to fix: Add the missing ami attribute to your EC2 resource.
Example Error 2: Invalid Reference
Error: Reference to undeclared resource
  on ec2.tf line 6:
   6:   subnet_id = data.aws_subnet.wrong_name.id
A data source named "aws_subnet" "wrong_name" has not been declared in the
root module.
How to fix: Correct the resource reference to match your data source name.
β
 Best Practice: Run terraform validate frequently while writing configuration. It catches errors early before you attempt to apply changes to real infrastructure.
Step 3: terraform fmt
Format automatically rewrites your configuration files to follow Terraform's standard formatting conventions. This ensures consistent code style across your team.
terraform fmt
What this command does:
- Scans all 
.tffiles in current directory - Adjusts indentation to standard (2 spaces)
 - Aligns equal signs in blocks
 - Removes extra blank lines
 - Fixes spacing around operators
 - Writes changes back to files
 
Expected output:
data.tf
ec2.tf
main.tf
outputs.tf
security.tf
variables.tf
Understanding the output:
- Lists all files that were modified
 - If no files listed, all were already properly formatted
 - Changes are automatically saved
 
Before and After Formatting
Before terraform fmt (poor formatting):
resource "aws_instance" "terraform_instance"{
ami= data.aws_ami.amazon_linux.id
  instance_type=var.instance_type
    subnet_id =data.aws_subnet.default.id
}
After terraform fmt (proper formatting):
resource "aws_instance" "terraform_instance" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = data.aws_subnet.default.id
}
Changes made:
- Space added after resource block opening brace
 - Indentation standardized to 2 spaces
 - Equal signs aligned vertically
 - Spaces added around 
=operator - Consistent spacing throughout
 
π‘ Team Workflow Tip: Many teams add terraform fmt -check to their CI/CD pipeline to ensure all code follows consistent formatting before merging.
Useful fmt Options
| Command | Purpose | 
|---|---|
terraform fmt | Format files in current directory | 
terraform fmt -recursive | Format files in subdirectories too | 
terraform fmt -check | Check if formatting needed (doesn't modify) | 
terraform fmt -diff | Show formatting changes before applying | 
Step 4: terraform plan - The Most Important Command
Plan is perhaps the most important Terraform command. It shows you exactly what will happen before you make any changes to real infrastructure. Always run plan before apply!
terraform plan
What this command does:
- Reads all configuration files
 - Queries current state (if exists)
 - Queries AWS for actual resource state
 - Compares desired state (config) with current state
 - Calculates required changes
 - Displays detailed execution plan
 - Does NOT make any changes
 
This output will be long, but understanding it is crucial. Let me show you the key sections and explain each one.
Expected output (excerpt - showing key sections):
data.aws_ami.amazon_linux: Reading...
data.aws_vpc.default: Reading...
data.aws_ami.amazon_linux: Read complete after 1s [id=ami-012967cc5a8c9f891]
data.aws_vpc.default: Read complete after 1s [id=vpc-026ffc4b9]
data.aws_subnet.default: Reading...
data.aws_subnet.default: Read complete after 0s [id=subnet-03f7ad9]
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.terraform_instance will be created
  + resource "aws_instance" "terraform_instance" {
      + ami                          = "ami-012967cc5a8c9f891"
      + instance_type                = "t2.micro"
      + subnet_id                    = "subnet-03f7ad9"
      + vpc_security_group_ids       = (known after apply)
      + tags                         = {
          + "CreatedBy"   = "Terraform"
          + "Environment" = "dev"
          + "Name"        = "terraform-lab-instance"
        }
    }
  # aws_security_group.terraform_sg will be created
  + resource "aws_security_group" "terraform_sg" {
      + id                     = (known after apply)
      + name_prefix            = "terraform-lab-sg-"
      + vpc_id                 = "vpc-026ffc4b9"
      + egress {
          + cidr_blocks      = ["0.0.0.0/0"]
          + from_port        = 0
          + protocol         = "-1"
          + to_port          = 0
        }
      + ingress {
          + cidr_blocks      = ["0.0.0.0/0"]
          + description      = "HTTP"
          + from_port        = 80
          + protocol         = "tcp"
          + to_port          = 80
        }
    }
Plan: 2 to add, 0 to change, 0 to destroy.
Changes to Outputs:
  + instance_id         = (known after apply)
  + instance_public_dns = (known after apply)
  + instance_public_ip  = (known after apply)
  + security_group_id   = (known after apply)
  + website_url         = (known after apply)
Understanding the Plan Output
Section 1: Data Source Queries
data.aws_ami.amazon_linux: Reading...
data.aws_ami.amazon_linux: Read complete after 1s [id=ami-012967cc5a8c9f891]
- Terraform queries AWS for existing resources
 - Finds the latest Amazon Linux 2 AMI
 - Takes about 1 second to complete the query
 
Section 2: Resource Actions Legend
Resource actions are indicated with the following symbols:
  + create
| Symbol | Meaning | Safety Level | 
|---|---|---|
+ | Create new resource | Safe (new resource) | 
~ | Update in-place | Usually safe (no downtime) | 
-/+ | Destroy and recreate | Caution (causes downtime) | 
- | Destroy resource | Danger (resource deleted) | 
Section 3: Summary
Plan: 2 to add, 0 to change, 0 to destroy.
- 2 to add: Will create 2 new resources (security group + EC2 instance)
 - 0 to change: No existing resources will be modified
 - 0 to destroy: No resources will be deleted
 
Section 4: Output Changes
Changes to Outputs:
  + instance_public_ip  = (known after apply)
- Shows which output values will become available
 (known after apply)means the value doesn't exist yet
β Critical Safety Check: Always review the plan output carefully before applying! Pay special attention to:
- Any 
-/+(replace) or-(destroy) actions - The summary line (X to add, Y to change, Z to destroy)
 - Resources being modified that you didn't expect
 
Step 5: terraform apply - Creating Real Infrastructure
Now for the exciting partβactually creating your AWS infrastructure! The apply command executes the plan and makes real changes.
terraform apply
What this command does:
- Runs 
terraform planautomatically - Shows you the plan
 - Asks for confirmation
 - Upon approval, executes the changes
 - Creates/modifies AWS resources
 - Updates the state file
 - Displays outputs
 
Expected output (showing key sections):
[... plan output similar to above ...]
Plan: 2 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
  Enter a value: yes
aws_security_group.terraform_sg: Creating...
aws_security_group.terraform_sg: Creation complete after 3s [id=sg-0a34cd43d1c56f4e2]
aws_instance.terraform_instance: Creating...
aws_instance.terraform_instance: Still creating... [10s elapsed]
aws_instance.terraform_instance: Still creating... [20s elapsed]
aws_instance.terraform_instance: Creation complete after 25s [id=i-0afea07377a6c62ff]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-0afea07377a6c62ff"
instance_public_dns = "ec2-3-92-197-145.compute-1.amazonaws.com"
instance_public_ip = "3.92.197.145"
security_group_id = "sg-0a34cd43d1c56f4e2"
website_url = "http://3.92.197.145"
Understanding Apply Output
Confirmation Prompt:
Do you want to perform these actions?
  Enter a value: yes
- Type exactly 
yes(notyorYes) - Any other input cancels the operation
 - Use 
terraform apply -auto-approveto skip (not recommended for production) 
Resource Creation Progress:
aws_security_group.terraform_sg: Creating...
aws_security_group.terraform_sg: Creation complete after 3s [id=sg-0a34cd43d1c56f4e2]
- Shows real-time progress
 - Security group created first (EC2 depends on it)
 - 3 seconds to create
 - Shows the generated ID
 
Long-Running Resources:
aws_instance.terraform_instance: Still creating... [10s elapsed]
aws_instance.terraform_instance: Still creating... [20s elapsed]
aws_instance.terraform_instance: Creation complete after 25s
- EC2 instances take longer (~25 seconds)
 - Progress updates every 10 seconds
 - User data script running during boot
 
β οΈ Wait for User Data: Even after Terraform completes, the user data script (Apache installation) continues running. Wait about 60 seconds before accessing the web server to ensure Apache is fully installed and started.
Verifying Your Infrastructure
Let's verify that everything was created successfully!
Check the Web Server
Open your browser and visit the website URL from the outputs:
http://3.92.197.145
You should see a page displaying:
- Hello from Terraform Lab!
 - Instance ID: i-0afea07377a6c62ff
 - Availability Zone: us-east-1a
 
π Congratulations! You've successfully deployed real infrastructure to AWS using Terraform! Your EC2 instance is running, Apache is serving web pages, and everything is accessible from the internet.
Using AWS CLI to Verify
You can also verify using AWS CLI:
aws ec2 describe-instances --instance-ids i-0afea07377a6c62ff --query 'Reservations[0].Instances[0].State.Name'
Should return: "running"
Step 6: terraform output
Query specific output values without showing the entire state:
terraform output
Shows all outputs:
instance_id = "i-0afea07377a6c62ff"
instance_public_ip = "3.92.197.145"
Query a specific output:
terraform output instance_public_ip
Output:
"3.92.197.145"
Raw value (no quotes, useful for scripts):
terraform output -raw instance_public_ip
Step 7: terraform destroy - Clean Up
When you're done experimenting, destroy all resources to avoid charges:
terraform destroy
You'll see a plan showing resources to be destroyed, then a confirmation prompt. Type yes to proceed.
Expected output:
Plan: 0 to add, 0 to change, 2 to destroy.
Do you really want to destroy all resources?
  Enter a value: yes
aws_instance.terraform_instance: Destroying... [id=i-0afea07377a6c62ff]
aws_instance.terraform_instance: Still destroying... [10s elapsed]
aws_instance.terraform_instance: Destruction complete after 32s
aws_security_group.terraform_sg: Destroying... [id=sg-0a34cd43d1c56f4e2]
aws_security_group.terraform_sg: Destruction complete after 2s
Destroy complete! Resources: 2 destroyed.
π¨ Important: Always run terraform destroy when you're done with tutorial infrastructure. This ensures you don't accidentally incur AWS charges for resources you're not using.
Best Practices Summary
| Practice | Why It Matters | 
|---|---|
| Always run plan before apply | Preview changes, catch mistakes | 
| Review destroy plans carefully | Prevent accidental deletion | 
| Commit .terraform.lock.hcl | Version consistency across team | 
| Never commit terraform.tfstate | Contains sensitive data | 
| Use remote state for teams | Enables collaboration, locking | 
Command Reference Cheat Sheet
| Command | Purpose | When to Use | 
|---|---|---|
terraform init | Initialize directory | First time, after adding providers | 
terraform validate | Check syntax | After writing configuration | 
terraform fmt | Format code | Before committing | 
terraform plan | Preview changes | Before every apply | 
terraform apply | Create/modify | To deploy changes | 
terraform destroy | Delete all | Clean up resources | 
terraform output | Show outputs | Get resource info | 
π Congratulations! You've completed the entire hands-on Terraform series!
You now have practical experience with:
- Installing and configuring Terraform and AWS
 - Writing complete infrastructure configuration files
 - Understanding every Terraform command and its output
 - Deploying real infrastructure to AWS
 - Managing infrastructure lifecycle
 - Safely destroying resources
 
You're now ready to:
- Build your own Terraform projects
 - Manage AWS infrastructure professionally
 - Explore advanced Terraform features (modules, remote state, workspaces)
 - Apply these skills to other cloud providers
 
Part 3 of 3 in the Terraform Hands-On series. You've completed the entire journey from installation to deployment. Continue exploring Terraform's advanced features to further enhance your infrastructure automation skills!
