Olivier Guandalini
ProjectsExperienceContact

Guided tour · 18 steps

platform-eks-gitops, command by command

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
ON THE MACHINE, IN DOCKERa kubeconfig of its ownAWS ACCOUNT · EU-NORTH-1TO CREATE · VPC 10.20.0.0/16 · EKS 1.36EU-NORTH-1 · VPC 10.20.0.0/16EKS 1.36State bucketnative lockingplatform-eks-gitopsterraform/gitops/apps/IN NUMBERS1 repowhere it all starts6 toolsto run it locally22 sto create the cluster0 registryimported directly49 sfor Argo CD1 rootthe app-of-apps13 / 13applications ready9 / 9checks passed3 ports8081, 3000 and 909074 / 245requests failed0 clustersafter make local-down1 commandthe first time on AWS28resources to create$0.18an hour of demo12 / 12applications ready10 / 10checks on EKS1 bucketthe only thing left14checks on every PRFOR THE LOCAL PLATFORMdockerk3dkubectlhelmpython3curlAND FOR AWSterraformawsroot-local reads the reporoot-aws reads the repoArgo CDapp-of-appsWAVES-2cert-managerEnvoy GatewayExternal Secrets-1Platform CASecretStoreVault0gatewayPrometheusGrafanaSloth1demo-api × 2/chaos: 3 in 10 failGrafana route:8081 · Argo CD:3000 · Grafana:9090 · PrometheusIMAGEdemo-api, imported into k3dWAY INlocalhost:8443public NLB · 80 and 443SECRETSVault in dev modeSecrets Manager · Pod IdentitySLO · AVAILABILITY99.5% of requests without an errorerror budgetburning 60 times too fastburn-rate alert firing→ its runbookSpot node t3.medium · 1aSpot node t3.medium · 1bLocal cluster deletedthe repo stays as it wasSet up by make aws-setupthe profile, and an open sessionthe region eu-north-1 and two zonesthe state bucket, or its creation28 resources to createthe VPC and two public subnetsthe internet gateway, no NATthe EKS cluster and two Spot nodesIAM roles, Pod Identity, 2 secretsCreated by Terraformthe VPC and two public subnetsthe internet gateway, no NATthe EKS cluster and two Spot nodesIAM roles, Pod Identity, 2 secretsEverything is gonethe state bucket alone remainsGitHub Actions, on every pull requestPRE-COMMITfmt, validate, tflint, checkov, gitleaksMANIFESTSkustomize render and kubeconformIMAGE, WHEN THE API CHANGESbuilt, tested, scanned by Trivy, pushed to GHCR from mainEVERY WEEKbase image age, a new scanStatebucketStatebucketplatform-eks-gitopsterraform/gitops/apps/FOR THE LOCAL PLATFORMdockerk3dkubectlhelmpython3curlAND FOR AWSterraformawsON THE MACHINE, IN DOCKERa kubeconfig of its ownAWS ACCOUNT · EU-NORTH-1TO CREATE · VPC 10.20.0.0/16 · EKS 1.36EU-NORTH-1 · VPC 10.20.0.0/16EKS 1.36root-local reads the reporoot-aws reads the repoArgo CDIMAGEdemo-api, imported into k3dWAVES-2cert-managerEnvoy GatewayExternal Secrets-1Platform CASecretStoreVault0gatewayPrometheusGrafanaSloth1demo-api × 2Grafana route/chaos: 3 in 10 failWAY INlocalhost:8443public NLB · 80 and 443SECRETSVault in dev modeSecrets Manager · Pod IdentitySLO · AVAILABILITY99.5% of requests without an errorerror budgetburning 60 times too fastburn-rate alert firing→ its runbook:8081 · Argo CD:3000 · Grafana:9090 · PrometheusSpot node t3.medium · 1aSpot node t3.medium · 1bLocal cluster deletedthe repo stays as it wasSet up by make aws-setupthe profile, and an open sessionthe region eu-north-1 and two zonesthe state bucket, or its creation28 resources to createthe VPC and two public subnetsthe internet gateway, no NATthe EKS cluster and two Spot nodesIAM roles, Pod Identity, 2 secretsCreated by Terraformthe VPC and two public subnetsthe internet gateway, no NATthe EKS cluster and two Spot nodesIAM roles, Pod Identity, 2 secretsSpot node t3.medium · 1aSpot node t3.medium · 1bEverything is gonethe state bucket alone remainsGitHub Actions, on every pull requestPRE-COMMITfmt, validate, tflint, checkov, gitleaksMANIFESTSkustomize render and kubeconformIMAGE, WHEN THE API CHANGESbuilt, tested, scanned by Trivy,pushed to GHCR from mainEVERY WEEKbase image age, a new scan

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.git
Cloning 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.

