18 min read

Terraform Fundamentals: Infrastructure as Code Best Practices

Master the fundamentals of Terraform and Infrastructure as Code. Learn HCL syntax, work with providers and resources, manage state effectively, use workspaces, and implement best practices for reliable, maintainable infrastructure.

Terraform Fundamentals: Infrastructure as Code for Beginners

Key Takeaways

  • Terraform enables declarative infrastructure management across multiple cloud providers using HashiCorp Configuration Language (HCL).
  • State files track the current state of your infrastructure, enabling Terraform to determine what changes are needed.
  • Remote backends provide state locking and team collaboration, preventing concurrent modifications.
  • Workspaces enable managing multiple environments (dev, staging, prod) with the same configuration.

What is Terraform?

Terraform is an open-source Infrastructure as Code (IaC) tool created by HashiCorp. It allows you to define, provision, and manage infrastructure across multiple cloud providers and services using a declarative configuration language.

Unlike imperative tools that require you to specify the exact steps to create infrastructure, Terraform uses a declarative approach. You describe the desired end state, and Terraform determines the actions needed to achieve it.

“Terraform enables you to safely and predictably create, change, and improve infrastructure. It is an open source tool that codifies APIs into declarative configuration files.”

— HashiCorp

How Terraform Works

Terraform operates through a clear, three-step workflow that manages infrastructure lifecycle from creation to destruction.

Write

Define infrastructure in HCL configuration files. Describe resources, providers, and their relationships.

Plan

Preview changes Terraform will make. The plan shows what will be created, modified, or destroyed.

Apply

Execute the plan to create or modify infrastructure. Terraform calls provider APIs to make changes.

State

Track the current state of infrastructure. State enables Terraform to detect drift and plan updates.

Installing Terraform

BASH
# macOS (with Homebrew)# Ubuntu/Debian# Ubuntu/Debian# Ubuntu/Debian# Ubuntu/Debian# Ubuntu/Debian# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Windows (with Chocolatey)# Verify installation# Verify installationallation
terraform --version

HCL Basics

HashiCorp Configuration Language (HCL) is the language used to write Terraform configurations. It's designed to be human-readable and machine-friendly.

Basic Syntax

HCL
# Comments start with hash// Or double slash# Blocks define configuration sections# Arguments use key = value syntax# Arguments use key = value syntax# Arguments use key = value syntax# Arguments use key = value syntax# Arguments use key = value syntax# Arguments use key = value syntax# Arguments use key = value syntax# Expressions reference other values# Strings support interpolation# Strings support interpolation# Strings support interpolation# Multiline strings use heredoc# Multiline strings use heredocuse heredoc
script = <<-EOF
  #!/bin/bash
  echo "Hello World"
EOF

Data Types

HCL
# Strings# Numbers# Numbers# Numbers# Booleans# Booleans# Booleans# Lists# Lists# Lists# Lists# Maps# Maps# Maps# Maps# Maps# Maps# Maps# Maps# Maps# Maps# Maps# Maps# Objects# Objects# Objects# Objects# Objects# Objects# Objects# Objects# Objectsts
instance_config = {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  monitoring    = true
}

Providers and Resources

Providers are plugins that enable Terraform to interact with cloud platforms, SaaS providers, and other APIs. Resources are the infrastructure objects managed by providers.

Provider Configuration

versions.tfhcl
# versions.tf - Provider version constraints# Provider configuration# Provider configuration# Provider configuration# Provider configuration# Provider configuration# Provider configuration# Provider configuration# Provider configuration# Provider configuration# Provider configuration# Multiple provider configurations with aliases# Multiple provider configurations with aliases# Multiple provider configurations with aliases# Multiple provider configurations with aliasesconfigurations with aliases
provider "aws" {
  alias  = "us_east"
  region = "us-east-1"
}

Resource Definitions

HCL
# Basic resource# Resource with dependencies# Resource with dependencies# Resource with dependencies# Resource with dependencies# Resource with dependencies# Resource with dependencies# Resource with dependencies# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with count# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_each# Resource with for_eacheach
resource "aws_s3_bucket" "buckets" {
  for_each = toset(["logs", "assets", "backups"])
  
  bucket = "my-company-${each.value}-${random_id.suffix.hex}"
}

Variables and Outputs

Input Variables

variables.tfhcl
# variables.tf

variable "environment" {
  description = "The deployment environment"
  type        = string
  default     = "development"
  
  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "Environment must be development, staging, or production."
  }
}

variable "instance_count" {
  description = "Number of instances to create"
  type        = number
  default     = 1
}

variable "enable_monitoring" {
  description = "Enable detailed monitoring"
  type        = bool
  default     = false
}

variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
  default     = ["eu-west-2a", "eu-west-2b"]
}

variable "instance_config" {
  description = "Instance configuration"
  type = object({
    ami           = string
    instance_type = string
    key_name      = optional(string)
  })
}

variable "tags" {
  description = "Tags to apply to resources"
  type        = map(string)
  default     = {}
}

Output Values

outputs.tfhcl
# outputs.tf

output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "instance_public_ips" {
  description = "Public IPs of EC2 instances"
  value       = aws_instance.web[*].public_ip
}

output "database_endpoint" {
  description = "Database connection endpoint"
  value       = aws_db_instance.main.endpoint
  sensitive   = true  # Marks output as sensitive
}

Setting Variables

HCL
# terraform.tfvars# Command line# Command line# Command line# Command line# Command line# Command line# Environment variables# Environment variables# Environment variables# Variable file# Variable file# Variable file# Variable fileile
terraform apply -var-file="production.tfvars"

State Management

Terraform state is a critical concept. The state file tracks which real-world resources correspond to your configuration, enabling Terraform to determine what changes need to be made.

Remote Backend (S3)

backend.tfhcl
# backend.tf# Create the S3 bucket and DynamoDB table first# Create the S3 bucket and DynamoDB table first# Create the S3 bucket and DynamoDB table first# Create the S3 bucket and DynamoDB table first# Create the S3 bucket and DynamoDB table first# Create the S3 bucket and DynamoDB table firstket and DynamoDB table first
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state"
  
  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

State Commands

BASH
# List resources in state# Show resource details# Show resource details# Move resource in state# Move resource in state# Remove resource from state (without destroying)# Remove resource from state (without destroying)# Import existing infrastructure# Import existing infrastructure# Import existing infrastructure# Pull remote state locally# Pull remote state locally# Pull remote state locally locally
terraform state pull > backup.tfstate

Workspaces

Workspaces allow you to manage multiple instances of your infrastructure (e.g., development, staging, production) using the same configuration.

HCL
# Create and switch workspaces# List workspaces# List workspaces# List workspaces# List workspaces# List workspaces# List workspaces# List workspaces# Switch workspace# Switch workspace# Show current workspace# Show current workspace# Show current workspace# Using workspace in configuration# Conditional count based on workspace# Conditional count based on workspace# Conditional count based on workspace# Conditional count based on workspace# Conditional count based on workspace# Conditional count based on workspace# Conditional count based on workspacee
resource "aws_instance" "worker" {
  count = terraform.workspace == "production" ? 5 : 1
  # ...
}

Data Sources

Data sources allow Terraform to query and use information from external sources or existing infrastructure not managed by Terraform.

HCL
# Get latest Amazon Linux 2 AMI# Get current AWS region# Get current AWS region# Get current AWS region# Get current AWS region# Get current AWS region# Get current AWS region# Get current AWS region# Get current caller identity# Get current caller identity# Get availability zones# Get availability zones# Get availability zones# Using data sources# Using data sources# Using data sources# Using data sources# Using data sources sources
resource "aws_instance" "web" {
  ami               = data.aws_ami.amazon_linux.id
  instance_type     = "t3.micro"
  availability_zone = data.aws_availability_zones.available.names[0]
}

Built-in Functions

Terraform includes many built-in functions for transforming and manipulating data.

String Functions

join(separator, list)

Joins list elements with separator

split(separator, string)

Splits string into list

lower(string)

Converts to lowercase

upper(string)

Converts to uppercase

replace(string, search, replace)

Replaces occurrences

trimspace(string)

Removes leading/trailing whitespace

Collection Functions

length(list)

Returns number of elements

merge(map1, map2)

Merges maps together

concat(list1, list2)

Concatenates lists

flatten(list)

Flattens nested lists

keys(map)

Returns map keys as list

values(map)

Returns map values as list

Numeric Functions

min(numbers...)

Returns smallest number

max(numbers...)

Returns largest number

abs(number)

Returns absolute value

ceil(number)

Rounds up to nearest integer

floor(number)

Rounds down to nearest integer

Filesystem Functions

file(path)

Reads file contents as string

fileexists(path)

Checks if file exists

templatefile(path, vars)

Renders template with variables

basename(path)

Returns filename from path

dirname(path)

Returns directory from path

Terraform Workflow

CommandDescription
terraform initInitialises the working directory and downloads providers
terraform planCreates an execution plan showing what will change
terraform applyApplies changes to reach the desired state
terraform destroyDestroys all managed infrastructure
terraform fmtFormats configuration files to canonical style
terraform validateValidates the configuration files
terraform outputDisplays output values from state
terraform state listLists resources in the state file
terraform importImports existing infrastructure into state
terraform workspaceManages workspaces for environment separation

Typical Workflow

BASH
# 1. Initialise the working directory# 2. Format configuration files# 3. Validate configuration# 3. Validate configuration# 4. Create execution plan# 4. Create execution plan# 5. Apply the plan# 5. Apply the plan# 6. View outputs# 6. View outputs# 6. View outputs# 7. When done, destroy infrastructureestroy infrastructure
terraform destroy

Best Practices

☁️

Use Remote State

Store state in a remote backend like S3 or Terraform Cloud for team collaboration and state locking.

🔧

Use Variables

Parameterise your configurations to make them reusable across environments.

📐

Format Consistently

Run terraform fmt regularly to maintain consistent code style across your team.

📦

Use Modules

Encapsulate reusable infrastructure patterns into modules for consistency and maintainability.

👀

Review Plans Carefully

Always review terraform plan output before applying to avoid unintended changes.

📌

Version Pin Providers

Specify provider version constraints to ensure reproducible builds.

Recommended File Structure

BASH
terraform-project/
 main.tf           # Primary resource definitions
 variables.tf      # Input variable declarations
 outputs.tf        # Output value declarations
 versions.tf       # Provider and Terraform versions
 backend.tf        # Backend configuration
 data.tf           # Data source definitions
 locals.tf         # Local values
 terraform.tfvars  # Variable values (not in git)
 modules/          # Local modules
    vpc/
        main.tf
        variables.tf
        outputs.tf
 environments/     # Environment-specific configs
     dev.tfvars
     staging.tfvars
     production.tfvars

Troubleshooting

Common issues and solutions when working with Terraform.

State lock errors

Error: “Error acquiring the state lock”

Common causes:

  • Previous Terraform run crashed or was interrupted
  • Another team member is running Terraform
  • Stale lock in DynamoDB or storage backend

Solution:

BASH
# Check who holds the lock# For S3 backend, check DynamoDB table for stale locks# Always use -lock-timeout for long operations# Always use -lock-timeout for long operationsse -lock-timeout for long operations
terraform apply -lock-timeout=10m

Provider authentication failures

Error: “Error configuring provider: no valid credential sources found”

Common causes:

  • Missing or expired credentials
  • Wrong region or profile specified
  • IAM role without proper trust relationship

Solution:

BASH
# AWS: Check credentials# Set credentials explicitly# Set credentials explicitly# Or use profiles# Or use profiles# Or use profiles# Or use profiles# Or use profiles# Or use profiles# Azure: Login and set subscription# Azure: Login and set subscription# GCP: Authenticate# GCP: Authenticate# GCP: Authenticate# GCP: Authenticate: Authenticate
gcloud auth application-default login

State drift and unexpected changes

Symptoms: Plan shows changes you didn't make, or resources keep getting recreated

Common causes:

  • Manual changes made outside Terraform
  • Different Terraform or provider versions
  • Default values changed in provider
  • Ignored attributes causing perpetual diff

Solution:

HCL
# Refresh state from actual infrastructure# Import manually created resources# Use lifecycle to ignore specific changes# Use lifecycle to ignore specific changese lifecycle to ignore specific changes
resource "aws_instance" "example" {
  # ...
  lifecycle {
    ignore_changes = [tags["LastModified"]]
  }
}

Dependency cycle errors

Error: “Cycle: resource.a, resource.b”

Common causes:

  • Two resources referencing each other
  • Module outputs creating circular dependencies
  • Security group rules referencing each other

Solution:

HCL
# Visualise the dependency graph# Break cycles with separate rule resources Break cycles with separate rule resources
resource "aws_security_group" "a" {
  name = "sg-a"
  # Don't reference sg-b here
}

resource "aws_security_group_rule" "a_to_b" {
  security_group_id        = aws_security_group.a.id
  source_security_group_id = aws_security_group.b.id
  type                     = "ingress"
  # ...
}

Module version conflicts

Error: “Module version requirements have changed” or unexpected module behaviour

Common causes:

  • Using unpinned module versions
  • Cached old module versions
  • Breaking changes in module updates

Solution:

HCL
# Always pin module versions# Clear module cache and re-download# Clear module cache and re-download# Clear module cache and re-download# Clear module cache and re-download# Lock provider versions# Lock provider versions# Lock provider versionsrsions
terraform providers lock -platform=linux_amd64

Conclusion

Terraform is an essential tool for modern infrastructure management. Its declarative approach, multi-cloud support, and state management make it the industry standard for Infrastructure as Code.

The fundamentals covered in this guide—HCL syntax, providers and resources, variables and outputs, state management, workspaces, data sources, and functions—provide the foundation for building consistent, maintainable infrastructure.

As you progress, explore Terraform modules for reusability, integrate with CI/CD pipelines for automated deployments, and consider Terraform Cloud or Enterprise for team collaboration and governance.

Frequently Asked Questions

Terraform is an open-source Infrastructure as Code (IaC) tool created by HashiCorp. It allows you to define, provision, and manage infrastructure across multiple cloud providers (AWS, Azure, GCP) and services using a declarative configuration language called HCL. Rather than manually creating resources, you describe the desired end state and Terraform determines the actions needed to achieve it.

HCL is the declarative language used to write Terraform configurations. It is designed to be both human-readable and machine-friendly. HCL uses a block-based syntax with key-value arguments, supports various data types (strings, numbers, booleans, lists, maps), and allows string interpolation and expressions to reference other values in your configuration.

Terraform state is a JSON file that tracks which real-world infrastructure resources correspond to your configuration. It enables Terraform to determine what changes need to be made when you update your configuration. State is critical for detecting drift, planning updates, and maintaining the mapping between your code and actual infrastructure. For team collaboration, state should be stored remotely with locking enabled.

The "terraform plan" command creates an execution plan that shows what changes Terraform will make to your infrastructure without actually making them. It previews creates, modifications, and deletions. The "terraform apply" command executes the plan and makes the actual changes to your infrastructure by calling provider APIs. Always review the plan output before applying.

Sensitive data in Terraform can be managed by: marking variables as sensitive (sensitive = true) to prevent them from being displayed in logs, using environment variables (TF_VAR_) for secrets, integrating with secret management tools like HashiCorp Vault or AWS Secrets Manager, never committing terraform.tfvars files containing secrets to version control, and encrypting your state file with a remote backend.

Workspaces allow you to manage multiple instances of your infrastructure using the same configuration files but with separate state files. This is useful for managing different environments like development, staging, and production. You can create workspaces with "terraform workspace new", switch between them with "terraform workspace select", and reference the current workspace in your configuration using "terraform.workspace".

References & Further Reading

Related Articles

Ayodele Ajayi

Principal Engineer based in Kent, UK. Specialising in security governance, cloud architecture, and platform engineering.