feat: deploy to Frostmoln production
unbound-site / build (pull_request) Successful in 1m53s
unbound-site / deploy-prod-frostmoln (pull_request) Skipped

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 served 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ma8eHhW59hLAJuLzBnLvUu
This commit is contained in:
argoyleandClaude Opus 5.5 committed 2026-10-14 19:43:57 +02:00
1 parent b8b30a4fbe
commit a41e44367a
5 files changed
+37 -17

No files matched your search

+18 -4
View File
@@ -18,16 +18,30 @@ jobs:
- name: Build and push - name: Build and push
run: unset GITEA_TOKEN && build && push run: unset GITEA_TOKEN && build && push
deploy-prod: deploy-prod-frostmoln:
needs: build needs: build
if: gitea.ref == 'refs/heads/main' if: gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }} BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_FROSTMOLN_PRODUCTION }}
GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_REPOSITORY: ${{ gitea.repository }}
FM_TENANT: 9ae61342-0f76-476f-8176-e46f6b0b0c32
environment: prod environment: prod
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- uses: buildtool/setup-buildtools-action@v1 - uses: buildtool/setup-buildtools-action@v1
- name: Deploy to production - name: Install fm
run: deploy prod 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
+2 -1
View File
@@ -13,7 +13,8 @@ COPY ./ /site
WORKDIR /site WORKDIR /site
RUN hugo RUN hugo
FROM amd64/nginx:1.31.6-alpine@sha256:5f4b1cbc2fac6baad5fd73efa91aef5f40ee315cacc5af719402ae21bb573776 # Unprivileged (uid 101, port 8080): Frostmoln enforces restricted Pod Security.
FROM nginxinc/nginx-unprivileged:1.31.6-alpine@sha256:884e0b7f42f303641b0f4e78755aea647521dca0b89ca3be7992ad78b5c07434
ADD default.conf /etc/nginx/conf.d/default.conf ADD default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /site/public /usr/share/nginx/html COPY --from=build /site/public /usr/share/nginx/html
+1 -1
View File
@@ -1,5 +1,5 @@
server { server {
listen 80; listen 8080;
server_name localhost; server_name localhost;
access_log /dev/stdout combined; access_log /dev/stdout combined;
+12 -4
View File
@@ -22,6 +22,11 @@ spec:
labels: labels:
app.kubernetes.io/name: unbound-site app.kubernetes.io/name: unbound-site
spec: spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
seccompProfile:
type: RuntimeDefault
affinity: affinity:
podAntiAffinity: podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution: preferredDuringSchedulingIgnoredDuringExecution:
@@ -39,14 +44,18 @@ spec:
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /health path: /health
port: 80 port: 8080
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 5 periodSeconds: 5
timeoutSeconds: 5 timeoutSeconds: 5
imagePullPolicy: Always imagePullPolicy: Always
image: ${IMAGE} image: ${IMAGE}
ports: ports:
- containerPort: 80 - containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources: resources:
requests: requests:
cpu: 5m cpu: 5m
@@ -63,9 +72,8 @@ metadata:
labels: labels:
app.kubernetes.io/name: unbound-site app.kubernetes.io/name: unbound-site
spec: spec:
type: NodePort
selector: selector:
app.kubernetes.io/name: unbound-site app.kubernetes.io/name: unbound-site
ports: ports:
- port: 80 - port: 80
targetPort: 80 targetPort: 8080
@@ -1,15 +1,12 @@
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: unbound-site-ingress name: unbound-site
annotations: annotations:
kubernetes.io/ingress.class: "alb" traefik.ingress.kubernetes.io/router.entrypoints: websecure
alb.ingress.kubernetes.io/group.name: default traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
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: spec:
ingressClassName: traefik
rules: rules:
- host: "unbound.se" - host: "unbound.se"
http: http: