LabHub

Blog

The Complete Guide to Terraform & Ansible Commands: Everything About IaC and Configuration Management

한국어English日本語中文


1. Introduction: The Era of Infrastructure as Code

1.1 Why IaC and Configuration Management?

In the cloud-native era, managing infrastructure by hand is no longer an option. Click your way through hundreds of servers, dozens of VPCs, and a tangle of security groups and IAM policies in a console, and you are taking the fast route to "snowflake servers" — every server different, like a snowflake. They cannot be reproduced, they are hard to audit, and a single mistake can bring the whole infrastructure down.

Infrastructure as Code (IaC) and Configuration Management (CM) are the industry's answer to that problem.

The de facto standards in these two areas are Terraform and Ansible.

1.2 How Terraform and Ansible Divide the Work

Terraform and Ansible are not competitors but complements. Each owns a clearly different area of responsibility.

┌──────────────────────────────────────────────────────────────────┐
IaC + CM workflow                             │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────┐       ┌─────────────────────────┐      │
│  │     Terraform        │       │       Ansible            │      │
  (Provisioning)      │──────▶│  (Configuration)         │      │
│  │                      │       │                          │      │
│  │  - Create VPC/Subnet │       │  - Install packages      │      │
│  │  - Create EC2/RDS    │       │  - Configure Nginx/Apache││  │  - Create S3 buckets │       │  - Deploy applications   │      │
│  │  - Create IAM roles  │       │  - Security hardening    │      │
│  │  - Security Group    │       │  - Install monitoring    │      │
│  └─────────────────────┘       └─────────────────────────┘      │
│                                                                  │
"Builds the infrastructure"   "Configures the infrastructure"Declarative                   Procedural + DeclarativeState-based                   Agentless (SSH/WinRM)└──────────────────────────────────────────────────────────────────┘

1.3 How This Article Is Organized

This article is organized into three parts.

  1. Part 1 — Terraform: from HCL syntax through the core workflow, state management, workspaces, modules, and advanced features
  2. Part 2 — Ansible: inventory, ad-hoc commands, playbooks, roles, Vault, and Galaxy
  3. Part 3 — Integration and cheat sheets: combining Terraform and Ansible, command cheat sheets, and troubleshooting

Part 1: The Complete Terraform Guide


2. Introducing Terraform and Its Architecture

2.1 What Is Terraform?

Terraform is an open-source IaC tool released by HashiCorp in 2014. You define infrastructure in a declarative language called HCL (HashiCorp Configuration Language), and Terraform compares the current state against the desired configuration and applies only the difference.

In August 2024 HashiCorp was acquired by IBM, and Terraform's license changed to the BSL (Business Source License). The community's response was OpenTofu, under the Linux Foundation. Most of the commands covered in this article work identically in OpenTofu.

2.2 Terraform's Architecture

Terraform's core architecture consists of three pieces: Core + Providers + State.

┌──────────────────────────────────────────────────────────────┐
Terraform Architecture├──────────────────────────────────────────────────────────────┤
│                                                               │
│  ┌──────────────┐                                            │
│  │  .tf FilesHCL Configuration  (desired     │                                           │
│  │   state)      │                                           │
│  └──────┬───────┘                                            │
│         │                                                    │
│         ▼                                                    │
│  ┌──────────────────────────────────────────┐                │
│  │           Terraform Core                  │                │
│  │                                           │                │
│  │  ┌───────────┐    ┌──────────────┐       │                │
│  │  │ Resource   │    │  Dependency   │       │                │
│  │  │ Graph      │    │  Resolution   │       │                │
│  │  └───────────┘    └──────────────┘       │                │
│  │                                           │                │
│  │  ┌───────────┐    ┌──────────────┐       │                │
│  │  │ Plan      │    │  Apply        │       │                │
│  │  │ Engine    │    │  Engine       │       │                │
│  │  └───────────┘    └──────────────┘       │                │
│  └─────────┬───────────────┬────────────────┘                │
│            │               │                                  │
│     ┌──────▼──────┐ ┌─────▼──────────┐                      │
│     │  Providers   │ │  State File     │                      │
│     │              │ │                 │                      │
│     │ - AWS        │ │ terraform.tfstate││     │ - Azure (JSON)          │                      │
│     │ - GCP        │ │                 │                      │
│     │ - Kubernetes │ │ Local / Remote  │                      │
│     │ - 3000+ (S3, GCS, etc.) │                      │
│     └──────────────┘ └─────────────────┘                     │
│                                                               │
└──────────────────────────────────────────────────────────────┘

2.3 Terraform vs OpenTofu vs Pulumi

ItemTerraformOpenTofuPulumi
LicenseBSL 1.1MPL 2.0 (OSS)Apache 2.0
LanguageHCLHCLPython/Go/TS/C#
State managementterraform.tfstateterraform.tfstatePulumi Cloud
Provider ecosystem3,000+Terraform compatible100+
Maintained byHashiCorp (IBM)Linux FoundationPulumi Inc.
CLI commandterraformtofupulumi

3. Installing Terraform and Setting Up the Environment

3.1 Version Management with tfenv

Projects often need different Terraform versions, so using tfenv (Terraform Version Manager) is strongly recommended.

# macOS (Homebrew)
brew install tfenv

# Linux (Git Clone)
git clone https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc

# List the available versions
tfenv list-remote

# Install a specific version
tfenv install 1.9.8
tfenv install 1.10.3

# Set the global default version
tfenv use 1.10.3

# Pin the version per project (the .terraform-version file)
echo "1.9.8" > .terraform-version

# List the installed versions
tfenv list

# Check the current version
terraform version

3.2 Installing Directly

# macOS (Homebrew)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

# Linux (APT)
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# Check the version
terraform version
# Terraform v1.10.3
# on darwin_arm64

3.3 Autocompletion and Editor Setup

# Bash/Zsh autocompletion
terraform -install-autocomplete

# VS Code extension
# - HashiCorp Terraform (official)
# - Terraform Autocomplete

4. The Core Terraform Workflow

Terraform's core workflow has three stages: Write → Plan → Apply. Let us look in detail at the commands that support them.

4.1 terraform init — Initializing the Project

terraform init is the first command in a Terraform project. It downloads provider plugins, downloads modules, and initializes the backend.

# Basic initialization
terraform init

# Main flags
terraform init -upgrade              # Upgrade providers/modules to the newest allowed version
terraform init -reconfigure           # Reconfigure the backend (ignoring existing state)
terraform init -migrate-state         # Migrate state when the backend changes
terraform init -backend=false         # Skip backend initialization (for validation)
terraform init -get=false             # Skip module downloads
terraform init -input=false           # Disable interactive input (for CI/CD)
terraform init -no-color              # Disable colored output (for log parsing)
terraform init -lockfile=readonly     # Forbid changes to .terraform.lock.hcl (for CI)

# Pass backend settings from the CLI (useful in CI/CD)
terraform init \
  -backend-config="bucket=my-tf-state" \
  -backend-config="key=prod/terraform.tfstate" \
  -backend-config="region=ap-northeast-2" \
  -backend-config="dynamodb_table=tf-lock"

The files and directories created after running terraform init:

.terraform/              # Provider plugins and module cache
.terraform.lock.hcl      # Provider version lock file (commit this)

4.2 terraform validate — Validating the Syntax

# Check syntactic validity (available after init)
terraform validate

# JSON output (for CI/CD pipelines)
terraform validate -json

# Example output (success)
# Success! The configuration is valid.

# Example output (failure, JSON)
# {
#   "valid": false,
#   "error_count": 1,
#   "diagnostics": [
#     {
#       "severity": "error",
#       "summary": "Unsupported argument",
#       "detail": "An argument named \"vps_id\" is not expected here. Did you mean \"vpc_id\"?"
#     }
#   ]
# }

4.3 terraform fmt — Formatting the Code

# Format the .tf files in the current directory
terraform fmt

# Format every subdirectory recursively
terraform fmt -recursive

# Show only the files that need changes (for a CI check)
terraform fmt -check

# diff output
terraform fmt -diff

# Using it in a CI/CD pipeline
terraform fmt -check -recursive -diff
# Exit code 0: no formatting changes
# Exit code 3: formatting changes needed

4.4 terraform plan — The Execution Plan

terraform plan is one of Terraform's most important commands. It compares the current state against the configuration and shows you in advance what will change. It makes no change to the real infrastructure.

# A basic plan
terraform plan

# Main flags
terraform plan -out=tfplan              # Save the plan to a file (used by apply)
terraform plan -destroy                 # Review a destroy plan
terraform plan -target=aws_instance.web # Plan only a specific resource
terraform plan -var="instance_type=t3.large"  # Pass a variable
terraform plan -var-file="prod.tfvars"  # Specify a variable file
terraform plan -refresh=false           # Skip refreshing state (faster)
terraform plan -parallelism=20          # Number of concurrent operations (default: 10)
terraform plan -compact-warnings        # Condense warning messages
terraform plan -no-color                # Disable colored output
terraform plan -input=false             # Disable interactive input
terraform plan -json                    # JSON output (for automation)
terraform plan -detailed-exitcode       # Detailed exit codes
# Exit code 0: no changes
# Exit code 1: an error occurred
# Exit code 2: there are changes

# The recommended pattern for a CI/CD pipeline
terraform plan -out=tfplan -input=false -no-color -detailed-exitcode

How to read the symbols in the plan output:

# + create    (create a resource)
# - destroy   (destroy a resource)
# ~ update    (modify a resource, in place)
# -/+ replace (destroy then recreate a resource)
# <= read     (read a data source)

4.5 terraform apply — Applying the Changes

# A basic apply (plan, then a confirmation prompt)
terraform apply

# Apply from a saved plan file (no confirmation prompt)
terraform apply tfplan

# Auto-approve (for CI/CD; use with care!)
terraform apply -auto-approve

# Main flags
terraform apply -target=aws_instance.web      # Apply only a specific resource
terraform apply -var="instance_type=t3.large"  # Pass a variable
terraform apply -var-file="prod.tfvars"        # Specify a variable file
terraform apply -parallelism=20                # Number of concurrent operations
terraform apply -refresh=false                 # Skip refreshing state
terraform apply -replace=aws_instance.web      # Force recreation of a resource (replaces taint)
terraform apply -lock=false                    # Disable state locking (not recommended)
terraform apply -lock-timeout=5m               # How long to wait for the state lock

# A safe CI/CD pattern (plan → save → apply)
terraform plan -out=tfplan -input=false
# ... review ...
terraform apply tfplan

4.6 terraform destroy — Destroying the Infrastructure

# Destroy every resource (with a confirmation prompt)
terraform destroy

# Auto-approve
terraform destroy -auto-approve

# Destroy only a specific resource
terraform destroy -target=aws_instance.web

# Specify a variable file
terraform destroy -var-file="prod.tfvars"

# Review the destroy plan first
terraform plan -destroy

4.7 terraform output — Reading Output Values

# Show every output value
terraform output

# A specific output value
terraform output vpc_id

# The raw value (no quotes, for scripts)
terraform output -raw vpc_id

# JSON output
terraform output -json

# Using it from another Terraform project or from Ansible
VPC_ID=$(terraform output -raw vpc_id)
echo "VPC ID: $VPC_ID"

5. Terraform State Management

5.1 What Is State?

Terraform state (terraform.tfstate) is a JSON file recording the current state of the infrastructure Terraform manages. Terraform uses it to work out the difference between the configuration (the desired state) and the real infrastructure (the current state).

The state file contains resource IDs, attribute values, and metadata, and sensitive information such as passwords and access keys may be stored in plaintext, so you must use an encrypted remote backend (S3 + KMS, for example).

5.2 Configuring a Remote Backend

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"    # DynamoDB table for state locking
    kms_key_id     = "alias/terraform"   # KMS encryption key
  }
}

5.3 The terraform state Commands

# ── List the resources ──
terraform state list
# aws_vpc.main
# aws_subnet.public[0]
# aws_subnet.public[1]
# aws_instance.web
# aws_db_instance.main

# Filtering
terraform state list aws_subnet.*
terraform state list module.network

# ── Show resource details ──
terraform state show aws_instance.web
# resource "aws_instance" "web" {
#     ami                    = "ami-0c55b159cbfafe1f0"
#     arn                    = "arn:aws:ec2:ap-northeast-2:123456789:instance/i-0abc123def456"
#     instance_type          = "t3.medium"
#     private_ip             = "10.0.1.50"
#     public_ip              = "54.180.xxx.xxx"
#     ...
# }

# ── Move a resource (rename / move into a module) ──
# Rename a resource (the code has to change too)
terraform state mv aws_instance.web aws_instance.app_server

# Move it into a module
terraform state mv aws_vpc.main module.network.aws_vpc.main

# Move it into a different state file
terraform state mv -state-out=other.tfstate aws_instance.web aws_instance.web

# Dry run (check without making the change)
terraform state mv -dry-run aws_instance.web aws_instance.app

# ── Remove a resource from state (the real infrastructure stays) ──
terraform state rm aws_instance.web
# Terraform no longer manages this resource
# The real EC2 instance is not deleted

# ── Download the remote state ──
terraform state pull > terraform.tfstate.backup

# ── Upload local state to the remote ──
terraform state push terraform.tfstate

# Force the upload (ignores a serial number clash - dangerous!)
terraform state push -force terraform.tfstate

# ── Release a state lock ──
# When an abnormal exit left the lock in place
terraform force-unlock LOCK_ID
# LOCK_ID is shown in the error message

# Force the release without a confirmation prompt
terraform force-unlock -force LOCK_ID

5.4 terraform import — Bringing In Existing Resources

To bring a resource you created by hand under Terraform's management, use import.

# The traditional CLI import (before Terraform 1.5)
terraform import aws_instance.web i-0abc123def456
terraform import aws_vpc.main vpc-0abc123def
terraform import 'aws_subnet.public[0]' subnet-0abc123def
terraform import module.network.aws_vpc.main vpc-0abc123def

The import block in Terraform 1.5+ (declarative import, recommended):

# import.tf
import {
  to = aws_instance.web
  id = "i-0abc123def456"
}

import {
  to = aws_vpc.main
  id = "vpc-0abc123def"
}
# A plan based on the import block (generates the code automatically)
terraform plan -generate-config-out=generated.tf

# Review the generated code, then apply
terraform apply

6. Terraform Workspace

Workspaces are useful when you manage several environments (dev/staging/prod) from the same configuration. Each workspace has its own state file.

# ── List the workspaces ──
terraform workspace list
# * default
#   dev
#   staging
#   prod

# ── Create a new workspace ──
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# ── Switch workspaces ──
terraform workspace select prod

# ── Show the current workspace ──
terraform workspace show
# prod

# ── Delete a workspace (only if its state is empty) ──
terraform workspace delete dev

# Force the deletion (even when state remains)
terraform workspace delete -force dev

A pattern for using workspaces from HCL:

# Branch the instance type by environment
locals {
  instance_type = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.large"
  }
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.instance_type[terraform.workspace]

  tags = {
    Name        = "app-${terraform.workspace}"
    Environment = terraform.workspace
  }
}

7. Managing Terraform Modules

7.1 Module Structure

modules/
├── network/
│   ├── main.tf       # VPC, Subnet, IGW, NAT
│   ├── variables.tf  # Input variables
│   ├── outputs.tf    # Output values
│   └── README.md
├── compute/
│   ├── main.tf       # EC2, ASG, ALB
│   ├── variables.tf
│   └── outputs.tf
└── database/
    ├── main.tf       # RDS, ElastiCache
    ├── variables.tf
    └── outputs.tf

7.2 Module Source Types

# A local module
module "network" {
  source = "./modules/network"
}

# Terraform Registry
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.16.0"
}

# GitHub
module "vpc" {
  source = "github.com/terraform-aws-modules/terraform-aws-vpc?ref=v5.16.0"
}

# S3 Bucket
module "vpc" {
  source = "s3::https://s3-ap-northeast-2.amazonaws.com/my-modules/vpc.zip"
}

# Git (SSH)
module "vpc" {
  source = "git::ssh://git@github.com/myorg/modules.git//network?ref=v1.0.0"
}
# Download/update the modules
terraform init -upgrade

# Show the providers a module uses
terraform providers

# Update the provider lock file (covering several platforms)
terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_arm64

# Provider mirror (air-gapped environment)
terraform providers mirror /path/to/mirror

8. Advanced Terraform Features

8.1 terraform console — The Interactive Console

# Evaluate expressions interactively
terraform console

# Example usage
> var.instance_type
"t3.medium"

> length(var.subnet_ids)
3

> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"

> formatdate("YYYY-MM-DD", timestamp())
"2026-03-01"

> [for s in var.subnet_ids : upper(s)]
["SUBNET-AAA", "SUBNET-BBB", "SUBNET-CCC"]

# Exit: Ctrl+D or exit

8.2 terraform graph — The Dependency Graph

# Print the dependency graph in DOT format
terraform graph

# Render an image with Graphviz
terraform graph | dot -Tpng > graph.png
terraform graph | dot -Tsvg > graph.svg

# A plan-based graph
terraform graph -type=plan

# A graph centered on a specific resource
terraform graph -draw-cycles

8.3 Other Utility Commands

# Show the provider tree used by the current configuration
terraform providers

# Print the provider schema as JSON
terraform providers schema -json

# Check the Terraform version
terraform version

# JSON output
terraform version -json

# Show the location of the Terraform configuration file
terraform -help

# Help for a specific command
terraform plan -help

9. An HCL Syntax Cheat Sheet

9.1 Variables (Input Variables)

# variables.tf

# Basic types
variable "region" {
  description = "AWS Region"
  type        = string
  default     = "ap-northeast-2"
}

variable "instance_count" {
  description = "Number of instances"
  type        = number
  default     = 2
}

variable "enable_monitoring" {
  description = "Enable CloudWatch monitoring"
  type        = bool
  default     = true
}

# Composite types - List
variable "availability_zones" {
  type    = list(string)
  default = ["ap-northeast-2a", "ap-northeast-2c"]
}

# Composite types - Map
variable "instance_types" {
  type = map(string)
  default = {
    dev  = "t3.micro"
    prod = "t3.large"
  }
}

# Composite types - Object
variable "database_config" {
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    multi_az       = bool
    storage_gb     = number
  })
  default = {
    engine         = "mysql"
    engine_version = "8.0"
    instance_class = "db.t3.medium"
    multi_az       = true
    storage_gb     = 100
  }
}

# A sensitive variable
variable "db_password" {
  description = "Database master password"
  type        = string
  sensitive   = true  # Masked in plan/apply output
}

# Validation Rule
variable "instance_type" {
  type = string
  validation {
    condition     = can(regex("^t3\\.", var.instance_type))
    error_message = "Instance type must start with t3."
  }
}

# Nullable
variable "override_name" {
  type     = string
  default  = null
  nullable = true
}

Ways to pass variable values (highest precedence first):

# 1. The CLI -var flag (highest precedence)
terraform apply -var="region=us-east-1"

# 2. The -var-file flag
terraform apply -var-file="prod.tfvars"

# 3. *.auto.tfvars (loaded automatically)
# terraform.tfvars, *.auto.tfvars

# 4. Environment variables (the TF_VAR_ prefix)
export TF_VAR_region="us-east-1"
export TF_VAR_db_password="SuperSecret123!"

# 5. The default value

9.2 Locals (Local Variables)

locals {
  project_name = "my-app"
  environment  = terraform.workspace

  common_tags = {
    Project     = local.project_name
    Environment = local.environment
    ManagedBy   = "terraform"
    Team        = "platform"
  }

  # A conditional value
  is_prod = local.environment == "prod"

  # A computed value
  name_prefix = "${local.project_name}-${local.environment}"
}

resource "aws_instance" "app" {
  # ...
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-app"
  })
}

9.3 Outputs (Output Values)

# 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 "db_endpoint" {
  description = "RDS endpoint"
  value       = aws_db_instance.main.endpoint
  sensitive   = true  # Masks a sensitive output value
}

# Referencing it from another module
# module.network.vpc_id

9.4 count and for_each

# count — create N copies of the same resource
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]

  tags = {
    Name = "public-subnet-${count.index}"
  }
}

# References: aws_subnet.public[0], aws_subnet.public[1]

# for_each — iterate over a map or set (recommended)
resource "aws_iam_user" "users" {
  for_each = toset(["alice", "bob", "charlie"])
  name     = each.value
}

# Map-based for_each
variable "instances" {
  default = {
    web = { type = "t3.small", az = "ap-northeast-2a" }
    api = { type = "t3.medium", az = "ap-northeast-2c" }
    worker = { type = "t3.large", az = "ap-northeast-2a" }
  }
}

resource "aws_instance" "servers" {
  for_each      = var.instances
  ami           = data.aws_ami.ubuntu.id
  instance_type = each.value.type
  availability_zone = each.value.az

  tags = {
    Name = "${each.key}-server"
  }
}

# References: aws_instance.servers["web"], aws_instance.servers["api"]

9.5 dynamic Block

# Generate security group rules dynamically
variable "ingress_rules" {
  default = [
    { port = 80,  cidr = "0.0.0.0/0",   description = "HTTP" },
    { port = 443, cidr = "0.0.0.0/0",   description = "HTTPS" },
    { port = 22,  cidr = "10.0.0.0/8",  description = "SSH (internal)" },
  ]
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web server security group"
  vpc_id      = aws_vpc.main.id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = [ingress.value.cidr]
      description = ingress.value.description
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

9.6 The lifecycle Meta-Argument

resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  lifecycle {
    # Create the new resource before destroying the old one (minimizes downtime)
    create_before_destroy = true

    # Ignore changes to specific attributes (tags edited outside Terraform, etc.)
    ignore_changes = [
      tags["LastModified"],
      ami,
    ]

    # Prevent deletion (so destroy cannot happen by accident)
    prevent_destroy = true

    # Pre/post conditions
    precondition {
      condition     = var.instance_type != "t3.nano"
      error_message = "t3.nano is too small for this application."
    }

    postcondition {
      condition     = self.public_ip != ""
      error_message = "Instance must have a public IP."
    }

    # Replacement trigger (the resource is recreated when the value changes)
    replace_triggered_by = [
      aws_ami.app_ami.id
    ]
  }
}

10. A Practical Terraform Example — AWS VPC + EC2 + RDS

10.1 Project Structure

terraform-aws-project/
├── main.tf           # Provider and backend configuration
├── variables.tf      # Input variable definitions
├── outputs.tf        # Output value definitions
├── terraform.tfvars  # Variable values (gitignored)
├── network.tf        # VPC, Subnet, IGW, NAT, Route Table
├── compute.tf        # EC2, Security Group, Key Pair
├── database.tf       # RDS, Subnet Group
├── data.tf           # Data sources (AMI lookups, etc.)
└── versions.tf       # Provider/Terraform version constraints

10.2 The Full Code

# versions.tf
terraform {
  required_version = ">= 1.9.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.80"
    }
  }

  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

# main.tf
provider "aws" {
  region = var.region

  default_tags {
    tags = {
      Project     = var.project_name
      Environment = terraform.workspace
      ManagedBy   = "terraform"
    }
  }
}

# variables.tf
variable "region" {
  type    = string
  default = "ap-northeast-2"
}

variable "project_name" {
  type    = string
  default = "myapp"
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "azs" {
  type    = list(string)
  default = ["ap-northeast-2a", "ap-northeast-2c"]
}

variable "db_password" {
  type      = string
  sensitive = true
}

# data.tf
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"] # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
  }
}

# network.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = { Name = "${var.project_name}-vpc" }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "${var.project_name}-igw" }
}

resource "aws_subnet" "public" {
  count                   = length(var.azs)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone       = var.azs[count.index]
  map_public_ip_on_launch = true

  tags = { Name = "${var.project_name}-public-${count.index}" }
}

resource "aws_subnet" "private" {
  count             = length(var.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = var.azs[count.index]

  tags = { Name = "${var.project_name}-private-${count.index}" }
}

resource "aws_eip" "nat" {
  domain = "vpc"
  tags   = { Name = "${var.project_name}-nat-eip" }
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id

  tags = { Name = "${var.project_name}-nat" }
  depends_on = [aws_internet_gateway.main]
}

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 = { Name = "${var.project_name}-public-rt" }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }

  tags = { Name = "${var.project_name}-private-rt" }
}

resource "aws_route_table_association" "public" {
  count          = length(var.azs)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  count          = length(var.azs)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private.id
}

# compute.tf
resource "aws_security_group" "web" {
  name        = "${var.project_name}-web-sg"
  description = "Security group for web servers"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTP"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "HTTPS"
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
    description = "SSH internal"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "${var.project_name}-web-sg" }
}

resource "aws_instance" "web" {
  count                  = 2
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.small"
  subnet_id              = aws_subnet.public[count.index % length(var.azs)].id
  vpc_security_group_ids = [aws_security_group.web.id]

  root_block_device {
    volume_size = 30
    volume_type = "gp3"
    encrypted   = true
  }

  tags = { Name = "${var.project_name}-web-${count.index}" }

  lifecycle {
    create_before_destroy = true
    ignore_changes        = [ami]
  }
}

# database.tf
resource "aws_security_group" "db" {
  name        = "${var.project_name}-db-sg"
  description = "Security group for RDS"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
    description     = "MySQL from web servers"
  }

  tags = { Name = "${var.project_name}-db-sg" }
}

resource "aws_db_subnet_group" "main" {
  name       = "${var.project_name}-db-subnet-group"
  subnet_ids = aws_subnet.private[*].id

  tags = { Name = "${var.project_name}-db-subnet-group" }
}

resource "aws_db_instance" "main" {
  identifier           = "${var.project_name}-mysql"
  engine               = "mysql"
  engine_version       = "8.0"
  instance_class       = "db.t3.medium"
  allocated_storage    = 100
  storage_type         = "gp3"
  storage_encrypted    = true
  db_name              = "myapp"
  username             = "admin"
  password             = var.db_password
  multi_az             = true
  db_subnet_group_name = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]
  skip_final_snapshot  = false
  final_snapshot_identifier = "${var.project_name}-final-snapshot"
  backup_retention_period   = 7

  tags = { Name = "${var.project_name}-mysql" }

  lifecycle {
    prevent_destroy = true
  }
}

# outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "web_instance_ips" {
  value = aws_instance.web[*].public_ip
}

output "rds_endpoint" {
  value     = aws_db_instance.main.endpoint
  sensitive = true
}

Part 2: The Complete Ansible Guide


11. Introducing Ansible and Its Architecture

11.1 What Is Ansible?

Ansible is an open-source automation tool maintained by Red Hat, created by Michael DeHaan in 2012. It handles configuration management, application deployment, and task automation in a single tool.

Ansible's core philosophy:

11.2 Ansible's Architecture

┌──────────────────────────────────────────────────────────────────┐
Ansible Architecture├──────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌─────────────────────────────────────────────────┐             │
│  │              Control Node                        │             │
│  │                                                  │             │
│  │  ┌────────────┐  ┌──────────┐  ┌────────────┐  │             │
│  │  │ Playbook   │  │ Inventory │  │  Modules    │  │             │
│  │   (.yml) (hosts)  (2,500+)   │  │             │
│  │  └─────┬──────┘  └────┬─────┘  └──────┬─────┘  │             │
│  │        │              │               │         │             │
│  │        ▼              ▼               ▼         │             │
│  │  ┌──────────────────────────────────────────┐   │             │
│  │  │            Ansible Engine                 │   │             │
│  │  │                                           │   │             │
│  │  │  - Task Execution                         │   │             │
│  │  │  - Variable Resolution                    │   │             │
│  │  │  - Connection Management (SSH/WinRM)      │   │             │
│  │  │  - Fact Gathering                         │   │             │
│  │  └─────────────┬─────────────────────────────┘   │             │
│  └────────────────┼─────────────────────────────────┘             │
│                   │                                               │
SSH / WinRM (No Agent!)│                   │                                               │
│     ┌─────────────┼──────────────────────────────┐               │
│     │             ▼                              │               │
│     │  ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐  │               │
│     │  │ Web1 │  │ Web2 │  │ DB1  │  │ DB2  │  │               │
│     │  └──────┘  └──────┘  └──────┘  └──────┘  │               │
│     │            Managed Nodes                   │               │
│     └────────────────────────────────────────────┘               │
│                                                                   │
└──────────────────────────────────────────────────────────────────┘

12. Installing Ansible

# ── Install with pip (recommended) ──
pip install ansible

# Install a specific version
pip install ansible==10.6.0

# Install ansible-core only (a minimal install)
pip install ansible-core

# ── macOS (Homebrew) ──
brew install ansible

# ── Ubuntu/Debian ──
sudo apt update
sudo apt install ansible

# ── RHEL/CentOS/Fedora ──
sudo dnf install ansible

# ── Check the version ──
ansible --version
# ansible [core 2.17.7]
#   config file = /etc/ansible/ansible.cfg
#   configured module search path = ['~/.ansible/plugins/modules']
#   ansible python module location = /usr/lib/python3.12/site-packages/ansible
#   python version = 3.12.8

# ── Install ansible-navigator (TUI, supports execution environments) ──
pip install ansible-navigator

13. The Ansible Inventory

13.1 A Static Inventory (INI Format)

# inventory/hosts.ini

# Individual hosts
web1.example.com
web2.example.com

# Group definitions
[webservers]
web1.example.com ansible_host=10.0.1.10
web2.example.com ansible_host=10.0.1.11

[dbservers]
db1.example.com ansible_host=10.0.2.10 ansible_port=2222
db2.example.com ansible_host=10.0.2.11

[monitoring]
grafana.example.com

