Cloud Native 28 min read

Terraform in Practice: From IaC Basics to Production

This article walks readers through using Terraform for Infrastructure as Code, covering installation, core concepts, workflow, remote state management, modular design, variable handling, sensitive data protection, production best practices, troubleshooting, and advanced topics such as Terragrunt, CDK, policy-as-code, testing, multi‑cloud deployment, and import strategies.

Ops Community
Ops Community
Ops Community
Terraform in Practice: From IaC Basics to Production

Terraform Basics

Install Terraform (Linux/macOS via brew install terraform, Ubuntu/Debian via

wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip && unzip terraform_1.6.0_linux_amd64.zip && sudo mv terraform /usr/local/bin/

). Verify with terraform version (v1.6.0).

Core Concepts

Provider : Configures API access for a cloud. Example for AWS:

provider "aws" {
  region     = "us-east-1"
  access_key = "your-access-key"
  secret_key = "your-secret-key"
}

Resource : Declares an infrastructure component. Example EC2 instance:

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags = {
    Name = "web-server"
  }
}

Data Source : Read‑only lookup, e.g., latest Ubuntu AMI.

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

Variable : Parameterizes configuration. Example with validation:

variable "environment" {
  description = "Environment name"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment must be dev, staging, or prod."
  }
}

Output : Exposes resource attributes.

output "instance_ip" {
  description = "EC2 public IP"
  value       = aws_instance.web.public_ip
}

Typical Workflow

Write .tf files → terraform init → terraform plan → terraform apply → resources created/updated
                                            |
                                            v
                                      terraform destroy (optional)

Basic Configuration Example

Create a project directory and a main.tf that defines required Terraform version, provider, remote S3 backend, variables, and a VPC with subnets, IGW, route tables, security group, and an EC2 instance. The full file is omitted for brevity.

Initialize

terraform init

Output shows backend initialization and provider plugin installation (e.g., hashicorp/aws v5.0.0).

Plan

terraform plan
terraform plan -var-file="prod.tfvars"

The command lists resources that will be created or updated.

Apply

terraform apply
terraform apply -auto-approve
terraform apply -var="environment=prod" -var="instance_type=t3.medium"

Destroy

terraform destroy
terraform destroy -auto-approve

State Management

Local vs Remote State

Local state (default) stores terraform.tfstate in the working directory – suitable only for development/testing.

Remote state (production) stores state in a shared backend such as S3 with DynamoDB locking.

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "env:/dev/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

S3 Remote State Setup

# Create bucket
aws s3 mb s3://my-terraform-state
# Enable versioning
aws s3api put-bucket-versioning --bucket my-terraform-state --versioning-configuration Status=Enabled
# Enable server‑side encryption
aws s3api put-bucket-encryption --bucket my-terraform-state --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefaults":{"SSEAlgorithm":"AES256"}}]}'
# Create DynamoDB lock table
aws dynamodb create-table --table-name terraform-locks --attribute-definitions AttributeName=LockID,AttributeType=S --key-schema AttributeName=LockID,AttributeType=S --billing-mode PAY_PER_REQUEST

State Locking

Terraform automatically acquires a lock during apply. If a lock exists, the CLI prints an error similar to:

Error: Error acquiring the state lock
Lock ID: <em>LOCK_ID</em>
Who: user@hostname
Operation: OperationTypeApply
Time: 2025-01-15T10:00:00Z

Force unlock can be performed with terraform force-unlock <lock_id> (use with caution).

State Operations

# List resources
terraform state list
# Rename a resource
terraform state mv aws_instance.web aws_instance.app
# Remove a resource from state
terraform state rm aws_instance.old
# Pull remote state to a local file
terraform state pull > terraform.tfstate
# Push a local state file to remote
terraform state push terraform.tfstate

Modularity

Module Structure

modules/
└── vpc/
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    └── versions.tf

VPC Module Example

variable "environment" { type = string }
variable "vpc_cidr" { type = string }
variable "availability_zones" { type = list(string) }

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = {
    Name        = "vpc-${var.environment}"
    Environment = var.environment
  }
}

resource "aws_subnet" "public" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = var.availability_zones[count.index]
  map_public_ip_on_launch = true
  tags = { Name = "subnet-public-${count.index + 1}" }
}

output "vpc_id" { value = aws_vpc.main.id }
output "vpc_cidr" { value = aws_vpc.main.cidr_block }
output "public_subnet_ids" { value = aws_subnet.public[*].id }

Using Modules in Root Configuration

terraform {
  required_version = ">= 1.0.0"
  required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } }
  backend "s3" { bucket = "my-terraform-state" key = "project1/terraform.tfstate" region = "us-east-1" }
}

provider "aws" { region = "us-east-1" }

module "vpc" {
  source      = "./modules/vpc"
  environment = "prod"
  vpc_cidr    = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b"]
}

module "ec2" {
  source        = "./modules/ec2"
  vpc_id        = module.vpc.vpc_id
  subnet_ids    = module.vpc.public_subnet_ids
  instance_type = "t3.medium"
  environment   = "prod"
}

Variables and Sensitive Information

Variable Definitions

