Eighteen steps, from git clone to make down. As you scroll, the drawing builds itself, and each command says what it does and shows what it printed in a real run.
Overview: the platform on AWS, every check passedScroll, and the platform builds itself
Prepare · Step 01/18
The repository
Everything is in git: Terraform for AWS, the manifests Argo CD applies, the demo API and its Dockerfile, and the Makefile that runs it all. Nothing is created by hand, locally or on AWS.
1 repo where it all starts
~/platform-eks-gitops
$ git clone https://github.com/Olivg92/platform-eks-gitops.gitCloning into 'platform-eks-gitops'...$ cd platform-eks-gitops
Prepare · Step 02/18
The tools
One command says what is installed, grouped by use. Six tools are enough for the local platform; Terraform and the AWS CLI only matter from chapter 3, the linters only to contribute.
make local-up chains five steps, one line each. The first creates a two-node Kubernetes cluster in Docker, at the same Kubernetes version as on EKS. Its credentials go to a file of their own, never to the default kubeconfig, which may point at other clusters.
22 s to create the cluster
Target it runs: make local-cluster
~/platform-eks-gitops
$ make local-up k3d cluster 22s ok
Locally · Step 04/18
The API image
The image of the demo API is built on the machine, then imported straight into the cluster: no registry to run locally.
0 registry imported directly
Target it runs: make demo-image
~/platform-eks-gitops
$ make local-up k3d cluster 22s ok Demo API image, built and imported 8s ok
Locally · Step 05/18
Argo CD
Helm installs Argo CD, the one component Argo CD does not install itself. Everything else will come from git.
49 s for Argo CD
Target it runs: make local-argocd
~/platform-eks-gitops
$ make local-up k3d cluster 22s ok Demo API image, built and imported 8s ok Argo CD 49s ok
Locally · Step 06/18
The root application
One Application, applied once, points Argo CD at the repository: the app-of-apps. From there, Argo CD reads git and installs the rest by itself.
1 root the app-of-apps
Target it runs: make local-bootstrap
~/platform-eks-gitops
$ make local-up k3d cluster 22s ok Demo API image, built and imported 8s ok Argo CD 49s ok Root application 0s ok
Locally · Step 07/18
The platform, wave by wave
Argo CD installs in waves: the operators and their CRDs first, then what uses them, then the demo API. The command waits until every application is synced and healthy, and lists while it waits what each one is still waiting on.
13 / 13 applications ready
Target it runs: make local-wait
~/platform-eks-gitops
$ make local-up k3d cluster 22s ok Demo API image, built and imported 8s ok Argo CD 49s ok Root application 0s ok Platform, synced by Argo CD 3m37s ok 13/13 readyThe platform is up, in 4m56s. Log: logs/local-up-20261004-215503.log
Locally · Step 08/18
Check
Nine end-to-end checks, each of which passes or says what is missing: the applications, the gateway over HTTP and HTTPS, a secret that came from Vault, Grafana and the demo API behind the gateway, the metrics, the SLO rules and their dashboard.
9 / 9 checks passed
~/platform-eks-gitops
$ make local-verifychecking the local platform:ok Argo CD applications all applications healthyok gateway, HTTP listener listener answers (HTTP 404)ok gateway, HTTPS listener TLS terminated, issuer CN=platform-local-caok secret synced from Vault "stored in Vault, never in git"ok Grafana behind the gateway reachable over HTTPS (HTTP 200)ok demo API behind the gateway answers through the gatewayok demo API scraped by Prometheus ServiceMonitor in placeok SLO rules from Sloth 6 rule groups generated by Slothok SLO dashboard in Grafana dashboard shipped with the application
Locally · Step 09/18
Explore
Each interface opens through a port-forward of its own, until Ctrl+C, without touching /etc/hosts: Argo CD, Grafana, Prometheus. Their passwords come from make argocd-password and make grafana-password.
3 ports 8081, 3000 and 9090
~/platform-eks-gitops
$ make argocd-uikubectl port-forward -n argocd svc/argocd-server 8081:80Forwarding from 127.0.0.1:8081 -> 8080$ make grafana-uikubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3000:80Forwarding from 127.0.0.1:3000 -> 3000$ make prometheus-uikubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090Forwarding from 127.0.0.1:9090 -> 9090
Locally · Step 10/18
Break it on purpose
The demo API can fail on demand. With 30% of requests failing against an SLO of 99.5%, the error budget burns 60 times too fast: the burn-rate alert Sloth generated fires, and links to its runbook. make demo-fix puts things back.
74 / 245 requests failed
~/platform-eks-gitops
$ make demo-breaksetting error_rate=0.3 latency_ms=0 on every pod demo-api-6bbd6667c-8wf8j: {"error_rate":0.3,"latency_ms":0} demo-api-6bbd6667c-knkhd: {"error_rate":0.3,"latency_ms":0}$ make demo-loadsending ~5 req/s to https://demo.platform.local:8443/ (127.0.0.1) for 60ssent 245 requests, 74 of them failed$ make demo-fixsetting error_rate=0 latency_ms=0 on every pod demo-api-6bbd6667c-8wf8j: {"error_rate":0.0,"latency_ms":0} demo-api-6bbd6667c-knkhd: {"error_rate":0.0,"latency_ms":0}
Locally · Step 11/18
Delete it all
One command deletes the cluster and its kubeconfig, and nothing else: the repository stays as it was, ready for the next make local-up.
0 clusters after make local-down
~/platform-eks-gitops
$ make local-downdeleted the k3d cluster platform-local
On AWS · Step 12/18
Prepare the account
The first time on AWS takes one command, here in a fresh clone. It keeps the profile in local.mk, checks the session, finds the region (here, the one where the state bucket already is) and two zones EKS accepts, then writes terraform.tfvars and backend.hcl. Without a state bucket, it would create one, after showing its plan.
1 command the first time on AWS
~/platform-eks-gitops
$ make aws-setupAWS setup of the demo environment:Which AWS CLI profile should this project use? It is kept in local.mk, which is not committed.Profiles on this machine: default perso my-profileProfile (a new name is created at the login): perso profile perso (now in local.mk) session opened 0h00 ago, about 11h59 left account <account id> region eu-north-1, where the state bucket is (now in local.mk) terraform.tfvars written, zones eu-north-1a eu-north-1b backend.hcl written, bucket platform-eks-gitops-tfstate-og92Ready. Next: make plan to see what would be created, or make up to create it.
On AWS · Step 13/18
See the plan
Before anything exists, Terraform says what it would create: the VPC and its public subnets, the EKS cluster and two Spot nodes, the IAM roles and the secrets. No NAT gateway: the nodes sit in public subnets, behind strict security groups.
28 resources to create
~/platform-eks-gitops
$ make plan# the detail of each of the 28 resources, then:Plan: 28 to add, 0 to change, 0 to destroy.
On AWS · Step 14/18
Create the infrastructure
make up shows the plan in a few lines and what it costs, then waits for a yes. Terraform applies exactly that plan: the network, the cluster, its nodes.
$0.18 an hour of demo
~/platform-eks-gitops
$ make upAWS session opened 0h01 ago, about 11h58 left Terraform init 2s ok Terraform plan 6s okTerraform plan: to create: 28 1 aws_default_security_group, 4 aws_eks_addon, 1 aws_eks_cluster, 1 aws_eks_node_group, 1 aws_eks_pod_identity_association, 3 aws_iam_role, 1 aws_iam_role_policy, 4 aws_iam_role_policy_attachment, 1 aws_internet_gateway, 1 aws_launch_template, 1 aws_route_table, 2 aws_route_table_association, 2 aws_secretsmanager_secret, 2 aws_secretsmanager_secret_version, 2 aws_subnet, 1 aws_vpcThe Kubernetes API will answer to <your IP>/32 only.This costs about $0.18 an hour, until `make down`.Apply it? Only 'yes' will be accepted: yes Terraform apply 11m17s ok 28 of 28 resources
On AWS · Step 15/18
Install the platform
The same command writes the credentials of the new cluster to a file of their own, installs Argo CD, then the root application, as locally. Argo CD installs the AWS version: secrets from Secrets Manager through Pod Identity, the gateway behind a load balancer.
12 / 12 applications ready
Target it runs: make aws-argocd, make aws-bootstrap, make aws-wait
~/platform-eks-gitops
$ make upAWS session opened 0h01 ago, about 11h58 left Terraform init 2s ok Terraform plan 6s okTerraform plan: to create: 28 1 aws_default_security_group, 4 aws_eks_addon, 1 aws_eks_cluster, 1 aws_eks_node_group, 1 aws_eks_pod_identity_association, 3 aws_iam_role, 1 aws_iam_role_policy, 4 aws_iam_role_policy_attachment, 1 aws_internet_gateway, 1 aws_launch_template, 1 aws_route_table, 2 aws_route_table_association, 2 aws_secretsmanager_secret, 2 aws_secretsmanager_secret_version, 2 aws_subnet, 1 aws_vpcThe Kubernetes API will answer to <your IP>/32 only.This costs about $0.18 an hour, until `make down`.Apply it? Only 'yes' will be accepted: yes Terraform apply 11m17s ok 28 of 28 resources Credentials for the new cluster 3s ok Argo CD 49s ok Root application 3s ok Platform, synced by Argo CD 5m45s ok 12/12 readyThe platform is up, in 18m09s. Log: logs/up-20261005-092741.log
On AWS · Step 16/18
Check on EKS
Ten checks for the AWS version: the nodes really run on Spot, the secret store reaches Secrets Manager with Pod Identity, the demo API answers through the load balancer, with the image the CI published. Then the same interfaces as locally, with ENV=aws.
10 / 10 checks on EKS
~/platform-eks-gitops
$ make aws-verifychecking the platform on EKS:ok Argo CD applications all applications healthyok nodes running on spot capacity 2 of 2 nodes on spot capacityok secret store (Pod Identity) Secrets Manager reachable with Pod Identityok secret from Secrets Manager credentials materialised for "admin"ok gateway load balancer a07fe9cf596134f19a0589778c38f0ef-a5a171c81b806794.elb.eu-north-1.amazonaws.comok Grafana behind the gateway reachable over HTTPS (HTTP 200)ok demo API behind the gateway answers through the load balancerok demo API image published by CI, sha-339a41ab09ceok demo API scraped by Prometheus ServiceMonitor in placeok SLO rules from Sloth 6 rule groups generated by Sloth
On AWS · Step 17/18
Destroy it all
make down stops Argo CD so it stops recreating what is being deleted, removes the load balancer Kubernetes made, destroys the rest with Terraform, then asks the AWS API whether anything is left that still bills.
1 bucket the only thing left
~/platform-eks-gitops
$ make downAWS session opened 0h20 ago, about 11h39 left Terraform init 3s ok Terraform plan of the destroy 9s okTerraform plan: to destroy: 28 1 aws_default_security_group, 4 aws_eks_addon, 1 aws_eks_cluster, 1 aws_eks_node_group, 1 aws_eks_pod_identity_association, 3 aws_iam_role, 1 aws_iam_role_policy, 4 aws_iam_role_policy_attachment, 1 aws_internet_gateway, 1 aws_launch_template, 1 aws_route_table, 2 aws_route_table_association, 2 aws_secretsmanager_secret, 2 aws_secretsmanager_secret_version, 2 aws_subnet, 1 aws_vpcDestroy it? Only 'yes' will be accepted: yes Stopping Argo CD, so it stops recreating 5s ok Removing the load balancer Kubernetes made 5s ok Terraform destroy 6m49s ok 28 of 28 resourceschecking nothing is left behind:ok load balancers noneok classic load balancers noneok elastic IPs noneok EBS volumes noneok EKS clusters noneok NAT gateways noneEverything is gone, in 7m23s. The state bucket is the only resource left, and it costs cents.
Continuously · Step 18/18
The CI
The same checks run locally and on every pull request, from the same file: Terraform and Kubernetes linting, secrets detection. When the demo API changes, GitHub Actions also builds its image, tests it and scans it with Trivy, then pushes it to GHCR from main; every week, a workflow checks the age of the base images and scans the published image again.
14 checks on every PR
~/platform-eks-gitops
$ make lintpre-commit run -atrim trailing whitespace.................................................Passedfix end of files.........................................................Passedcheck for merge conflicts................................................Passedcheck for added large files..............................................Passeddetect private key.......................................................Passedcheck yaml...............................................................PassedDetect hardcoded secrets.................................................Passedyamllint.................................................................PassedTerraform fmt............................................................PassedTerraform validate.......................................................PassedTerraform validate with tflint...........................................PassedCheckov..................................................................PassedLint GitHub Actions workflow files.......................................Passedkubeconform..............................................................Passed