# Group variables
[webservers:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/web_key.pem
http_port=80

[dbservers:vars]
ansible_user=ec2-user
ansible_ssh_private_key_file=~/.ssh/db_key.pem

# Groups of groups (children)
[production:children]
webservers
dbservers
monitoring

[production:vars]
env=production

# Host range patterns
[loadbalancers]
lb[01:03].example.com  # lb01, lb02, lb03

13.2 A Static Inventory (YAML Format)

# inventory/hosts.yml
all:
  children:
    production:
      children:
        webservers:
          hosts:
            web1.example.com:
              ansible_host: 10.0.1.10
            web2.example.com:
              ansible_host: 10.0.1.11
          vars:
            ansible_user: ubuntu
            ansible_ssh_private_key_file: ~/.ssh/web_key.pem
            http_port: 80

        dbservers:
          hosts:
            db1.example.com:
              ansible_host: 10.0.2.10
            db2.example.com:
              ansible_host: 10.0.2.11
          vars:
            ansible_user: ec2-user
      vars:
        env: production

    staging:
      children:
        webservers_stg:
          hosts:
            stg-web1.example.com:
              ansible_host: 10.1.1.10

13.3 A Dynamic Inventory

A dynamic inventory pulls the host list from a cloud API in real time.

# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - ap-northeast-2

keyed_groups:
  - key: tags.Environment
    prefix: env
  - key: instance_type
    prefix: type
  - key: placement.availability_zone
    prefix: az

filters:
  tag:ManagedBy: ansible
  instance-state-name: running

compose:
  ansible_host: private_ip_address
# Test the dynamic inventory
ansible-inventory -i inventory/aws_ec2.yml --list
ansible-inventory -i inventory/aws_ec2.yml --graph
# Show the hosts in the inventory
ansible-inventory -i inventory/hosts.yml --list
ansible-inventory -i inventory/hosts.yml --graph

# Show only a specific group
ansible-inventory -i inventory/hosts.yml --graph webservers

# Show a host's variables
ansible-inventory -i inventory/hosts.yml --host web1.example.com

# JSON output
ansible-inventory -i inventory/hosts.yml --list --yaml

14. Ansible Ad-Hoc Commands

An ad-hoc command is a one-off command you run in a single line, without a playbook. It is useful for quick checks and simple tasks.

14.1 Basic Syntax

ansible [host pattern] -i [inventory] -m [module] -a "[arguments]" [options]

14.2 Examples by Module

# ── ping: check connectivity ──
ansible all -i inventory/hosts.yml -m ping
ansible webservers -i inventory/hosts.yml -m ping

# ── command: run a command (the default module, no shell features) ──
ansible webservers -m command -a "uptime"
ansible webservers -m command -a "df -h"
ansible webservers -m command -a "free -m"

# ── shell: run a shell command (pipes and redirects supported) ──
ansible webservers -m shell -a "ps aux | grep nginx | wc -l"
ansible webservers -m shell -a "cat /etc/os-release | head -5"
ansible dbservers -m shell -a "mysql -e 'SHOW DATABASES;'"

# ── copy: copy a file ──
ansible webservers -m copy -a "src=./app.conf dest=/etc/nginx/conf.d/app.conf owner=root group=root mode=0644"

# ── file: manage files and directories ──
ansible webservers -m file -a "path=/opt/app state=directory owner=www-data mode=0755"
ansible webservers -m file -a "path=/tmp/old_file state=absent"
ansible webservers -m file -a "src=/etc/nginx/sites-available/app dest=/etc/nginx/sites-enabled/app state=link"

# ── apt: package management (Debian/Ubuntu) ──
ansible webservers -m apt -a "name=nginx state=present update_cache=yes" --become
ansible webservers -m apt -a "name=nginx state=latest" --become
ansible webservers -m apt -a "name=nginx state=absent" --become
ansible webservers -m apt -a "upgrade=dist" --become

# ── yum/dnf: package management (RHEL/CentOS) ──
ansible dbservers -m dnf -a "name=mysql-server state=present" --become

# ── service/systemd: service management ──
ansible webservers -m service -a "name=nginx state=started enabled=yes" --become
ansible webservers -m service -a "name=nginx state=restarted" --become
ansible webservers -m service -a "name=nginx state=stopped" --become
ansible webservers -m systemd -a "name=nginx state=reloaded daemon_reload=yes" --become

# ── user: user management ──
ansible all -m user -a "name=deploy state=present groups=sudo shell=/bin/bash" --become
ansible all -m user -a "name=olduser state=absent remove=yes" --become

# ── setup: gather facts (system information) ──
ansible webservers -m setup
ansible webservers -m setup -a "filter=ansible_os_family"
ansible webservers -m setup -a "filter=ansible_memtotal_mb"
ansible webservers -m setup -a "filter=ansible_distribution*"

# ── lineinfile: manage a line inside a file ──
ansible webservers -m lineinfile -a "path=/etc/ssh/sshd_config regexp='^PermitRootLogin' line='PermitRootLogin no'" --become

# ── cron: manage cron jobs ──
ansible webservers -m cron -a "name='log cleanup' minute='0' hour='3' job='find /var/log -name \"*.gz\" -mtime +30 -delete'"

# ── get_url: download a file from a URL ──
ansible webservers -m get_url -a "url=https://example.com/app.tar.gz dest=/tmp/app.tar.gz"

# ── git: clone/pull a Git repository ──
ansible webservers -m git -a "repo=https://github.com/myorg/myapp.git dest=/opt/myapp version=main"

14.3 Main Ad-Hoc Options

# Main options
-i inventory/hosts.yml  # Specify the inventory file
-m module_name           # Specify the module (default: command)
-a "arguments"           # Module arguments
--become (-b)            # Escalate to sudo
--become-user root       # The user to escalate to
--become-method sudo     # The escalation method
-u ubuntu                # The SSH user
--private-key ~/.ssh/key # The SSH key
-f 10                    # Number of parallel executions (default: 5)
--limit web1             # Run against specific hosts only
-v / -vv / -vvv / -vvvv  # Verbosity level
--check                  # Dry run (no real changes)
--diff                   # Show a diff of the changes
-o                       # One-line output (summary)
--ask-pass (-k)          # Prompt for the SSH password
--ask-become-pass (-K)   # Prompt for the sudo password

15. Ansible Playbook

15.1 ansible-playbook Command Options

# Basic run
ansible-playbook -i inventory/hosts.yml playbook.yml

# Main options
ansible-playbook playbook.yml \
  -i inventory/hosts.yml \       # Inventory
  --limit webservers \           # Restrict the target hosts
  --tags "nginx,ssl" \           # Run only specific tags
  --skip-tags "debug" \          # Skip specific tags
  -e "env=prod version=2.1" \    # Pass extra variables
  -e @vars/prod.yml \            # Pass a variable file
  --check \                      # Dry-run
  --diff \                       # Diff of the changes
  --start-at-task "Install Nginx" \ # Start from a specific task
  --step \                       # Confirmation prompt per task
  --list-tasks \                 # List the tasks only
  --list-tags \                  # List the tags only
  --list-hosts \                 # List the target hosts only
  -f 20 \                        # Number of parallel executions
  --become \                     # sudo
  --vault-password-file .vault_pass \ # Vault password file
  --ask-vault-pass \             # Prompt for the Vault password
  -v                             # Verbose output

# Syntax check
ansible-playbook playbook.yml --syntax-check

15.2 The Basic Structure of a Playbook

# site.yml
---
- name: Configure web servers
  hosts: webservers
  become: yes
  gather_facts: yes
  vars:
    http_port: 80
    app_version: '2.1.0'

  pre_tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
      tags: [nginx]

    - name: Deploy Nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Restart Nginx
      tags: [nginx, config]

    - name: Deploy application
      git:
        repo: 'https://github.com/myorg/myapp.git'
        dest: /opt/myapp
        version: '{{ app_version }}'
      tags: [deploy]

    - name: Ensure Nginx is running
      service:
        name: nginx
        state: started
        enabled: yes
      tags: [nginx]

  post_tasks:
    - name: Verify HTTP response
      uri:
        url: 'http://localhost:{{ http_port }}'
        status_code: 200
      register: result
      retries: 3
      delay: 5
      until: result.status == 200

  handlers:
    - name: Restart Nginx
      service:
        name: nginx
        state: restarted

15.3 Conditionals (when)

tasks:
  # A basic condition
  - name: Install Apache (Debian)
    apt:
      name: apache2
      state: present
    when: ansible_os_family == "Debian"

  - name: Install httpd (RedHat)
    dnf:
      name: httpd
      state: present
    when: ansible_os_family == "RedHat"

  # A compound condition
  - name: Configure for production
    template:
      src: prod.conf.j2
      dest: /etc/app/app.conf
    when:
      - env == "prod"
      - ansible_memtotal_mb >= 4096

  # An OR condition
  - name: Install on Debian or Ubuntu
    apt:
      name: curl
      state: present
    when: ansible_distribution == "Debian" or ansible_distribution == "Ubuntu"

  # Whether a variable exists
  - name: Configure custom DNS
    template:
      src: resolv.conf.j2
      dest: /etc/resolv.conf
    when: custom_dns is defined

  # Based on the result of an earlier task
  - name: Check if config exists
    stat:
      path: /etc/app/app.conf
    register: config_file

  - name: Create default config
    template:
      src: default.conf.j2
      dest: /etc/app/app.conf
    when: not config_file.stat.exists

15.4 Loops (loop)

tasks:
  # A basic loop
  - name: Install packages
    apt:
      name: '{{ item }}'
      state: present
    loop:
      - nginx
      - python3
      - git
      - curl
      - htop

  # A more efficient approach (install them all at once)
  - name: Install packages (optimized)
    apt:
      name:
        - nginx
        - python3
        - git
      state: present

  # Dict loop
  - name: Create users
    user:
      name: '{{ item.name }}'
      groups: '{{ item.groups }}'
      shell: '{{ item.shell }}'
      state: present
    loop:
      - { name: 'alice', groups: 'sudo', shell: '/bin/bash' }
      - { name: 'bob', groups: 'developers', shell: '/bin/zsh' }
      - { name: 'charlie', groups: 'developers', shell: '/bin/bash' }

  # with_fileglob (file globbing)
  - name: Copy all config files
    copy:
      src: '{{ item }}'
      dest: /etc/app/conf.d/
    with_fileglob:
      - 'files/configs/*.conf'

  # loop_control
  - name: Create directories with index
    file:
      path: '/opt/app/data-{{ idx }}'
      state: directory
    loop:
      - logs
      - cache
      - uploads
    loop_control:
      index_var: idx
      label: '{{ item }}' # The label to show in the output (hides sensitive data)

15.5 Handlers

tasks:
  - name: Update Nginx config
    template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify:
      - Validate Nginx config
      - Restart Nginx

  - name: Update SSL certificate
    copy:
      src: ssl/cert.pem
      dest: /etc/ssl/certs/app.pem
    notify: Restart Nginx

handlers:
  # A handler runs only when notified, and only once no matter how many times it is notified
  - name: Validate Nginx config
    command: nginx -t
    listen: 'Validate Nginx config'

  - name: Restart Nginx
    service:
      name: nginx
      state: restarted
    listen: 'Restart Nginx'

15.6 Block (Error Handling)

tasks:
  - name: Application deployment with rollback
    block:
      - name: Pull latest code
        git:
          repo: 'https://github.com/myorg/myapp.git'
          dest: /opt/myapp
          version: '{{ app_version }}'

      - name: Install dependencies
        pip:
          requirements: /opt/myapp/requirements.txt
          virtualenv: /opt/myapp/venv

      - name: Run database migrations
        command: /opt/myapp/venv/bin/python manage.py migrate
        args:
          chdir: /opt/myapp

      - name: Restart application
        systemd:
          name: myapp
          state: restarted

    rescue:
      - name: Rollback to previous version
        git:
          repo: 'https://github.com/myorg/myapp.git'
          dest: /opt/myapp
          version: '{{ previous_version }}'

      - name: Restart application (rollback)
        systemd:
          name: myapp
          state: restarted

      - name: Send failure notification
        slack:
          token: '{{ slack_token }}'
          channel: '#deploy'
          msg: 'Deployment of {{ app_version }} FAILED. Rolled back to {{ previous_version }}.'

    always:
      - name: Clean up temp files
        file:
          path: /tmp/deploy_artifacts
          state: absent

      - name: Log deployment result
        lineinfile:
          path: /var/log/deployments.log
          line: "{{ ansible_date_time.iso8601 }} - {{ app_version }} - {{ ansible_failed_task.name | default('SUCCESS') }}"
          create: yes

16. Ansible Role

16.1 Role Structure

roles/
└── nginx/
    ├── tasks/
    │   ├── main.yml         # The main tasks
    │   ├── install.yml      # Installation tasks
    │   └── configure.yml    # Configuration tasks
    ├── handlers/
    │   └── main.yml         # Handlers
    ├── templates/
    │   └── nginx.conf.j2    # Jinja2 template
    ├── files/
    │   └── index.html       # Static file
    ├── vars/
    │   └── main.yml         # Variables (high precedence)
    ├── defaults/
    │   └── main.yml         # Defaults (low precedence)
    ├── meta/
    │   └── main.yml         # Metadata and dependencies
    ├── tests/
    │   ├── inventory
    │   └── test.yml
    └── README.md

16.2 The ansible-galaxy Command

# ── Managing roles ──

# Initialize a role (creates the directory structure)
ansible-galaxy role init roles/nginx
ansible-galaxy role init --init-path=./roles nginx

# Install a role from Galaxy
ansible-galaxy role install geerlingguy.nginx
ansible-galaxy role install geerlingguy.docker -p roles/

# Install a specific version
ansible-galaxy role install geerlingguy.nginx,3.1.0

# Install everything from requirements.yml
ansible-galaxy role install -r requirements.yml

# List the installed roles
ansible-galaxy role list

# Remove a role
ansible-galaxy role remove geerlingguy.nginx

# Search for roles
ansible-galaxy role search nginx --author geerlingguy

# Show role information
ansible-galaxy role info geerlingguy.nginx

# ── Managing collections ──

# Install a collection
ansible-galaxy collection install amazon.aws
ansible-galaxy collection install community.general

# Install everything from requirements.yml
ansible-galaxy collection install -r requirements.yml

# List the installed collections
ansible-galaxy collection list

# Build a collection
ansible-galaxy collection build

# Publish a collection
ansible-galaxy collection publish ./myns-mycoll-1.0.0.tar.gz

An example requirements.yml:

# requirements.yml
---
roles:
  - name: geerlingguy.nginx
    version: '3.1.0'
  - name: geerlingguy.docker
    version: '7.4.1'
  - name: geerlingguy.certbot
  - src: https://github.com/myorg/ansible-role-custom.git
    scm: git
    version: main
    name: custom_role

collections:
  - name: amazon.aws
    version: '>=8.0.0'
  - name: community.general
  - name: ansible.posix

16.3 An Example of Using a Role

# site.yml
---
- name: Configure web servers
  hosts: webservers
  become: yes

  roles:
    # Basic usage
    - nginx

    # Passing variables
    - role: nginx
      vars:
        nginx_worker_processes: 4
        nginx_worker_connections: 2048

    # Conditional execution
    - role: certbot
      when: enable_ssl | default(false)

    # Specifying tags
    - role: monitoring
      tags: [monitoring, observability]

17. Ansible Vault

Ansible Vault is the feature that encrypts sensitive data such as passwords, API keys, and certificates.

17.1 The Vault Commands

# ── Create an encrypted file ──
ansible-vault create secrets.yml
# An editor opens, and the file is encrypted automatically on save

# Create it with a specified editor
EDITOR=nano ansible-vault create secrets.yml

# ── Edit an encrypted file ──
ansible-vault edit secrets.yml

# ── Encrypt an existing file ──
ansible-vault encrypt vars/prod_secrets.yml

# Encrypt several files at once
ansible-vault encrypt vars/secret1.yml vars/secret2.yml

# ── Decrypt an encrypted file ──
ansible-vault decrypt vars/prod_secrets.yml

# ── View the contents of an encrypted file (without decrypting it) ──
ansible-vault view secrets.yml

# ── Change the password ──
ansible-vault rekey secrets.yml

# ── Encrypt a string (inline) ──
ansible-vault encrypt_string 'SuperSecretPassword123!' --name 'db_password'
# Output:
# db_password: !vault |
#   $ANSIBLE_VAULT;1.1;AES256
#   61636530396131653661313936353764...

ansible-vault encrypt_string --vault-password-file .vault_pass 'my_api_key_value' --name 'api_key'

# ── Ways to supply the Vault password ──
# 1. A prompt
ansible-playbook site.yml --ask-vault-pass

# 2. A file
ansible-playbook site.yml --vault-password-file .vault_pass

# 3. An environment variable
export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
ansible-playbook site.yml

# 4. A script (integrating with a password manager)
ansible-playbook site.yml --vault-password-file ./get_vault_pass.sh

# ── Using several Vault IDs (a different password per environment) ──
ansible-vault create --vault-id prod@prompt secrets_prod.yml
ansible-vault create --vault-id dev@.vault_pass_dev secrets_dev.yml

ansible-playbook site.yml \
  --vault-id prod@prompt \
  --vault-id dev@.vault_pass_dev

17.2 An Example of Using Vault

# vars/secrets.yml (encrypted)
---
db_password: 'SuperSecretPassword123!'
api_key: 'sk-1234567890abcdef'
ssl_private_key: |
  -----BEGIN PRIVATE KEY-----
  MIIEvQIBADANBgkqhkiG9w0BAQEFAASC...
  -----END PRIVATE KEY-----
# Using it in a playbook
- name: Deploy application
  hosts: webservers
  become: yes
  vars_files:
    - vars/defaults.yml
    - vars/secrets.yml # A file encrypted with Vault

  tasks:
    - name: Configure database connection
      template:
        src: db_config.j2
        dest: /etc/app/database.yml
        mode: '0600'

18. Advanced Ansible Features

18.1 ansible-doc — Module Documentation

# View the module help
ansible-doc apt
ansible-doc copy
ansible-doc template
ansible-doc amazon.aws.ec2_instance

# List the modules
ansible-doc --list
ansible-doc --list | grep aws

# Short help (usage examples)
ansible-doc -s apt
ansible-doc -s copy

# Documentation by plugin type
ansible-doc -t callback -l          # List the callback plugins
ansible-doc -t connection -l        # List the connection plugins
ansible-doc -t inventory -l         # List the inventory plugins
ansible-doc -t lookup -l            # List the lookup plugins

18.2 ansible-navigator — A TUI-Based Tool

# Run a playbook in TUI mode
ansible-navigator run site.yml -i inventory/hosts.yml

# stdout mode (output similar to plain ansible-playbook)
ansible-navigator run site.yml -i inventory/hosts.yml -m stdout

# Browse the inventory
ansible-navigator inventory -i inventory/hosts.yml

# Browse the module documentation
ansible-navigator doc apt

# Browse collections
ansible-navigator collections

# Check the configuration
ansible-navigator config

18.3 ansible-lint — Code Quality

# Install it
pip install ansible-lint

# Lint a playbook
ansible-lint site.yml

# Lint a whole directory
ansible-lint

# Ignore specific rules
ansible-lint -x yaml[truthy]

# Fix automatically
ansible-lint --fix

18.4 ansible.cfg Configuration

# ansible.cfg
[defaults]
inventory = inventory/hosts.yml
remote_user = ubuntu
private_key_file = ~/.ssh/id_rsa
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callback_whitelist = timer, profile_tasks
forks = 20
timeout = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
control_path_dir = ~/.ansible/cp

Part 3: Integration, Cheat Sheets, and Troubleshooting


19. Combining Terraform and Ansible

19.1 The Integration Architecture

The most common pattern for connecting Terraform and Ansible is as follows.

┌─────────────────────────────────────────────────────────────┐
Terraform + Ansible Integration├─────────────────────────────────────────────────────────────┤
│                                                              │
1. Provision the infrastructure with Terraform│     terraform apply                                          │
│         │                                                    │
│         ├── Create VPC, subnets, security groups             │
│         ├── Create EC2 instances                             │
│         └── terraform output -json                           │
│                    │                                         │
│                    ▼                                         │
2. Terraform OutputAnsible Dynamic Inventory│     terraform output -json > tf_output.json│         │                                                    │
│         ▼                                                    │
3. Configuration management with Ansible│     ansible-playbook -i dynamic_inventory.py site.yml│         │                                                    │
│         ├── Install packages                                 │
│         ├── Configure the application                        │
│         └── Start the services                               │
│                                                              │
└─────────────────────────────────────────────────────────────┘

19.2 Using Terraform Output from Ansible

# Terraform outputs.tf
output "web_server_ips" {
  value = aws_instance.web[*].public_ip
}

output "db_endpoint" {
  value     = aws_db_instance.main.endpoint
  sensitive = true
}

output "ssh_key_name" {
  value = aws_key_pair.deployer.key_name
}
#!/usr/bin/env python3
# dynamic_inventory.py — a dynamic inventory built from Terraform output
import json
import subprocess
import sys

def get_terraform_output():
    result = subprocess.run(
        ["terraform", "output", "-json"],
        capture_output=True, text=True
    )
    return json.loads(result.stdout)

def main():
    tf_output = get_terraform_output()
    web_ips = tf_output["web_server_ips"]["value"]

    inventory = {
        "webservers": {
            "hosts": web_ips,
            "vars": {
                "ansible_user": "ubuntu",
                "ansible_ssh_private_key_file": "~/.ssh/deployer.pem",
                "db_endpoint": tf_output["db_endpoint"]["value"]
            }
        },
        "_meta": {
            "hostvars": {}
        }
    }

    print(json.dumps(inventory, indent=2))

if __name__ == "__main__":
    main()
# Run it
chmod +x dynamic_inventory.py
ansible-playbook -i dynamic_inventory.py site.yml

19.3 Calling Ansible from a Terraform local-exec Provisioner

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.small"
  key_name      = aws_key_pair.deployer.key_name

  # Run Ansible (use provisioners only as a last resort)
  provisioner "local-exec" {
    command = <<-EOT
      sleep 30  # Wait for SSH to be ready
      ANSIBLE_HOST_KEY_CHECKING=False \
      ansible-playbook \
        -i '${self.public_ip},' \
        -u ubuntu \
        --private-key ~/.ssh/deployer.pem \
        -e "db_endpoint=${aws_db_instance.main.endpoint}" \
        ansible/site.yml
    EOT
  }

  depends_on = [aws_db_instance.main]
}