variable "project_name" { description = "Project name" type = string default = "myapp" }
variable "environment" { description = "Environment" 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 = "t2.micro" }
variable "ami_id" { description = "AMI ID map" type = map(string) default = { us-east-1 = "ami-0c55b159cbfafe1f0" cn-north-1 = "ami-0a36940d" } }
variable "tags" { description = "Resource tags" type = map(string) default = { Project = "myapp" ManagedBy = "Terraform" } }
variable "db_password" { description = "Database password" type = string sensitive = true }

Variable Assignment Methods (priority high→low)

Command‑line -var arguments

Environment variables prefixed with

TF_VAR_
terraform.tfvars

file .auto.tfvars files

Default values in variable blocks

# terraform.tfvars example
project_name = "myapp"
environment   = "prod"
instance_type = "t3.medium"

tags = {
  Environment = "prod"
  CostCenter  = "IT"
}
# Command line
terraform apply -var="environment=prod" -var="instance_type=t3.large"
# Environment variables
export TF_VAR_environment=prod
export TF_VAR_db_password="secret123"
terraform apply

Sensitive Data Handling

# AWS Secrets Manager data source
data "aws_secretsmanager_secret_version" "db_creds" { secret_id = "prod/db/password" }
# SSM Parameter Store data source
data "aws_ssm_parameter" "db_password" { name = "/prod/db/password" }
# Use in a resource
resource "aws_db_instance" "main" {
  # … other arguments …
  password = data.aws_secretsmanager_secret_version.db_creds.secret_string
}

Production Best Practices

Multi‑Environment Directory Layout

environments/
├── dev/
│   ├── main.tf
│   ├── variables.tf
│   ├── terraform.tfvars
│   └── backend.tf
├── staging/
│   ├── main.tf
│   ├── variables.tf
│   ├── terraform.tfvars
│   └── backend.tf
└── prod/
    ├── main.tf
    ├── variables.tf
    ├── terraform.tfvars
    └── backend.tf

Production backend example (S3 + DynamoDB lock):

terraform {
  backend "s3" {
    bucket = "my-terraform-state-prod"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
    encrypt = true
  }
}

Workspaces for Environment Isolation

# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# List and select
terraform workspace list
terraform workspace select prod
terraform workspace show

Dynamic backend key using the workspace name:

terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "env:/${terraform.workspace}/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_instance" "web" {
  tags = {
    Name        = "web-${terraform.workspace}"
    Environment = terraform.workspace
  }
}

CI/CD Integration (GitHub Actions)

# .github/workflows/terraform.yml
name: Terraform
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.6.0
      - name: Terraform Init
        run: terraform init
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      - name: Terraform Format
        run: terraform fmt -check -recursive
      - name: Terraform Validate
        run: terraform validate
      - name: Terraform Plan
        run: terraform plan -no-color
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      - name: Comment PR with Plan
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Terraform plan details: ${{ steps.plan.outputs.stdout }}'
            })
      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Resource Lifecycle Protection

resource "aws_db_instance" "main" {
  # … other arguments …
  lifecycle {
    prevent_destroy = true   # Must remove config before destroying
    ignore_changes  = [password]   # Password updates ignored
  }
}

Cost Estimation Integration (Infracost)

# Add Infracost step to the workflow
- name: Run Infracost
  run: |
    infracost --tfplan "$(terraform show -json tfplan.plan)"
  env:
    INFRACOST_API_KEY: ${{ secrets.INFRACOST_API_KEY }}

FAQ & Troubleshooting

State Drift

When the state file diverges from actual resources:

# 1. List current state
terraform state list
# 2. Inspect real resources (e.g., AWS CLI)
aws ec2 describe-instances
# 3. Import missing resources
terraform import aws_instance.web i-1234567890abcdef0
# 4. Or remove stale resources from state
terraform state rm aws_instance.web

Concurrent Conflicts

Enable DynamoDB locking. If a lock is held, wait or use terraform force-unlock <lock_id> cautiously.

Large State Files