6 tools to run it locally

~/platform-eks-gitops
$ make check-tools
local platform (make local-up):
  ok       docker       29.1.3
  ok       k3d          v5.9.0
  ok       kubectl      v1.37.0
  ok       helm         v4.3.0+gbec5b06
  ok       python3      3.10.12
  ok       curl         7.81.0
AWS demo (make up):
  ok       terraform    v1.16.1
  ok       aws          2.36.49
contributing (make lint):
  ok       pre-commit   4.6.2
  ok       tflint       0.64.0
  ok       checkov      3.3.19
  ok       kubeconform  v0.8.0
everything needed is there

Locally · Step 03/18

The k3d cluster

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 ready

The 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-verify
checking the local platform:
  ok    Argo CD applications                       all applications healthy
  ok    gateway, HTTP listener                     listener answers (HTTP 404)
  ok    gateway, HTTPS listener                    TLS terminated, issuer CN=platform-local-ca
  ok    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 gateway
  ok    demo API scraped by Prometheus             ServiceMonitor in place
  ok    SLO rules from Sloth                       6 rule groups generated by Sloth
  ok    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-ui
kubectl port-forward -n argocd svc/argocd-server 8081:80
Forwarding from 127.0.0.1:8081 -> 8080
$ make grafana-ui
kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3000:80
Forwarding from 127.0.0.1:3000 -> 3000
$ make prometheus-ui
kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090
Forwarding 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-break
setting 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-load
sending ~5 req/s to https://demo.platform.local:8443/ (127.0.0.1) for 60s
sent 245 requests, 74 of them failed
$ make demo-fix
setting 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-down
deleted 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-setup
AWS 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-profile
Profile (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-og92

Ready. 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 up
AWS session opened 0h01 ago, about 11h58 left
  Terraform init                                      2s  ok
  Terraform plan                                      6s  ok

Terraform 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_vpc
The 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 up
AWS session opened 0h01 ago, about 11h58 left
  Terraform init                                      2s  ok
  Terraform plan                                      6s  ok

Terraform 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_vpc
The 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 ready

The 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-verify
checking the platform on EKS:
  ok    Argo CD applications                       all applications healthy
  ok    nodes running on spot capacity             2 of 2 nodes on spot capacity
  ok    secret store (Pod Identity)                Secrets Manager reachable with Pod Identity
  ok    secret from Secrets Manager                credentials materialised for "admin"
  ok    gateway load balancer                      a07fe9cf596134f19a0589778c38f0ef-a5a171c81b806794.elb.eu-north-1.amazonaws.com
  ok    Grafana behind the gateway                 reachable over HTTPS (HTTP 200)
  ok    demo API behind the gateway                answers through the load balancer
  ok    demo API image                             published by CI, sha-339a41ab09ce
  ok    demo API scraped by Prometheus             ServiceMonitor in place
  ok    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 down
AWS session opened 0h20 ago, about 11h39 left
  Terraform init                                      3s  ok
  Terraform plan of the destroy                       9s  ok

Terraform 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_vpc

Destroy 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 resources

checking nothing is left behind:
  ok    load balancers         none
  ok    classic load balancers none
  ok    elastic IPs            none
  ok    EBS volumes            none
  ok    EKS clusters           none
  ok    NAT gateways           none

Everything 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 lint
pre-commit run -a
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check for merge conflicts................................................Passed
check for added large files..............................................Passed
detect private key.......................................................Passed
check yaml...............................................................Passed
Detect hardcoded secrets.................................................Passed
yamllint.................................................................Passed
Terraform fmt............................................................Passed
Terraform validate.......................................................Passed
Terraform validate with tflint...........................................Passed
Checkov..................................................................Passed
Lint GitHub Actions workflow files.......................................Passed
kubeconform..............................................................Passed