19.4 A Terraform + Ansible Automation Script

#!/bin/bash
# deploy.sh — deploy the whole infrastructure plus configuration
set -euo pipefail

echo "=== Step 1: Terraform Init ==="
terraform init -input=false

echo "=== Step 2: Terraform Plan ==="
terraform plan -out=tfplan -input=false

echo "=== Step 3: Terraform Apply ==="
terraform apply tfplan

echo "=== Step 4: Generate Ansible Inventory ==="
terraform output -json > tf_output.json

echo "=== Step 5: Wait for instances to be ready ==="
WEB_IPS=$(terraform output -json web_server_ips | jq -r '.[]')
for ip in $WEB_IPS; do
  echo "Waiting for $ip..."
  until ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 ubuntu@"$ip" true 2>/dev/null; do
    sleep 5
  done
  echo "$ip is ready!"
done

echo "=== Step 6: Run Ansible Playbook ==="
ANSIBLE_HOST_KEY_CHECKING=False \
ansible-playbook \
  -i ./dynamic_inventory.py \
  --vault-password-file .vault_pass \
  site.yml

echo "=== Deployment Complete ==="
terraform output

20. The Top 20 Terraform Command Cheat Sheet

# ── Initialization and validation ──
terraform init                           # 1. Initialize the project
terraform init -upgrade                  # 2. Upgrade providers/modules
terraform validate                       # 3. Validate the syntax
terraform fmt -recursive -check          # 4. Check the formatting