Use remote state, avoid storing large artifacts in the state, and clean old versions (e.g., aws s3 ls s3://my-terraform-state/ --recursive). Import existing resources with terraformer import when needed.

Circular Dependencies

# Problem
resource "aws_eip" "lb" { instance = aws_instance.web.id }
resource "aws_instance" "web" { associate_public_ip_address = true }
# Fix with explicit depends_on
resource "aws_eip" "lb" { instance = aws_instance.web.id }
resource "aws_instance" "web" { depends_on = [aws_eip.lb] }

Common Commands Cheat‑Sheet

# Init
terraform init
terraform init -upgrade

# Plan
terraform plan
terraform plan -out=tfplan
terraform plan -var-file=prod.tfvars

# Apply
terraform apply
terraform apply -auto-approve
terraform apply tfplan

# Destroy
terraform destroy
terraform destroy -target=aws_instance.web

# State
terraform state list
terraform state mv
terraform state rm
terraform state pull > state.json
terraform state push state.json
terraform import

# Workspaces
terraform workspace new dev
terraform workspace select prod
terraform workspace list
terraform workspace show

# Output
terraform output
terraform output instance_ip

# Misc
terraform show
terraform validate
terraform fmt
terraform version

Advanced Topics

Terragrunt

# Install Terragrunt
curl -L https://github.com/gruntwork-io/terragrunt/releases/download/v0.48.0/terragrunt_linux_amd64 -o /usr/local/bin/terragrunt
chmod +x /usr/local/bin/terragrunt

# terragrunt.hcl (root)
remote_state {
  backend = "s3"
  config = {
    bucket         = "my-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}
include { path = find_in_parent_folders() }

Terraform CDK (CDKTF) – TypeScript Example

import { Construct } from "constructs";
import { App, TerraformStack, TerraformOutput } from "cdk.tf";
import { AwsProvider, Instance } from "cdk.tf/aws";

class MyStack extends TerraformStack {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    new AwsProvider(this, "aws", { region: "us-east-1" });
    const instance = new Instance(this, "web", {
      ami: "ami-0c55b159cbfafe1f0",
      instanceType: "t3.micro",
    });
    new TerraformOutput(this, "publicIp", { value: instance.publicIp });
  }
}

const app = new App();
new MyStack(app, "my-stack");
app.synth();

Policy as Code – OPA

# policy.rego
package terraform

deny[msg] {
  resource := input.resource.aws_instance[_]
  not resource.ami
  msg = "AWS Instance must have an AMI specified"
}

deny[msg] {
  resource := input.resource.aws_db_instance[_]
  storage_encrypted := resource.storage_encrypted
  storage_encrypted == false
  msg = "RDS must have encryption enabled"
}
# Test with conftest
conftest test terraform plan.json

Testing with Terratest (Go)

package test

import (
  "testing"
  "time"
  "github.com/gruntwork-io/terratest/modules/terraform"
  "github.com/stretchr/testify/assert"
)

func TestTerraformWebServer(t *testing.T) {
  terraformOptions := &terraform.Options{
    TerraformDir: "../examples/web-server",
    Vars: map[string]interface{}{ "environment": "test" },
  }
  defer terraform.Destroy(t, terraformOptions)
  terraform.InitAndApply(t, terraformOptions)

  instanceId := terraform.Output(t, terraformOptions, "instance_id")
  assert.NotEmpty(t, instanceId)
  publicIp := terraform.Output(t, terraformOptions, "public_ip")
  assert.NotEmpty(t, publicIp)
  time.Sleep(30 * time.Second)
  // Additional verification steps …
}

Dynamic Provider Configuration

variable "aws_profiles" {
  description = "AWS profiles to deploy"
  type = map(object({ region = string, env = string }))
  default = {
    prod = { region = "us-east-1", env = "production" }
    staging = { region = "us-west-2", env = "staging" }
  }
}

provider "aws" {
  for_each = var.aws_profiles
  alias    = each.key
  region   = each.value.region
  assume_role { role_arn = "arn:aws:iam::${each.value.account_id}:role/terraform" }
}

module "vpc_prod" { providers = { aws = aws.prod } source = "./modules/vpc" env = "prod" }
module "vpc_staging" { providers = { aws = aws.staging } source = "./modules/vpc" env = "staging" }

Multi‑Cloud Deployment Example

# providers.tf
provider "aws" { region = "us-east-1" alias = "aws" }
provider "azurerm" { features {} alias = "azure" }

# main.tf
module "aws_vpc" { source = "./modules/vpc" providers = { aws = aws.aws } cidr = "10.0.0.0/16" }
module "azure_vnet" { source = "./modules/vnet" providers = { azurerm = azure.azure } address_space = ["10.1.0.0/16"] }

Import Existing Resources

# Single resource import
terraform import aws_instance.web i-1234567890abcdef0

# Import an entire module with terraformer (generate config, then adjust manually)
terraformer import aws --resources=vpc,subnet,instance --connect=true

Debugging

# Enable detailed logs
export TF_LOG=TRACE
export TF_LOG_PATH=/tmp/terraform.log

# Debug a specific target
terraform apply -target=aws_instance.web

# Inspect state JSON
terraform state pull > /tmp/state.json
jq '.resources[] | select(.type == "aws_instance")' /tmp/state.json

State Versioning & Locking Example

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "env:/${terraform.workspace}/vpc/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    version        = 4   # Enable state versioning
  }
}

Production Checklist Script

#!/bin/bash
set -e

echo "=== Terraform Configuration Checks ==="
# 1. Format
terraform fmt -check -recursive && echo "Format check passed"
# 2. Validate
terraform validate && echo "Validation passed"
# 3. Sensitive info scan (simple grep)
if grep -r "password\|secret\|key" *.tf | grep -v "^#"; then
  echo "Warning: Potential sensitive information found"
  exit 1
fi
echo "Sensitive info check passed"
# 4. State lock check
terraform show > /dev/null && echo "State is healthy"
# 5. Dependency graph preview
terraform graph | head -20 && echo "Dependency check passed"

echo "=== All checks completed ==="
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ci/cdstate-managementsecurityawsmodulesterraformvariablesinfrastructure-as-code
Ops Community
Written by

Ops Community

A leading IT operations community where professionals share and grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.