Files
cloud-host/RUNBOOK-CICD.fa.md
T
keyhan 6d9cd89cc5 docs: add portable from-zero deploy runbook and GitOps templates
Document server-side rollout (values, Sealed Secrets, logging, greenfield
reset) with environment variables so any cluster can follow the same steps.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 12:17:55 +03:30

23 KiB

RUNBOOK — خط CI/CD (Gitea Actions → Kaniko → Harbor → Argo CD)

این مستند جریان کامل Build و Deploy پلتفرم را توضیح می‌دهد: از Push شدن کد روی main تا استقرار خودکار روی Kubernetes.

استقرار از صفر روی سرور جدید: RUNBOOK-DEPLOY.fa.md — شامل جدول متغیرها، seal کردن Secretها، logging stack، greenfield reset، و چک‌لیست سلامت.


معماری و جریان کلی

flowchart TD
    Dev[Developer] -->|git push main| AppRepo["Gitea: abrban/cloud-host (کد + چارت)"]
    AppRepo -->|trigger workflow| Runner["Act Runner (namespace: gitea)"]
    Runner -->|"checkout با CI_TOKEN"| AppRepo
    Runner -->|kubectl apply Job| Kaniko["Kaniko Job (namespace: cloudhost-builds)"]
    Kaniko -->|"push با harbor_registry_user"| Harbor["Harbor (harbor-registry:5000)"]
    Runner -->|"آپدیت image.tag + commit/push"| GitOpsRepo["Gitea: abrban/cloud-host-gitops (state)"]
    GitOpsRepo -->|"poll (پیش‌فرض هر ۳ دقیقه)"| Argo["Argo CD (automated sync)"]
    AppRepo -->|"Helm Chart (source دوم)"| Argo
    Argo -->|"helm render + apply"| K8s["Kubernetes (namespace: cloudhost)"]
    Harbor -->|"pull از طریق mirror در k3s"| K8s
    Rollback["Rollback: git revert در cloud-host-gitops"] -.-> GitOpsRepo

مراحل به ترتیب:

  1. Developer روی شاخهٔ main در ریپوی اپلیکیشن (git.abrban.com/abrban/cloud-host) push می‌کند.
  2. Workflow در .gitea/workflows/build-deploy.yaml روی Runner با لیبل abrban-builder اجرا می‌شود.
  3. Runner کد را با توکن CI کلون می‌کند و تگ ایمیج (YYYYMMDD-HHMM-<sha>) را می‌سازد.
  4. Job تست بک‌اند در namespace cloudhost-builds اجرا می‌شود (npm ci + jest --ci) — در صورت fail، بیلد ایمیج شروع نمی‌شود.
  5. برای هر ایمیج (backend و frontend) یک Kaniko Job در namespace cloudhost-builds ساخته می‌شود که کد را کلون، ایمیج را build و به Harbor push می‌کند.
  6. بعد از موفقیت هر دو Build، همان Runner ریپوی cloud-host-gitops را کلون می‌کند، مقدار images.backend.tag و images.frontend.tag را در platform/values-abrban.yaml عوض و commit/push می‌کند (با retry و git pull --rebase در صورت race).
  7. Argo CD (Application به نام abrban-platform با sync خودکار) تغییر را تشخیص می‌دهد و نسخهٔ جدید را در namespace cloudhost مستقر می‌کند.

جلوگیری از حلقهٔ CI: کامیتِ Pipeline به ریپوی جدا (cloud-host-gitops) می‌رود که هیچ Workflowای ندارد؛ بنابراین Build دوباره trigger نمی‌شود.

زمان تقریبی یک Pipeline کامل: ۱۵–۲۵ دقیقه (backend سنگین‌تر است؛ شامل دانلود npm، helm و kubectl داخل Dockerfile).


Bootstrap — پیش‌نیازهای یک‌بار (کلاستر تازه)

قبل از اولین push به main، این موارد باید در کلاستر آماده باشند:

# کار دستور / فایل
1 Mirror k3s → Harbor ./scripts/apply-k3s-registries.sh
2 Secretهای TLS و registry در nsهای gitea, cloudhost-builds, argocd gitops/README.md گام ۴
3 پروکسی egress در gitea و cloudhost-builds همان گام ۴ — برای npm/helm/kubectl داخل build و دانلود kubectl توسط Runner
4 Seed ایمیج‌های CI در Harbor abrban/ gitops/jobs/seed-ci-images.yaml
5 Sealed Secrets controller helm upgrade --install sealed-secrets ... -f gitops/sealed-secrets/values.yaml
6 SealedSecretها از ریپوی gitops kubectl apply -f روی cloud-host-gitops/sealed-secrets/
7 Gitea Runner + Secret CI_TOKEN در ریپو gitops/gitea/act-runner.yaml
8 ریپوی cloud-host-gitops + Argo Application gitops/argocd/application-platform.yaml

Seed ایمیج‌های CI (الزامی)

kubelet و Kaniko نمی‌توانند reliably از proxy-cache هاربر برای همهٔ ایمیج‌ها استفاده کنند. این ایمیج‌ها باید یک‌بار با skopeo در پروژهٔ abrban/ کپی شوند:

ایمیج در Harbor منبع upstream مصرف
abrban/act-runner:0.2.11 docker.io/gitea/act_runner Gitea Actions runner
abrban/alpine-git:2.43.0 docker.io/alpine/git initContainer کلون در Kaniko Job
abrban/node:24-alpine docker.io/library/node BASE_IMAGE در Dockerfile (هر stage)
abrban/kaniko-executor:v1.27.6-debug gcr.io/kaniko-project/executor Kaniko Job
# پیش‌نیاز: secret registry-egress-proxy و registry-pull-secret در ns cloudhost
kubectl apply -f gitops/jobs/seed-ci-images.yaml
kubectl -n cloudhost wait --for=condition=complete job/seed-ci-images --timeout=15m
kubectl -n cloudhost logs job/seed-ci-images --tail=5
# انتظار: SEED_OK

بررسی:

kubectl -n cloudhost run tags --rm -i --restart=Never \
  --image=registry.abrban.com/abrban/alpine:3 \
  --overrides='{"spec":{"imagePullSecrets":[{"name":"registry-pull-secret"}]}}' \
  -- sh -c 'H="harbor_registry_user:$(kubectl -n cloudhost get secret harbor-core -o jsonpath="{.data.REGISTRY_CREDENTIAL_PASSWORD}" | base64 -d)@harbor-registry.cloudhost.svc.cluster.local:5000"; for r in act-runner kaniko-executor alpine-git node; do wget -qO- "http://${H}/v2/abrban/${r}/tags/list"; echo; done'

بعد از bootstrap، Pipeline با push به main خودکار اجرا می‌شود؛ نیازی به ./scripts/trigger-platform-build.sh برای جریان عادی نیست (فقط برای دیباگ دستی).


ساختار Repository (دو ریپو)

abrban/cloud-host — Application Repo

مسیر نقش
backend/, frontend/ کد اپلیکیشن + Dockerfile
backend/helm/cloudhost-platform/ Helm Chart پلتفرم
.gitea/workflows/build-deploy.yaml Pipeline (Build + آپدیت GitOps)
gitops/ نصب زیرساخت (Argo CD، Gitea، Sealed Secrets، k3s و…)

abrban/cloud-host-gitops — GitOps Repo (منبع حقیقت Argo CD)

مسیر نقش
platform/values-abrban.yaml مقادیر Production — تنها فایلی که CI آپدیت می‌کند
argocd/application-platform.yaml تعریف Application (نسخهٔ mirror آن در gitops/argocd/ ریپوی اپ هم هست)
sealed-secrets/*.yaml SealedSecretهای CI — رمزشده و قابل کامیت

Application در Argo CD به‌صورت multi-source تعریف شده: چارت از cloud-host و values از cloud-host-gitops:

sources:
  - repoURL: https://git.abrban.com/abrban/cloud-host.git
    path: backend/helm/cloudhost-platform
    helm:
      valueFiles:
        - $values/platform/values-abrban.yaml
  - repoURL: https://git.abrban.com/abrban/cloud-host-gitops.git
    ref: values

مزیت این جداسازی: history تمیز، دسترسی نوشتن CI محدود به ریپوی state، و امکان دیدن کل تاریخچهٔ Deployها با git log یک ریپوی کوچک.


احراز هویت‌ها (چه کسی با چه چیزی به کجا وصل می‌شود)

مسیر مکانیزم محل نگهداری
Runner → Gitea (ثبت) Registration Token SealedSecret gitea-act-runner-token (ns gitea) در ریپوی gitops
Workflow → Gitea (clone/push هر دو ریپو) PAT کاربر ci Secret ریپوی cloud-host در Gitea با نام CI_TOKEN (نام‌های GITEA_* رزرو هستند)
Kaniko → Harbor (push) harbor_registry_user SealedSecret kaniko-harbor-auth (ns cloudhost-builds) در ریپوی gitops
kubelet → Harbor (pull) user cloudhost Secret registry-pull-secret + mirror در gitops/k3s/registries.yaml
Argo CD → cloud-host (read) repo credential Secret gitea-repo-creds (ns argocd)
Argo CD → cloud-host-gitops (read) PAT کاربر ci SealedSecret gitea-gitops-repo-creds (ns argocd) در ریپوی gitops

توکن CI برای Gitea (CI_TOKEN)

کاربر ci در Gitea ساخته شده و روی هر دو ریپو دسترسی write دارد. PAT آن با scope read:repository, write:repository به‌عنوان Secret با نام CI_TOKEN در Settings → Actions → Secrets ریپوی cloud-host ثبت شده است.

برای rotate: در Gitea با کاربر ci توکن جدید بسازید (یا از API ادمین: POST /api/v1/users/ci/tokens)، مقدار Secret را در تنظیمات ریپو آپدیت کنید و SealedSecret gitea-gitops-repo-creds را هم دوباره seal کنید.

احراز هویت Kaniko به Harbor

Kaniko به endpoint داخلی harbor-registry.cloudhost.svc.cluster.local:5000 push می‌کند که مستقیم به کامپوننت registry می‌رود و harbor-core را دور می‌زند. نکتهٔ مهم:

  • Robot Accountهای Harbor اینجا کار نمی‌کنند — توکن آن‌ها را harbor-core صادر می‌کند و endpoint داخلی به سرویس توکن دسترسی ندارد.
  • credential درست، کاربر داخلی harbor_registry_user است با پسورد REGISTRY_CREDENTIAL_PASSWORD از Secret harbor-core:
REG_PASS="$(kubectl -n cloudhost get secret harbor-core \
  -o jsonpath='{.data.REGISTRY_CREDENTIAL_PASSWORD}' | base64 -d)"
kubectl -n cloudhost-builds create secret docker-registry kaniko-harbor-auth \
  --docker-server=harbor-registry.cloudhost.svc.cluster.local:5000 \
  --docker-username=harbor_registry_user \
  --docker-password="${REG_PASS}"

نمونهٔ manifest: gitops/jobs/kaniko-harbor-auth.example.yaml — نسخهٔ واقعی به‌صورت SealedSecret در ریپوی gitops است.

ورک‌فلو این Secret را در مسیر /kaniko/.docker/config.json هر دو Kaniko Job مانت می‌کند. چون push/pull داخلی و بدون TLS است، این فلگ‌ها لازم‌اند:

  • --insecure / --skip-tls-verify — push
  • --insecure-pull / --insecure-registry=${PUSH_REGISTRY} — pull ایمیج پایه از harbor-registry:5000

ایمیج پایه (node:24-alpine) از endpoint داخلی کشیده می‌شود، نه از registry.abrban.com:

# در .gitea/workflows/build-deploy.yaml
--build-arg=BASE_IMAGE=harbor-registry.cloudhost.svc.cluster.local:5000/abrban/node:24-alpine

Dockerfileها از ARG BASE_IMAGE=node:24-alpine استفاده می‌کنند (build محلی بدون تغییر).

پروکسی egress (registry-egress-proxy در ns cloudhost-builds) هنوز لازم است برای npm ci و دانلود helm/kubectl داخل مراحل RUN در Dockerfile — فقط pull ایمیج پایه از docker.io حذف شده است.

عارضهٔ جانبی push مستقیم به :5000 — Harbor DB از این ایمیج‌ها بی‌خبر می‌ماند؛ در UI هاربر دیده نمی‌شوند ولی pull به‌درستی کار می‌کند. برای دیدن تگ‌ها از registry API استفاده کنید (بخش عیب‌یابی).

ارتباط Runner با Harbor

Runner خودش با Harbor حرف نمی‌زند؛ فقط Job می‌سازد. دو مسیر Harbor:

  • Push (داخلی): harbor-registry.cloudhost.svc.cluster.local:5000 — بدون عبور از Traefik.
  • Pull (kubelet): registry.abrban.com — از طریق mirror در k3s (scripts/apply-k3s-registries.sh) به harbor-core route می‌شود.

Versioning ایمیج‌ها

استاندارد فعلی: YYYYMMDD-HHMM-<git-sha-short> (مثلاً 20260702-1230-a1b2c3d)

  • Immutable است — هیچ‌وقت یک تگ بازنویسی نمی‌شود (برخلاف latest).
  • قابل ردیابی است — از روی تگ ایمیجِ در حال اجرا مستقیماً به کامیت می‌رسید.
  • مرتب‌شونده است — به‌ترتیب زمانی دیده می‌شود.

از latest هرگز برای Deploy استفاده نکنید؛ هم قابلیت Rollback را از بین می‌برد و هم Argo CD تغییری برای sync نمی‌بیند.

SemVer برای Releaseها (اختیاری): روی کامیت release یک Git Tag مثل v1.4.0 بزنید و همان ایمیج را با skopeo copy تگ اضافه بزنید (rebuild لازم نیست). تگ SemVer برای انسان‌هاست؛ منبع حقیقتِ Deploy همان تگ SHA-دار در values است.


آپدیت خودکار Helm Values

مرحلهٔ آخر Workflow ریپوی cloud-host-gitops را کلون می‌کند و فقط دو مقدار را در platform/values-abrban.yaml عوض می‌کند:

images:
  backend:
    repository: registry.abrban.com/abrban/cloudhost-backend
    tag: "20260702-1230-a1b2c3d"   # ← CI این را آپدیت می‌کند
  frontend:
    repository: registry.abrban.com/abrban/cloudhost-frontend
    tag: "20260702-1230-a1b2c3d"   # ← CI این را آپدیت می‌کند

اگر yq روی Runner موجود باشد از آن استفاده می‌شود، وگرنه sed هدفمند (فقط خطِ tag: بلافاصله بعد از repository: ...cloudhost-*) اجرا می‌شود.

جایگزین بررسی‌شده و کنارگذاشته‌شده: Argo CD Image Updater — با روش فعلی هم‌پوشانی دارد و شفافیت کامیتِ صریح از CI را ندارد.


Rollback

چون Deploy فقط از Git انجام می‌شود، Rollback هم یک عملیات Git است — این بار در ریپوی cloud-host-gitops:

git clone https://git.abrban.com/abrban/cloud-host-gitops.git && cd cloud-host-gitops

# 1. پیدا کردن کامیت deploy مشکل‌دار
git log --oneline -- platform/values-abrban.yaml

# 2. برگرداندن آن (تگ ایمیج به نسخهٔ قبلی برمی‌گردد)
git revert <commit-sha>
git push origin main

# 3. Argo CD به‌صورت خودکار به نسخهٔ قبلی sync می‌کند (ایمیج قبلی هنوز در Harbor هست)

نکته‌ها:

  • git revert (نه reset --force) — history حفظ می‌شود و مشخص است چه چیزی چرا برگشت.
  • Rollback اضطراری (وقتی Git در دسترس نیست): argocd app rollback abrban-platform یا Sync به revision قبلی در UI. هشدار: چون selfHeal: true فعال است، Argo در sync بعدی دوباره به HEAD گیت برمی‌گردد — rollback اضطراری موقتی است و باید بلافاصله با git revert دائمی شود.
  • اگر Deployment جدید خراب باشد (CrashLoopBackOff)، به‌خاطر RollingUpdate نسخهٔ قبلی تا آماده‌شدن نسخهٔ جدید بالا می‌ماند.

مدیریت Secretها (Sealed Secrets)

کنترلر Sealed Secrets در kube-system نصب است (values در gitops/sealed-secrets/values.yaml؛ ایمیج آن از ghcr.io/bitnami به پروژهٔ abrban/ هاربر seed شده). Secretهای CI به‌صورت SealedSecret در ریپوی cloud-host-gitops (پوشهٔ sealed-secrets/) نگهداری می‌شوند — رمزشده با کلید عمومی کلاستر؛ فقط کنترلرِ داخل کلاستر می‌تواند رمزگشایی کند، پس کامیت‌کردنشان امن است.

SealedSecret Namespace محتوا
gitea-act-runner-token gitea توکن ثبت Runner
kaniko-harbor-auth cloudhost-builds dockerconfig کاربر harbor_registry_user
gitea-gitops-repo-creds argocd repo credential ریپوی gitops (کاربر ci)
abrban-platform-secrets cloudhost postgres-password، jwt-secret، jwt-refresh-secret، cluster-kubeconfig-key، redis-password
elasticsearch-credentials logging ELASTIC_PASSWORD، FLUENTBIT_PASSWORD (خارج از چارت پلتفرم — elasticsearch-credentials.example.yaml)

چارت Helm با secrets.existingSecret: abrban-platform-secrets در platform/values-abrban.yaml (ریپوی gitops) از Secret ازپیش‌ساخته استفاده می‌کند — Argo CD با helm template نمی‌تواند Secret تصادفی بسازد (lookup خالی است و هر sync مقادیر JWT/Redis را عوض می‌کند).

نمونهٔ کامل values: gitops/platform/values-abrban.example.yaml — شامل mirror ایمیج postgres/redis، BASE_IMAGE_REGISTRY، و envهای Elastic.

Greenfield / ارتقا از نسخهٔ قدیم

اگر کلاستر قبلاً با schema یا namespace قدیمی بالا آمده، قبل از deploy جدید reset دیتابیس لازم است. مراحل کامل (با متغیرهای قابل‌تنظیم برای هر محیط) در RUNBOOK-DEPLOY.fa.md — فاز ۶.

ساخت/به‌روزرسانی یک SealedSecret

brew install kubeseal   # فقط بار اول

kubectl -n <ns> create secret generic <name> --from-literal=key=value --dry-run=client -o json \
  | kubeseal --controller-name=sealed-secrets-controller --controller-namespace=kube-system --format yaml \
  > sealed-secrets/<name>.yaml
# سپس commit/push در ریپوی cloud-host-gitops و kubectl apply (یا sync توسط Argo در آینده)

اگر Secret از قبل در کلاستر وجود دارد و می‌خواهید کنترلر آن را تصاحب کند، اول annotate کنید: kubectl -n <ns> annotate secret <name> sealedsecrets.bitnami.com/managed="true"

Secretهایی که هنوز دستی‌اند (خارج از چرخهٔ CI): abrban-wildcard-tls، registry-pull-secret، registry-egress-proxy، harbor-core (ساختهٔ Helm) — می‌توانند به‌تدریج seal شوند.

نکتهٔ امنیتی: توکن ثبت Runner و پسورد پروکسی که قبلاً در history گیت افشا شده بودند rotate شده‌اند (توکن Runner جدید صادر و Runner دوباره ثبت شد). پسورد کاربر پروکسی (builder) روی سرور پروکسی هنوز باید توسط ادمین عوض شود؛ بعد از تغییر، Secret registry-egress-proxy را در namespaceهای cloudhost و gitea آپدیت کنید.


Best Practiceهای GitOps در این استک (چک‌لیست)

  • Git تنها منبع حقیقت — Argo CD با automated + prune + selfHeal؛ تغییر دستی با kubectl edit برگردانده می‌شود.
  • جداسازی App Repo از GitOps Repo — history تمیز و دسترسی حداقلی CI.
  • تگ Immutable به‌جای latest — هر Build تگ یکتا دارد.
  • جلوگیری از CI Loop — کامیت CI به ریپوی جدا می‌رود که Workflow ندارد.
  • Build بدون Docker Daemon — Kaniko داخل Job، بدون docker.sock و بدون privileged.
  • جداسازی push/pull هاربر — push داخلی بدون عبور از Ingress؛ pull از طریق mirror k3s.
  • Concurrency در Workflow — دو push پشت‌سرهم روی آپدیت values با هم race نمی‌کنند.
  • Secretهای GitOps-شده — Sealed Secrets نصب و secretهای CI رمزشده در Git.
  • محیط Staging — با platform/values-staging.yaml و Application دوم قابل اضافه‌شدن است.
  • Notification — Argo CD Notifications برای اطلاع از Sync موفق/ناموفق.

عیب‌یابی سریع

علامت بررسی
Workflow اجرا نمی‌شود kubectl -n gitea logs deploy/gitea-act-runner — ثبت Runner و لیبل abrban-builder
Build fail — clone معتبربودن Secret CI_TOKEN در تنظیمات ریپوی cloud-host
Build fail — pull ایمیج پایه node:24-alpine باید seed شده باشد؛ Workflow باید BASE_IMAGE=harbor-registry.../abrban/node:24-alpine + --insecure-pull داشته باشد؛ نه pull مستقیم از docker.io
Build fail — UNAUTHORIZED روی registry.abrban.com BASE_IMAGE نباید registry.abrban.com/... باشد — credential کانیکو فقط برای harbor-registry:5000 است
Build fail — HTTP response to HTTPS client --insecure-pull و --insecure-registry=harbor-registry.cloudhost.svc.cluster.local:5000 در Kaniko args
Build fail — timeout npm/helm/kubectl registry-egress-proxy در ns cloudhost-builds و سلامت پروکسی egress
Build fail — push به Harbor kubectl -n cloudhost-builds get secret kaniko-harbor-auth؛ پسورد باید با REGISTRY_CREDENTIAL_PASSWORD هاربر یکی باشد
کامیت values push نمی‌شود دسترسی write کاربر ci روی cloud-host-gitops
Argo sync نمی‌کند kubectl -n argocd get app abrban-platform؛ هر دو repo credential (gitea-repo-creds و gitea-gitops-repo-creds)
Pod ایمیج را pull نمی‌کند registry-pull-secret در ns cloudhost و mirror k3s (scripts/apply-k3s-registries.sh)
دیدن تگ‌های موجود در registry از داخل کلاستر: wget -qO- "http://harbor_registry_user:<REG_PASS>@harbor-registry.cloudhost.svc.cluster.local:5000/v2/abrban/cloudhost-backend/tags/list"
Backend CrashLoop — CLUSTER_KUBECONFIG_KEY Secret abrban-platform-secrets باید کلید cluster-kubeconfig-key داشته باشد و در values: secrets.existingSecret: abrban-platform-secrets
Backend CrashLoop — DB auth پسورد postgres در Secret با DB واقعی هم‌خوان باشد (ALTER USER ... WITH PASSWORD در صورت rotate شدن Secret)
Backend CrashLoop — Redis auth Secret abrban-platform-secrets باید کلید redis-password داشته باشد؛ backend و Redis پلتفرم هر دو از آن استفاده می‌کنند
Backend CrashLoop — ELASTIC_PASSWORD در production مقدار پیش‌فرض رد می‌شود — env در values-abrban.yaml باید رمز rotate‌شده داشته باشد
Workflow fail — tests Job test-be-* در ns cloudhost-buildskubectl logs job/... -c test
SealedSecret باز نمی‌شود kubectl get sealedsecrets -A (ستون SYNCED) و لاگ kubectl -n kube-system logs deploy/sealed-secrets-controller