From c6191756c5e251169d25dbd52e697bb02c6233ef Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Wed, 14 Oct 2026 19:32:45 +0200 Subject: [PATCH] feat: deploy to Frostmoln production The AWS deploy-prod job has failed since CI left AWS (no k8s.unbound.se context on the runners). Deploys now go to the Frostmoln production cluster (namespace sites), the way Shiny deploys to Frostmoln staging. Frostmoln enforces restricted Pod Security, so the image is nginx-unprivileged (uid 101, port 8080) with a matching securityContext. The ALB ingress becomes a Traefik one with a Let's Encrypt certificate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ma8eHhW59hLAJuLzBnLvUu --- .gitea/workflows/ci.yaml | 22 ++++++++++++++++++---- Dockerfile | 3 ++- default.conf | 2 +- k8s/deploy.yaml | 16 ++++++++++++---- k8s/ingress-prod-frostmoln.yaml | 20 ++++++++++++++++++++ k8s/ingress-prod.yaml | 23 ----------------------- 6 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 k8s/ingress-prod-frostmoln.yaml delete mode 100644 k8s/ingress-prod.yaml diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 07ab4a4..20e762c 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -18,16 +18,30 @@ jobs: - name: Build and push run: unset GITEA_TOKEN && build && push - deploy-prod: + deploy-prod-frostmoln: needs: build if: gitea.ref == 'refs/heads/main' runs-on: ubuntu-latest env: - BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }} + BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_FROSTMOLN_PRODUCTION }} GITEA_REPOSITORY: ${{ gitea.repository }} + FM_TENANT: 9ae61342-0f76-476f-8176-e46f6b0b0c32 environment: prod steps: - uses: actions/checkout@v7 - uses: buildtool/setup-buildtools-action@v1 - - name: Deploy to production - run: deploy prod + - name: Install fm + run: | + sudo install -d -m 0755 /etc/apt/keyrings + sudo curl -fsSL -o /etc/apt/keyrings/frostmoln.gpg https://get.frostmoln.cloud/deb/frostmoln.gpg + sudo curl -fsSL -o /etc/apt/sources.list.d/frostmoln.sources https://get.frostmoln.cloud/deb/frostmoln.sources + sudo apt-get update && sudo apt-get install -y fm + - name: Deploy to Frostmoln production + env: + FROSTMOLN_API_KEY: ${{ secrets.FROSTMOLN_PRODUCTION_DEPLOY_KEY }} + # The cluster API is private: the kubeconfig's exec plugin opens an fm + # tunnel on demand. fm reads the key from its config file, not the env. + run: | + fm auth api-key set "$FROSTMOLN_API_KEY" > /dev/null + fm kubernetes cluster kubeconfig ${{ vars.FROSTMOLN_PRODUCTION_CLUSTER_ID }} --exec --file "$RUNNER_TEMP/kubeconfig" + KUBECONFIG_CONTENT="$(cat "$RUNNER_TEMP/kubeconfig")" deploy prod-frostmoln -n sites diff --git a/Dockerfile b/Dockerfile index f0b39f5..99189b3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM amd64/nginx:1.31.6@sha256:9a54e7e76ae7fb26e733c7d43ca1c8586f1894b876ec8b8ca0124daae38d509b +# Unprivileged (uid 101, port 8080): Frostmoln enforces restricted Pod Security. +FROM nginxinc/nginx-unprivileged:1.31.6@sha256:1d26a516fb93b18f9c0214dc9c645054486e516f13ff09ef63d5bd5c622278a2 MAINTAINER Joakim Olsson ADD files/*.html files/*.jpg files/*.mp3 /usr/share/nginx/html/ diff --git a/default.conf b/default.conf index abe2b7a..0e662e7 100644 --- a/default.conf +++ b/default.conf @@ -1,5 +1,5 @@ server { - listen 80; + listen 8080; server_name localhost; access_log /dev/stdout combined; diff --git a/k8s/deploy.yaml b/k8s/deploy.yaml index 5d5d268..4e05f60 100644 --- a/k8s/deploy.yaml +++ b/k8s/deploy.yaml @@ -22,6 +22,11 @@ spec: labels: app.kubernetes.io/name: lingonpaj-site spec: + securityContext: + runAsNonRoot: true + runAsUser: 101 + seccompProfile: + type: RuntimeDefault affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: @@ -39,14 +44,18 @@ spec: readinessProbe: httpGet: path: /health - port: 80 + port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 5 imagePullPolicy: Always image: ${IMAGE} ports: - - containerPort: 80 + - containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] resources: requests: cpu: 5m @@ -64,9 +73,8 @@ metadata: labels: app.kubernetes.io/name: lingonpaj-site spec: - type: NodePort selector: app.kubernetes.io/name: lingonpaj-site ports: - port: 80 - targetPort: 80 + targetPort: 8080 diff --git a/k8s/ingress-prod-frostmoln.yaml b/k8s/ingress-prod-frostmoln.yaml new file mode 100644 index 0000000..de02ce5 --- /dev/null +++ b/k8s/ingress-prod-frostmoln.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: lingonpaj-site + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt +spec: + ingressClassName: traefik + rules: + - host: "lingonpaj.com" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: lingonpaj-site + port: + number: 80 diff --git a/k8s/ingress-prod.yaml b/k8s/ingress-prod.yaml deleted file mode 100644 index ae819c8..0000000 --- a/k8s/ingress-prod.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: lingonpaj-site-ingress - annotations: - kubernetes.io/ingress.class: "alb" - alb.ingress.kubernetes.io/group.name: default - alb.ingress.kubernetes.io/scheme: internet-facing - alb.ingress.kubernetes.io/target-type: instance - alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80},{"HTTPS": 443}]' - alb.ingress.kubernetes.io/ssl-redirect: "443" -spec: - rules: - - host: "lingonpaj.com" - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: lingonpaj-site - port: - number: 80 -- 2.54.0