nomad job: bind-mount compose .env (simpler than nomadVar templates);
enable docker volumes
This commit is contained in:
parent
1ae14536e6
commit
a264b20c1c
2 changed files with 159 additions and 223 deletions
|
|
@ -5,23 +5,19 @@
|
||||||
# docker0 host gateway 172.17.0.1 and the host-published ports
|
# docker0 host gateway 172.17.0.1 and the host-published ports
|
||||||
# (15432/17379/5672/9000). The nginx edge cascade is unchanged.
|
# (15432/17379/5672/9000). The nginx edge cascade is unchanged.
|
||||||
#
|
#
|
||||||
# Secrets flow (nothing sensitive lives in this file):
|
# Env: the compose .env file (~/DealDocumentScreening/.env on the VPS) is
|
||||||
# - app secrets + connection strings → Nomad Variables at
|
# bind-mounted into every container at /secrets/.env and sourced before the
|
||||||
# nomad/jobs/contract-check (see deploy/nomad/README.md §5), rendered
|
# app starts. A sed one-liner rewrites compose hostnames (postgres, redis,
|
||||||
# agent-side into env by `template` blocks;
|
# rabbitmq, minio) to 172.17.0.1 + published ports so the connections work
|
||||||
# - image tag + APP_VERSION use ${IMAGE_TAG}, rendered by `envsubst` at
|
# from outside the compose network.
|
||||||
# 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
|
# Image tag uses ${IMAGE_TAG}, rendered by envsubst at submit time.
|
||||||
# cutover workers one by one (README / docs/DEPLOY.md §15).
|
# Registry auth: host-level `docker login` on the VPS (README §5).
|
||||||
|
|
||||||
job "contract-check" {
|
job "contract-check" {
|
||||||
datacenters = ["vps"]
|
datacenters = ["vps"]
|
||||||
type = "service"
|
type = "service"
|
||||||
|
|
||||||
# ── Rolling defaults for every group ────────────────────────────────────────
|
|
||||||
update {
|
update {
|
||||||
max_parallel = 1
|
max_parallel = 1
|
||||||
min_healthy_time = "15s"
|
min_healthy_time = "15s"
|
||||||
|
|
@ -34,9 +30,6 @@ job "contract-check" {
|
||||||
group "api" {
|
group "api" {
|
||||||
count = 1
|
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 {
|
update {
|
||||||
max_parallel = 1
|
max_parallel = 1
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +67,7 @@ job "contract-check" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# One-shot migrations before the API starts (replaces `make migrate`).
|
# One-shot migrations before the API starts.
|
||||||
task "migrate" {
|
task "migrate" {
|
||||||
lifecycle {
|
lifecycle {
|
||||||
hook = "prestart"
|
hook = "prestart"
|
||||||
|
|
@ -84,20 +77,19 @@ job "contract-check" {
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}"
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}"
|
||||||
args = ["alembic", "upgrade", "head"]
|
command = "sh"
|
||||||
|
args = [
|
||||||
# registry auth: host-level `docker login` on the VPS (README §5)
|
"-c",
|
||||||
}
|
"set -a; . /secrets/.env; set +a; exec alembic upgrade head",
|
||||||
|
]
|
||||||
template {
|
mounts = [
|
||||||
destination = "secrets/env"
|
{
|
||||||
env = true
|
type = "bind"
|
||||||
change_mode = "restart"
|
target = "/secrets/.env"
|
||||||
data = <<-EOT
|
source = "/root/DealDocumentScreening/.env"
|
||||||
{{ with nomadVar "nomad/jobs/contract-check" }}
|
readonly = true
|
||||||
DATABASE_URL="{{ .database_url }}"
|
},
|
||||||
{{ end }}
|
]
|
||||||
EOT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resources {
|
resources {
|
||||||
|
|
@ -111,43 +103,28 @@ job "contract-check" {
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}"
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}"
|
||||||
|
command = "sh"
|
||||||
|
args = [
|
||||||
|
"-c",
|
||||||
|
<<-SH
|
||||||
|
set -a; . /secrets/.env; set +a
|
||||||
|
export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
|
||||||
|
export REDIS_URL="$${REDIS_URL/@redis:/@172.17.0.1:17379:}"
|
||||||
|
export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
|
||||||
|
export S3_ENDPOINT_URL="http://172.17.0.1:9000"
|
||||||
|
export APP_VERSION="${IMAGE_TAG}"
|
||||||
|
exec python -m src.contract_check.api
|
||||||
|
SH
|
||||||
|
]
|
||||||
ports = ["http"]
|
ports = ["http"]
|
||||||
|
mounts = [
|
||||||
# registry auth: host-level `docker login` on the VPS (README §5)
|
{
|
||||||
}
|
type = "bind"
|
||||||
|
target = "/secrets/.env"
|
||||||
env {
|
source = "/root/DealDocumentScreening/.env"
|
||||||
ENV = "prod"
|
readonly = true
|
||||||
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"
|
kill_timeout = "30s"
|
||||||
|
|
@ -160,10 +137,6 @@ job "contract-check" {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ══ WORKERS ═══════════════════════════════════════════════════════════════
|
# ══ 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" {
|
group "worker-extract" {
|
||||||
count = 1
|
count = 1
|
||||||
|
|
@ -185,39 +158,31 @@ job "contract-check" {
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-extract:${IMAGE_TAG}"
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-extract:${IMAGE_TAG}"
|
||||||
|
command = "sh"
|
||||||
# registry auth: host-level `docker login` on the VPS (README §5)
|
args = [
|
||||||
}
|
"-c",
|
||||||
|
<<-SH
|
||||||
env {
|
set -a; . /secrets/.env; set +a
|
||||||
ENV = "prod"
|
export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
|
||||||
LOG_LEVEL = "INFO"
|
export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
|
||||||
LOG_FORMAT = "json"
|
export S3_ENDPOINT_URL="http://172.17.0.1:9000"
|
||||||
APP_VERSION = "${IMAGE_TAG}"
|
export APP_VERSION="${IMAGE_TAG}"
|
||||||
}
|
exec python -m src.contract_check.worker_extract
|
||||||
|
SH
|
||||||
template {
|
]
|
||||||
destination = "secrets/env"
|
mounts = [
|
||||||
env = true
|
{
|
||||||
change_mode = "restart"
|
type = "bind"
|
||||||
data = <<-EOT
|
target = "/secrets/.env"
|
||||||
{{ with nomadVar "nomad/jobs/contract-check" }}
|
source = "/root/DealDocumentScreening/.env"
|
||||||
DATABASE_URL="{{ .database_url }}"
|
readonly = true
|
||||||
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"
|
kill_timeout = "60s"
|
||||||
|
|
||||||
resources {
|
resources {
|
||||||
# CPU-bound OCR (tesseract); the heaviest task of the pipeline.
|
|
||||||
cpu = 500
|
cpu = 500
|
||||||
memory = 640
|
memory = 640
|
||||||
}
|
}
|
||||||
|
|
@ -244,39 +209,31 @@ job "contract-check" {
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-analyze:${IMAGE_TAG}"
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-analyze:${IMAGE_TAG}"
|
||||||
|
command = "sh"
|
||||||
# registry auth: host-level `docker login` on the VPS (README §5)
|
args = [
|
||||||
}
|
"-c",
|
||||||
|
<<-SH
|
||||||
env {
|
set -a; . /secrets/.env; set +a
|
||||||
ENV = "prod"
|
export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
|
||||||
LOG_LEVEL = "INFO"
|
export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
|
||||||
LOG_FORMAT = "json"
|
export S3_ENDPOINT_URL="http://172.17.0.1:9000"
|
||||||
APP_VERSION = "${IMAGE_TAG}"
|
export APP_VERSION="${IMAGE_TAG}"
|
||||||
}
|
exec python -m src.contract_check.worker_analyze
|
||||||
|
SH
|
||||||
template {
|
]
|
||||||
destination = "secrets/env"
|
mounts = [
|
||||||
env = true
|
{
|
||||||
change_mode = "restart"
|
type = "bind"
|
||||||
data = <<-EOT
|
target = "/secrets/.env"
|
||||||
{{ with nomadVar "nomad/jobs/contract-check" }}
|
source = "/root/DealDocumentScreening/.env"
|
||||||
DATABASE_URL="{{ .database_url }}"
|
readonly = true
|
||||||
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"
|
kill_timeout = "120s"
|
||||||
|
|
||||||
resources {
|
resources {
|
||||||
# LLM calls (I/O bound, long in-flight requests on shutdown).
|
|
||||||
cpu = 250
|
cpu = 250
|
||||||
memory = 384
|
memory = 384
|
||||||
}
|
}
|
||||||
|
|
@ -303,33 +260,26 @@ job "contract-check" {
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-prescreen:${IMAGE_TAG}"
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-prescreen:${IMAGE_TAG}"
|
||||||
|
command = "sh"
|
||||||
# registry auth: host-level `docker login` on the VPS (README §5)
|
args = [
|
||||||
}
|
"-c",
|
||||||
|
<<-SH
|
||||||
env {
|
set -a; . /secrets/.env; set +a
|
||||||
ENV = "prod"
|
export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
|
||||||
LOG_LEVEL = "INFO"
|
export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
|
||||||
LOG_FORMAT = "json"
|
export S3_ENDPOINT_URL="http://172.17.0.1:9000"
|
||||||
APP_VERSION = "${IMAGE_TAG}"
|
export APP_VERSION="${IMAGE_TAG}"
|
||||||
}
|
exec python -m src.contract_check.worker_prescreen
|
||||||
|
SH
|
||||||
template {
|
]
|
||||||
destination = "secrets/env"
|
mounts = [
|
||||||
env = true
|
{
|
||||||
change_mode = "restart"
|
type = "bind"
|
||||||
data = <<-EOT
|
target = "/secrets/.env"
|
||||||
{{ with nomadVar "nomad/jobs/contract-check" }}
|
source = "/root/DealDocumentScreening/.env"
|
||||||
DATABASE_URL="{{ .database_url }}"
|
readonly = true
|
||||||
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"
|
kill_timeout = "60s"
|
||||||
|
|
@ -361,32 +311,29 @@ job "contract-check" {
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-billing:${IMAGE_TAG}"
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-billing:${IMAGE_TAG}"
|
||||||
|
command = "sh"
|
||||||
# registry auth: host-level `docker login` on the VPS (README §5)
|
args = [
|
||||||
}
|
"-c",
|
||||||
|
<<-SH
|
||||||
env {
|
set -a; . /secrets/.env; set +a
|
||||||
ENV = "prod"
|
export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
|
||||||
LOG_LEVEL = "INFO"
|
export APP_VERSION="${IMAGE_TAG}"
|
||||||
LOG_FORMAT = "json"
|
exec python -m src.contract_check.worker_billing
|
||||||
APP_VERSION = "${IMAGE_TAG}"
|
SH
|
||||||
}
|
]
|
||||||
|
mounts = [
|
||||||
template {
|
{
|
||||||
destination = "secrets/env"
|
type = "bind"
|
||||||
env = true
|
target = "/secrets/.env"
|
||||||
change_mode = "restart"
|
source = "/root/DealDocumentScreening/.env"
|
||||||
data = <<-EOT
|
readonly = true
|
||||||
{{ with nomadVar "nomad/jobs/contract-check" }}
|
},
|
||||||
DATABASE_URL="{{ .database_url }}"
|
]
|
||||||
{{ end }}
|
|
||||||
EOT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kill_timeout = "60s"
|
kill_timeout = "60s"
|
||||||
|
|
||||||
resources {
|
resources {
|
||||||
# Background DB-only worker (dunning, renewals); lightest of the set.
|
|
||||||
cpu = 150
|
cpu = 150
|
||||||
memory = 256
|
memory = 256
|
||||||
}
|
}
|
||||||
|
|
@ -413,30 +360,25 @@ job "contract-check" {
|
||||||
|
|
||||||
config {
|
config {
|
||||||
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-notify:${IMAGE_TAG}"
|
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-notify:${IMAGE_TAG}"
|
||||||
|
command = "sh"
|
||||||
# registry auth: host-level `docker login` on the VPS (README §5)
|
args = [
|
||||||
}
|
"-c",
|
||||||
|
<<-SH
|
||||||
env {
|
set -a; . /secrets/.env; set +a
|
||||||
ENV = "prod"
|
export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
|
||||||
LOG_LEVEL = "INFO"
|
export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
|
||||||
LOG_FORMAT = "json"
|
export APP_VERSION="${IMAGE_TAG}"
|
||||||
APP_VERSION = "${IMAGE_TAG}"
|
exec python -m src.contract_check.worker_notify
|
||||||
}
|
SH
|
||||||
|
]
|
||||||
template {
|
mounts = [
|
||||||
destination = "secrets/env"
|
{
|
||||||
env = true
|
type = "bind"
|
||||||
change_mode = "restart"
|
target = "/secrets/.env"
|
||||||
data = <<-EOT
|
source = "/root/DealDocumentScreening/.env"
|
||||||
{{ with nomadVar "nomad/jobs/contract-check" }}
|
readonly = true
|
||||||
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"
|
kill_timeout = "60s"
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,11 @@ client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Docker driver is intentionally left at defaults:
|
# Docker driver: host bind mounts enabled so tasks can source /root's .env.
|
||||||
# - host bind mounts are DISABLED (docker.volumes.enabled=false by default)
|
plugin "docker" {
|
||||||
# — tasks are 12-factor (env-only config), no volumes required;
|
config {
|
||||||
# - Nomad garbage-collects unused images periodically — good for the
|
volumes {
|
||||||
# 50 GB NVMe.
|
enabled = true
|
||||||
# Enable only if a task ever needs a bind mount:
|
}
|
||||||
#
|
}
|
||||||
# plugin "docker" {
|
}
|
||||||
# config {
|
|
||||||
# volumes {
|
|
||||||
# enabled = true
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue