Back to Blog
TerraformGitOpsInfrastructure as CodeAWSCI/CDcloud architectureLahorePakistan

Declarative Infrastructure: Scaling Cloud Environments with Terraform and Automated GitOps Pipelines

Anber Aziz•Lahore, Pakistan•
Terraform plan posted on a pull request before a GitOps pipeline applies AWS changes

Declarative Terraform plus GitOps makes the repository the cloud's source of truth. You describe the desired network, compute, and data stores; plan shows the delta; merge applies it. That loop removes the class of outages caused by console clicks, laptop applies, and environments nobody can rebuild.

In the early eras of cloud computing, provisioning servers, firewalls, and databases was a point-and-click process. That works for small systems and collapses at enterprise scale. Manual management produces human error, undocumented drift, and environments that cannot be replicated.

Modern teams treat infrastructure the same way they treat application software: version-controlled code. The rest of this article is the operating model I use when a product team is ready to stop treating AWS as a workshop and start treating it as a reviewed artifact.

Who this is for

This is for product teams that have more than one environment, for startups about to hire a second engineer who will need staging, and for engineering teams in Lahore, across Pakistan, and working remote-from-Pakistan whose production lives in a global cloud while the people live on another timezone.

If you are a product manager, "it works on my account" is not a release. If you are a founder, a documented apply path is cheaper than a unique production you cannot recreate after a region incident. If you are an engineer, this is state, plans, and IAM—not a catalog of every AWS resource type.

Why choose declarative automation over imperative scripts?

  • Imperative automation writes the steps: install this package, wait, open this port. If a script fails midway, retries often leave a partial environment.
  • Declarative automation states the desired end: three servers behind a load balancer and a secured database.

Terraform evaluates the files, compares them with live cloud state, and calculates the API calls required to converge.

I still use imperative tools inside images and userdata when the guest OS needs packages. The cloud shape—VPC, security groups, IAM, RDS, the load balancer—belongs in Terraform. Mixing those layers is how people end up with a 2,000-line bash file that creates a VPC "just this once."

Idempotency is the property you are buying. A second apply of the same config should be a no-op. A second run of an imperative script should not create a second database. If it can, it is not your source of truth.

How do I structure reusable cloud layouts?

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

resource "aws_vpc" "production_network" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "production-vpc"
  }
}

resource "aws_instance" "application_server" {
  ami           = "ami-0c7217cdde317cfec"
  instance_type = "t3.medium"
  subnet_id     = aws_vpc.production_network.id

  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }

  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

Architectural notes

  • State mapping: Terraform stores mapped history in a protected state file and diffs before apply.
  • Metadata protections: requiring IMDSv2 tokens reduces metadata scraping risk.
  • Loose coupling: declaring dependencies lets the engine create the network before instances.

The snippet is a teaching layout, not a production topology. A real app server belongs in a private subnet, with a public load balancer or a bastion pattern, and an AMI you own rather than a hardcoded public id that will rot. I still keep http_tokens = "required" as a default I do not negotiate.

Workspaces or separate state files per environment both work. I prefer separate states (staging vs prod backends) so a workspace mix-up cannot plan production with staging variables. Variable files are cheap. Blast radius is not.

Modules should encode decisions you have already repeated: a private RDS module with backups and encryption on, a VPC module with flow logs. They should not hide a single aws_instance behind three layers of for_each you cannot debug on a Friday.

How do GitOps pipelines apply Terraform safely?

Running applies from a laptop reintroduces human variance. Under GitOps, the repository is the source of truth:

  1. An engineer branches, edits infrastructure variables, and opens a pull request.
  2. CI runs terraform plan and posts the exact change set.
  3. Reviewers approve the merge.
  4. The pipeline applies to the live environment.

That is the whole product. Everything else is guardrails: terraform fmt -check, tflint, policy as code if you have a compliance story, and a required review from someone who can read a destroy line.

A minimal plan job looks like this:

# .github/workflows/terraform-plan.yml
on:
  pull_request:
    paths:
      - "infra/**"

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init -input=false
        working-directory: infra
      - run: terraform plan -input=false -no-color -out=tfplan
        working-directory: infra

OIDC to the cloud beats long-lived access keys in GitHub secrets. Keys in a shared chat channel are how remote-from-Pakistan teams accidentally donate a cluster to the internet.

Apply on main should be locked so two pipelines cannot converge at once. Terraform state locking exists for this. If a lock is stuck, that is an incident, not a --force habit.

Plan vs apply vs console: who wins?

PathReviewableRepeatableI allow it
Console clickNoNoBreak-glass only, then import
Laptop terraform applyMaybeDepends who ran itLocal sandbox
PR plan + CI applyYesYesDefault
Hotfix branch with the same pipelineYesYesPrefer over console

Shifting infrastructure onto an automated, reviewable Git workflow is how teams deploy stable, repeatable, auditable cloud systems. Import the snowflake when you find one. Do not pretend the console never happened.

What operational details keep this from rotting?

  • Provider and module version pins. Floating latest is a surprise apply.
  • Secrets out of git. Parameter Store, Secrets Manager, or sealed secrets—not terraform.tfvars committed with a password.
  • Drift detection. A scheduled plan that fails the build when someone clicked anyway.
  • Destroy protection on databases and state buckets.
  • Human-readable plan comments. A 2,000-line plan with three real destroys needs a summary in the PR, not a dump.

Where does remote state belong, and how do I import a snowflake?

A backend block is not ceremony. It is the lock that stops two applies from rewriting the same graph.

terraform {
  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Restrict who can read that bucket. State can contain database endpoints, IAM ARNs, and plaintext-adjacent values from poorly written resources. I treat state access like production SSH: named humans, short-lived CI roles, no anonymous listing.

When someone already clicked the VPC into existence, I do not recreate it to feel pure. terraform import (or an import block in current Terraform) attaches the real object to the address in code. Then the next plan should be small. If the plan wants to replace the database, I stop and fix the config until it does not. GitOps does not excuse a reckless first apply.

Startups and engineering teams in Pakistan often run the control plane in us-east-1 or eu-central-1 while people are on PKT. That is fine. Put CI in the same cloud account, not on a laptop that disappears at 2 a.m. Choose regions for data residency and latency to users, not for proximity to the office. The GitOps loop is what makes a remote-from-Pakistan team able to change production without sharing a root key in a group chat.

I build production AI and full-stack systems from Lahore, and the infrastructure I am willing to operate at 2 a.m. is the infrastructure a pipeline already planned in daylight. Anber Aziz is the name on this post because I have inherited accounts where the only documentation was a screenshot of the VPC wizard.

If you are moving a product from console-managed cloud to Terraform and GitOps, services is how I work with product and engineering teams, and contact is the right next step.

FAQ

Why is declarative Terraform better than clicking in a cloud console?

A console change is an unrehearsed mutation with no diff and no review. Terraform states the desired layout, compares it to remote state, and shows the API calls before they happen. You can reproduce staging, recover from mistakes, and explain who changed the VPC.

What is GitOps for infrastructure?

GitOps means the repository is the source of truth. A pull request runs terraform plan, humans review the diff, merge unlocks apply in CI. Laptops do not apply production. Drift is either merged code or an alert, not a surprise in the console.

Where should Terraform state live?

Remote, locked, and access-controlled—typically S3 plus DynamoDB, Terraform Cloud, or the equivalent for your cloud. Local state files on laptops cause split-brain applies. State contains secrets-shaped data; treat the backend like production.

How do I keep Terraform modules from becoming a second monolith?

Module around stable seams: networking, data stores, compute, and the app’s own peripherals. Pin module versions. Do not abstract a resource you have only created once. Copy-paste two times, extract the third.

What GitOps setup fits startups and teams in Pakistan?

One repo (or one infra repo), remote state, PR plans, and apply on main for non-prod first. Product and engineering teams in Lahore and remote-from-Pakistan companies get the most from a boring pipeline: no laptop applies, no shared access keys in chat, no snowflake staging.

Ada
AdaOnline
Anber's AI Assistant

Before we begin

So Anber can follow up with you if needed.