Terraform Basics
Terraform is an Infrastructure-as-Code tool for building, changing, and versioning infrastructure safely and efficiently across multiple cloud providers. This guide covers the fundamentals through production patterns used to manage infrastructure at Samsung scale.
Core Concepts
| Concept | Description | Example |
|---|---|---|
| Provider | Plugin that manages resources for a specific platform | aws, azurerm, google, kubernetes |
| Resource | A component of your infrastructure to create and manage | aws_instance, aws_s3_bucket |
| Module | A reusable container for multiple resources | modules/vpc, modules/eks |
| State | JSON file mapping Terraform config to real infrastructure | terraform.tfstate |
| Workspace | Multiple named states for the same configuration | dev, staging, prod |
| Data Source | Read-only access to existing infrastructure | data.aws_ami.amazon_linux |
| Variable | Input parameter for configuration | var.instance_type |
| Output | Exported value from a module or root configuration | output.vpc_id |
| Local | Named local value for intermediate computation | local.common_tags |
HCL Syntax Basics
Terraform uses HashiCorp Configuration Language (HCL). The key syntax elements:
# โโ Providers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = local.common_tags
}
}
# โโ Variables โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "allowed_cidr_blocks" {
description = "CIDR blocks allowed to access the VPC"
type = list(string)
default = ["10.0.0.0/8"]
}
# โโ Locals โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
CostCenter = var.cost_center
Owner = var.team_email
}
name_prefix = "${var.project_name}-${var.environment}"
}
# โโ Resources โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-vpc"
})
}
# โโ Data Sources โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# โโ Outputs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
output "vpc_id" {
description = "The ID of the created VPC"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "List of private subnet IDs"
value = aws_subnet.private[*].id
}
State Management
Terraform state is the mapping between your configuration and real-world infrastructure. State management is the most critical operational concern for Terraform at scale.
Local vs. Remote State
| Aspect | Local State | Remote State |
|---|---|---|
| Storage location | terraform.tfstate on local disk | S3, Terraform Cloud, Consul, GCS, Azure Blob |
| Team collaboration | Not suitable (conflicts, no locking) | Designed for team use |
| State locking | None | DynamoDB, Terraform Cloud native |
| State encryption | None (plain JSON) | Server-side encryption (S3 SSE-KMS) |
| Versioning | Manual | S3 versioning, Terraform Cloud history |
| Recommended for | Local development only | All production environments |
S3 Backend with DynamoDB Locking (Production Standard)
# terraform/backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-state-prod"
key = "vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
dynamodb_table = "terraform-state-lock"
# Enable state locking with DynamoDB
# The DynamoDB table must have a primary key named "LockID"
}
}
DynamoDB Lock Table Setup
# Terraform: Create the state management infrastructure once
# This is typically in a separate "bootstrap" configuration
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state-${var.environment}"
tags = {
Name = "Terraform State"
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.terraform.arn
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
point_in_time_recovery {
enabled = true
}
tags = {
Name = "Terraform State Lock"
}
}
Workspaces for Environment Separation
Terraform workspaces allow managing multiple environments with the same configuration:
# Initialize with a local backend (workspaces are always available)
terraform init
# Create workspaces for each environment
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
# Select a workspace
terraform workspace select dev
# The current workspace name is available as terraform.workspace
# Use it to conditionally configure resources:
locals {
instance_type = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.medium"
}[terraform.workspace]
min_size = {
dev = 1
staging = 2
prod = 3
}[terraform.workspace]
}
Workspace Strategy: For small teams, Terraform workspaces with S3 backends are sufficient. For larger organizations, use workspace-per-environment in Terraform Enterprise/Cloud or separate directories with separate state files. The latter is preferred for complex environments with significant differences between dev and prod.
Complete Working Example: AWS VPC + EC2
# main.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "mycompany-terraform-state"
key = "networking/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = local.common_tags
}
}
# โโ Variables โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "project_name" {
description = "Project name for resource naming"
type = string
default = "webapp"
}
variable "environment" {
description = "Environment name"
type = string
default = "dev"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "key_name" {
description = "EC2 key pair name for SSH access"
type = string
default = null
}
# โโ Data Sources โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# โโ Locals โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 2)
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
name_prefix = "${var.project_name}-${var.environment}"
}
# โโ VPC โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-vpc"
})
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-igw"
})
}
# โโ Public Subnets โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-${local.azs[count.index]}"
Type = "public"
})
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-public-rt"
})
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# โโ Security Group โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_security_group" "web" {
name_prefix = "${local.name_prefix}-web-"
vpc_id = aws_vpc.main.id
description = "Security group for web servers"
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
dynamic "ingress" {
for_each = var.key_name != null ? [1] : []
content {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-sg"
})
lifecycle {
create_before_destroy = true
}
}
# โโ EC2 Instance โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.web.id]
key_name = var.key_name
iam_instance_profile = aws_iam_instance_profile.web.name
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
environment = var.environment
}))
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
delete_on_termination = true
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-root"
})
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # IMDSv2
http_put_response_hop_limit = 1
}
monitoring = var.environment == "prod"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-01"
})
}
# โโ IAM Role for EC2 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
resource "aws_iam_role" "web" {
name = "${local.name_prefix}-web-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
tags = local.common_tags
}
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.web.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "web" {
name = "${local.name_prefix}-web-profile"
role = aws_iam_role.web.name
}
# โโ Outputs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "security_group_id" {
description = "Web security group ID"
value = aws_security_group.web.id
}
output "instance_id" {
description = "EC2 instance ID"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "EC2 instance public IP"
value = aws_instance.web.public_ip
}
user_data.sh
#!/bin/bash
# user_data.sh โ EC2 bootstrap script
set -euo pipefail
# Install required packages
dnf update -y
dnf install -y nginx aws-cli amazon-cloudwatch-agent
# Configure Nginx
cat > /etc/nginx/conf.d/default.conf <<'NGINX'
server {
listen 80;
server_name _;
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
location / {
root /usr/share/nginx/html;
index index.html;
}
}
NGINX
# Create index page
mkdir -p /usr/share/nginx/html
cat > /usr/share/nginx/html/index.html <
Variable Types and Validation
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}
variable "instance_config" {
type = object({
type = string
count = number
root_volume = optional(number, 20)
encrypted = optional(bool, true)
tags = optional(map(string), {})
})
description = "Instance configuration"
default = {
type = "t3.micro"
count = 1
root_volume = 20
encrypted = true
}
}
variable "allowed_ports" {
type = list(number)
description = "List of allowed ingress ports"
default = [80, 443]
}
variable "tags" {
type = map(string)
description = "Common tags for all resources"
default = {}
}
Common Commands Reference
| Command | Description |
|---|---|
terraform init | Initialize working directory, download providers and modules |
terraform init -upgrade | Upgrade provider and module versions |
terraform plan | Show execution plan without making changes |
terraform plan -out=tfplan | Save plan to file for later application |
terraform apply | Execute planned changes |
terraform apply tfplan | Apply saved plan file |
terraform apply -auto-approve | Apply without interactive approval (CI/CD) |
terraform destroy | Remove all resources defined in configuration |
terraform validate | Validate configuration syntax |
terraform fmt | Format configuration files to canonical style |
terraform fmt -check -recursive | Check formatting in CI (fails if not formatted) |
terraform show | Show state or plan in human-readable format |
terraform state list | List all resources in state |
terraform state show aws_instance.web | Show details of a specific resource |
terraform state mv OLD NEW | Move/rename a resource in state |
terraform import aws_instance.web i-12345 | Import existing resource into state |
terraform workspace list | List all workspaces |
terraform workspace new dev | Create a new workspace |
terraform workspace select dev | Switch to an existing workspace |
terraform output | Show all outputs |
terraform output -json | Show outputs in JSON format |
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Resource name | Lowercase with underscores | aws_instance.web_server |
| Variable name | Descriptive, lowercase with underscores | instance_type |
| Module directory | Lowercase, hyphenated | modules/vpc-endpoint |
| AWS resource name | {project}-{env}-{resource}-{index} | webapp-prod-web-01 |
| State file key | {component}/terraform.tfstate | vpc/terraform.tfstate |
| File structure | Separate by concern | main.tf, variables.tf, outputs.tf, backend.tf, versions.tf |
Recommended File Structure
terraform/
โโโ modules/
โ โโโ vpc/
โ โ โโโ main.tf
โ โ โโโ variables.tf
โ โ โโโ outputs.tf
โ โ โโโ README.md
โ โ โโโ examples/
โ โ โโโ complete/
โ โ โโโ main.tf
โ โ โโโ variables.tf
โ โโโ security-group/
โ โโโ main.tf
โ โโโ variables.tf
โ โโโ outputs.tf
โโโ environments/
โ โโโ dev/
โ โ โโโ main.tf
โ โ โโโ variables.tf
โ โ โโโ terraform.tfvars
โ โ โโโ backend.tf
โ โโโ staging/
โ โ โโโ ...
โ โโโ prod/
โ โโโ ...
โโโ global/
โโโ iam/
โ โโโ main.tf
โ โโโ variables.tf
โ โโโ backend.tf
โโโ dns/
โโโ ...
Terraform Cloud/Enterprise Overview
| Feature | Open Source | Terraform Cloud | Terraform Enterprise |
|---|---|---|---|
| Remote state storage | S3, GCS, etc. (manual setup) | Built-in | Built-in, self-hosted |
| State locking | DynamoDB (manual setup) | Built-in | Built-in |
| Web UI | No | Yes | Yes |
| VCS integration | No | Yes | Yes |
| Approval workflows | No | Yes | Yes |
| Module registry | No | Private registry | Private registry |
| Sentinel policies | No | Available (Team+) | Yes |
| SSO / SAML | No | Available (Business) | Yes |
| Self-hosted | Yes | No (SaaS) | Yes |
| Audit logging | No | Available | Yes |
Recommendation: For teams of 5+ managing production infrastructure, Terraform Cloud's free tier (500 resources, 5 users) is a significant upgrade from local state. Terraform Enterprise is justified when you require self-hosting for compliance or need Sentinel policy enforcement.