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.
- Never commit
.tfvarsfiles containing secrets to Git. Add them to your.gitignoreand commit only aterraform.tfvars.exampletemplate. - Use external secret stores. For production systems, fetch secrets dynamically using data sources like
aws_secretsmanager_secret_versionrather 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