Forgejo workflows were added. Nomad config was created.
Some checks are pending
ci / test (push) Waiting to run
Some checks are pending
ci / test (push) Waiting to run
This commit is contained in:
parent
0c354492ef
commit
c64f8ac3b7
7 changed files with 1019 additions and 0 deletions
43
.forgejo/workflows/ci.yml
Normal file
43
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# CI: lint + typecheck + unit tests on every branch / PR.
|
||||||
|
#
|
||||||
|
# Self-contained on purpose: python:3.14-trixie has git built in, so no
|
||||||
|
# node/actions-checkout dependency; uv comes from PyPI (both reachable from
|
||||||
|
# the runner network). Mirrors the Makefile lint/test-unit targets.
|
||||||
|
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["**"]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: python:3.14-trixie
|
||||||
|
steps:
|
||||||
|
- name: Checkout (sha-pinned, no node actions needed)
|
||||||
|
run: |
|
||||||
|
git init -q .
|
||||||
|
git remote add origin ${{ gitea.server_url }}/${{ gitea.repository }}.git
|
||||||
|
git fetch --depth 1 origin ${{ gitea.sha }}
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
run: pip install --quiet uv
|
||||||
|
|
||||||
|
- name: Sync dev dependencies
|
||||||
|
run: uv sync --group dev --frozen
|
||||||
|
|
||||||
|
- name: Lint (ruff)
|
||||||
|
run: |
|
||||||
|
uv run ruff check src tests
|
||||||
|
uv run ruff format --check src tests
|
||||||
|
uv run isort --check-only src tests
|
||||||
|
|
||||||
|
- name: Typecheck (ty)
|
||||||
|
run: uv run ty check src
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: uv run pytest -m "not integration" tests/unit
|
||||||
76
.forgejo/workflows/deploy.yml
Normal file
76
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Deploy: build → push → nomad job run, on every push to main.
|
||||||
|
#
|
||||||
|
# Runner requirements (verify on the Forgejo VPS):
|
||||||
|
# - job container gets the docker socket (runner config must allow
|
||||||
|
# valid_volumes for /var/run/docker.sock — see README §deploy notes), or
|
||||||
|
# switch `runs-on` to the host label and drop the container/socket bits;
|
||||||
|
# - Docker Hub reachable (nomad CLI is extracted from hashicorp/nomad:1.9 —
|
||||||
|
# releases.hashicorp.com is blocked from this network).
|
||||||
|
#
|
||||||
|
# Repo secrets (Settings → Secrets):
|
||||||
|
# REGISTRY_TOKEN forgejo token, write:package scope (docker login)
|
||||||
|
# NOMAD_TOKEN CI ACL token from deploy/nomad/README.md §5
|
||||||
|
# NOMAD_CACERT contents of nomad-ca.crt from README §3
|
||||||
|
# Repo variables (Settings → Variables) or edit the env block below:
|
||||||
|
# REGISTRY_HOST e.g. git.example.com
|
||||||
|
# REGISTRY_OWNER forgejo user/org owning the images
|
||||||
|
# NOMAD_ADDR_HOST services VPS public IP or DNS name (API :4646)
|
||||||
|
|
||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY_HOST: ${{ vars.REGISTRY_HOST }}
|
||||||
|
REGISTRY_OWNER: ${{ vars.REGISTRY_OWNER }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: docker:28-cli
|
||||||
|
options: --volume /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
run: |
|
||||||
|
apk add --no-cache git
|
||||||
|
git init -q .
|
||||||
|
git remote add origin ${{ gitea.server_url }}/${{ gitea.repository }}.git
|
||||||
|
git fetch --depth 1 origin ${{ gitea.sha }}
|
||||||
|
git checkout -q FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Extract nomad CLI (official image; CDN is blocked here)
|
||||||
|
run: |
|
||||||
|
docker create --name nomad-extract hashicorp/nomad:1.9 >/dev/null
|
||||||
|
docker cp nomad-extract:/bin/nomad /usr/local/bin/nomad
|
||||||
|
docker rm nomad-extract >/dev/null
|
||||||
|
nomad version
|
||||||
|
|
||||||
|
- name: Registry login
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | \
|
||||||
|
docker login "$REGISTRY_HOST" -u "$REGISTRY_OWNER" --password-stdin
|
||||||
|
|
||||||
|
- name: Build & push images (SHA tag)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
TAG="${GITEA_SHA}"
|
||||||
|
for name in api worker-extract worker-analyze worker-prescreen worker-billing worker-notify; do
|
||||||
|
image="$REGISTRY_HOST/$REGISTRY_OWNER/contract-check-$name"
|
||||||
|
docker build -f "srv/$name/Dockerfile" -t "$image:$TAG" .
|
||||||
|
docker push "$image:$TAG"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Deploy to Nomad
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/nomad-tls
|
||||||
|
echo "${{ secrets.NOMAD_CACERT }}" > /tmp/nomad-tls/ca.crt
|
||||||
|
export IMAGE_TAG="${GITEA_SHA}"
|
||||||
|
export NOMAD_ADDR="https://${{ vars.NOMAD_ADDR_HOST }}:4646"
|
||||||
|
export NOMAD_CACERT=/tmp/nomad-tls/ca.crt
|
||||||
|
export NOMAD_TOKEN="${{ secrets.NOMAD_TOKEN }}"
|
||||||
|
nomad job run deploy/nomad/contract-check.nomad.hcl
|
||||||
|
# the CLI follows the deployment and exits non-zero if health
|
||||||
|
# checks fail — auto_revert then rolls the job back server-side.
|
||||||
256
deploy/nomad/README.md
Normal file
256
deploy/nomad/README.md
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
# Nomad on the services VPS — install & operations runbook
|
||||||
|
|
||||||
|
Single-node Nomad agent (server+client) running the app services
|
||||||
|
(`api` + 5 workers) while stateful infra (postgres, redis, rabbitmq, minio),
|
||||||
|
the nginx edge cascade, and observability stay on docker compose.
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/nomad/
|
||||||
|
├── nomad.hcl # agent config → /etc/nomad.d/nomad.hcl
|
||||||
|
├── policies/ci.hcl # ACL policy for the Forgejo deploy token
|
||||||
|
├── contract-check.nomad.hcl # job: api + worker groups
|
||||||
|
└── README.md # this runbook
|
||||||
|
```
|
||||||
|
|
||||||
|
Placeholders used below — substitute before running:
|
||||||
|
|
||||||
|
| Placeholder | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `<VPS_PUBLIC_IP>` | public IP of the services VPS |
|
||||||
|
| `<FORGEJO_SERVER_IP>` | public IP of the Forgejo VPS (runner) |
|
||||||
|
| `<FORGEJO_HOST>` | Forgejo hostname, e.g. `git.example.com` |
|
||||||
|
| `<OWNER>` | Forgejo user/org owning the repo + images |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Install Nomad
|
||||||
|
|
||||||
|
### 1a. Manual install via the official Docker image
|
||||||
|
|
||||||
|
The services VPS network 404s `apt.releases.hashicorp.com`,
|
||||||
|
`releases.hashicorp.com` and GitHub release assets, and Ubuntu `resolute`
|
||||||
|
is newer than the apt repo's suites — so extract the binary from the
|
||||||
|
official Docker image instead (Docker Hub is reachable there):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull hashicorp/nomad:1.9
|
||||||
|
docker create --name nomad-extract hashicorp/nomad:1.9
|
||||||
|
docker cp nomad-extract:/bin/nomad /tmp/nomad
|
||||||
|
docker rm nomad-extract
|
||||||
|
chmod +x /tmp/nomad && /tmp/nomad version
|
||||||
|
sudo install -m 0755 /tmp/nomad /usr/local/bin/nomad
|
||||||
|
# if the cp path is wrong:
|
||||||
|
# docker run --rm --entrypoint sh hashicorp/nomad:1.9 -c 'command -v nomad'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why 1.9.x and not 2.x:** Nomad 2.x extracts the docker driver into an
|
||||||
|
external plugin distributed via the blocked hosts above. 1.9.x has it
|
||||||
|
built in — everything this stack uses (nomadVar templates, native
|
||||||
|
services, static ports, canary updates) is fully supported there.
|
||||||
|
|
||||||
|
No `.deb` ⇒ no systemd unit ships; install ours:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp deploy/nomad/nomad.service /etc/systemd/system/nomad.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1b. apt install (only on networks that reach HashiCorp's repo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget -O- https://apt.releases.hashicorp.com/gpg | \
|
||||||
|
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
|
||||||
|
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/hashicorp.list
|
||||||
|
sudo apt-get update && sudo apt-get install -y nomad
|
||||||
|
nomad version # expect 1.7+
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the `.deb`'s own unit lives in /lib/systemd/system — do NOT install
|
||||||
|
`deploy/nomad/nomad.service` in that case.
|
||||||
|
|
||||||
|
## 2. Install config + data dir
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /var/lib/nomad /etc/nomad.d/tls
|
||||||
|
# from the repo root on the VPS:
|
||||||
|
sudo cp deploy/nomad/nomad.hcl /etc/nomad.d/nomad.hcl
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Generate TLS material
|
||||||
|
|
||||||
|
The API is reachable from the internet (Forgejo runner → VPS), so TLS is
|
||||||
|
mandatory. One mini-CA + one server cert; the runner gets **only** the CA
|
||||||
|
cert.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /tmp && mkdir nomad-tls && cd nomad-tls
|
||||||
|
|
||||||
|
# CA (keep nomad-ca.key offline afterwards; only the .crt is ever copied)
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||||
|
-keyout nomad-ca.key -out nomad-ca.crt -subj "/CN=Contract-Check Nomad CA"
|
||||||
|
|
||||||
|
# server cert — SANs must cover the RPC hostname AND how clients reach it
|
||||||
|
openssl req -newkey rsa:2048 -nodes \
|
||||||
|
-keyout server.key -out server.csr \
|
||||||
|
-subj "/CN=server.global.vps.nomad"
|
||||||
|
|
||||||
|
cat > server.ext <<'EOF'
|
||||||
|
subjectAltName = DNS:server.global.vps.nomad, DNS:localhost, IP:127.0.0.1, IP:<VPS_PUBLIC_IP>
|
||||||
|
extendedKeyUsage = serverAuth, clientAuth
|
||||||
|
EOF
|
||||||
|
|
||||||
|
openssl x509 -req -in server.csr \
|
||||||
|
-CA nomad-ca.crt -CAkey nomad-ca.key -CAcreateserial \
|
||||||
|
-out server.crt -days 825 -extfile server.ext
|
||||||
|
|
||||||
|
sudo install -m 644 nomad-ca.crt server.crt /etc/nomad.d/tls/
|
||||||
|
sudo install -m 600 server.key /etc/nomad.d/tls/
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `nomad-ca.crt` to the Forgejo VPS (or a repo secret / secure storage)
|
||||||
|
— the deploy workflow needs it as `NOMAD_CACERT`.
|
||||||
|
|
||||||
|
## 4. Start the agent + bootstrap ACLs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable --now nomad
|
||||||
|
systemctl status nomad --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
For every CLI call on the VPS, export:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export NOMAD_ADDR=https://127.0.0.1:4646
|
||||||
|
export NOMAD_CACERT=/etc/nomad.d/tls/nomad-ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
Bootstrap ACLs once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nomad acl bootstrap
|
||||||
|
# → prints an AccessorID/SecretID management token. Store the SecretID in a
|
||||||
|
# password manager; export it for the following commands:
|
||||||
|
export NOMAD_TOKEN=<management-secret-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
Sanity checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nomad server members # self as leader
|
||||||
|
nomad node status # client ready, docker driver detected
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. CI policy + token + registry variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# policy for the deploy token
|
||||||
|
nomad acl policy apply -description "Forgejo CI deploy" ci \
|
||||||
|
deploy/nomad/policies/ci.hcl
|
||||||
|
|
||||||
|
# long-lived token for the runner (no TTL)
|
||||||
|
nomad acl token create -name forgejo-ci -policy ci
|
||||||
|
# → AccessorID + SecretID. Forgejo repo secret: NOMAD_TOKEN = SecretID
|
||||||
|
```
|
||||||
|
|
||||||
|
Registry credentials and app secrets — the job file templates ALL of these
|
||||||
|
from `nomad/jobs/contract-check` (missing keys render empty; define every
|
||||||
|
key once). Connection strings point at the docker0 host gateway
|
||||||
|
`172.17.0.1` and the host-published infra ports (15432/17379/5672/9000) —
|
||||||
|
substitute real credentials where the compose `.env` deviates from
|
||||||
|
defaults:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nomad var put -in=json nomad/jobs/contract-check - <<'EOF'
|
||||||
|
{
|
||||||
|
"registry_host": "<FORGEJO_HOST>",
|
||||||
|
"registry_owner": "<OWNER>",
|
||||||
|
"registry_user": "<OWNER>",
|
||||||
|
"registry_token": "<forgejo token with write:package scope>",
|
||||||
|
|
||||||
|
"database_url": "postgresql+asyncpg://contract_check:<POSTGRES_PASSWORD>@172.17.0.1:15432/contract_check",
|
||||||
|
"redis_url": "redis://172.17.0.1:17379/0",
|
||||||
|
"rabbitmq_url": "amqp://contract_check:<RABBITMQ_PASS>@172.17.0.1:5672/",
|
||||||
|
"s3_endpoint_url": "http://172.17.0.1:9000",
|
||||||
|
"s3_access_key": "<S3_ACCESS_KEY>",
|
||||||
|
"s3_secret_key": "<S3_SECRET_KEY>",
|
||||||
|
"s3_bucket": "contract-check-docs",
|
||||||
|
|
||||||
|
"jwt_secret": "<openssl rand -hex 32 — reuse the value from compose .env>",
|
||||||
|
"telegram_bot_token": "<TELEGRAM_BOT_TOKEN>",
|
||||||
|
"ollama_api_key": "<OLLAMA_API_KEY>",
|
||||||
|
"yandexgpt_api_key": "<YANDEXGPT_API_KEY or empty>",
|
||||||
|
"smtp_host": "<SMTP_HOST or empty>",
|
||||||
|
"smtp_username": "<SMTP_USERNAME or empty>",
|
||||||
|
"smtp_password": "<SMTP_PASSWORD or empty>",
|
||||||
|
"metrics_bearer_token": "<METRICS_BEARER_TOKEN or empty>"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
nomad var get nomad/jobs/contract-check # sanity check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Firewall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 4646: only the Forgejo runner may talk to the API
|
||||||
|
sudo ufw allow from <FORGEJO_SERVER_IP> to any port 4646 proto tcp \
|
||||||
|
comment 'nomad http (forgejo runner)'
|
||||||
|
# single-node: no external rpc/gossip peers
|
||||||
|
sudo ufw deny 4647/tcp comment 'nomad rpc (local only)'
|
||||||
|
sudo ufw deny 4648/tcp comment 'nomad gossip (local only)'
|
||||||
|
sudo ufw status verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
Not using ufw? Apply the equivalent (443/80 stay as-is; only 4646 needs a
|
||||||
|
source restriction) in iptables/nft/cloud-secgroup.
|
||||||
|
|
||||||
|
## 7. Web UI
|
||||||
|
|
||||||
|
The UI has no login of its own — it rides on TLS+ACL, and neither should be
|
||||||
|
public. Reach it through an SSH tunnel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -L 4646:127.0.0.1:4646 <vps>
|
||||||
|
# browser: https://localhost:4646/ui (self-signed warning is expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Everyday operations
|
||||||
|
|
||||||
|
First submission (the job file requires IMAGE_TAG — it is rendered by the
|
||||||
|
CLI at submit time; CI exports it automatically):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
IMAGE_TAG=<git-sha> nomad job validate deploy/nomad/contract-check.nomad.hcl
|
||||||
|
IMAGE_TAG=<git-sha> nomad job plan deploy/nomad/contract-check.nomad.hcl
|
||||||
|
IMAGE_TAG=<git-sha> nomad job run deploy/nomad/contract-check.nomad.hcl
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<FORGEJO_HOST>`/`<OWNER>` in the job file image paths first.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nomad job status contract-check # groups, allocs, deployments
|
||||||
|
nomad alloc logs -f <alloc-id> # or: nomad logs -f contract-check
|
||||||
|
nomad job scale contract-check worker-extract 3
|
||||||
|
nomad deployment list / promote / rollback <deployment-id>
|
||||||
|
nomad job revert contract-check <prior-job-version>
|
||||||
|
```
|
||||||
|
|
||||||
|
Health gates: the api group checks `/healthz`; every group sets
|
||||||
|
`update { auto_revert = true }` — a deployment that turns unhealthy rolls
|
||||||
|
back on its own.
|
||||||
|
|
||||||
|
## 9. Failure behavior (single server)
|
||||||
|
|
||||||
|
- Agent process dies → containers keep running untouched; deploys/scaling
|
||||||
|
impossible until `systemctl restart nomad`. Data in `/var/lib/nomad`
|
||||||
|
survives restarts.
|
||||||
|
- `systemctl status nomad`, `journalctl -u nomad -f` for diagnostics.
|
||||||
|
|
||||||
|
## 10. Image retention
|
||||||
|
|
||||||
|
Every merge pushes 6 SHA-tagged images to the Forgejo registry. Periodically
|
||||||
|
prune old tags (Forgejo UI → Packages, or the API), or keep `latest` + the
|
||||||
|
last few SHAs. Nomad also GCs unused images on the VPS automatically.
|
||||||
527
deploy/nomad/contract-check.nomad.hcl
Normal file
527
deploy/nomad/contract-check.nomad.hcl
Normal file
|
|
@ -0,0 +1,527 @@
|
||||||
|
# «Контракт-чек» 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):
|
||||||
|
# - registry creds + all app secrets → Nomad Variables at
|
||||||
|
# nomad/jobs/contract-check (see deploy/nomad/README.md §5), rendered
|
||||||
|
# agent-side into env / auth files by `template` blocks;
|
||||||
|
# - image tag is injected CLI-side at submit time:
|
||||||
|
# IMAGE_TAG=<git-sha> nomad job run deploy/nomad/contract-check.nomad.hcl
|
||||||
|
#
|
||||||
|
# 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 = "<FORGEJO_HOST>/<OWNER>/contract-check-api:{{ mustEnv \"IMAGE_TAG\" }}"
|
||||||
|
args = ["alembic", "upgrade", "head"]
|
||||||
|
|
||||||
|
auth {
|
||||||
|
usernameFile = "secrets/registry-user"
|
||||||
|
passwordFile = "secrets/registry-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-user"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_user }}{{ end }}"
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-pass"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_token }}{{ end }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = "<FORGEJO_HOST>/<OWNER>/contract-check-api:{{ mustEnv \"IMAGE_TAG\" }}"
|
||||||
|
ports = ["http"]
|
||||||
|
|
||||||
|
auth {
|
||||||
|
usernameFile = "secrets/registry-user"
|
||||||
|
passwordFile = "secrets/registry-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
ENV = "prod"
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
LOG_FORMAT = "json"
|
||||||
|
APP_VERSION = "{{ env \"IMAGE_TAG\" }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-user"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_user }}{{ end }}"
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-pass"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_token }}{{ end }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 = "<FORGEJO_HOST>/<OWNER>/contract-check-worker-extract:{{ mustEnv \"IMAGE_TAG\" }}"
|
||||||
|
|
||||||
|
auth {
|
||||||
|
usernameFile = "secrets/registry-user"
|
||||||
|
passwordFile = "secrets/registry-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
ENV = "prod"
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
LOG_FORMAT = "json"
|
||||||
|
APP_VERSION = "{{ env \"IMAGE_TAG\" }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-user"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_user }}{{ end }}"
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-pass"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_token }}{{ end }}"
|
||||||
|
}
|
||||||
|
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 = "<FORGEJO_HOST>/<OWNER>/contract-check-worker-analyze:{{ mustEnv \"IMAGE_TAG\" }}"
|
||||||
|
|
||||||
|
auth {
|
||||||
|
usernameFile = "secrets/registry-user"
|
||||||
|
passwordFile = "secrets/registry-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
ENV = "prod"
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
LOG_FORMAT = "json"
|
||||||
|
APP_VERSION = "{{ env \"IMAGE_TAG\" }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-user"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_user }}{{ end }}"
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-pass"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_token }}{{ end }}"
|
||||||
|
}
|
||||||
|
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 = "<FORGEJO_HOST>/<OWNER>/contract-check-worker-prescreen:{{ mustEnv \"IMAGE_TAG\" }}"
|
||||||
|
|
||||||
|
auth {
|
||||||
|
usernameFile = "secrets/registry-user"
|
||||||
|
passwordFile = "secrets/registry-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
ENV = "prod"
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
LOG_FORMAT = "json"
|
||||||
|
APP_VERSION = "{{ env \"IMAGE_TAG\" }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-user"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_user }}{{ end }}"
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-pass"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_token }}{{ end }}"
|
||||||
|
}
|
||||||
|
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 = "<FORGEJO_HOST>/<OWNER>/contract-check-worker-billing:{{ mustEnv \"IMAGE_TAG\" }}"
|
||||||
|
|
||||||
|
auth {
|
||||||
|
usernameFile = "secrets/registry-user"
|
||||||
|
passwordFile = "secrets/registry-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
ENV = "prod"
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
LOG_FORMAT = "json"
|
||||||
|
APP_VERSION = "{{ env \"IMAGE_TAG\" }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-user"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_user }}{{ end }}"
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-pass"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_token }}{{ end }}"
|
||||||
|
}
|
||||||
|
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 = "<FORGEJO_HOST>/<OWNER>/contract-check-worker-notify:{{ mustEnv \"IMAGE_TAG\" }}"
|
||||||
|
|
||||||
|
auth {
|
||||||
|
usernameFile = "secrets/registry-user"
|
||||||
|
passwordFile = "secrets/registry-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
ENV = "prod"
|
||||||
|
LOG_LEVEL = "INFO"
|
||||||
|
LOG_FORMAT = "json"
|
||||||
|
APP_VERSION = "{{ env \"IMAGE_TAG\" }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-user"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_user }}{{ end }}"
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
destination = "secrets/registry-pass"
|
||||||
|
data = "{{ with nomadVar \"nomad/jobs/contract-check\" }}{{ .registry_token }}{{ end }}"
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
deploy/nomad/nomad.hcl
Normal file
74
deploy/nomad/nomad.hcl
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Single-node Nomad agent for «Контракт-чек» (services VPS).
|
||||||
|
#
|
||||||
|
# One process = scheduler (server) + docker workload host (client).
|
||||||
|
# Install & bootstrap runbook: deploy/nomad/README.md.
|
||||||
|
#
|
||||||
|
# Target location on the VPS: /etc/nomad.d/nomad.hcl
|
||||||
|
|
||||||
|
datacenter = "vps"
|
||||||
|
region = "global"
|
||||||
|
data_dir = "/var/lib/nomad"
|
||||||
|
log_level = "INFO"
|
||||||
|
|
||||||
|
# The agent binds 4646 (http) / 4647 (rpc) / 4648 (gossip) on 0.0.0.0 by
|
||||||
|
# default. The host firewall MUST restrict them (see README §6):
|
||||||
|
# - 4646 tcp: allow ONLY the Forgejo server IP (runner deploys)
|
||||||
|
# - 4647/4648: single-node cluster, no external peers — keep blocked.
|
||||||
|
|
||||||
|
# Point-to-point TLS: the Forgejo runner reaches the API over the public
|
||||||
|
# internet. Self-signed CA (README §3); the runner only needs nomad-ca.crt.
|
||||||
|
tls {
|
||||||
|
http = true
|
||||||
|
rpc = true
|
||||||
|
|
||||||
|
ca_file = "/etc/nomad.d/tls/nomad-ca.crt"
|
||||||
|
cert_file = "/etc/nomad.d/tls/server.crt"
|
||||||
|
key_file = "/etc/nomad.d/tls/server.key"
|
||||||
|
|
||||||
|
# Requires the server cert SAN to carry server.global.vps.nomad
|
||||||
|
# (region.region/datacenter convention) — the README cert command adds it.
|
||||||
|
verify_server_hostname = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Anonymous requests are rejected; every client needs a token.
|
||||||
|
acl {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
# Single-server cluster: acceptable for one VPS. If the agent is down,
|
||||||
|
# running allocations keep running (docker does not kill them) — only
|
||||||
|
# scheduling/deployments pause until it is back.
|
||||||
|
enabled = true
|
||||||
|
bootstrap_expect = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
client {
|
||||||
|
enabled = true
|
||||||
|
# Docker driver is auto-detected. Tasks pull images from the Forgejo
|
||||||
|
# registry via per-task `auth {}` templated from Nomad Variables
|
||||||
|
# (nomad/jobs/contract-check) — no host-level `docker login` needed.
|
||||||
|
|
||||||
|
# The compose stack (postgres, rabbitmq, minio, redis, nginx, observability)
|
||||||
|
# runs OUTSIDE Nomad but on the same 4 GB VPS. Reserve its share so the
|
||||||
|
# scheduler only bin-packs what is actually left (~2.5 GB / ~1.6 GHz).
|
||||||
|
reserved {
|
||||||
|
cpu = 400
|
||||||
|
memory = 1536
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker driver is intentionally left at defaults:
|
||||||
|
# - host bind mounts are DISABLED (docker.volumes.enabled=false by default)
|
||||||
|
# — tasks are 12-factor (env-only config), no volumes required;
|
||||||
|
# - Nomad garbage-collects unused images periodically — good for the
|
||||||
|
# 50 GB NVMe.
|
||||||
|
# Enable only if a task ever needs a bind mount:
|
||||||
|
#
|
||||||
|
# plugin "docker" {
|
||||||
|
# config {
|
||||||
|
# volumes {
|
||||||
|
# enabled = true
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
22
deploy/nomad/nomad.service
Normal file
22
deploy/nomad/nomad.service
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# systemd unit for a MANUAL Nomad install (GitHub releases route, README §1b).
|
||||||
|
# The HashiCorp .deb ships an equivalent unit at /lib/systemd/system/nomad.service —
|
||||||
|
# do NOT install this file if nomad was installed via apt.
|
||||||
|
#
|
||||||
|
# Target location: /etc/systemd/system/nomad.service
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Nomad
|
||||||
|
Documentation=https://developer.hashicorp.com/nomad/docs
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# agent reads /etc/nomad.d/*.hcl (deploy/nomad/nomad.hcl copied there)
|
||||||
|
ExecStart=/usr/local/bin/nomad agent -config /etc/nomad.d/
|
||||||
|
ExecReload=/bin/kill -HUP $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
LimitNOFILE=65536
|
||||||
|
# Runs as root on purpose: the docker driver needs the docker socket.
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
21
deploy/nomad/policies/ci.hcl
Normal file
21
deploy/nomad/policies/ci.hcl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# ACL policy for the Forgejo Actions deploy token.
|
||||||
|
#
|
||||||
|
# Scopes: read the default namespace (job list / deployment status polling),
|
||||||
|
# full control over the contract-check job only. The token cannot touch
|
||||||
|
# agents, other jobs, or Nomad Variables (set via the management token).
|
||||||
|
#
|
||||||
|
# Apply with the management token (README §4):
|
||||||
|
# nomad acl policy apply -description "Forgejo CI deploy" ci \
|
||||||
|
# deploy/nomad/policies/ci.hcl
|
||||||
|
#
|
||||||
|
# If `nomad job run` from CI ever fails with permission errors during
|
||||||
|
# deployment evaluation, fall back to namespace-wide write:
|
||||||
|
# namespace "default" { policy = "write" }
|
||||||
|
|
||||||
|
namespace "default" {
|
||||||
|
policy = "read"
|
||||||
|
|
||||||
|
job "contract-check" {
|
||||||
|
policy = "write"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue