You know what the platform is, how a request moves through it, and what you need
to build it. One question is left before the first command: where does any of it
go?
The answer matters more than it sounds. Each phase of this build produces files —
Terraform for the infrastructure, Ansible for the Jenkins host, manifests for the
cluster, a chart, a pipeline definition. If they land wherever is convenient, the
GitOps loop later has no single place to watch, and you have a pile of folders
rather than a platform.
A repository layout is not tidiness. It is the contract that lets a pipeline find
what to build and a reconciler find what to deploy — and it lets an engineer who
joins on Monday see how the platform fits together from the folder names alone.
In the capstone, this is where each phase of the build lives.
Imagine a massive library. If you just dumped 10,000 books into the middle of the room, nobody could find anything. You need shelves labeled "History", "Science", and "Fiction".
This repository takes that idea further than most: the shelves are numbered
in the order you build them. 01 is the application in containers, 07 is
watching it in production, and the numbers in between are the journey from one
to the other. You can read the repository top to bottom and it tells you the
story in sequence.
CloudDevOpsProject/├── src/ <-- The application itself (three services)│ ├── frontend/ Node.js — the only one users reach│ ├── auth-service/ Python — logins│ └── roadmap-service/ Java — the domain logic│├── 01-Docker/ <-- Containerise it, and run the stack locally├── 02-Terraform/ <-- Build the AWS account│ ├── modules/ network · eks · ecr · server│ └── bootstrap/ the S3 state backend, created once├── 03-Ansible/ <-- Configure the Jenkins host│ ├── roles/ nine of them, one per tool│ └── playbook.yml the entry point├── 04-Kubernetes/ <-- Run it on the cluster│ └── manifests/ 00-namespace … 12-network-policies├── 05-Jenkins/ <-- Build, test and scan it (CI)│ ├── vars/ the shared library — logic lives here│ └── Jenkinsfiles/ one per service, thin├── 06-ArgoCD/ <-- Deploy it (CD)│ └── applications/ what Argo CD reconciles├── 07-Monitoring/ <-- Watch it│ ├── dashboards/ committed JSON, not clicked in the UI│ └── manifests/ alert rules and probes│├── docs/ <-- Architecture and decisions└── scripts/ <-- The helpers you would otherwise retype
Notice what the numbering encodes: you cannot deploy to a cluster that does
not exist, and you cannot build an image of an application you have not
containerised. The order is the dependency graph.
What it does: HashiCorp Configuration Language (HCL) describing the AWS account — around eighty resources, including the ACM certificate that puts the load balancer on HTTPS.
Subfolders:
modules/: four reusable blocks — network (VPC, subnets, NAT, route tables, NACLs, flow logs), eks (the cluster, its node group, OIDC and the IRSA roles), ecr (the registry, with immutable tags) and server (the Jenkins instance and its IAM profile).
bootstrap/: the S3 bucket that holds the state. It is separate for a reason — the thing that stores your state cannot itself be stored in that state, so it is created once, first, on its own.
What it does: Turns the bare EC2 instance Terraform created into a working Jenkins host.
Subfolders:
inventory/: not a list of IP addresses. It uses the aws_ec2 plugin to discover hosts by tag, so replacing an instance does not mean editing a file.
roles/: nine of them — aws_cli, common, docker, java, jenkins, kubectl, helm, sonarqube, trivy. One tool each, so they can be reordered or reused.
playbook.yml: the entry point that calls the roles in order.
Why isn't Ansible building the cluster? On a self-managed cluster it would — that is what kubeadm is for. Here the control plane is AWS's responsibility and the node group comes from Terraform, so Ansible's remaining job is the machine that is genuinely yours: the build host.
What it does: The manifests, numbered in apply order: namespace, RBAC, config, storage, database, the two backends, the frontend, ingress, the Jenkins and SonarQube proxies, the Argo CD ingress, and the network policies last.
Why separate it from Terraform? Terraform builds the cluster. These manifests run inside it. Mixing them creates a chicken-and-egg problem — Terraform would need a cluster that its own run is still creating.
What it does: Holds the Argo CD Application resources and the project definition.
The Concept: It watches 04-Kubernetes/manifests/. Change a file there, and Argo CD notices and reconciles the cluster to match — with prune and selfHeal on, so removals are applied and manual edits are reverted.
This repository is a Monorepo (Monolithic Repository). We put the Infrastructure, the CI pipelines, and the Kubernetes manifests all in one giant GitHub repository.
The Alternative (Polyrepo): We could have 4 separate Git repositories: platform-terraform, platform-ansible, app-backend, app-frontend.
Why we chose Monorepo: In a learning or unified platform engineering team, a Monorepo ensures that a single Pull Request can contain a Terraform change (adding a new subnet) AND the Kubernetes change that relies on it. It ensures atomic commits.
Disadvantage: As the team grows to 500 engineers, the Monorepo becomes chaotic. CI pipelines take hours to figure out which folder changed. At that scale, companies often migrate to Polyrepos or use advanced build systems like Bazel.
Notice the 06-ArgoCD/applications/ directory.
This is not an accident; it is the App-of-Apps pattern.
Instead of writing a complex deployment script, our Root ArgoCD application literally points to this directory and says: "Deploy every YAML file inside this folder."
If we want to add a new microservice to our company, we don't touch Kubernetes or AWS. We just drop a new 10-line YAML file into this folder, commit it to Git, and ArgoCD instantly deploys it.
In a Fortune 500 company, repository structure is enforced by Compliance teams.
Separation of Duties (SoD): A junior developer should be able to edit 04-Kubernetes/manifests/07-frontend.yaml, but be blocked from editing 02-Terraform/modules/eks/main.tf. We enforce this using GitHub CODEOWNERS.
The CODEOWNERS file: We place a .github/CODEOWNERS file at the root of the repo:
If a developer tries to modify a production Terraform file, GitHub will automatically block the Pull Request until a Senior Cloud Architect explicitly approves it. This satisfies SOC2 access control requirements.
Enterprise GitOps repositories often split the "App Code" from the "Manifest Code".
Repo 1 (App Code): Developers write Java code here. The CI pipeline compiles it and pushes image:v2.0 to Docker Hub.
Repo 2 (Manifest Repo - THIS PROJECT): The CI pipeline from Repo 1 automatically commits a change to kubernetes/base/api-deployment.yaml in this repository, updating the image tag. ArgoCD only watches this repository. This isolates application logic from deployment logic.
Q: Why don't we put all our files in one single folder?
A: Organization and predictability. If a team member needs to fix a Jenkins pipeline, they know exactly where to look (jenkins/pipelines/) without having to search through hundreds of Terraform and Kubernetes files.
Q: What is the difference between the terraform/modules folder and the terraform/environments folder?
A: modules contains generic, reusable templates (like a blueprint for a house). environments contains the specific instances of that blueprint (like building the house at a specific address in dev or prod). You write the code once in modules, and call it multiple times from environments.
Q: Explain the chicken-and-egg problem of mixing Terraform and Kubernetes YAML in the same state file.
A: If you use the Terraform kubernetes provider to apply YAML manifests in the exact same main.tf file that builds the EKS cluster, Terraform will evaluate the plan before creating anything. It will try to connect to the Kubernetes API to plan the YAML changes, but the API doesn't exist yet because the cluster hasn't been built. This causes Terraform to crash. This is why 02-Terraform/ and 04-Kubernetes/ are separate directories applied in separate steps.
Q: In an enterprise setting, how do you handle secrets management across a Monorepo that contains multiple environments (Dev/Stage/Prod)?
A: You never store plaintext secrets in the repo. You structure the repo to integrate with a dynamic secrets manager (like HashiCorp Vault or AWS Secrets Manager). For Terraform, you use data sources to fetch secrets at runtime. For Kubernetes, you use the External Secrets Operator (ESO) configured in the kubernetes/ directory. ESO authenticates with AWS Secrets Manager via IRSA (IAM Roles for Service Accounts) and dynamically injects the secrets into the cluster memory, keeping the Git repository completely devoid of sensitive data while maintaining a unified directory structure.
Contents | The Foundation (Linux) |
Check yourself
4 questions from this chapter. Try answering before you look.
?Why don't we put all our files in one single folder?
?What is the difference between the `terraform/modules` folder and the `terraform/environments` folder?
?Explain the chicken-and-egg problem of mixing Terraform and Kubernetes YAML in the same state file.
?In an enterprise setting, how do you handle secrets management across a Monorepo that contains multiple environments (Dev/Stage/Prod)?