From 6d9cd89cc5d40515c4a365ecd6cab8da510192ec Mon Sep 17 00:00:00 2001 From: keyhan Date: Fri, 3 Jul 2026 12:17:55 +0330 Subject: [PATCH] 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 --- README.md | 14 +- RUNBOOK-CICD.fa.md | 23 +- RUNBOOK-DEPLOY.fa.md | 426 ++++++++++++++++++ RUNBOOK.fa.md | 2 + gitops/README.md | 11 +- gitops/platform/values-abrban.example.yaml | 148 ++++++ .../abrban-platform-secrets.example.yaml | 30 ++ .../elasticsearch-credentials.example.yaml | 20 + scripts/gitops-deploy.sh | 7 +- 9 files changed, 668 insertions(+), 13 deletions(-) create mode 100644 RUNBOOK-DEPLOY.fa.md create mode 100644 gitops/platform/values-abrban.example.yaml create mode 100644 gitops/sealed-secrets/abrban-platform-secrets.example.yaml create mode 100644 gitops/sealed-secrets/elasticsearch-credentials.example.yaml diff --git a/README.md b/README.md index 868fdef..544df53 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ custom `wp-content` entrypoint). > Iran-network workarounds — is documented step-by-step in **[RUNBOOK.fa.md](RUNBOOK.fa.md)** (Persian). > 🔄 **CI/CD (Gitea Actions → Kaniko → Harbor → Argo CD):** see **[RUNBOOK-CICD.fa.md](RUNBOOK-CICD.fa.md)** (Persian) and **[gitops/README.md](gitops/README.md)** for bootstrap (`seed-ci-images`, Sealed Secrets, two-repo GitOps layout). +> +> 🚀 **Deploy from zero (any cluster):** **[RUNBOOK-DEPLOY.fa.md](RUNBOOK-DEPLOY.fa.md)** — server checklist, values, secrets, logging, greenfield reset. --- @@ -111,6 +113,8 @@ cloud-host/ ├── README.md # This file ├── ARCHITECTURE.md # Detailed system design ├── RUNBOOK.fa.md # Persian runbook: local dev + abrban/k3s production deploy +├── RUNBOOK-DEPLOY.fa.md # Deploy platform from zero (any cluster): values, secrets, health checks +├── RUNBOOK-CICD.fa.md # CI/CD pipeline: Gitea Actions → Kaniko → Argo CD ├── CHANGELOG.md / CONTRIBUTING.md / UPGRADE.md / UPGRADE.en.md ├── docker-compose.yml # Local dev stack (Postgres + Redis + API + UI) │ @@ -211,9 +215,13 @@ to the backend URL. ## Deploy on Kubernetes (Helm) -> This is the **generic** path. For the production `abrban.com` k3s cluster — base-image -> mirroring, the Iran-network proxy/npmmirror, the wildcard TLS cert, registry bootstrap, -> and the exact image-build flow — follow **[RUNBOOK.fa.md](RUNBOOK.fa.md)**. +> **Production GitOps (from zero):** [`RUNBOOK-DEPLOY.fa.md`](RUNBOOK-DEPLOY.fa.md) — variable table, values, Sealed Secrets, logging, health checks. +> +> **Production abrban.com specifics:** [`RUNBOOK.fa.md`](RUNBOOK.fa.md) — Iran network, Ceph, Harbor details. +> +> **CI/CD pipeline:** [`RUNBOOK-CICD.fa.md`](RUNBOOK-CICD.fa.md). + +This section is the **generic Helm-only** path (Path B in RUNBOOK-DEPLOY) without Gitea/Argo. **Prerequisites:** a Kubernetes cluster, an Ingress controller (Traefik on k3s by default, or set `INGRESS_CLASS=nginx`), a default StorageClass for PVCs, and a container registry diff --git a/RUNBOOK-CICD.fa.md b/RUNBOOK-CICD.fa.md index a5ba949..5554991 100644 --- a/RUNBOOK-CICD.fa.md +++ b/RUNBOOK-CICD.fa.md @@ -2,6 +2,8 @@ این مستند جریان کامل Build و Deploy پلتفرم را توضیح می‌دهد: از Push شدن کد روی `main` تا استقرار خودکار روی Kubernetes. +> **استقرار از صفر روی سرور جدید:** [`RUNBOOK-DEPLOY.fa.md`](RUNBOOK-DEPLOY.fa.md) — شامل جدول متغیرها، seal کردن Secretها، logging stack، greenfield reset، و چک‌لیست سلامت. + --- ## معماری و جریان کلی @@ -26,9 +28,10 @@ flowchart TD 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` مستقر می‌کند. +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 نمی‌شود. @@ -249,9 +252,16 @@ git push origin main | `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** | +| `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`](gitops/sealed-secrets/elasticsearch-credentials.example.yaml)) | -چارت Helm با `secrets.existingSecret: abrban-platform-secrets` در `platform/values-abrban.yaml` (ریپوی gitops) از Secret ازپیش‌ساخته استفاده می‌کند — Argo CD با `helm template` نمی‌تواند Secret تصادفی بسازد (lookup خالی است و هر sync مقادیر JWT را عوض می‌کند). +چارت 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`](gitops/platform/values-abrban.example.yaml) — شامل mirror ایمیج postgres/redis، `BASE_IMAGE_REGISTRY`، و envهای Elastic. + +### Greenfield / ارتقا از نسخهٔ قدیم + +اگر کلاستر قبلاً با schema یا namespace قدیمی بالا آمده، قبل از deploy جدید **reset دیتابیس** لازم است. مراحل کامل (با متغیرهای قابل‌تنظیم برای هر محیط) در **[`RUNBOOK-DEPLOY.fa.md` — فاز ۶](RUNBOOK-DEPLOY.fa.md#فاز-۶--greenfield--ارتقا-از-نسخهٔ-قدیم)**. ### ساخت/به‌روزرسانی یک SealedSecret @@ -305,4 +315,7 @@ Secretهایی که هنوز دستی‌اند (خارج از چرخهٔ CI): `a | دیدن تگ‌های موجود در registry | از داخل کلاستر: `wget -qO- "http://harbor_registry_user:@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-builds` — `kubectl logs job/... -c test` | | SealedSecret باز نمی‌شود | `kubectl get sealedsecrets -A` (ستون SYNCED) و لاگ `kubectl -n kube-system logs deploy/sealed-secrets-controller` | diff --git a/RUNBOOK-DEPLOY.fa.md b/RUNBOOK-DEPLOY.fa.md new file mode 100644 index 0000000..9758d38 --- /dev/null +++ b/RUNBOOK-DEPLOY.fa.md @@ -0,0 +1,426 @@ +# RUNBOOK — استقرار پلتفرم CloudHost از صفر + +این سند **کارهایی را که روی سرور/کلاستر باید انجام دهید** مرحله‌به‌مرحله توضیح می‌دهد — از bootstrap زیرساخت تا اولین deploy موفق پس از hardening. + +> **برای چه کسی است:** هر کسی که می‌خواهد CloudHost را روی یک کلاستر Kubernetes تازه (یا کلاستر دیگری غیر از abrban) بالا بیاورد. +> +> **چه چیزی اینجا نیست:** جزئیات معماری اپ → [`RUNBOOK.fa.md`](RUNBOOK.fa.md)؛ جزئیات pipeline CI → [`RUNBOOK-CICD.fa.md`](RUNBOOK-CICD.fa.md). + +--- + +## قبل از شروع — جدول متغیرها + +همهٔ دستورات زیر از این متغیرها استفاده می‌کنند. **یک‌بار** آن‌ها را برای محیط خودتان پر کنید: + +| متغیر | توضیح | مثال abrban | مثال محیط جدید | +|-------|--------|-------------|----------------| +| `PLATFORM_NS` | namespace پلتفرم | `cloudhost` | `cloudhost` | +| `BUILD_NS` | namespace بیلد Kaniko | `cloudhost-builds` | `cloudhost-builds` | +| `LOGGING_NS` | namespace Elasticsearch | `logging` | `logging` | +| `REGISTRY_HOST` | آدرس pull ایمیج (Ingress/registry عمومی) | `registry.abrban.com` | `registry.example.com` | +| `REGISTRY_PROJECT` | پروژه Harbor برای ایمیج‌های platform | `abrban` | `cloudhost` | +| `REGISTRY_PUSH` | endpoint داخلی push (بدون TLS) | `harbor-registry.cloudhost.svc.cluster.local:5000` | `registry.registry.svc:5000` | +| `GIT_HOST` | URL گیت (Gitea/GitHub) | `git.abrban.com` | `git.example.com` | +| `APP_REPO` | ریپوی کد + چارت | `abrban/cloud-host` | `org/cloud-host` | +| `GITOPS_REPO` | ریپوی state (values + sealed secrets) | `abrban/cloud-host-gitops` | `org/cloud-host-gitops` | +| `VALUES_FILE` | فایل values در gitops | `platform/values-abrban.yaml` | `platform/values-production.yaml` | +| `PLATFORM_SECRET` | Secret پلتفرم (JWT, DB, Redis, …) | `abrban-platform-secrets` | `cloudhost-platform-secrets` | +| `ARGO_APP` | نام Application در Argo CD | `abrban-platform` | `cloudhost-platform` | +| `DOMAIN_LANDING` | لندینگ | `abrban.com` | `example.com` | +| `DOMAIN_PANEL` | پنل | `panel.abrban.com` | `panel.example.com` | +| `DOMAIN_API` | API | `api.abrban.com` | `api.example.com` | +| `DOMAIN_APPS` | دامنهٔ اپ‌های کاربر | `apps.abrban.com` | `apps.example.com` | +| `STORAGE_CLASS` | StorageClass PVCها | `local-path` | `standard` | +| `INGRESS_CLASS` | Ingress controller | `traefik` | `nginx` | + +```bash +# نمونه — قبل از اجرای دستورات export کنید: +export PLATFORM_NS=cloudhost +export BUILD_NS=cloudhost-builds +export LOGGING_NS=logging +export REGISTRY_HOST=registry.example.com +export REGISTRY_PROJECT=cloudhost +export REGISTRY_PUSH=harbor-registry.cloudhost.svc.cluster.local:5000 +export GIT_HOST=git.example.com +export APP_REPO=org/cloud-host +export GITOPS_REPO=org/cloud-host-gitops +export VALUES_FILE=platform/values-production.yaml +export PLATFORM_SECRET=cloudhost-platform-secrets +export ARGO_APP=cloudhost-platform +export DOMAIN_LANDING=example.com +export DOMAIN_PANEL=panel.example.com +export DOMAIN_API=api.example.com +export DOMAIN_APPS=apps.example.com +export STORAGE_CLASS=standard +export INGRESS_CLASS=nginx +``` + +--- + +## دو مسیر استقرار + +| | **مسیر A — GitOps (توصیه Production)** | **مسیر B — Helm مستقیم** | +|---|--------------------------------------|---------------------------| +| CI/CD | Gitea Actions → Kaniko → Argo CD | build/push دستی + `helm upgrade` | +| Values | ریپوی جدا `GITOPS_REPO` | فایل محلی `my-values.yaml` | +| Secretها | Sealed Secrets در gitops | inline در values یا Secret دستی | +| مستند | **همین سند +** [`RUNBOOK-CICD.fa.md`](RUNBOOK-CICD.fa.md) | [`README.md`](README.md) بخش Deploy | + +بقیهٔ این سند **مسیر A** را پوشش می‌دهد. برای مسیر B به انتهای سند بروید. + +--- + +## مسیر A — GitOps: فاز ۰ تا ۷ + +### فاز ۰ — پیش‌نیازهای سخت‌افزاری و شبکه + +- [ ] کلاستر Kubernetes (k3s یا دیگر) با kubectl از ماشین admin +- [ ] DNS: رکوردهای A/CNAME برای `$DOMAIN_LANDING`, `$DOMAIN_PANEL`, `$DOMAIN_API`, `$REGISTRY_HOST`, `$GIT_HOST`, Argo CD +- [ ] گواهی TLS (wildcard یا cert-manager + `clusterIssuer`) +- [ ] دسترسی `kubectl` به کلاستر +- [ ] `helm`, `kubeseal` (برای Sealed Secrets) روی ماشین admin +- [ ] دو ریپوی Git: `$APP_REPO` (کد) و `$GITOPS_REPO` (خالی یا با skeleton) + +--- + +### فاز ۱ — Bootstrap زیرساخت (یک‌بار per cluster) + +این مراحل در [`gitops/README.md`](gitops/README.md) هم هست؛ خلاصه: + +```bash +cd cloud-host # ریپوی اپلیکیشن + +# 1) mirror رجیستری k3s → Harbor (یا registry خودتان) +./scripts/apply-k3s-registries.sh # در صورت k3s؛ برای کلاستر دیگر mirror معادل تنظیم کنید + +# 2) Argo CD +helm upgrade --install argocd argo/argo-cd -n argocd --create-namespace \ + -f gitops/argocd/values-bootstrap.yaml --timeout 15m --wait + +# 3) Gitea (یا GitHub/GitLab — workflow را متناسب تنظیم کنید) +helm upgrade --install gitea gitea-charts/gitea -n gitea --create-namespace \ + -f gitops/gitea/values.yaml --timeout 15m --wait + +# 4) Secretهای TLS + registry-pull + egress در nsهای لازم +# (wildcard TLS و registry-pull-secret را یک‌بار در $PLATFORM_NS بسازید، سپس کپی) +for ns in argocd gitea $BUILD_NS; do + kubectl -n $PLATFORM_NS get secret -o yaml \ + | sed "s/namespace: ${PLATFORM_NS}/namespace: ${ns}/" | kubectl apply -f - + kubectl -n $PLATFORM_NS get secret registry-pull-secret -o yaml \ + | sed "s/namespace: ${PLATFORM_NS}/namespace: ${ns}/" | kubectl apply -f - +done + +# 5) Seed ایمیج‌های CI (act-runner, alpine-git, node, kaniko) — فایل را برای REGISTRY_* خودتان ویرایش کنید +kubectl apply -f gitops/jobs/seed-ci-images.yaml +kubectl -n $PLATFORM_NS wait --for=condition=complete job/seed-ci-images --timeout=15m + +# 6) Sealed Secrets controller +helm repo add sealed-secrets https://bitnami.github.io/sealed-secrets +helm upgrade --install sealed-secrets sealed-secrets/sealed-secrets \ + -n kube-system -f gitops/sealed-secrets/values.yaml --timeout 10m --wait + +# 7) Gitea Actions runner + Secret CI_TOKEN در ریپوی app +kubectl apply -f gitops/gitea/act-runner.yaml +# در Gitea: Settings → Actions → Secrets → CI_TOKEN = PAT کاربر ci + +# 8) Argo CD Application (chart از app repo، values از gitops repo) +# قبل از apply: repoURLها در gitops/argocd/application-platform.yaml را با GIT_HOST/APP_REPO/GITOPS_REPO هم‌خوان کنید +kubectl apply -f gitops/argocd/application-platform.yaml +``` + +**بررسی فاز ۱:** + +```bash +kubectl get nodes +kubectl -n argocd get pods +kubectl -n gitea get pods +kubectl -n kube-system get pods -l app.kubernetes.io/name=sealed-secrets +``` + +--- + +### فاز ۲ — آماده‌سازی ریپوی GitOps (values) + +```bash +# کلون ریپوی gitops (کنار ریپوی app یا هر مسیر دلخواه) +git clone "https://${GIT_HOST}/${GITOPS_REPO}.git" cloud-host-gitops +cd cloud-host-gitops + +# کپی template values از ریپوی app +cp ../cloud-host/gitops/platform/values-abrban.example.yaml "${VALUES_FILE}" +``` + +**فایل values را برای محیط خودتان ویرایش کنید** — حداقل این فیلدها: + +| بخش | چه چیزی عوض شود | +|-----|------------------| +| `images.postgres/redis/busybox` | مسیر mirror در `$REGISTRY_HOST` (مثلاً `proxy-dockerhub/library/postgres:16-alpine`) | +| `images.backend/frontend.repository` | `$REGISTRY_HOST/$REGISTRY_PROJECT/cloudhost-backend` | +| `secrets.existingSecret` | `$PLATFORM_SECRET` | +| `ingress.*.host` | `$DOMAIN_LANDING`, `$DOMAIN_PANEL`, `$DOMAIN_API` | +| `ingress.className` | `$INGRESS_CLASS` | +| `global.storageClass` | `$STORAGE_CLASS` | +| `backend.env.PLATFORM_DOMAIN` | `$DOMAIN_APPS` | +| `backend.env.FRONTEND_URL` | `https://${DOMAIN_PANEL},https://${DOMAIN_LANDING}` | +| `backend.env.REGISTRY_URL` | push داخلی: `$REGISTRY_PUSH/$REGISTRY_PROJECT` | +| `backend.env.REGISTRY_PULL_URL` | `$REGISTRY_HOST/$REGISTRY_PROJECT` | +| `backend.env.BASE_IMAGE_REGISTRY` | prefix mirror برای Dockerfileهای کاربر | +| `backend.env.ELASTIC_*` | بعد از فاز ۴ پر می‌شود | +| `postgres/redis.imagePullSecrets` | `[{ name: registry-pull-secret }]` | + +```bash +git add "${VALUES_FILE}" +git commit -m "chore: initial platform values for $(hostname -s 2>/dev/null || echo production)" +git push origin main +``` + +> **نکته:** CI فقط `images.backend.tag` و `images.frontend.tag` را عوض می‌کند — بقیهٔ فایل دست شماست. + +--- + +### فاز ۳ — Secretهای پلتفرم (Sealed Secrets) + +Secret پلتفرم **نباید** در values به‌صورت plaintext commit شود. از SealedSecret استفاده کنید. + +**کلیدهای الزامی** در `$PLATFORM_SECRET`: + +| کلید | کاربرد | +|------|--------| +| `postgres-password` | Postgres پلتفرم + migration Job | +| `jwt-secret` | JWT access (حداقل ۳۲ کاراکتر تصادفی) | +| `jwt-refresh-secret` | JWT refresh | +| `cluster-kubeconfig-key` | رمزگذاری kubeconfig کلاسترها (۶۴ hex یا passphrase قوی) | +| `redis-password` | Redis پلتفرم + backend (Bull queues) | + +```bash +cd cloud-host-gitops + +# تولید رمزهای تصادفی (یا خودتان مقدار قوی بگذارید) +PG_PASS="$(openssl rand -base64 24)" +JWT="$(openssl rand -base64 32)" +JWT_REFRESH="$(openssl rand -base64 32)" +KUBE_KEY="$(openssl rand -hex 32)" +REDIS_PASS="$(openssl rand -base64 24)" + +kubectl -n $PLATFORM_NS create secret generic "$PLATFORM_SECRET" \ + --from-literal=postgres-password="$PG_PASS" \ + --from-literal=jwt-secret="$JWT" \ + --from-literal=jwt-refresh-secret="$JWT_REFRESH" \ + --from-literal=cluster-kubeconfig-key="$KUBE_KEY" \ + --from-literal=redis-password="$REDIS_PASS" \ + --dry-run=client -o json \ +| kubeseal \ + --controller-name=sealed-secrets-controller \ + --controller-namespace=kube-system \ + --format yaml \ +> "sealed-secrets/${PLATFORM_SECRET}.yaml" + +kubectl apply -f "sealed-secrets/${PLATFORM_SECRET}.yaml" +git add "sealed-secrets/${PLATFORM_SECRET}.yaml" +git commit -m "chore: seal platform secrets" +git push origin main +``` + +**بررسی:** + +```bash +kubectl -n $PLATFORM_NS get secret "$PLATFORM_SECRET" +kubectl get sealedsecrets -A | grep "$PLATFORM_SECRET" +``` + +SealedSecretهای CI دیگر (kaniko، runner، repo creds) را طبق [`RUNBOOK-CICD.fa.md`](RUNBOOK-CICD.fa.md) بسازید. + +--- + +### فاز ۴ — Logging stack + Secret Elasticsearch + +```bash +cd cloud-host + +# 1) namespace logging (اگر در manifest نیست) +kubectl create namespace $LOGGING_NS --dry-run=client -o yaml | kubectl apply -f - + +# 2) Secret elasticsearch — خارج از git (plaintext commit ممنوع) +ELASTIC_PASS="$(openssl rand -base64 24)" +FLUENT_PASS="$(openssl rand -base64 24)" + +kubectl -n $LOGGING_NS create secret generic elasticsearch-credentials \ + --from-literal=ELASTIC_PASSWORD="$ELASTIC_PASS" \ + --from-literal=FLUENTBIT_PASSWORD="$FLUENT_PASS" + +# یا seal کنید: +kubectl -n $LOGGING_NS create secret generic elasticsearch-credentials \ + --from-literal=ELASTIC_PASSWORD="$ELASTIC_PASS" \ + --from-literal=FLUENTBIT_PASSWORD="$FLUENT_PASS" \ + --dry-run=client -o json \ +| kubeseal --controller-name=sealed-secrets-controller \ + --controller-namespace=kube-system --format yaml \ +> ../cloud-host-gitops/sealed-secrets/elasticsearch-credentials.yaml + +# 3) deploy stack (بدون Secret inline — manifest فقط ConfigMap/Deployment دارد) +kubectl apply -f backend/k8s/logging/elasticsearch-stack.yaml + +# 4) همان مقادیر را در values پلتفرم بگذارید (backend.env) +# ELASTIC_PASSWORD, FLUENTBIT_PASSWORD, KIBANA_SYSTEM_PASSWORD +# سپس commit/push در gitops repo +``` + +> backend در production بدون `ELASTIC_PASSWORD` معتبر **بالا نمی‌آید** (`validate-production-config`). + +--- + +### فاز ۵ — اولین Deploy + +**روش ۱ — CI (توصیه):** push به `main` در `$APP_REPO` → workflow تست + Kaniko + آپدیت tag در gitops → Argo sync. + +```bash +cd cloud-host +git push origin main # یا push به Gitea remote +# پیگیری: Gitea Actions UI یا kubectl -n $BUILD_NS get jobs -w +``` + +**روش ۲ — دستی (bootstrap / بدون CI):** + +```bash +# build ایمیج‌ها (روی ماشینی که به registry دسترسی دارد) یا trigger-platform-build.sh +TAG="$(date +%Y%m%d-%H%M)-manual" +VALUES="../cloud-host-gitops/${VALUES_FILE}" +./scripts/gitops-deploy.sh TAG="$TAG" VALUES="$VALUES" +``` + +**بررسی Argo:** + +```bash +kubectl -n argocd get app "$ARGO_APP" +argocd app sync "$ARGO_APP" # در صورت sync خودکار غیرفعال +kubectl -n $PLATFORM_NS get pods +kubectl -n $PLATFORM_NS rollout status deploy/cloudhost-backend --timeout=300s +kubectl -n $PLATFORM_NS rollout status deploy/cloudhost-frontend --timeout=300s +``` + +--- + +### فاز ۶ — Greenfield / ارتقا از نسخهٔ قدیم + +اگر کلاستر **قبلاً** با نسخهٔ قدیمی CloudHost بالا آمده (namespace کوتاه UUID، migration بدون `schema_migrations`)، **قبل از deploy جدید** دیتابیس را reset کنید. + +> ⚠️ **فقط greenfield / بدون دادهٔ واقعی.** در production با داده، اول backup بگیرید. + +```bash +# 1) backend را متوقف کنید +kubectl -n $PLATFORM_NS scale deploy/cloudhost-backend --replicas=0 + +# 2) schema را از نو بسازید +kubectl -n $PLATFORM_NS exec deploy/cloudhost-postgres -- \ + psql -U cloudhost -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;' + +# 3) Argo sync — migration Job (pre-upgrade hook) base schema + migrations را اجرا می‌کند +argocd app sync "$ARGO_APP" + +# 4) backend را بالا بیاورید +kubectl -n $PLATFORM_NS scale deploy/cloudhost-backend --replicas=1 +``` + +**تغییرات breaking که reset می‌خواهند:** + +| تغییر | اثر | +|-------|-----| +| namespace کاربر `user-` به‌جای `user-<8char>` | namespaceهای قدیمی دیگر استفاده نمی‌شوند — اپ‌ها redeploy | +| `000_base_schema.sql` + `schema_migrations` | DB باید از نو migrate شود | +| `redis-password` جدید | Secret + restart Redis و backend | + +--- + +### فاز ۷ — چک‌لیست تأیید سلامت + +```bash +# Podها +kubectl -n $PLATFORM_NS get deploy,pods +kubectl -n $LOGGING_NS get pods + +# API +curl -sf "https://${DOMAIN_API}/api/v1/health" && echo OK +curl -sf "https://${DOMAIN_API}/api/v1/ready" && echo OK + +# Frontend +curl -sf -o /dev/null -w '%{http_code}\n' "https://${DOMAIN_LANDING}" +curl -sf -o /dev/null -w '%{http_code}\n' "https://${DOMAIN_PANEL}" + +# Migration +kubectl -n $PLATFORM_NS logs job/$(kubectl -n $PLATFORM_NS get jobs -o name | grep migration | tail -1 | cut -d/ -f2) 2>/dev/null || true + +# Redis auth +kubectl -n $PLATFORM_NS exec deploy/cloudhost-redis -- redis-cli ping + +# Backup CronJob (اگر enabled) +kubectl -n $PLATFORM_NS get cronjobs +``` + +| علامت | اقدام | +|-------|-------| +| Backend CrashLoop — JWT/DB/Redis | Secret `$PLATFORM_SECRET` و keys — [`RUNBOOK-CICD.fa.md`](RUNBOOK-CICD.fa.md) عیب‌یابی | +| Backend CrashLoop — ELASTIC_PASSWORD | env در values + Secret logging | +| Migration fail | `kubectl logs` روی migration Job؛ `schema_migrations` و فایل‌های `backend/migrations/` | +| Argo OutOfSync | `argocd app diff $ARGO_APP` | + +--- + +## مسیر B — Helm مستقیم (بدون GitOps) + +برای lab، staging، یا کلاستری **بدون** Gitea/Argo: + +```bash +cp backend/helm/cloudhost-platform/values-production.example.yaml my-values.yaml +# ویرایش: hosts, registry, secrets (jwtSecret, postgres.password, redis.password), ingress + +docker build -t $REG/cloudhost-backend:1.0.0 ./backend +docker build -t $REG/cloudhost-frontend:1.0.0 \ + --build-arg NEXT_PUBLIC_API_URL=https://${DOMAIN_API} ./frontend +docker push $REG/cloudhost-backend:1.0.0 +docker push $REG/cloudhost-frontend:1.0.0 + +helm upgrade --install cloudhost ./backend/helm/cloudhost-platform \ + -n $PLATFORM_NS --create-namespace \ + -f my-values.yaml \ + --set images.backend.repository=$REG/cloudhost-backend \ + --set images.frontend.repository=$REG/cloudhost-frontend \ + --set images.backend.tag=1.0.0 \ + --set images.frontend.tag=1.0.0 \ + --set global.storageClass=$STORAGE_CLASS +``` + +> در این مسیر `secrets.existingSecret` خالی بماند تا Helm Secret بسازد — **برای production با Argo CD توصیه نمی‌شود** (lookup در `helm template` خالی است). + +--- + +## فایل‌های مرجع در ریپو + +| فایل | نقش | +|------|-----| +| [`gitops/platform/values-abrban.example.yaml`](gitops/platform/values-abrban.example.yaml) | Template values — کپی و rename برای محیط جدید | +| [`gitops/sealed-secrets/abrban-platform-secrets.example.yaml`](gitops/sealed-secrets/abrban-platform-secrets.example.yaml) | دستور seal Secret پلتفرم | +| [`gitops/sealed-secrets/elasticsearch-credentials.example.yaml`](gitops/sealed-secrets/elasticsearch-credentials.example.yaml) | دستور seal Secret logging | +| [`backend/helm/cloudhost-platform/values-production.example.yaml`](backend/helm/cloudhost-platform/values-production.example.yaml) | Template برای مسیر B | +| [`scripts/gitops-deploy.sh`](scripts/gitops-deploy.sh) | deploy دستی با Helm + values از gitops | + +--- + +## خلاصهٔ ترتیب (Quick reference) + +``` +فاز ۰ DNS + kubectl + helm + kubeseal + دو ریپو + ↓ +فاز ۱ Argo + Gitea + registry + sealed-secrets + runner + Application + ↓ +فاز ۲ کپی values template → ویرایش → push gitops + ↓ +فاز ۳ seal platform secrets → push gitops + ↓ +فاز ۴ elasticsearch stack + secret + env در values + ↓ +فاز ۵ push main (CI) یا gitops-deploy.sh (دستی) + ↓ +فاز ۶ (در صورت upgrade) reset DB + ↓ +فاز ۷ health check +``` diff --git a/RUNBOOK.fa.md b/RUNBOOK.fa.md index 73d3840..349e201 100644 --- a/RUNBOOK.fa.md +++ b/RUNBOOK.fa.md @@ -8,6 +8,8 @@ > **به‌روزرسانی ۲۰۲۶:** pipeline بیلد فعلی از **Kaniko** + Dockerfileهای نگهداری‌شده توسط پلتفرم استفاده می‌کند (نه Nixpacks/MinIO). آرشیو سورس روی دیسک/PVC آپلود می‌شود. manifest بوت‌استرپ namespace بیلد: [`backend/k8s/builds/cloudhost-builds-bootstrap.yaml`](backend/k8s/builds/cloudhost-builds-bootstrap.yaml). +> **استقرار از صفر روی سرور:** [`RUNBOOK-DEPLOY.fa.md`](RUNBOOK-DEPLOY.fa.md) — مراحل values، Secretها، deploy، greenfield reset. + --- ### ۱.۱ CloudHost چیست diff --git a/gitops/README.md b/gitops/README.md index 19ab905..a2993c8 100644 --- a/gitops/README.md +++ b/gitops/README.md @@ -1,5 +1,7 @@ # GitOps stack for abrban.com +> **راهنمای استقرار از صفر (هر محیط):** [`RUNBOOK-DEPLOY.fa.md`](../RUNBOOK-DEPLOY.fa.md) — متغیرها، values، Sealed Secrets، logging، deploy، greenfield reset. + ## DNS (A record → cluster IP `78.157.39.52`) | Host | Purpose | @@ -84,8 +86,9 @@ kubectl apply -f gitops/argocd/application-platform.yaml Gitea Actions: [.gitea/workflows/build-deploy.yaml](../.gitea/workflows/build-deploy.yaml) -Push به `main` → Kaniko → push به `abrban/` → کامیت tag در ریپوی [cloud-host-gitops](https://git.abrban.com/abrban/cloud-host-gitops) → ArgoCD sync. +Push به `main` → **تست Jest** → Kaniko → push به `abrban/` → کامیت tag در ریپوی [cloud-host-gitops](https://git.abrban.com/abrban/cloud-host-gitops) → ArgoCD sync. -- مقادیر Production در ریپوی جدا `abrban/cloud-host-gitops` است (`platform/values-abrban.yaml`)؛ Application به‌صورت multi-source تعریف شده. -- Secretهای CI به‌صورت SealedSecret در همان ریپو هستند (کنترلر در `kube-system`، values در `gitops/sealed-secrets/values.yaml`). -- مستند کامل: [RUNBOOK-CICD.fa.md](../RUNBOOK-CICD.fa.md) +- **استقرار اولیه از صفر:** [`RUNBOOK-DEPLOY.fa.md`](../RUNBOOK-DEPLOY.fa.md) (فاز ۲–۷) +- مقادیر Production: [`platform/values-abrban.example.yaml`](platform/values-abrban.example.yaml) → کپی به gitops و ویرایش +- SealedSecretهای نمونه: [`sealed-secrets/abrban-platform-secrets.example.yaml`](sealed-secrets/abrban-platform-secrets.example.yaml)، [`sealed-secrets/elasticsearch-credentials.example.yaml`](sealed-secrets/elasticsearch-credentials.example.yaml) +- Pipeline و rollback: [RUNBOOK-CICD.fa.md](../RUNBOOK-CICD.fa.md) diff --git a/gitops/platform/values-abrban.example.yaml b/gitops/platform/values-abrban.example.yaml new file mode 100644 index 0000000..6adff0a --- /dev/null +++ b/gitops/platform/values-abrban.example.yaml @@ -0,0 +1,148 @@ +# Production values template — copy and customize for YOUR environment. +# +# Full step-by-step (from zero, any cluster): +# See RUNBOOK-DEPLOY.fa.md — Phase 2 (values) and Phase 3 (secrets) +# +# Example for abrban.com: +# cp values-abrban.example.yaml ../cloud-host-gitops/platform/values-abrban.yaml +# +# CI only updates images.backend.tag and images.frontend.tag on each deploy. + +namespace: cloudhost +createNamespace: false + +global: + storageClass: local-path + +images: + # Harbor proxy-cache — first pull is slow, no manual seed needed (see gitops/README.md) + postgres: registry.abrban.com/proxy-dockerhub/library/postgres:16-alpine + redis: registry.abrban.com/proxy-dockerhub/library/redis:7-alpine + busybox: registry.abrban.com/proxy-dockerhub/library/busybox:1.36 + backend: + repository: registry.abrban.com/abrban/cloudhost-backend + tag: "1.0.0" # ← CI overwrites on each deploy + pullPolicy: IfNotPresent + frontend: + repository: registry.abrban.com/abrban/cloudhost-frontend + tag: "1.0.0" # ← CI overwrites on each deploy + pullPolicy: IfNotPresent + +postgres: + enabled: true + database: cloudhost + username: cloudhost + password: "" # managed in abrban-platform-secrets (postgres-password) + storage: 10Gi + imagePullSecrets: + - name: registry-pull-secret + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + +redis: + enabled: true + storage: 1Gi + password: "" # managed in abrban-platform-secrets (redis-password) + imagePullSecrets: + - name: registry-pull-secret + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 512Mi + +# GitOps: never let Helm generate random JWT/redis passwords on each sync. +# Create once with kubeseal — see gitops/sealed-secrets/abrban-platform-secrets.example.yaml +secrets: + existingSecret: abrban-platform-secrets + +backend: + enabled: true + replicas: 1 + imagePullSecrets: + - name: registry-pull-secret + uploads: + size: 20Gi + sourceStorage: + enabled: false + existingSecret: ceph-app-sources-credentials + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + env: + NODE_ENV: production + PORT: "4000" + JWT_EXPIRES_IN: 15m + JWT_REFRESH_EXPIRES_IN: 7d + PLATFORM_DOMAIN: apps.abrban.com + PREVIEW_BASE_DOMAIN: apps.abrban.com + FRONTEND_URL: https://panel.abrban.com,https://abrban.com + REGISTRY_URL: harbor-registry.cloudhost.svc.cluster.local:5000/abrban + REGISTRY_PULL_URL: registry.abrban.com/abrban + BUILD_NAMESPACE: cloudhost-builds + BUILD_SERVICE_ACCOUNT: kaniko-builder + UPLOAD_DIR: /app/uploads + PLATFORM_CREATE_STORAGE_CLASS: "true" + PLATFORM_STORAGE_CLASS: cloudhost-expandable + PLATFORM_STORAGE_PROVISIONER: rancher.io/local-path + ELASTICSEARCH_HOST: elasticsearch.logging.svc.cluster.local + ELASTICSEARCH_AUTO_PORT_FORWARD: "false" + # Mirror prefix for user-app Dockerfiles and managed DB/Redis/RabbitMQ charts + BASE_IMAGE_REGISTRY: registry.abrban.com/proxy-dockerhub/library + # Must match elasticsearch-credentials Secret in logging namespace (not in Helm chart) + ELASTIC_PASSWORD: "CHANGE_VIA_SEALEDSECRET_OR_KUBECTL" + FLUENTBIT_PASSWORD: "CHANGE_VIA_SEALEDSECRET_OR_KUBECTL" + KIBANA_SYSTEM_PASSWORD: "CHANGE_VIA_SEALEDSECRET_OR_KUBECTL" + # Swagger disabled in production unless explicitly enabled + # SWAGGER_ENABLED: "true" + +frontend: + enabled: true + replicas: 1 + imagePullSecrets: + - name: registry-pull-secret + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 1Gi + +ingress: + enabled: true + className: traefik + frontend: + host: abrban.com + panel: + host: panel.abrban.com + api: + host: api.abrban.com + tls: + enabled: true + clusterIssuer: letsencrypt-prod + +migrations: + enabled: true + image: registry.abrban.com/proxy-dockerhub/library/postgres:16-alpine + +backups: + postgres: + enabled: true + schedule: "0 3 * * *" + storageSize: 10Gi + retentionDays: 7 + +monitoring: + enabled: false diff --git a/gitops/sealed-secrets/abrban-platform-secrets.example.yaml b/gitops/sealed-secrets/abrban-platform-secrets.example.yaml new file mode 100644 index 0000000..34db3ff --- /dev/null +++ b/gitops/sealed-secrets/abrban-platform-secrets.example.yaml @@ -0,0 +1,30 @@ +# Example: seal platform secrets for namespace cloudhost. +# Full guide (any environment): RUNBOOK-DEPLOY.fa.md — Phase 3 +# Real SealedSecret lives in cloud-host-gitops/sealed-secrets/ — never commit plaintext passwords. +# +# Required keys (must match backend Deployment + validate-production-config): +# postgres-password, jwt-secret, jwt-refresh-secret, cluster-kubeconfig-key, redis-password +# +# Generate (replace CHANGE_ME_* with strong random values): +# +# kubectl -n cloudhost create secret generic abrban-platform-secrets \ +# --from-literal=postgres-password='CHANGE_ME_PG' \ +# --from-literal=jwt-secret='CHANGE_ME_JWT_32CHARS_MIN' \ +# --from-literal=jwt-refresh-secret='CHANGE_ME_REFRESH_32CHARS_MIN' \ +# --from-literal=cluster-kubeconfig-key='0123456789abcdef0123456789abcdef' \ +# --from-literal=redis-password='CHANGE_ME_REDIS' \ +# --dry-run=client -o json \ +# | kubeseal \ +# --controller-name=sealed-secrets-controller \ +# --controller-namespace=kube-system \ +# --format yaml \ +# > ../cloud-host-gitops/sealed-secrets/abrban-platform-secrets.yaml +# +# Then in platform/values-abrban.yaml: +# secrets: +# existingSecret: abrban-platform-secrets +# +# Apply: +# kubectl apply -f ../cloud-host-gitops/sealed-secrets/abrban-platform-secrets.yaml +# +# Rotate redis-password: update SealedSecret, sync Argo, restart backend + redis pods. diff --git a/gitops/sealed-secrets/elasticsearch-credentials.example.yaml b/gitops/sealed-secrets/elasticsearch-credentials.example.yaml new file mode 100644 index 0000000..1f32ee4 --- /dev/null +++ b/gitops/sealed-secrets/elasticsearch-credentials.example.yaml @@ -0,0 +1,20 @@ +# Example: seal Elasticsearch stack credentials (namespace logging). +# Full guide (any environment): RUNBOOK-DEPLOY.fa.md — Phase 4 +# Apply elasticsearch-stack.yaml FIRST (without inline passwords), then create this Secret. +# +# kubectl -n logging create secret generic elasticsearch-credentials \ +# --from-literal=ELASTIC_PASSWORD="$(openssl rand -base64 24)" \ +# --from-literal=FLUENTBIT_PASSWORD="$(openssl rand -base64 24)" \ +# --dry-run=client -o json \ +# | kubeseal \ +# --controller-name=sealed-secrets-controller \ +# --controller-namespace=kube-system \ +# --format yaml \ +# > ../cloud-host-gitops/sealed-secrets/elasticsearch-credentials.yaml +# +# Backend must receive the same ELASTIC_* values via backend.env in values-abrban.yaml +# (or a separate SealedSecret referenced with envFrom). +# +# After deploy, verify: +# kubectl -n logging get secret elasticsearch-credentials +# curl -u elastic:$ELASTIC_PASSWORD https://elasticsearch.logging.svc.cluster.local:9200 diff --git a/scripts/gitops-deploy.sh b/scripts/gitops-deploy.sh index eb9ecd1..92bc827 100755 --- a/scripts/gitops-deploy.sh +++ b/scripts/gitops-deploy.sh @@ -6,13 +6,18 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" NAMESPACE="${NAMESPACE:-cloudhost}" RELEASE="${RELEASE:-cloudhost}" # Production values now live in the cloud-host-gitops repo (platform/values-abrban.yaml). +# Fallback: example template in this repo for bootstrap / local helm. VALUES="${VALUES:-${ROOT}/../cloud-host-gitops/platform/values-abrban.yaml}" +if [[ ! -f "${VALUES}" ]]; then + VALUES="${ROOT}/gitops/platform/values-abrban.example.yaml" +fi TAG="${TAG:-}" if [[ ! -f "${VALUES}" ]]; then - echo "ERROR: values file not found: ${VALUES}" >&2 + echo "ERROR: values file not found." >&2 echo "Clone the GitOps repo next to this one, or pass VALUES=/path/to/values-abrban.yaml:" >&2 echo " git clone https://git.abrban.com/abrban/cloud-host-gitops.git" >&2 + echo "Or copy gitops/platform/values-abrban.example.yaml to your gitops repo." >&2 exit 1 fi