<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Building a Multi-Environment Terraform Architecture on AWS]]></title><description><![CDATA[Building a Multi-Environment Terraform Architecture on AWS]]></description><link>https://oneassignment.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 16:24:26 GMT</lastBuildDate><atom:link href="https://oneassignment.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Multi-Environment Terraform Architecture on AWS]]></title><description><![CDATA[Github repo: In modern DevOps, infrastructure is no longer managed only by clicking around in the AWS Console. Infrastructure is now written as code…]]></description><link>https://oneassignment.hashnode.dev/building-a-multi-environment-terraform-architecture-on-aws</link><guid isPermaLink="true">https://oneassignment.hashnode.dev/building-a-multi-environment-terraform-architecture-on-aws</guid><dc:creator><![CDATA[Muhammad Hashim]]></dc:creator><pubDate>Tue, 28 Apr 2026 10:36:22 GMT</pubDate><content:encoded><![CDATA[<p><strong>Github repo:</strong> <a href="https://github.com/HashimZaffar/Asignment-1">https://github.com/HashimZaffar/Asignment-1</a></p>
<p>In modern DevOps, infrastructure is no longer managed only by clicking around in the AWS Console. Infrastructure is now written as code, reviewed through Git, reused through modules, and deployed consistently across multiple environments. This approach is called Infrastructure as Code, and Terraform is one of the most widely used tools for it.</p>
<p>In this assignment, I designed a multi-environment Terraform architecture for AWS. The goal was to create reusable Terraform code that can deploy separate environments such as dev, staging, and prod while keeping each environment isolated, configurable, and professionally structured.</p>
<p>This project is suitable for a junior to intermediate DevOps, Cloud DevOps, DevSecOps, Platform Engineering, or SRE portfolio because it demonstrates real infrastructure practices, not just basic Terraform syntax.</p>
<h2>Project Objective</h2>
<p>The objective of this assignment was to design Terraform code that meets the following requirements:</p>
<p>Use reusable Terraform modules.</p>
<p>Create separate environments for dev, staging, and prod.</p>
<p>Use separate Terraform state files for each environment.</p>
<p>Store Terraform state remotely in an S3 bucket.</p>
<p>Use DynamoDB for Terraform state locking.</p>
<p>Use either Terraform workspaces or a folder-based environment strategy.</p>
<p>Implement a consistent tagging strategy.</p>
<p>Use environment-specific variables.</p>
<p>For this project, I used a folder-based strategy because it is easier to understand, easier to review, and commonly used in production-style Terraform repositories.</p>
<h2>What This Project Deploys</h2>
<p>Each environment can deploy its own basic AWS infrastructure, including a VPC, public subnet, internet gateway, route table, security group, and EC2 instance.</p>
<p>The purpose of this project is not to create a very complex production platform. The purpose is to demonstrate how Terraform should be structured properly when supporting multiple environments.</p>
<p>Each environment uses the same reusable modules, but with different variable values.</p>
<p>For example, the dev environment may use smaller resources, while staging and prod may use different CIDR ranges, instance types, or access rules.</p>
<h2>Project Setup</h2>
<p>I started the project on an AWS EC2 instance running Ubuntu 24.04.</p>
<p>The base folder name for the project was:</p>
<pre><code class="language-bash">Asignment-1
</code></pre>
<p>I created the project folder and initialized Git:</p>
<pre><code class="language-bash">mkdir Asignment-1
cd Asignment-1
git init
</code></pre>
<p>Using Git is important because Terraform code should be treated like application code. It should be version-controlled, reviewed, and improved over time.</p>
<h2>Final Project Structure</h2>
<p>The project follows this structure:</p>
<pre><code class="language-text">Asignment-1/
├── README.md
├── .gitignore
├── modules/
│   ├── network/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── security_group/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── ec2/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
└── environments/
    ├── dev/
    │   ├── backend.tf
    │   ├── provider.tf
    │   ├── main.tf
    │   ├── variables.tf
    │   ├── terraform.tfvars
    │   └── outputs.tf
    ├── staging/
    │   ├── backend.tf
    │   ├── provider.tf
    │   ├── main.tf
    │   ├── variables.tf
    │   ├── terraform.tfvars
    │   └── outputs.tf
    └── prod/
        ├── backend.tf
        ├── provider.tf
        ├── main.tf
        ├── variables.tf
        ├── terraform.tfvars
        └── outputs.tf
