Infrastructure as Code Fundamentals

4 Nov 2026, 11:2822 min readby ITYantra Admin

Complete guide to Infrastructure as Code using Terraform, including state management, modules, providers, and best practices for cloud infrastructure provisioning.

# Infrastructure as Code Fundamentals

Introduction

Infrastructure as Code (IaC) is the practice of managing and provisioning cloud infrastructure through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools. IaC enables teams to version control infrastructure, automate provisioning, ensure consistency across environments, and reduce configuration drift.

This tutorial covers IaC fundamentals using Terraform, including state management, resource providers, modules, variables, outputs, workspaces, and production best practices.

Learning Objectives

By the end of this tutorial, you will be able to:

  • Explain IaC benefits and the declarative model
  • Install and configure Terraform with cloud providers
  • Write Terraform configuration for compute, storage, and networking resources
  • Manage Terraform state locally and remotely
  • Create reusable Terraform modules
  • Use variables, outputs, and data sources
  • Manage multiple environments with workspaces
  • Apply IaC best practices for security and reliability

Technical Background

Declarative vs. Imperative IaC

| Approach | Description | Examples | |----------|-------------|----------| | Declarative | Define desired state; tool determines how to achieve it | Terraform, CloudFormation, Azure Bicep | | Imperative | Define step-by-step instructions | Ansible, Chef, shell scripts |

Terraform Architecture

flowchart LR
    Config[Terraform Configuration .tf] --> Init[terraform init]
    Init --> Plan[terraform plan]
    Plan --> Apply[terraform apply]
    Apply --> State[State File]
    State --> Cloud[Cloud Provider API]
    Cloud --> State
    Providers[Provider Plugins] --> Init
    Variables[Variables] --> Config
    Modules[Modules] --> Config

Core Terraform Concepts

| Concept | Description | |---------|-------------| | Provider | Plugin that interacts with a cloud platform API (AWS, Azure, GCP) | | Resource | Infrastructure object (EC2 instance, VPC, database) | | Module | Reusable container for multiple resources | | State | JSON file tracking managed resources | | Variable | Input parameter for Terraform configuration | | Output | Value returned after applying configuration | | Data Source | Read-only reference to existing infrastructure | | Workspace | Isolated state for multiple environments |

Prerequisites

  • Cloud account (AWS, Azure, or GCP) with IAM permissions
  • Terraform CLI installed (v1.5 or later)
  • Understanding of cloud resource concepts (VPC, subnet, instance, etc.)
  • Git repository for version control
  • Remote state storage (S3, Azure Blob Storage, or GCS) for team collaboration

Hardware and Software Requirements

| Component | Requirement | |-----------|-------------| | Operating System | Linux, macOS, or Windows | | Terraform | v1.5 or later | | Cloud CLI | AWS CLI v2, Azure CLI, or gcloud CLI | | IDE | VS Code with Terraform extension | | Storage | S3 bucket, Azure Blob Storage, or GCS bucket for remote state | | Database | DynamoDB (AWS) or similar for state locking |

Step-by-Step Implementation

Phase 1: Install Terraform

# Ubuntu/Debian
wget -O- https://apt.releases.hashicorp.com/gpg | \
  gpg --dearmor | sudo tee /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

# Verify installation
terraform version

Phase 2: Configure Terraform Provider

Create a new directory and configure the AWS provider:

# main.tf
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  description = "AWS region for resource deployment"
  type        = string
  default     = "us-east-1"
}

Initialize the working directory:

terraform init

Phase 3: Define Infrastructure Resources

Create a VPC with public and private subnets:

# vpc.tf
variable "vpc_cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "List of CIDR blocks for public subnets"
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  description = "List of CIDR blocks for private subnets"
  type        = list(string)
  default     = ["10.0.10.0/24", "10.0.20.0/24"]
}

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

resource "aws_subnet" "public" {
  count             = length(var.public_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true
  tags = {
    Name        = "public-subnet-${count.index}"
    Environment = var.environment
    Tier        = "public"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = false
  tags = {
    Name        = "private-subnet-${count.index}"
    Environment = var.environment
    Tier        = "private"
  }
}

data "aws_availability_zones" "available" {
  state = "available"
}

Define EC2 instance and security group:

# compute.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
  default     = "dev"
}

resource "aws_security_group" "web" {
  name_prefix = "web-sg-"
  vpc_id      = aws_vpc.main.id

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

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

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

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

  tags = {
    Name        = "web-security-group"
    Environment = var.environment
  }
}

resource "aws_instance" "web" {
  count               = 2
  ami                 = data.aws_ami.ubuntu.id
  instance_type       = var.instance_type
  subnet_id           = aws_subnet.public[count.index].id
  vpc_security_group_ids = [aws_security_group.web.id]
  key_name            = var.key_name

  user_data = <<-EOF
              #!/bin/bash
              apt update
              apt install -y nginx
              systemctl start nginx
              systemctl enable nginx
              EOF

  tags = {
    Name        = "web-server-${count.index}"
    Environment = var.environment
    Role        = "web"
  }
}

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

variable "key_name" {
  description = "EC2 key pair name"
  type        = string
}

Phase 4: Define Outputs

# outputs.tf
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "IDs of public subnets"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "IDs of private subnets"
  value       = aws_subnet.private[*].id
}

output "web_instance_public_ips" {
  description = "Public IP addresses of web instances"
  value       = aws_instance.web[*].public_ip
}

output "security_group_id" {
  description = "ID of the web security group"
  value       = aws_security_group.web.id
}

Phase 5: Initialize, Plan, and Apply

# Initialize Terraform working directory
terraform init

# Review execution plan
terraform plan -var="environment=dev" -var="key_name=my-key"

# Apply changes
terraform apply -var="environment=dev" -var="key_name=my-key"

# Confirm with 'yes'

Phase 6: Manage State

For team collaboration, configure remote state:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "infrastructure/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-lock"
    encrypt        = true
  }
}
# Re-initialize to migrate state to remote backend
terraform init

# List state resources
terraform state list

# Show state details
terraform state show aws_instance.web[0]

Phase 7: Create Reusable Modules

Create a module structure:

modules/
  vpc/
    main.tf
    variables.tf
    outputs.tf
  ec2/
    main.tf
    variables.tf
    outputs.tf

Use the module in root configuration:

# main.tf with module
module "vpc" {
  source = "./modules/vpc"
  cidr   = "10.0.0.0/16"
  env    = var.environment
}

module "web_servers" {
  source       = "./modules/ec2"
  vpc_id       = module.vpc.vpc_id
  subnet_ids   = module.vpc.public_subnet_ids
  instance_type = "t3.micro"
  count         = 2
  env           = var.environment
}

Phase 8: Manage Multiple Environments with Workspaces

# Create workspaces for different environments
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

# List workspaces
terraform workspace list

# Select a workspace
terraform workspace select staging

# Apply with workspace-specific variables
terraform apply -var-file="environments/staging.tfvars"

Expected Output

After `terraform apply`:

  • VPC with public and private subnets across multiple AZs
  • Security group allowing HTTP/HTTPS/SSH
  • EC2 instances running with Nginx installed
  • Outputs displaying resource IDs and IP addresses
  • State file tracking all managed resources

Verification Procedures

| Check | Command | Expected Result | |-------|---------|-----------------| | Validate configuration | `terraform validate` | No errors | | Review plan | `terraform plan` | Shows intended changes | | Apply infrastructure | `terraform apply` | Resources created successfully | | Check state | `terraform state list` | All resources tracked | | Verify in console | AWS Management Console | Resources visible |

Troubleshooting Scenarios

| Issue | Diagnosis | Resolution | |-------|-----------|------------| | `terraform init` fails | Provider not found or network issue | Check provider source; verify network connectivity | | State lock error | Another operation is running | Wait or force unlock with `terraform force-unlock` | | Resource already exists | Resource created outside Terraform | Import existing resource with `terraform import` | | Plan shows unwanted changes | Configuration drift | Review and update configuration; use `terraform refresh` | | Module not found | Incorrect source path | Verify module path exists and is accessible | | Backend authentication fails | Missing cloud credentials | Configure AWS CLI or environment variables |

Best Practices

  • **Version control everything:** Store Terraform configuration in Git with peer review
  • **Use remote state:** Never store state files locally for team projects
  • **Enable state locking:** Use DynamoDB or similar to prevent concurrent modifications
  • **Pin provider versions:** Use version constraints to avoid breaking changes
  • **Separate environments:** Use workspaces or separate directories for dev/staging/prod
  • **Keep modules small:** Each module should do one thing well
  • **Use data sources:** Reference existing resources instead of hardcoding IDs
  • **Validate before apply:** Always run `terraform plan` and review changes

Security Considerations

  • **Never commit secrets:** Use environment variables or secret managers for passwords and keys
  • **Restrict provider permissions:** Use least-privilege IAM policies for Terraform service accounts
  • **Encrypt state files:** Remote state backends should support encryption at rest
  • **Review plan output:** Check for unintended resource destruction before applying
  • **Use Sentinel or OPA:** Implement policy-as-code for compliance enforcement
  • **Rotate state encryption keys:** Periodically rotate KMS keys used for state encryption

Conclusion

Infrastructure as Code transforms infrastructure management from a manual, error-prone process into a version-controlled, automated workflow. Terraform's declarative model, provider ecosystem, and state management make it a leading choice for multi-cloud IaC. By following best practices for state management, module design, and security, teams can provision infrastructure reliably and consistently across environments.

References

  • Terraform Documentation: https://developer.hashicorp.com/terraform/docs
  • Terraform AWS Provider: https://registry.terraform.io/providers/hashicorp/aws/latest/docs
  • Terraform Azure Provider: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs
  • Terraform Google Provider: https://registry.terraform.io/providers/hashicorp/google/latest/docs
  • IaC Best Practices: https://www.hashicorp.com/resources/infrastructure-as-code-best-practices
  • OWASP Infrastructure as Code Security: https://owasp.org/www-community/controls/Infrastructure_as_Code
#declarative-infrastructure#providers#modules#state-management#cloud-provisioning#IaC#Terraform#infrastructure-as-code