Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion charts/mongodb/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ line behavior, and official image lifecycle.

Standalone and replica set modes use StatefulSets so pod identity and storage are
stable. Replica set mode adds a Helm hook Job that initializes the replica set
after members are reachable. Sharded mode deploys config servers and shard
after members are reachable. When enabled, the arbiter runs in a separate
single-replica StatefulSet with stable DNS and ephemeral local storage. The
post-install/post-upgrade hook registers it as `arbiterOnly` and reconciles its
presence without routing client traffic to it. Sharded mode deploys config servers and shard
members as StatefulSets, mongos as a Deployment, and a hook Job that initializes
the cluster and registers shards.

Expand All @@ -44,6 +47,11 @@ validated with client connection strings and election behavior. Sharded clusters
require shard key design, balancer monitoring, and capacity planning outside the
chart.

The chart never removes data-bearing replica set members automatically. An
operator must use MongoDB's member-removal procedure before lowering
`replicaSet.members`. Arbiter enablement is narrower: the hook safely reconciles
the single known arbiter hostname on install and upgrade.

The backup CronJob performs logical `mongodump` archives to S3-compatible object
storage. It is suitable for scheduled full backups and restore drills, but it is
not a point-in-time recovery system or a substitute for storage snapshots where
Expand Down
36 changes: 36 additions & 0 deletions charts/mongodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Read before choosing an architecture:

- **Official `mongo` image** — no vendor lock-in, standard paths (`/data/db`)
- **Auto keyFile generation** — replica set internal auth handled automatically
- **Optional arbiter** — one lightweight voting member for cost-constrained replica sets
- **Helm hook Jobs** — idempotent `rs.initiate()` and shard registration
- **Prometheus exporter** — optional `percona/mongodb_exporter` sidecar + ServiceMonitor
- **Built-in S3 backups** — scheduled `mongodump` archive upload for standalone, replica set, and sharded topologies
Expand Down Expand Up @@ -121,6 +122,22 @@ compatible with `--configsvr` or `--shardsvr`. Production sharded authentication
requires an operator-managed bootstrap flow outside this chart's automatic root
user initialization path.

### Replica Set with Arbiter

```yaml
architecture: replicaset
replicaSet:
members: 2
arbiter:
enabled: true
```

This topology creates two persistent data-bearing members and one arbiter with
ephemeral local storage. MongoDB recommends three data-bearing members when
capacity permits. Use an arbiter only when the cost of a third data copy is the
deciding constraint, and review the
[official arbiter considerations](https://www.mongodb.com/docs/manual/core/replica-set-arbiter/).

## Parameters

### Global
Expand Down Expand Up @@ -150,6 +167,8 @@ user initialization path.
|-----------|-------------|---------|
| `replicaSet.name` | Replica set name | `rs0` |
| `replicaSet.members` | Number of data-bearing members | `3` |
| `arbiter.enabled` | Add one non-data-bearing voting member | `false` |
| `arbiter.resources` | Arbiter container resources | `{}` |

### Sharded Cluster Parameters

Expand Down Expand Up @@ -241,6 +260,7 @@ user initialization path.
|-------------|-----------|
| standalone | Secret, 2x Service (headless + client), StatefulSet |
| replicaset | 2x Secret (auth + keyfile), 2x Service, StatefulSet, Job (rs-init) |
| replicaset + arbiter | 2x Secret, 3x Service, 2x StatefulSet, Job (rs-init) |
| sharded | 2x Secret, 4x Service, 3x StatefulSet (config + shards), Deployment (mongos), Job (init) |

## Examples
Expand All @@ -249,6 +269,7 @@ See the [`examples/`](examples/) directory:

- [`standalone-simple.yaml`](examples/standalone-simple.yaml) — Minimal standalone instance
- [`replicaset-production.yaml`](examples/replicaset-production.yaml) — Production RS with monitoring, init scripts, and anti-affinity
- [`replicaset-arbiter.yaml`](examples/replicaset-arbiter.yaml) — Two data members with one voting arbiter
- [`sharded-cluster.yaml`](examples/sharded-cluster.yaml) — Full sharded cluster

## Architecture Guides
Expand All @@ -270,6 +291,21 @@ Keep replica set keyFiles and root credentials stable across `helm upgrade`;
those values are initialized by MongoDB and should be rotated with MongoDB
administrative commands instead of changing chart values.

Enabling or disabling `arbiter.enabled` on an existing replica set reconciles
the arbiter membership through the post-upgrade hook. Increasing
`replicaSet.members` adds missing data-bearing members. Reducing that value does
not remove members from the MongoDB configuration; remove members safely with
MongoDB administrative procedures before scaling down the StatefulSet.

MongoDB requires a global default write concern before a reconfiguration that
would change its implicit value. When arbiter reconciliation encounters an
implicit default, the hook preserves the currently effective value (`w: 1` or
`majority`) by promoting it to a global default. Existing operator-managed
global defaults are not changed. The same preservation runs before adding a
missing data-bearing member because scale-up can also change the implicit
default. MongoDB 5.0 and newer do not allow a global default write concern to be
unset after it is established.

## Security Scan

Security Scan: Kubescape local scan against `MITRE,NSA,SOC2` reports a 72.73% resource summary score.
Expand Down
32 changes: 32 additions & 0 deletions charts/mongodb/ci/arbiter-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-License-Identifier: Apache-2.0
architecture: replicaset

auth:
enabled: true
rootPassword: arbiter-ci-password
replicaSetKey: 0123456789abcdef0123456789abcdef

replicaSet:
name: rs0
members: 2

arbiter:
enabled: true
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 100m
memory: 512Mi

persistence:
enabled: false

resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 1Gi
41 changes: 41 additions & 0 deletions charts/mongodb/docs/replicaset.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Common cases:

- multiple data-bearing members
- automatic `rs.initiate()` bootstrap via Helm hook job
- an optional non-data-bearing arbiter for an odd number of election votes
- internal member authentication with key file
- a standard MongoDB replica set topology
- optional metrics and `ServiceMonitor`
Expand Down Expand Up @@ -42,6 +43,7 @@ secondaries replicating from it, and elections on failure.
## Common risks

- running only 2 members and expecting safe elections
- treating an arbiter as a replacement for data redundancy
- forgetting the replica set connection string in clients
- scheduling all members in the same node or zone
- ignoring backup and restore testing because failover exists
Expand All @@ -62,6 +64,8 @@ secondaries replicating from it, and elections on failure.
| `architecture` | Must be `replicaset` |
| `replicaSet.name` | Replica set name used by members and clients |
| `replicaSet.members` | Number of data-bearing members |
| `arbiter.enabled` | Add one non-data-bearing voting member |
| `arbiter.resources` | Resource settings for the arbiter container |
| `auth.replicaSetKey` | Internal auth key when not using existing secret |
| `auth.existingKeySecret` | Existing secret for key file |
| `persistence.*` | Storage settings for the members |
Expand Down Expand Up @@ -93,6 +97,43 @@ metrics:
enabled: true
```

## Arbiter topology

MongoDB recommends three data-bearing members. When capacity constraints make a
third data copy impractical, the chart can deploy two data-bearing members and
one arbiter:

```yaml
architecture: replicaset

replicaSet:
members: 2

arbiter:
enabled: true
```

The arbiter uses the official MongoDB image, the same internal keyFile, stable
StatefulSet DNS, and ephemeral local storage. It votes in elections but does not
store the application data set and cannot become primary. The chart permits one
arbiter with an even number of 2 to 6 data-bearing members. Review MongoDB's
[arbiter limitations](https://www.mongodb.com/docs/manual/core/replica-set-arbiter/),
especially majority write behavior and reduced fault tolerance.

The post-upgrade hook adds or removes the arbiter when `arbiter.enabled`
changes. It can add missing data-bearing members, but deliberately does not
remove them. Follow MongoDB's member-removal procedure before reducing
`replicaSet.members`.

Adding or removing an arbiter can change MongoDB's implicit default write
concern. Before that reconfiguration, the hook preserves the currently effective
implicit value by promoting it to a global default. If the operator already set
a global default, the hook leaves it unchanged. This preservation also runs
before `rs.add()` when scale-up adds a missing data-bearing member. MongoDB 5.0
and newer do not allow the global default write concern to be unset afterward. Review
[`setDefaultRWConcern`](https://www.mongodb.com/docs/manual/reference/command/setDefaultRWConcern/)
before changing `arbiter.enabled` on an existing deployment.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## When to move to another architecture

- move back to `standalone` only for non-critical simplified environments
Expand Down
28 changes: 28 additions & 0 deletions charts/mongodb/examples/replicaset-arbiter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SPDX-License-Identifier: Apache-2.0
# Two data-bearing members plus one non-data-bearing voting arbiter.
# Prefer three data-bearing members when capacity permits.

architecture: replicaset

auth:
enabled: true
existingSecret: mongodb-root-credentials
existingKeySecret: mongodb-replica-key

replicaSet:
name: rs0
members: 2

arbiter:
enabled: true
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 512Mi

persistence:
enabled: true
size: 50Gi
8 changes: 7 additions & 1 deletion charts/mongodb/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Persistence: {{ .Values.persistence.enabled }}
Metrics: {{ .Values.metrics.enabled }}
Backup: {{ .Values.backup.enabled }}
Arbiter: {{ .Values.arbiter.enabled }}
External Secrets: {{ .Values.externalSecrets.enabled }}

{{- if include "mongodb.isStandalone" . }}
Expand All @@ -25,7 +26,7 @@

2. Connect
----------
Replica Set: {{ .Values.replicaSet.name }} ({{ include "mongodb.replicaCount" . }} members)
Replica Set: {{ .Values.replicaSet.name }} ({{ include "mongodb.replicaCount" . }} data-bearing members{{ if .Values.arbiter.enabled }} + 1 arbiter{{ end }})

Connection string:
mongodb://{{ if .Values.auth.enabled }}{{ .Values.auth.rootUser }}:$(kubectl get secret -n {{ $namespace }} {{ include "mongodb.secretName" . }} -o jsonpath='{.data.mongodb-root-password}' | base64 -d)@{{ end }}{{ range $i := until (int (include "mongodb.replicaCount" .)) }}{{ if $i }},{{ end }}{{ $fullname }}-{{ $i }}.{{ $fullname }}-headless.{{ $namespace }}.svc.cluster.local:{{ $.Values.port }}{{ end }}/admin?replicaSet={{ .Values.replicaSet.name }}
Expand Down Expand Up @@ -71,6 +72,11 @@

Check logs:
kubectl logs -n {{ $namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} --tail=200
{{- if and (include "mongodb.isReplicaSet" .) .Values.arbiter.enabled }}

Verify arbiter membership:
kubectl exec -n {{ $namespace }} {{ $fullname }}-0 -- mongosh --quiet{{ if .Values.auth.enabled }} --username {{ .Values.auth.rootUser }} --password "$(kubectl get secret -n {{ $namespace }} {{ include "mongodb.secretName" . }} -o jsonpath='{.data.mongodb-root-password}' | base64 -d)" --authenticationDatabase admin{{ end }} --eval 'rs.conf().members.map(({host, arbiterOnly}) => ({host, arbiterOnly}))'
{{- end }}

5. Metrics
----------
Expand Down
20 changes: 20 additions & 0 deletions charts/mongodb/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Validate cross-field settings that JSON Schema cannot express.
{{- if and .Values.persistence.existingClaim (ne .Values.architecture "standalone") -}}
{{- fail "persistence.existingClaim is supported only when architecture is standalone" -}}
{{- end -}}
{{- if and .Values.arbiter.enabled (ne .Values.architecture "replicaset") -}}
{{- fail "arbiter.enabled is supported only when architecture is replicaset" -}}
{{- end -}}
{{- if and .Values.arbiter.enabled (or (lt (int .Values.replicaSet.members) 2) (gt (int .Values.replicaSet.members) 6) (ne (mod (int .Values.replicaSet.members) 2) 0)) -}}
{{- fail "arbiter.enabled requires an even replicaSet.members value between 2 and 6" -}}
{{- end -}}
{{- end -}}

{{/*
Expand Down Expand Up @@ -78,6 +84,20 @@ Headless service name (for StatefulSet DNS).
{{- printf "%s-headless" (include "mongodb.fullname" .) -}}
{{- end -}}

{{/*
Arbiter StatefulSet name.
*/}}
{{- define "mongodb.arbiterName" -}}
{{- printf "%s-arbiter" (include "mongodb.fullname" . | trunc 46 | trimSuffix "-") -}}
{{- end -}}

{{/*
Arbiter headless service name.
*/}}
{{- define "mongodb.arbiterHeadlessServiceName" -}}
{{- printf "%s-headless" (include "mongodb.arbiterName" .) -}}
{{- end -}}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
{{/*
Auth secret name.
*/}}
Expand Down
21 changes: 21 additions & 0 deletions charts/mongodb/templates/arbiter-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{/* SPDX-License-Identifier: Apache-2.0 */}}
{{- if and (include "mongodb.isReplicaSet" .) .Values.arbiter.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "mongodb.arbiterHeadlessServiceName" . }}
labels:
{{- include "mongodb.labels" . | nindent 4 }}
app.kubernetes.io/component: arbiter
spec:
type: ClusterIP
clusterIP: None
publishNotReadyAddresses: true
ports:
- name: mongodb
port: {{ .Values.service.port }}
targetPort: mongodb
selector:
{{- include "mongodb.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: arbiter
{{- end }}
Loading