41 pages ยท 8 sections
Ctrl K
GitHub Portfolio

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

ConceptDescriptionExample
ProviderPlugin that manages resources for a specific platformaws, azurerm, google, kubernetes
ResourceA component of your infrastructure to create and manageaws_instance, aws_s3_bucket
ModuleA reusable container for multiple resourcesmodules/vpc, modules/eks
StateJSON file mapping Terraform config to real infrastructureterraform.tfstate
WorkspaceMultiple named states for the same configurationdev, staging, prod
Data SourceRead-only access to existing infrastructuredata.aws_ami.amazon_linux
VariableInput parameter for configurationvar.instance_type
OutputExported value from a module or root configurationoutput.vpc_id
LocalNamed local value for intermediate computationlocal.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

AspectLocal StateRemote State
Storage locationterraform.tfstate on local diskS3, Terraform Cloud, Consul, GCS, Azure Blob
Team collaborationNot suitable (conflicts, no locking)Designed for team use
State lockingNoneDynamoDB, Terraform Cloud native
State encryptionNone (plain JSON)Server-side encryption (S3 SSE-KMS)
VersioningManualS3 versioning, Terraform Cloud history
Recommended forLocal development onlyAll 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

CommandDescription
terraform initInitialize working directory, download providers and modules
terraform init -upgradeUpgrade provider and module versions
terraform planShow execution plan without making changes
terraform plan -out=tfplanSave plan to file for later application
terraform applyExecute planned changes
terraform apply tfplanApply saved plan file
terraform apply -auto-approveApply without interactive approval (CI/CD)
terraform destroyRemove all resources defined in configuration
terraform validateValidate configuration syntax
terraform fmtFormat configuration files to canonical style
terraform fmt -check -recursiveCheck formatting in CI (fails if not formatted)
terraform showShow state or plan in human-readable format
terraform state listList all resources in state
terraform state show aws_instance.webShow details of a specific resource
terraform state mv OLD NEWMove/rename a resource in state
terraform import aws_instance.web i-12345Import existing resource into state
terraform workspace listList all workspaces
terraform workspace new devCreate a new workspace
terraform workspace select devSwitch to an existing workspace
terraform outputShow all outputs
terraform output -jsonShow outputs in JSON format

Naming Conventions

ElementConventionExample
Resource nameLowercase with underscoresaws_instance.web_server
Variable nameDescriptive, lowercase with underscoresinstance_type
Module directoryLowercase, hyphenatedmodules/vpc-endpoint
AWS resource name{project}-{env}-{resource}-{index}webapp-prod-web-01
State file key{component}/terraform.tfstatevpc/terraform.tfstate
File structureSeparate by concernmain.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

FeatureOpen SourceTerraform CloudTerraform Enterprise
Remote state storageS3, GCS, etc. (manual setup)Built-inBuilt-in, self-hosted
State lockingDynamoDB (manual setup)Built-inBuilt-in
Web UINoYesYes
VCS integrationNoYesYes
Approval workflowsNoYesYes
Module registryNoPrivate registryPrivate registry
Sentinel policiesNoAvailable (Team+)Yes
SSO / SAMLNoAvailable (Business)Yes
Self-hostedYesNo (SaaS)Yes
Audit loggingNoAvailableYes
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.