</code></pre>
<p>This structure separates reusable infrastructure code from environment-specific configuration.</p>
<p>The <code>modules</code> folder contains reusable building blocks.</p>
<p>The <code>environments</code> folder contains separate configurations for dev, staging, and prod.</p>
<h2>Why I Used a Folder-Based Strategy</h2>
<p>Terraform supports workspaces, but for this assignment I used a folder-based strategy.</p>
<p>In a folder-based strategy, each environment has its own directory:</p>
<pre><code class="language-text">environments/dev
environments/staging
environments/prod
</code></pre>
<p>This makes the project easier to understand because each environment has its own backend file, variable file, and state path.</p>
<p>For beginners, this is much clearer than using workspaces because the separation is visible in the repository structure.</p>
<p>It also reduces confusion when reviewing code because the dev, staging, and prod configurations are clearly separated.</p>
<h2>Understanding Terraform Modules</h2>
<p>A Terraform module is reusable Terraform code.</p>
<p>Instead of writing the same VPC, subnet, security group, and EC2 configuration three times, I created reusable modules and called them from each environment.</p>
<p>For example, the network module creates the VPC, subnet, internet gateway, route table, and route table association.</p>
<p>The security group module creates firewall rules for SSH and HTTP access.</p>
<p>The EC2 module creates the virtual machine.</p>
<p>This approach avoids duplication and makes the code easier to maintain.</p>
<h2>Network Module</h2>
<p>The network module is responsible for creating basic networking resources.</p>
<p>It creates:</p>
<pre><code class="language-text">VPC
Public subnet
Internet gateway
Public route table
Route table association
</code></pre>
<p>Example:</p>
<pre><code class="language-hcl">resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.common_tags, {
    Name = "${var.environment}-vpc"
  })
}
</code></pre>
<p>This creates a VPC using the CIDR block provided by the environment.</p>
<p>For example, dev may use:</p>
<pre><code class="language-hcl">vpc_cidr = "10.10.0.0/16"
</code></pre>
<p>Staging may use:</p>
<pre><code class="language-hcl">vpc_cidr = "10.20.0.0/16"
</code></pre>
<p>Prod may use:</p>
<pre><code class="language-hcl">vpc_cidr = "10.30.0.0/16"
</code></pre>
<p>This keeps each environment isolated at the network level.</p>
<h2>Security Group Module</h2>
<p>The security group module controls traffic to the EC2 instance.</p>
<p>It allows SSH traffic on port 22 and HTTP traffic on port 80.</p>
<p>Example:</p>
<pre><code class="language-hcl">resource "aws_security_group" "this" {
  name        = "${var.environment}-web-sg"
  description = "Security group for ${var.environment} EC2 instance"
  vpc_id      = var.vpc_id

  ingress {
    description = "Allow SSH from trusted IP range"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_allowed_cidr]
  }

  ingress {
    description = "Allow HTTP traffic"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = var.http_allowed_cidrs
  }

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

  tags = merge(var.common_tags, {
    Name = "${var.environment}-web-sg"
  })
}
</code></pre>
<p>From a DevSecOps perspective, SSH should not be open to the whole internet in a real production environment. For learning, <code>0.0.0.0/0</code> may be used temporarily, but in a professional setup it should be restricted to a trusted IP range.</p>
<h2>EC2 Module</h2>
<p>The EC2 module creates the compute instance.</p>
<p>Example:</p>
<pre><code class="language-hcl">resource "aws_instance" "this" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [var.security_group_id]

  tags = merge(var.common_tags, {
    Name = "${var.environment}-web-server"
  })
}
</code></pre>
<p>The EC2 module does not hardcode the AMI ID, instance type, subnet ID, or security group ID. These values are passed as variables.</p>
<p>This makes the module reusable across different environments.</p>
<h2>Environment-Specific Variables</h2>
<p>Each environment has its own <code>terraform.tfvars</code> file.</p>
<p>For example, the dev environment may use:</p>
<pre><code class="language-hcl">aws_region         = "us-east-1"
environment        = "dev"
vpc_cidr           = "10.10.0.0/16"
public_subnet_cidr = "10.10.1.0/24"
availability_zone  = "us-east-1a"
ami_id             = "ami-020cba7c55df1f615"
instance_type      = "t2.micro"
ssh_allowed_cidr   = "0.0.0.0/0"

common_tags = {
  Project     = "Asignment-1"
  Environment = "dev"
  Owner       = "Muhammad Virk"
  ManagedBy   = "Terraform"
  CostCenter  = "Learning"
}
</code></pre>
<p>The staging environment can use a different VPC range and instance type:</p>
<pre><code class="language-hcl">environment        = "staging"
vpc_cidr           = "10.20.0.0/16"
public_subnet_cidr = "10.20.1.0/24"
instance_type      = "t3.micro"
</code></pre>
<p>The prod environment can use another separate range:</p>
<pre><code class="language-hcl">environment        = "prod"
vpc_cidr           = "10.30.0.0/16"
public_subnet_cidr = "10.30.1.0/24"
instance_type      = "t3.micro"
</code></pre>
<p>This is the main benefit of reusable Terraform code. The same module can be used across environments, but each environment can behave differently.</p>
<h2>Remote Backend with S3</h2>
<p>Terraform state is very important. It tracks the infrastructure Terraform manages.</p>
<p>If Terraform creates an EC2 instance, the state file records that resource. The next time Terraform runs, it compares the code with the state file and decides what needs to be created, updated, or destroyed.</p>
<p>In a team or professional environment, storing state locally is risky. If the local machine is lost, the state may be lost. If multiple people run Terraform from different machines, they may create conflicts.</p>
<p>To solve this, I used an S3 remote backend.</p>
<p>Each environment has its own backend configuration.</p>
<p>Example for dev:</p>
<pre><code class="language-hcl">terraform {
  backend "s3" {
    bucket         = "asignment-1-terraform-state-34-238-244-58"
    key            = "dev/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "asignment-1-terraform-locks"
    encrypt        = true
  }
}
</code></pre>
<p>The important part is the <code>key</code>.</p>
<p>For dev:</p>
<pre><code class="language-hcl">key = "dev/terraform.tfstate"
</code></pre>
<p>For staging:</p>
<pre><code class="language-hcl">key = "staging/terraform.tfstate"
</code></pre>
<p>For prod:</p>
<pre><code class="language-hcl">key = "prod/terraform.tfstate"
</code></pre>
<p>This means each environment has a separate state file.</p>
<h2>Why Separate State Files Matter</h2>
<p>Separate state files reduce the risk of accidental changes across environments.</p>
<p>Dev state only tracks dev resources.</p>
<p>Staging state only tracks staging resources.</p>
<p>Prod state only tracks prod resources.</p>
<p>This is very important because production infrastructure should not be affected by changes made while testing in dev.</p>
<p>For example, if I run Terraform inside the dev folder, Terraform only uses the dev backend and dev state file. It does not touch staging or prod state.</p>
<p>This is a key safety principle in infrastructure management.</p>
<h2>DynamoDB State Locking</h2>
<p>State locking prevents two Terraform operations from modifying the same state file at the same time.</p>
<p>For example, if two engineers run <code>terraform apply</code> against the same environment at the same time, they could corrupt the state or create unexpected infrastructure changes.</p>
<p>DynamoDB locking helps prevent this by allowing only one operation to hold the lock at a time.</p>
<p>For this project, I created a DynamoDB table called:</p>
<pre><code class="language-text">asignment-1-terraform-locks
</code></pre>
<p>The table uses <code>LockID</code> as the primary key.</p>
<p>This is a common Terraform backend locking pattern used in AWS-based infrastructure projects.</p>
<h2>Creating Backend Resources</h2>
<p>Before using the remote backend, I created the S3 bucket and DynamoDB table.</p>
<p>Create the S3 bucket:</p>
<pre><code class="language-bash">aws s3api create-bucket \
  --bucket asignment-1-terraform-state-34-238-244-58 \
  --region us-east-1
</code></pre>
<p>Enable versioning:</p>
<pre><code class="language-bash">aws s3api put-bucket-versioning \
  --bucket asignment-1-terraform-state-34-238-244-58 \
  --versioning-configuration Status=Enabled
</code></pre>
<p>Enable encryption:</p>
<pre><code class="language-bash">aws s3api put-bucket-encryption \
  --bucket asignment-1-terraform-state-34-238-244-58 \
  --server-side-encryption-configuration '{
    "Rules": [
      {
        "ApplyServerSideEncryptionByDefault": {
          "SSEAlgorithm": "AES256"
        }
      }
    ]
  }'
</code></pre>
<p>Create the DynamoDB table:</p>
<pre><code class="language-bash">aws dynamodb create-table \
  --table-name asignment-1-terraform-locks \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1
</code></pre>
<p>This gives Terraform a remote backend for state storage and locking.</p>
<h2>Provider Configuration</h2>
<p>Each environment has a provider configuration.</p>
<p>Example:</p>
<pre><code class="language-hcl">terraform {
  required_version = "&gt;= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&gt; 6.0"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = var.common_tags
  }
}
</code></pre>
<p>The provider tells Terraform that it will manage AWS resources.</p>
<p>The <code>default_tags</code> block helps apply common tags across supported AWS resources.</p>
<p>This improves consistency and makes resources easier to track.</p>
<h2>Tagging Strategy</h2>
<p>A good tagging strategy is important in cloud environments.</p>
<p>Tags help with cost tracking, ownership, governance, automation, and auditing.</p>
<p>In this project, I used the following common tags:</p>
<pre><code class="language-hcl">common_tags = {
  Project     = "Asignment-1"
  Environment = "dev"
  Owner       = "Muhammad Virk"
  ManagedBy   = "Terraform"
  CostCenter  = "Learning"
}
</code></pre>
<p>The <code>Environment</code> tag changes depending on the environment.</p>
<p>For dev:</p>
<pre><code class="language-hcl">Environment = "dev"
</code></pre>
<p>For staging:</p>
<pre><code class="language-hcl">Environment = "staging"
</code></pre>
<p>For prod:</p>
<pre><code class="language-hcl">Environment = "prod"
</code></pre>
<p>This makes it easy to identify which resources belong to which environment.</p>
<h2>Calling Modules from an Environment</h2>
<p>Each environment calls the reusable modules.</p>
<p>Example:</p>
<pre><code class="language-hcl">module "network" {
  source = "../../modules/network"

  environment        = var.environment
  vpc_cidr           = var.vpc_cidr
  public_subnet_cidr = var.public_subnet_cidr
  availability_zone  = var.availability_zone
  common_tags        = var.common_tags
}

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

  environment        = var.environment
  vpc_id             = module.network.vpc_id
  ssh_allowed_cidr   = var.ssh_allowed_cidr
  http_allowed_cidrs = ["0.0.0.0/0"]
  common_tags        = var.common_tags
}

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

  environment       = var.environment
  ami_id            = var.ami_id
  instance_type     = var.instance_type
  subnet_id         = module.network.public_subnet_id
  security_group_id = module.security_group.security_group_id
  common_tags       = var.common_tags
}
</code></pre>
<p>The network module creates the VPC and subnet.</p>
<p>The security group module uses the VPC ID from the network module.</p>
<p>The EC2 module uses the subnet ID from the network module and the security group ID from the security group module.</p>
<p>This shows how Terraform modules can pass values between each other using outputs.</p>
<h2>Outputs</h2>
<p>Outputs show useful information after Terraform creates infrastructure.</p>
<p>Example:</p>
<pre><code class="language-hcl">output "vpc_id" {
  value = module.network.vpc_id
}

output "ec2_instance_id" {
  value = module.ec2.instance_id
}

output "ec2_public_ip" {
  value = module.ec2.public_ip
}
</code></pre>
<p>After running <code>terraform apply</code>, Terraform can display important values such as the VPC ID, EC2 instance ID, and public IP address.</p>
<p>Outputs are useful for verification, documentation, and connecting infrastructure components.</p>
<h2>Terraform Workflow</h2>
<p>The standard Terraform workflow is:</p>
<pre><code class="language-bash">terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
</code></pre>
<p><code>terraform init</code> initializes the working directory and downloads provider plugins.</p>
<p><code>terraform fmt</code> formats Terraform files.</p>
<p><code>terraform validate</code> checks whether the configuration is valid.</p>
<p><code>terraform plan</code> previews what Terraform will create, update, or delete.</p>
<p><code>terraform apply</code> applies the changes and creates the infrastructure.</p>
<p>For the dev environment:</p>
<pre><code class="language-bash">cd environments/dev
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
</code></pre>
<p>For staging:</p>
<pre><code class="language-bash">cd environments/staging
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
</code></pre>
<p>For prod:</p>
<pre><code class="language-bash">cd environments/prod
terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
</code></pre>
<p>Each environment is deployed separately.</p>
<h2>Why This Architecture Is Useful</h2>
<p>This architecture is useful because it follows important DevOps principles.</p>
<p>It separates environments clearly.</p>
<p>It avoids code duplication through modules.</p>
<p>It stores state remotely instead of locally.</p>
<p>It uses locking to reduce the risk of state corruption.</p>
<p>It allows each environment to have different variables.</p>
<p>It applies a consistent tagging strategy.</p>
<p>It supports Git-based infrastructure workflows.</p>
<p>These are the same ideas used in real infrastructure repositories.</p>
<h2>DevSecOps Considerations</h2>
<p>From a DevSecOps perspective, this project can be improved further by adding stronger security controls.</p>
<p>For example, SSH access should be restricted to a trusted IP address instead of being open to the internet.</p>
<p>The S3 backend bucket should have versioning and encryption enabled.</p>
<p>Public access to the state bucket should be blocked.</p>
<p>IAM permissions should follow least privilege.</p>
<p>Terraform code can later be scanned using tools such as Checkov, tfsec, or Trivy.</p>
<p>Secrets should not be committed to Git.</p>
<p>Production environments should have stricter controls than dev environments.</p>
<p>Even though this assignment is beginner-friendly, these security considerations make the project more professional.</p>
<h2>Cleanup</h2>
<p>After testing, resources should be destroyed to avoid unnecessary AWS charges.</p>
<p>Destroy dev:</p>
<pre><code class="language-bash">cd environments/dev
terraform destroy
</code></pre>
<p>Destroy staging:</p>
<pre><code class="language-bash">cd environments/staging
terraform destroy
</code></pre>
<p>Destroy prod:</p>
<pre><code class="language-bash">cd environments/prod
terraform destroy
</code></pre>
<p>It is important not to delete the S3 backend bucket before destroying the infrastructure, because Terraform needs the state files to know which resources it manages.</p>
<h2>What I Learned</h2>
<p>This assignment helped me understand that Terraform is not only about creating cloud resources. The real value of Terraform is in how the code is structured, reused, secured, and operated.</p>
<p>I learned how to separate infrastructure into reusable modules, how to manage different environments, how remote state works, why state locking matters, and how variables make Terraform flexible.</p>
<p>I also learned that good Terraform architecture should be predictable, repeatable, and safe.</p>
<h2>Portfolio Value</h2>
<p>This project demonstrates practical skills that are relevant to DevOps and cloud engineering roles in 2026.</p>
<p>It shows understanding of Infrastructure as Code, AWS fundamentals, Terraform modules, remote state management, environment separation, tagging, and basic DevSecOps practices.</p>
<p>For a beginner, this is a strong foundation. For a junior to intermediate portfolio, it shows that the project is structured with real-world engineering practices in mind.</p>
<h2>Conclusion</h2>
<p>Building a multi-environment Terraform architecture is an important step in learning professional DevOps. It teaches how infrastructure should be organized when multiple environments are involved.</p>
<p>The key lesson from this assignment is simple: infrastructure should be reusable, version-controlled, environment-aware, and safely managed.</p>
<p>By using Terraform modules, separate state files, S3 remote backend, DynamoDB locking, environment-specific variables, and consistent tags, this project provides a strong foundation for building more advanced cloud infrastructure projects in the future.</p>
]]></content:encoded></item></channel></rss>