450 lines
12 KiB
HCL
450 lines
12 KiB
HCL
# «Контракт-чек» app services on Nomad (single VPS, docker driver).
|
|
#
|
|
# Groups: api (with prestart migrations) + 5 workers. Stateful infra
|
|
# (postgres/redis/rabbitmq/minio) stays on compose; tasks reach it via the
|
|
# docker0 host gateway 172.17.0.1 and the host-published ports
|
|
# (15432/17379/5672/9000). The nginx edge cascade is unchanged.
|
|
#
|
|
# Secrets flow (nothing sensitive lives in this file):
|
|
# - app secrets + connection strings → Nomad Variables at
|
|
# nomad/jobs/contract-check (see deploy/nomad/README.md §5), rendered
|
|
# agent-side into env by `template` blocks;
|
|
# - image tag + APP_VERSION use ${IMAGE_TAG}, rendered by `envsubst` at
|
|
# submit time:
|
|
# IMAGE_TAG=<git-sha> envsubst < deploy/nomad/contract-check.nomad.hcl | nomad job run -
|
|
# - registry auth: host-level `docker login` on the VPS (README §5).
|
|
#
|
|
# First deploy: verify with `nomad job validate` + `nomad job plan`, then
|
|
# cutover workers one by one (README / docs/DEPLOY.md §15).
|
|
|
|
job "contract-check" {
|
|
datacenters = ["vps"]
|
|
type = "service"
|
|
|
|
# ── Rolling defaults for every group ────────────────────────────────────────
|
|
update {
|
|
max_parallel = 1
|
|
min_healthy_time = "15s"
|
|
healthy_deadline = "5m"
|
|
progress_deadline = "10m"
|
|
auto_revert = true
|
|
}
|
|
|
|
# ══ API ════════════════════════════════════════════════════════════════════
|
|
group "api" {
|
|
count = 1
|
|
|
|
# No canary here: the static host port (18000) cannot be bound twice on a
|
|
# single node. Rolling = brief seconds-level gap per deploy; switch to
|
|
# dynamic ports + Traefik if zero-downtime becomes a requirement.
|
|
update {
|
|
max_parallel = 1
|
|
}
|
|
|
|
restart {
|
|
attempts = 3
|
|
interval = "10m"
|
|
delay = "15s"
|
|
mode = "delay"
|
|
}
|
|
|
|
network {
|
|
port "http" {
|
|
static = 18000
|
|
to = 8000
|
|
}
|
|
}
|
|
|
|
service {
|
|
name = "contract-check-api"
|
|
port = "http"
|
|
provider = "nomad"
|
|
|
|
check {
|
|
name = "healthz"
|
|
type = "http"
|
|
path = "/healthz"
|
|
interval = "10s"
|
|
timeout = "3s"
|
|
}
|
|
|
|
check_restart {
|
|
limit = 3
|
|
grace = "30s"
|
|
}
|
|
}
|
|
|
|
# One-shot migrations before the API starts (replaces `make migrate`).
|
|
task "migrate" {
|
|
lifecycle {
|
|
hook = "prestart"
|
|
}
|
|
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}"
|
|
args = ["alembic", "upgrade", "head"]
|
|
|
|
# registry auth: host-level `docker login` on the VPS (README §5)
|
|
}
|
|
|
|
template {
|
|
destination = "secrets/env"
|
|
env = true
|
|
change_mode = "restart"
|
|
data = <<-EOT
|
|
{{ with nomadVar "nomad/jobs/contract-check" }}
|
|
DATABASE_URL="{{ .database_url }}"
|
|
{{ end }}
|
|
EOT
|
|
}
|
|
|
|
resources {
|
|
cpu = 150
|
|
memory = 256
|
|
}
|
|
}
|
|
|
|
task "api" {
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}"
|
|
ports = ["http"]
|
|
|
|
# registry auth: host-level `docker login` on the VPS (README §5)
|
|
}
|
|
|
|
env {
|
|
ENV = "prod"
|
|
LOG_LEVEL = "INFO"
|
|
LOG_FORMAT = "json"
|
|
APP_VERSION = "${IMAGE_TAG}"
|
|
}
|
|
|
|
# All connection strings + app secrets, agent-rendered from Nomad
|
|
# Variables. Missing keys render empty — define them all once (README §5).
|
|
template {
|
|
destination = "secrets/env"
|
|
env = true
|
|
change_mode = "restart"
|
|
data = <<-EOT
|
|
{{ with nomadVar "nomad/jobs/contract-check" }}
|
|
DATABASE_URL="{{ .database_url }}"
|
|
REDIS_URL="{{ .redis_url }}"
|
|
RABBITMQ_URL="{{ .rabbitmq_url }}"
|
|
S3_ENDPOINT_URL="{{ .s3_endpoint_url }}"
|
|
S3_ACCESS_KEY="{{ .s3_access_key }}"
|
|
S3_SECRET_KEY="{{ .s3_secret_key }}"
|
|
S3_BUCKET="{{ .s3_bucket }}"
|
|
JWT_SECRET="{{ .jwt_secret }}"
|
|
TELEGRAM_BOT_TOKEN="{{ .telegram_bot_token }}"
|
|
OLLAMA_API_KEY="{{ .ollama_api_key }}"
|
|
YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}"
|
|
SMTP_HOST="{{ .smtp_host }}"
|
|
SMTP_USERNAME="{{ .smtp_username }}"
|
|
SMTP_PASSWORD="{{ .smtp_password }}"
|
|
METRICS_BEARER_TOKEN="{{ .metrics_bearer_token }}"
|
|
{{ end }}
|
|
EOT
|
|
}
|
|
|
|
kill_timeout = "30s"
|
|
|
|
resources {
|
|
cpu = 300
|
|
memory = 512
|
|
}
|
|
}
|
|
}
|
|
|
|
# ══ WORKERS ═══════════════════════════════════════════════════════════════
|
|
# Workers bind no ports, so each group gets canary + auto_promote:
|
|
# new version starts alongside the old one, must pass min_healthy_time,
|
|
# then old allocations stop. auto_revert rolls back on failure.
|
|
# Scale with: nomad job scale contract-check worker-<name> <count>
|
|
|
|
group "worker-extract" {
|
|
count = 1
|
|
|
|
update {
|
|
canary = 1
|
|
auto_promote = true
|
|
}
|
|
|
|
restart {
|
|
attempts = 3
|
|
interval = "10m"
|
|
delay = "15s"
|
|
mode = "delay"
|
|
}
|
|
|
|
task "worker-extract" {
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-extract:${IMAGE_TAG}"
|
|
|
|
# registry auth: host-level `docker login` on the VPS (README §5)
|
|
}
|
|
|
|
env {
|
|
ENV = "prod"
|
|
LOG_LEVEL = "INFO"
|
|
LOG_FORMAT = "json"
|
|
APP_VERSION = "${IMAGE_TAG}"
|
|
}
|
|
|
|
template {
|
|
destination = "secrets/env"
|
|
env = true
|
|
change_mode = "restart"
|
|
data = <<-EOT
|
|
{{ with nomadVar "nomad/jobs/contract-check" }}
|
|
DATABASE_URL="{{ .database_url }}"
|
|
RABBITMQ_URL="{{ .rabbitmq_url }}"
|
|
S3_ENDPOINT_URL="{{ .s3_endpoint_url }}"
|
|
S3_ACCESS_KEY="{{ .s3_access_key }}"
|
|
S3_SECRET_KEY="{{ .s3_secret_key }}"
|
|
S3_BUCKET="{{ .s3_bucket }}"
|
|
OLLAMA_API_KEY="{{ .ollama_api_key }}"
|
|
YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}"
|
|
{{ end }}
|
|
EOT
|
|
}
|
|
|
|
kill_timeout = "60s"
|
|
|
|
resources {
|
|
# CPU-bound OCR (tesseract); the heaviest task of the pipeline.
|
|
cpu = 500
|
|
memory = 640
|
|
}
|
|
}
|
|
}
|
|
|
|
group "worker-analyze" {
|
|
count = 1
|
|
|
|
update {
|
|
canary = 1
|
|
auto_promote = true
|
|
}
|
|
|
|
restart {
|
|
attempts = 3
|
|
interval = "10m"
|
|
delay = "15s"
|
|
mode = "delay"
|
|
}
|
|
|
|
task "worker-analyze" {
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-analyze:${IMAGE_TAG}"
|
|
|
|
# registry auth: host-level `docker login` on the VPS (README §5)
|
|
}
|
|
|
|
env {
|
|
ENV = "prod"
|
|
LOG_LEVEL = "INFO"
|
|
LOG_FORMAT = "json"
|
|
APP_VERSION = "${IMAGE_TAG}"
|
|
}
|
|
|
|
template {
|
|
destination = "secrets/env"
|
|
env = true
|
|
change_mode = "restart"
|
|
data = <<-EOT
|
|
{{ with nomadVar "nomad/jobs/contract-check" }}
|
|
DATABASE_URL="{{ .database_url }}"
|
|
RABBITMQ_URL="{{ .rabbitmq_url }}"
|
|
S3_ENDPOINT_URL="{{ .s3_endpoint_url }}"
|
|
S3_ACCESS_KEY="{{ .s3_access_key }}"
|
|
S3_SECRET_KEY="{{ .s3_secret_key }}"
|
|
S3_BUCKET="{{ .s3_bucket }}"
|
|
OLLAMA_API_KEY="{{ .ollama_api_key }}"
|
|
YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}"
|
|
{{ end }}
|
|
EOT
|
|
}
|
|
|
|
kill_timeout = "120s"
|
|
|
|
resources {
|
|
# LLM calls (I/O bound, long in-flight requests on shutdown).
|
|
cpu = 250
|
|
memory = 384
|
|
}
|
|
}
|
|
}
|
|
|
|
group "worker-prescreen" {
|
|
count = 1
|
|
|
|
update {
|
|
canary = 1
|
|
auto_promote = true
|
|
}
|
|
|
|
restart {
|
|
attempts = 3
|
|
interval = "10m"
|
|
delay = "15s"
|
|
mode = "delay"
|
|
}
|
|
|
|
task "worker-prescreen" {
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-prescreen:${IMAGE_TAG}"
|
|
|
|
# registry auth: host-level `docker login` on the VPS (README §5)
|
|
}
|
|
|
|
env {
|
|
ENV = "prod"
|
|
LOG_LEVEL = "INFO"
|
|
LOG_FORMAT = "json"
|
|
APP_VERSION = "${IMAGE_TAG}"
|
|
}
|
|
|
|
template {
|
|
destination = "secrets/env"
|
|
env = true
|
|
change_mode = "restart"
|
|
data = <<-EOT
|
|
{{ with nomadVar "nomad/jobs/contract-check" }}
|
|
DATABASE_URL="{{ .database_url }}"
|
|
RABBITMQ_URL="{{ .rabbitmq_url }}"
|
|
S3_ENDPOINT_URL="{{ .s3_endpoint_url }}"
|
|
S3_ACCESS_KEY="{{ .s3_access_key }}"
|
|
S3_SECRET_KEY="{{ .s3_secret_key }}"
|
|
S3_BUCKET="{{ .s3_bucket }}"
|
|
OLLAMA_API_KEY="{{ .ollama_api_key }}"
|
|
YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}"
|
|
{{ end }}
|
|
EOT
|
|
}
|
|
|
|
kill_timeout = "60s"
|
|
|
|
resources {
|
|
cpu = 150
|
|
memory = 256
|
|
}
|
|
}
|
|
}
|
|
|
|
group "worker-billing" {
|
|
count = 1
|
|
|
|
update {
|
|
canary = 1
|
|
auto_promote = true
|
|
}
|
|
|
|
restart {
|
|
attempts = 3
|
|
interval = "10m"
|
|
delay = "15s"
|
|
mode = "delay"
|
|
}
|
|
|
|
task "worker-billing" {
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-billing:${IMAGE_TAG}"
|
|
|
|
# registry auth: host-level `docker login` on the VPS (README §5)
|
|
}
|
|
|
|
env {
|
|
ENV = "prod"
|
|
LOG_LEVEL = "INFO"
|
|
LOG_FORMAT = "json"
|
|
APP_VERSION = "${IMAGE_TAG}"
|
|
}
|
|
|
|
template {
|
|
destination = "secrets/env"
|
|
env = true
|
|
change_mode = "restart"
|
|
data = <<-EOT
|
|
{{ with nomadVar "nomad/jobs/contract-check" }}
|
|
DATABASE_URL="{{ .database_url }}"
|
|
{{ end }}
|
|
EOT
|
|
}
|
|
|
|
kill_timeout = "60s"
|
|
|
|
resources {
|
|
# Background DB-only worker (dunning, renewals); lightest of the set.
|
|
cpu = 150
|
|
memory = 256
|
|
}
|
|
}
|
|
}
|
|
|
|
group "worker-notify" {
|
|
count = 1
|
|
|
|
update {
|
|
canary = 1
|
|
auto_promote = true
|
|
}
|
|
|
|
restart {
|
|
attempts = 3
|
|
interval = "10m"
|
|
delay = "15s"
|
|
mode = "delay"
|
|
}
|
|
|
|
task "worker-notify" {
|
|
driver = "docker"
|
|
|
|
config {
|
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-notify:${IMAGE_TAG}"
|
|
|
|
# registry auth: host-level `docker login` on the VPS (README §5)
|
|
}
|
|
|
|
env {
|
|
ENV = "prod"
|
|
LOG_LEVEL = "INFO"
|
|
LOG_FORMAT = "json"
|
|
APP_VERSION = "${IMAGE_TAG}"
|
|
}
|
|
|
|
template {
|
|
destination = "secrets/env"
|
|
env = true
|
|
change_mode = "restart"
|
|
data = <<-EOT
|
|
{{ with nomadVar "nomad/jobs/contract-check" }}
|
|
DATABASE_URL="{{ .database_url }}"
|
|
RABBITMQ_URL="{{ .rabbitmq_url }}"
|
|
SMTP_HOST="{{ .smtp_host }}"
|
|
SMTP_USERNAME="{{ .smtp_username }}"
|
|
SMTP_PASSWORD="{{ .smtp_password }}"
|
|
{{ end }}
|
|
EOT
|
|
}
|
|
|
|
kill_timeout = "60s"
|
|
|
|
resources {
|
|
cpu = 150
|
|
memory = 256
|
|
}
|
|
}
|
|
}
|
|
}
|