Ажиллагаа¶
Байршуулалт, ажиглалт, нөөцлөлт. deploy/, .github/workflows/deploy.yml ба
ажиллаж байгаа системээс уншиж бичигдсэн.
Баримтын төв · Архитектур · Гарын авлага
Найман нэр, нэг хост¶
| Нэр | Юу | Ард нь |
|---|---|---|
petronet.mn |
Платформ өөрөө — frontend ба /api/v1/* |
Next.js + Go |
admin.petronet.mn |
Операторын консол — /api/platform/v1/* |
Мөн тэр хоёр |
developer.petronet.mn |
Хөгжүүлэгчийн портал — OAuth клиент, scope, түлхүүр (цөмийн /dev) |
Мөн тэр хоёр, DEVELOPER_HOST |
dwh.petronet.mn |
Дата агуулахын зураглал | Статик, өөрийн контейнер |
backups.petronet.mn |
Нөөцлөлтийн тайлбар хуудас; шифрлэгдсэн хуулбар нь өөр байршилд | Статик, өөрийн контейнер |
monitoring.petronet.mn |
Grafana | Ажиглалтын стек |
docs.petronet.mn |
Энэ баримт | Статик, MkDocs |
plan.petronet.mn |
Төслийн баримт — шаардлага, төлөвлөгөө, жишиг | Статик, MkDocs |
Хоёр урсгал өөр origin дээр байгаа нь гоо зүйн биш: cookie нь hostname-аар хязгаарлагддаг тул тусдаа нэр нь тусдаа ambient authority гэсэн үг.
Эдгээрээс гадна native клиентүүдийн дөрвөн төхөөрөмжийн шугам
(desktop. / mobile. / kiosk. / pos.petronet.mn) байна — тэдгээр нь өөр
үйлчилгээ БИШ, ижил frontend ба API зөвхөн өөр origin дээр. Бүртгэл ба асаах
дараалал: Байрлуулалт § Төхөөрөмжийн шугамууд.
Платформын нүүр хуудас эдгээрийг карт болгож харуулна. Хаягууд нь кодод биш,
системийн орчинд (SERVICE_URL_ADMIN, _DWH, _BACKUPS, _MONITOR,
_DOCS, мөн үндэсний _EID): admin.petronet.mn бол PetroNet System-ийнх,
өөр хэн ч биш. Тохируулаагүй үйлчилгээ зурагдахгүй, нэг ч тохируулаагүй бол
хэсэг бүхэлдээ зурагдахгүй.
Консолын хаалга нь мөн платформын landing дизайнтай: нэвтрэх маягт нь hero дотор, доор нь эрхийн загвар ба impersonation-ий нөхцөлүүд. Session-гүй ирсэн хүн ард нь юу байгааг мэдэлгүй үлдэх ёсгүй — тэр хаяг юу болохыг хэлэх нь нууц задруулах биш, нэвтрэх шалгалт нь ард нь хэвээр байна.
Статик хуудсууд контейнерт¶
dwh.* нь deploy/docker-compose.dwh.yml-ийн nginx:alpine контейнерээс
үйлчилнэ — read-only файлын систем, no-new-privileges, зөвхөн loopback.
Хостын nginx-ээс шууд файл өгч ч болох байсан (docs.* тэгдэг) ч тэр төсөл
дараа нь Metabase, Cube, Postgres нэмнэ: тэдгээр нь бүгд контейнер болох тул
нэгдүгээрх нь ч контейнер байсан нь хожим stack-ийн хажууд зөөхийг compose
файлд мөр нэмэх ажил болгоно.
Байршуулалт¶
CI ногоон болсны дараа deploy.yml:
ghcr.io-д backend ба frontend image бүтээж түлхэх
↓ (tag = commit sha, мөн latest)
хост руу ssh: compose файл хуулах → pull → migrate → restart
Хоёр зүйл зориуд:
- Хувилбар нь image дотор ldflags-аар шатаагдана. Тэгэхгүй бол систем
бүр өөрийн хувилбарыг мэдэхгүй болно — каталог
"platform": ">=1.1.0"шаардахад хариулах зүйлгүй. - Хуучныг шинэ нь бэлэн болтол буулгахгүй. GHCR дөнгөж түлхсэн хувийн
image-д
deniedгэж хариулах нь ховор биш бөгөөд нэг удаа тэр нь тухайн ажиллагааны өөрийнх нь түлхсэн image-ийн rollout-ыг унагаасан.
Тохиргоо CI-гаас ирнэ, хостоос биш¶
deploy.yml нь серверийн .env-ийг ажиллагаа бүрд шинээр бичдэг —
«written fresh every deploy so the server never drifts from CI». Энэ нь зөв
зан төлөв: хост дээр гараар хийсэн засвар нь хаана ч бүртгэгдээгүй, хэн ч
давтаж чадахгүй тохиргооны эх сурвалж болно.
Үр дагавар нь мартагдамхай: хостын .env рүү гараар нэмсэн мөр дараагийн
rollout дээр арчигдана. Шинэ тохиргоо нэмэх зам нь гурван газарт бичих:
deploy.yml env блок ${{ vars.X }} эсвэл ${{ secrets.X }}
deploy.yml envs: жагсаалт ssh-action тэр хувьсагчийг дамжуулна
deploy.yml .env heredoc X=${X}
Нууц биш утга — нийтийн hostname, URL — нь vars, нууц нь secrets.
Тавиагүй vars нь хоосон мөр болж, хэрэглэгч тал нь «тохируулаагүй» гэж
уншина.
Энэ баримтыг бичих үед үйлчилгээний хаягуудыг хостын .env-д гараар нэмээд
дараагийн deploy дээр алга болохыг харсны дараа л зөв газарт нь тавьсан.
Хоосон нууц чимээгүй өнгөрдөггүй. Тохируулаагүй нууц нь .env-д хоосон
мөр болж бичигдэж, платформ түүгээрээ асдаг: rollout юу ч анзаарахгүй,
алдаа нь тэр утгыг хамгийн түрүүнд асуусан зүйл дээр гарна. Тиймээс хоосон
байж болохгүй утгуудыг эхний байт серверт хүрэхээс өмнө шалгана. Зөвхөн
хоосон эсэхийг шалгаж, зөвхөн нэрийг хэвлэнэ — тавигдсаныг батлахын тулд
утгыг нь echo хийдэг шалгалт бол түүнийг лог уншдаг бүх хүнд нийтэлсэн хэрэг.
Нууцын сан — Infisical¶
Нууцууд GitHub-ийн secret-ээс эсвэл Infisical-аас ирж болно. deploy.yml
дахь утга бүр ${{ secrets.X || env.X }} хэлбэртэй: GitHub-д байвал түүнийг,
байхгүй бол сангаас ирснийг авна.
Тэгсний ач холбогдол нь нэг нэгээр нь шилжүүлж болох явдал. Үйлдвэрлэлийн нууцыг нэг шөнөд бүхэлд нь зөөх шаардлагагүй: санд нэгийг тавь, GitHub-аас тэрийг устга, нэг rollout ажиглаж үз, дараагийнх руу шилж. Аль ч мөчид буцах зам нь secret-ээ буцааж тавих.
Нэвтрэлт нь OIDC — client secret байхгүй. GitHub ажиллагаа бүрд богино настай токен гаргаж, Infisical түүнийг шалгана. Тэгснээр «нууц хадгалах газар руу орох нууц» гэдэг асуудал огт үүсэхгүй.
Асаах: Infisical дээр machine identity үүсгээд, репод дараах variable-ууд тавина (нууц биш — эдгээр нь зөвхөн хаяг ба танигч):
INFISICAL_IDENTITY_ID machine identity-ийн ID
INFISICAL_PROJECT_SLUG төслийн slug
INFISICAL_DOMAIN сангийн хаяг
INFISICAL_ENV_SLUG анхдагч prod
INFISICAL_SECRET_PATH анхдагч /
INFISICAL_IDENTITY_ID тавиагүй бол алхам ажиллахгүй бөгөөд бүх зүйл
өмнөх шигээ GitHub-ийн secret-ээс ирнэ.
Санд ордоггүй зүйлс. DEPLOY_SSH_KEY, DEPLOY_HOST, DEPLOY_USER нь
GitHub-д үлдэнэ: тэдгээр нь хост руу хүрэх зам бөгөөд санд хүрэхээс өмнө
хэрэгтэй болдог. GITHUB_TOKEN нь GitHub-ийн өөрийнх.
Санд байх ёстой ч CI-д хэзээ ч ордоггүй зүйлс. Хостын дээрх нөөцлөлт, ажиглалтын түлхүүрүүд — ялангуяа age-ийн хувийн түлхүүр — нь deploy-гоор дамждаггүй. Тэдгээрийн хувьд сан нь цорын ганц хуулбар: хост алдвал сэргээх эх сурвалж өөр байхгүй.
CI-ийн Windows worker¶
macOS, iOS, Android гурвыг GitHub-ийн үүлэн runner барина. Windows нь өөр: WinUI-ийн XamlCompiler ажиллуулахад Windows SDK ба Visual Studio Build Tools хэрэгтэй бөгөөд тэдгээрийн хувилбарыг өөрсдөө барихын тулд өөрийн төмөр дээр барина.
| Хост | Windows 11 Pro, 4 цөм, 16 ГБ |
| Хандалт | SSH (OpenSSH) |
| Runner | petronet-win11, шошго petronet-win |
| Байрлал | C:\actions-runner-petronet, сервис нь actions.runner.gerege-systems-petronet-gerege-nexus.petronet-win11 |
| Сервисийн данс | NT AUTHORITY\NETWORK SERVICE — build хийхэд хангалттай, админ эрх хэрэггүй |
| Toolchain | .NET SDK 8.0.422 (C:\dotnet), git, VS Build Tools 2022: MSBuildTools + ManagedDesktopBuildTools + VCTools + Windows 11 SDK 26100 |
VCTools (MSVC) нь заавал. WinUI-ийн Microsoft.UI.Xaml.Markup.Compiler.interop.targets
нь VC\Tools\MSVC-г уншиж хамгийн сүүлийн хувилбарыг олдог — байхгүй бол
DirectoryNotFoundException шидэж build унана. dotnet SDK ганцаараа
хангалтгүй.
Тэр машин дээр хоёр дахь runner — eid-platform-mn-ийнх — зэрэгцэн
ажиллаж байгаа. Тиймээс job нь runs-on: [self-hosted, windows, petronet-win]
гэж ШОШГООР сонгоно: self-hosted дангаараа хоёр репогийн ажлыг хольж
явуулна.
Энэ репо хувийн (private), гэхдээ runner нь хуваалцсан төмөр. Fork-оос ирсэн PR (хувийн репог fork хийх эрхтэй хэн нэгний) workflow-г солиод тэр машин дээр дурын тушаал ажиллуулж чадах тул хамгаалалт хоёр давхар:
native-clients.yml-ийн windows job ньif:-ээр fork-ийн PR дээр огт ажиллахгүй (зөвхөн энэ репогийн салбарууд).- Репогийн Actions тохиргоо
all_external_contributors— гаднын хүний PR бүр workflow ажиллахын өмнө зөвшөөрөл хүснэ.
Хоёуланг нь сулруулах нь тэр машиныг гаднын хүнд нээж өгнө. Хэрэв Windows-ийн
шалгалт fork-ийн PR дээр ч хэрэгтэй болвол зөв зам нь windows-latest руу
буцаах, runner-ийг нээх БИШ.
Runner дахин бүртгэх¶
Token хугацаа дуусах, эсвэл машин солигдох үед:
gh api -X POST repos/gerege-systems/petronet-gerege-nexus/actions/runners/registration-token --jq .token
# дараа нь тэр машин дээр:
# C:\actions-runner-petronet\config.cmd --unattended --replace \
# --url https://github.com/gerege-systems/petronet-gerege-nexus \
# --token <TOKEN> --name petronet-win11 --labels petronet-win --runasservice
Ажлын хавтас (_work) машин дээр үлддэг тул job бүрийн төгсгөлд
git clean -xdf хийнэ — үгүй бол гурван form factor-ийн build гаралт дискийг
хэдхэн долоо хоногт дүүргэнэ.
XAML-ийн алдааг хэрхэн УНШИХ вэ¶
dotnet build дээр WinUI-ийн XamlCompiler нь алдаатай XAML тулгарвал
чимээгүй 1 буцаана: stdout хоосон, output.json үүсэхгүй, MSBuild зөвхөн
MSB3073 ... exited with code 1 гэж хэлнэ. Жинхэнэ мессежийг харах цорын ганц
зам нь компайлерыг MSBuild дотор ажиллуулах:
& "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\amd64\MSBuild.exe" `
PetroNetDesktop.sln -p:Configuration=Release -p:FormFactor=Desktop -p:RestoreLockedMode=false -v:m
amd64 хувилбарыг заавал: 32 битийн MSBuild нь RID-ыг win-x86 гэж
таамаглаад PlatformTarget=x64-тэй зөрж NETSDK1032 өгнө. Мөн -p:Platform
БҮҮ дамжуул — solution нь зөвхөн Any CPU тохиргоотой.
Энэ замаар олдсон нэг жишээ: XML коммент дотор давхар зураас (--) байж
болохгүй, тиймээс CSS-ийн --pn-blue мэтийн нэрийг .xaml комментод шууд
бичих нь бүх Windows build-ийг унагаана (WMC9999).
Ажиглалт¶
deploy/docker-compose.monitoring.yml — тусдаа стек, тусдаа сүлжээ,
платформын сүлжээнд зөвхөн scrape хийхээр холбогдоно.
| Хадгалах хугацаа | ||
|---|---|---|
| Prometheus 3.1 | Хэмжүүр | 60 хоног / 20 GB |
| Loki 3.4 | Лог | 31 хоног |
| Tempo 2.7 | Trace | 72 цаг |
| Alloy 1.7 | Цуглуулагч | — |
| Grafana 11.5 | Дэлгэц | — |
| Alertmanager 0.28 | Сэрэмжлүүлэг | — |
Экспортерууд: node_exporter, cAdvisor, postgres_exporter, redis_exporter.
Trace 72 цаг байгаа шалтгаан: trace нь хүсэлт болсноос хойш минут, эсвэл цагийн дотор уншигддаг. Долоо хоногийн настай trace-ыг хэн ч нээдэггүй.
Хэмжүүр¶
Хэмжүүр бүр OpenTelemetry-ийн SDK-аар дамжиж, Prometheus exporter-ээр
/metrics дээр гарна. HTTP-ийнх нь semantic convention-ыг дагана:
Хүсэлтийн тоо нь тусдаа counter биш — тэр гистограммын _count цуврал.
http.request.method нь хаалттай олонлог. Танихгүй үйл үг
_OTHER болж нугалагдана: PROPFIND гэж дуудсан сканнер time series
үүсгэх ёсгүй.
Гадаад систем бүр нэг histogram-аар хэмжигдэнэ —
external_request_duration_seconds{system,operation,status} — системийн нэр
нь мөн адил хаалттай олонлог, танихгүй нь other.
Exemplar¶
Trace асаалттай үед гистограммын сэмпл бүр trace_id авч явна. Латенси
графикийн удаан цэг дээр дарахад Grafana тэр яг тэр хүсэлтийн trace-ыг
нээнэ — «аль хүсэлт удаан байсан бэ» гэдгийг таамаглах хэрэггүй.
/metrics нь OpenMetrics хэлбэрээр үйлчилдэг байх ёстой, эс бөгөөс exemplar
дамжихгүй.
Лог¶
Alloy Docker-ийн лог цуглуулж Loki руу өгнө. Backend нь slog-оор JSON
бичдэг тул | json ажиллана.
Хоёр урхи, хоёулаа PetroNet System дээр бодитоор гарсан:
- LogQL-ийн шүүлт том жижиг үсэг ялгана.
|= "audit"нь юу ч олохгүй — бичигдсэн зүйл нь"msg":"AUDIT_EVENT". $__rate_intervalнь зөвхөн Prometheus-ийнх. Loki дээр$__auto.
Мөн: Grafana нь панелийг панелийн датасурс дээр ажиллуулдаг, target-ийнх дээр биш. Хоёр нь зөрвөл LogQL нь Prometheus руу очиж parse алдаа өгнө.
Дэлгэцүүд¶
deploy/monitoring/grafana/dashboards/ — provisioning-оор ирдэг, гараар
үүсгэдэггүй:
api-overview · infrastructure · logs · security · resilience ·
external-systems · monitoring-self
Сүүлийнх нь хамгийн чухал: ажиглалт өөрөө ажиллаж байгаа эсэх. Унтарсан Prometheus нь бүх зүйл хэвийн байгаа мэт харагддаг.
Сэрэмжлүүлэг¶
Энэ байршуулалтад одоогоор сэрэмжлүүлгийн дамжуулга байхгүй.
deploy/monitoring/нь зөвхөн консол ба Grafana-д зориулсан Prometheus-тай; дүрмийн файл (rule_files) ч, Alertmanager ч алга. Доорх жагсаалт нь цөмийн стекийнх. Нэмэх үед нөөцлөлтийн хамгийн бага хоёр дүрэм:
Цөмийн стект 31 дүрэм, зургаан файлд. Хамрах хүрээ:
- API — унтарсан, латенси, алдааны төсвийн шаталт (хурдан / удаан)
- Дэд бүтэц — диск, санах ой, Postgres, Redis, контейнерын дахин эхлэл, TLS
- Консол — нэвтрэлтийн бүтэлгүйтэл, түгжээ, break-glass ашиглалт, бүртгэгдээгүй бичилт
- Гадаад систем — удаан, доройтсон, бүтэлгүй
- Нөөцлөлт — бүтэлгүй, хуучирсан, хэзээ ч харагдаагүй, хостоос гараагүй
- Ажиглалт өөрөө — Alertmanager мэдэгдэхгүй байна, дүрэм тооцоолж чадахгүй байна, Loki лог татгалзаж байна, target унтарсан
NexusTLSExpiryUnknown нь тусгай: тэр нь «гэрчилгээ дуусах гэж байна» гэсэн
үг биш, «хэн ч хэмжихгүй байна» гэсэн үг. Хугацааг бичдэг cron ажил нь
гараар суулгагддаг бөгөөд нэг хост дээр огт суулгагдаагүй байсан.
Брэндлэлт ба Монгол хэл¶
Ажиглалтын домэйн нь системийн нэг хэсэг мэт харагдах ёстой: Grafana дээр PetroNet-ийн лого, нэр, tab icon, цагаан нэвтрэх карт, монгол хэл. Grafana OSS-д брэндлэх цэг байхгүй (white labeling нь Enterprise) тул бүгд nginx дээр хийгдэнэ:
Скрипт нь ажиллаж буй Grafana-аас хоёр webpack chunk-ийн нэрийг уншиж (нэр нь
агуулгын hash агуулдаг), монгол орчуулгын chunk-ийг
deploy/monitoring/grafana/branding/i18n/mn.txt-ээс барьж, хэлний жагсаалт
дахь Svenska-г Монгол болгож, /var/www/monitor руу хэв маяг, скрипт,
логог тавиад nginx-ийн snippet-ийг үүсгэнэ. Төгсгөлд нь зургаан шалгалтыг
домэйнээр дамжуулан хийнэ — файл байрандаа байгаа эсэх биш, браузарт юу
ирж байгааг.
Гурван зүйлийг санах:
- Grafana шинэчлэх бүрд дахин ажиллуулна. chunk-ийн hash өөрчлөгдөхөд орлуулалт таарахаа болино; тэр үед швед хэл эргэж ирнэ, өөр юу ч эвдрэхгүй.
- Монгол хэл нь
sv-SE-ийн үүрэнд сууна (Grafana-дmnlocale байхгүй). ТиймээсGRAFANA_DEFAULT_LANGUAGE=sv-SEнь зөвхөн скрипт ажилласан хостод зөв. - Орчуулга хэсэгчилсэн (255 түлхүүр). Дутуу түлхүүр англи руугаа буцдаг тул
mn.txt-д мөр нэмээд скриптийг дахин ажиллуулахад л хангалттай.
Цөмийн хувилбараас нэг ялгаа: тэнд Grafana нь /grafana/ дэд зам дээр сууж,
домэйны үндэс дээр landing хуудас үйлчилдэг. Энд Grafana нь үндэс дээрээ
сууна — энэ домэйны ард түүнээс өөр юу ч байхгүй — тул landing хуудас
хуулагдаагүй: үйлчилгээний картууд petronet.mn-ийн нүүр хуудсан дээр байна.
Платформын бүртгэлээр Grafana руу нэвтрэх¶
Grafana нь PetroNet System-ийн өөрийн OIDC provider-оос хэн болохыг асууна:
операторууд аль хэдийн байгаа бүртгэлээрээ ордог, платформын админ нь
Grafana-ийн сервер админ болно (platform_admin && 'GrafanaAdmin' || 'Viewer').
Бусад нь уншиж чадах ч юуг ч өөрчилж чадахгүй.
Дотоод админы нэвтрэлт (GRAFANA_ADMIN_PASSWORD) хэвээр байна. Унасан танигч
нь нээгдэхгүй ажиглалтын стек болох ёсгүй — тэр нь яг түүнийг хэрэгтэй
болгодог цаг.
Клиентийг платформ дээр бүртгэнэ (Хөгжүүлэгч → Аппликейшн, эсвэл SQL-ээр):
| Талбар | Утга |
|---|---|
redirect_uris |
https://monitoring.petronet.mn/grafana/login/generic_oauth |
post_logout_redirect_uris |
https://monitoring.petronet.mn/grafana/login |
grant_types |
authorization_code, refresh_token |
scopes |
openid, profile, email, roles |
client_type |
confidential |
client_secret_hash нь нууц үгийн SHA-256, hex-ээр (64 тэмдэгт) — бааз нь
нууцыг өөрийг нь хадгалдаггүй.
Хоёр урхи:
OAUTH_REDIRECT_HOSTS. Redirect URI-ийн хост энэ жагсаалтад байх ёстой бөгөөд жагсаалт нь дэд домэйныг өвлүүлдэггүй:petronet.mnбичсэн ньmonitoring.petronet.mn-ыг зөвшөөрөхгүй. Хоосон орхивол цөмийн анхдагч (nexus.gerege.mn) хүчинтэй болж, redirect бүр татгалзагдана.- Гарах.
GF_AUTH_SIGNOUT_REDIRECT_URLдахьclient_idнь заавал: logout цэг буцах хаягийг тэр клиентийн бүртгэлтэй жагсаалттай тулгадаг. Параметргүй бол «post_logout_redirect_uri is not registered» гэж унана — дутуу биш, буруу URI мэт уншигдана.
Өгөгдлийн сангийн role¶
Backend нь gerege_nexus_api-аар холбогдоно (цөмийн 00115: SUPERUSER, BYPASSRLS,
CREATE эрхгүй). postgres-ээр холбогдвол RLS зөвхөн зөвлөмж болж, dbguard асах
бүрд ERROR бичдэг. Хоёр миграцын job (migrate, migrate-petro) эзэмшигч
postgres-ээр ажиллаж, backend асахаас өмнө дуусна — gerege_nexus_api DDL хийж
чадахгүй тул модулийн миграцыг backend өөрөө хэрэгжүүлэхгүй.
Анх шилжүүлэх (эсвэл нууц үг солих):
ssh petronet
sudo docker exec -it petronet2_postgres psql -U postgres -d platform_db
platform_db=# \password gerege_nexus_api
/opt/petronet-v2/.env-д API_DATABASE_URL=postgres://gerege_nexus_api:<нууц үг>@postgres:5432/platform_db?sslmode=disable
нэмээд backend-ийг дахин үүсгэнэ. Шалгах: логт
dbguard: the database login role is unprivileged, мөн
docker logs petronet2_backend 2>&1 | grep -i "permission denied\|42501" хоосон.
Буцаах: .env-ээс API_DATABASE_URL-ыг хасаад backend-ийг дахин үүсгэнэ —
compose postgres руу буцна.
Нөөцлөлт¶
deploy/scripts/backup.sh, cron дээр өдөр бүр.
pg_dump + pg_dumpall --roles-only → хуучныг цэвэрлэх
→ age-ээр шифрлэж S3 руу / restic руу (тохируулсан бол)
→ platform_backups-д мөр бичих
→ node_exporter-ийн textfile руу хэмжүүр бичих
Нэг багц = хоёр файл: petronet-<огноо>.sql.gz (сан, --no-owner) ба
petronet-roles-<огноо>.sql.gz (кластерын role-ууд, нууц үггүй). pg_dump
role авдаггүй — шинэ кластерт role-гүй сэргээсэн сан дээр GRANT/POLICY унаж RLS
эвдэрнэ. Хоёулаа ижил off-site байршил руу явна.
Тохируулсан off-site байршлын аль нэг рүү хуулж чадаагүй бол локал dump үлдэнэ,
харин ажиллалт амжилтгүй: platform_backups.ok = false,
nexus_backup_last_ok 0, nexus_backup_offsite_ok 0, exit 1.
Гурав дахь алхам нь консол уншдаг мөр. Дөрөв дэх нь Prometheus уншдаг — шөнө дунд хэн нэгэнд сэрэмжлүүлэг илгээж чадах цорын ганц хувилбар. Хэмжигдэхгүй нөөцлөлт нь нөөцлөлт байхгүйтэй бараг адил: cron-ий чимээгүй бүтэлгүйтэл нь сэргээх өдрөө л илэрдэг.
Хостод зөвхөн age-ийн нийтийн түлхүүр байна. Эвдэрсэн платформ өөрийн илгээсэн зүйлээ уншиж чадахгүй; хувийн түлхүүр нь операторт байна. Түүнийг хост дээр үлдээвэл шифрлэлт нь утгагүй; түүнийг алдвал нөөцлөлт нь утгагүй.
Off-site сан нь MinIO, объектын хувилбарлалт асаалттай, байршуулалтын түлхүүр нь зөвхөн нэмэх эрхтэй. Тэр нь эвдэрсэн хост нь өөрийн өмнөх нөөцлөлтүүдийг устгаж чадахгүй гэсэн үг.
Хэмжүүр:
nexus_backup_last_run_timestamp_seconds
nexus_backup_last_success_timestamp_seconds
nexus_backup_last_size_bytes
nexus_backup_last_ok
nexus_backup_offsite_ok
Бүтэлгүйтсэн ажиллагаа нь өмнөх амжилтын мөчийг хадгална — эс бөгөөс нэг
бүтэлгүйтэл нь «хэзээ ч амжилттай болоогүй»-тэй ялгагдахаа болино.
Тохируулаагүй систем дээр offsite_ok нь 0 хэвээр байна, тэр нь зөв:
хуулбар өөр газар байхгүй гэдэг нь хэмжигдэх ёстой баримт.
Сэргээлт¶
Нөөцлөлт нь сэргээгдэх хүртэл нөөцлөлт биш. Дараалал нь үргэлж ижил: role-ууд → сан → шалгалт, хаягдах кластер руу — ажиллаж байгаа руу биш.
S3 (age) хуулбараас:
age -d -i key.txt petronet-roles-<огноо>.sql.gz.age | gunzip > roles.sql
age -d -i key.txt petronet-<огноо>.sql.gz.age | gunzip > restore.sql
psql -d postgres -f roles.sql # «role "postgres" already exists» — хэвийн
createdb platform_db && psql -v ON_ERROR_STOP=1 -d platform_db -f restore.sql
restic-ээс сэргээх¶
restic-ийн нууц нь backup.sh-ийн уншдаг файлд (BACKUP_RESTIC_ENV_FILE,
root-ийн 600). Хостод restic суулгахгүй — backup.sh-ийн ашигладаг pin хийсэн
образ:
RESTIC="docker run --rm --network host --env-file <restic env файл> restic/restic:0.18.1"
# 1. Snapshot-ууд — багц бүр нэг snapshot, хоёр файлтай
$RESTIC snapshots --host petronet
$RESTIC ls <snapshot-id> --host petronet # /backup/petronet-*.sql.gz хоёр
# 2. Сэргээх (latest эсвэл тодорхой id)
mkdir -p ./restore
docker run --rm --network host --env-file <restic env файл> \
-v "$PWD/restore:/restore" restic/restic:0.18.1 \
restore <snapshot-id> --host petronet --target /restore
ls restore/backup/ # petronet-<огноо>.sql.gz, petronet-roles-<огноо>.sql.gz
# 3. Хаягдах кластер — эхлээд role-ууд, дараа нь сан
docker run -d --name pn_restore_check -e POSTGRES_PASSWORD=restore postgres:16-alpine
sleep 5 # эсвэл pg_isready хүлээх
gunzip -c restore/backup/petronet-roles-*.sql.gz \
| docker exec -i pn_restore_check psql -U postgres -d postgres # «already exists» — хэвийн
docker exec pn_restore_check createdb -U postgres platform_db
gunzip -c restore/backup/petronet-2*.sql.gz \
| docker exec -i pn_restore_check psql -U postgres -v ON_ERROR_STOP=1 -d platform_db
Role-ууд нууц үггүй сэргээгдэнэ (--no-role-passwords). Нэвтрэх role
(жишээ нь апп-ийн холболтын хэрэглэгч postgres-ээс өөр бол) бүрийн нууц
үгийг .env-ээс ALTER ROLE … PASSWORD …-оор дахин тавина.
Сэргээлтийг шалгах¶
docker exec pn_restore_check psql -U postgres -d platform_db -c \
"select count(*) from information_schema.tables
where table_schema in ('workspace','registry','operator')"
# RLS амьд эсэх: бодлогууд ирсэн, тенантын role байгаа
docker exec pn_restore_check psql -U postgres -d platform_db -c \
"select count(*) from pg_policies"
docker exec pn_restore_check psql -U postgres -d platform_db -c \
"select rolname from pg_roles where rolname = 'gerege_nexus_tenant'"
docker rm -f pn_restore_check && rm -rf ./restore
pg_policies 0 эсвэл тенантын role алга бол role-уудын файл ачаалагдаагүй —
сангийн dump-ын GRANT мөрүүд тэр үед алдаа өгсөн байх.
nginx-ийн гурван урхи¶
Гурвуулаа энэ хост дээр бодитоор гарсан. Эхний хоёр нь хариу 200 хэвээр
байхад чимээгүй ажилладаг; гурав дахь нь домэйныг бүтнээр нь унагаадаг ч
nginx -t өнгөрдөг тул мөн адил анзаарагдахгүй өнгөрч болно. Тиймээс curl -I нь vhost нэмсний дараах заавал хийх
алхам.
add_header өв залгамжлахгүй. Хүү блок дотор НЭГ ч add_header байвал
дээд түвшнийхийг нь бүгдийг нь хаяна. location / дотор Cache-Control
нэмсэн нь server түвшний дөрвөн толгойг — CSP, HSTS, X-Frame-Options,
X-Content-Type-Options — чимээгүй унтраасан. Хуудас зөв харагдсаар, хариу
200 хэвээр; зөвхөн толгойг нь уншиж байж мэдэгдэнэ. Шийдэл нь бүх
тогтвортой add_header-ийг server түвшинд байлгах. Одоо nginx upstream-ийн
давхар хуулбарыг proxy_hide_header-ээр хасаад HSTS, frame, MIME, permission
толгойг нэг удаа өөрөө гаргана. CSP бол тогтвортой утга биш: Next.js хүсэлт
бүрд nonce үүсгэж HTML-ийн script-т суулгадаг тул зөвхөн тэр толгойг nginx
өөрчлөлгүй нэвтрүүлнэ.
Certbot HTTP/2 бичдэггүй. Тэр нь listen 443 ssl мөрийг өөрөө бичдэг ба
протоколыг нэмдэггүй. Үр дүнд нь 2026-08-28-ны HTTP/2 шилжилтийн дараа
үүсгэсэн vhost бүр — docs, backups, monitor, cp — HTTP/1.1 дээр
үлдсэн байв. http2 on; нь тусдаа заавар байх ёстой: тэгвэл гэрчилгээ
шинэчлэх бүрд алга болохгүй.
Репогийн vhost-ыг дахин хуулах нь TLS-ийг арчина. nginx/*.conf дахь
файлууд зөвхөн listen 80 мөртэй — 443 блокийг certbot суулгасны дараа
өөрөө нэмдэг. Тиймээс файл өөрчлөгдөөд түүнийг sites-available дээр дахин
хуулбал гэрчилгээний хэсэг устаж, домэйн HTTPS дээр бүтэн унана. nginx -t
өнгөрнө: синтакс зөв, зүгээр л 80-ыг л сонсож байна. 2026-09-02-нд
monitoring.petronet.mn дээр яг ингэж болсон. Хуулсан бол дараа нь заавал:
curl -s -o /dev/null -w "http/%{http_version} %{http_code}\n" https://<нэр>/
curl -sI https://<нэр>/ | grep -iE 'content-security|strict-transport|x-frame'
Баримтын сайтыг шинэчлэх¶
docs.petronet.mn ба plan.petronet.mn хоёр нь өөрийн nginx контейнертэй
бөгөөд тэдгээр нь репо доторх docs/mkdocs/build/site,
docs/mkdocs/build-plan/site-ыг bind mount-оор үйлчилдэг. Хостын nginx нь
зөвхөн 3031 / 3033 руу дамжуулна.
Тусдаа алхам байхгүй: ердийн deploy (git → CI → runner → deploy.sh) хоёр
сайтыг docs/mkdocs/build.sh-ээр угсарч, docs ба plan контейнерийг
--force-recreate-ээр дахин үүсгэдэг. Баримтын өөрчлөлт ч бусад өөрчлөлт шиг
main руу merge хийгдэж гарна.
Хоёр урхи, хоёулаа 200 хариу өгсөөр ажилладаг тул анзаарагдахгүй өнгөрнө:
/var/www/docs руу хуулах нь ямар ч нөлөөгүй. Цөмийн deploy.sh тэгж
хийдэг — тэнд nginx статик хавтаснаас уншдаг. Энд уншдаггүй. Файлууд очих ч
хэн ч уншихгүй, скрипт «published» гэж хэлээд хуудас 404 хэвээр үлдэнэ.
Дахин угсарсны дараа контейнерыг ДАХИН АСААНА. mkdocs build нь гаралтын
хавтсыг устгаад дахин үүсгэдэг («Cleaning site directory») тул inode нь
солигдоно. Контейнерийн mount хуучин inode дээр үлдэж, доторх nginx устсан
модыг үйлчилсээр байна: шинэ хуудас 404, хуучин хуудас хуучин агуулгаараа
200. docker ps эрүүл, nginx -t цэвэр, лог чимээгүй. deploy.sh угсарсны
дараа контейнерийг өөрөө дахин үүсгэдэг.
Замын чиглүүлэгч (OSRM)¶
Цистерн баазаас ШТС рүү замд гарахад backend замыг OSRM-ээс асууж рейс дээр
хадгална (route_geom, route_distance_m, route_duration_s); нүүрний ба
/map-ийн зураг тэр замаар зурж, GPS ирээгүй үед цистернийг явсан хугацаагаар
нь зам дээр тооцоолон тавьдаг («≈» шошготой). OSRM нь deploy/docker-compose.yml
дэх osrm үйлчилгээ (osrm/osrm-backend, гадагш портгүй, backend
OSRM_URL=http://osrm:5000-оор ханддаг).
График нь репод байхгүй, нэрлэсэн volume petronet2_petronet2_osrm_data-д.
Шинэ хост дээр osrm анх асахаас өмнө нэг удаа, дараа нь зам шинэчлэхэд
(сард нэг хангалттай):
bash deploy/scripts/setup_osrm.sh # Geofabrik-ийн Монголын хандалт → extract/partition/customize, 3–5 мин
docker compose -f deploy/docker-compose.yml restart osrm
Графикгүй бол osrm контейнер унана, backend үүнийг шаарддаггүй: рейс хэвийн
бичигдэж, зураг бааз–ШТС хоёрыг шулуун тасархай шугамаар холбоно. Шалгах:
docker run --rm --network petronet2_default alpine:3.20 wget -qO- \
'http://osrm:5000/route/v1/driving/106.9177,47.9184;106.856,47.918?overview=false' | head -c 200
Гараар хийгддэг зүйлс¶
Байршуулалт автомат, гэхдээ эдгээр нь биш. Шинэ хост дээр мартагддаг зүйлс:
backup.sh-ийн cron бичлэг- OSRM-ийн график —
deploy/scripts/setup_osrm.sh(дээрх «Замын чиглүүлэгч») - TLS дуусах хугацааг бичдэг cron ажил
- Docker 29 дээр cAdvisor-ын containerd тохиргоо (
--containerd-namespace=mobyбаpid: host) — үүнгүйгээр нэг л цуврал экспортлогдоно, мөн энэ нь дахин эхлүүлэхийг шаарддаг, дахин үүсгэхийг биш - Хуучин Docker image-ийн цэвэрлэгээ.
ghcr-retention.ymlнь GHCR талыг цэвэрлэдэг ч хостын дискийг цэвэрлэдэггүй: deploy бүр хоёр image нэмнэ ба тэдгээр нь өөрөө хэзээ ч явахгүй. Гарын авлага.
Дэлгэрэнгүй алхмуудыг Гарын авлага.