gitlab-haproxy-agent

command module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: MIT Imports: 9 Imported by: 0

README

gitlab-haproxy-agent

The agent-check responder for the HAProxy control plane: haproxy asks it about every backend server on a short interval, and it answers with the desired admin state held in consul KV. Writing one KV key drains, un-drains, or reweights a server fleet-wide within an agent-check interval, with no haproxy reload.

Design, rationale, contracts, rollout, and alternatives live in the control plane work item.

How it works

Two components around an in-memory copy of the desired state, so answers never wait on consul:

  • a consul watcher: a blocking-query loop on a KV prefix, swapping in a full copy of the state on every change
  • a TCP responder: per connection, read the newline-terminated agent-send line carrying the server identity (by convention backend/server), look it up, write one reply line, close

haproxy side, per server:

server srv1 10.0.0.1:443 weight 100 check agent-check agent-addr <agent-host> agent-port 9777 agent-send "be_web/srv1\n" agent-inter 2s
KV contract

Keys live under a prefix (default gitlab-haproxy-agent/state). The part below the prefix is the agent-send identity, the value is the agent-check reply:

key value
gitlab-haproxy-agent/state/<backend>/<server> ready | drain | maint | <weight>% (0–256)

An absent key means ready: operators only write state to divert from the default, and deleting the key restores it. Values that don't parse as a valid reply are dropped (logged and counted), as if the key were absent.

Use maint for take-out-and-return work and drain for instant removal from new-connection balancing: leaving maintenance triggers the configured slowstart ramp, leaving drain does not.

Health states (up, down, stopped, fail) are deliberately not accepted: server health belongs to haproxy's own checks, the control plane only sets admin state.

Failure behavior

The agent never guesses. In every degraded situation it either answers from the last known state or answers nothing. Per the agent-check protocol, a connection without a parseable reply line leaves the server's state unchanged ("Ignoring incomplete line from agent").

situation behavior
consul unreachable at startup agent exits and the supervisor retries. haproxy keeps last known state while the agent is unreachable
consul unreachable at runtime answers continue from the last synced copy. sync errors are counted and the last-sync gauge goes stale
identity has no KV entry reply ready, counted (drift between haproxy config and KV shows up here)
no newline-terminated identity before the timeout close without reply
listener up before first sync close without reply (only possible in the start window)
agent down entirely haproxy freezes the last agent-provided state

Running it

gitlab-haproxy-agent \
  -listen-addr :9777 \
  -monitoring-addr :9778 \
  -consul-addr 127.0.0.1:8500 \
  -kv-prefix gitlab-haproxy-agent/state

All flags are optional and also available as environment variables (GITLAB_HAPROXY_AGENT_LISTEN_ADDR etc.). -consul-addr falls back to the consul client defaults (CONSUL_HTTP_ADDR etc.). Configuration stays flags-only until options outgrow flags. Then it becomes a labkit v2 config file, not before.

Observability

-monitoring-addr serves /-/metrics, /-/liveness, /-/readiness, and /debug/pprof. Readiness reports 503 until the first consul sync completes. Logs are JSON on stderr (GITLAB_LOG_LEVEL, GITLAB_LOG_FORMAT).

metric meaning
gitlab_haproxy_agent_response_duration_seconds accept-to-reply latency, must stay well below haproxy's check timeout (agent-inter unless timeout check is set)
gitlab_haproxy_agent_responses_total{state} replies by state (ready / drain / maint / weight)
gitlab_haproxy_agent_unknown_server_lookups_total identities with no KV entry, answered ready
gitlab_haproxy_agent_malformed_requests_total connections closed without a reply
gitlab_haproxy_agent_kv_last_sync_timestamp_seconds staleness of the state copy answers are served from
gitlab_haproxy_agent_kv_sync_errors_total failed consul reads
gitlab_haproxy_agent_kv_entries entries currently held
gitlab_haproxy_agent_kv_invalid_values_total KV entries dropped as unparseable

Packaging

Both delivery forms come from goreleaser:

  • linux binaries as archives attached to GitLab releases, for the Chef VMs (remote_file in the gitlab-haproxy cookbook, same pattern as hatop)
  • a container image pushed to the registry, for the k8s sidecar

Both are also published on ops.gitlab.net for HA: the repo push-mirrors to ops and the mirrored tag pipeline publishes archives and images there. The agent must stay buildable and installable during a gitlab.com outage. That is when it gets used.

Development

  1. Follow the developer setup guide at https://gitlab.com/gitlab-com/gl-infra/common-ci-tasks/-/blob/main/docs/developer-setup.md.
  2. Clone this project
  3. Run scripts/prepare-dev-env.sh
Tests
mise run test:unit
mise run test:integration   # needs the haproxy binary (brew install haproxy), consul comes from mise

The integration test is a real haproxy and a real consul around ordinary Go code: consul in dev mode, haproxy with one agent-checked backend server (a net.Listener in the test), the agent in-process. The test writes KV and asserts through show stat on the haproxy stats socket. At agent-inter 100ms the suite runs in seconds. Covered beyond the happy path: startup convergence, responder-down state freeze, garbage replies, weight percentages.

In CI the same test runs in an in-repo image (.gitlab/ci/test-image/Dockerfile) that pins the haproxy minor production runs, plus the Go and consul versions from .tool-versions. The image rebuilds automatically when any pinned version changes the tag.

Weight stepping

The agent has none, deliberately. It stays a stateless lookup: ramp state, restart semantics, and divergence between KV and what haproxy sees are not worth carrying for something the rest of the system already provides.

  • Undrain ramps come from haproxy's own slowstart on the server line. Leaving agent-driven maintenance ramps the effective weight natively, per node (covered by TestSlowstartRampsAfterMaint).
  • Explicit trajectories (canary shifting, gradual drain-down), should they ever be needed, belong in the KV writer: the contract accepts arbitrary N% values, so a control-plane tool can implement any curve by writing intermediate weights. Every step is visible in KV and in the metrics.

What's next

  • the gstg rollout, via the gitlab-haproxy cookbook and the k8s sidecar

Documentation

Overview

gitlab-haproxy-agent answers haproxy agent-check probes with the desired server state held in consul KV.

Directories

Path Synopsis
internal
responder
Package responder answers haproxy agent-check probes over TCP: read one newline-terminated agent-send identity, write one reply line, close.
Package responder answers haproxy agent-check probes over TCP: read one newline-terminated agent-send identity, write one reply line, close.
state
Package state holds the desired admin state for haproxy servers and keeps it in sync with consul KV.
Package state holds the desired admin state for haproxy servers and keeps it in sync with consul KV.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL