Infrastructure as Code Terraform Example

Why Terraform Remains the Baseline for Infrastructure as Code

When you are building cloud infrastructure that needs to scale without turning into a fragile web of manual AWS console clicks, Infrastructure as Code (IaC) is non-negotiable. I am Muhammad Ramzan, a DevOps and MLOps engineer at techsolss. Over the years building environments for global clients and startups here in Pakistan, I have seen teams waste hundreds of hours debugging drift because someone manually tweaked a security group or modified an IAM role.

Terraform by HashiCorp remains the industry baseline for IaC because of its declarative syntax, vast provider ecosystem, and robust state management. If you are comparing operational models, setting up automated provisioning is often the first step before tackling broader cloud cost optimization for startups or integrating complex MLOps starter stack for a 5-person data team.

Let's walk through a clean, production-grade Terraform configuration that provisions a secure VPC, a managed database subnet, and an EC2 instance or container host.

Directory Structure for Production Terraform

Never dump all your Terraform code into a single main.tf file. That works for a 10-line sandbox, but the moment you onboard multiple environments (staging, production), flat files become impossible to maintain. A modular directory structure keeps your code DRY (Don't Repeat Yourself) and auditable.

Here is the layout we use at Techsolss services for standard client deployments:

terraform-aws-baseline/
        ├── modules/
        │   ├── vpc/
        │   │   ├── main.tf
        │   │   ├── variables.tf
        │   │   └── outputs.tf
        │   └── compute/
        │       ├── main.tf
        │       ├── variables.tf
        │       └── outputs.tf
        ├── environments/
        │   ├── staging/
        │   │   ├── main.tf
        │   │   ├── variables.tf
        │   │   └── terraform.tfvars
        │   └── production/
        │       ├── main.tf
        │       ├── variables.tf
        │       └── terraform.tfvars
        └── README.md
        

Separating reusable modules from environment-specific configurations allows you to promote exact infrastructure versions across stages without rewriting logic.

Writing the VPC Module

Let's start with a foundational VPC module. Networking is where most misconfigurations happen, so we will define public and private subnets explicitly.

Inside modules/vpc/main.tf:

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

        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" {
          vpc_id            = aws_vpc.main.id
          cidr_block        = var.public_subnet_cidr
          availability_zone = var.availability_zone

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

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

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

        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"
          }
        }

        resource "aws_route_table_association" "public" {
          subnet_id      = aws_subnet.public.id
          route_table_id = aws_route_table.public.id
        }
        

And its corresponding modules/vpc/variables.tf:

variable "environment" {
          type        = string
          description = "Deployment environment (staging, production)"
        }

        variable "vpc_cidr" {
          type        = string
          description = "CIDR block for the VPC"
        }

        variable "public_subnet_cidr" {
          type        = string
          description = "CIDR block for the public subnet"
        }

        variable "availability_zone" {
          type        = string
          description = "AWS Availability Zone"
        }
        

Instantiating in the Production Environment

Now, let's look at how the production environment consumes this module. In environments/production/main.tf:

terraform {
          required_version = ">= 1.5.0"
          backend "s3" {
            bucket         = "techsolss-terraform-state-prod"
            key            = "prod/vpc/terraform.tfstate"
            region         = "us-east-1"
            dynamodb_table = "terraform-locks"
            encrypt        = true
          }
        }

        provider "aws" {
          region = var.aws_region
        }

        module "vpc" {
          source             = "../../modules/vpc"
          environment        = var.environment
          vpc_cidr           = var.vpc_cidr
          public_subnet_cidr = var.public_subnet_cidr
          availability_zone  = var.aws_availability_zone
        }
        

Notice the remote backend block (s3 and dynamodb_table). Never store your Terraform state files locally on your laptop when working in a team. State locking via DynamoDB prevents two engineers from running terraform apply simultaneously and corrupting the state.

Handling State and Secrets Safely

One of the most common pitfalls in infrastructure as code is hardcoding database passwords, API keys, or access tokens directly into .tfvars files.

  1. Never commit .tfvars files containing secrets to Git. Add them to your .gitignore and commit only a terraform.tfvars.example template.
  2. Use external secret stores. For production systems, fetch secrets dynamically using data sources like aws_secretsmanager_secret_version rather than passing them through plain text CLI arguments.

If your architecture grows to include container orchestration or microservices, pairing your Terraform setup with a robust CI pipeline is essential. Many teams choose to execute these configurations via GitHub Actions; you can review our guide on CI/CD pipeline setup in GitHub: a practical guide to automate your terraform plan and terraform apply lifecycle safely.

Moving Forward with Automated Infrastructure

Implementing an infrastructure as code terraform example like the one above shifts your operations from reactive firefighting to predictable, version-controlled deployments. Whether you are spinning up cloud primitives for standard web applications or heavy AI product development cost: a real MLOps breakdown environments, IaC keeps your foundation clean.

If you want expert eyes on your cloud architecture or need help setting up modular Terraform pipelines for your team, feel free to contact Techsolss.

Want help with this in your own stack?

We build and run this in production for clients — and we’ll tell you honestly what it will take in yours. Book a free 20-minute call.

Book a free 20-min call