Skip to content

Commit a5a3d63

Browse files
authored
feat(nomad-nodepool-apm): add deployment-aware target plugin (#3270)
Adds a Nomad Autoscaler target plugin that fails a conflicting active deployment before changing a task group count (Nomad rejects scaling while a deployment is in progress). Scaling is serialized per namespaced job and retried from fresh state on races. The plugin is a no-op when the durable count already matches, and the convergence check ignores the rollout deployment spawned by its own scale, so in-flight rollouts converging to the right count are never failed.
1 parent b1361ef commit a5a3d63

6 files changed

Lines changed: 906 additions & 10 deletions

File tree

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ packages/
363363
db/ Postgres migrations (goose) + queries (sqlc)
364364
clickhouse/ ClickHouse schema, batching writers, query clients
365365
otel-collector/ Collector config
366-
nomad-nodepool-apm/ Nomad autoscaler plugin (scale job = node count)
366+
nomad-nodepool-apm/ Nomad autoscaler metric and deployment-aware target plugins
367367
local-dev/ docker-compose local stack + DB seeding
368368
spec/ OpenAPI specs (public, edge, dashboard) — codegen sources
369369
iac/ Terraform + Nomad jobs (provider-gcp, provider-aws, shared modules)

packages/nomad-nodepool-apm/Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ AWS_BUCKET_PREFIX ?= $(PREFIX)$(AWS_ACCOUNT_ID)-
55
GCP_BUCKET_PREFIX ?= $(GCP_PROJECT_ID)-
66

77
PLUGIN_NAME := nomad-nodepool-apm
8+
TARGET_PLUGIN_NAME := nomad-deployment-aware-target
89
BIN_DIR := bin
910
BINARY := $(BIN_DIR)/$(PLUGIN_NAME)
11+
TARGET_BINARY := $(BIN_DIR)/$(TARGET_PLUGIN_NAME)
1012

1113
.PHONY: build
1214
build:
1315
@mkdir -p $(BIN_DIR)
1416
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(BINARY) .
17+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(TARGET_BINARY) ./cmd/nomad-deployment-aware-target
1518

1619
.PHONY: clean
1720
clean:
@@ -25,12 +28,17 @@ test:
2528
upload:
2629
$(eval COMMIT_SHA := $(shell git rev-parse --short HEAD))
2730
chmod +x $(BINARY)
31+
chmod +x $(TARGET_BINARY)
2832
ifeq ($(PROVIDER),aws)
2933
aws s3 cp $(BINARY) "s3://${AWS_BUCKET_PREFIX}fc-env-pipeline/nomad-nodepool-apm" --profile ${AWS_PROFILE} --cache-control "no-cache, max-age=0"
3034
aws s3 cp $(BINARY) "s3://${AWS_BUCKET_PREFIX}fc-env-pipeline/nomad-nodepool-apm.$(COMMIT_SHA)" --profile ${AWS_PROFILE} --cache-control "no-cache, max-age=0"
35+
aws s3 cp $(TARGET_BINARY) "s3://${AWS_BUCKET_PREFIX}fc-env-pipeline/nomad-deployment-aware-target" --profile ${AWS_PROFILE} --cache-control "no-cache, max-age=0"
36+
aws s3 cp $(TARGET_BINARY) "s3://${AWS_BUCKET_PREFIX}fc-env-pipeline/nomad-deployment-aware-target.$(COMMIT_SHA)" --profile ${AWS_PROFILE} --cache-control "no-cache, max-age=0"
3137
else
3238
gcloud storage cp --cache-control="no-cache, max-age=0" $(BINARY) "gs://${GCP_BUCKET_PREFIX}fc-env-pipeline/nomad-nodepool-apm"
3339
gcloud storage cp --cache-control="no-cache, max-age=0" $(BINARY) "gs://${GCP_BUCKET_PREFIX}fc-env-pipeline/nomad-nodepool-apm.$(COMMIT_SHA)"
40+
gcloud storage cp --cache-control="no-cache, max-age=0" $(TARGET_BINARY) "gs://${GCP_BUCKET_PREFIX}fc-env-pipeline/nomad-deployment-aware-target"
41+
gcloud storage cp --cache-control="no-cache, max-age=0" $(TARGET_BINARY) "gs://${GCP_BUCKET_PREFIX}fc-env-pipeline/nomad-deployment-aware-target.$(COMMIT_SHA)"
3442
endif
3543

3644
.PHONY: build-and-upload

packages/nomad-nodepool-apm/README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
# Nomad NodePool APM Plugin
1+
# Nomad Node Pool Autoscaler Plugins
22

3-
A custom APM (Application Performance Monitoring) plugin for the [Nomad Autoscaler](https://github.com/hashicorp/nomad-autoscaler) that provides the count of nodes in a Nomad node pool.
3+
Custom plugins for the [Nomad Autoscaler](https://github.com/hashicorp/nomad-autoscaler) that keep a service-job allocation count aligned with the number of nodes in a Nomad node pool.
44

55
## Purpose
66

7-
This plugin enables service jobs to scale their count based on the number of nodes in a specific node pool, effectively replicating the behavior of system jobs while benefiting from proper rolling update support.
7+
The package builds two plugins because Nomad Autoscaler supports only one plugin type per external binary:
8+
9+
- `nomad-nodepool-apm` reports the number of ready, eligible nodes in a pool.
10+
- `nomad-deployment-aware-target` fails an active deployment before applying the requested task-group count.
11+
12+
Together they let service jobs replicate system-job placement while retaining rolling updates. The deployment-aware target is used by the `orchestrator-ee` service job. When the task-group count must change, scaling intentionally abandons a conflicting in-progress rollout (Nomad rejects scaling while a deployment is active); Nomad records that deployment as `failed`, rather than `cancelled`. The rollout spawned by the scale itself is left to run, and no deployment is touched when the count already matches. The target requires `auto_revert = false` on the scaled group (otherwise failing a deployment would restore an older job version and ping-pong); it refuses to scale groups with `auto_revert = true` and returns an error on every attempt.
813

914
## Usage
1015

@@ -25,14 +30,23 @@ GCP_PROJECT_ID=your-project-id make build-and-upload
2530
In your Nomad Autoscaler configuration:
2631

2732
```hcl
28-
apm "nomad-nodepool" {
29-
driver = "nomad-nodepool"
33+
apm "nomad-nodepool-apm" {
34+
driver = "nomad-nodepool-apm"
3035
config = {
3136
nomad_address = "http://localhost:4646" # Optional, uses NOMAD_ADDR env var
3237
nomad_token = "your-token" # Optional, uses NOMAD_TOKEN env var
3338
nomad_region = "global" # Optional
3439
}
3540
}
41+
42+
target "nomad-deployment-aware-target" {
43+
driver = "nomad-deployment-aware-target"
44+
config = {
45+
nomad_address = "http://localhost:4646"
46+
nomad_token = "your-token"
47+
nomad_region = "global"
48+
}
49+
}
3650
```
3751

3852
### In Job Scaling Policy
@@ -47,9 +61,11 @@ scaling {
4761
evaluation_interval = "30s"
4862
cooldown = "2m"
4963
64+
target "nomad-deployment-aware-target" {}
65+
5066
check "match_node_count" {
51-
source = "nomad-nodepool"
52-
query = "build" # Node pool name
67+
source = "nomad-nodepool-apm"
68+
query = "orchestrator" # Node pool name
5369
5470
strategy "pass-through" {} # Use node count directly as desired count
5571
}
@@ -65,12 +81,18 @@ scaling {
6581
- Scheduling eligibility: `eligible`
6682
3. Returns the count as a metric for the autoscaler to use
6783
4. With the `pass-through` strategy, this count becomes the desired number of allocations
84+
5. The target serializes scaling per namespaced job and is a no-op when the durable count already matches
85+
6. When the count must change it fails a conflicting active deployment, rereads the job, and scales with the current job modify index
86+
7. It verifies the final durable count; the rollout spawned by the scale proceeds normally
87+
8. Concurrent job or deployment changes are retried from fresh state with a bounded attempt count
88+
89+
Dry-run actions do not fail deployments or write task-group counts.
6890

6991
## Query Format
7092

7193
The `query` parameter should be the name of the node pool you want to count nodes from.
7294

73-
Example: `query = "build"` will count all ready, eligible nodes in the "build" node pool.
95+
Example: `query = "orchestrator"` counts all ready, eligible nodes in the `orchestrator` node pool.
7496

7597
## Configuration Options
7698

@@ -80,4 +102,3 @@ Example: `query = "build"` will count all ready, eligible nodes in the "build" n
80102
| `nomad_token` | Nomad ACL token | `NOMAD_TOKEN` env var |
81103
| `nomad_region` | Nomad region | Default region |
82104
| `nomad_namespace` | Nomad namespace | Default namespace |
83-
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package main
2+
3+
import (
4+
"github.com/hashicorp/go-hclog"
5+
"github.com/hashicorp/nomad-autoscaler/plugins"
6+
7+
targetplugin "github.com/e2b-dev/infra/packages/nomad-nodepool-apm/target"
8+
)
9+
10+
func main() {
11+
plugins.Serve(factory)
12+
}
13+
14+
func factory(log hclog.Logger) any {
15+
return targetplugin.New(log)
16+
}

0 commit comments

Comments
 (0)