Terraform Basics — Infrastructure as Code Core Concepts Guide

Terraform beginner's guide: Infrastructure as Code core concepts, HCL syntax, providers, state management, modules, and deploying your first AWS/GCP infrastructure.

What is Infrastructure as Code?

Infrastructure as Code (IaC) means defining your cloud infrastructure — servers, databases, networking, DNS, permissions — in code files that can be version-controlled, reviewed, and deployed automatically. Terraform, by HashiCorp, is the most widely adopted IaC tool. It works with 1000+ providers including AWS, GCP, Azure, Cloudflare, GitHub, and Kubernetes.

The key insight: instead of clicking through cloud consoles, you write declarative HCL (HashiCorp Configuration Language) files that describe the desired end state. Terraform figures out what to create, update, or delete to reach that state.

Installation and First Steps

# Install via homebrew (macOS/Linux)
brew install terraform

# Or via tfenv (version manager, recommended)
brew install tfenv
tfenv install 1.7.0
tfenv use 1.7.0

# Verify
terraform version

# Project structure
my-infra/
├── main.tf          # Main resources
├── variables.tf     # Input variables
├── outputs.tf       # Output values
├── providers.tf     # Provider configuration
└── terraform.tfvars # Variable values (gitignore secrets!)

HCL Syntax Fundamentals

HCL is a declarative language. You describe what you want, not how to create it:

# Resource block: type + name (local identifier)
resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name        = "web-server"
    Environment = "production"
  }
}

# Data source: read existing infrastructure
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-22.04-*"]
  }
}

# Variable: parameterize your config
variable "instance_type" {
  type        = string
  description = "EC2 instance type"
  default     = "t3.micro"
}

# Output: expose values for use or display
output "instance_ip" {
  value = aws_instance.web_server.public_ip
}

Providers

Providers are plugins that let Terraform talk to specific APIs. Configure them in providers.tf:

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
  # Credentials from environment: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  # Or from ~/.aws/credentials
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

The Core Workflow

# 1. Initialize: download providers and modules
terraform init

# 2. Validate syntax
terraform validate

# 3. Preview changes (ALWAYS do this before apply)
terraform plan

# 4. Apply — creates/updates/destroys resources
terraform apply

# 5. Destroy all resources (use with extreme caution)
terraform destroy

# Useful flags
terraform plan -out=tfplan        # Save plan to file
terraform apply tfplan            # Apply saved plan (no confirmation prompt)
terraform apply -auto-approve     # Skip confirmation (CI only)
terraform plan -target=aws_instance.web_server  # Plan specific resource

Variables and tfvars

# variables.tf
variable "environment" {
  type        = string
  description = "Deployment environment"
  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "Must be dev, staging, or production."
  }
}

variable "db_password" {
  type      = string
  sensitive = true  # Never shown in plans or logs
}

variable "tags" {
  type = map(string)
  default = {
    Project   = "myapp"
    ManagedBy = "terraform"
  }
}
# terraform.tfvars (commit this for non-secrets)
environment = "production"
tags = {
  Project = "devkits"
  Owner   = "platform-team"
}

# terraform.tfvars.local or secrets.tfvars (gitignore this!)
db_password = "super-secret-password"
# Apply with secret vars file
terraform apply -var-file=secrets.tfvars

State Management

Terraform tracks what it has created in a state file (terraform.tfstate). Never store state locally in production — use a remote backend:

# Remote state in S3 (most common for AWS)
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"  # Prevents concurrent applies
  }
}

# Remote state in Terraform Cloud
terraform {
  cloud {
    organization = "my-org"
    workspaces {
      name = "production"
    }
  }
}
# Import existing resource into state (for pre-existing infra)
terraform import aws_s3_bucket.mybucket my-bucket-name

# Remove a resource from state without destroying it
terraform state rm aws_instance.old_server

# Show current state
terraform state list
terraform state show aws_instance.web_server

Modules — Reusable Infrastructure

Modules are reusable building blocks. A module is just a directory with Terraform files:

# modules/web-server/main.tf
resource "aws_instance" "server" {
  ami           = var.ami_id
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.server.id]

  tags = {
    Name = var.name
  }
}

resource "aws_security_group" "server" {
  name = "${var.name}-sg"
  # ...
}

# modules/web-server/variables.tf
variable "name" { type = string }
variable "ami_id" { type = string }
variable "instance_type" { type = string; default = "t3.micro" }

# modules/web-server/outputs.tf
output "public_ip" { value = aws_instance.server.public_ip }
# Use the module in main.tf
module "web_server" {
  source        = "./modules/web-server"
  name          = "prod-web"
  ami_id        = data.aws_ami.ubuntu.id
  instance_type = "t3.small"
}

# Use module output
output "server_ip" {
  value = module.web_server.public_ip
}

# Use public registry modules
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "production-vpc"
  cidr = "10.0.0.0/16"
}

Practical Example: Static Website on S3 + CloudFront

resource "aws_s3_bucket" "website" {
  bucket = "my-website-${var.environment}"
}

resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id

  index_document { suffix = "index.html" }
  error_document { key    = "404.html" }
}

resource "aws_cloudfront_distribution" "website" {
  enabled             = true
  default_root_object = "index.html"

  origin {
    domain_name = aws_s3_bucket.website.bucket_regional_domain_name
    origin_id   = "S3Origin"
  }

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "S3Origin"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

Frequently Asked Questions

Terraform vs Pulumi vs AWS CDK?

Terraform uses HCL (declarative, infrastructure-focused). Pulumi and CDK use real programming languages (Python, TypeScript) — better for complex logic, but more complex to manage. Terraform has the largest ecosystem and most community modules. Start with Terraform unless you need complex logic or are deeply tied to one cloud provider.

How do I handle secrets in Terraform?

Never hardcode secrets. Use: environment variables (TF_VAR_name), AWS Secrets Manager / Vault data sources, or mark variables as sensitive = true. Never commit .tfvars files with secrets to git — add them to .gitignore.

What is a Terraform workspace?

Workspaces let you manage multiple environments (dev/staging/production) with the same configuration, each with their own state file. Use terraform workspace new staging and reference terraform.workspace in your config to parameterize environment-specific values.

For managing configuration files and templates, use DevKits JSON Formatter to validate Terraform variable JSON files. The DevKits Dockerfile Generator Pro complements your IaC workflow for containerized deployments.