Files
cloud-host/RUNBOOK-CICD.fa.md
T
keyhan 6ba77eebcf
Build and Deploy Platform / build-and-deploy (push) Failing after 47s
ci: pull node base image from Harbor instead of docker.io
Kaniko builds failed with context deadline exceeded pulling node:24-alpine
from index.docker.io through the egress proxy. Seed node:24-alpine into
abrban/ and pass BASE_IMAGE build-arg so builds use the internal registry.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 18:32:41 +03:30

16 KiB

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

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


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

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. برای هر ایمیج (backend و frontend) یک Kaniko Job در namespace cloudhost-builds ساخته می‌شود که کد را کلون، ایمیج را build و به Harbor push می‌کند.
  5. بعد از موفقیت هر دو Build، همان Runner ریپوی cloud-host-gitops را کلون می‌کند، مقدار images.backend.tag و images.frontend.tag را در platform/values-abrban.yaml عوض و commit/push می‌کند.
  6. Argo CD (Application به نام abrban-platform با sync خودکار) تغییر را تشخیص می‌دهد و نسخهٔ جدید را در namespace cloudhost مستقر می‌کند.

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


ساختار 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 داخلی و بدون TLS است، فلگ‌های --insecure --skip-tls-verify لازم‌اند — این ترافیک از کلاستر خارج نمی‌شود.

عارضهٔ جانبی 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)

ساخت/به‌روزرسانی یک 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 ایمیج پایه (403/timeout از docker.io) Kaniko نباید مستقیم از docker.io بکشد؛ ایمیج node:24-alpine باید در Harbor پروژهٔ abrban/ seed شده باشد (gitops/jobs/seed-ci-images.yaml) و Workflow --build-arg=BASE_IMAGE=registry.abrban.com/abrban/node:24-alpine را پاس می‌دهد
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"
SealedSecret باز نمی‌شود kubectl get sealedsecrets -A (ستون SYNCED) و لاگ kubectl -n kube-system logs deploy/sealed-secrets-controller