# 🚀 استقرار، CI/CD و زیرساخت > **منابع ادغام‌شده:** `CICD-PIPELINE-GUIDE.md`, `DEPLOYMENT-README.md`, `INFRASTRUCTURE-GUIDE.md`, `INGRESS-NGINX-WARNING.md`, `OFFLINE-DEPLOYMENT-GUIDE.md`, `SERVER-MIRRORS-CONFIG.md` > **آخرین بروزرسانی:** اسفند ۱۴۰۴ (بروزرسانی: PVC آپلود + K8s Secret برای config دائمی + جدا کردن appsettings هر برنچ) --- ## ۱. سرورها | سرور | IP | نقش | منابع | |------|-----|------|--------| | **Staging** | 194.5.195.53 | توسعه + تست | 4 CPU, 8GB RAM | | **Production** | 45.149.79.127 | محیط نهایی | 4 CPU, 16GB RAM | | **Git** | git.se.kbs1.ir | Gitea (مخازن کد) | — | | **Registry** | داخلی | Docker Registry / Nexus | — | --- ## ۲. Docker و Container ### ۲.۱ سرویس‌ها ```yaml # docker-compose.yml (production) services: cms: image: foursat/cms:latest ports: ["5001:5001"] # gRPC environment: - ConnectionStrings__Default=Server=db;Database=FourSatCMS - ASPNETCORE_ENVIRONMENT=Production depends_on: [db] backoffice: image: foursat/backoffice:latest ports: ["5002:80"] # Static Blazor WASM frontoffice: image: foursat/frontoffice:latest ports: ["5003:5003"] # Blazor Server db: image: mcr.microsoft.com/mssql/server:2022-CU16-ubuntu-22.04 ports: ["1433:1433"] volumes: ["sqldata:/var/opt/mssql"] nexus: # NuGet + Docker registry image: sonatype/nexus3 ports: ["8081:8081"] volumes: sqldata: ``` ### ۲.۲ Dockerfile (CMS) ```dockerfile FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 5001 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["CMSMicroservice/CMSMicroservice.csproj", "CMSMicroservice/"] RUN dotnet restore COPY . . RUN dotnet publish -c Release -o /app/publish FROM base AS final COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "CMSMicroservice.dll"] ``` --- ## ۳. Kubernetes ### ۳.۱ Manifests ساختار مانیفست‌های K8s **داخل ریپوی CMS** نگهداری می‌شن و توسط CI/CD اعمال می‌شن: ``` CMS/ k8s/ staging/ cms-config.yaml ← K8s Secret (appsettings.Staging.json) cms-deployment.yaml ← PVC + Deployment + Service + Ingress production/ cms-config.yaml ← K8s Secret (appsettings.Production.json) cms-deployment.yaml ← PVC + Deployment + Service + Ingress ``` > ⚠️ **هر دو محیط از namespace `default` استفاده می‌کنن.** ### ۳.۲ PersistentVolume برای آپلود فایل فایل‌های آپلود‌شده (عکس محصولات، بلاگ، آواتار و ...) در `/app/Uploads` ذخیره می‌شن. برای جلوگیری از حذف فایل‌ها با ریستارت Pod، یک **PersistentVolumeClaim** مونت شده: ```yaml # PVC — 20Gi ذخیره‌سازی دائمی apiVersion: v1 kind: PersistentVolumeClaim metadata: name: cms-uploads-pvc namespace: default spec: accessModes: [ReadWriteOnce] resources: requests: storage: 20Gi ``` ```yaml # Volume Mount در Deployment volumeMounts: - name: cms-uploads mountPath: /app/Uploads volumes: - name: cms-uploads persistentVolumeClaim: claimName: cms-uploads-pvc ``` | تنظیم | مقدار | |--------|-------| | **PVC Name** | `cms-uploads-pvc` | | **Mount Path** | `/app/Uploads` | | **Access Mode** | `ReadWriteOnce` | | **حجم** | `20Gi` | | **StorageClass** | `local-path` (K3s default) | | **Replicas** | `1` (محدودیت RWO) | > 💡 **نکته مهم:** چون `ReadWriteOnce` هست، فقط **1 replica** می‌تونه بنویسه. برای 2+ replica نیاز به NFS/CephFS با `ReadWriteMany` هست. ### ۳.۳ تنظیمات محیطی (K8s Secret) تنظیمات حساس (ConnectionString, Email, SMS, ZarinPal) **در K8s Secret** نگهداری می‌شن — نه داخل Docker image. فایل `appsettings.{Environment}.json` از Secret به `/app/` مونت می‌شه و .NET اون رو override می‌خونه. ```mermaid flowchart LR S["K8s Secret
cms-appsettings"] -->|volumeMount| F["/app/appsettings.*.json"] F --> D[".NET reads config"] I["Docker Image
appsettings.json (base)"] --> D ``` | محیط | `ASPNETCORE_ENVIRONMENT` | فایل Config (از Secret) | |------|---------------------------|-------------| | **Staging** | `Staging` | `appsettings.Staging.json` | | **Production** | `Production` | `appsettings.Production.json` | **Secret manifest** (`cms-config.yaml`): ```yaml apiVersion: v1 kind: Secret metadata: name: cms-appsettings namespace: default type: Opaque stringData: appsettings.Staging.json: | # یا appsettings.Production.json { "ConnectionStrings": { ... }, "ZarinPal": { ... }, ... } ``` **Volume mount در Deployment:** ```yaml volumeMounts: - name: cms-config mountPath: /app/appsettings.Staging.json subPath: appsettings.Staging.json readOnly: true volumes: - name: cms-config secret: secretName: cms-appsettings ``` env var‌های K8s manifest (فقط environment و URL): ```yaml env: - name: ASPNETCORE_ENVIRONMENT value: "Staging" # یا "Production" - name: ASPNETCORE_URLS value: "http://+:8080" ``` > 💡 **تغییر config بدون deploy:** `kubectl edit secret cms-appsettings && kubectl rollout restart deployment/cms` ### ۳.۴ مثال Deployment (واقعی) ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: cms namespace: default spec: replicas: 1 selector: matchLabels: app: cms template: spec: containers: - name: cms image: 194.5.195.53:30080/admin/cms:latest imagePullPolicy: Always ports: - containerPort: 8080 env: - name: ASPNETCORE_ENVIRONMENT value: "Staging" - name: ASPNETCORE_URLS value: "http://+:8080" volumeMounts: - name: cms-uploads mountPath: /app/Uploads - name: cms-config mountPath: /app/appsettings.Staging.json subPath: appsettings.Staging.json readOnly: true resources: requests: { memory: "512Mi", cpu: "500m" } limits: { memory: "1Gi", cpu: "1000m" } volumes: - name: cms-uploads persistentVolumeClaim: claimName: cms-uploads-pvc - name: cms-config secret: secretName: cms-appsettings ``` ### ۳.۵ Ingress **Staging:** ```yaml spec: ingressClassName: nginx rules: - host: cms.se.kbs1.ir ``` **Production:** ```yaml spec: ingressClassName: nginx tls: - hosts: [cms.kbs1.ir, cms.kbs2.ir] secretName: cms-tls rules: - host: cms.kbs2.ir - host: cms.kbs1.ir ``` > ⚠️ **هشدار:** از `spec.ingressClassName: nginx` استفاده کنید، نه `kubernetes.io/ingress.class` annotation (deprecated). ### ۳.۶ جداسازی appsettings در Git هر برنچ فقط فایل config مربوط به محیط خودش رو داره: | برنچ | `appsettings.json` | `appsettings.Staging.json` | `appsettings.Production.json` | |------|---|---|---| | `kub-stage` | ✅ | ✅ | ❌ حذف شده | | `production` | ✅ | ❌ حذف شده | ✅ | **چرا؟** چون config اصلی از K8s Secret میاد (`cms-config.yaml`)، فایل‌های محیط دیگه داخل ایمیج اضافی و گمراه‌کننده‌ان. همچنین وقتی merge/cherry-pick می‌کنید، فایل config محیط دیگه دیگه conflict ایجاد نمی‌کنه. > ⚠️ **کامیت‌های حذف فایل config رو هرگز cherry-pick نکنید به برنچ دیگه!** > `e72673c` (حذف Production از staging) و `3ebe0f9` (حذف Staging از production) ### ۳.۷ خلاصه: چه چیزهایی دائمی هستند (مستقل از ایمیج) | چه چیزی | مکانیزم K8s | محل Mount | |---------|-------------|------------| | **فایل‌های آپلود** (عکس، آواتار، ...) | `PersistentVolumeClaim` | `/app/Uploads` | | **تنظیمات اپلیکیشن** (DB, SMS, IPG, ...) | `Secret` (`cms-appsettings`) | `/app/appsettings.{Env}.json` | --- ## ۴. CI/CD Pipeline ### ۴.۱ Gitea Actions Workflows (CMS) فایل‌های پایپلاین: ``` CMS/.gitea/workflows/ ├── kub-deploy.yml ← Staging (branch: kub-stage) ├── prod-deploy.yml ← Production (branch: production) └── cms-stage.yml ← قدیمی (IIS روی Windows — غیرفعال) ``` ### ۴.۲ فلوی Staging (`kub-deploy.yml`) ```mermaid flowchart TD A["Push to kub-stage"] --> B["Start Docker daemon"] B --> C["Clone repo"] C --> D["Pack & Push Proto NuGet"] D --> E["Docker build → tag :latest"] E --> F["Push to 194.5.195.53:30080"] F --> G["SCP cms-config.yaml + cms-deployment.yaml"] G --> H["kubectl apply -f cms-config.yaml (Secret)"] H --> I["kubectl apply -f cms-deployment.yaml"] I --> J["kubectl rollout restart"] J --> K["✅ Deployed to Staging"] ``` ### ۴.۳ فلوی Production (`prod-deploy.yml`) ```mermaid flowchart TD A["Push to production"] --> B["Start Docker daemon"] B --> C["Clone repo"] C --> D["Pack & Push Proto NuGet"] D --> E["Docker build → tag :sha + :prod"] E --> F["Push to 194.5.195.53:30080"] F --> G["SCP cms-config.yaml + cms-deployment.yaml"] G --> H["kubectl apply -f cms-config.yaml (Secret)"] H --> I["kubectl apply -f cms-deployment.yaml"] I --> J["kubectl set image → sha"] J --> K["✅ Deployed to Production"] ``` ### ۴.۴ شاخه‌ها و محیط‌ها | شاخه | محیط | سرور | Image Tag | Deploy | |------|------|------|-----------|--------| | `kub-stage` | Staging | 194.5.195.53 | `:latest` | Auto | | `production` | Production | 45.149.79.127 | `:sha` + `:prod` | Auto | ### ۴.۵ نکات مهم CI/CD - **Proto NuGet:** هر deploy ابتدا proto packages رو build و به Nexus push می‌کنه - **Manifest apply:** پایپلاین ابتدا `cms-config.yaml` (Secret) رو apply می‌کنه، بعد `cms-deployment.yaml` → Secret + PVC + Deployment + Service + Ingress هر بار اعمال می‌شه - **Image registry:** `194.5.195.53:30080` (داخلی Nexus) — نه `git.se.kbs1.ir` - **Config دائمی:** تنظیمات در K8s Secret هست، نه داخل Docker image — تغییر config بدون rebuild ایمیج ممکنه - **جداسازی برنچ:** هر برنچ فقط appsettings محیط خودش رو داره (بخش ۳.۶) --- ## ۵. استقرار آفلاین (Offline Deployment) ### ۵.۱ فلوی آماده‌سازی ```mermaid flowchart TD subgraph ONLINE["🌐 سرور اینترنت‌دار"] A1["pull-base-images.sh\nدانلود Docker images"] --> A2["cache-nuget-packages.sh\nدانلود NuGet packages"] A2 --> A3["save-images.sh\nذخیره تصاویر به tar"] A3 --> A4["بسته‌بندی"] end A4 -->|"💾 انتقال فیزیکی\nUSB / HDD"| B1 subgraph OFFLINE["🔒 سرور آفلاین"] B1["load-images.sh\nبارگذاری تصاویر"] --> B2["setup-nexus-complete.sh\nراه‌اندازی Nexus"] B2 --> B3["build-all-offline.sh\nبیلد با Nexus محلی"] B3 --> B4["k8s-deploy.sh\nاستقرار در K8s"] end ``` ### ۵.۲ اسکریپت‌های کلیدی | اسکریپت | کاربرد | |----------|--------| | `pull-base-images.sh` | دانلود ۱۵+ Docker image پایه | | `save-images.sh` | Export به tar (4-8 GB) | | `load-images.sh` | Import از tar به Docker | | `cache-nuget-packages.sh` | دانلود NuGet offline | | `setup-nexus-complete.sh` | راه‌اندازی NuGet proxy | | `build-all-offline.sh` | بیلد بدون اینترنت | | `k8s-deploy.sh` | Deploy تمام سرویس‌ها | | `k8s-health-check.sh` | بررسی سلامت سرویس‌ها | --- ## ۶. Nexus Repository Manager ### ۶.۱ نقش ```mermaid graph TD NEXUS["Nexus داخلی"] --> NP["NuGet proxy\ncache nuget.org"] NEXUS --> NH["NuGet hosted\nبسته‌های proto داخلی"] NEXUS --> DP["Docker proxy\ncache Docker Hub"] NEXUS --> DH["Docker hosted\nتصاویر داخلی FourSat"] ``` ### ۶.۲ NuGet.config ```xml ``` --- ## ۷. Mirror و Cache ### ۷.۱ Docker Mirror ```json // /etc/docker/daemon.json { "registry-mirrors": [ "https://mirror.gcr.io", "https://docker.arvancloud.ir" ], "insecure-registries": [ "localhost:8082" ] } ``` ### ۷.۲ NuGet Mirror ``` Primary: nuget.org Fallback: Nexus local proxy Proto packages: BaGet (internal) at http://localhost:5555 ``` --- ## ۸. Proto Packages (NuGet) ### ۸.۱ فلوی بسته‌بندی ```mermaid flowchart TD A["CMS/src/Protos/*.proto"] --> B["pack-protos.sh\ndotnet pack → .nupkg"] B --> C["Push to BaGet / Nexus"] C --> D["BackOffice + FrontOffice\ndotnet restore → مصرف proto"] ``` ### ۸.۲ نام بسته ```xml ``` --- ## ۹. مانیتورینگ و Health Check ### ۹.۱ مرج پروداکشن (اسفند ۱۴۰۴) | ریپو | شاخه مبدأ | commit | نکات | |------|------------|--------|------| | **CMS** | `kub-stage` → `production` | `eb1b249` | حل conflict در `appsettings.Production.json` + حذف migration تکراری `u21` | | **FrontOffice** | `kub-stage` → `production` | `f02d082` | 21 فایل، 400 insertion + فیکس GwUrl به `cms.kbs2.ir` | | **BackOffice** | `kub-stage` → `production` | `bdea2e8` | 36 فایل، بدون conflict | ### ۹.۲ کامیت‌های PVC و اصلاحات K8s (تیر ۱۴۰۴) | commit | شرح | |--------|------| | `3153fd8` | feat: add PersistentVolume for CMS uploads + apply manifests in CI/CD | | `68da3f4` | fix: staging uses namespace default, not foursat | | `e41747a` | fix: production ingress — add cms.kbs2.ir, use ingressClassName | | `2d6c95e` | fix: use local registry 194.5.195.53:30080 instead of git.se.kbs1.ir | | `f8dc4ab` | fix: staging ASPNETCORE_ENVIRONMENT=Staging, remove secretKeyRef | | `de83c31` | fix: production uses namespace default + remove foursat namespace references | | `9288d06` | feat: externalize appsettings to K8s Secret — config persists independently | | `e72673c` | chore(staging): remove appsettings.Production.json (فقط kub-stage) | | `3ebe0f9` | chore(production): remove appsettings.Staging.json (فقط production) | > کامیت‌های PVC و Secret به هر دو شاخه push شده‌اند. > ⚠️ کامیت‌های حذف appsettings فقط به برنچ مربوطه push شده — cherry-pick نکنید! **تنظیمات محیطی Production (`appsettings.Production.json`):** | تنظیم | مقدار | |--------|-------| | `ZarinPal.MerchantId` | `4225d555-5fa9-4df0-9b61-1ce152cbbba8` | | `ZarinPal.UseSandbox` | `false` | | `SeedWorkers.MagicWalletCycleSeed.Enabled` | `true` | | `Kestrel.Endpoints.Grpc.Protocols` | `Http2` | | `Seq.ServerUrl` | `http://seq-svc:5341` | | `ConnectionStrings.Default` | `Server=mssql-svc;Database=KBS` | ```bash # k8s-health-check.sh (namespace = default) kubectl get pods kubectl top pods kubectl logs deployment/cms --tail=50 # بررسی PVC kubectl get pvc cms-uploads-pvc kubectl exec deployment/cms -- ls /app/Uploads | wc -l # تست سرویس‌ها grpcurl -plaintext localhost:5001 list # لیست سرویس‌ها grpcurl -plaintext localhost:5001 grpc.health.v1.Health/Check # Health curl http://localhost:5002/index.html # BackOffice curl http://localhost:5003/ # FrontOffice ```