GitOps infrastructure repository. ArgoCD watches this repo and reconciles all changes to the cluster automatically.
| Directory | Purpose |
|---|---|
pulumi/networking/ |
Pulumi Go IaC — VPCs, Transit Gateway, Client VPN, EKS clusters |
argocd/install/ |
ArgoCD installation (Kustomize overlay, self-managed) |
argocd/apps/ |
ArgoCD Application and ApplicationSet manifests |
argocd/root.yaml |
App of Apps root — auto-registers everything in argocd/apps/ |
infrastructure/ |
Cluster infrastructure managed by ArgoCD |
apps/ |
Team application workloads |
.claude/skills/ |
Claude Code skills for this repo |
- ArgoCD v3.4.2 — GitOps controller, self-manages from
argocd/install/ - Envoy Gateway v1.2.0 — Kubernetes Gateway API implementation
- Sealed Secrets — Encrypt secrets with a cluster key so they can be committed to Git
Applications follow a Kustomize base + overlays pattern:
apps/
└── <team>/
└── <app>/
├── base/ # Shared manifests (all environments)
│ ├── deployment.yaml
│ ├── service.yaml # Omit if the app has no port
│ └── kustomization.yaml
└── overlays/
├── dev/ # Auto-synced on push
├── qa/ # Auto-synced on push
└── prod/ # Manual sync required
Namespaces are created automatically: <team>-dev, <team>-qa, <team>-prod.
ArgoCD Applications are generated automatically by the ApplicationSets in argocd/apps/. No ArgoCD YAML is required per app — just push the directory structure.
Open this repo in Claude Code and run:
/new-app
The skill will prompt you for team name, app name, container image, and (optionally) a port, then generate all required files and offer to commit.
Installing the skill: The skill is bundled in .claude/skills/new-app/SKILL.md and is automatically available when you open this repo in Claude Code — no installation required.
cp -r apps/team-a/example-app apps/<your-team>/<your-app>Then update:
- Namespaces in each overlay (
overlays/*/kustomization.yaml) fromteam-a-*to<your-team>-* - The image name in
base/deployment.yaml(no tag — tag goes in overlays) - The
images[0].nameandimages[0].newTagin each overlay - Remove
base/service.yamland its entry inbase/kustomization.yamlif your app has no port
Commit and push. ArgoCD will deploy to dev and qa automatically within ~3 minutes.
Image tags live in each overlay's kustomization.yaml under images:, not in base/deployment.yaml. This lets environments run independently — dev can have a new build while prod stays on a validated version.
# apps/<team>/<app>/overlays/dev/kustomization.yaml
images:
- name: ghcr.io/org/my-app
newTag: abc1234 # ← update this to promotePromotion workflow:
- Build and push a new image from your app repo (
ghcr.io/org/my-app:abc1234) - Open a PR updating
newTaginoverlays/dev/kustomization.yaml→ merge → ArgoCD auto-deploys to dev - After validating in dev, open a PR updating
newTaginoverlays/qa/kustomization.yaml→ merge → ArgoCD auto-deploys to qa - After validating in qa, open a PR updating
newTaginoverlays/prod/kustomization.yaml→ merge → trigger a manual sync in the ArgoCD UI to deploy to prod
Every PR opened in an app's source repo gets its own isolated preview namespace deployed automatically. ArgoCD's ApplicationSet Pull Request generator polls GitHub for open PRs, creates a namespaced Application per PR, and deletes it when the PR is closed.
- Developer opens PR in the app source repo
- CI builds and pushes an image tagged with the PR's head commit SHA:
ghcr.io/org/my-app:<sha> - ArgoCD detects the PR (polls every 3 min), creates Application
<team>-<app>-pr-<number> - Application deploys to namespace
<team>-<app>-pr-<number>using the dev overlay, with the image tag overridden to<sha> - PR is merged or closed → ArgoCD deletes the Application and namespace automatically
The PR generator authenticates with GitHub using a Personal Access Token stored as a SealedSecret in the argocd namespace. Create it once per cluster:
- Generate a GitHub PAT with
reposcope (private repos) orpublic_reposcope (public repos) - Encrypt and commit it:
kubectl create secret generic github-token \
--dry-run=client \
--from-literal=token=<YOUR_GITHUB_PAT> \
-n argocd \
-o yaml \
| kubeseal --format yaml > argocd/install/github-token-sealed.yaml- Add
github-token-sealed.yamltoargocd/install/kustomization.yamlunderresources: - Commit and push — ArgoCD will deploy the SealedSecret and the controller will decrypt it
Your app repo's CI pipeline must build and push an image tagged with the full head commit SHA when a PR is opened or updated:
# Example GitHub Actions step
- name: Build and push PR image
run: |
docker build -t ghcr.io/org/my-app:${{ github.event.pull_request.head.sha }} .
docker push ghcr.io/org/my-app:${{ github.event.pull_request.head.sha }}The preview ApplicationSet uses {{head_sha}} to reference this exact tag.
Run /new-app in Claude Code — preview ApplicationSets are generated automatically as part of every scaffold. The ApplicationSet file is created at argocd/apps/<team>-<app>-preview.yaml and committed alongside the app manifests.
To add previews manually to an existing app, copy argocd/apps/team-a-example-app-preview.yaml, update github.owner, github.repo, the path:, name:, namespace:, and images: fields, and commit.
Use Sealed Secrets to store secrets in Git. The controller's public key is in the cluster — never commit the private key.
Important: Each cluster has its own Sealed Secrets encryption key. A secret sealed for one cluster cannot be decrypted by another. Seal secrets per environment by running kubeseal against the appropriate cluster:
# Encrypt a secret for a specific environment cluster
kubectl create secret generic my-secret \
--dry-run=client \
--from-literal=password=... \
-n <team>-<env> \
-o yaml \
| kubeseal --controller-namespace kube-system --format yaml \
> apps/<team>/<app>/overlays/<env>/sealed-secret.yamlRun this once per environment (dev, qa, prod), each time with your kubeconfig pointed at the respective cluster. Commit all three resulting files.
Commit the SealedSecret YAML. ArgoCD will deploy it; the controller decrypts it at runtime.
Back up the controller key for each cluster — if a cluster is recreated without its key, sealed secrets cannot be decrypted:
kubectl -n kube-system get secret \
-l sealedsecrets.bitnami.com/sealed-secrets-key=active \
-o yaml > sealed-secrets-key-backup-<cluster>.yaml
# Store outside the cluster and outside this repoThe underlying AWS resources (VPCs, Transit Gateway, Client VPN, EKS clusters) are managed by the Pulumi project in pulumi/networking/. Run this before the cluster bootstrap steps below.
Three stacks — ops, qa, prod — must be deployed in order:
cd pulumi/networking
# One-time: set VPN cert ARNs (certs must exist in ACM first)
pulumi stack select ops
pulumi config set --secret networking:vpnServerCertArn arn:aws:acm:...
pulumi config set --secret networking:vpnClientCaArn arn:aws:acm:...
# Also update networking:opsStackRef in Pulumi.qa.yaml and Pulumi.prod.yaml
# to match your Pulumi org: <org>/networking/ops (run: pulumi whoami)
pulumi stack select ops && pulumi up # ~25 min — VPN association is slow
pulumi stack select qa && pulumi up
pulumi stack select prod && pulumi up| Stack | VPC CIDR | EKS cluster | Notes |
|---|---|---|---|
| ops | 10.0.0.0/16 | mgmt (hosts ArgoCD) | owns TGW + Client VPN |
| qa | 10.1.0.0/16 | qa | attaches to TGW |
| prod | 10.2.0.0/16 | prod | attaches to TGW |
Retrieve the mgmt cluster kubeconfig after the ops stack deploys:
pulumi stack select ops
pulumi stack output kubeconfig --show-secrets > ~/.kube/mgmt-config
export KUBECONFIG=~/.kube/mgmt-configRun these steps once after the Pulumi stacks are up. ArgoCD runs on the mgmt cluster (ops stack) and manages the qa and prod clusters remotely.
HA install: The mgmt cluster should have at least 3 nodes so Redis HA sentinel pods (3 replicas) and server/repo-server pod anti-affinity rules can spread across nodes. Pods will still schedule on fewer nodes but without true failure isolation.
# 1. On the mgmt cluster — install ArgoCD
kubectl apply --server-side -k argocd/install/
# 2. Wait for all ArgoCD HA components
kubectl -n argocd rollout status deployment/argocd-server
kubectl -n argocd rollout status deployment/argocd-repo-server
kubectl -n argocd rollout status deployment/argocd-applicationset-controller
kubectl -n argocd rollout status statefulset/argocd-application-controller
# 3. Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d && echo
# 4. Log in with the argocd CLI
argocd login <ARGOCD_URL>
# 5. Register the app clusters (run once per cluster)
# Retrieve kubeconfigs from Pulumi stack outputs:
# pulumi stack select qa && pulumi stack output kubeconfig --show-secrets > /tmp/qa-kube
# pulumi stack select prod && pulumi stack output kubeconfig --show-secrets > /tmp/prod-kube
argocd cluster add <QA_KUBECONFIG_CONTEXT> --name qa-cluster
argocd cluster add <PROD_KUBECONFIG_CONTEXT> --name prod-cluster
# 6. Register the self-managing Application
kubectl apply -f argocd/apps/argocd.yaml
# 7. Bootstrap Envoy Gateway on mgmt cluster
# (ArgoCD will manage it on all clusters after step 8, but the mgmt cluster
# needs it running before the root Application can sync the HTTPRoute)
kubectl apply --server-side \
-f https://github.com/envoyproxy/gateway/releases/download/v1.2.0/install.yaml
# 8. (Recommended) Bootstrap cert-manager on mgmt cluster before registering root.
# This prevents the ArgoCD self-managing Application from briefly showing degraded
# due to the Certificate resource in argocd/install/ referencing cert-manager CRDs
# that don't exist yet. Skip this step if you're okay with eventual consistency.
kubectl apply --server-side -k infrastructure/cert-manager/
# 9. Register the App of Apps root (auto-registers everything else)
kubectl apply -f argocd/root.yamlAfter step 9, ArgoCD manages everything: it deploys Sealed Secrets, Envoy Gateway, and cert-manager to the appropriate clusters automatically, and syncs app workloads to the qa and prod clusters based on their overlay path.
To keep cluster registrations in Git (so they survive a mgmt cluster rebuild), seal and commit the cluster secrets ArgoCD created:
kubectl get secret -n argocd -l argocd.argoproj.io/secret-type=cluster \
-o yaml | kubeseal --format yaml > argocd/install/cluster-secrets-sealed.yaml
# Add cluster-secrets-sealed.yaml to argocd/install/kustomization.yaml resourcesThe Gateway API route is configured for argocd.local. Once the LoadBalancer IP is assigned:
- HTTPS:
https://argocd.local(self-signed cert — accept the browser warning) - HTTP: automatically redirects to HTTPS
On a local cluster without a cloud LoadBalancer, use port-forward instead:
kubectl -n argocd port-forward svc/argocd-server 8080:80
# Open http://localhost:8080