towerops/k8s/deployment.yaml
Graham McIntire 50324c61ce feat(metrics): expose Prometheus /metrics via PromEx on :9568
Add PromEx with Application/BEAM/Phoenix/LiveView/Ecto/Oban plugins plus
a custom plugin that bridges existing Towerops Oban/Redis telemetry events.
The metrics HTTP server runs isolated on port 9568 so scrape traffic never
traverses the public Traefik IngressRoute.

The pod template carries prometheus.io annotations (scrape, port, path, job)
so the external Prometheus on 10.0.15.31 discovers each pod through the
existing kubernetes-pods scrape job (apiserver-proxy).
2026-05-08 10:25:25 -05:00

249 lines
7.8 KiB
YAML

apiVersion: apps/v1
kind: Deployment
metadata:
name: towerops
namespace: towerops
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Allow 3rd pod during rollout to avoid downtime
maxUnavailable: 0 # Keep all pods running during rollout
minReadySeconds: 30 # Wait 30s after pod is ready before considering it available (endpoint propagation buffer)
selector:
matchLabels:
app: towerops
template:
metadata:
labels:
app: towerops
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9568"
prometheus.io/path: "/metrics"
prometheus.io/job: "towerops"
spec:
# Use system-cluster-critical priority to ensure towerops starts after CNI is ready
priorityClassName: system-cluster-critical
terminationGracePeriodSeconds: 90 # Allow 90s for graceful shutdown and connection draining
imagePullSecrets:
- name: forgejo-registry
securityContext:
runAsNonRoot: true
runAsUser: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
initContainers:
- name: migrate
image: codeberg.org/gmcintire/towerops:main-1777403311-23304e1
imagePullPolicy: IfNotPresent
command: ["/app/bin/migrate"]
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 65534
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
env:
- name: MIX_ENV
value: "prod"
- name: RELEASE_COOKIE
valueFrom:
secretKeyRef:
name: towerops-secrets
key: RELEASE_COOKIE
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: towerops-secrets
key: SECRET_KEY_BASE
- name: CLOAK_KEY
valueFrom:
secretKeyRef:
name: towerops-secrets
key: CLOAK_KEY
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_SECRET_KEY
optional: true
- name: STRIPE_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_WEBHOOK_SECRET
optional: true
- name: STRIPE_PRICE_ID
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_PRICE_ID
optional: true
- name: STRIPE_METER_ID
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_METER_ID
optional: true
envFrom:
- secretRef:
name: towerops-db
- secretRef:
name: towerops-aws
resources:
requests:
memory: "512Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
containers:
- name: towerops
image: codeberg.org/gmcintire/towerops:main-1777403311-23304e1
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 65534
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
ports:
- containerPort: 4000
name: http
- containerPort: 4369
name: epmd
- containerPort: 9000
name: dist
- containerPort: 9568
name: metrics
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# Stop accepting new connections by failing readiness probe
# Give load balancer time to remove pod from rotation
# Allow existing connections to complete
sleep 20
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ETCD_ENABLED
value: "true"
- name: RELEASE_DISTRIBUTION
value: "name"
- name: RELEASE_NODE
value: "towerops@$(POD_IP)"
- name: PORT
value: "4000"
- name: PHX_HOST
value: "towerops.net"
- name: MIX_ENV
value: "prod"
- name: RELEASE_COOKIE
valueFrom:
secretKeyRef:
name: towerops-secrets
key: RELEASE_COOKIE
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: towerops-secrets
key: SECRET_KEY_BASE
- name: CLOAK_KEY
valueFrom:
secretKeyRef:
name: towerops-secrets
key: CLOAK_KEY
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_SECRET_KEY
optional: true
- name: STRIPE_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_WEBHOOK_SECRET
optional: true
- name: STRIPE_PRICE_ID
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_PRICE_ID
optional: true
- name: STRIPE_METER_ID
valueFrom:
secretKeyRef:
name: towerops-billing
key: STRIPE_METER_ID
optional: true
envFrom:
# Redis connection configured via towerops-redis secret
- secretRef:
name: towerops-redis
- secretRef:
name: towerops-db
- secretRef:
name: towerops-aws
resources:
requests:
memory: "1.5Gi"
cpu: "500m"
limits:
memory: "3Gi"
cpu: "2000m"
startupProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 24 # 10s + (24 * 5s) = 130s max startup time
livenessProbe:
httpGet:
path: /health
port: 4000
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 4000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
successThreshold: 3 # Require 3 consecutive successes (15s) before marking ready
failureThreshold: 2 # Allow 1 failure before marking not ready
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
nfs:
server: 10.0.15.103
path: /data