Status: No MariaDB databases currently run on the cluster. This page sets the pattern for the next one, and some parts haven't been proven here yet (see "Verify on first use" below). Prefer PostgreSQL (CNPG) if your app supports it. Use MariaDB only when the app requires MySQL or MariaDB.
Every app that needs MariaDB gets its own MariaDB resource in the app's namespace. Don't deploy a standalone MariaDB container, a Bitnami chart, or the database bundled in an app's Helm chart. The operator (mdbo-system, v26.6.0) handles provisioning, replication, failover, users and backups.
All CRDs are k8s.mariadb.com/v1alpha1. The example below is for an app called myapp in the namespace myapp.
One MariaDB resource creates the server, the app's database, the app user with full rights on that database, and a ready-made connection Secret. The operator generates all passwords, so none go in Vault.
resource "kubectl_manifest" "myapp_db" {
yaml_body = <<-YAML
apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
name: myapp-db
namespace: myapp
spec:
image: docker-registry1.mariadb.com/library/mariadb:11.4.5
# Passwords are generated by the operator and stored in these Secrets.
rootPasswordSecretKeyRef:
name: myapp-db-root
key: password
generate: true
database: myapp
username: myapp
passwordSecretKeyRef:
name: myapp-db-password
key: password
generate: true
# Secret for the app: username, password, host, port, database, dsn
connection:
secretName: myapp-db-connection
secretTemplate:
key: dsn
# Primary + 1 replica with automatic failover. The volumes are strict-local,
# so HA comes from MariaDB replication, not from Longhorn. Anti-affinity puts
# the two pods on different storage nodes.
replicas: 2
replication:
enabled: true
primary:
autoFailover: true
affinity:
antiAffinityEnabled: true
# Required for longhorn-database: DB pods must run on storage nodes.
nodeSelector:
tier: pv-storage
tolerations:
- key: dedicated-purpose
operator: Equal
value: storage
effect: NoSchedule
storage:
size: 5Gi
storageClassName: longhorn-database
resizeInUseVolumes: true
waitForVolumeResize: true
myCnf: |
[mariadb]
bind-address=*
default_storage_engine=InnoDB
binlog_format=row
innodb_autoinc_lock_mode=2
max_allowed_packet=256M
# Replication needs binlogs. Without an expiry they grow until the volume fills.
binlog_expire_logs_seconds=604800
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1Gi
YAML
# longhorn-database uses a Retain policy. This guard stops a stray destroy from
# taking the database with it. To decommission, remove this block, apply, then remove.
lifecycle {
prevent_destroy = true
}
}
Physical backups are taken as Longhorn volume snapshots and pushed to the NAS through the longhorn-backup class. The operator takes them from the replica while it holds a backup lock, so the primary keeps serving.
resource "kubectl_manifest" "myapp_db_backup" {
yaml_body = <<-YAML
apiVersion: k8s.mariadb.com/v1alpha1
kind: PhysicalBackup
metadata:
name: myapp-db-daily
namespace: myapp
spec:
mariaDbRef:
name: ${kubectl_manifest.myapp_db.name}
schedule:
cron: "0 0 * * *" # 00:00 UTC daily. Standard 5-field cron (unlike CNPG)
maxRetention: 720h # operator deletes snapshots older than 30 days
storage:
volumeSnapshot:
volumeSnapshotClassName: longhorn-backup # NOT "longhorn" (local only)
YAML
}
Retention comes from maxRetention. The platform's cnpg-backup-pruner doesn't cover MariaDB. longhorn-backup uses deletionPolicy: Delete, so every snapshot the operator prunes, or deletes along with the PhysicalBackup, also deletes its NAS copy. Treat the PhysicalBackup resource as carefully as the backups themselves.
To restore, create a new MariaDB with spec.bootstrapFrom.volumeSnapshotRef pointing at the snapshot you want.
| Object | Name | Use |
|---|---|---|
| Secret | myapp-db-connection |
Keys: username, password, host, port, database, dsn. Use this one. |
| Secret | myapp-db-password |
The app user's password on its own, for charts that take only a password Secret |
| Service | myapp-db-primary |
Read/write. Follows the primary after a failover |
| Service | myapp-db-secondary |
Read-only replica, for read-heavy workloads that can tolerate lag |
| Secret | myapp-db-root |
Root password. Not for apps. For admin access only |
Raw Deployment / kubernetes_deployment_v1:
env {
name = "DB_HOST"
value_from {
secret_key_ref {
name = "myapp-db-connection"
key = "host"
}
}
}
env {
name = "DB_PASSWORD"
value_from {
secret_key_ref {
name = "myapp-db-connection"
key = "password"
}
}
}
# ...same for username, port, database. Or use `dsn` if the app takes one connection string.
For Helm charts, use the chart's existingSecret (or similar) option. Don't template the password into the values file. Add depends_on = [kubectl_manifest.myapp_db] so the database and Secrets exist before the app starts.
Only add these when the app needs more than its single owner account, for example a read-only reporting user. Store their passwords in Vault (apps/myapp) and sync them with an ExternalSecret, because User can't generate passwords:
apiVersion: k8s.mariadb.com/v1alpha1
kind: User
metadata:
name: myapp-readonly
namespace: myapp
spec:
mariaDbRef:
name: myapp-db
passwordSecretKeyRef:
name: myapp-readonly-password # synced from Vault by ESO
key: password
maxUserConnections: 10
host: "%"
---
apiVersion: k8s.mariadb.com/v1alpha1
kind: Grant
metadata:
name: myapp-readonly
namespace: myapp
spec:
mariaDbRef:
name: myapp-db
username: myapp-readonly
host: "%"
database: myapp # never "*" for app users
table: "*"
privileges:
- SELECT
MariaDB per app, named <app>-db, in the app's namespacereplicas: 2, replication.enabled: true, autoFailover: true, antiAffinityEnabled: truestorageClassName: longhorn-database with nodeSelector: tier: pv-storage and the dedicated-purpose=storage tolerationgenerate: true) for root and the app user. No DB passwords in Vault or Terraformbinlog_expire_logs_seconds set in myCnfPhysicalBackup using longhorn-backup, with an explicit maxRetentionlifecycle { prevent_destroy = true } on the MariaDBrandom_password plus a kubernetes_secret for DB passwords. That puts the password into Terraform state. Use generate: true.longhorn-database.database: "*" to app users.replicas: 1 unless you've accepted downtime whenever a storage node is drained. A strict-local volume can't move to another node.The manifests above pass the operator's validation webhooks on this cluster (server-side dry-run). These have not been tested on a running database here:
host in myapp-db-connection points at myapp-db-primary when replication is on.PhysicalBackup with longhorn-backup completes, and the snapshot appears on the NAS backup target.bootstrapFrom.volumeSnapshotRef works.Update this page once the first real MariaDB has gone through all four.