A realistic, end-to-end EKS + GitOps reference - infrastructure to deployments to observability, autoscaling, backup, and policy, all reconciled from one repo. The demo trade-offs are documented, so the line between learning environment and production hardening is explicit.
Self-managed Kubernetes carries heavy operational toil - control-plane and etcd upgrades, config drift between environments, manual kubectl apply deploys, and a scattered story for observability, secrets, and policy. Standardising on managed EKS removes some of that, but a running cluster is only the first 10%.
Most references stop exactly there: "here's an EKS cluster." What's missing is everything that makes it a platform - how apps get deployed and reconciled, how you see metrics/logs/traces, how nodes scale, how you back up and restore, how policy is enforced, and what it all costs. Wiring those ~30 components together correctly is the actual work, and it's rarely shown end to end.
I wanted a concrete, opinionated, working example of how a real EKS + GitOps platform is assembled - closer to production complexity than a tutorial, but with the cost and shortcuts made explicit so you know exactly what to harden before taking it further.
The organising principle is GitOps as the single source of truth: Terraform provisions the AWS substrate, and from there everything - the app, monitoring, autoscaling, logging, tracing, backup, and policy - reconciles declaratively from this one repo via ArgoCD. Nothing is applied by hand; the cluster converges to what's in Git and self-heals when it drifts.
A root module wires a VPC (2 public + 2 private subnets across 2 AZs) and an EKS 1.34 cluster. The managed node group is tainted app=core and runs only platform components; Karpenter provisions separate app=workload nodes on demand, so a crash-looping or noisy app can't destabilise the control-plane components that manage the cluster. Terraform also creates every IRSA role, the S3 buckets (Thanos / Loki / Velero / Tempo), and the Secrets Manager secrets - then hands off to GitOps through ArgoCD cluster-secret annotations, so the manifests carry no hardcoded account IDs, bucket names, or ARNs and the same repo works across clusters.
ArgoCD is self-managed via Helm. A single root Application recursively syncs the repo and acts as a stable, self-healing anchor - you kubectl apply it to a fresh cluster and it takes over. (A plain ApplicationSet root would be circular: the generator must already be running before it can generate the apps that install it.) Each subsystem is then an ApplicationSet, ordered with sync-waves so CRDs land before the controllers that need them and workloads land last. The layers:
gp3 as the default StorageClass.The demo workload, SimpleTimeService (a tiny Python service returning timestamp + caller IP), ships as a Helm chart complete with Ingress, HPA, PodDisruptionBudget, NetworkPolicy, and ServiceMonitor - so it exercises the whole platform, not just "hello world."
ClusterSecretStore; the operator's IRSA policy grants GetSecretValue only on secrets tagged ExternalSecret=true, so a compromised ESO can't read arbitrary account secrets - just the opted-in set.ClusterPolicy rules (resource limits, no privileged containers, no :latest, non-root) evaluate every pod and write PolicyReport objects (Audit mode by default, surfaced in Policy Reporter).workloads can't deploy into kube-system or mint a ClusterRoleBinding, and drift in one concern can't cascade into another.gp3 StorageClass is encrypted and uses reclaimPolicy: Retain, so an accidental prune or kubectl delete pvc leaves the EBS volume intact (a human must delete the PV) instead of silently destroying Prometheus/Thanos data.One root app owns the platform; ApplicationSets template the ~30 components. Adding or changing a component is a Git commit, and ArgoCD reconciles and self-heals it - no imperative install steps to drift.
Components are interdependent, so waves sequence them: CRDs at wave 0, controllers next, workloads last. Prometheus Operator CRDs are a separate app so they can be upgraded safely without churning the whole stack.
The full kube-prometheus-stack (Alertmanager, rules, dashboards) lives in Git and stays cloud-portable; Thanos ships blocks to S3 with tiered downsampling (raw 30d → 5m 90d → 1h 180d), which at this cardinality is materially cheaper than AMP's per-sample pricing - and AMP needs a self-managed Alertmanager anyway, so going half-managed buys little.
They operate at different levels: Cluster Autoscaler scales the managed node groups (minutes, ASG-coupled - a safety net for pinned/system workloads); Karpenter provisions right-sized nodes outside any group in seconds, bin-packs, and handles spot interruption natively. Karpenter does the fast, flexible app scaling; CA holds baseline capacity.
Thanos, Loki, Tempo, and Velero each get their own S3 bucket (11 nines, pay-per-byte, survives cluster/AZ loss). Separate buckets give per-workload IRSA scoping, independent lifecycle/retention, and separate audit trails - a single shared bucket would need brittle prefix-based IAM.
The log agent is a DaemonSet on every node, so footprint compounds: Fluent Bit (C) idles at ~450 KB vs Fluentd's (Ruby) ~40 MB, and starts fast - which matters when Karpenter churns nodes and a slow agent means a window of dropped logs per launch.
A DaemonSet OTel agent takes each pod's spans node-locally (no cross-node hop; the app SDK stays fire-and-forget); a central gateway owns the durable buffering and the single Tempo connection - so a Tempo restart or backpressure is absorbed in one place, not propagated to every node.
An app-bootstrap module gets a service up on a public NLB with one terraform apply (no ArgoCD) for a fast look; the full GitOps path provisions infra only and lets ArgoCD own the rest.
| Decision | Gained | Gave up |
|---|---|---|
| Managed EKS | No etcd/control-plane ops; supported upgrades | Control-plane cost vs self-managed |
| ArgoCD GitOps | Declarative, auditable, self-healing platform | Another controller to run + a learning curve |
| Kyverno in Audit mode | Full policy visibility with zero breakage risk | Nothing is enforced until you flip to Enforce |
| S3-backed observability | Cheap, durable retention that survives churn | Query latency vs. local block storage |
| Single cluster / region | Simple, legible, affordable to run | Not multi-cluster or multi-region HA |
Running continuously costs roughly $310–330/month at idle (on-demand, ap-south-1), $400–500 under load - documented up front, with a one-command teardown so at rest only S3/Route53/Secrets Manager accrue (< $10/mo).
terraform destroy - because half-deleted LBs and un-emptied buckets otherwise block Terraform.
Synced / Healthy from the root app.

trace_id / span_id fields.

[FIRING] / [RESOLVED] notifications to Slack with summary, description, and severity.Two paths - a fast look, or the full platform.
# 1. Slack webhook is required; admin passwords auto-generate if omitted $ export TF_VAR_alertmanager_slack_webhook_url="https://hooks.slack.com/..." # 2. Bootstrap - provisions infra, installs ArgoCD via Helm, applies projects + root app $ bash terraform/scripts/bootstrap.sh # 3. Watch ArgoCD reconcile the platform (first sync takes ~3 min) $ kubectl get applications -n argocd -w
Once synced, services come up at argocd.platform.<domain>, grafana.platform.<domain>, and simple-time-service.platform.<domain> (Prometheus / Alertmanager / Policy Reporter via kubectl port-forward).
# one apply provisions VPC + EKS + the Helm release, on a public NLB - no ArgoCD $ cd terraform/app-bootstrap && terraform init && terraform apply $ aws eks update-kubeconfig --region ap-south-1 --name simple-eks $ curl $(terraform output -raw application_url)
A validated wildcard ACM certificate for *.platform.<domain> must exist before bootstrap.sh - the ALB controller discovers it by hostname match. Tear everything down with bash terraform/scripts/cleanup.sh.
The production-hardening directions (tracked in docs/security.md):
Full detail lives in the repo's design-decisions and security docs.