Practical Terraform Infrastructure as Code Example

Why Most Terraform Examples Fail in Production

When you search for a terraform infrastructure as code example, you typically find a single main.tf file containing a VPC, a random subnet, and an EC2 instance all crammed into fifty lines. While that works for a local terraform apply test, it completely falls apart the moment a second engineer touches the codebase, or when you need to deploy the same architecture across staging and production environments.

At Techsolss, when we implement Infrastructure as Code (IaC) for client platforms—ranging from simple cloud setups to complex Kubernetes clusters—we rely on modular, predictable, and strictly versioned HCL (HashiCorp Configuration Language).

Let's walk through a production-grade layout that separates configuration from state, utilizes input variables securely, and structures modules so you can scale your cloud footprint without tearing your hair out.

---

The Recommended Directory Structure

A maintainable Terraform layout separates reusable logic from environment-specific configurations. Putting everything in one directory is the fastest route to technical debt.

terraform-infra/
├── modules/
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── compute/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── environments/
    ├── staging/
    │   ├── main.tf
    │   ├── variables.tf
    │   ├── outputs.tf
    │   └── backend.hcl
    └── production/
        ├── main.tf
        ├── variables.tf
        ├── outputs.tf
        └── backend.hcl

By keeping your infrastructure logic inside modules/, your environments/ folder simply acts as an orchestrator that passes parameters into those modules.

---

Step 1: Writing a Reusable VPC Module

Let's start with a foundational networking module. Inside modules/vpc/main.tf, we define a virtual private cloud with public and private subnets.

# modules/vpc/main.tf

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

  tags = {
    Name        = "${var.environment}-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 = var.azs[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name        = "${var.environment}-public-subnet-${count.index + 1}"
    Environment = var.environment
  }
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name        = "${var.environment}-igw"
    Environment = var.environment
  }
}

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

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }

  tags = {
    Name        = "${var.environment}-public-rt"
    Environment = var.environment
  }
}

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

To make this module truly reusable, we expose the necessary variables in modules/vpc/variables.tf and outputs in modules/vpc/outputs.tf.

# modules/vpc/variables.tf
variable "environment" {
  type = string
}

variable "vpc_cidr" {
  type = string
}

variable "public_subnet_cidrs" {
  type = list(string)
}

variable "azs" {
  type = list(string)
}
# modules/vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

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

---

Step 2: Consuming the Module in Production

Now, inside your environments/production/main.tf file, you wire up the module. This is where you lock down your provider configurations and remote state backends.

# environments/production/main.tf

terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  backend "s3" {
    bucket         = "techsolss-terraform-state-prod"
    key            = "networking/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks-prod"
    encrypt        = true
  }
}

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

  default_tags {
    tags = {
      ManagedBy   = "Terraform"
      Environment = "production"
      Owner       = "DevOps Team"
    }
  }
}

module "vpc" {
  source = "../../modules/vpc"

  environment         = "production"
  vpc_cidr            = "10.100.0.0/16"
  public_subnet_cidrs = ["10.100.1.0/24", "10.100.2.0/24"]
  azs                 = ["us-east-1a", "us-east-1b"]
}

By enforcing remote state in S3 with DynamoDB state locking, you prevent two engineers from running terraform apply concurrently and corrupting the state file.

---

Best Practices We Enforce on Client Projects

Writing the HCL is only half the battle. When integrating Infrastructure as Code into your CI/CD pipelines, keep these operational guardrails in mind:

  1. Never Hardcode Secrets: Use environment variables, AWS Secrets Manager, or HashiCorp Vault data sources rather than committing database passwords or API tokens directly into your .tf files.
  2. Run terraform fmt and tflint in CI: Catch formatting errors and deprecated syntax before code review. Adding static analysis to your GitHub Actions or GitLab CI pipeline prevents messy commits.
  3. Plan Before You Apply: Always execute terraform plan -out=tfplan and inspect the output. Better yet, save the binary plan file and execute it specifically to ensure no unexpected resource drift or recreation occurs during deployment.

If your team is transitioning legacy infrastructure to automated IaC pipelines, or if you need guidance on structuring your services architecture for scale, we can help.

---

Conclusion

Using a structured approach to your terraform infrastructure as code example ensures your systems remain transparent, secure, and easy to modify as your engineering organization grows. Start small with a modular network layer, lock down your remote state backend, and build your CI/CD gates early.

If you want an experienced set of eyes on your cloud architecture or need help setting up robust MLOps and DevOps workflows, check out about us or contact us to discuss your stack.

Ready to streamline your deployment pipelines? Book a 20-minute introductory call with Muhammad Ramzan to discuss your infrastructure bottlenecks.

FAQ

What is the best way to manage Terraform state across multiple team members?

You should always use a remote state backend (such as AWS S3 with DynamoDB for state locking, or Terraform Cloud) rather than storing state locally on your machine. This prevents race conditions and state file corruption.

Should I put all my infrastructure into a single Terraform module?

No. Monolithic Terraform configurations become extremely difficult to maintain and plan. Split your infrastructure into logical modules (like VPC, compute, databases) and orchestrate them through environment-specific directories.

How do I handle sensitive variables in Terraform?

Mark sensitive variables using sensitive = true in your variable definitions, and source secrets dynamically from secure stores like AWS Secrets Manager, HashiCorp Vault, or CI/CD environment secrets instead of hardcoding them in HCL.

Related reading

[
  {
    "@context": "https://schema.org",
    "@type": "BlogPosting",
    "headline": "Practical Terraform Infrastructure as Code Example",
    "author": {
      "@type": "Person",
      "name": "Muhammad Ramzan"
    },
    "publisher": {
      "@type": "Organization",
      "name": "Techsolss"
    },
    "datePublished": "2026-08-21",
    "mainEntityOfPage": "https://techsolss.online/posts/practical-terraform-infrastructure-as-code-example.html"
  },
  {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "What is the best way to manage Terraform state across multiple team members?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "You should always use a remote state backend (such as AWS S3 with DynamoDB for state locking, or Terraform Cloud) rather than storing state locally on your machine. This prevents race conditions and state file corruption."
        }
      },
      {
        "@type": "Question",
        "name": "Should I put all my infrastructure into a single Terraform module?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "No. Monolithic Terraform configurations become extremely difficult to maintain and plan. Split your infrastructure into logical modules (like VPC, compute, databases) and orchestrate them through environment-specific directories."
        }
      },
      {
        "@type": "Question",
        "name": "How do I handle sensitive variables in Terraform?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Mark sensitive variables using `sensitive = true` in your variable definitions, and source secrets dynamically from secure stores like AWS Secrets Manager, HashiCorp Vault, or CI/CD environment secrets instead of hardcoding them in HCL."
        }
      }
    ]
  }
]

Need senior DevOps, MLOps, or Cloud Architecture expertise?

We help startups and fast-shipping teams build rock-solid cloud infrastructure, automate deployments, and deploy production AI pipelines without full-time agency overhead. Let's discuss your architecture on a free 20-minute strategy call.

Book a free 20-min call