deploy/kubernetes/gunnar.yaml
# gunnar in Kubernetes (roadmap phase 5).
#
# Plain manifests, and NOT a Helm chart. A chart would be a second description
# of the same deployment in a second language, and this repository keeps
# one authority for a thing. `kubectl apply -k deploy/kubernetes` is the door; the two knobs
# an operator actually turns — the image digest and the storage class — are
# kustomize patches, which is data, not templating.
#
# ── Three things here are load-bearing, and `cargo xtask deploy-check` fails
# the build if any of them regresses ────────────────────────────────────────
#
# 1. THE IMAGE IS PINNED BY DIGEST. A product whose entire thesis is "you can
# prove what this binary was built from" cannot deploy a floating tag. The
# digest below is a placeholder and the check refuses a `:tag` reference.
#
# 2. THE DATA IS A PERSISTENT VOLUME. The transparency log and the forge log
# live under the data root. On an `emptyDir` they would be
# forgotten on every reschedule, and a log that forgets is not a log — it is
# a cache with a signature on it.
#
# 3. THE KEYS COME FROM A SECRET AND ARE NEVER IN A CONFIGMAP. The KEK unseals
# the transparency key. A ConfigMap is world-readable to
# anything with `get configmaps` in the namespace.
#
# ── What is deliberately NOT here ────────────────────────────────────────────
#
# * No `Ingress`. Git over SSH is not HTTP and does not go through one; the
# `Service` below is what a `LoadBalancer` or a `NodePort` attaches to, and
# which of those is right is a fact about the cluster, not about gunnar.
# * No `HorizontalPodAutoscaler`. gunnar is a `StatefulSet` of ONE: the object
# store is a znippy archive with a lock on it, and a second replica would be
# a second writer. Replication is `gunnar mirror`, not a second pod.
---
apiVersion: v1
kind: Namespace
metadata:
name: gunnar
---
# The secrets. **This file ships placeholders and no key material.**
#
# Create the real one out of band, once:
#
# kubectl -n gunnar create secret generic gunnar-keys \
# --from-literal=GUNNAR_SECRET_KEK="$(openssl rand -hex 32)"
#
# and delete this document from the kustomization. It is here so the shape is
# documented and so the check below has something to check.
apiVersion: v1
kind: Secret
metadata:
name: gunnar-keys
namespace: gunnar
type: Opaque
stringData:
# 32 bytes, hex. Everything gunnar seals is sealed under this; losing it means
# losing the transparency and provenance keys, which means the log can no
# longer be headed and the chain stops at the last signed head.
GUNNAR_SECRET_KEK: "REPLACE-ME-WITH-32-HEX-BYTES"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gunnar-config
namespace: gunnar
data:
GUNNAR_DATA_ROOT: "/var/lib/gunnar"
RUST_LOG: "info"
---
apiVersion: v1
kind: Service
metadata:
name: gunnar
namespace: gunnar
spec:
# A headless service, because the StatefulSet is one pod with a stable name
# and a client that needs to reach *that* pod should say so.
clusterIP: None
selector:
app.kubernetes.io/name: gunnar
ports:
- name: ssh
port: 2222
targetPort: ssh
- name: http
# Git over smart HTTP (and LFS), one listener in the server.
port: 8080
targetPort: http
- name: grpc
port: 50051
targetPort: grpc
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: gunnar
namespace: gunnar
spec:
serviceName: gunnar
# ONE. See the header: the store has a lock and a second writer is corruption,
# not capacity.
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: gunnar
template:
metadata:
labels:
app.kubernetes.io/name: gunnar
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: gunnar
# PINNED BY DIGEST. Replace the digest, never the form.
image: ghcr.io/example/gunnar@sha256:0000000000000000000000000000000000000000000000000000000000000000
args:
- serve
- --ssh-bind=0.0.0.0:2222
- --http-bind=0.0.0.0:8080
- --grpc-bind=0.0.0.0:50051
- --metrics-bind=0.0.0.0:9090
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
envFrom:
- configMapRef:
name: gunnar-config
- secretRef:
name: gunnar-keys
ports:
- name: ssh
containerPort: 2222
- name: http
containerPort: 8080
- name: grpc
containerPort: 50051
- name: metrics
containerPort: 9090
# `/healthz` answers without a credential once the HTTP listener is up.
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
tcpSocket:
port: ssh
initialDelaySeconds: 10
periodSeconds: 20
volumeMounts:
- name: data
mountPath: /var/lib/gunnar
- name: tmp
mountPath: /tmp
volumes:
# The root filesystem is read-only, so the one writable scratch path is
# declared rather than left to a container that would fail on first use.
- name: tmp
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi