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

541 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🚀 استقرار، 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
### ۲.۱ سرویس‌ها
```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<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`):
```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
<?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
```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
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="1.0.x" />
```
---
## ۹. مانیتورینگ و 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) |
| `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` |
```bash
# فیکس مستقیم روی سرور (بدون نیاز به 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 زرین‌پال می‌شود.
```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
```