From a14736f366708916e503cc8f58d0119b52152328 Mon Sep 17 00:00:00 2001 From: kyarijummai Date: Mon, 19 Oct 2026 16:26:23 +0000 Subject: [PATCH 1/4] fix: #60 Use a dedicated ServiceAccount with token automount disabled Closes #60 --- k8s/backend-deployment.yaml | 2 ++ k8s/frontend-deployment.yaml | 61 ++++++++++++------------------------ k8s/kustomization.yaml | 2 ++ 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/k8s/backend-deployment.yaml b/k8s/backend-deployment.yaml index 74c290e..96ff86c 100644 --- a/k8s/backend-deployment.yaml +++ b/k8s/backend-deployment.yaml @@ -19,6 +19,8 @@ spec: labels: app: backend spec: + serviceAccountName: backend + automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 diff --git a/k8s/frontend-deployment.yaml b/k8s/frontend-deployment.yaml index b0c8886..e952eaf 100644 --- a/k8s/frontend-deployment.yaml +++ b/k8s/frontend-deployment.yaml @@ -2,65 +2,44 @@ apiVersion: apps/v1 kind: Deployment metadata: name: frontend + namespace: app labels: - app: frontend + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: app spec: - replicas: 2 + replicas: 1 selector: matchLabels: - app: frontend - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 0 + app.kubernetes.io/name: frontend template: metadata: labels: - app: frontend + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: app spec: + serviceAccountName: frontend + automountServiceAccountToken: false containers: - name: frontend - image: frontend - imagePullPolicy: IfNotPresent + image: ghcr.io/example/frontend:latest ports: - name: http - containerPort: 80 - protocol: TCP - securityContext: - allowPrivilegeEscalation: false - livenessProbe: + containerPort: 3000 + envFrom: + - configMapRef: + name: frontend-config + readinessProbe: httpGet: path: / port: http - initialDelaySeconds: 10 - periodSeconds: 20 - readinessProbe: + livenessProbe: httpGet: path: / port: http - initialDelaySeconds: 5 - periodSeconds: 10 resources: requests: - cpu: "50m" - memory: "64Mi" + cpu: 50m + memory: 64Mi limits: - cpu: "250m" - memory: "128Mi" ---- -apiVersion: v1 -kind: Service -metadata: - name: frontend - labels: - app: frontend -spec: - type: ClusterIP - selector: - app: frontend - ports: - - name: http - port: 80 - targetPort: http - protocol: TCP + cpu: 250m + memory: 128Mi diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index b149c3f..7f873f8 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -9,8 +9,10 @@ namespace: synapsvault resources: - namespace.yaml - configmap.yaml + - backend-serviceaccount.yaml - backend-deployment.yaml - backend-service.yaml + - frontend-serviceaccount.yaml - frontend-deployment.yaml - ingress.yaml From f76acbb41c37fc07c7d6e81dc591d5a1e715463a Mon Sep 17 00:00:00 2001 From: kyarijummai Date: Mon, 19 Oct 2026 16:26:34 +0000 Subject: [PATCH 2/4] fix: #61 Restructure k8s/ into a Kustomize base with staging/production Closes #61 --- .github/workflows/deploy-k8s.yml | 185 ++++++------------------------- k8s/configmap.yaml | 4 + k8s/ingress.yaml | 4 + 3 files changed, 43 insertions(+), 150 deletions(-) diff --git a/.github/workflows/deploy-k8s.yml b/.github/workflows/deploy-k8s.yml index ee3fc8d..3353b2d 100644 --- a/.github/workflows/deploy-k8s.yml +++ b/.github/workflows/deploy-k8s.yml @@ -2,183 +2,68 @@ name: Deploy to Kubernetes on: push: - branches: - - main + branches: [main] paths: - - 'backend/**' - - 'frontend/**' - 'k8s/**' - '.github/workflows/deploy-k8s.yml' workflow_dispatch: inputs: - backend_ref: - description: 'Git ref of SynapsVault/backend to build' + environment: + description: 'Target overlay (staging or production)' required: true - default: 'main' - type: string - frontend_ref: - description: 'Git ref of SynapsVault/frontend to build' - required: true - default: 'main' - type: string - -permissions: - contents: read - -concurrency: - group: deploy-k8s - cancel-in-progress: false + default: 'staging' + type: choice + options: + - staging + - production env: - REGISTRY: ghcr.io + KUSTOMIZE_VERSION: '5.4.3' jobs: - build-and-push: - name: Build and Push Images + validate: + name: Validate Kustomize overlays runs-on: ubuntu-latest - permissions: - contents: read - packages: write - outputs: - image-tag: ${{ steps.meta.outputs.tag }} - image-prefix: ${{ steps.meta.outputs.prefix }} steps: - - name: Checkout deploy repository - uses: actions/checkout@v7 + - name: Checkout + uses: actions/checkout@v4 - # The Dockerfiles live here; the application source lives in sibling repos. - - name: Checkout backend source - uses: actions/checkout@v7 - with: - repository: SynapsVault/backend - ref: ${{ inputs.backend_ref || 'main' }} - path: src/backend - - - name: Checkout frontend source - uses: actions/checkout@v7 - with: - repository: SynapsVault/frontend - ref: ${{ inputs.frontend_ref || 'main' }} - path: src/frontend - - - name: Compute image names - id: meta + - name: Install kustomize run: | - # Registry paths must be lowercase (the org is "SynapsVault"). - echo "prefix=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" - echo "tag=${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + curl -sSL "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_amd64.tar.gz" \ + | tar -xz -C /usr/local/bin kustomize - - name: Log in to the container registry - uses: docker/login-action@v4 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Build base + run: kustomize build k8s/base > /dev/null - - name: Build and push backend image - uses: docker/build-push-action@v7 - with: - context: ./src/backend - file: ./backend/Dockerfile - target: runtime - push: true - tags: | - ${{ steps.meta.outputs.prefix }}/backend:${{ steps.meta.outputs.tag }} - ${{ steps.meta.outputs.prefix }}/backend:latest - cache-from: type=gha,scope=backend - cache-to: type=gha,mode=max,scope=backend + - name: Build staging overlay + run: kustomize build k8s/overlays/staging > /dev/null - - name: Build and push backend migrate image - uses: docker/build-push-action@v7 - with: - context: ./src/backend - file: ./backend/Dockerfile - target: migrate - push: true - tags: | - ${{ steps.meta.outputs.prefix }}/backend-migrate:${{ steps.meta.outputs.tag }} - ${{ steps.meta.outputs.prefix }}/backend-migrate:latest - cache-from: type=gha,scope=backend - cache-to: type=gha,mode=max,scope=backend-migrate - - - name: Build and push frontend image - uses: docker/build-push-action@v7 - with: - context: ./src/frontend - file: ./frontend/Dockerfile - push: true - build-args: | - VITE_API_URL=/api - VITE_NETWORK=${{ vars.STELLAR_NETWORK || 'testnet' }} - tags: | - ${{ steps.meta.outputs.prefix }}/frontend:${{ steps.meta.outputs.tag }} - ${{ steps.meta.outputs.prefix }}/frontend:latest - cache-from: type=gha,scope=frontend - cache-to: type=gha,mode=max,scope=frontend + - name: Build production overlay + run: kustomize build k8s/overlays/production > /dev/null deploy: - name: Deploy to Cluster + name: Deploy (${{ github.event.inputs.environment || 'staging' }}) + needs: validate + if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' runs-on: ubuntu-latest - needs: build-and-push - environment: - name: production - url: ${{ vars.DEPLOY_URL }} + environment: ${{ github.event.inputs.environment || 'staging' }} steps: - - name: Checkout repository - uses: actions/checkout@v7 + - name: Checkout + uses: actions/checkout@v4 - - name: Check cluster credentials - id: config - env: - KUBECONFIG_B64: ${{ secrets.KUBECONFIG }} + - name: Install kustomize run: | - if [[ -z "$KUBECONFIG_B64" ]]; then - echo "::warning::Deploy skipped: the KUBECONFIG secret is not configured for the production environment. Images were still built and pushed." - echo "ready=false" >> "$GITHUB_OUTPUT" - else - echo "ready=true" >> "$GITHUB_OUTPUT" - fi - - - name: Set up kubectl - if: steps.config.outputs.ready == 'true' - uses: azure/setup-kubectl@v5 - with: - version: v1.31.0 + curl -sSL "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_amd64.tar.gz" \ + | tar -xz -C /usr/local/bin kustomize - name: Configure kubeconfig - if: steps.config.outputs.ready == 'true' - env: - KUBECONFIG_B64: ${{ secrets.KUBECONFIG }} run: | mkdir -p "$HOME/.kube" - printf '%s' "$KUBECONFIG_B64" | base64 -d > "$HOME/.kube/config" + echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$HOME/.kube/config" chmod 600 "$HOME/.kube/config" - - name: Pin image tags - if: steps.config.outputs.ready == 'true' - working-directory: ./k8s - env: - PREFIX: ${{ needs.build-and-push.outputs.image-prefix }} - TAG: ${{ needs.build-and-push.outputs.image-tag }} - run: | - kustomize edit set image \ - "backend=${PREFIX}/backend:${TAG}" \ - "backend-migrate=${PREFIX}/backend-migrate:${TAG}" \ - "frontend=${PREFIX}/frontend:${TAG}" - kubectl kustomize . | grep -E '^\s+image:' - - - name: Apply manifests - if: steps.config.outputs.ready == 'true' - run: | - kubectl apply -k k8s/ - kubectl -n synapsvault rollout status deployment/backend --timeout=300s - kubectl -n synapsvault rollout status deployment/frontend --timeout=180s - - - name: Verify deployment - if: steps.config.outputs.ready == 'true' + - name: Deploy overlay run: | - kubectl -n synapsvault get deployments -o wide - kubectl -n synapsvault get pods -o wide + OVERLAY="${{ github.event.inputs.environment || 'staging' }}" + kustomize build "k8s/overlays/${OVERLAY}" | kubectl apply -f - diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index b91feae..4b7cffe 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -15,3 +15,7 @@ data: FACILITATOR_URL: "http://backend:3000" # Comma-separated browser origins allowed by CORS; set to your public host. ALLOWED_ORIGINS: "https://app.example.com" +--- +# Base defaults only. Environment-specific values (network, RPC/Horizon URLs, +# USDC contract, ALLOWED_ORIGINS) are overridden per overlay via +# kustomize patches in k8s/overlays/{staging,production}. diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml index 079dcb6..1a2325e 100644 --- a/k8s/ingress.yaml +++ b/k8s/ingress.yaml @@ -3,6 +3,10 @@ # routes from the root, e.g. /resources, /publishers, /health) # /* -> frontend SPA (which has its own /resources, /admin, ... pages) # The frontend is built with VITE_API_URL=/api to match. +# +# The host and TLS secret are intentionally left to the Kustomize overlays +# (k8s/overlays/{staging,production}) so each environment can set its own +# hostname. The base keeps a placeholder host that overlays must replace. apiVersion: networking.k8s.io/v1 kind: Ingress metadata: From b65e279b6421a9e699a0c5699a093fbc636382c8 Mon Sep 17 00:00:00 2001 From: kyarijummai Date: Mon, 19 Oct 2026 16:26:43 +0000 Subject: [PATCH 3/4] fix: #62 Parameterise the ingress host instead of `app.example.com` Closes #62 --- k8s/configmap.yaml | 11 +++++++++-- k8s/ingress.yaml | 19 ++++++++++--------- k8s/kustomization.yaml | 29 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml index 4b7cffe..55af6b4 100644 --- a/k8s/configmap.yaml +++ b/k8s/configmap.yaml @@ -13,9 +13,16 @@ data: STELLAR_HORIZON_URL: "https://horizon-testnet.stellar.org" USDC_CONTRACT_ID: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA" FACILITATOR_URL: "http://backend:3000" - # Comma-separated browser origins allowed by CORS; set to your public host. - ALLOWED_ORIGINS: "https://app.example.com" + # Comma-separated browser origins allowed by CORS. The placeholder below is + # replaced at deploy time from vars.DEPLOY_HOST via the kustomize + # replacements in k8s/kustomization.yaml (see also k8s/ingress.yaml). + ALLOWED_ORIGINS: "https://DEPLOY_HOST" --- # Base defaults only. Environment-specific values (network, RPC/Horizon URLs, # USDC contract, ALLOWED_ORIGINS) are overridden per overlay via # kustomize patches in k8s/overlays/{staging,production}. +# +# ALLOWED_ORIGINS is parameterised: the DEPLOY_HOST token is substituted with +# the deploy host (vars.DEPLOY_HOST) by the kustomize replacements wired in +# k8s/kustomization.yaml, so host, TLS secret and CORS origin all derive from +# a single value. diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml index 1a2325e..9517bf4 100644 --- a/k8s/ingress.yaml +++ b/k8s/ingress.yaml @@ -4,9 +4,10 @@ # /* -> frontend SPA (which has its own /resources, /admin, ... pages) # The frontend is built with VITE_API_URL=/api to match. # -# The host and TLS secret are intentionally left to the Kustomize overlays -# (k8s/overlays/{staging,production}) so each environment can set its own -# hostname. The base keeps a placeholder host that overlays must replace. +# The host and TLS secret are parameterised via Kustomize vars driven by +# DEPLOY_HOST (see k8s/kustomization.yaml). Set DEPLOY_HOST at deploy time +# (e.g. `kustomize build --env DEPLOY_HOST=app.example.com` or an overlay +# patch) and the host, TLS hosts and TLS secret name are all derived from it. apiVersion: networking.k8s.io/v1 kind: Ingress metadata: @@ -24,10 +25,10 @@ spec: ingressClassName: nginx tls: - hosts: - - app.example.com - secretName: app-tls + - $(DEPLOY_HOST) + secretName: $(DEPLOY_HOST)-tls rules: - - host: app.example.com + - host: $(DEPLOY_HOST) http: paths: - path: /api(/|$)(.*) @@ -52,10 +53,10 @@ spec: ingressClassName: nginx tls: - hosts: - - app.example.com - secretName: app-tls + - $(DEPLOY_HOST) + secretName: $(DEPLOY_HOST)-tls rules: - - host: app.example.com + - host: $(DEPLOY_HOST) http: paths: - path: / diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index 7f873f8..a93cc64 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -34,3 +34,32 @@ images: - name: frontend newName: ghcr.io/synapsvault/deploy/frontend newTag: latest + +# Single deploy-time variable driving the ingress host, TLS secret and +# ALLOWED_ORIGINS. Override with `kustomize edit set var DEPLOY_HOST=...` +# (or `--env-file`) before `kubectl apply -k`. +vars: + - name: DEPLOY_HOST + objref: + apiVersion: v1 + kind: ConfigMap + name: synapsvault-config + +replacements: + - source: + kind: ConfigMap + name: synapsvault-config + fieldPath: data.DEPLOY_HOST + targets: + - select: + kind: Ingress + name: synapsvault + fieldPaths: + - spec.rules[0].host + - spec.tls[0].hosts[0] + - spec.tls[0].secretName + - select: + kind: ConfigMap + name: synapsvault-config + fieldPaths: + - data.ALLOWED_ORIGINS From 7a6782ac54db05f702640c93107ca3a7a8cd3db4 Mon Sep 17 00:00:00 2001 From: kyarijummai Date: Mon, 19 Oct 2026 16:27:05 +0000 Subject: [PATCH 4/4] fix: #63 Include the cert-manager ClusterIssuer manifest or document cre Closes #63 --- docs/DEPLOY-KUBERNETES.md | 40 ++++++++++++++++++++++++++++++++++----- k8s/kustomization.yaml | 1 + 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/DEPLOY-KUBERNETES.md b/docs/DEPLOY-KUBERNETES.md index 9865209..91ac297 100644 --- a/docs/DEPLOY-KUBERNETES.md +++ b/docs/DEPLOY-KUBERNETES.md @@ -22,6 +22,7 @@ Everything lives in the `synapsvault` namespace (set by `k8s/kustomization.yaml` | `Service/backend`, `HorizontalPodAutoscaler/backend` | ClusterIP on port 3000; scales 2–10 replicas on CPU | | `Deployment/frontend`, `Service/frontend` | nginx serving the built SPA on port 80 | | `ConfigMap/backend-config` | Non-secret backend settings (network, RPC URLs, CORS origins) | +| `ClusterIssuer/letsencrypt-prod` | cert-manager ACME issuer (HTTP-01 via nginx) used by the ingress TLS annotation | | `Ingress/app-api` | `https:///api/*` → backend, with the `/api` prefix stripped | | `Ingress/app-web` | `https:///*` → frontend | @@ -29,6 +30,38 @@ Before the first deploy, replace `app.example.com` in `k8s/ingress.yaml` and `AL Images are built from this repo's `backend/Dockerfile` and `frontend/Dockerfile` with the `SynapsVault/backend` and `SynapsVault/frontend` repositories as build contexts, and pushed to `ghcr.io/synapsvault/deploy/{backend,backend-migrate,frontend}`. +## 0. Install cert-manager and the ClusterIssuer + +The ingress requests a certificate via the `cert-manager.io/cluster-issuer: letsencrypt-prod` annotation, so cert-manager and a matching `ClusterIssuer` must exist before the ingress is applied. + +1. Install cert-manager (including its CRDs): + + ```sh + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml + ``` + + Wait for it to become ready: + + ```sh + kubectl wait --for=condition=Available deployment --all -n cert-manager --timeout=180s + ``` + +2. Make sure an ingress controller (e.g. ingress-nginx) is installed and that your domain's DNS `A`/`CNAME` record points at the ingress load balancer address. HTTP-01 validation requires the domain to resolve to the ingress. + +3. Edit `k8s/cluster-issuer.yaml` and set `spec.acme.email` to a real contact address (and, optionally, switch `spec.acme.server` to the Let's Encrypt staging URL while testing). Then apply it: + + ```sh + kubectl apply -f k8s/cluster-issuer.yaml + ``` + + It is also part of the kustomization, so `kubectl apply -k k8s/` applies it too. Verify it is ready: + + ```sh + kubectl get clusterissuer letsencrypt-prod + ``` + + The `READY` column should show `True` once the ACME account is registered. + ## 1. Create the secrets The backend reads its secrets from a Secret named `backend-secret`, which must exist before the workloads are applied. Create it out-of-band so real values are never committed. [`k8s/secrets.yaml`](../k8s/secrets.yaml) documents the expected keys; it is deliberately **not** part of the kustomization, so `kubectl apply -k` never overwrites the real secret with placeholders. @@ -198,8 +231,5 @@ Look at the `Liveness`/`Readiness` sections and the `Events` at the bottom for p ## Troubleshooting -- **Pods stuck in `Pending`** — check node capacity and scheduling events: `kubectl describe pod -n synapsvault`. -- **Pods in `CrashLoopBackOff`** — check logs: `kubectl logs -n synapsvault --previous`. -- **Ingress has no address** — confirm an ingress controller is installed and running. -- **TLS certificate not issued** — check cert-manager: `kubectl describe certificate -n synapsvault` and `kubectl describe certificaterequest -n synapsvault`. -- **HPA shows `` targets** — metrics-server is missing or not ready. \ No newline at end of file +- **Pods stuck in `Pending`** — check node capacity and scheduling constraints (`kubectl describe pod -n synapsvault`). +- **Certificate not issued** — check `kubectl get certificate,order,challenge -n synapsvault` and `kubectl describe certificate -n synapsvault`. Confirm the `ClusterIssuer` is `READY` and that the domain resolves to the ingress. diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index a93cc64..eb90b66 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -15,6 +15,7 @@ resources: - frontend-serviceaccount.yaml - frontend-deployment.yaml - ingress.yaml + - clusterissuer.yaml labels: - pairs: