Skip to content

feat(valkey): add an optional HAProxy front-end for Sentinel - #235

Open
pat-s wants to merge 6 commits into
valkey-io:mainfrom
pat-s:feat/sentinel-haproxy
Open

feat(valkey): add an optional HAProxy front-end for Sentinel#235
pat-s wants to merge 6 commits into
valkey-io:mainfrom
pat-s:feat/sentinel-haproxy

Conversation

@pat-s

@pat-s pat-s commented Aug 1, 2026

Copy link
Copy Markdown

Stacked on #234, review that one first. Only the last commit belongs to this PR.

Sentinel moves the master between pods and clients have to follow it. Clients whose library cannot talk to Sentinel have no way to do that, so this adds an optional HAProxy deployment (haproxy.enabled) that does it for them:

  • valkey-haproxy:6379 always forwards to the current master
  • valkey-haproxy:6380 load balances reads across all healthy nodes

HAProxy finds the master itself, by health checking every node with INFO replication and only routing to the one answering role:master. That replaces the sentinel-watcher sidecar from #137, and with it the apk add socat at startup that broke air-gapped installs, readOnlyRootFilesystem and MS Defender. No sidecar, no runtime installs, no admin socket: one container running the stock image with a read-only root filesystem as non-root.

Also addressed from the #137 review: timeout tunnel plus TCP keepalives so pub/sub connections are not dropped (@lazariv), and the health check password comes from the secret via an env var rather than being written into the ConfigMap.

Testing

helm lint, 173 unit tests, rendering against k8s 1.28/1.35/1.36, and haproxy -c against the rendered config with and without TLS. Verified on a kind cluster with ACL auth:

  • write port reports role:master, read port round-robins across all 3 nodes
  • frozen master → one connection error, then writes resume on the promoted node within ~7s

Two bugs found and fixed during that testing, both of which silently sent writes to a replica:

  • HAProxy starts servers UP, so a starting pod forwarded writes to a replica before its first check ran. Fixed with init-state down.
  • A replica's check only failed on timeout, because HAProxy kept waiting for role:master to appear. Sending QUIT before the expect makes the node close the connection, so it fails immediately.

A short -READONLY window remains possible while a recovered old master is being demoted by Sentinel. That is inherent to Sentinel and documented in the README.

Replication mode alone cannot recover from a master failure: the master is
always pod-0 and clients keep writing to it until an operator intervenes.

Run a valkey-sentinel container next to the Valkey server in every pod, so
that the Sentinels monitor each other and promote a replica automatically
when the master stops responding. Enabled with replica.sentinel.enabled.

* Stop pinning the valkey service selector to pod-0 when Sentinel is enabled,
  the master can be any pod after a failover.
* Preserve the replication target Sentinel wrote via CONFIG REWRITE across
  restarts, instead of resetting every pod to "pod-0 is master".
* Rediscover the current master on Sentinel startup by asking the running
  peers first and the local Valkey node second, so that a restarted Sentinel
  never tries to demote the real master.
* Ask Sentinel to fail over in a preStop hook when the terminating pod is the
  master, so a rolling update does not leave clients on a dying master.
* Secure the Sentinel port and the Sentinel gossip with the default user
  credentials whenever auth is enabled.
* Drop a stray tab in the statefulset volumeClaimTemplates that made the
  rendered manifests unparseable for strict YAML parsers.

Co-authored-by: KHTee <teekahhui@hotmail.com>
Co-authored-by: Jose Dominguez <jdominguez.ops@gmail.com>
Co-authored-by: yoannrt <yoann.rousseau@gmail.com>
Co-authored-by: Dieter Maes <dieter.maes@dmaes.be>
Co-authored-by: Tim Karger <49390121+tkarger@users.noreply.github.com>
Co-authored-by: lazariv <lazariv.taras@gmail.com>
Signed-off-by: pat-s <patrick.schratz@devxy.io>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pat-s, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 156d6351-4b64-4cf8-aba3-3cef15824e92

📥 Commits

Reviewing files that changed from the base of the PR and between 629bd4a and 78304d1.

📒 Files selected for processing (15)
  • valkey/README.md
  • valkey/examples/ha-sentinel.yaml
  • valkey/templates/NOTES.txt
  • valkey/templates/_helpers.tpl
  • valkey/templates/haproxy-configmap.yaml
  • valkey/templates/haproxy-deployment.yaml
  • valkey/templates/haproxy-service.yaml
  • valkey/templates/init_config.yaml
  • valkey/templates/sentinel-configmap.yaml
  • valkey/templates/sentinel-service.yaml
  • valkey/templates/statefulset.yaml
  • valkey/tests/haproxy_test.yaml
  • valkey/tests/sentinel_test.yaml
  • valkey/values.schema.json
  • valkey/values.yaml
📝 Walkthrough

Walkthrough

The Helm chart adds Valkey Sentinel high availability, failover handling, Sentinel services, optional HAProxy read/write routing, configuration validation, tests, examples, documentation, and chart version 0.12.0.

Changes

Sentinel high availability and HAProxy routing

Layer / File(s) Summary
Configuration contracts and validation
valkey/values.yaml, valkey/values.schema.json, valkey/templates/_helpers.tpl, valkey/templates/deploy_valkey.yaml, valkey/templates/statefulset.yaml
Sentinel and HAProxy values, schemas, helper functions, and render-time validation were added.
Sentinel bootstrap and failover runtime
valkey/templates/init_config.yaml, valkey/templates/sentinel-configmap.yaml, valkey/templates/statefulset.yaml
Sentinel sidecars, topology-aware initialization, persistence, health checks, configuration generation, and optional pre-stop failover were added.
Sentinel services and validation
valkey/templates/sentinel-service.yaml, valkey/templates/service-headless.yaml, valkey/templates/service.yaml, valkey/tests/sentinel_test.yaml
Sentinel ports and services were added. Tests cover validation, storage, failover, service selection, TLS, authentication, restart handling, and disabled rendering.
HAProxy routing and validation
valkey/templates/haproxy-configmap.yaml, valkey/templates/haproxy-deployment.yaml, valkey/templates/haproxy-service.yaml, valkey/tests/haproxy_test.yaml
HAProxy now routes writes to the master and reads to replicas. Deployment, service, authentication, TLS, health checks, security settings, and disabled rendering are tested.
Examples, documentation, and chart release
valkey/README.md, valkey/examples/ha-sentinel.yaml, valkey/templates/NOTES.txt, valkey/Chart.yaml, valkey/.helmignore
Sentinel and HAProxy usage, values, failover behavior, an example configuration, chart version 0.12.0, and Helm ignore rules were updated.

Sequence Diagram(s)

sequenceDiagram
  participant StatefulSet
  participant SentinelSidecar
  participant ValkeyPods
  StatefulSet->>SentinelSidecar: Start Sentinel sidecar
  SentinelSidecar->>ValkeyPods: Discover master and generate configuration
  ValkeyPods->>SentinelSidecar: Return Sentinel health
  SentinelSidecar->>ValkeyPods: Request failover before shutdown
Loading
sequenceDiagram
  participant Client
  participant HAProxyService
  participant HAProxy
  participant ValkeySentinel
  participant ValkeyPods
  Client->>HAProxyService: Send read or write request
  HAProxyService->>HAProxy: Forward request
  HAProxy->>ValkeySentinel: Check current master
  ValkeySentinel->>HAProxy: Return master health
  HAProxy->>ValkeyPods: Route writes to master or reads to replicas
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding an optional HAProxy front-end for Valkey Sentinel.
Description check ✅ Passed The description directly explains the HAProxy and Sentinel changes, behavior, security details, and validation performed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
valkey/templates/NOTES.txt (1)

94-104: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not advertise the load-balanced valkey Service as a direct write endpoint in Sentinel mode.

The valkey Service selects all pods, so the SET commands in these ranges can reach a replica and return -READONLY. If HAProxy is enabled, use ...-haproxy and its write port. Otherwise, show Sentinel master discovery and do not provide a direct valkey-cli SET command.

Also applies to: 142-146

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/templates/NOTES.txt` around lines 94 - 104, Update the Sentinel-mode
write instructions in the NOTES template so they never use the load-balanced
valkey Service for SET operations. When HAProxy is enabled, reference the
valkey-haproxy Service and its write port; otherwise document Sentinel master
discovery without providing a direct valkey-cli SET command, while preserving
the existing non-Sentinel instructions.
🧹 Nitpick comments (9)
valkey/templates/sentinel-configmap.yaml (1)

187-195: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Restrict the permissions on sentinel.conf.

The block appends the plaintext default password and the monitor password to $SENTINEL_CONF. The file keeps the default creation mode from the container umask, commonly world-readable. init_config.yaml sets chmod 0400 on /etc/valkey/users.acl for the same class of data. Apply the same posture here. Sentinel rewrites this file at runtime, so use 0600 rather than 0400.

🔒️ Proposed change
     rm -f "$SENTINEL_CONF"
+    umask 077
     cat > "$SENTINEL_CONF" << EOF
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/templates/sentinel-configmap.yaml` around lines 187 - 195, Update the
configuration-generation flow around SENTINEL_CONF to ensure the file is created
or chmodded with mode 0600 after appending the plaintext credentials. Preserve
Sentinel’s ability to rewrite the file and apply the permission consistently to
the generated sentinel.conf.
valkey/templates/statefulset.yaml (1)

46-59: 🗄️ Data Integrity & Integration | 🔵 Trivial

Document that enabling Sentinel persistence needs StatefulSet recreation.

Kubernetes rejects updates to volumeClaimTemplates on an existing StatefulSet. A user who enables replica.sentinel.persistence.enabled on a running release gets a failed upgrade until the StatefulSet is deleted with --cascade=orphan. Add this note to the README or to the values comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/templates/statefulset.yaml` around lines 46 - 59, Document near
replica.sentinel.persistence.enabled, preferably in the values comment or
README, that enabling Sentinel persistence requires recreating the existing
StatefulSet because volumeClaimTemplates are immutable. Instruct users to delete
the StatefulSet with --cascade=orphan before upgrading, preserving the existing
release and pods.
valkey/tests/sentinel_test.yaml (1)

285-293: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for replica.sentinel.service.enabled: false.

valkey/templates/sentinel-service.yaml line 1 gates rendering on three values. The suite covers replica.enabled and replica.sentinel.enabled, but never sets replica.sentinel.service.enabled to false. That branch stays untested.

♻️ Proposed additional test
   - it: should not render any sentinel resource when sentinel is disabled
     set:
       replica.sentinel.enabled: false
     templates:
       - templates/sentinel-configmap.yaml
       - templates/sentinel-service.yaml
     asserts:
       - hasDocuments:
           count: 0
+
+  - it: should not render the sentinel service when the service is disabled
+    set:
+      replica.sentinel.service.enabled: false
+    template: templates/sentinel-service.yaml
+    asserts:
+      - hasDocuments:
+          count: 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/tests/sentinel_test.yaml` around lines 285 - 293, Add a test case in
sentinel_test.yaml covering replica.sentinel.service.enabled: false, while
keeping the replica and sentinel settings enabled so the service-specific gate
is exercised. Render templates/sentinel-service.yaml and assert that zero
documents are produced.
valkey/templates/haproxy-deployment.yaml (4)

38-39: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Disable the service account token mount for HAProxy.

HAProxy makes no Kubernetes API calls. It resolves backends through DNS, per the resolvers kubernetes section in valkey/templates/haproxy-configmap.yaml. The Deployment inherits serviceAccount.automount, so a chart-level true projects an API token into a network-facing proxy for no reason.

Hardcode automountServiceAccountToken: false, or add a dedicated haproxy.automountServiceAccountToken value that defaults to false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/templates/haproxy-deployment.yaml` around lines 38 - 39, Update the
HAProxy Deployment’s automountServiceAccountToken setting to always be false, or
use a dedicated haproxy.automountServiceAccountToken value defaulting to false
instead of inheriting serviceAccount.automount; leave serviceAccountName
unchanged.

115-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the scheduling fallbacks consistent.

nodeSelector at Line 115 and tolerations at Line 127 fall back to the chart-level values. affinity at Line 119 and topologySpreadConstraints at Line 123 do not. A user who sets a chart-level affinity gets it on the Valkey pods but not on the HAProxy pods.

♻️ Proposed change
-      {{- with .Values.haproxy.affinity }}
+      {{- with .Values.haproxy.affinity | default .Values.affinity }}
       affinity:
         {{- toYaml . | nindent 8 }}
       {{- end }}
-      {{- with .Values.haproxy.topologySpreadConstraints }}
+      {{- with .Values.haproxy.topologySpreadConstraints | default .Values.topologySpreadConstraints }}
       topologySpreadConstraints:
         {{- toYaml . | nindent 8 }}
       {{- end }}

Confirm that affinity and topologySpreadConstraints exist at the chart level before you apply this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/templates/haproxy-deployment.yaml` around lines 115 - 130, Update the
HAProxy deployment template’s affinity and topologySpreadConstraints blocks to
fall back to the corresponding chart-level values, matching nodeSelector and
tolerations; first verify those chart-level values exist, while preserving
HAProxy-specific values as the preferred configuration.

79-87: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Set explicit thresholds on the three probes.

All three probes use the Kubernetes defaults: periodSeconds: 10, failureThreshold: 3, initialDelaySeconds: 0. Two consequences follow.

The startupProbe allows only 30 seconds before the kubelet kills the container. The readinessProbe needs up to 10 seconds to add a new HAProxy replica to the Service, which extends the failover window that the README describes.

A TCP probe on valkey-write also succeeds as soon as HAProxy binds the port, even while every backend server is still down because of init-state down. Readiness therefore does not reflect the ability to serve writes.

♻️ Proposed change
           startupProbe:
             tcpSocket:
               port: valkey-write
+            periodSeconds: 2
+            failureThreshold: 15
           livenessProbe:
             tcpSocket:
               port: valkey-write
+            periodSeconds: 10
+            failureThreshold: 3
           readinessProbe:
             tcpSocket:
               port: valkey-write
+            periodSeconds: 2
+            failureThreshold: 3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/templates/haproxy-deployment.yaml` around lines 79 - 87, Update the
startupProbe, livenessProbe, and readinessProbe definitions in the deployment
template with explicit periodSeconds, failureThreshold, and initialDelaySeconds
values that meet the documented startup and failover timing. Also make readiness
verify backend write availability rather than only HAProxy’s listening socket,
while preserving the existing probe structure and port target where applicable.

14-19: 🧹 Nitpick | 🔵 Trivial

Consider a PodDisruptionBudget and default anti-affinity for HAProxy.

HAProxy becomes the write path for the whole cluster. With haproxy.replicas: 1, a node drain removes the write endpoint until the pod reschedules. With more replicas and no anti-affinity, the scheduler can place them all on one node.

Add a PodDisruptionBudget template gated on haproxy.enabled, and ship a default podAntiAffinity with preferredDuringSchedulingIgnoredDuringExecution on the HAProxy component label.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/templates/haproxy-deployment.yaml` around lines 14 - 19, Add a
PodDisruptionBudget template gated by haproxy.enabled and configure its selector
to target the HAProxy component labels, preserving the chart’s replica and
availability settings. In the HAProxy Deployment template, add default
preferredDuringSchedulingIgnoredDuringExecution pod anti-affinity matching
app.kubernetes.io/component=haproxy, while allowing user-provided affinity
configuration to override or extend the default.
valkey/tests/haproxy_test.yaml (2)

33-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the read backend and for the config checksum.

The suite asserts the valkey_master check sequence in detail, but never asserts the valkey_replicas sequence. valkey/templates/haproxy-configmap.yaml Lines 83-88 define PING, +PONG, and fall 2. A regression that swaps the two backends, or that copies role:master into the read backend, passes the current suite.

valkey/templates/haproxy-deployment.yaml Line 35 sets checksum/config, and no test asserts it. valkey/tests/sentinel_test.yaml Lines 254-258 assert the equivalent Sentinel annotation, so the coverage is inconsistent.

