BUSINESS-02: - فرمول هایبرید: حذف MIN، اضافه validation کیفپول اعتباری - فلوی خرید: اضافه مرحله بررسی موجودی + UserWalletChangeLog - نامگذاری جدید کیفپولها: اصلی، اعتباری، پاداش تیمی - جدول وضعیت: اضافه WalletChangeLog + Validation BUSINESS-03: - بخش ۹ جدید: ExpirePendingOrdersService (۱۵ دقیقه) - دیاگرام Mermaid فلوی انقضا TECH-03: - فیکس URL پروداکشن (kbs1→kbs2) + هشدار - ۴ کامیت جدید در بخش ۹.۲ - بخش ۹.۳ فیکس URL پروداکشن - بخش ۹.۴ نامگذاری کیفپولها
18 KiB
🚀 استقرار، 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.classannotation (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-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 |
# فیکس مستقیم روی سرور (بدون نیاز به 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