Skip to content

Introduction To Terraform

Terraform is an open-source tool by HashiCorp for defining, provisioning, and managing infrastructure through code. In essence, it enables you to automate the entire lifecycle of your infrastructure (servers, networking, databases, SaaS services, etc.) by describing your desired resources in declarative configuration files. Once you’ve written those files, Terraform handles creating, modifying, and destroying resources across a wide variety of providers (AWS, Azure, Google Cloud, Kubernetes, GitHub, and many others).

Key benefits of using Terraform include:

  1. Build (Create): You write configuration files (.tf files) declaring what infrastructure you want. Terraform figures out how to create those resources.
  2. Modify (Update): When you change your configurations, Terraform computes a diff (plan) between the existing real-world resources and your updated code, then applies only the necessary additions, deletions, or modifications.
  3. Version (Track): Terraform stores the current state of your managed infrastructure in a state file (terraform.tfstate). By committing your configuration files to version control (e.g., Git), you can track changes over time, revert erroneous updates, and collaborate safely in teams.

Terraform seamlessly integrates with traditional Infrastructure as a Service (IaaS), Platform as a Service (PaaS), and even many Software as a Service (SaaS) offerings, making it a true Infrastructure as Code (IaC) solution.

Infrastructure as Code (IaC)

Before delving into commands, let’s clarify the core IaC concepts Terraform builds upon:

  • Automated Provisioning Instead of manually clicking around cloud consoles or running dozens of shell scripts, you describe your infrastructure in code. Terraform parses these files and calls provider APIs to create the resources you need, in a fully automated fashion.

  • Infrastructure Maintenance As requirements change, you update your Terraform configuration, and Terraform computes a safe set of changes to apply. This ensures consistency between what you declared and what actually exists.

  • Continuous Integration / Continuous Delivery (CI/CD) Terraform can be integrated into CI/CD pipelines (e.g., GitHub Actions, GitLab CI, Jenkins, etc.). Every time you update your configuration in source control, you can automatically run terraform validate / terraform plan / terraform apply to test, preview, and apply changes. This guarantees that infrastructure changes are versioned, peer-reviewed, and applied consistently.

Terraform’s “State”

One of Terraform’s distinguishing features is its reliance on a state file (terraform.tfstate by default). The state represents Terraform’s snapshot of:

  • Which resources exist.
  • How those resources are configured.
  • Any metadata needed to map Terraform resources to actual provider resources.

When you run a command like terraform plan or terraform apply, Terraform does the following:

  1. Reads the configuration (.tf files) in your working directory.
  2. Loads the current state from terraform.tfstate (or a remote backend, if configured).
  3. Queries real-world resources (the actual cloud or service) to discover their current attributes.
  4. Computes a diff between:

  5. The state file (what Terraform last knew existed).

  6. The real-world resources (what actually exists right now).
  7. Your desired configuration (what you want to exist based on .tf files).
  8. Plans and/or applies changes so that reality matches the desired configuration.

Because Terraform tracks a state, it can efficiently determine which resources to add, modify, or destroy—rather than recreating everything from scratch.

Installation

Before using Terraform, you need to install the Terraform binary appropriate for your operating system.

Official Download Page: https://developer.hashicorp.com/terraform/install

Below is an example of installing Terraform on a RHEL/CentOS system via yum:

# 1. Install yum-utils if not already present
sudo yum install -y yum-utils

# 2. Add the HashiCorp Linux repository
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo

# 3. Install Terraform
sudo yum -y install terraform

# 4. Verify installation
terraform version

On other distributions or operating systems, follow the instructions at the Terraform website. Typically, it boils down to downloading a zip archive, unzipping it, and placing the terraform binary on your PATH.

“Hello, World!” with Terraform

A minimal Terraform configuration can simply declare an output. This example shows how to:

  1. Initialize a new directory for Terraform.
  2. Create a main.tf file that defines a simple output.
  3. Apply the configuration to see the output and understand how the state file is generated.

1. Initialize a New Workspace

# Create a brand-new directory for our example
mkdir hello-world
cd hello-world

# Initialize Terraform in this directory
terraform init

# Expected output:
# Terraform has been successfully initialized!
# The directory has no Terraform configuration files. You may begin working
# with Terraform immediately by creating Terraform configuration files.
  • terraform init

  • This is always the first command you run in a new Terraform working directory.

  • It downloads and installs the necessary provider plugins (e.g., AWS, Azure, Google Cloud) and sets up backend configuration (if any).
  • If there are no .tf files yet, it simply prepares the directory and lets you know you can start writing configuration.

2. Create main.tf

Open your favorite text editor and create a file named main.tf with the following content:

# main.tf

# There are no provider blocks, since we’re not provisioning any real resources.
# We’ll define a simple output to demonstrate how Terraform generates a state.

output "helloworld" {
  value = "Hello World!!!"
}
  • output "helloworld"

  • Defines an output named “helloworld.”

  • Its value is a literal string: "Hello World!!!".
  • When you run terraform apply, Terraform will show that output in your terminal.

Save main.tf and return to your terminal.

3. Validate, Plan, and Apply

Although validate and plan are optional for this trivial configuration, it’s good practice to run them in a real project.

  1. Validate:
terraform validate

# Expected output:
# Success! The configuration is valid.
  • terraform validate

    • Checks the syntax and internal consistency of your configuration.
    • It does not check against real-world API calls. It simply ensures your HCL (HashiCorp Configuration Language) is well-formed and references valid attributes.
  • Plan:

terraform plan

# Expected output (approximate):
# Refreshing Terraform state in-memory prior to plan...
# The refreshed state will be used to calculate this plan, but will not be
# persisted to local or remote state storage.
#
# No changes. 
#   Outputs:
#     helloworld = "Hello World!!!"
#
# Note: You didn't specify an "-out" parameter to save this plan, so Terraform
# can't guarantee that exactly these actions will be performed if
# "terraform apply" is subsequently run.
  • terraform plan

    • Looks at your current configuration (main.tf), reads the current state (which is empty or nonexistent on first run), then shows you what would change if you ran terraform apply.
    • Since there are no real resources and only an output, Terraform simply notes “No changes” and displays the helloworld output value.
  • Apply:

terraform apply

# It will prompt:
# Do you want to perform these actions?
#   Terraform will perform the actions described above.
#   Only 'yes' will be accepted to approve.
#
#   ...
#   Outputs:
#     helloworld = "Hello World!!!"
#
# Plan: 0 to add, 0 to change, 0 to destroy.
#
# Do you want to perform these actions? yes
# helloworld = "Hello World!!!"
  • terraform apply

    • Actually applies the changes to the “real world” (in our case, there are no resources to create, so it simply generates state and displays outputs).
    • After you confirm with yes, Terraform writes a new file terraform.tfstate to track the state of your (trivial) infrastructure.

At this point, you have:

hello-world/
├── main.tf
└── terraform.tfstate

Inspecting the State File

Open terraform.tfstate in a text editor (or use cat) to see its contents. You will find JSON similar to:

{
  "version": 4,
  "terraform_version": "1.12.1",           
  "serial": 1,
  "lineage": "f52964b5-606f-ae0c-347b-88102b64e414",
  "outputs": {
    "helloworld": {
      "value": "Hello World!!!",
      "type": "string"
    }
  },
  "resources": [],
  "check_results": null
}
  • version: Internal state file format version.
  • terraform_version: Which Terraform binary created this state.
  • serial: A counter that increments every time the state is updated.
  • lineage: A unique identifier for a given state “lineage.” If you copy or migrate state, you can preserve lineage to continue tracking.
  • outputs: A map of the declared outputs (helloworld) and their values/types.
  • resources: An array of managed resources. It’s empty here because we didn’t create anything but an output.
  • check_results: Reserved for future system checks (currently null).

This state file is how Terraform knows what it has already created or tracked. In a real-world scenario, resources would list every resource (e.g., AWS EC2 instances, VPCs, S3 buckets) along with their IDs, attributes, and dependencies.

Terraform Workflow & Common Commands

When using Terraform in a real project, you typically follow a repeatable workflow. Below is an overview of the most commonly used commands, along with some “less common” but still important commands.

1. Primary Workflow Commands

# 1) terraform init
#    - Prepares your working directory by downloading plugins, setting up backends, etc.

# 2) terraform validate
#    - Checks the syntax and internal consistency of your configuration files.

# 3) terraform plan
#    - Shows the changes required to reach the current configuration from the existing state.
#    - Does NOT apply changes; only previews them.

# 4) terraform apply
#    - Applies the changes to create, update, or delete real resources to match your configuration.
#    - Prompts for approval unless run with -auto-approve.

# 5) terraform destroy
#    - Destroys all managed resources defined in your configuration.
#    - Use with caution—removes infrastructure.

1.1. terraform init

  • Purpose: Initialize a working directory containing Terraform configuration files.
  • Actions performed:

  • Validates backend configuration.

  • Downloads and installs provider plugins referenced in the configuration.
  • Creates a .terraform/ directory to store plugin binaries and module registry information.
  • Typical Usage:
terraform init
  • If you add a new provider or switch backends (e.g., migrating from local state to AWS S3), you must re-run terraform init.

