# 3-Pool Node Split Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Split the cluster into 3 isolated node pools (gitlab / platform / foxhunt) by creating a new `platform` pool, migrating infrastructure + monitoring there, and repurposing one `services` node as `foxhunt`. **Architecture:** Create `platform` DEV1-L pool in Scaleway. Move stateless monitoring first (low risk), then stateful databases (requires PVC migration with backups), then rename services → foxhunt for app pods and drop the extra node. **Tech Stack:** Scaleway Kapsule, Terragrunt/Terraform, kubectl, pg_dump/pg_restore, sed --- ### Task 1: Create `platform` Node Pool via Terraform (Phase 1 — additive only) **Context:** The cluster is managed by Terragrunt at `infra/live/production/kapsule/`. We need to create the `platform` pool first (additive) WITHOUT renaming `services` → `foxhunt` yet. The rename would destroy the existing services pool before databases are migrated. We do this in two Terraform applies. **Files already modified:** - `infra/modules/kapsule/main.tf` — added `platform` pool resource, renamed `services` → `foxhunt` - `infra/modules/kapsule/variables.tf` — added `platform_type`, `enable_platform_pool`, renamed `services_*` → `foxhunt_*` - `infra/modules/kapsule/outputs.tf` — added `platform_pool_id`, renamed `services_pool_id` → `foxhunt_pool_id` - `infra/live/production/kapsule/terragrunt.hcl` — updated inputs **Step 1: Run terragrunt plan to preview changes** ```bash cd infra/live/production/kapsule terragrunt plan ``` Expected output should show: - `scaleway_k8s_pool.platform[0]` — **create** (new platform pool) - `scaleway_k8s_pool.services` — **destroy** (old name being removed) - `scaleway_k8s_pool.foxhunt` — **create** (new name for app services) **IMPORTANT:** Do NOT apply yet — the destroy of `services` would evict all pods before databases are migrated. **Step 2: Use `-target` to apply ONLY the platform pool first** ```bash cd infra/live/production/kapsule terragrunt apply -target=scaleway_k8s_pool.platform ``` This creates the platform pool without touching the existing services pool. **Step 3: Wait for platform node to become Ready** ```bash kubectl get nodes -l k8s.scaleway.com/pool-name=platform -w ``` Expected: Node shows `STATUS=Ready` within 2-3 minutes. **Step 4: Verify node resources** ```bash kubectl get node -l k8s.scaleway.com/pool-name=platform \ -o custom-columns="NAME:.metadata.name,CPU:.status.allocatable.cpu,MEM:.status.allocatable.memory" ``` Expected: CPU ~3800m, MEM ~6450608Ki (DEV1-L allocatable). **Step 5: Commit Terraform changes** ```bash git add infra/modules/kapsule/main.tf infra/modules/kapsule/variables.tf \ infra/modules/kapsule/outputs.tf infra/live/production/kapsule/terragrunt.hcl git commit -m "infra(tf): add platform pool, rename services → foxhunt in Kapsule module" ``` --- ### Task 2: Move Stateless Monitoring to `platform` (Loki, Tempo, kube-state-metrics, Pushgateway) **Context:** These pods are currently on the `gitlab` node (loki, tempo, kube-state-metrics) or `services` node (pushgateway). They have no persistent data that can't be recreated. Moving them is just a nodeSelector change. **Files:** - Modify: `infra/k8s/monitoring/loki.yaml:64` - Modify: `infra/k8s/monitoring/tempo.yaml:60` - Modify: `infra/k8s/monitoring/kube-state-metrics.yaml:118` - Modify: `infra/k8s/monitoring/pushgateway.yaml:20` **Step 1: Change nodeSelectors in manifests** In each file, change `k8s.scaleway.com/pool-name` value to `platform`: ```bash # loki.yaml line 64: gitlab → platform sed -i 's/pool-name: gitlab/pool-name: platform/' infra/k8s/monitoring/loki.yaml # tempo.yaml line 60: gitlab → platform sed -i 's/pool-name: gitlab/pool-name: platform/' infra/k8s/monitoring/tempo.yaml # kube-state-metrics.yaml line 118: gitlab → platform sed -i 's/pool-name: gitlab/pool-name: platform/' infra/k8s/monitoring/kube-state-metrics.yaml # pushgateway.yaml line 20: services → platform sed -i 's/pool-name: services/pool-name: platform/' infra/k8s/monitoring/pushgateway.yaml ``` **Step 2: Apply and restart** ```bash kubectl apply -f infra/k8s/monitoring/loki.yaml -n foxhunt kubectl apply -f infra/k8s/monitoring/tempo.yaml -n foxhunt kubectl apply -f infra/k8s/monitoring/kube-state-metrics.yaml -n foxhunt kubectl apply -f infra/k8s/monitoring/pushgateway.yaml -n foxhunt ``` **Step 3: Verify pods rescheduled to platform node** ```bash kubectl get pods -n foxhunt -l 'app.kubernetes.io/name in (loki,tempo,kube-state-metrics,pushgateway)' \ -o custom-columns="POD:.metadata.name,NODE:.spec.nodeName,STATUS:.status.phase" ``` Expected: All 4 pods show a node name containing `platform` and STATUS=Running. **Step 4: Verify Loki is ingesting** ```bash GRAFANA_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].metadata.name}') kubectl exec -n foxhunt "$GRAFANA_POD" -c grafana -- \ curl -s "http://loki.foxhunt.svc.cluster.local:3100/loki/api/v1/labels" | head -5 ``` Expected: JSON response with `"status": "success"` and label names. **Step 5: Commit** ```bash git add infra/k8s/monitoring/loki.yaml infra/k8s/monitoring/tempo.yaml \ infra/k8s/monitoring/kube-state-metrics.yaml infra/k8s/monitoring/pushgateway.yaml git commit -m "infra: move monitoring stack (loki, tempo, kube-state-metrics, pushgateway) to platform pool" ``` --- ### Task 3: Move Grafana to `platform` **Context:** Grafana is deployed via Helm with values in `grafana-values.yaml`. It has a 2Gi PVC for dashboards/state, but all dashboards are provisioned from ConfigMaps so PVC data loss is acceptable. The simplest approach is to change the nodeSelector, delete the old PVC, and let a new one get created on the platform node. **Files:** - Modify: `infra/k8s/gitlab/grafana-values.yaml:7` **Step 1: Change nodeSelector in Helm values** ```bash sed -i 's/pool-name: gitlab/pool-name: platform/' infra/k8s/gitlab/grafana-values.yaml ``` Verify the file now reads: ```yaml nodeSelector: k8s.scaleway.com/pool-name: platform ``` **Step 2: Upgrade Grafana via Helm** ```bash helm upgrade grafana grafana/grafana \ -n foxhunt \ -f infra/k8s/gitlab/grafana-values.yaml \ --reuse-values ``` **Step 3: If pod is Pending due to PVC bound to old node** The grafana PVC is ReadWriteOnce and may be bound to the old gitlab node. If the pod is stuck Pending: ```bash # Delete the old PVC — dashboards are all provisioned, no data loss kubectl delete pvc grafana -n foxhunt # Restart the deployment to trigger new PVC creation kubectl rollout restart deployment grafana -n foxhunt ``` **Step 4: Wait for Grafana to be ready** ```bash kubectl rollout status deployment grafana -n foxhunt --timeout=120s ``` Expected: `deployment "grafana" successfully rolled out` **Step 5: Verify Grafana is accessible and dashboards load** ```bash GRAFANA_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].metadata.name}') ADMIN_PASS=$(kubectl get secret grafana -n foxhunt -o jsonpath='{.data.admin-password}' | base64 -d) kubectl exec -n foxhunt "$GRAFANA_POD" -c grafana -- \ curl -s -u "admin:${ADMIN_PASS}" http://localhost:3000/api/search?type=dash-db | python3 -c "import sys,json; print(f'{len(json.load(sys.stdin))} dashboards loaded')" ``` Expected: `13 dashboards loaded` **Step 6: Commit** ```bash git add infra/k8s/gitlab/grafana-values.yaml git commit -m "infra: move Grafana to platform pool" ``` --- ### Task 4: Move Tailscale Proxies to `platform` **Context:** Two tailscale deployments: `tailscale-gitlab-proxy` (in `gitlab/`) handles SSH to GitLab, and `tailscale-subnet-router` (in `tailscale/`) provides network access. Both are stateless and can simply be moved. **Files:** - Modify: `infra/k8s/gitlab/tailscale-proxy.yaml:54` - Modify: `infra/k8s/tailscale/deployment.yaml:20` **Step 1: Change nodeSelectors** ```bash sed -i 's/pool-name: gitlab/pool-name: platform/' infra/k8s/gitlab/tailscale-proxy.yaml sed -i 's/pool-name: services/pool-name: platform/' infra/k8s/tailscale/deployment.yaml ``` **Step 2: Apply both** ```bash kubectl apply -f infra/k8s/gitlab/tailscale-proxy.yaml -n foxhunt kubectl apply -f infra/k8s/tailscale/deployment.yaml -n tailscale ``` **Step 3: Verify pods moved** ```bash kubectl get pod -n foxhunt -l app.kubernetes.io/name=tailscale-gitlab-proxy \ -o custom-columns="POD:.metadata.name,NODE:.spec.nodeName" kubectl get pod -n tailscale -l app.kubernetes.io/name=tailscale-subnet-router \ -o custom-columns="POD:.metadata.name,NODE:.spec.nodeName" ``` Expected: Both show node containing `platform`. **Step 4: Verify GitLab SSH still works through tailscale proxy** ```bash ssh -T git@100.90.76.85 -p 2222 2>&1 | head -3 ``` Expected: `Welcome to GitLab` or similar greeting (not connection refused). **Step 5: Commit** ```bash git add infra/k8s/gitlab/tailscale-proxy.yaml infra/k8s/tailscale/deployment.yaml git commit -m "infra: move tailscale proxies to platform pool" ``` --- ### Task 5: Migrate Redis to `platform` (Stateless Move) **Context:** Redis is used for caching — no persistent data that can't be regenerated. Simply change nodeSelector and restart. No PVC migration needed. **Files:** - Modify: `infra/k8s/databases/redis.yaml:21` **Step 1: Change nodeSelector** ```bash sed -i 's/pool-name: services/pool-name: platform/' infra/k8s/databases/redis.yaml ``` **Step 2: Apply and wait** ```bash kubectl apply -f infra/k8s/databases/redis.yaml -n foxhunt kubectl rollout status deployment redis -n foxhunt --timeout=60s ``` Expected: `deployment "redis" successfully rolled out` **Step 3: Verify Redis is responding** ```bash kubectl exec -n foxhunt deployment/redis -- redis-cli ping ``` Expected: `PONG` **Step 4: Commit** ```bash git add infra/k8s/databases/redis.yaml git commit -m "infra: move Redis to platform pool" ``` --- ### Task 6: Migrate Postgres to `platform` (PVC Migration) **Context:** Postgres has a 10Gi ReadWriteOnce PVC bound to the current services node. We need to back up the data, create a new PVC on the platform node, restore, and switch over. This causes a few minutes of downtime for postgres. **Files:** - Modify: `infra/k8s/databases/postgres.yaml:38` - Modify: `infra/k8s/gitlab/postgres-init.yaml:15` **Step 1: Backup postgres data** ```bash # Get current postgres pod PG_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=postgres -o jsonpath='{.items[0].metadata.name}') # Run pg_dumpall to capture everything kubectl exec -n foxhunt "$PG_POD" -- pg_dumpall -U foxhunt > /tmp/foxhunt-pg-backup.sql echo "Backup size: $(wc -c < /tmp/foxhunt-pg-backup.sql) bytes" ``` Expected: Non-zero file size (should be several MB with GitLab + foxhunt data). **Step 2: Scale postgres to 0** ```bash kubectl scale deployment postgres -n foxhunt --replicas=0 kubectl get pods -n foxhunt -l app.kubernetes.io/name=postgres ``` Expected: No pods running. **Step 3: Delete old PVC and create a new one** ```bash # Delete the PVC bound to the old node kubectl delete pvc postgres-pvc -n foxhunt # Apply the manifest again — it includes the PVC definition and will create a fresh one # The new PVC will bind to the platform node when the pod schedules there ``` **Step 4: Change nodeSelector in postgres manifest** ```bash sed -i 's/pool-name: services/pool-name: platform/' infra/k8s/databases/postgres.yaml ``` **Step 5: Apply manifest and scale up** ```bash kubectl apply -f infra/k8s/databases/postgres.yaml -n foxhunt # The deployment should automatically create 1 replica with the new nodeSelector kubectl rollout status deployment postgres -n foxhunt --timeout=120s ``` Expected: Pod starts on the platform node with a fresh (empty) database. **Step 6: Restore data** ```bash PG_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=postgres -o jsonpath='{.items[0].metadata.name}') # Copy backup into pod and restore kubectl cp /tmp/foxhunt-pg-backup.sql foxhunt/"$PG_POD":/tmp/backup.sql kubectl exec -n foxhunt "$PG_POD" -- psql -U foxhunt -f /tmp/backup.sql postgres ``` **Step 7: Verify postgres is healthy** ```bash kubectl exec -n foxhunt "$PG_POD" -- psql -U foxhunt -c "SELECT datname FROM pg_database;" ``` Expected: Lists databases including `foxhunt`, `gitlabhq_production`, etc. **Step 8: Update postgres-init nodeSelector too** ```bash sed -i 's/pool-name: services/pool-name: platform/' infra/k8s/gitlab/postgres-init.yaml ``` **Step 9: Commit** ```bash git add infra/k8s/databases/postgres.yaml infra/k8s/gitlab/postgres-init.yaml git commit -m "infra: migrate Postgres to platform pool with PVC migration" ``` --- ### Task 7: Migrate QuestDB to `platform` (PVC Migration) **Context:** QuestDB has a 10Gi ReadWriteOnce PVC. It stores ML model metrics (predictions, trade outcomes). Data is nice-to-have but not critical — QuestDB will recreate tables on startup. We can do a simple file copy if desired, or just start fresh. **Files:** - Modify: `infra/k8s/databases/questdb.yaml:38` **Step 1: Scale QuestDB to 0** ```bash kubectl scale deployment questdb -n foxhunt --replicas=0 ``` **Step 2: Delete old PVC** ```bash kubectl delete pvc questdb-pvc -n foxhunt ``` **Step 3: Change nodeSelector** ```bash sed -i 's/pool-name: services/pool-name: platform/' infra/k8s/databases/questdb.yaml ``` **Step 4: Apply and scale up** ```bash kubectl apply -f infra/k8s/databases/questdb.yaml -n foxhunt kubectl rollout status deployment questdb -n foxhunt --timeout=120s ``` Expected: QuestDB starts fresh on the platform node. **Step 5: Verify QuestDB is responding** ```bash QUESTDB_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=questdb -o jsonpath='{.items[0].metadata.name}') kubectl exec -n foxhunt "$QUESTDB_POD" -- curl -s "http://localhost:9000/exec?query=SELECT+1" 2>/dev/null | head -3 ``` Expected: JSON response with result. **Step 6: Commit** ```bash git add infra/k8s/databases/questdb.yaml git commit -m "infra: migrate QuestDB to platform pool (fresh start)" ``` --- ### Task 8: Migrate MinIO to `platform` (PVC Migration) **Context:** MinIO has a 20Gi PVC containing compiled binaries and trained models. We need to preserve this data. Strategy: use a temp pod to mount both old and new PVCs and copy data via rsync. **Files:** - Modify: `infra/k8s/minio/minio.yaml:57,152` **Step 1: Record what's in MinIO** ```bash # Using minio mc client from the init job approach MINIO_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=minio -o jsonpath='{.items[0].metadata.name}') kubectl exec -n foxhunt "$MINIO_POD" -- ls -la /data/ ``` Note the contents for verification after migration. **Step 2: Scale MinIO to 0** ```bash kubectl scale deployment minio -n foxhunt --replicas=0 ``` **Step 3: Create a migration pod to copy data** ```bash cat <<'EOF' | kubectl apply -n foxhunt -f - apiVersion: v1 kind: PersistentVolumeClaim metadata: name: minio-data-new namespace: foxhunt labels: app.kubernetes.io/name: minio app.kubernetes.io/part-of: foxhunt spec: accessModes: - ReadWriteOnce storageClassName: scw-bssd resources: requests: storage: 20Gi --- apiVersion: v1 kind: Pod metadata: name: minio-migrate namespace: foxhunt spec: nodeSelector: k8s.scaleway.com/pool-name: platform restartPolicy: Never containers: - name: migrate image: debian:bookworm-slim command: ["sleep", "3600"] volumeMounts: - name: new-data mountPath: /new volumes: - name: new-data persistentVolumeClaim: claimName: minio-data-new EOF ``` Wait for the migration pod to be Running: ```bash kubectl wait --for=condition=Ready pod/minio-migrate -n foxhunt --timeout=120s ``` **Step 4: Copy data from old PVC to new PVC** Since the old PVC is ReadWriteOnce and bound to a services node, we can't mount it directly on the platform node. Instead, we'll use a second pod on the services node to tar the data and pipe it: ```bash # Create a reader pod on the old node cat <<'EOF' | kubectl apply -n foxhunt -f - apiVersion: v1 kind: Pod metadata: name: minio-reader namespace: foxhunt spec: nodeSelector: k8s.scaleway.com/pool-name: services restartPolicy: Never containers: - name: reader image: debian:bookworm-slim command: ["sleep", "3600"] volumeMounts: - name: old-data mountPath: /old readOnly: true volumes: - name: old-data persistentVolumeClaim: claimName: minio-data EOF kubectl wait --for=condition=Ready pod/minio-reader -n foxhunt --timeout=120s # Pipe tar from reader to migrate pod kubectl exec -n foxhunt minio-reader -- tar cf - -C /old . | \ kubectl exec -i -n foxhunt minio-migrate -- tar xf - -C /new # Verify data arrived kubectl exec -n foxhunt minio-migrate -- ls -la /new/ ``` Expected: Same directory listing as Step 1. **Step 5: Clean up migration pods** ```bash kubectl delete pod minio-reader minio-migrate -n foxhunt ``` **Step 6: Swap PVCs** ```bash # Delete old PVC kubectl delete pvc minio-data -n foxhunt # Rename new PVC (can't rename in k8s — need to update the manifest to use the new name, # or recreate with the original name) # Simplest: update minio.yaml to reference minio-data-new ``` Actually, the simplest approach: edit the minio manifest to use `minio-data-new` as the PVC name: ```bash sed -i 's/claimName: minio-data/claimName: minio-data-new/' infra/k8s/minio/minio.yaml ``` **Step 7: Change nodeSelectors** ```bash sed -i 's/pool-name: services/pool-name: platform/' infra/k8s/minio/minio.yaml ``` **Step 8: Apply and start MinIO** ```bash kubectl apply -f infra/k8s/minio/minio.yaml -n foxhunt kubectl rollout status deployment minio -n foxhunt --timeout=120s ``` **Step 9: Verify MinIO has all data** ```bash MINIO_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=minio -o jsonpath='{.items[0].metadata.name}') kubectl exec -n foxhunt "$MINIO_POD" -- ls -la /data/ ``` Expected: Same contents as Step 1. **Step 10: Commit** ```bash git add infra/k8s/minio/minio.yaml git commit -m "infra: migrate MinIO to platform pool with data migration" ``` --- ### Task 9: Rename Services → Foxhunt Pool via Terraform (Phase 2 — destructive) **Context:** All databases and monitoring are now on the `platform` node. The `services` pool only has foxhunt app pods + binary-cache PVCs (disposable). Now we run `terragrunt apply` to complete the rename: Terraform will destroy the old `services` pool (2 nodes) and create a new `foxhunt` pool (1 node). App pods will be evicted temporarily. **Files:** - Modify: `infra/k8s/services/api-gateway.yaml:39` - Modify: `infra/k8s/services/trading-service.yaml:39` - Modify: `infra/k8s/services/broker-gateway.yaml:39` - Modify: `infra/k8s/services/ib-gateway.yaml:26` - Modify: `infra/k8s/services/ml-training-service.yaml:82` - Modify: `infra/k8s/services/trading-agent-service.yaml:39` - Modify: `infra/k8s/services/backtesting-service.yaml:39` - Modify: `infra/k8s/services/data-acquisition-service.yaml:39` - Modify: `infra/k8s/services/web-gateway.yaml:39` - Modify: `infra/k8s/training/idle-reaper.yaml:58` **Step 1: Update all foxhunt service manifests to target `foxhunt` pool** ```bash # All services currently say pool-name: services → change to pool-name: foxhunt for f in infra/k8s/services/api-gateway.yaml \ infra/k8s/services/trading-service.yaml \ infra/k8s/services/broker-gateway.yaml \ infra/k8s/services/ib-gateway.yaml \ infra/k8s/services/ml-training-service.yaml \ infra/k8s/services/trading-agent-service.yaml \ infra/k8s/services/backtesting-service.yaml \ infra/k8s/services/data-acquisition-service.yaml \ infra/k8s/services/web-gateway.yaml \ infra/k8s/training/idle-reaper.yaml; do sed -i 's/pool-name: services/pool-name: foxhunt/' "$f" done ``` **Step 2: Verify changes are correct** ```bash grep -r "pool-name: services" infra/k8s/services/ infra/k8s/training/idle-reaper.yaml ``` Expected: No output (all changed to `foxhunt`). ```bash grep -r "pool-name: foxhunt" infra/k8s/services/ infra/k8s/training/idle-reaper.yaml | wc -l ``` Expected: 10 (9 services + 1 idle-reaper). **Step 3: Delete all binary-cache PVCs (they'll be recreated on the new node)** ```bash kubectl delete pvc -n foxhunt -l app.kubernetes.io/part-of=foxhunt --field-selector metadata.name!=postgres-pvc,metadata.name!=questdb-pvc,metadata.name!=minio-data-new 2>/dev/null || true # More precisely — delete just the binary-cache PVCs for SVC in api-gateway backtesting-service broker-gateway data-acquisition-service \ ml-training-service trading-agent-service trading-service web-gateway; do kubectl delete pvc "binary-cache-${SVC}" -n foxhunt 2>/dev/null || true done ``` **Step 4: Run Terraform Phase 2 — destroy services pool, create foxhunt pool** ```bash cd infra/live/production/kapsule terragrunt apply ``` This will: - **Destroy** `scaleway_k8s_pool.services` (both services nodes get deleted) - **Create** `scaleway_k8s_pool.foxhunt` (1× DEV1-L) All pods on the old services nodes get evicted. Wait for the new foxhunt node to become Ready. **Step 5: Wait for new foxhunt node** ```bash kubectl get nodes -l k8s.scaleway.com/pool-name=foxhunt -w ``` Expected: 1 node with STATUS=Ready within 2-3 minutes. **Step 6: Apply updated K8s manifests** ```bash kubectl apply -f infra/k8s/services/ -n foxhunt kubectl apply -f infra/k8s/training/idle-reaper.yaml -n foxhunt ``` **Step 7: Wait for all foxhunt services to be Running** ```bash kubectl get pods -n foxhunt -l app.kubernetes.io/part-of=foxhunt -w ``` Expected: All 9 service pods + ib-gateway Running on the foxhunt node. Binary caches will be populated from S3 via initContainers. **Step 8: Commit** ```bash git add infra/k8s/services/ infra/k8s/training/idle-reaper.yaml git commit -m "infra: rename services → foxhunt pool in all K8s manifests" ``` --- ### Task 10: Move idle-reaper to `foxhunt` pool **Context:** The idle-reaper CronJob currently targets `services`. Since it manages GPU node scaling, it logically belongs on the foxhunt/services node. Since we kept the pool name as `services`, no actual change is needed. However, if we want to be explicit: **Files:** - Verify: `infra/k8s/training/idle-reaper.yaml:58` — already says `services`, no change needed **Step 1: Verify idle-reaper runs on the services (foxhunt) node** ```bash kubectl get cronjob gpu-idle-reaper -n foxhunt -o jsonpath='{.spec.jobTemplate.spec.template.spec.nodeSelector}' ``` Expected: `{"k8s.scaleway.com/pool-name":"services"}` — already correct. No commit needed. --- ### Task 11: Full Cluster Verification **Context:** Verify the entire cluster is healthy after migration. Check every pool has the right pods, all services are communicating, dashboards work, CI works. **Step 1: Verify node pool distribution** ```bash echo "=== Pods by Node Pool ===" for POOL in gitlab platform services; do echo "" echo "--- $POOL ---" for NODE in $(kubectl get nodes -l k8s.scaleway.com/pool-name="$POOL" -o jsonpath='{.items[*].metadata.name}'); do kubectl get pods -A --field-selector spec.nodeName="$NODE" --no-headers 2>/dev/null | \ awk '{print $1"/"$2}' | sort done done ``` Expected: - `gitlab`: Only gitlab-* pods + daemonsets (node-exporter, promtail) - `platform`: postgres, redis, minio, questdb, grafana, loki, tempo, kube-state-metrics, tailscale-*, pushgateway + daemonsets - `services`: Only foxhunt app pods (api-gateway, trading-service, etc.) + ib-gateway + daemonsets **Step 2: Verify Grafana dashboards** ```bash GRAFANA_POD=$(kubectl get pod -n foxhunt -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].metadata.name}') ADMIN_PASS=$(kubectl get secret grafana -n foxhunt -o jsonpath='{.data.admin-password}' | base64 -d) kubectl exec -n foxhunt "$GRAFANA_POD" -c grafana -- \ curl -s -u "admin:${ADMIN_PASS}" http://localhost:3000/api/search?type=dash-db | \ python3 -c "import sys,json; ds=json.load(sys.stdin); print(f'{len(ds)} dashboards'); [print(f' {d[\"title\"]}') for d in ds]" ``` Expected: 13 dashboards listed. **Step 3: Verify Loki ingestion** ```bash kubectl exec -n foxhunt "$GRAFANA_POD" -c grafana -- \ curl -s "http://loki.foxhunt.svc.cluster.local:3100/loki/api/v1/labels" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Loki status={d[\"status\"]}, {len(d.get(\"data\",[]))} labels')" ``` Expected: `Loki status=success, N labels` **Step 4: Verify GitLab CI pipeline** Push a trivial commit or trigger a pipeline manually: ```bash git push # The commit from earlier tasks triggers CI ``` Check pipeline status in GitLab UI or: ```bash curl -s --header "PRIVATE-TOKEN: $(cat ~/.gitlab-token)" \ "http://100.90.76.85:2222/api/v4/projects/1/pipelines?per_page=1" | python3 -c "import sys,json; p=json.load(sys.stdin)[0]; print(f'Pipeline #{p[\"id\"]}: {p[\"status\"]}')" ``` Expected: Pipeline running or passed. **Step 5: Verify network connectivity between pools** ```bash # Test foxhunt services can reach platform databases kubectl exec -n foxhunt deployment/trading-service -- curl -s http://redis.foxhunt.svc.cluster.local:6379/ping 2>/dev/null || \ kubectl exec -n foxhunt deployment/trading-service -- nc -zv redis.foxhunt.svc.cluster.local 6379 2>&1 ``` Expected: Connection successful (TCP to Redis on platform node). **Step 6: Check resource utilization on new layout** ```bash kubectl top nodes ``` Expected: 3 nodes visible (gitlab, platform, services) with reasonable utilization. **Step 7: Final commit** ```bash git add -A git status # Verify only expected changes git commit -m "infra: complete 3-pool node split (gitlab/platform/foxhunt) - Created platform pool (DEV1-L) for databases + monitoring - Migrated Postgres (pg_dump/restore), MinIO (tar copy), QuestDB (fresh) - Moved Loki, Tempo, Grafana, kube-state-metrics, pushgateway to platform - Scaled services pool from 2 to 1 node (now foxhunt app services only) - Same EUR 61/mo total, proper blast radius isolation" ``` --- ## Summary of Changes by File | File | Change | |------|--------| | `infra/k8s/monitoring/loki.yaml` | `gitlab` → `platform` | | `infra/k8s/monitoring/tempo.yaml` | `gitlab` → `platform` | | `infra/k8s/monitoring/kube-state-metrics.yaml` | `gitlab` → `platform` | | `infra/k8s/monitoring/pushgateway.yaml` | `services` → `platform` | | `infra/k8s/gitlab/grafana-values.yaml` | `gitlab` → `platform` | | `infra/k8s/gitlab/tailscale-proxy.yaml` | `gitlab` → `platform` | | `infra/k8s/tailscale/deployment.yaml` | `services` → `platform` | | `infra/k8s/databases/redis.yaml` | `services` → `platform` | | `infra/k8s/databases/postgres.yaml` | `services` → `platform` | | `infra/k8s/databases/questdb.yaml` | `services` → `platform` | | `infra/k8s/minio/minio.yaml` | `services` → `platform`, PVC name update | | `infra/k8s/gitlab/postgres-init.yaml` | `services` → `platform` | **Unchanged (stay on `gitlab`):** `values.yaml`, `runner-values.yaml`, `runner-rl-values.yaml`, `pat-rotation.yaml` **Changed `services` → `foxhunt`:** All 9 foxhunt service YAMLs, `idle-reaper.yaml` **Terraform:** `infra/modules/kapsule/{main,variables,outputs}.tf`, `infra/live/production/kapsule/terragrunt.hcl` ## Rollback Procedure **Before Phase 2 (services pool still exists):** 1. Revert `platform` nodeSelectors back to `gitlab` or `services` 2. `kubectl apply` each file 3. For stateful services (postgres, minio): restore from backups 4. `terragrunt destroy -target=scaleway_k8s_pool.platform` to remove platform pool 5. Revert Terraform changes in git **After Phase 2 (services pool destroyed, foxhunt pool created):** 1. Revert Terraform to recreate `services` pool: `git revert` the TF commits, `terragrunt apply` 2. Revert all K8s manifests: `pool-name: foxhunt` → `pool-name: services` 3. Restore databases from backups 4. Destroy platform pool