DealDocumentScreening/deploy/nomad/contract-check.nomad.hcl
febux 1ae14536e6
Some checks failed
ci / Lint & typecheck (push) Successful in 30s
ci / Unit tests (push) Successful in 1m7s
deploy / build-and-deploy (push) Failing after 2m12s
Deploy job was fixed. Workflow was fixed.
2026-09-14 02:28:43 +03:00

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
}
}
}
}