Skip to content

Commit 1451ff0

Browse files
authored
feat(mongodb): add MongoDB datastore chart and service-chart support (#324)
1 parent cae24bf commit 1451ff0

22 files changed

Lines changed: 1073 additions & 210 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ Below is a list of available charts along with their links:
121121
| **SolrCloud** | [helm.zop.dev/solrcloud](https://helm.zop.dev/src/readme.html?id=solrcloud) | |
122122
| **ScyllaDB** | [helm.zop.dev/scylladb](https://helm.zop.dev/src/readme.html?id=scylladb) | |
123123
| **Qdrant** | [helm.zop.dev/qdrant](https://helm.zop.dev/src/readme.html?id=qdrant) | ✅ |
124+
| **MongoDB** | [helm.zop.dev/mongodb](https://helm.zop.dev/src/readme.html?id=mongodb) | ✅ |
124125

125126

126127
2. **APPLICATIONS**

‎charts/mongodb/Chart.yaml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: v2
2+
appVersion: "7.0"
3+
description: Helm chart for deploying a MongoDB datastore
4+
name: mongodb
5+
version: 0.0.1
6+
icon: "https://storage.googleapis.com/zopdev-blog-resources/1/internal-dashboard/2f3de4b7-aad3-466a-82a1-d0f0a3b34f84-mongodb.webp"
7+
maintainers:
8+
- name: ZopDev
9+
url: zop.dev
10+
annotations:
11+
type: datasource

‎charts/mongodb/README.md‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# MongoDB Helm Chart
2+
3+
Deploys a single-node [MongoDB](https://www.mongodb.com/) server on Kubernetes, with persistence, a Prometheus exporter, alerts, and one database + user per application listed in `services`.
4+
5+
> **Single node.** One `mongod` (`replicas: 1`), no replica set. It is meant as an application datastore, not a highly available MongoDB cluster.
6+
7+
## Prerequisites
8+
9+
- Kubernetes 1.19+
10+
- Helm 3+
11+
- **Prometheus Operator CRDs** (`ServiceMonitor`, `PrometheusRule`). The chart renders both unconditionally, so `helm install` fails on a cluster without them.
12+
13+
## Add Helm Repository
14+
15+
```bash
16+
helm repo add zopdev https://helm.zop.dev
17+
helm repo update
18+
```
19+
20+
## Install Helm Chart
21+
22+
```bash
23+
helm install my-mongo zopdev/mongodb \
24+
--set 'services[0].name=myapp' \
25+
--set 'services[0].database=myapp'
26+
```
27+
28+
## Uninstall Helm Chart
29+
30+
```bash
31+
helm uninstall my-mongo
32+
```
33+
34+
The data volume (`<release>-mongodb-persistent-storage-<release>-mongodb-0`) is kept after uninstall, as with any StatefulSet. Delete the PVC to drop the data.
35+
36+
---
37+
38+
## Configuration
39+
40+
| Parameter | Description | Default |
41+
|-----------|-------------|---------|
42+
| `version` | MongoDB image tag (`mongo:<version>`), pinned to a patch release | `7.0.45` |
43+
| `diskSize` | Size of the PVC backing `/data/db`. Fixed at install time | `10Gi` |
44+
| `wiredTigerCacheSizeGB` | WiredTiger cache size, minimum `0.25`. Keep it at about half of the memory limit or less | `0.25` |
45+
| `resources.requests.cpu` | CPU request | `250m` |
46+
| `resources.requests.memory` | Memory request | `500Mi` |
47+
| `resources.limits.cpu` | CPU limit | `1000m` |
48+
| `resources.limits.memory` | Memory limit | `1Gi` |
49+
| `services` | Databases to create: a list of `{name, database}` | `[]` |
50+
| `name` | Service the alerts are attributed to | release name |
51+
| `nodeSelector` / `tolerations` / `affinity` | Pod scheduling | empty |
52+
53+
---
54+
55+
## What each `services` entry gets
56+
57+
For every entry `{name: <svc>, database: <db>}` the chart creates a user scoped to `<db>` (roles `readWrite` and `dbAdmin` on that database only) through a one-shot Job, plus:
58+
59+
**ConfigMap** `<release>-<db>-<svc>-mongodb-configmap`
60+
61+
| Key | Value |
62+
|-----|-------|
63+
| `MONGO_HOST` | `<release>-mongodb` |
64+
| `MONGO_PORT` | `27017` |
65+
| `MONGO_DB` | `<db>` |
66+
67+
**Secret** `<release>-<db>-<svc>-mongodb-database-secret`
68+
69+
| Key | Value |
70+
|-----|-------|
71+
| `MONGO_USER` | generated username |
72+
| `MONGO_PASSWORD` | generated password |
73+
| `MONGO_URI` / `MONGODB_URI` | `mongodb://<user>:<password>@<release>-mongodb:27017/<db>?authSource=<db>` |
74+
75+
Underscores in `<db>` become hyphens in the object names. Credentials are generated on install and read back from the live Secret on every upgrade, so they stay stable.
76+
77+
Attach them to an app built on the `service` chart (0.0.34+) with:
78+
79+
```yaml
80+
datastores:
81+
mongodb:
82+
- datastore: <release>
83+
database: <db>
84+
```
85+
86+
The `service` chart looks for `<datastore>-<db>-<its release>-mongodb-*`, so this matches when the mongodb release, the `services[].name` and the app release share one name, which is the case when mongodb is bundled as a subchart.
87+
88+
The root password lives in `<release>-mongodb-root-secret`.
89+
90+
### Credential caveats
91+
92+
- **Uninstall keeps the data and the root password.** `helm uninstall` leaves the data PVC (as with any StatefulSet) and, through `helm.sh/resource-policy: keep`, the `<release>-mongodb-root-secret`. The root password is written into the volume on first boot only, so a reinstall with the **same release name in the same namespace** adopts the kept Secret and can still log in. To start from scratch, delete both: `kubectl delete pvc <release>-mongodb-persistent-storage-<release>-mongodb-0 secret <release>-mongodb-root-secret`.
93+
94+
- **Render with a cluster connection.** Credentials are kept stable by reading the live Secrets with `lookup`. Under `helm template`, or a GitOps tool that renders without cluster access (for example Argo CD), `lookup` returns nothing, so every render generates a new username and password. The postgres chart behaves the same way. Install and upgrade with `helm install` / `helm upgrade`.
95+
- **Don't hand-edit `MONGO_PASSWORD`.** The init Job's name hashes the username but deliberately not the password, so changing only the password in the Secret does not re-run the Job, and mongod keeps the old password. If the Secret is deleted instead, the next upgrade generates a new username as well, which changes the Job name and re-runs it.
96+
97+
---
98+
99+
## Monitoring
100+
101+
A `percona/mongodb_exporter` sidecar serves metrics on port `2121`, scraped through the `<release>-mongodb` ServiceMonitor. The `<release>-mongodb` PrometheusRule raises:
102+
103+
| Alert | Fires when |
104+
|-------|------------|
105+
| `MongodbDown` | `mongod` is unreachable, or the exporter has stopped reporting (pod or exporter gone) for 1 minute |
106+
| `MongodbRestarted` | `mongod` has been up for less than a minute |
107+
| `MongodbTooManyConnections` | more than 80% of available connections are in use for 2 minutes |
108+
109+
Every alert carries a `service` label set to `name` (the owning service), or to the release name when `name` is empty, so alerts can be routed to whoever owns the app.
110+
111+
The rule is named `<release>-mongodb`, not after the bare release name, so an application chart can bundle mongodb next to redis or postgres without a PrometheusRule name collision.
112+
113+
---
114+
115+
## Contributing
116+
117+
Please refer to [CONTRIBUTING.md](../../CONTRIBUTING.md).
118+
119+
## License
120+
121+
This project is licensed under the [LICENSE](../../LICENSE).
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{{/*
2+
Fully qualified name of the MongoDB server: <release>-mongodb, capped for DNS.
3+
*/}}
4+
{{- define "mongodb.fullname" -}}
5+
{{- printf "%s-mongodb" .Release.Name | trunc 63 | trimSuffix "-" -}}
6+
{{- end -}}
7+
8+
{{/*
9+
Name of the Secret holding the generated root password.
10+
*/}}
11+
{{- define "mongodb.rootSecretName" -}}
12+
{{- printf "%s-mongodb-root-secret" .Release.Name -}}
13+
{{- end -}}
14+
15+
{{- define "mongodb.chart" -}}
16+
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
17+
{{- end -}}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
apiVersion: monitoring.coreos.com/v1
2+
kind: PrometheusRule
3+
metadata:
4+
# <release>-mongodb, NOT the bare release name the postgres and redis charts
5+
# use. An application chart that bundles mongodb next to redis would otherwise
6+
# render two PrometheusRules named after the release, and helm refuses to
7+
# install two objects of one kind+name:
8+
# Error: prometheusrules.monitoring.coreos.com "<release>" already exists
9+
name: {{ include "mongodb.fullname" . }}
10+
namespace: {{ .Release.Namespace }}
11+
labels:
12+
app: kube-prometheus-stack
13+
heritage: Helm
14+
release: prometheus
15+
spec:
16+
groups:
17+
- name: {{ .Release.Namespace }}.{{ .Release.Name }}-mongodb.rules
18+
rules:
19+
# mongodb_up == 0 only fires while the exporter is alive to report it.
20+
# If the exporter container or the whole pod is gone the series goes
21+
# absent instead, which absent() catches.
22+
- alert: MongodbDown
23+
expr: |
24+
mongodb_up{namespace="{{ .Release.Namespace }}", service="{{ include "mongodb.fullname" . }}-headless"} == 0
25+
or
26+
absent(mongodb_up{namespace="{{ .Release.Namespace }}", service="{{ include "mongodb.fullname" . }}-headless"})
27+
for: 1m
28+
labels:
29+
severity: critical
30+
servicealert: "true"
31+
namespace: {{ .Release.Namespace }}
32+
service: {{ .Values.name | default .Release.Name | quote }}
33+
annotations:
34+
summary: 'MongoDB instance {{ .Release.Name }} is down'
35+
description: 'MongoDB {{ .Release.Name }} is unreachable, or its exporter has stopped reporting'
36+
37+
- alert: MongodbRestarted
38+
expr: mongodb_ss_uptime{namespace="{{ .Release.Namespace }}", service="{{ include "mongodb.fullname" . }}-headless"} < 60
39+
for: 0m
40+
labels:
41+
severity: critical
42+
servicealert: "true"
43+
namespace: {{ .Release.Namespace }}
44+
service: {{ .Values.name | default .Release.Name | quote }}
45+
annotations:
46+
summary: 'MongoDB instance {{ .Release.Name }} restarted'
47+
description: 'MongoDB instance {{ .Release.Name }} restarted < 1 minute ago'
48+
49+
- alert: MongodbTooManyConnections
50+
expr: |
51+
mongodb_ss_connections{namespace="{{ .Release.Namespace }}", service="{{ include "mongodb.fullname" . }}-headless", conn_type="current"}
52+
/ ignoring(conn_type)
53+
(mongodb_ss_connections{namespace="{{ .Release.Namespace }}", service="{{ include "mongodb.fullname" . }}-headless", conn_type="current"}
54+
+ ignoring(conn_type)
55+
mongodb_ss_connections{namespace="{{ .Release.Namespace }}", service="{{ include "mongodb.fullname" . }}-headless", conn_type="available"}) * 100 > 80
56+
for: 2m
57+
labels:
58+
severity: warning
59+
servicealert: "true"
60+
namespace: {{ .Release.Namespace }}
61+
service: {{ .Values.name | default .Release.Name | quote }}
62+
annotations:
63+
summary: 'MongoDB instance {{ .Release.Name }} has too many connections (> 80%)'
64+
description: 'More than 80% of MongoDB connections of instance {{ .Release.Name }} are in use'
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
{{- range $index, $service := .Values.services }}
2+
{{- $dbName := replace "_" "-" $service.database }}
3+
{{- $host := include "mongodb.fullname" $ }}
4+
{{- $generatedUsername := printf "%s_%s_user" (trunc 27 $service.name) (randAlphaNum 4 | lower) }}
5+
{{- $generatedPassword := printf "%s%s%s" (randAlpha 6) (randNumeric 6) (randAlpha 6 | upper) }}
6+
7+
{{/* Same naming as the other datastore charts, so the service chart can find
8+
these by datastore/database/release: <release>-<database>-<service>-mongodb-* */}}
9+
{{- $secretName := printf "%s-%s-%s-mongodb-database-secret" $.Release.Name $dbName $service.name }}
10+
11+
{{/* Keep the credentials of an existing install: the user already exists in
12+
mongod with this password, and regenerating it would lock the app out. */}}
13+
{{- $username := $generatedUsername }}
14+
{{- $password := $generatedPassword }}
15+
{{- $existingSecret := lookup "v1" "Secret" $.Release.Namespace $secretName }}
16+
{{- if $existingSecret }}
17+
{{- with $existingSecret.data }}
18+
{{- if hasKey . "MONGO_USER" }}
19+
{{- $username = index . "MONGO_USER" | b64dec }}
20+
{{- end }}
21+
{{- if hasKey . "MONGO_PASSWORD" }}
22+
{{- $password = index . "MONGO_PASSWORD" | b64dec }}
23+
{{- end }}
24+
{{- end }}
25+
{{- end }}
26+
{{- $uri := printf "mongodb://%s:%s@%s:27017/%s?authSource=%s" $username $password $host $service.database $service.database }}
27+
---
28+
apiVersion: v1
29+
kind: ConfigMap
30+
metadata:
31+
name: {{ $.Release.Name }}-{{ $dbName }}-{{ $service.name }}-mongodb-configmap
32+
namespace: {{ $.Release.Namespace }}
33+
data:
34+
MONGO_HOST: {{ $host | quote }}
35+
MONGO_PORT: "27017"
36+
MONGO_DB: {{ $service.database | quote }}
37+
---
38+
apiVersion: v1
39+
kind: Secret
40+
metadata:
41+
name: {{ $secretName }}
42+
namespace: {{ $.Release.Namespace }}
43+
type: Opaque
44+
data:
45+
MONGO_USER: {{ $username | b64enc }}
46+
MONGO_PASSWORD: {{ $password | b64enc }}
47+
# The user is created inside its own database, so authSource points there.
48+
MONGO_URI: {{ $uri | b64enc }}
49+
MONGODB_URI: {{ $uri | b64enc }}
50+
---
51+
{{/* A Job's spec.template is immutable, so anything that changes the pod must
52+
change the name too: helm then creates a new Job and prunes the old one
53+
instead of failing the upgrade with "field is immutable". The password is
54+
left out of the hash on purpose - Job names are visible to anyone who can
55+
list jobs - and is read from the Secret at run time instead of being
56+
templated into the script. */}}
57+
{{- $sig := printf "%s|%s|%s|%s" $.Chart.Version $.Values.version $username $service.database | sha256sum | trunc 8 }}
58+
{{- $initName := printf "mongodb-%s-%s-init" $.Release.Name $service.name | trunc 54 | trimSuffix "-" }}
59+
apiVersion: batch/v1
60+
kind: Job
61+
metadata:
62+
name: {{ $initName }}-{{ $sig }}
63+
namespace: {{ $.Release.Namespace }}
64+
labels:
65+
app.kubernetes.io/name: mongodb
66+
app.kubernetes.io/instance: {{ $.Release.Name }}
67+
app.kubernetes.io/managed-by: {{ $.Release.Service }}
68+
app.kubernetes.io/component: db-init
69+
helm.sh/chart: {{ include "mongodb.chart" $ }}
70+
spec:
71+
# A Job, not a bare Pod, so `helm --wait` is satisfied once it succeeds.
72+
# The script itself waits for mongod; activeDeadlineSeconds caps the case
73+
# where mongod never comes up.
74+
activeDeadlineSeconds: 1800
75+
template:
76+
metadata:
77+
labels:
78+
app.kubernetes.io/name: mongodb
79+
app.kubernetes.io/instance: {{ $.Release.Name }}
80+
app.kubernetes.io/component: db-init
81+
spec:
82+
{{- with $.Values.nodeSelector }}
83+
nodeSelector:
84+
{{- toYaml . | nindent 8 }}
85+
{{- end }}
86+
{{- with $.Values.tolerations }}
87+
tolerations:
88+
{{- toYaml . | nindent 8 }}
89+
{{- end }}
90+
{{- with $.Values.affinity }}
91+
affinity:
92+
{{- toYaml . | nindent 8 }}
93+
{{- end }}
94+
restartPolicy: OnFailure
95+
containers:
96+
- name: mongodb-init-user
97+
image: "mongo:{{ $.Values.version }}"
98+
securityContext:
99+
runAsNonRoot: true
100+
runAsUser: 999
101+
runAsGroup: 999
102+
allowPrivilegeEscalation: false
103+
capabilities:
104+
drop:
105+
- ALL
106+
command: ["/bin/bash", "-c"]
107+
args:
108+
- |
109+
set -euo pipefail
110+
until mongosh --quiet --norc --host "$MONGO_HOST" --eval "db.adminCommand('ping')" >/dev/null 2>&1; do
111+
echo "waiting for $MONGO_HOST"; sleep 3
112+
done
113+
# Credentials are read from the environment inside the script rather
114+
# than passed as -u/-p, so they never appear in the process arguments.
115+
mongosh --quiet --norc --host "$MONGO_HOST" \
116+
--eval '
117+
db.getSiblingDB("admin").auth(process.env.ROOT_USER, process.env.ROOT_PASSWORD);
118+
const d = db.getSiblingDB(process.env.APP_DB);
119+
const roles = [{ role: "readWrite", db: process.env.APP_DB }, { role: "dbAdmin", db: process.env.APP_DB }];
120+
if (d.getUser(process.env.APP_USER)) {
121+
d.updateUser(process.env.APP_USER, { pwd: process.env.APP_PASSWORD, roles: roles });
122+
print("updated user " + process.env.APP_USER);
123+
} else {
124+
d.createUser({ user: process.env.APP_USER, pwd: process.env.APP_PASSWORD, roles: roles });
125+
print("created user " + process.env.APP_USER);
126+
}
127+
'
128+
env:
129+
- name: MONGO_HOST
130+
value: {{ $host | quote }}
131+
- name: APP_DB
132+
value: {{ $service.database | quote }}
133+
- name: ROOT_USER
134+
valueFrom:
135+
secretKeyRef:
136+
name: {{ include "mongodb.rootSecretName" $ }}
137+
key: mongodb-root-username
138+
- name: ROOT_PASSWORD
139+
valueFrom:
140+
secretKeyRef:
141+
name: {{ include "mongodb.rootSecretName" $ }}
142+
key: mongodb-root-password
143+
- name: APP_USER
144+
valueFrom:
145+
secretKeyRef:
146+
name: {{ $secretName }}
147+
key: MONGO_USER
148+
- name: APP_PASSWORD
149+
valueFrom:
150+
secretKeyRef:
151+
name: {{ $secretName }}
152+
key: MONGO_PASSWORD
153+
resources:
154+
requests:
155+
cpu: "50m"
156+
memory: "128Mi"
157+
limits:
158+
cpu: "500m"
159+
# mongosh is a Node process and needs headroom over its idle size.
160+
memory: "512Mi"
161+
---
162+
{{- end }}

0 commit comments

Comments
 (0)