1.2. terraform validate

  • Purpose: Ensures that the Terraform files in the current directory are syntactically valid and internally consistent.
  • Does NOT check whether the provider credentials are valid or whether the referenced infrastructure exists.
  • Typical Usage:
terraform validate
  • Run this as part of automated CI pipelines to catch mistakes early.

1.3. terraform plan

  • Purpose: Performs a “dry run” by comparing:

  • The current state (from terraform.tfstate or remote backend).

  • Real-world infrastructure (by querying providers).
  • The desired configuration (your .tf files).
  • Outputs: A summary of what actions Terraform will take (e.g., “+ create 3 resources,” “\~ update attribute X on resource Y,” “– destroy resource Z”).
  • Typical Usage:
terraform plan -out=tfplan
  • Using -out=tfplan saves the proposed changes to a binary plan file. You can later run terraform apply tfplan to ensure you apply exactly what was planned.

1.4. terraform apply

  • Purpose: Applies the changes required to reach the desired configuration.
  • Behavior:

  • If you pass a plan file (terraform apply tfplan), it applies exactly those changes.

  • If you run without a plan file (terraform apply), Terraform implicitly performs a plan and then prompts for approval before applying.
  • Typical Usage:
terraform apply
# or, in automated pipelines:
terraform apply -auto-approve

1.5. terraform destroy

  • Purpose: Safely removes all resources managed by the current configuration.
  • Typical Usage:
terraform destroy
# or, to skip approval:
terraform destroy -auto-approve

2. Additional / Less Common Commands

Once you have the basics down, you can explore other helpful Terraform commands:

  • terraform fmt

  • Formats your .tf files according to canonical style. Keeps code consistent across teams.

  • terraform validate

  • Already covered above (syntax check).

  • terraform taint <resource>

  • Manually marks a resource as “tainted,” forcing Terraform to destroy and recreate it on the next apply.

  • terraform untaint <resource>

  • Reverses a taint, telling Terraform not to recreate that resource on next apply.

  • terraform output [<name>]

  • Reads outputs from the state. Without arguments, lists all outputs. With a <name>, prints that single output’s value.

  • terraform state subcommands

  • Manipulates the state file directly (e.g., terraform state list, terraform state show, terraform state rm, terraform state mv). Use with caution.

  • terraform import <ADDRESS> <ID>

  • Brings existing real-world infrastructure under Terraform management by importing it into state. You still need to write matching resource blocks in your .tf files.

  • terraform graph

  • Generates a DOT-format graph of resource dependencies. You can visualize this with Graphviz.

  • terraform workspace

  • Manages named workspaces (environments). Each workspace has its own state. Useful for isolating “production,” “staging,” and “dev.”

  • terraform providers

  • Shows which providers are required and where Terraform is sourcing them from.

  • terraform version

  • Prints Terraform’s version and the versions of installed providers.

Terraform Resource Lifecycle (CRUD)

Terraform interacts with provider APIs using the standard CRUD model:

  1. Create: Terraform issues an API call to provision a resource.
  2. Read (aka “Refresh”): Terraform periodically (or during plan) reads the current state of each resource from the provider to detect drifts.
  3. Update: If the configuration changes, Terraform figures out how to issue “update” calls to modify existing resources.
  4. Delete: When a resource is removed from the configuration or you run terraform destroy, Terraform calls the provider API to remove that resource.

The typical lifecycle in Terraform is:

  1. Refresh

  2. Terraform reconciles state by comparing terraform.tfstate with the real infrastructure. This happens implicitly during plan and apply, but you can run terraform refresh explicitly if you want to update local state without making changes.

  3. Plan

  4. Calculates a diff and tells you exactly what will be created/changed/destroyed.

  5. Apply

  6. Makes the actual API calls to reach the desired state (Create and Update).

  7. Destroy

  8. Removes resources no longer defined, or if you explicitly run terraform destroy.

Anatomy of a Real-World Terraform Project

In practice, a larger Terraform project will be organized with multiple files and folders. Below is an overview of best practices and typical file structure:

my-terraform-project/
├── modules/
│   ├── network/
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── compute/
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── database/
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── envs/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── prod/
│       ├── main.tf
│       ├── terraform.tfvars
│       └── backend.tf
├── scripts/
│   └── bootstrap-ci.sh
├── .gitignore
├── README.md
└── versions.tf
  • modules/

  • Contains reusable, self-contained “modules” such as networking, compute, and database. Modules encapsulate resources, inputs (variables.tf), and outputs (outputs.tf). Teams can share modules across projects.

  • envs/dev/, envs/staging/, envs/prod/

  • Separate directories for each environment.

  • Each environment typically has its own:
    • backend.tf to define where the state is stored (e.g., S3/GCS bucket, Terraform Cloud workspace).
    • terraform.tfvars for environment-specific variable values (e.g., machine sizes, counts, region).
    • main.tf that invokes the modules with the appropriate variables.
  • versions.tf

  • Pin Terraform core and provider versions to guarantee consistent behavior. Example:

    terraform {
      required_version = ">= 1.2.0, < 2.0.0"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = "~> 5.0"
        }
        kubernetes = {
          source  = "hashicorp/kubernetes"
          version = "~> 2.0"
        }
      }
    }
    
  • README.md

  • High-level documentation on how to get started, environment variables, CI/CD instructions, and on-call procedures for handling failures.

  • .gitignore

  • Always exclude local state files (terraform.tfstate, terraform.tfstate.backup), CLI history, and any sensitive files (e.g., local *.tfvars with secrets).

  • Example entries:

    *.tfstate
    *.tfstate.backup
    *.tfvars
    .terraform/
    

Best Practices & Takeaways

  1. Always initialize first

  2. Run terraform init whenever you clone a repository or add a new provider.

  3. Validate before you plan

  4. terraform validate catches syntax errors early (e.g., missing braces, undeclared variables).

  5. Use plan in CI/CD

  6. Store the plan file with a unique name (e.g., tfplan-dev). Then, have a human review the plan output before applying. This ensures no unintended changes slip through.

  7. Pin provider and Terraform versions

  8. Prevent surprise upgrades by specifying required_version and required_providers in versions.tf.

  9. Keep state secure and backed up

  10. Use a remote state backend (e.g., AWS S3 with DynamoDB locking, Terraform Cloud/Enterprise, Azure Storage Account, Google Cloud Storage).

  11. Enable state locking to prevent concurrent modifications that could corrupt your state.

  12. Modularize common patterns

  13. Create reusable modules (e.g., networking, IAM roles, compute autoscaling) so multiple teams/projects can share well-tested code.

  14. Write clear outputs

  15. Outputs give quick access to important values (e.g., URLs, resource IDs, IP addresses). This is especially helpful when invoking Terraform from automated scripts.

  16. Use workspaces or separate state files for environments

  17. Isolate “dev,” “staging,” and “prod” to avoid accidental cross-environment changes.

  18. Review state before destroy

  19. Always run terraform plan -destroy to verify what will be deleted, especially in production.

  20. Leverage remote execution if needed

    • Terraform Cloud / Terraform Enterprise offer remote run capabilities so that your CI/CD pipeline can push plans to Terraform Cloud, receive an interactive UI for plan review, and then apply from a trusted environment.

Summary of Key Terraform Commands

Below is a concise reference for the most common Terraform commands:

# Initialize a working directory
terraform init

# Check syntax and validity of configuration files
terraform validate

# Show plan diff (without applying changes)
terraform plan -out=MYPLAN.tfplan

# Apply changes (apply a saved plan or run implicit plan)
terraform apply MYPLAN.tfplan
# or
terraform apply

# Destroy all managed infrastructure
terraform destroy

# Format configuration files according to canonical style
terraform fmt

# Mark a resource as tainted (force recreation)
terraform taint aws_instance.example

# Unmark a tainted resource
terraform untaint aws_instance.example

# Import an existing resource into state
terraform import aws_instance.example i-0123456789abcdef0

# List all resources in the state
terraform state list

# Show detailed state for a single resource
terraform state show aws_instance.example

# Save state remotely or switch backends
terraform init -backend-config="..."

# Generate a graph (DOT) of resource dependencies
terraform graph | dot -Tsvg > graph.svg

# Manage workspaces (environments)
terraform workspace new dev
terraform workspace list
terraform workspace select prod

Conclusion

This tutorial has walked you through:

  • What Terraform is and why you’d use it to manage infrastructure as code (IaC).
  • Core IaC concepts: automation, maintenance, CI/CD integration.
  • Understanding the Terraform state, how it tracks real-world resources, and why it’s essential.
  • Installation steps for a RHEL/CentOS environment (as an example).
  • A “Hello, World!” example that demonstrated initializing a directory, writing main.tf, validating, planning, and applying, as well as inspecting the resulting state file.
  • The primary Terraform workflow commands (init, validate, plan, apply, destroy) and a quick reference to helpful additional commands (fmt, taint, import, state, etc.).
  • Best practices around version pinning, remote state, module reuse, and CI/CD integration.

In a real-world project, always remember to follow the order:

terraform init      # Prepare your workspace and download providers
terraform validate  # Check syntax
terraform plan      # Preview changes
terraform apply     # Apply changes
terraform destroy   # Destroy resources when you no longer need them

By adhering to these steps and incorporating the best practices outlined above, you’ll be well on your way to managing infrastructure reliably and reproducibly with Terraform.