Files
docs/technical/TECH-03-DEPLOYMENT-INFRA.md
masoodafar-web c78850f86e docs: update BUSINESS-02, BUSINESS-03, TECH-03
BUSINESS-02:
- فرمول هایبرید: حذف MIN، اضافه validation کیف‌پول اعتباری
- فلوی خرید: اضافه مرحله بررسی موجودی + UserWalletChangeLog
- نام‌گذاری جدید کیف‌پول‌ها: اصلی، اعتباری، پاداش تیمی
- جدول وضعیت: اضافه WalletChangeLog + Validation

BUSINESS-03:
- بخش ۹ جدید: ExpirePendingOrdersService (۱۵ دقیقه)
- دیاگرام Mermaid فلوی انقضا

TECH-03:
- فیکس URL پروداکشن (kbs1→kbs2) + هشدار
- ۴ کامیت جدید در بخش ۹.۲
- بخش ۹.۳ فیکس URL پروداکشن
- بخش ۹.۴ نام‌گذاری کیف‌پول‌ها
2026-02-24 00:29:03 +03:30

18 KiB
Raw Permalink Blame History

🚀 استقرار، CI/CD و زیرساخت

منابع ادغام‌شده: CICD-PIPELINE-GUIDE.md, DEPLOYMENT-README.md, INFRASTRUCTURE-GUIDE.md, INGRESS-NGINX-WARNING.md, OFFLINE-DEPLOYMENT-GUIDE.md, SERVER-MIRRORS-CONFIG.md
آخرین بروزرسانی: اسفند ۱۴۰۴ (بروزرسانی: فیکس URL پروداکشن + چری‌پیک فیکس‌های WalletChangeLog/Validation/Expiry)


۱. سرورها

سرور 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

۲.۱ سرویس‌ها

# 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)

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 مونت شده:

# PVC — 20Gi ذخیره‌سازی دائمی
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: cms-uploads-pvc
  namespace: default
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 20Gi
# 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 می‌خونه.

flowchart LR
    S["K8s Secret<br/>cms-appsettings"] -->|volumeMount| F["/app/appsettings.*.json"]
    F --> D[".NET reads config"]
    I["Docker Image<br/>appsettings.json (base)"] --> D
محیط ASPNETCORE_ENVIRONMENT فایل Config (از Secret)
Staging Staging appsettings.Staging.json
Production Production appsettings.Production.json

Secret manifest (cms-config.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:

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):

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 (واقعی)

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:

spec:
  ingressClassName: nginx
  rules:
  - host: cms.se.kbs1.ir

Production:

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)

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)

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)

۵.۱ فلوی آماده‌سازی

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

۶.۱ نقش

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 version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="nexus" value="http://localhost:8081/repository/nuget-group/index.json" />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

۷. Mirror و Cache

۷.۱ Docker Mirror

// /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)

۸.۱ فلوی بسته‌بندی

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"]

۸.۲ نام بسته

<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="1.0.x" />

۹. مانیتورینگ و Health Check

۹.۱ مرج پروداکشن (اسفند ۱۴۰۴)

ریپو شاخه مبدأ commit نکات
CMS kub-stageproduction eb1b249 حل conflict در appsettings.Production.json + حذف migration تکراری u21
FrontOffice kub-stageproduction f02d082 21 فایل، 400 insertion + فیکس GwUrl به cms.kbs2.ir
BackOffice kub-stageproduction 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)
f3ac5ad fix: add missing UserWalletChangeLog for discount shop purchases
0457ef6 fix: validate discount wallet balance before applying discount
e206b71 fix: reduce discount order expiry from 30 to 15 minutes
2620a24 fix: correct production URLs from kbs1 to kbs2 in cms-config

کامیت‌های PVC و Secret به هر دو شاخه push شده‌اند.
⚠️ کامیت‌های حذف appsettings فقط به برنچ مربوطه push شده — cherry-pick نکنید!

۹.۳ فیکس URL پروداکشن (اسفند ۱۴۰۴)

مشکل: در cms-config.yaml پروداکشن، URL‌ها به اشتباه kbs1.ir (استیج) بودند. زرین‌پال callback را به سرور استیج می‌فرستاد → خطای 401 → Code=-1 (خطای ناشناخته).

فیلد مقدار اشتباه مقدار صحیح
CmsBaseUrl https://cms.kbs1.ir https://cms.kbs2.ir
FrontOfficeBaseUrl https://kbs1.ir https://kbs2.ir
# فیکس مستقیم روی سرور (بدون نیاز به rebuild)
kubectl apply -f cms-config.yaml
kubectl rollout restart deployment/cms

۹.۴ نام‌گذاری کیف‌پول‌ها (اسفند ۱۴۰۴)

تغییر عنوان کیف‌پول‌ها در تمام UI (FrontOffice: 5 فایل، BackOffice: 7 فایل):

فیلد نام قدیم نام جدید
Balance عادی / نقدی کیف پول اصلی
DiscountBalance تخفیفی / تخفیف کیف پول اعتباری
NetworkBalance شبکه / طلایی پاداش تیمی

تنظیمات محیطی Production (appsettings.Production.json):

تنظیم مقدار
ZarinPal.MerchantId 4225d555-5fa9-4df0-9b61-1ce152cbbba8
ZarinPal.UseSandbox false
CmsBaseUrl https://cms.kbs2.ir
FrontOfficeBaseUrl https://kbs2.ir
SeedWorkers.MagicWalletCycleSeed.Enabled true
Kestrel.Endpoints.Grpc.Protocols Http2
Seq.ServerUrl http://seq-svc:5341
ConnectionStrings.Default Server=mssql-svc;Database=KBS

⚠️ مهم: URL‌ها باید kbs2.ir باشند نه kbs1.ir — اشتباه در URL باعث خطای 401 زرین‌پال می‌شود.

# 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