# ── The core workflow ──
terraform plan -out=tfplan               # 5. Save the execution plan
terraform apply tfplan                   # 6. Apply the plan
terraform apply -auto-approve            # 7. Apply with auto-approve (CI/CD)
terraform destroy -auto-approve          # 8. Destroy everything

# ── State management ──
terraform state list                     # 9. List the resources
terraform state show aws_instance.web    # 10. Resource details
terraform state mv OLD NEW              # 11. Move/rename a resource
terraform state rm RESOURCE             # 12. Remove it from state
terraform state pull > backup.tfstate    # 13. Back up the state
terraform force-unlock LOCK_ID          # 14. Release the lock

# ── Import ──
terraform import RESOURCE ID            # 15. Bring in an existing resource
terraform plan -generate-config-out=g.tf # 16. Generate import code automatically

# ── Workspace ──
terraform workspace new ENV             # 17. Create a workspace
terraform workspace select ENV          # 18. Switch workspaces

# ── Utilities ──
terraform output -json                   # 19. Output values (JSON)
terraform console                        # 20. Interactive console

21. The Top 20 Ansible Command Cheat Sheet

# ── Checking connectivity and gathering information ──
ansible all -m ping                              # 1. Ping every host
ansible all -m setup -a "filter=ansible_os*"     # 2. Gather system information

