ClickStack on EKS Dev — Runbook
Status (2026-06-05): Sandbox stack is fully deployed end-to-end on
eks-dev-262. ClickHouse + HyperDX accessible via Cloudflare Tunnel + Zero Trust Access. HyperDX is wired to MongoDB Atlas (not in-cluster MongoDB).
What is deployed
| Component | Resource | Notes |
|---|---|---|
| ClickStack chart | helm_release.clickstack v3.0.0 | clickstack/clickstack from oci://registry.clickhouse.com/clickstack/clickstack |
| ClickHouse server | statefulset/clickstack-clickhouse-clickhouse-0 | Altinity operator, 1 replica, gp3 CSI, 100Gi |
| ClickHouse keeper | statefulset/clickstack-keeper-keeper-0 | 1 replica |
| HyperDX | deployment/clickstack-app | 1 replica, port 3000/4320 |
| Cloudflare Tunnel | cloudflare_zero_trust_tunnel_cloudflared.clickstack | name = hh-clickstack-dev |
| Tunnel daemon | deployment/cloudflared (in clickhouse ns) | 2 replicas |
| Zero Trust Access | clickhouse and hyperdx apps | email OTP + Google Workspace IdP, @hungryhub.com allowed |
| External ClickHouse endpoint | https://clickhouse.hhstaging.dev (via tunnel) | routes to clickhouse:8123 |
| External HyperDX endpoint | https://hyperdx.hhstaging.dev (via tunnel) | routes to clickstack-app:3000 |
| HyperDX persistence | MongoDB Atlas cluster staging.owjxrkd.mongodb.net | user clickstack_dev, db hyperdx |
Login to HyperDX
There is no direct HyperDX username/password. Auth is delegated to Cloudflare Access:
- Open
https://hyperdx.hhstaging.dev/ - CF Access redirects to its login page
- Choose Google SSO (Workspace account) or email OTP (one-time code to your
@hungryhub.comaddress) - After auth you land on the HyperDX UI
Configuration sources
All cluster-side config flows from:
| Source | What it provides |
|---|---|
helm-releases/clickhouse.tf | ClickStack Helm release definition, namespace, IAM, secrets |
helm-releases/values/clickhouse.yaml.tft | Per-environment Helm values (replicas, storage, MONGO_URI, etc.) |
eks-services/cloudflared.tf | cloudflared Deployment + tunnel-token K8s Secret |
hh-cloudflare/environments/staging/hhstaging-dev/ | Tunnel + Access apps + DNS CNAMEs |
/hungryhub/dev AWS Secrets Manager | CLICKSTACK_MONGO_URI_DEV (Atlas URI) |
terraform/tfvars/dev/infra AWS Secrets Manager | cloudflare_clickstack_tunnel_token (base64 JSON: {"a","t","s"}) |
The Atlas URI is read by clickhouse.tf via data "aws_secretsmanager_secret_version" at apply time and templated into clickhouse.yaml.tft as hyperdx.config.MONGO_URI. Never stored in TF state or git.
Re-apply procedure (sandbox dev)
# 0. Verify your AWS SSO + gh auth
aws sso login --profile sandbox
gh auth status # should be saiqulhaq-hh
# 1. Update Cloudflare (tunnel + Access + DNS) — if any of those changed
gh workflow run terraform.yml --ref main \
--field environment=dev \
--field services=hh-cloudflare \
--field cloudflare_deployment_target=hhstaging-dev \
--field cloudflare_auto_approve=true \
--field action=apply
# 2. Update EKS (cloudflared pods) — needs tunnel_token in tfvars infra
gh workflow run terraform.yml --ref main \
--field environment=dev \
--field services=eks-services \
--field action=apply
# 3. Update ClickStack (Helm release) — needs CLICKSTACK_MONGO_URI_DEV in /hungryhub/dev
gh workflow run terraform.yml --ref main \
--field environment=dev \
--field services=helm-releases \
--field action=apply
# 4. Force rollout restart to pick up new configmap (env vars are loaded at pod start)
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
rollout restart deployment -n clickhouse clickstack-app
Verification
# Tunnel healthy?
TOKEN_ID="79ea58f8-565f-465c-ad5d-7d6242770f84"
ACCOUNT_ID="6bacbc90386bf7634898c165b7dbc611"
CF_TOKEN="cfut_rZjinOgbMyqbpJAZ13yEgCHZH8yKEEJJOIBzwFMN25448ff5"
curl -s "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/cfd_tunnel/${TOKEN_ID}" \
-H "Authorization: Bearer ${CF_TOKEN}" | python3 -c \
"import sys,json; d=json.load(sys.stdin)['result']; print(d['status'], len(d['connections']))"
# Endpoints respond?
curl -sI https://clickhouse.hhstaging.dev/ # 302 → CF Access login (tunnel working)
curl -sI https://hyperdx.hhstaging.dev/ # 302 → CF Access login
# HyperDX pod is on Atlas?
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
exec -n clickhouse deploy/clickstack-app -- printenv MONGO_URI
# should print mongodb+srv://clickstack_dev:***@staging.owjxrkd.mongodb.net/hyperdx?...
# HyperDX connected to Atlas (look for "Connection established to MongoDB")
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
logs -n clickhouse deploy/clickstack-app --tail=50 | grep -E "MongoDB|Atlas"
Known caveats / pitfalls
-
Chart values path is
hyperdx.config, nothyperdx.configmap. The v2.x chart reads non-sensitive env vars fromhyperdx.config.<KEY>. The old v1.x pathhyperdx.configmap.<KEY>is silently ignored — the chart falls back to its in-cluster MongoDB defaults, which would point at the orphanedclickstack-mongodb-svc:27017. See PR #362 + commitedd0095for the fix. -
mongodb.enabled: falsedoes NOT remove thewait-for-mongodbinit container in chart v3.0.0. Even withmongodb.enabled: false, the chart’s deployment template renders the init containeruntil nc -z clickstack-mongodb-svc 27017, which never succeeds when the in-cluster MongoDB is gone. The pod gets stuck inInit:0/1forever. Fix: sethyperdx.deployment.initContainers: []explicitly in the values to drop the init container. See commitf08221a. -
mongodb.enabled: falsedoes not remove the in-cluster MongoDB subchart resources. The chart still renders theMongoDBCommunityCR, the StatefulSet, the service, and the operator. The MCK operator then creates the actual MongoDB pods. To get rid of in-cluster MongoDB you need to either delete theMongoDBCommunityCR (which the operator watches) or uninstall the chart. We chose the former — see “Cleanup” below. -
hyperdx.config.FRONTEND_URLMUST match the public URL the browser uses (NOThyperdx.frontendUrl— that key does not exist in the v3.0.0 chart, it’s silently ignored). HyperDX reads this env var for: (a) the session cookie’sDomainattribute, (b) constructing invite/reset links shown in the Copy Link button. If unset (chart default =http://localhost:3000), the browser refuses to send the cookie tohyperdx.hhstaging.dev(login spinner hangs) and the invite link points at localhost. Set it viahyperdx.config.FRONTEND_URL: "https://hyperdx.hhstaging.dev"in the values. See commit1e282a4. -
cloudflared needs
--metrics 0.0.0.0:2000for the liveness probe to reach the metrics/ready endpoint. The default islocalhost:2000, which the kubelet can’t reach via the pod IP. The pod would be killed ~36s after start. See commit226e462for the fix. -
ACM validation CNAMEs need
lifecycle.ignore_changes = [content]. CloudFlare normalises the trailing dot on ACM tokens, causing a perpetual no-op diff. See commita383599. -
Pre-existing DNS records must be imported into Terraform state before the first apply. Otherwise the apply fails with CF error 81053 (“A, AAAA, or CNAME record with that host already exists”). Use
terraform import cloudflare_dns_record.<name> <zone_id>/<record_id>for each existing record. -
Ruleset name mismatches need
lifecycle.ignore_changes = [name]. Rulesets created via API have"...via API"suffix; the module writes"...via Terraform". Combined withlifecycle.prevent_destroy, this would block the apply. See commit91e4caf. -
Helm timeouts: the clickstack chart deploys many CRDs/operators and can recreate StatefulSets, so the default 5min timeout is insufficient. Bump
timeout = 900(15 min) on thehelm_releaseresource. If a helm upgrade still times out, runhelm rollback clickstack <last-deployed-revision>to recover, then delete the half-applied release secret (kubectl delete secret -n clickhouse sh.helm.release.v1.clickstack.v<stuck-rev>) before re-applying. See commit08b3f49. -
Helm
frontendUrlchange doesn’t take effect onkubectl rollout restartalone — the values change only propagates to the configmap when the chart’s release is re-applied via Terraform. After a manualkubectl rollout restartyou can verify by reading the configmap (kubectl get cm -n clickhouse clickstack-config -o yaml) and confirming the new keys are present.
SMTP / outbound email (team invites, alerts)
HyperDX uses nodemailer for outbound email. The SMTP transport only activates if the SMTP env vars are set; otherwise the system silently drops the email (e.g. team invites get created in MongoDB but no email is sent).
Configuration
SMTP_HOST = smtp.sendgrid.net(SendGrid SMTP relay)SMTP_PORT = 587(TLS)SMTP_USER = apikey(literal string)SMTP_PASSWORD = <SendGrid API key>— read from/hungryhub/{env}Secrets Manager keySENDGRID_API_KEYat apply time, templated intohyperdx.config.SMTP_PASSWORDin the valuesSMTP_FROM = no-reply@hungryhub.email— must be a verified Single Sender in SendGrid (Settings → Sender Authentication). If not verified, SendGrid will reject the send with553 Unverified email address.
Verify in pod
NEW_POD=$(kubectl get pods -n clickhouse -l app=clickstack-app -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n clickhouse $NEW_POD -- printenv SMTP_HOST SMTP_PORT SMTP_USER SMTP_FROM
kubectl exec -n clickhouse $NEW_POD -- sh -c 'echo "SMTP_PASSWORD length: ${#SMTP_PASSWORD}"'
# Expected: smtp.sendgrid.net / 587 / apikey / no-reply@hungryhub.email / 69
Verify in configmap
kubectl get cm -n clickhouse clickstack-config -o yaml | grep -E "SMTP_"
# Should show SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM
Test the invite flow
-
Log in to HyperDX
-
Team Settings → Invite Team Member → enter your email
-
Check inbox (and spam) for an email from
no-reply@hungryhub.email -
If no email arrives, check the pod logs:
kubectl logs -n clickhouse -l app=clickstack-app --tail=100 | grep -iE "smtp|email|nodemailer|invite"Common errors:
Invalid login: 535 Authentication failed(bad API key),553 Unverified email address(SMTP_FROM not verified in SendGrid),Connection timeout(EKS NAT egress blocked — already allowlisted to13.228.95.112but SendGrid IPs are different and may need allowlisting in your SG). -
CF API token scopes vary by purpose.
CLOUDFLARE_TOKEN_ACCOUNT_LEVEL(in/home/chucky/projects/github.com/hungryhub-team/hh-infra/.env) has the account-levelcfd_tunnel+ DNS scopes. It does NOT haveZone:Zone WAF(rulesets) orAccount:Access:Policiesby default. If you hit 403 on ruleset or access resources, add those scopes to the token in the Cloudflare dashboard.
Cleanup (in-cluster MongoDB)
If mongodb.enabled: false is set but the in-cluster MongoDB is still running (left over from a previous apply with enabled: true), the cleanest way to remove it is:
# 1. Delete the MongoDBCommunity CR — this releases the StatefulSet
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
delete mongodbcommunity clickstack-mongodb -n clickhouse
# 2. Now safely delete the rest
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
delete statefulset,svc,deployment -n clickhouse \
clickstack-mongodb clickstack-mongodb-arb clickstack-mongodb-svc mongodb-kubernetes-operator
# 3. Secrets
for s in clickstack-mongodb-agent-password clickstack-mongodb-config \
clickstack-mongodb-hyperdx-hyperdx clickstack-mongodb-keyfile \
clickstack-mongodb-password clickstack-mongodb-scram-scram-credentials; do
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
delete secret -n clickhouse $s
done
# 4. ClusterRoles + ClusterRoleBindings
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
delete clusterrole mongodb-kubernetes-operator-clickhouse-cluster-mongodb-role \
mongodb-kubernetes-operator-clickhouse-webhook-cr \
mongodb-kubernetes-operator-cluster-telemetry
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
delete clusterrolebinding mongodb-kubernetes-operator-clickhouse-cluster-mongodb-role-binding \
mongodb-kubernetes-operator-clickhouse-cluster-telemetry-binding \
mongodb-kubernetes-operator-clickhouse-webhook-crb
# 5. PVCs
kubectl --context=arn:aws:eks:ap-southeast-1:079994049689:cluster/eks-dev-262 \
delete pvc -n clickhouse data-volume-clickstack-mongodb-0 logs-volume-clickstack-mongodb-0
The order matters — delete the CR first, otherwise K8s garbage collection won’t release the StatefulSet-owned PVCs.
Issue tracking
EPIC: https://github.com/hungryhub-team/hungryhub-terraform/issues/339
| Issue | Status |
|---|---|
| #339 EPIC: ClickHouse on EKS — Development to Production | Open |
| #340 [DEV/SANDBOX] ClickHouse on EKS Setup | Closed |
| #341 [PROD] ClickHouse on EKS Setup | Open (blocked on prod tunnel + prod Atlas URI) |
| #343 📦 [SANDBOX] Deploy ClickHouse Helm releases | Closed |
| #344 🔌 [SANDBOX] Test ClickHouse HTTP and Native endpoints | Closed |
| #345 🔄 [SANDBOX] Verify existing DMS S3 pipeline | Open |
| #346 [SANDBOX] 1. Verify EKS cluster access | Closed |
| #347 [SANDBOX] 2. Deploy ClickStack Helm charts | Closed |
| #348 [SANDBOX] 3. Verify ClickHouse and HyperDX endpoints | Closed |
| #349 [SANDBOX] 4. Configure DMS S3 pipeline for data migration | Open |
| #350 [PROD] 1. Verify EKS cluster access (eks-prod-21) | Open |
| #351 [PROD] 2. Deploy ClickStack Helm charts to production | Open |
| #352 [PROD] 3. Verify ClickHouse and HyperDX in production | Open |
| #353 [PROD] 4. Configure DMS S3 pipeline for booking_production | Open |