Terraform Directory structure and FLOW?
The Final Picture — What We're Building Toward
Series: Zero to Production on AWS EKS with Terraform — Part 2 · The Final Picture The destination episode — the complete platform we're building, end to end.
Last updated: 26 October 2026
In short: The finished platform is a production-grade AWS EKS environment built entirely from versioned Terraform modules, in five wired layers: network and identity, the EKS control plane, platform tools, GitOps delivery, and observability. Each environment (dev, staging, prod) uses the same pinned module library with its own tfvars and remote state.
In Day 0 we got our heads straight: what Terraform is, why modules matter, and how our setup is split into a library (devops-terraform-modules) and its consumers (the per-environment stacks that call it).
This post answers the natural next question: "Okay… so what does the finished thing actually look like — technically?"
Think of it as the photo on the front of the puzzle box. Every future post builds one piece — this is the full picture, with enough technical detail that an engineer can see how the pieces wire together, not just what they are.
What is the goal of this platform?
The goal fits in one sentence:
A complete, production-grade AWS platform — private networking, a managed Kubernetes control plane, GitOps delivery, secrets management, and full observability — described entirely as versioned Terraform, reproducible across
dev,staging, andprodfrom the same module library.
Everything below is that sentence, unpacked layer by layer.
How is the Terraform code structured?
The library defines the resources and consumers supply the values, through variables.tf, main.tf and outputs.tf. Before the architecture, the mechanics — because every layer below follows the same pattern.
The library defines what; the consumer supplies which values. A module exposes a small surface:
variables.tf— the inputs a caller must/can provide (name, CIDR, instance sizes, env).main.tf— the resources, wired together, with the hard decisions already made.outputs.tf— the handful of values other modules need downstream (VPC ID, subnet IDs, cluster endpoint, OIDC issuer).
A consumer stack then does roughly this:
module "vpc" {
source = "git::https://github.com/<org>/devops-terraform-modules.git//modules/vpc?ref=v1.4.0"
vpc_name = var.vpc_name
vpc_cidr = var.vpc_cidr # e.g. 10.0.0.0/16
availability_zones = var.availability_zones
private_subnet_cidrs = var.private_subnet_cidrs
public_subnet_cidrs = var.public_subnet_cidrs
environment = var.environment # dev | staging | prod
}
module "eks" {
source = "git::https://github.com/<org>/devops-terraform-modules.git//modules/eks?ref=v1.4.0"
vpc_id = module.vpc.vpc_id # ← output of one module
subnet_ids = module.vpc.private_subnets # becomes input of the next
# ...
}
Three things to notice, because they're the backbone of the whole platform:
- Pinned versions (
?ref=v1.4.0) — pin every environment to an exact module version (a Git tag). Upgrades become an explicit, reviewable bump, not a surprise. Pointing at a moving branch likemainworks too, but then a library change can reach prod without anyone deciding it should. - Outputs → inputs — modules chain.
vpcemits subnet IDs;eksconsumes them. Terraform reads this dependency graph and builds things in the correct order automatically. - State — each environment keeps its own remote state (S3 backend + DynamoDB lock). State is Terraform's memory of "what exists," and the lock stops two engineers from applying at once.
Every layer below is just more of this: a module, its inputs, its outputs, wired into the graph.
What are the five layers of the finished environment?
The five layers are foundation, control plane, platform tools, delivery and observability. We build from the ground up. Each layer consumes the outputs of the one beneath it, like floors of a building sharing load-bearing walls.
┌─────────────────────────────────────────────────────────────┐
│ LAYER 5 — Observability Prometheus · Loki · Tempo · Grafana │ "Is everything ok?"
├─────────────────────────────────────────────────────────────┤
│ LAYER 4 — Delivery ArgoCD · GitHub Actions Runners │ "Ship code to prod"
├─────────────────────────────────────────────────────────────┤
│ LAYER 3 — Platform tools ALB Ctlr · ExternalSecrets · EBS │ "Make the cluster usable"
├─────────────────────────────────────────────────────────────┤
│ LAYER 2 — Control plane EKS + managed node groups │ "Where apps run"
├─────────────────────────────────────────────────────────────┤
│ LAYER 1 — Foundation VPC · Subnets · SGs · TGW · IAM │ "The private land"
└─────────────────────────────────────────────────────────────┘
What does Layer 1, the foundation, contain?
Layer 1 is the network and identity: VPC, subnets, security groups, Transit Gateway and IAM policies. The land everything stands on. Built by vpc, security-group, tgw / tgw-attachment, iam-policy.
- A VPC with a deliberate CIDR (e.g.
10.0.0.0/16) carved into public and private subnets across multiple Availability Zones for redundancy. - NAT gateways so private workloads can reach out to the internet for image pulls and API calls, while nothing reaches in to them uninvited.
- An S3 gateway endpoint so traffic to S3 stays on AWS's backbone (cheaper, faster, no NAT charges).
- Security groups as the stateful firewall — least-privilege rules describing exactly what may talk to what.
- Transit Gateway + attachments to bridge separate VPCs (e.g. a shared monitoring VPC peering into workload VPCs) without exposing anything publicly.
- IAM policies as the identity backbone — the foundation for the fine-grained, per-workload permissions used higher up.
Subnets are tagged for Kubernetes here (kubernetes.io/role/elb, internal-elb) so that, two layers up, the ALB controller can auto-discover where to place load balancers. The foundation quietly sets up the layers above it.
What does Layer 2, the EKS control plane, provide?
Layer 2 provides the managed EKS cluster, its nodes and the OIDC provider for IRSA. Built by the eks module.
- A managed EKS cluster — AWS runs the Kubernetes control plane (API server, etcd) so we don't babysit it.
- Managed node groups (or Karpenter-provisioned nodes) supplying the actual compute, placed in the private subnets from Layer 1, scaling on demand.
- An OIDC provider wired to the cluster — this is the keystone for IRSA (IAM Roles for Service Accounts) / EKS Pod Identity: it lets a specific pod assume a specific AWS IAM role, with no static credentials anywhere. Layers 3 and 4 lean on this constantly.
The eks module's outputs — cluster name, API endpoint, certificate authority data, OIDC issuer URL — are exactly what the kubernetes, helm, and kubectl providers need to start installing things on top.
Which platform tools does Layer 3 add?
Layer 3 adds the AWS Load Balancer Controller, ACM certificates, External Secrets Operator and the EBS CSI driver. A fresh cluster is an empty room. These add the furniture, mostly via Helm releases bundled in eks-addon. Built by eks-addon, acm, ebs-csi, alb.
- AWS Load Balancer Controller → watches
Ingress/Serviceobjects and provisions real ALBs/NLBs, auto-discovering subnets via the Layer 1 tags. "I want a website" → a real DNS-addressable endpoint. - ACM certificates → ACM-issued TLS certs attached to those load balancers — the 🔒 padlock on every hostname, with auto-renewal.
- External Secrets Operator → syncs secrets from AWS Secrets Manager / SSM Parameter Store into native Kubernetes
Secrets. Apps read a normal secret; the real value never lives in Git. Authenticated via IRSA from Layer 2. - EBS CSI driver → backs
PersistentVolumeClaims with real gp3 EBS volumes and a defaultStorageClass, so stateful workloads get durable disk. Runs with pod-identity on the addon itself to avoid the controller-credentials bootstrap deadlock.
How does Layer 4 deliver code to production?
Layer 4 delivers code through ArgoCD (GitOps) and self-hosted GitHub Actions runners. Built inside eks-addon plus the monitoring-runner-cluster stack.
- ArgoCD → the GitOps engine. It continuously reconciles cluster state against a Git repo: merge to the target branch → ArgoCD detects the diff → rolls it out → reports health and sync status. Deploys become declarative and auditable — the repo is the source of truth, and drift is corrected automatically.
- GitHub Actions Runner Controller (ARC) → runs CI/CD jobs on self-hosted runners inside our own cluster, scaling pods up for queued jobs and down to zero when idle. Cheaper than hosted runners and able to reach private resources directly.
ArgoCD typically needs a token to register/manage itself; that token-minting is isolated in a dedicated argocd-token module so the shared eks-addon stays GitOps-engine-agnostic and clusters that don't run ArgoCD (like pure monitoring clusters) stay clean.
What does Layer 5 use for observability?
Layer 5 uses Promtail, Loki, a Prometheus agent, Tempo and Grafana for logs, metrics and traces. Built inside the eks-addon bundle.
- Promtail (DaemonSet) → tails container logs on every node and ships them with labels.
- Loki → stores logs cheaply and makes them queryable with LogQL.
- Prometheus agent → scrapes metrics on each workload cluster and remote-writes them to a central Prometheus in the shared monitoring cluster.
- Tempo → stores distributed traces, so you can follow one slow request across every service it touched.
- Grafana → dashboards and alerts over logs, metrics and traces, so a misbehaving service is seen, not guessed at.
When all five layers exist and reconcile cleanly, the environment is done — and every byte of it came from version-controlled text.
How do the Terraform modules connect to each other?
Modules connect by passing outputs as inputs, and IRSA links pods to IAM roles. The layers aren't just stacked — they're wired. The important data-flows:
vpc.outputs.private_subnets ────────────────► eks.subnet_ids
vpc.outputs.vpc_id ────────────────────────► eks / security-group
eks.outputs.oidc_provider_arn ──────────────► IRSA roles for:
• ALB controller
• External Secrets
• GitHub runners
eks.outputs.cluster_name ──────────────────► EBS CSI addon (EKS Pod Identity)
eks.outputs.cluster_endpoint + ca + token ──► kubernetes / helm providers
acm.outputs.certificate_arn ────────────────► ALB ingress annotations
subnet tags (Layer 1) ──────────────────────► ALB controller auto-discovery
Git repo ──► ArgoCD ──► live Kubernetes objects (Layer 4 reconcile loop)
This dependency graph is why ordering "just works": Terraform builds Layer 1 before Layer 2 because Layer 2 references Layer 1's outputs. You never hand-sequence the build — the references do it.
What does the final repo topology look like?
One versioned module library is consumed by several per-environment stacks, each with its own state. By the end of the series, this is the full shape — one shared, versioned library, several environments that consume it:
┌────────────────────────────────────────────────────────────┐
│ devops-terraform-modules (versioned: vX.Y.Z) │
│ THE LIBRARY — stateless, provisions nothing │
│ vpc · eks · eks-addon · acm · ecr · ec2 · ebs-csi │
│ iam-policy · alb · security-group · tgw │
└────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ source + ?ref= │ │
┌────────┴──────┐ ┌─────────┴────────┐ ┌──────────┴──────────┐
│ platform-infra│ │ platform-addons │ │ monitoring-runner- │
│ network+EKS │ │ ArgoCD, logs, │ │ cluster (shared │
│ │ │ secrets, ALB │ │ monitoring + CI) │
│ per env │ │ per env │ │ per env │
│ + own state │ │ + own state │ │ + own state │
└───────────────┘ └──────────────────┘ └─────────────────────┘
Each environment folder carries its own tfvars (the values that should differ — CIDRs, sizes, hostnames, region) and its own remote state. The library carries the engineering. The only thing separating dev from prod is a config file, so they cannot drift apart by accident — they're stamped from the identical modules at the identical pinned version.
What does "done" look like?
When the series is finished, provisioning a brand-new environment is:
- Copy an environment folder, rename it, point its backend at a fresh state key.
- Edit a handful of
tfvarsvalues — name, region, CIDR, instance types, domain. terraform init(pulls the pinned module versions + providers).terraform plan— read the diff, confirm it creates what you expect.terraform apply→ ☕.- ~15–20 minutes later (mostly AWS spinning up the EKS control plane): a fully-wired environment — VPC, cluster, ALB + TLS, secrets sync, GitOps delivery, observability — alive and structurally identical to its siblings.
No console clicking. No "did I forget a step?" No drift. That's the destination.
How does the series build the platform?
The series builds one layer per post, bottom to top. Each upcoming post builds one layer, bottom to top — module internals, the inputs that matter, the outputs it hands upward, and the gotchas:
| Episode | We build | Layer | Key modules |
|---|---|---|---|
| Day 0 ✅ | The mental model | — | — |
| Day 1 | The private network | 1 — foundation | vpc |
| Day 2 | Firewalls + bridges between networks | 1 — foundation | security-group, tgw, tgw-attachment |
| Day 3 | Permissions + the image registry | 1 — foundation | iam-policy, ecr |
| Day 4 | The EKS control plane, nodes and IRSA | 2 — control plane | eks |
| Day 5 | Storage, TLS and load balancing | 3 — platform tools | ebs-csi, acm, alb, ALB controller |
| Day 6 | Secrets sync + smart node autoscaling | 3 — platform tools | eks-addon (External Secrets, Karpenter) |
| Day 7 | GitOps delivery + self-hosted CI | 4 — delivery | eks-addon (ArgoCD, runners) |
| Day 8 | Logs, metrics, traces + the finale | 5 — observability | eks-addon, ec2 |
| Bonus 1–3 | What broke in production, and the fixes | all | — |
By the finale, every piece in the puzzle-box photo is real, reconciling, and reproducible.
Frequently asked questions
Why pin Terraform modules to a Git tag?
Pinning with ?ref=v1.4.0 locks every environment to an exact module version. Upgrades become an explicit, reviewable bump, instead of a library change reaching prod without anyone deciding it should.
How does Terraform know which order to build modules in?
Terraform reads the dependency graph created when one module's outputs are used as another's inputs. Because eks references vpc outputs, the VPC is built first automatically.
Where should Terraform state live for each environment?
Each environment keeps its own remote state in an S3 backend with a DynamoDB lock. The lock stops two engineers from applying at the same time.
What is IRSA and why does the platform depend on it?
IRSA (IAM Roles for Service Accounts) uses the cluster's OIDC provider to let a specific pod assume a specific IAM role. The ALB controller, External Secrets and GitHub runners all use it, so no static credentials are needed.
How long does it take to create a new environment?
About 15–20 minutes after terraform apply, mostly AWS creating the EKS control plane. The result is a fully wired environment structurally identical to its siblings.
What's the takeaway?
- The end goal is one complete, production-grade AWS platform, expressed entirely as versioned Terraform.
- It assembles in five wired layers: foundation → control plane → platform tools → delivery → observability — connected by outputs → inputs and IRSA, so ordering is automatic and credentials stay dynamic.
- One versioned library powers many environments; each keeps its own
tfvarsand remote state, so environments stay identical by design and upgrade by explicit version bump. - "Done" means a fresh, full-stack environment is
terraform apply+ a coffee away — not a two-week project.
Now that you can see the finished picture and how the wiring runs underneath it, the rest of the series is just building it, one well-defined layer at a time.
See you in Day 1, where we lay the foundation. 🚀
About the author
- DevOps & Platform Engineer | AWS · Azure · GCP · Terraform · GitOps · SRE | Building reliable, cost-efficient cloud platforms
Comments
Join the discussionNo comments yet. Be the first to comment.
Terraform and our infrastructure
An end-to-end, hands-on guide to designing, building, and deploying production-grade cloud infrastructure using Terraform. Learn how to create reusable Terraform modules, compose them into complete infrastructure stacks, manage multiple environments, implement networking, compute, databases, security, IAM, remote state, and secrets management, and automate deployments with CI/CD pipelines. From individual modules to fully functional, scalable, secure, and maintainable production infrastructure, this series covers the complete journey using real-world architecture and best practices.