Run Stalwart as a 3-node StatefulSet coordinated through Valkey
This commit is contained in:
@@ -12,8 +12,10 @@ resources:
|
|||||||
- stalwart-apply-rbac.yaml
|
- stalwart-apply-rbac.yaml
|
||||||
- stalwart-blobs-bucket.yaml
|
- stalwart-blobs-bucket.yaml
|
||||||
- stalwart-configmap.yaml
|
- stalwart-configmap.yaml
|
||||||
- stalwart-deployment.yaml
|
- stalwart-pdb.yaml
|
||||||
- stalwart-service.yaml
|
- stalwart-service.yaml
|
||||||
|
- stalwart-statefulset.yaml
|
||||||
|
- valkey.yaml
|
||||||
- vaultauth.yaml
|
- vaultauth.yaml
|
||||||
- vaultstaticsecret.yaml
|
- vaultstaticsecret.yaml
|
||||||
- vmpodscrape.yaml
|
- vmpodscrape.yaml
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
# Converges Stalwart's database-held settings to plan.ndjson after every sync,
|
# Converges Stalwart's database-held settings to plan.ndjson after every sync,
|
||||||
# then restarts the pod: listener and store changes only bind on startup.
|
# then rolls the StatefulSet: listener, store and coordinator changes only bind
|
||||||
# ponytail: restarts on every sync since neither image has a shell to diff apply output.
|
# on startup. It rolls on every sync because neither image has a shell to diff the
|
||||||
|
# apply output.
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: Job
|
kind: Job
|
||||||
metadata:
|
metadata:
|
||||||
@@ -68,7 +69,7 @@ spec:
|
|||||||
args:
|
args:
|
||||||
- rollout
|
- rollout
|
||||||
- restart
|
- restart
|
||||||
- deployment/stalwart
|
- statefulset/stalwart
|
||||||
- --namespace=mail
|
- --namespace=mail
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsUser: 65532
|
runAsUser: 65532
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ metadata:
|
|||||||
name: stalwart-apply
|
name: stalwart-apply
|
||||||
namespace: mail
|
namespace: mail
|
||||||
---
|
---
|
||||||
# Lets the stalwart-apply Job roll the Stalwart pod after applying the plan.
|
# Lets the stalwart-apply Job roll the Stalwart pods after applying the plan.
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
kind: Role
|
kind: Role
|
||||||
metadata:
|
metadata:
|
||||||
@@ -15,7 +15,7 @@ rules:
|
|||||||
- apiGroups:
|
- apiGroups:
|
||||||
- apps
|
- apps
|
||||||
resources:
|
resources:
|
||||||
- deployments
|
- statefulsets
|
||||||
resourceNames:
|
resourceNames:
|
||||||
- stalwart
|
- stalwart
|
||||||
verbs:
|
verbs:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
# config.json only names the datastore; everything else lives in Postgres and
|
# config.json only names the datastore; everything else lives in Postgres and
|
||||||
# is converged from plan.ndjson by the stalwart-apply PostSync Job.
|
# is converged from plan.ndjson by the stalwart-apply PostSync Job.
|
||||||
|
# The Coordinator uses the plain Redis client: its cluster client never
|
||||||
|
# resubscribes once the Valkey node it subscribed on is replaced.
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
@@ -19,7 +21,8 @@ data:
|
|||||||
{"@type":"reconcile","object":"Tracer","matchOn":"*","value":{"stdout":{"@type":"Stdout","enable":true,"level":"info","ansi":false,"multiline":false,"buffered":false,"lossy":false,"eventsPolicy":"exclude","events":{}}}}
|
{"@type":"reconcile","object":"Tracer","matchOn":"*","value":{"stdout":{"@type":"Stdout","enable":true,"level":"info","ansi":false,"multiline":false,"buffered":false,"lossy":false,"eventsPolicy":"exclude","events":{}}}}
|
||||||
{"@type":"update","object":"BlobStore","value":{"@type":"S3","bucket":"stalwart-blobs","region":{"@type":"Custom","customEndpoint":"https://s3.ceph.unkin.net","customRegion":"us-east-1"},"accessKey":{"@type":"EnvironmentVariable","variableName":"AWS_ACCESS_KEY_ID"},"secretKey":{"@type":"EnvironmentVariable","variableName":"AWS_SECRET_ACCESS_KEY"},"securityToken":{"@type":"None"},"sessionToken":{"@type":"None"}}}
|
{"@type":"update","object":"BlobStore","value":{"@type":"S3","bucket":"stalwart-blobs","region":{"@type":"Custom","customEndpoint":"https://s3.ceph.unkin.net","customRegion":"us-east-1"},"accessKey":{"@type":"EnvironmentVariable","variableName":"AWS_ACCESS_KEY_ID"},"secretKey":{"@type":"EnvironmentVariable","variableName":"AWS_SECRET_ACCESS_KEY"},"securityToken":{"@type":"None"},"sessionToken":{"@type":"None"}}}
|
||||||
{"@type":"update","object":"SearchStore","value":{"@type":"Default"}}
|
{"@type":"update","object":"SearchStore","value":{"@type":"Default"}}
|
||||||
{"@type":"update","object":"InMemoryStore","value":{"@type":"Default"}}
|
{"@type":"update","object":"InMemoryStore","value":{"@type":"RedisCluster","urls":{"redis://valkey-mail-valkey.mail.svc:6379":true},"authUsername":null,"authSecret":{"@type":"None"},"readFromReplicas":false,"protocolVersion":"resp3"}}
|
||||||
|
{"@type":"update","object":"Coordinator","value":{"@type":"Redis","url":"redis://valkey-mail-valkey.mail.svc:6379"}}
|
||||||
{"@type":"update","object":"Authentication","value":{"directoryId":"#authentik"}}
|
{"@type":"update","object":"Authentication","value":{"directoryId":"#authentik"}}
|
||||||
{"@type":"update","object":"MtaOutboundStrategy","value":{"route":{"match":{"0":{"if":"is_local_domain(rcpt_domain)","then":"'local'"}},"else":"'smarthost'"}}}
|
{"@type":"update","object":"MtaOutboundStrategy","value":{"route":{"match":{"0":{"if":"is_local_domain(rcpt_domain)","then":"'local'"}},"else":"'smarthost'"}}}
|
||||||
{"@type":"update","object":"MtaStageAuth","value":{"require":{"match":{},"else":"local_port != 25 && local_port != 24"}}}
|
{"@type":"update","object":"MtaStageAuth","value":{"require":{"match":{},"else":"local_port != 25 && local_port != 24"}}}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: stalwart
|
||||||
|
namespace: mail
|
||||||
|
spec:
|
||||||
|
maxUnavailable: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: stalwart
|
||||||
@@ -16,6 +16,21 @@ spec:
|
|||||||
port: 24
|
port: 24
|
||||||
targetPort: lmtp
|
targetPort: lmtp
|
||||||
---
|
---
|
||||||
|
# Governing Service for the StatefulSet's per-pod DNS.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: stalwart-headless
|
||||||
|
namespace: mail
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: stalwart
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8080
|
||||||
|
targetPort: http
|
||||||
|
---
|
||||||
# Client-facing mail ports on the lab network. 443 is here too so the single
|
# Client-facing mail ports on the lab network. 443 is here too so the single
|
||||||
# internal mail.unkin.net record serves web as well as IMAP/submission.
|
# internal mail.unkin.net record serves web as well as IMAP/submission.
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@@ -24,7 +39,7 @@ metadata:
|
|||||||
name: stalwart-lb
|
name: stalwart-lb
|
||||||
namespace: mail
|
namespace: mail
|
||||||
annotations:
|
annotations:
|
||||||
purelb.io/addresses: 198.18.200.11
|
purelb.io/addresses: 198.18.200.18
|
||||||
purelb.io/service-group: common
|
purelb.io/service-group: common
|
||||||
spec:
|
spec:
|
||||||
type: LoadBalancer
|
type: LoadBalancer
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
---
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: StatefulSet
|
||||||
metadata:
|
metadata:
|
||||||
name: stalwart
|
name: stalwart
|
||||||
namespace: mail
|
namespace: mail
|
||||||
annotations:
|
annotations:
|
||||||
secret.reloader.stakater.com/reload: "stalwart-admin,stalwart-db-app,stalwart-blobs-s3,wildcard-unkin-net-tls,vault-ca-cert,stalwart-db-ca"
|
secret.reloader.stakater.com/reload: "stalwart-admin,stalwart-db-app,stalwart-blobs-s3,wildcard-unkin-net-tls,vault-ca-cert,stalwart-db-ca"
|
||||||
spec:
|
spec:
|
||||||
# ponytail: single replica, no cluster coordinator; add one before scaling out.
|
# Nodes coordinate through mail-valkey; each leases its cluster node id by
|
||||||
replicas: 1
|
# hostname, so stable pod names keep a stable id across restarts.
|
||||||
strategy:
|
replicas: 3
|
||||||
type: Recreate
|
serviceName: stalwart-headless
|
||||||
|
podManagementPolicy: Parallel
|
||||||
|
updateStrategy:
|
||||||
|
type: RollingUpdate
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app.kubernetes.io/name: stalwart
|
app.kubernetes.io/name: stalwart
|
||||||
@@ -26,6 +29,13 @@ spec:
|
|||||||
fsGroup: 2000
|
fsGroup: 2000
|
||||||
seccompProfile:
|
seccompProfile:
|
||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
|
topologySpreadConstraints:
|
||||||
|
- maxSkew: 1
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
whenUnsatisfiable: DoNotSchedule
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: stalwart
|
||||||
# rustls loads SSL_CERT_FILE, so public roots, the internal CA (RGW) and the CNPG CA go in one bundle.
|
# rustls loads SSL_CERT_FILE, so public roots, the internal CA (RGW) and the CNPG CA go in one bundle.
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: combine-certs
|
- name: combine-certs
|
||||||
@@ -61,6 +71,10 @@ spec:
|
|||||||
image: docker.io/stalwartlabs/stalwart:v0.16.25
|
image: docker.io/stalwartlabs/stalwart:v0.16.25
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
env:
|
env:
|
||||||
|
- name: STALWART_HOSTNAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
- name: SSL_CERT_FILE
|
- name: SSL_CERT_FILE
|
||||||
value: /etc/ssl/combined/ca-certificates.crt
|
value: /etc/ssl/combined/ca-certificates.crt
|
||||||
- name: DB_PASSWORD
|
- name: DB_PASSWORD
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
# Stalwart's in-memory store (locks, rate limits, queue leases) and cluster
|
||||||
|
# coordinator pub/sub. Ephemeral state, so no persistence; the operator runs it
|
||||||
|
# cluster-mode, passwordless and without TLS behind valkey-mail-valkey.
|
||||||
|
apiVersion: valkey.io/v1alpha1
|
||||||
|
kind: ValkeyCluster
|
||||||
|
metadata:
|
||||||
|
name: mail-valkey
|
||||||
|
namespace: mail
|
||||||
|
spec:
|
||||||
|
shards: 1
|
||||||
|
replicas: 2
|
||||||
|
image: docker.io/valkey/valkey:9.0.0
|
||||||
|
exporter:
|
||||||
|
enabled: false
|
||||||
|
scheduling:
|
||||||
|
node:
|
||||||
|
spread:
|
||||||
|
shard:
|
||||||
|
mode: Required
|
||||||
|
podDisruptionBudget:
|
||||||
|
mode: Cluster
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
Reference in New Issue
Block a user