# ── Ad-hoc commands ──
ansible web -m shell -a "uptime"                 # 3. Run a shell command
ansible web -m apt -a "name=nginx state=present" -b  # 4. Install a package
ansible web -m service -a "name=nginx state=started" -b # 5. Start a service
ansible web -m copy -a "src=f dest=/etc/app/" -b # 6. Copy a file
ansible web -m user -a "name=deploy state=present" -b   # 7. Create a user

# ── Running playbooks ──
ansible-playbook site.yml                        # 8. Run a playbook
ansible-playbook site.yml --check --diff         # 9. Dry-run + diff
ansible-playbook site.yml --limit web1           # 10. Specific hosts only
ansible-playbook site.yml --tags deploy          # 11. Specific tags only
ansible-playbook site.yml -e "env=prod"          # 12. Pass a variable
ansible-playbook site.yml --syntax-check         # 13. Syntax check

# ── Vault ──
ansible-vault create secrets.yml                 # 14. Create an encrypted file
ansible-vault edit secrets.yml                   # 15. Edit an encrypted file
ansible-vault encrypt file.yml                   # 16. Encrypt a file
ansible-vault decrypt file.yml                   # 17. Decrypt a file
ansible-vault encrypt_string 'secret' --name key # 18. Encrypt a string

# ── Galaxy and utilities ──
ansible-galaxy role install geerlingguy.nginx    # 19. Install a role
ansible-doc -s apt                               # 20. Module help

22. Troubleshooting

22.1 Terraform Troubleshooting

# ── 1. State lock problems ──
# Error: Error acquiring the state lock
# Cause: a previous terraform apply exited abnormally
# Fix:
terraform force-unlock LOCK_ID

# ── 2. Provider authentication failure ──
# Error: NoCredentialProviders
# Fix: check the AWS credentials
aws sts get-caller-identity
export AWS_PROFILE=myprofile

# ── 3. State and real infrastructure disagree ──
# Fix: refresh the state
terraform apply -refresh-only

# Remove the specific resource from state and import it again
terraform state rm aws_instance.web
terraform import aws_instance.web i-0abc123def

# ── 4. Provider version conflicts ──
# Fix: regenerate the lock file
rm .terraform.lock.hcl
terraform init -upgrade

# ── 5. Circular dependencies ──
# Error: Cycle detected
# Fix: check depends_on, split the resources apart
terraform graph | dot -Tpng > graph.png  # Visualize the dependencies

# ── 6. Enable debug logging ──
export TF_LOG=DEBUG        # TRACE, DEBUG, INFO, WARN, ERROR
export TF_LOG_PATH=terraform.log
terraform apply

# ── 7. Plan succeeds but apply fails ──
# Cause: insufficient API permissions, resource limits, network problems
# Fix: run sequentially with -parallelism=1 to pin down the exact error
terraform apply -parallelism=1

# ── 8. Performance problems with large state ──
# Fix: split the state (across several Terraform projects)
# Split into network/ compute/ database/ and so on, and
# connect them with the terraform_remote_state data source

22.2 Ansible Troubleshooting

# ── 1. SSH connection failure ──
# Error: UNREACHABLE!
# Debug:
ansible webservers -m ping -vvvv  # Maximum verbosity

# Test SSH directly
ssh -i ~/.ssh/key.pem -o StrictHostKeyChecking=no ubuntu@10.0.1.10

# ── 2. sudo permission problems ──
# Error: Missing sudo password
# Fix:
ansible-playbook site.yml --ask-become-pass
# Or configure it in ansible.cfg:
# [privilege_escalation]
# become_ask_pass = True

# ── 3. Module not found ──
# Error: MODULE FAILURE
# Fix: install the collection
ansible-galaxy collection install amazon.aws

# ── 4. Jinja2 template errors ──
# Error: AnsibleUndefinedVariable
# Fix: check that the variable is defined
ansible-playbook site.yml -e "@vars/defaults.yml" --check
# Or use the default filter: "{{ my_var | default('fallback') }}"

# ── 5. Performance tuning ──
# ansible.cfg settings:
# [defaults]
# forks = 20                    # Increase the parallelism
# gathering = smart             # Fact caching
# [ssh_connection]
# pipelining = True             # Enable SSH pipelining
# ssh_args = -o ControlMaster=auto -o ControlPersist=60s

# ── 6. Lost the Vault password ──
# Fix: unrecoverable. You have to re-encrypt with a new password
# While you still remember the password:
ansible-vault rekey secrets.yml

# ── 7. Inventory parsing errors ──
# Debug: validate the inventory
ansible-inventory -i inventory/hosts.yml --list --export

# ── 8. Broken idempotency (repeated changed) ──
# Add a creates/removes condition to the command/shell module
- name: Initialize database
  command: /opt/app/init_db.sh
  args:
    creates: /opt/app/.db_initialized

23. A Summary of Best Practices

23.1 Terraform Best Practices

ItemRecommendation
State managementUse a remote backend (S3 + DynamoDB); never commit it to Git
Directory structureSeparate by environment (envs/dev, envs/prod) or use workspaces
Code reviewAttach the terraform plan output to the PR
ModularizationSplit into reusable modules and tag their versions
Variable managementMark sensitive variables sensitive = true; use env vars or Vault
Lock fileAlways commit .terraform.lock.hcl to Git
CI/CDPlan on the PR; apply automatically after the merge
Naming conventionUse the project-env-resource pattern consistently

23.2 Ansible Best Practices

ItemRecommendation
InventoryUse a dynamic inventory in cloud environments
Sensitive dataAlways encrypt it with Ansible Vault
Using rolesStructure tasks as roles; make good use of Galaxy roles
IdempotencyPrefer dedicated modules over command/shell; use creates/removes
TestingVerify ahead of time with --check --diff; test roles with Molecule
Variable precedenceUnderstand variable precedence and define variables in the right place
Using tagsTag every task so it can be run selectively
LoggingMeasure performance with callback_whitelist = timer, profile_tasks

24. References

24.1 Official Documentation

24.4 Community


Terraform and Ansible are the two pillars of modern infrastructure automation. Provisioning infrastructure declaratively with Terraform and configuring software on top of it with Ansible has become the industry's de facto standard. As you apply the commands and patterns covered here to your own work, my hope is that you replace repetitive manual effort with code and establish a culture in your team of managing infrastructure safely and predictably.

Comments

No comments yet.

Sign in to leave a comment