♻️ Proposed additional tests
+  - it: should ping the read backend and tolerate one failed check
+    template: templates/haproxy-configmap.yaml
+    asserts:
+      - matchRegex:
+          path: data["haproxy.cfg"]
+          pattern: 'backend valkey_replicas'
+      - matchRegex:
+          path: data["haproxy.cfg"]
+          pattern: 'tcp-check send PING\\r\\n\n  tcp-check send QUIT\\r\\n\n  tcp-check expect string \+PONG'
+      - matchRegex:
+          path: data["haproxy.cfg"]
+          pattern: 'fall 2 rise 1 init-addr last,libc,none init-state down'
+
+  - it: should roll the haproxy pods when the config changes
+    template: templates/haproxy-deployment.yaml
+    asserts:
+      - exists:
+          path: spec.template.metadata.annotations["checksum/config"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/tests/haproxy_test.yaml` around lines 33 - 45, Add assertions to the
HAProxy test covering the valkey_replicas backend’s PING/+PONG health-check
sequence and fall 2 setting, ensuring it remains distinct from the valkey_master
role check. Also assert the deployment’s checksum/config annotation, following
the equivalent assertion pattern in the Sentinel tests and using the rendered
ConfigMap checksum.

53-65: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Confirm the intent of timeout tunnel 0s.

This test locks in 0s, which disables the tunnel timeout. Pub/sub connections then survive, as the comment in valkey/templates/haproxy-configmap.yaml Lines 36-37 explains. A half-open connection that neither peer closes also survives forever, and it consumes a slot against maxconn.

option clitcpka and option srvtcpka mitigate this, but only when the kernel keepalive defaults are shorter than the leak window. Consider a large finite default instead, for example 24h, and document the pub/sub tradeoff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/tests/haproxy_test.yaml` around lines 53 - 65, Replace the HAProxy
tunnel timeout assertion in the “should keep pub/sub connections open” test with
a large finite timeout such as 24h, update the corresponding timeout
configuration and its comment in the haproxy ConfigMap template, and document
the pub/sub persistence versus half-open connection tradeoff while preserving
the client and server TCP keepalive options.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@valkey/examples/ha-sentinel.yaml`:
- Around line 23-36: Update the auth configuration in the example to require
operator-supplied secrets instead of shipping usable default and replicator
passwords, while preserving the required ACL permissions. Add a prominent
warning near the credential settings instructing operators to replace both
values before installation and not use the example credentials in deployed
environments.

In `@valkey/README.md`:
- Around line 76-90: Update valkey/README.md lines 76-90 to state that the
valkey-sentinel Service and its endpoint are available only when
replica.sentinel.service.enabled is true, or document the per-pod Sentinel
endpoints when disabled. Update valkey/templates/NOTES.txt lines 27-32 to
conditionally print Service-based connection commands when
replica.sentinel.service.enabled is true and provide the appropriate per-pod
Sentinel alternative otherwise.
- Around line 109-113: Update the README failover documentation around the
“Failover behaviour” section and related haproxy.config.checkInterval
descriptions to distinguish Sentinel failure detection and replica promotion
from HAProxy health-check convergence and routing. Replace the unqualified
“within a few seconds” claim with wording that reflects Sentinel’s promotion
timing, then describe checkInterval only as bounding HAProxy’s post-promotion
detection, not initiating promotion or defining end-to-end failover.
- Around line 104-107: Add the `text` language identifier to the fenced code
block containing the ACL permission list in the README, changing the opening
fence to a language-tagged fence while preserving its contents.

In `@valkey/templates/_helpers.tpl`:
- Around line 243-244: Update the HAProxy configuration template around the
requireClientCertificate condition so the crt directive explicitly references a
combined certificate/private-key bundle, or uses a chart value that guarantees
both files share the same filename. Do not rely on HAProxy’s automatic
private-key lookup based on serverPublicKey.

In `@valkey/templates/haproxy-configmap.yaml`:
- Around line 52-74: Add the HAProxy `balance first` directive within the
`backend valkey_master` configuration before the server definitions, so traffic
uses the first healthy master instead of round-robin distribution when multiple
nodes pass the `role:master` check. Preserve the existing health-check and
server configuration.

In `@valkey/templates/haproxy-deployment.yaml`:
- Around line 106-111: Update the TLS Secret volume in the Helm template’s
tls.enabled block to set defaultMode to 0440 instead of 0400, allowing the
non-root HAProxy container to read required Secret files such as ca.crt.

In `@valkey/templates/init_config.yaml`:
- Around line 188-196: Update the bootstrap marker handling in the init logic to
store the observed role value, either “master” or “replica”, rather than using
mere file presence. Change the `$BOOTSTRAP_MARKER` branch to read and honor that
value, preserving MASTER only when the marker contains “master”; replicas must
not be promoted when no on-disk `replicaof` exists. Also update the marker
creation near the existing init write to record the current role for every pod.
- Around line 81-84: Update the persistence handling around SAVED_REPLICAOF to
normalize any restored replicaof/slaveof target that contains a primary pod IP
to the configured stable master FQDN before reuse. Preserve the existing command
and protocol while ensuring restored replication always targets the master’s
stable DNS name rather than an ephemeral address.

In `@valkey/templates/sentinel-configmap.yaml`:
- Around line 66-81: Replace argument-based Valkey authentication with
REDISCLI_AUTH for every affected valkey-cli call: in
valkey/templates/sentinel-configmap.yaml#L66-L81, update sentinel_cli and
valkey_cli; in `#L232-L236`, export REDISCLI_AUTH="$PASSWORD" in sentinel-ping.sh;
and in `#L280-L297`, set REDISCLI_AUTH for both local_role and sentinel failover
in sentinel-prestop.sh. Remove each corresponding -a password argument while
preserving the existing authentication conditions and commands.
- Around line 36-40: The Sentinel configuration template currently assumes both
the monitor and default ACL users exist when auth is enabled. Update
valkey.validateSentinelConfig or the sentinel script-rendering guard to require
auth.aclUsers.default in addition to the monitor user, preventing Sentinel
scripts from rendering when either dependency is missing.
- Around line 141-146: Update the discovery branch in the Sentinel configuration
setup to validate both extracted fields before assigning MASTER_HOST and
MASTER_PORT. If either discovered value is empty, retain the existing defaults
and use the bootstrap path; only apply discovered values when both are present,
ensuring the monitor directive always receives a valid host and port.

In `@valkey/templates/service.yaml`:
- Around line 34-38: Update the primary Service selector in the service template
so Sentinel mode cannot expose all pods without master-only routing: require
haproxy.enabled whenever replica.sentinel.enabled is true, or implement and
select a label tracking the current master. Preserve the existing
replica-enabled, non-Sentinel pod-name selector behavior and ensure direct
clients using the primary Service cannot send writes to replicas.

In `@valkey/templates/statefulset.yaml`:
- Around line 168-173: Update the StatefulSet and sentinel configuration
validation so terminationGracePeriodSeconds is configurable with a default of 30
seconds, and ensure it is applied to the pod spec. In
valkey.validateSentinelConfig, when replica.sentinel.preStopFailover is enabled,
fail rendering if preStopFailoverTimeoutSeconds is greater than or equal to the
effective grace period, using the existing values and validation conventions.
- Around line 232-236: Update the sentinel peer-discovery command used by
master_from_peers() to pass a bounded -t connection timeout to sentinel_cli when
querying each peer’s master address. In the StatefulSet startupProbe, increase
failureThreshold so the startup budget covers the full local Valkey wait plus
bounded peer discovery, while preserving the existing periodSeconds and
startupTimeoutSeconds-based configuration.

---

Outside diff comments:
In `@valkey/templates/NOTES.txt`:
- Around line 94-104: Update the Sentinel-mode write instructions in the NOTES
template so they never use the load-balanced valkey Service for SET operations.
When HAProxy is enabled, reference the valkey-haproxy Service and its write
port; otherwise document Sentinel master discovery without providing a direct
valkey-cli SET command, while preserving the existing non-Sentinel instructions.

---

Nitpick comments:
In `@valkey/templates/haproxy-deployment.yaml`:
- Around line 38-39: Update the HAProxy Deployment’s
automountServiceAccountToken setting to always be false, or use a dedicated
haproxy.automountServiceAccountToken value defaulting to false instead of
inheriting serviceAccount.automount; leave serviceAccountName unchanged.
- Around line 115-130: Update the HAProxy deployment template’s affinity and
topologySpreadConstraints blocks to fall back to the corresponding chart-level
values, matching nodeSelector and tolerations; first verify those chart-level
values exist, while preserving HAProxy-specific values as the preferred
configuration.
- Around line 79-87: Update the startupProbe, livenessProbe, and readinessProbe
definitions in the deployment template with explicit periodSeconds,
failureThreshold, and initialDelaySeconds values that meet the documented
startup and failover timing. Also make readiness verify backend write
availability rather than only HAProxy’s listening socket, while preserving the
existing probe structure and port target where applicable.
- Around line 14-19: Add a PodDisruptionBudget template gated by haproxy.enabled
and configure its selector to target the HAProxy component labels, preserving
the chart’s replica and availability settings. In the HAProxy Deployment
template, add default preferredDuringSchedulingIgnoredDuringExecution pod
anti-affinity matching app.kubernetes.io/component=haproxy, while allowing
user-provided affinity configuration to override or extend the default.

In `@valkey/templates/sentinel-configmap.yaml`:
- Around line 187-195: Update the configuration-generation flow around
SENTINEL_CONF to ensure the file is created or chmodded with mode 0600 after
appending the plaintext credentials. Preserve Sentinel’s ability to rewrite the
file and apply the permission consistently to the generated sentinel.conf.

In `@valkey/templates/statefulset.yaml`:
- Around line 46-59: Document near replica.sentinel.persistence.enabled,
preferably in the values comment or README, that enabling Sentinel persistence
requires recreating the existing StatefulSet because volumeClaimTemplates are
immutable. Instruct users to delete the StatefulSet with --cascade=orphan before
upgrading, preserving the existing release and pods.

In `@valkey/tests/haproxy_test.yaml`:
- Around line 33-45: Add assertions to the HAProxy test covering the
valkey_replicas backend’s PING/+PONG health-check sequence and fall 2 setting,
ensuring it remains distinct from the valkey_master role check. Also assert the
deployment’s checksum/config annotation, following the equivalent assertion
pattern in the Sentinel tests and using the rendered ConfigMap checksum.
- Around line 53-65: Replace the HAProxy tunnel timeout assertion in the “should
keep pub/sub connections open” test with a large finite timeout such as 24h,
update the corresponding timeout configuration and its comment in the haproxy
ConfigMap template, and document the pub/sub persistence versus half-open
connection tradeoff while preserving the client and server TCP keepalive
options.

In `@valkey/tests/sentinel_test.yaml`:
- Around line 285-293: Add a test case in sentinel_test.yaml covering
replica.sentinel.service.enabled: false, while keeping the replica and sentinel
settings enabled so the service-specific gate is exercised. Render
templates/sentinel-service.yaml and assert that zero documents are produced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e5b2fd37-90c1-4b8b-a5d3-e72f7f171d4e

📥 Commits

Reviewing files that changed from the base of the PR and between 01b4fa4 and 629bd4a.

📒 Files selected for processing (20)
  • valkey/.helmignore
  • valkey/Chart.yaml
  • valkey/README.md
  • valkey/examples/ha-sentinel.yaml
  • valkey/templates/NOTES.txt
  • valkey/templates/_helpers.tpl
  • valkey/templates/deploy_valkey.yaml
  • valkey/templates/haproxy-configmap.yaml
  • valkey/templates/haproxy-deployment.yaml
  • valkey/templates/haproxy-service.yaml
  • valkey/templates/init_config.yaml
  • valkey/templates/sentinel-configmap.yaml
  • valkey/templates/sentinel-service.yaml
  • valkey/templates/service-headless.yaml
  • valkey/templates/service.yaml
  • valkey/templates/statefulset.yaml
  • valkey/tests/haproxy_test.yaml
  • valkey/tests/sentinel_test.yaml
  • valkey/values.schema.json
  • valkey/values.yaml

Comment thread valkey/examples/ha-sentinel.yaml Outdated
Comment thread valkey/README.md
Comment on lines +76 to +90
**Services:**

* `valkey`: load balances across all pods, the master can be any of them
* `valkey-sentinel`: Sentinel endpoints, used by clients to resolve the current master
* `valkey-headless`: headless service for pod and Sentinel discovery

**Connecting:**

Because the master moves, clients must ask Sentinel for its address instead of connecting to a fixed pod.
Most client libraries do this for you:

```python
from valkey.sentinel import Sentinel

sentinel = Sentinel([("valkey-sentinel", 26379)], password="...")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard Sentinel endpoint documentation on replica.sentinel.service.enabled.

The chart creates the valkey-sentinel Service only when this value is true. Both locations currently present the endpoint as unconditional.

  • valkey/README.md#L76-L90: state the Service prerequisite or document per-pod Sentinel endpoints.
  • valkey/templates/NOTES.txt#L27-L32: conditionally print the Service commands and provide an alternative when the Service is disabled.
📍 Affects 2 files
  • valkey/README.md#L76-L90 (this comment)
  • valkey/templates/NOTES.txt#L27-L32
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@valkey/README.md` around lines 76 - 90, Update valkey/README.md lines 76-90
to state that the valkey-sentinel Service and its endpoint are available only
when replica.sentinel.service.enabled is true, or document the per-pod Sentinel
endpoints when disabled. Update valkey/templates/NOTES.txt lines 27-32 to
conditionally print Service-based connection commands when
replica.sentinel.service.enabled is true and provide the appropriate per-pod
Sentinel alternative otherwise.

Comment thread valkey/README.md Outdated
Comment thread valkey/README.md
Comment thread valkey/templates/_helpers.tpl Outdated
Comment thread valkey/templates/sentinel-configmap.yaml Outdated
Comment thread valkey/templates/sentinel-configmap.yaml Outdated
Comment thread valkey/templates/service.yaml
Comment thread valkey/templates/statefulset.yaml
Comment thread valkey/templates/statefulset.yaml Outdated
pat-s and others added 2 commits August 1, 2026 12:37
The bootstrap marker was written on every pod, so a pod that lost its
valkey.conf while keeping the marker, for example when the init container was
interrupted between removing the old config and writing the new one, read the
marker as proof that it used to be the master and came back as a second
writable master.

Drop the marker and derive the role from the previous config instead, which
already carries the answer: a config with a replicaof directive means replica,
a complete config without one means master, and no config at all means this is
a first boot that falls back to the pod index. Assemble the config in a
temporary file and move it into place at the end, so a config on disk is
always complete and the distinction holds.

Also only disable protected mode on Sentinel when auth is disabled, where
peers could not connect otherwise, and say so in values.yaml.

Signed-off-by: pat-s <patrick.schratz@devxy.io>
Sentinel moves the master between pods, which every client has to follow.
Clients whose library cannot talk to Sentinel have no way to do that.

Add an optional HAProxy deployment that health checks every Valkey node with
INFO replication and forwards its write port only to the node answering
"role:master", with a second port load balancing reads across all healthy
nodes. The health check is the failover mechanism, so there is no watcher
sidecar, no runtime package installation and no admin socket.

* Send QUIT before the role check so a replica fails its check immediately
  instead of once the check timeout expires, which shortens the window in
  which a demoted master still receives writes.
* Start every server in the down state, so a starting HAProxy cannot forward
  writes to a replica before the first check has run.
* Pass the health check credentials through an environment variable read from
  the existing secret, keeping the password out of the ConfigMap.
* Keep pub/sub connections alive with a tunnel timeout and TCP keepalives.
* Validate the node certificates against the configured CA when TLS is on.

Co-authored-by: KHTee <teekahhui@hotmail.com>
Co-authored-by: Jose Dominguez <jdominguez.ops@gmail.com>
Co-authored-by: yoannrt <yoann.rousseau@gmail.com>
Co-authored-by: lazariv <lazariv.taras@gmail.com>
Co-authored-by: Tim Karger <49390121+tkarger@users.noreply.github.com>
Signed-off-by: pat-s <patrick.schratz@devxy.io>
@pat-s
pat-s force-pushed the feat/sentinel-haproxy branch from 629bd4a to 9630b55 Compare August 1, 2026 12:37
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an optional HAProxy frontend for Sentinel-based Valkey deployments, providing stable write and read endpoints for clients that do not connect to Sentinel directly.

The HAProxy configuration was rendered with Sentinel, ACL authentication, TLS certificate verification, and required client-certificate authentication enabled. The executed checks confirmed that port 6379 routes through the master-only backend, port 6380 routes through the replica backend, health-check credentials come from a Secret, and the configured CA and client certificate bundle are applied.

No review findings remain. The write-routing concern was disproved by the successful rendered configuration: the write frontend selects valkey_master, whose health check requires role:master, while read traffic selects valkey_replicas.

Confidence Score: 5/5

The PR is safe to merge; no blocking failure remains.

The exercised HAProxy and Sentinel configuration correctly separates master write routing from replica read routing and renders the authentication and TLS settings required for the supported secure deployment path.

T-Rex T-Rex Logs

What T-Rex did

  • Ran the self-contained Helm and PyYAML validation script, which rendered a Sentinel deployment with HAProxy, ACL authentication, TLS verification, and a required client certificate, and it completed with exit code 0 with all assertions passing.
  • The validation confirmed the routing and security configuration, including port 6379 mapped to valkey_master with a role:master health check and port 6380 to valkey_replicas, with the health-check password sourced from a Secret and the CA verification and client certificate settings rendered as expected, ensuring writes stay off replica backends.
  • Reviewed the successful observed output to corroborate the run results, noting exit code 0 and all assertions passed.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (2): Last reviewed commit: "fix(valkey): bound split-brain damage an..." | Re-trigger Greptile

Comment thread valkey/templates/_helpers.tpl Outdated
pat-s added 3 commits August 1, 2026 12:51
…argv

Review follow-ups on the Sentinel support.

* Ask the running Sentinels for the current master when a pod starts without a
  usable config. A pod that lost its volume previously fell back to the pod
  index, so pod-0 came back as a second writable master after a failover.
* Render the Valkey listen port from service.port instead of a hardcoded 6379,
  which made a custom service.port disagree with the container port.
* Pass credentials to valkey-cli through REDISCLI_AUTH so they no longer show
  up in the process list of the pod.
* Reject an incomplete answer from a peer Sentinel, an error reply would
  otherwise be written into the monitor directive and stop Sentinel from
  starting. Bound the peer queries with a timeout and widen the startup probe
  to cover discovery.
* Set terminationGracePeriodSeconds and refuse a preStop failover timeout that
  would be cut short by SIGKILL.
* Allow replica.podManagementPolicy to be pinned, so an existing release can
  enable Sentinel without recreating the immutable StatefulSet, and document
  the recreate for anyone who takes the default.
* Render loadBalancerSourceRanges as a YAML list, and point the notes at the
  headless service when the Sentinel service is disabled.

Signed-off-by: pat-s <patrick.schratz@devxy.io>
Review follow-ups on the HAProxy front-end.

* Balance the write backend with "first" instead of the default roundrobin.
  If two nodes ever pass the role:master check at once, for example an old
  master that Sentinel has not demoted yet, roundrobin would split writes
  between them and everything landing on the loser would be discarded.
* Mount the TLS secret with mode 0440 and give the pod an fsGroup, the files
  are owned by root so the non-root HAProxy user could not read them and the
  container never started with TLS enabled.
* Take the client certificate from haproxy.tls.clientCertFile, a single file
  holding the certificate and its key, because that is what HAProxy expects.
  The previous "crt tls.serverPublicKey" pointed at a certificate without its
  key. Fail rendering with an explanation when the value is missing.
* Describe a failover as Sentinel detection plus HAProxy convergence rather
  than checkInterval alone.

Signed-off-by: pat-s <patrick.schratz@devxy.io>
@pat-s

pat-s commented Aug 1, 2026

Copy link
Copy Markdown
Author

Review fixes pushed, and I tested the TLS path properly this time: a kind cluster with a real CA, ACL auth and tls.enabled, which is how two of these were confirmed.

Fixed

  • Round-robin across two apparent masters (CodeRabbit) — good catch. The write backend now uses balance first, so if an old master that Sentinel has not demoted yet still passes the role:master check, every connection goes to the same node instead of being split between two masters. The read backend keeps roundrobin.

  • HAProxy could not read the TLS secret (CodeRabbit) — real, and it made the whole TLS path unusable: secret files are owned by root, the container runs as uid 99, and defaultMode: 0400 left them unreadable. Now 0440 plus fsGroup: 99. With the fix all three HAProxy pods start with TLS on and the write port reaches role:master through a verified TLS connection.

  • crt needs a combined cert and key (CodeRabbit) — real. crt /tls/{{ tls.serverPublicKey }} pointed at a certificate with no key, and HAProxy's <cert>.key fallback would look for server.crt.key, which the secret does not contain. Added haproxy.tls.clientCertFile for a single file holding both, and rendering now fails with an explanation when tls.requireClientCertificate is set without it. Validated the mTLS config with haproxy -c against a real bundle.

  • Failover timing wording (CodeRabbit) — correct, checkInterval only bounds HAProxy's convergence after the promotion. The README now describes both steps, Sentinel detection then HAProxy convergence, and the values table says the same.

  • Credentials in the example (CodeRabbit) — the passwords are now REPLACE-ME placeholders with a security note at the top of the file pointing at auth.usersExistingSecret and a kubectl create secret command.

  • Sentinel endpoint docs (CodeRabbit) — both the README and the notes are guarded on replica.sentinel.service.enabled, handled in feat(valkey): add Valkey Sentinel for automatic failover #234.

The init.sh bootstrap-marker findings and the fenced-code-block one are addressed in #234, which this branch builds on.

186 unit tests, helm lint, haproxy -c on the rendered config with and without mTLS, and rendering against 1.28/1.35/1.36 all pass locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant