Autoscaler Deployment
The autoscaler decides how many replicas each application should run. It has two parts: a Go strategy plugin for the HashiCorp Nomad Autoscaler, and a Python optimizer service the plugin asks for replica counts. Both run on a Nomad cluster; none of this applies to Compose installs.
Components
- Nomadscaler Plugin - Go-based Nomad Autoscaler strategy plugin (
productify-scaler, source inautoscaler/nomadscaler) - Optimizer Service - Python (FastAPI) service that forecasts demand and solves a MILP for the replica count (source in
autoscaler/optimizer)
Optimizer Deployment
The Optimizer is configured through a config.ini file (see Optimizer Configuration). The container reads the config path from the CONFIG_PATH environment variable and the listen port from PORT (default: 8015).
The release workflow publishes the optimizer as ghcr.io/productifyfw/autoscaler/optimizer:<version> and :latest. That package is private: an anonymous pull is refused with 401, so docker login ghcr.io with a token that can read ProductifyFW packages first, until the package is made public. (ghcr.io/productifyfw/autoscaler-nomadscaler is public.)
The image ships with a baked-in development config.ini (test data and test metrics enabled), so it starts without any mount:
docker run --rm -p 8015:8015 ghcr.io/productifyfw/autoscaler/optimizer:latestDocker
For production, mount your own config.ini and point CONFIG_PATH at it (it overrides the baked-in config):
docker run -d \
--name optimizer \
-p 8015:8015 \
-e CONFIG_PATH=/app/config.ini \
-v $PWD/config.ini:/app/config.ini:ro \
ghcr.io/productifyfw/autoscaler/optimizer:latestDocker Compose
services:
optimizer:
image: ghcr.io/productifyfw/autoscaler/optimizer:latest
ports:
- "8015:8015"
environment:
CONFIG_PATH: /app/config.ini
volumes:
- ./config.ini:/app/config.ini:ro
restart: unless-stoppedNomad
job "optimizer" {
datacenters = ["dc1"]
type = "service"
group "optimizer" {
count = 2 # For high availability
network {
port "http" {
to = 8015
}
}
service {
name = "optimizer"
port = "http"
tags = ["productify", "autoscaler"]
check {
type = "http"
path = "/health"
interval = "10s"
timeout = "2s"
}
}
task "server" {
driver = "docker"
config {
image = "ghcr.io/productifyfw/autoscaler/optimizer:latest"
ports = ["http"]
}
env {
CONFIG_PATH = "${NOMAD_TASK_DIR}/config.ini"
PORT = "${NOMAD_PORT_http}"
}
template {
data = <<EOF
[main]
loglevel=info
api_loglevel=warning
only_test_data=false
enable_test_metrics=false
prometheus_url=http://prometheus:9090
port={{ env "NOMAD_PORT_http" }}
token=<optimizer-token>
EOF
destination = "${NOMAD_TASK_DIR}/config.ini"
}
resources {
cpu = 1000
memory = 1024
}
}
}
}Nomadscaler Plugin Deployment
Installation
# Build plugin (produces ./bin/productify-scaler and the productify/scaler:local Docker image)
cd autoscaler/nomadscaler
./build.sh
# Install to Nomad Autoscaler
sudo cp ./bin/productify-scaler /opt/nomad-autoscaler/plugins/
sudo chmod +x /opt/nomad-autoscaler/plugins/productify-scalerAlternatively, use the prebuilt image ghcr.io/productifyfw/autoscaler-nomadscaler:latest, which bundles the Nomad Autoscaler agent with the plugin preinstalled at /plugins/productify-scaler.
Nomad Autoscaler Configuration
# autoscaler.hcl
plugin_dir = "/opt/nomad-autoscaler/plugins"
nomad {
address = "http://localhost:4646"
}
apm "nomad" {
driver = "nomad-apm"
config = {
address = "http://localhost:4646"
}
}
strategy "productify-scaler" {
driver = "productify-scaler"
config = {
optimizer_url = "http://optimizer:8015"
optimizer_token = "<optimizer-token>"
}
}
http {
bind_address = "0.0.0.0"
bind_port = 8080
}
log {
level = "INFO"
}Scaling Policy
Scaling policies are embedded in the target job's scaling block and use the productify-scaler strategy:
group "frontend" {
count = 3
scaling {
enabled = true
min = 2
max = 20
policy {
evaluation_interval = "10s"
cooldown = "30s"
check "productify-scale-check" {
source = "nomad-apm"
query = "avg_cpu-allocated"
strategy "productify-scaler" {
min = 2
max = 20
metric_app_name = "web-app" # the `app` label on the pfy_* series
group = "frontend" # the Nomad task group this scaling block lives in
task = "web" # the task inside it, i.e. the task block below
cache_size = 30 # seconds the response covers; above step_seconds gives a multi-step ramp
}
}
}
}
task "web" {
# ...
}
}optimizer_url and optimizer_token can be set either in the agent-level strategy block (as above) or per check in the strategy "productify-scaler" config. See Scaling Policies for the full parameter reference.
Running Nomad Autoscaler
nomad-autoscaler agent \
-config=/etc/nomad-autoscaler/autoscaler.hcl \
-plugin-dir=/opt/nomad-autoscaler/pluginsComplete Stack Deployment
Docker Compose
services:
optimizer:
image: ghcr.io/productifyfw/autoscaler/optimizer:latest
ports:
- "8015:8015"
environment:
CONFIG_PATH: /app/config.ini
volumes:
- ./config.ini:/app/config.ini:ro
restart: unless-stopped
nomad-autoscaler:
image: ghcr.io/productifyfw/autoscaler-nomadscaler:latest
volumes:
- ./autoscaler.hcl:/etc/autoscaler.hcl:ro
command: agent -config=/etc/autoscaler.hcl
environment:
NOMAD_ADDR: http://nomad:4646
depends_on:
- optimizer
restart: unless-stoppedNomad Job (Complete)
A complete reference job that runs the Nomad Autoscaler agent, the Optimizer, and a Prometheus instance in a single group is at autoscaler/nomadscaler/config/autoscaler.hcl. That file uses the locally built productify/scaler:local and productify/optimizer:local images; the abbreviated version below substitutes the published ones:
job "autoscaler" {
datacenters = ["dc1"]
group "autoscaler" {
count = 1
network {
port "optimizer_port" {}
port "optimizer_port_prometheus" {}
}
task "autoscaler" {
driver = "docker"
config {
image = "ghcr.io/productifyfw/autoscaler-nomadscaler:latest"
command = "nomad-autoscaler"
args = ["agent", "-config", "${NOMAD_TASK_DIR}/config.hcl"]
}
template {
data = <<EOF
plugin_dir = "/plugins"
nomad {
address = "http://{{env "attr.unique.network.ip-address" }}:4646"
}
apm "nomad" {
driver = "nomad-apm"
config = {
address = "http://{{env "attr.unique.network.ip-address" }}:4646"
}
}
strategy "productify-scaler" {
driver = "productify-scaler"
config = {
{{with nomadVar "nomad/jobs/autoscaler/autoscaler/autoscaler" -}}
optimizer_token = "{{.TOKEN}}"
{{- end}}
optimizer_url = "http://{{env "NOMAD_IP_optimizer_port"}}:{{env "NOMAD_PORT_optimizer_port"}}"
}
}
EOF
destination = "${NOMAD_TASK_DIR}/config.hcl"
}
resources {
cpu = 200
memory = 256
}
}
task "optimizer" {
driver = "docker"
config {
image = "ghcr.io/productifyfw/autoscaler/optimizer:latest"
ports = ["optimizer_port"]
}
env {
CONFIG_PATH = "${NOMAD_TASK_DIR}/config.ini"
PORT = "${NOMAD_PORT_optimizer_port}"
}
template {
data = <<EOF
[main]
loglevel=info
api_loglevel=warning
only_test_data=false
enable_test_metrics=false
prometheus_url=http://{{env "NOMAD_IP_optimizer_port_prometheus"}}:{{env "NOMAD_PORT_optimizer_port_prometheus"}}
port={{env "NOMAD_PORT_optimizer_port"}}
{{ with nomadVar "nomad/jobs/autoscaler/autoscaler/optimizer" -}}
token={{.TOKEN}}
{{- end }}
EOF
destination = "${NOMAD_TASK_DIR}/config.ini"
}
resources {
cpu = 768
memory = 512
}
}
# A prometheus task also runs in this group; see
# autoscaler/nomadscaler/config/autoscaler.hcl for the full job.
}
}Configuration
Optimizer Configuration
See Optimizer Configuration for details.
Key config.ini settings ([main] section):
port- HTTP listen port (default: 8015)prometheus_url- Prometheus server to read metrics fromtoken- Shared token required on/optimizerequestsloglevel/api_loglevel- Logging levelsonly_test_data- Use synthetic test data instead of Prometheusenable_test_metrics- Serve a synthetic test-metrics endpoint (port set bymetrics_port, default: 8017)
Plugin Configuration
See Scaling Policies for details.
Key settings:
optimizer_url- Optimizer service URLoptimizer_token- Token for authenticating with the Optimizermin/max- Replica boundsmetric_app_name- Theapplabel on thepfy_*demand series (an application UUID for anythingpfy nomad renderemits). Not a Nomad identifiergroup- Nomad task group of the scaling block (recommended; the optimizer'stask_grouplabel)task- Nomad task inside that group (the optimizer'stasklabel on thenomad_client_allocs_*capacity queries). Omitted, it falls back tometric_app_name, which measures nothing unless the task is named after theapplabelcache_size- Seconds each optimizer response covers (default: 10). The response holdsceil(cache_size / step_seconds)values; the MILP plansmilp_startup_delaysteps further and returns the actionable tail. At or belowstep_seconds(15 by default) you get one value per call, so the count moves at most one scale step between calls; see Known Issues. It also bounds how long a scale-down has to pay for itself: a replica is released only once thereplica_costsaved over the actionable horizon reachesmilp_shutdown_cost, so a shortcache_sizeon a small allocation holds the count; see Configurationevaluation_interval- How often to evaluate (policy-level)cooldown- Minimum time between scaling actions (policy-level)
The task key is not optional in practice. The optimizer selects the nomad_client_allocs_* capacity series by the Nomad task name it carries; a strategy block that omits task falls back to metric_app_name, which is the app label (an application UUID on rendered jobs), so the capacity query matches nothing and the app is either answered 503 metrics_unavailable on every evaluation (replicas running) or sized from the fallback constants (at zero replicas). Set task = "<the Nomad task name>" on every policy, and re-render any job rendered without it. See Known Issues.
Health Checks
Optimizer Health
curl http://localhost:8015/healthNomad Autoscaler Status
curl http://localhost:8080/v1/healthPolicy Status
nomad scaling policy listMonitoring
Optimizer Metrics
The Optimizer exposes its own always-on Prometheus metrics at GET /metrics on the API port (default 8015: request counts, solve behaviour, cost breakdowns). Separately, when enable_test_metrics=true is set in config.ini, it serves a synthetic test-metrics endpoint on metrics_port (default 8017) that generates load-pattern data for development and testing. Never enable it in production.
Nomad Autoscaler Metrics
With telemetry { prometheus_metrics = true } in the agent configuration, the Nomad Autoscaler exposes Prometheus metrics on its HTTP endpoint:
curl http://localhost:8080/v1/metrics?format=prometheusTroubleshooting
Optimizer Not Responding
Check that:
- Service is running
- Port 8015 (or configured port) is accessible
- Health endpoint returns 200
- Logs for errors
Plugin Not Loading
Check that:
- Plugin binary in correct directory
- Binary has execute permissions
- Plugin version matches autoscaler version
- Logs show plugin registration
No Scaling Actions
Check that:
- Policy is enabled
- Metrics are being collected
- Optimizer is reachable
- Cooldown period hasn't been triggered
- Min/max bounds allow scaling