feat(deploy): add blue-green release manifests
This commit is contained in:
parent
0d8d8c43be
commit
3fa278d447
16 changed files with 739 additions and 0 deletions
41
deploy/base/api-services.yaml
Normal file
41
deploy/base/api-services.yaml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Active and preview Services for the blue-green Rollout.
|
||||||
|
#
|
||||||
|
# Argo Rollouts rewrites both selectors at runtime to pin them to the right
|
||||||
|
# ReplicaSet. What's below is just the starting state.
|
||||||
|
#
|
||||||
|
# Same app.kubernetes.io/name on both so one ServiceMonitor covers them, which
|
||||||
|
# is what gives the analysis query a `service` label to filter on.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nereus-api-active
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
app.kubernetes.io/component: active
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8080
|
||||||
|
targetPort: http
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nereus-api-preview
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
app.kubernetes.io/component: preview
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8080
|
||||||
|
targetPort: http
|
||||||
18
deploy/base/configmap.yaml
Normal file
18
deploy/base/configmap.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Non-secret API config. The DSN lives in the nereus-db Secret.
|
||||||
|
#
|
||||||
|
# Values must be quoted -- data is map[string]string, and an unquoted 8080
|
||||||
|
# gets rejected as an integer.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: nereus-api-config
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
data:
|
||||||
|
PORT: "8080"
|
||||||
|
LOG_LEVEL: "info"
|
||||||
|
# main.go only enables tracing when this is set.
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT: "http://nereus-otel-collector.observability.svc.cluster.local:4317"
|
||||||
|
# Patched to a nonzero value on the preview to trigger the rollback demo.
|
||||||
|
CHAOS_ERROR_RATE: "0.0"
|
||||||
40
deploy/base/ingress.yaml
Normal file
40
deploy/base/ingress.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Traefik ingress. Reachable at http://localhost:8080 in the k3d lab.
|
||||||
|
#
|
||||||
|
# Active Service only -- the preview stays internal so real traffic can't hit
|
||||||
|
# an unpromoted version. /metrics is not exposed either.
|
||||||
|
#
|
||||||
|
# No host, so any Host header matches. A real hostname belongs in the prod
|
||||||
|
# overlay.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: nereus-api
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- http:
|
||||||
|
paths:
|
||||||
|
- path: /api/v1
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: nereus-api-active
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
|
- path: /healthz
|
||||||
|
pathType: Exact
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: nereus-api-active
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
|
- path: /readyz
|
||||||
|
pathType: Exact
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: nereus-api-active
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
18
deploy/base/kustomization.yaml
Normal file
18
deploy/base/kustomization.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Plain Kubernetes only -- no Rollouts CRDs, so this still applies on a
|
||||||
|
# cluster without Argo installed.
|
||||||
|
#
|
||||||
|
# The nereus-db Secret is not here; see deploy/secrets/README.md.
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
# Sets the namespace on everything below, so the files don't repeat it.
|
||||||
|
namespace: nereus
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- configmap.yaml
|
||||||
|
- postgres.yaml
|
||||||
|
- api-services.yaml
|
||||||
|
- servicemonitor.yaml
|
||||||
|
- loadgen.yaml
|
||||||
|
- ingress.yaml
|
||||||
100
deploy/base/loadgen.yaml
Normal file
100
deploy/base/loadgen.yaml
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Load generator. A plain Deployment -- nothing to promote, and it has to keep
|
||||||
|
# running straight through an API rollout.
|
||||||
|
#
|
||||||
|
# Points at the active Service so its traffic never lands on the preview.
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nereus-loadgen
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-loadgen
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: nereus-loadgen
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-loadgen
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: nereus-registry
|
||||||
|
securityContext:
|
||||||
|
# distroless nonroot.
|
||||||
|
runAsUser: 65532
|
||||||
|
runAsGroup: 65532
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: loadgen
|
||||||
|
image: nereus-loadgen:0.1.0
|
||||||
|
# Side-loaded with `k3d image import`; there's no registry.
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: TARGET_URL
|
||||||
|
value: http://nereus-api-active.nereus.svc.cluster.local:8080
|
||||||
|
- name: RPS
|
||||||
|
value: "5"
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 25m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
memory: 128Mi
|
||||||
|
---
|
||||||
|
# Keeps request samples flowing through the preview while analysis runs.
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nereus-loadgen-preview
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-loadgen-preview
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: nereus-loadgen-preview
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-loadgen-preview
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: nereus-registry
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 65532
|
||||||
|
runAsGroup: 65532
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: loadgen
|
||||||
|
image: nereus-loadgen:0.1.0
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: TARGET_URL
|
||||||
|
value: http://nereus-api-preview.nereus.svc.cluster.local:8080
|
||||||
|
- name: RPS
|
||||||
|
value: "5"
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 25m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
memory: 128Mi
|
||||||
7
deploy/base/namespace.yaml
Normal file
7
deploy/base/namespace.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: nereus
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
113
deploy/base/postgres.yaml
Normal file
113
deploy/base/postgres.yaml
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
# Postgres 17, StatefulSet + headless Service.
|
||||||
|
#
|
||||||
|
# StatefulSet rather than Deployment for the stable pod name and a PVC that
|
||||||
|
# survives the pod.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nereus-postgres
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-postgres
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: nereus-postgres
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
port: 5432
|
||||||
|
targetPort: postgres
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: nereus-postgres
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-postgres
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
serviceName: nereus-postgres
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: nereus-postgres
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-postgres
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
# postgres:17-alpine runs as uid 70. The Debian tags use 999.
|
||||||
|
runAsUser: 70
|
||||||
|
runAsGroup: 70
|
||||||
|
fsGroup: 70
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: postgres:17-alpine
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
containerPort: 5432
|
||||||
|
env:
|
||||||
|
- name: POSTGRES_DB
|
||||||
|
value: nereus
|
||||||
|
- name: POSTGRES_USER
|
||||||
|
value: nereus
|
||||||
|
- name: POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: nereus-db
|
||||||
|
key: POSTGRES_PASSWORD
|
||||||
|
# Subdirectory, or initdb trips over lost+found on the volume.
|
||||||
|
- name: PGDATA
|
||||||
|
value: /var/lib/postgresql/data/pgdata
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
- name: runtime
|
||||||
|
mountPath: /var/run/postgresql
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["pg_isready", "-U", "nereus", "-d", "nereus"]
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 6
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["pg_isready", "-U", "nereus", "-d", "nereus"]
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 15
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
memory: 1Gi
|
||||||
|
volumes:
|
||||||
|
- name: runtime
|
||||||
|
emptyDir: {}
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
sizeLimit: 64Mi
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
24
deploy/base/servicemonitor.yaml
Normal file
24
deploy/base/servicemonitor.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Prometheus Operator CRD, not core Kubernetes. Works because the k3d values
|
||||||
|
# set serviceMonitorSelectorNilUsesHelmValues: false.
|
||||||
|
#
|
||||||
|
# Matches both Services, so each pod is scraped once per Service. That's the
|
||||||
|
# point -- the `service` label is how the analysis isolates the preview.
|
||||||
|
apiVersion: monitoring.coreos.com/v1
|
||||||
|
kind: ServiceMonitor
|
||||||
|
metadata:
|
||||||
|
name: nereus-api
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
namespaceSelector:
|
||||||
|
matchNames:
|
||||||
|
- nereus
|
||||||
|
endpoints:
|
||||||
|
- port: http
|
||||||
|
path: /metrics
|
||||||
|
interval: 15s
|
||||||
|
scrapeTimeout: 10s
|
||||||
9
deploy/observability/kustomization.yaml
Normal file
9
deploy/observability/kustomization.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Lands in `observability`, alongside kube-prometheus-stack.
|
||||||
|
# kubectl apply -k deploy/observability
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: observability
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- otel-collector.yaml
|
||||||
109
deploy/observability/otel-collector.yaml
Normal file
109
deploy/observability/otel-collector.yaml
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# OTLP collector for the cluster. The API's OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
|
# points here.
|
||||||
|
#
|
||||||
|
# Traces only. The log pipeline in observability/otel-collector/config.yaml
|
||||||
|
# needs a DaemonSet with hostPath access to /var/log/pods plus RBAC, and Loki
|
||||||
|
# isn't in the cluster yet.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: nereus-otel-collector
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-otel-collector
|
||||||
|
data:
|
||||||
|
config.yaml: |
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
endpoint: 0.0.0.0:4317
|
||||||
|
|
||||||
|
processors:
|
||||||
|
memory_limiter:
|
||||||
|
check_interval: 1s
|
||||||
|
limit_mib: 256
|
||||||
|
batch:
|
||||||
|
timeout: 5s
|
||||||
|
|
||||||
|
exporters:
|
||||||
|
otlp/tempo:
|
||||||
|
endpoint: tempo.observability.svc.cluster.local:4317
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
# Keeps traces visible in `kubectl logs` until Tempo is deployed.
|
||||||
|
debug:
|
||||||
|
verbosity: basic
|
||||||
|
|
||||||
|
service:
|
||||||
|
pipelines:
|
||||||
|
traces:
|
||||||
|
receivers: [otlp]
|
||||||
|
processors: [memory_limiter, batch]
|
||||||
|
exporters: [otlp/tempo, debug]
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nereus-otel-collector
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-otel-collector
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: nereus-otel-collector
|
||||||
|
ports:
|
||||||
|
- name: otlp-grpc
|
||||||
|
port: 4317
|
||||||
|
targetPort: otlp-grpc
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nereus-otel-collector
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-otel-collector
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: nereus-otel-collector
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-otel-collector
|
||||||
|
annotations:
|
||||||
|
# Restarts the pod when the config changes; the collector doesn't reload.
|
||||||
|
checksum/config: placeholder
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 10001
|
||||||
|
runAsGroup: 10001
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: otel-collector
|
||||||
|
image: otel/opentelemetry-collector-contrib:0.159.0
|
||||||
|
args: ["--config=/conf/config.yaml"]
|
||||||
|
ports:
|
||||||
|
- name: otlp-grpc
|
||||||
|
containerPort: 4317
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /conf
|
||||||
|
readOnly: true
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
memory: 384Mi
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: nereus-otel-collector
|
||||||
30
deploy/overlays/dev/kustomization.yaml
Normal file
30
deploy/overlays/dev/kustomization.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# dev -- the local k3d lab.
|
||||||
|
# kubectl apply -k deploy/overlays/dev
|
||||||
|
#
|
||||||
|
# Needs the images imported and the nereus-db Secret created first.
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: nereus
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../../base
|
||||||
|
- ../../rollouts
|
||||||
|
|
||||||
|
patches:
|
||||||
|
# One replica is easier to watch swap over, and enough on a laptop.
|
||||||
|
- target:
|
||||||
|
kind: Rollout
|
||||||
|
name: nereus-api
|
||||||
|
patch: |
|
||||||
|
- op: replace
|
||||||
|
path: /spec/replicas
|
||||||
|
value: 1
|
||||||
|
|
||||||
|
- target:
|
||||||
|
kind: StatefulSet
|
||||||
|
name: nereus-postgres
|
||||||
|
patch: |
|
||||||
|
- op: replace
|
||||||
|
path: /spec/volumeClaimTemplates/0/spec/resources/requests/storage
|
||||||
|
value: 2Gi
|
||||||
51
deploy/overlays/prod/kustomization.yaml
Normal file
51
deploy/overlays/prod/kustomization.yaml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# prod overlay -- the two Fedora nodes.
|
||||||
|
# kubectl apply -k deploy/overlays/prod
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: nereus
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ../../base
|
||||||
|
- ../../rollouts
|
||||||
|
|
||||||
|
images:
|
||||||
|
- name: nereus-api
|
||||||
|
newName: git.fiwlabs.dev/fiwdev/nereus-api
|
||||||
|
newTag: 0.1.0
|
||||||
|
- name: nereus-loadgen
|
||||||
|
newName: git.fiwlabs.dev/fiwdev/nereus-loadgen
|
||||||
|
newTag: 0.1.0
|
||||||
|
|
||||||
|
patches:
|
||||||
|
# 3 replicas so a node can go down mid-rollout.
|
||||||
|
- target:
|
||||||
|
kind: Rollout
|
||||||
|
name: nereus-api
|
||||||
|
patch: |
|
||||||
|
- op: replace
|
||||||
|
path: /spec/replicas
|
||||||
|
value: 3
|
||||||
|
|
||||||
|
# Spread the API across both nodes.
|
||||||
|
- target:
|
||||||
|
kind: Rollout
|
||||||
|
name: nereus-api
|
||||||
|
patch: |
|
||||||
|
- op: add
|
||||||
|
path: /spec/template/spec/topologySpreadConstraints
|
||||||
|
value:
|
||||||
|
- maxSkew: 1
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
whenUnsatisfiable: ScheduleAnyway
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
|
||||||
|
- target:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: nereus-api-config
|
||||||
|
patch: |
|
||||||
|
- op: replace
|
||||||
|
path: /data/LOG_LEVEL
|
||||||
|
value: "warn"
|
||||||
36
deploy/rollouts/analysistemplate.yaml
Normal file
36
deploy/rollouts/analysistemplate.yaml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# The real error-rate gate, replacing the vector(0.0) harness in
|
||||||
|
# scripts/k3d/analysis-harness/.
|
||||||
|
#
|
||||||
|
# Runs before promotion. Every sample has to pass; one failure aborts and the
|
||||||
|
# active Service never moves.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: AnalysisTemplate
|
||||||
|
metadata:
|
||||||
|
name: nereus-api-error-rate
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
args:
|
||||||
|
- name: service
|
||||||
|
metrics:
|
||||||
|
- name: error-rate
|
||||||
|
# Let the new pods take traffic first.
|
||||||
|
initialDelay: 30s
|
||||||
|
interval: 20s
|
||||||
|
count: 5
|
||||||
|
failureLimit: 0
|
||||||
|
# Empty result means no traffic yet -- not a failure.
|
||||||
|
successCondition: "len(result) == 0 || result[0] < 0.05"
|
||||||
|
provider:
|
||||||
|
prometheus:
|
||||||
|
address: http://kube-prometheus-stack-prometheus.observability.svc.cluster.local:9090
|
||||||
|
# 5xx as a fraction of all responses on the preview.
|
||||||
|
# `or vector(0)` keeps the division defined when there are no errors.
|
||||||
|
query: |
|
||||||
|
(
|
||||||
|
sum(rate(nereus_http_requests_total{service="{{args.service}}",status=~"5.."}[1m]))
|
||||||
|
or vector(0)
|
||||||
|
)
|
||||||
|
/
|
||||||
|
sum(rate(nereus_http_requests_total{service="{{args.service}}"}[1m]))
|
||||||
8
deploy/rollouts/kustomization.yaml
Normal file
8
deploy/rollouts/kustomization.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Argo Rollouts resources, kept separate from base/ so the plain-Kubernetes
|
||||||
|
# manifests stay applicable on a cluster without the Rollouts CRDs installed.
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- rollout.yaml
|
||||||
|
- analysistemplate.yaml
|
||||||
100
deploy/rollouts/rollout.yaml
Normal file
100
deploy/rollouts/rollout.yaml
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# The API, as a blue-green Rollout.
|
||||||
|
#
|
||||||
|
# Same shape as a Deployment, but the new version comes up alongside the old,
|
||||||
|
# gets checked against Prometheus, and only then takes over the active Service.
|
||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Rollout
|
||||||
|
metadata:
|
||||||
|
name: nereus-api
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
revisionHistoryLimit: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nereus-api
|
||||||
|
app.kubernetes.io/part-of: nereus
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: nereus-registry
|
||||||
|
securityContext:
|
||||||
|
# distroless nonroot.
|
||||||
|
runAsUser: 65532
|
||||||
|
runAsGroup: 65532
|
||||||
|
runAsNonRoot: true
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: api
|
||||||
|
image: nereus-api:0.1.0
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: nereus-api-config
|
||||||
|
env:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: nereus-db
|
||||||
|
key: DATABASE_URL
|
||||||
|
# /healthz ignores Postgres, so a database outage makes pods unready
|
||||||
|
# without restarting them.
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 2
|
||||||
|
failureThreshold: 3
|
||||||
|
# /readyz waits for migrations.
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /readyz
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
volumeMounts:
|
||||||
|
- name: tmp
|
||||||
|
mountPath: /tmp
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
volumes:
|
||||||
|
- name: tmp
|
||||||
|
emptyDir:
|
||||||
|
medium: Memory
|
||||||
|
sizeLimit: 16Mi
|
||||||
|
strategy:
|
||||||
|
blueGreen:
|
||||||
|
activeService: nereus-api-active
|
||||||
|
previewService: nereus-api-preview
|
||||||
|
# Auto-promote only if the analysis passes; a failure aborts instead.
|
||||||
|
autoPromotionEnabled: true
|
||||||
|
# Keep the old ReplicaSet warm so an abort falls straight back.
|
||||||
|
scaleDownDelaySeconds: 30
|
||||||
|
prePromotionAnalysis:
|
||||||
|
templates:
|
||||||
|
- templateName: nereus-api-error-rate
|
||||||
|
args:
|
||||||
|
- name: service
|
||||||
|
value: nereus-api-preview
|
||||||
35
deploy/secrets/README.md
Normal file
35
deploy/secrets/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Secrets
|
||||||
|
|
||||||
|
`nereus-db` holds two keys, used by `postgres.yaml` and `rollout.yaml`:
|
||||||
|
|
||||||
|
- `POSTGRES_PASSWORD`
|
||||||
|
- `DATABASE_URL` — `postgres://nereus:<password>@nereus-postgres:5432/nereus?sslmode=disable`
|
||||||
|
|
||||||
|
It is not in `base/kustomization.yaml`. Plaintext never lands in the repo, and
|
||||||
|
gitleaks runs on every push.
|
||||||
|
|
||||||
|
## dev (k3d)
|
||||||
|
|
||||||
|
Create it directly, before `kubectl apply -k deploy/overlays/dev`:
|
||||||
|
|
||||||
|
```fish
|
||||||
|
set pw (openssl rand -hex 16)
|
||||||
|
kubectl create secret generic nereus-db -n nereus \
|
||||||
|
--from-literal=POSTGRES_PASSWORD=$pw \
|
||||||
|
--from-literal=DATABASE_URL="postgres://nereus:$pw@nereus-postgres:5432/nereus?sslmode=disable"
|
||||||
|
```
|
||||||
|
|
||||||
|
Rotating means deleting the secret, the StatefulSet's PVC, and restarting —
|
||||||
|
Postgres only reads `POSTGRES_PASSWORD` when it initialises the data directory.
|
||||||
|
|
||||||
|
## prod (sealed)
|
||||||
|
|
||||||
|
Write the plaintext to `nereus-db.plain.yaml` (gitignored), then:
|
||||||
|
|
||||||
|
```fish
|
||||||
|
kubeseal --format yaml < nereus-db.plain.yaml > nereus-db-sealed.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit only `nereus-db-sealed.yaml` and add it to the prod overlay. The
|
||||||
|
controller isn't installed yet — `kubectl get crd | grep sealed` comes back
|
||||||
|
empty.
|
||||||
Loading…
Add table
Reference in a new issue