# RUNBOOK — خط CI/CD (Gitea Actions → Kaniko → Harbor → Argo CD) این مستند جریان کامل Build و Deploy پلتفرم را توضیح می‌دهد: از Push شدن کد روی `main` تا استقرار خودکار روی Kubernetes. --- ## معماری و جریان کلی ```mermaid 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`](.gitea/workflows/build-deploy.yaml) روی Runner با لیبل `abrban-builder` اجرا می‌شود. 3. Runner کد را با توکن CI کلون می‌کند و تگ ایمیج (`YYYYMMDD-HHMM-`) را می‌سازد. 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`: ```yaml 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`: ```bash 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`](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-` (مثلاً `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` عوض می‌کند: ```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`: ```bash 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 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`](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 ```bash brew install kubeseal # فقط بار اول kubectl -n create secret generic --from-literal=key=value --dry-run=client -o json \ | kubeseal --controller-name=sealed-secrets-controller --controller-namespace=kube-system --format yaml \ > sealed-secrets/.yaml # سپس commit/push در ریپوی cloud-host-gitops و kubectl apply (یا sync توسط Argo در آینده) ``` > اگر Secret از قبل در کلاستر وجود دارد و می‌خواهید کنترلر آن را تصاحب کند، اول annotate کنید: > `kubectl -n annotate secret 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 در این استک (چک‌لیست) - [x] **Git تنها منبع حقیقت** — Argo CD با `automated + prune + selfHeal`؛ تغییر دستی با `kubectl edit` برگردانده می‌شود. - [x] **جداسازی App Repo از GitOps Repo** — history تمیز و دسترسی حداقلی CI. - [x] **تگ Immutable به‌جای `latest`** — هر Build تگ یکتا دارد. - [x] **جلوگیری از CI Loop** — کامیت CI به ریپوی جدا می‌رود که Workflow ندارد. - [x] **Build بدون Docker Daemon** — Kaniko داخل Job، بدون `docker.sock` و بدون privileged. - [x] **جداسازی push/pull هاربر** — push داخلی بدون عبور از Ingress؛ pull از طریق mirror k3s. - [x] **Concurrency در Workflow** — دو push پشت‌سرهم روی آپدیت values با هم race نمی‌کنند. - [x] **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 — 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:@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` |