title, description, category, tags, resources
| title | description | category | tags | resources | |||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| GitOps on EKS (Argo CD + EBS-CSI) | Deliver a Spinifex-themed app to EKS with GitOps — the Argo CD addon syncs it from a git repo, an EBS-CSI (Viperblock) PersistentVolume holds its state, and it is served over HTTPS through the AWS Load Balancer Controller + ACM, using Terraform on Spinifex. | Terraform Workbooks |
|
|
Terraform: GitOps on EKS (Argo CD + EBS-CSI)
The top rung of the EKS ladder. It extends EKS HTTPS Ingress and changes how the app is delivered: the Argo CD addon syncs a more elaborate Spinifex-themed app from a git repo, an EBS-CSI PersistentVolume (Viperblock-backed) holds its state, and it is still served over HTTPS through the AWS Load Balancer Controller + ACM.
Table of Contents
Overview
This workbook keeps everything from EKS HTTPS Ingress — the public/private VPC, the NAT gateway, the LBC addon, the ACM cert, the HTTPS Ingress — and adds the parts you reach for once an app is real: GitOps delivery and persistent storage.
Three changes over the previous rung:
- Argo CD delivers the app. Instead of Terraform applying the Deployment, the argocd addon syncs the app from a git repository (
mulgadc/eks-demo-app). Terraform only registers the repo credential and creates the Argo CDApplication; Argo CD reconciles the manifests and self-heals drift. - State lives on an EBS volume. The aws-ebs-csi-driver addon ships a default gp3 StorageClass. The app's
PersistentVolumeClaim(in the git repo) dynamically provisions a Viperblock-backed EBS volume; the demo's hit counter persists to it and survives pod restarts. - Access-entry RBAC. A second IAM principal is granted read-only cluster access via an access entry bound to
AmazonEKSViewPolicy.
Terraform manages the cluster, the addons, the ACM cert, and the HTTPS Ingress; Argo CD manages the app's Deployment, Service, and PVC from git. The Ingress points at the Service Argo CD creates, so the app is reachable over HTTPS the moment Argo CD finishes its first sync.
What you'll learn:
- Installing the
argocdandaws-ebs-csi-driveraddons through the EKS API - Registering a private git repo with Argo CD and driving an
Applicationfrom Terraform - Dynamically provisioning a Viperblock-backed EBS volume with a
PersistentVolumeClaim - Splitting ownership: Terraform owns infra + Ingress, Argo CD owns the workload
- Granting scoped read-only access with an EKS access entry
What gets created
| Resource | Name | Purpose |
|---|---|---|
| VPC + subnets | eks-gitops-* |
Public/private network (10.32.0.0/16) with a NAT gateway |
| IAM Roles | eks-gitops-cluster-role, -node-role, -viewer |
Control-plane, worker, and read-only viewer roles |
| ECR Repository | spinifex-demo |
Holds the demo image the workers pull |
| EKS Cluster | eks-gitops |
Public + private endpoints; managed-ingress=false |
| Node Group | workers |
node_desired_size t3.large worker(s) — 1 or 3 |
| Addons | aws-load-balancer-controller, argocd, aws-ebs-csi-driver |
Ingress, GitOps delivery, persistent storage |
| ACM Certificate | eks-gitops-ingress |
Self-signed, imported; on the HTTPS listener |
| Access Entry + Assoc. | eks-gitops-viewer → AmazonEKSViewPolicy |
Read-only, cluster scope |
| Argo CD repo Secret | eks-demo-app-repo |
Credential for the private git repo |
| Argo CD Application | spinifex-demo |
Syncs the app from git |
| K8s Ingress | spinifex-demo |
ingressClassName: alb, HTTPS via the ACM cert |
| Git-managed (by Argo CD) | Deployment, Service, PVC | The app + its Viperblock-backed volume |
Spinifex specifics
- The
argocdandaws-ebs-csi-driverbundles must be baked into theeks-nodeAMI —describe-addonreturnsno baked bundleotherwise. - The EBS-CSI default StorageClass uses
provisioner: ebs.csi.aws.com; a PVC against it provisions a Viperblock-backed EBS volume. - If the PVC stays
Pending, the app still serves (the counter falls back to in-memory) — see Troubleshooting. - Leave addon versions unset (the AWS provider and the catalog disagree on the version-string format).
- The Argo CD
Applicationis akubernetes_manifest, so theargoproj.ioCRDs must exist beforeapply— apply the parent and let the addon reachACTIVEfirst.
Prerequisites:
- Spinifex running with an
eks-nodeimage carrying the LBC, Argo CD, and EBS-CSI bundles - OpenTofu or Terraform, plus
kubectl, the AWS CLI, and Docker - The demo image built and pushed to ECR (see
../demo-app/README.md) - A git repo with the app manifests (
mulgadc/eks-demo-app); a read-only PAT if it is private
Instructions
Step 1. Get the Template
git clone --depth 1 --filter=blob:none --sparse https://github.com/mulgadc/spinifex.git spinifex-tf
cd spinifex-tf
git sparse-checkout set docs/terraform-workbooks
cd docs/terraform-workbooks/eks-gitops-argocd
Step 2. Deploy the Cluster + Addons
export AWS_PROFILE=spinifex
tofu init
tofu apply # add -var node_desired_size=3 for an HA-shaped cluster
Wait for the cluster to reach ACTIVE and for all three addons (aws-load-balancer-controller, argocd, aws-ebs-csi-driver) to report healthy:
aws eks list-addons --cluster-name eks-gitops
aws eks describe-addon --cluster-name eks-gitops --addon-name argocd --query 'addon.status'
Step 3. Build and Push the Demo Image
cd ../demo-app
REGISTRY=$(cd ../eks-gitops-argocd && tofu output -raw ecr_repository_url)
REGISTRY_HOST=${REGISTRY%%/*}
aws ecr get-login-password | docker login --username AWS --password-stdin "$REGISTRY_HOST"
docker build -t "${REGISTRY}:latest" .
docker push "${REGISTRY}:latest"
cd ../eks-gitops-argocd
Point the app's manifests at this image: set the image ref in eks-demo-app/manifests (a kustomize images: override) to your ${REGISTRY}:latest, and push that to the git repo.
Step 4. Hand Delivery to Argo CD
cd workloads
tofu init
tofu apply \
-var git_repo_url=https://github.com/mulgadc/eks-demo-app.git \
-var git_token=<read-only-PAT> # omit for a public repo
This registers the repo credential, creates the Argo CD Application, the demo app's HTTPS Ingress, and an HTTPS Ingress for the Argo CD UI itself. Argo CD then syncs the Deployment, Service, and PVC from git.
Step 5. Watch the Sync and Open the App
The demo app and the Argo CD UI share one ALB (an LBC IngressGroup). On :443 the app is the catch-all and argocd.eks-gitops.spinifex.local host-routes to the UI; Argo CD also gets a hostless :8443 listener so it is reachable on the raw ALB IP before DNS exists. Grab the shared ALB address once:
# Point kubectl at the cluster first (writes ~/.kube/config) — without this every
# kubectl call fails to connect:
aws eks update-kubeconfig --name eks-gitops --region ap-southeast-2
kubectl -n argocd get applications
kubectl get pods -o wide
kubectl get pvc # the EBS-CSI volume should be Bound
DNSNAME=$(kubectl get ingress spinifex-demo -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
ALB_IP=$(aws elbv2 describe-load-balancers \
--query "LoadBalancers[?DNSName=='${DNSNAME}'].AvailabilityZones[].LoadBalancerAddresses[].IpAddress | [0]" \
--output text)
echo "$ALB_IP"
Reach it by raw IP (no DNS, no host header):
| Service | URL |
|---|---|
| Demo app | https://<ALB_IP>/ |
| Argo CD UI | https://<ALB_IP>:8443/ |
curl -k https://"$ALB_IP"/ # app — returns the Spinifex page
curl -k https://"$ALB_IP":8443/ # Argo CD UI
The bare ALB IP serves the app on :443. Hitting https://<ALB_IP>/ and expecting Argo CD gives the app instead — use :8443 for the UI. Once northstar (or Route 53) resolves the hostnames to the ALB, app.eks-gitops.spinifex.local and argocd.eks-gitops.spinifex.local both work on :443. Self-signed cert — accept the browser warning.
The page shows the "persisted to EBS volume" badge and a hit counter that keeps climbing — delete the pod (kubectl delete pod -l app=spinifex-demo) and the count survives, proving the volume is durable.
Step 6. Open the Argo CD UI
Managing deployments through the Argo CD console is the point of this workbook, so the UI is exposed on the same ALB as the app, not behind a port-forward. The workloads module adds a NodePort Service in front of argocd-server (the addon ships it ClusterIP only) and two Ingresses in the shared group — a host-routed one on :443 and a hostless one on :8443 for raw-IP access. argocd-server serves TLS on its own port, so both use backend-protocol: HTTPS.
Get the admin credentials. Argo CD generates a one-time admin password into a Secret on install; the username is always admin:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d ; echo
(tofu output argocd_admin_password_cmd prints this same command.)
Open the UI and log in as admin:
- By raw IP:
https://<ALB_IP>:8443/ - By DNS once wired:
https://argocd.eks-gitops.spinifex.local
The spinifex-demo Application shows the sync status, the resource tree, and live diffs against git — change a manifest in the repo and watch Argo CD reconcile it.
If you'd rather not expose the UI at all, port-forward instead:
kubectl -n argocd port-forward svc/argocd-server 8080:443
# then open https://localhost:8080
Cleanup
Destroy in reverse — the Argo CD Application + Ingress first (so Argo CD prunes the workload and the LBC tears the ALB down), then the cluster:
cd workloads
tofu destroy
cd ..
tofu destroy
Troubleshooting
Application Won't Sync
kubectl -n argocd get applications
kubectl -n argocd describe application spinifex-demo # conditions show repo/auth errors
kubectl -n argocd logs deploy/argocd-repo-server --tail=50
For a private repo, confirm the eks-demo-app-repo Secret exists in the argocd namespace with the argocd.argoproj.io/secret-type=repository label and a valid token.
kubernetes_manifest Fails: no matches for kind "Application"
The argoproj.io CRDs aren't installed yet. Apply the parent module and wait for the argocd addon to reach ACTIVE before applying workloads/.
PVC Stuck in Pending
kubectl get pvc
kubectl describe pvc <name>
kubectl get storageclass
Confirm the aws-ebs-csi-driver addon is ACTIVE and the default StorageClass exists. The app still serves with an in-memory counter if the volume never binds.
The Ingress Has No Address
The LBC populates status.loadBalancer once it provisions the ALB. Check the addon and the controller logs:
kubectl describe ingress spinifex-demo
kubectl -n kube-system logs deploy/aws-load-balancer-controller --tail=50
Confirm the cluster carries spinifex.io/managed-ingress = "false" so the LBC owns ingress.
If the controller logs show FailedBuildModel ... DescribeAvailabilityZones ... 403 ... AccessDenied, the node role is missing the LBC permissions. The controller runs with the node instance-profile credentials, so the ${var.cluster_name}-node-lbc policy (aws_iam_policy.node_lbc) must be attached to the node role — re-run tofu apply on the parent module if it was provisioned before that policy existed.
Provider Connection Refused
sudo systemctl status spinifex.target
curl -k https://localhost:9999/