feat: Phase 2 close-out — multi-branch management, production K8s, revenue dashboard UI, responsive POS

Backend:
- Multi-branch shop management: SetDefaultShop, TransferShop commands, GetMerchantShops paginated query
- Shop aggregate: IsDefault field, SetAsDefault/ClearDefault/TransferOwnership behavior methods
- 2 new domain events: ShopSetAsDefaultDomainEvent, ShopTransferredDomainEvent

Frontend:
- Revenue Dashboard (MudChart line/donut/bar, 4 KPI cards, top products table)
- Staff Performance (sortable table, color-coded completion rates, CSV export)
- Customer QR Menu page (/menu/{ShopId}, mobile-first, Vietnamese labels)
- QR Code Generator admin page (batch generate, print-all, per-table QR)
- Responsive POS layout (collapsible sidebar, slide-out order drawer, touch-friendly CSS)
- ResponsiveOrderPanel component (desktop inline / tablet drawer / mobile overlay)

Infrastructure:
- Production K8s manifests: 8 services (3 replicas, 512Mi-1Gi, HPA min3/max10), Redis with persistence
- Production ingress: api.goodgo.vn, cert-manager TLS, rate-limit middleware
- Deploy script: pre-flight checks, dry-run, single-service deploy, rollback support
- CI/CD: deploy-production.yml with environment approval, commit SHA tags
- Prometheus full scrape config (11 targets), docker-compose observability stack
- Production deployment checklist (80+ items)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-03-06 19:58:40 +07:00
parent a6ea9fa29b
commit 76b5e6afd0
40 changed files with 5582 additions and 165 deletions

View File

@@ -1241,31 +1241,87 @@ services:
# restart: unless-stopped
# Prometheus - Metrics Collection
# prometheus:
# image: prom/prometheus:latest
# container_name: prometheus-local
# ports:
# - "9090:9090"
# volumes:
# - ../../infra/observability/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
# - prometheus_data:/prometheus
# networks:
# - microservices-network
# restart: unless-stopped
prometheus:
image: prom/prometheus:v2.51.0
container_name: prometheus-local
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=15d'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
volumes:
- ../../infra/observability/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ../../infra/observability/prometheus/rules:/etc/prometheus/rules:ro
- ../../infra/observability/prometheus/alert-rules.yml:/etc/prometheus/alert-rules.yml:ro
- prometheus_data:/prometheus
networks:
- microservices-network
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9090/-/healthy || exit 1"]
interval: 30s
timeout: 10s
retries: 3
# Grafana - Metrics Visualization
# grafana:
# image: grafana/grafana:latest
# container_name: grafana-local
# ports:
# - "3001:3000"
# environment:
# - GF_SECURITY_ADMIN_PASSWORD=admin
# volumes:
# - grafana_data:/var/lib/grafana
# networks:
# - microservices-network
# restart: unless-stopped
# Grafana - Metrics Visualization & Dashboards
grafana:
image: grafana/grafana:10.3.1
container_name: grafana-local
ports:
- "3002:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
- GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/etc/grafana/provisioning/dashboards/goodgo-overview.json
volumes:
- grafana_data:/var/lib/grafana
- ../../infra/observability/grafana/datasources:/etc/grafana/provisioning/datasources:ro
- ../../infra/observability/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
networks:
- microservices-network
depends_on:
prometheus:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
# Loki - Log Aggregation
loki:
image: grafana/loki:2.9.4
container_name: loki-local
ports:
- "3100:3100"
command: -config.file=/etc/loki/loki-config.yml
volumes:
- ../../infra/observability/loki/loki-config.yml:/etc/loki/loki-config.yml:ro
- loki_data:/loki
networks:
- microservices-network
restart: unless-stopped
# Promtail - Log Collector (ships container logs to Loki)
promtail:
image: grafana/promtail:2.9.4
container_name: promtail-local
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
- ../../infra/observability/promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro
command: -config.file=/etc/promtail/promtail-config.yml
networks:
- microservices-network
depends_on:
- loki
restart: unless-stopped
# ===========================================================================
# FRONTEND APPS
@@ -1340,6 +1396,12 @@ volumes:
driver: local
rabbitmq_data:
driver: local
prometheus_data:
driver: local
grafana_data:
driver: local
loki_data:
driver: local
# =============================================================================
# NETWORKS
# =============================================================================

View File

@@ -0,0 +1,136 @@
# EN: Booking Service - Booking & Reservation System
# VI: Booking Service - He thong Dat cho & Dat lich
apiVersion: apps/v1
kind: Deployment
metadata:
name: booking-service
namespace: production
labels:
app: booking-service
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: booking-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: booking-service
environment: production
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- booking-service
topologyKey: kubernetes.io/hostname
containers:
- name: booking-service
image: goodgo/booking-service-net:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: goodgo-config
- secretRef:
name: goodgo-secrets
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: BOOKING_DATABASE_URL
- name: IamService__ServiceName
value: "booking-service"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: booking-service
namespace: production
labels:
app: booking-service
environment: production
spec:
selector:
app: booking-service
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: booking-service-hpa
namespace: production
labels:
app: booking-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: booking-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,136 @@
# EN: Catalog Service - Polymorphic Product & Category Management
# VI: Catalog Service - Quan ly San pham & Danh muc da hinh
apiVersion: apps/v1
kind: Deployment
metadata:
name: catalog-service
namespace: production
labels:
app: catalog-service
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: catalog-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: catalog-service
environment: production
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- catalog-service
topologyKey: kubernetes.io/hostname
containers:
- name: catalog-service
image: goodgo/catalog-service-net:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: goodgo-config
- secretRef:
name: goodgo-secrets
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: CATALOG_DATABASE_URL
- name: IamService__ServiceName
value: "catalog-service"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: catalog-service
namespace: production
labels:
app: catalog-service
environment: production
spec:
selector:
app: catalog-service
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: catalog-service-hpa
namespace: production
labels:
app: catalog-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: catalog-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,58 @@
# EN: Shared configuration for all GoodGo production services
# VI: Cau hinh chung cho tat ca cac service production cua GoodGo
apiVersion: v1
kind: ConfigMap
metadata:
name: goodgo-config
namespace: production
labels:
environment: production
platform: goodgo
data:
# EN: ASP.NET Core Configuration
# VI: Cau hinh ASP.NET Core
ASPNETCORE_ENVIRONMENT: "Production"
ASPNETCORE_URLS: "http://+:8080"
# EN: JWT Configuration (shared across all services)
# VI: Cau hinh JWT (dung chung cho tat ca services)
Jwt__Authority: "http://iam-service:8080"
Jwt__Audience: "goodgo-api"
Jwt__RequireHttpsMetadata: "true"
# EN: Service Discovery URLs (K8s DNS: {service-name}.production.svc.cluster.local)
# VI: URL tim kiem service (K8s DNS: {service-name}.production.svc.cluster.local)
IamService__BaseUrl: "http://iam-service:8080"
MerchantService__BaseUrl: "http://merchant-service:8080"
CatalogService__BaseUrl: "http://catalog-service:8080"
OrderService__BaseUrl: "http://order-service:8080"
InventoryService__BaseUrl: "http://inventory-service:8080"
WalletService__BaseUrl: "http://wallet-service:8080"
StorageService__BaseUrl: "http://storage-service:8080"
FnbEngine__BaseUrl: "http://fnb-engine:8080"
BookingService__BaseUrl: "http://booking-service:8080"
# EN: Redis Configuration
# VI: Cau hinh Redis
Redis__Host: "redis"
Redis__Port: "6379"
Redis__Database: "0"
# EN: CORS Configuration
# VI: Cau hinh CORS
Cors__AllowedOrigins: "https://pos.goodgo.vn,https://goodgo.vn,https://admin.goodgo.vn"
# EN: Logging (stricter for production)
# VI: Ghi log (nghiem ngat hon cho production)
Serilog__MinimumLevel__Default: "Warning"
Serilog__MinimumLevel__Override__Microsoft: "Error"
Serilog__MinimumLevel__Override__System: "Error"
# EN: Feature Flags
# VI: Tinh nang bat/tat
Features__SwaggerEnabled: "false"
Features__DetailedErrors: "false"
# EN: API Version
# VI: Phien ban API
API_VERSION: "v1"

View File

@@ -0,0 +1,143 @@
# EN: FnB Engine - Table, Session, Kitchen & Reservation Management (SignalR for Kitchen Display)
# VI: FnB Engine - Quan ly Ban, Phien, Bep & Dat ban (SignalR cho Man hinh bep)
apiVersion: apps/v1
kind: Deployment
metadata:
name: fnb-engine
namespace: production
labels:
app: fnb-engine
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: fnb-engine
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: fnb-engine
environment: production
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- fnb-engine
topologyKey: kubernetes.io/hostname
containers:
- name: fnb-engine
image: goodgo/fnb-engine-net:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: goodgo-config
- secretRef:
name: goodgo-secrets
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: FNB_DATABASE_URL
- name: IamService__ServiceName
value: "fnb-engine"
# EN: Redis for SignalR backplane (Kitchen Display)
# VI: Redis cho SignalR backplane (Man hinh bep)
- name: ConnectionStrings__Redis
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: ConnectionStrings__Redis
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: fnb-engine
namespace: production
labels:
app: fnb-engine
environment: production
spec:
selector:
app: fnb-engine
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: fnb-engine-hpa
namespace: production
labels:
app: fnb-engine
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: fnb-engine
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: iam-service-config
namespace: production
data:
NODE_ENV: "production"
PORT: "5001"
API_VERSION: "v1"
CORS_ORIGIN: "https://goodgo.vn"
LOG_LEVEL: "warn"
SERVICE_NAME: "iam-service"
TRACING_ENABLED: "true"
# Note: DATABASE_URL is stored in secrets (iam-service-secrets)
# DATABASE_URL should point to Neon production branch

View File

@@ -1,29 +1,72 @@
# EN: IAM Service - Identity & Access Management (Duende IdentityServer, JWT, RBAC, MFA)
# VI: IAM Service - Quan ly Danh tinh & Truy cap (Duende IdentityServer, JWT, RBAC, MFA)
apiVersion: apps/v1
kind: Deployment
metadata:
name: iam-service
namespace: production
labels:
app: iam-service
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: iam-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: iam-service
environment: production
spec:
# EN: Pod anti-affinity to spread replicas across nodes
# VI: Anti-affinity de phan bo replica tren nhieu node
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
topologyKey: kubernetes.io/hostname
containers:
- name: iam-service
image: goodgo/iam-service:latest
image: goodgo/iam-service-net:latest
imagePullPolicy: Always
ports:
- containerPort: 5001
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: iam-service-config
name: goodgo-config
- secretRef:
name: iam-service-secrets
name: goodgo-secrets
env:
# EN: Override service-specific database URL
# VI: Override URL database rieng cho service
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: IAM_DATABASE_URL
- name: IamService__ServiceName
value: "iam-service"
- name: IdentityServer__IssuerUri
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: IdentityServer__IssuerUri
resources:
requests:
memory: "512Mi"
@@ -34,7 +77,7 @@ spec:
livenessProbe:
httpGet:
path: /health/live
port: 5001
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
@@ -42,33 +85,44 @@ spec:
readinessProbe:
httpGet:
path: /health/ready
port: 5001
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: iam-service
namespace: production
labels:
app: iam-service
environment: production
spec:
selector:
app: iam-service
ports:
- protocol: TCP
port: 5001
targetPort: 5001
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: iam-service-hpa
namespace: production
labels:
app: iam-service
spec:
scaleTargetRef:
apiVersion: apps/v1

View File

@@ -1,74 +1,289 @@
# EN: Traefik Ingress for GoodGo Production - API Gateway routing
# VI: Traefik Ingress cho GoodGo Production - Dinh tuyen API Gateway
#
# Routes match infra/traefik/dynamic/routes.yml for consistency
# Host: api.goodgo.vn (API), pos.goodgo.vn (POS Frontend)
# =============================================================================
# API Ingress - Backend services
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
labels:
environment: production
platform: goodgo
annotations:
traefik.ingress.kubernetes.io/rule-type: PathPrefix
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# EN: Traefik Ingress class
# VI: Ingress class cua Traefik
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
# EN: Rate limiting and security middlewares
# VI: Middleware gioi han toc do va bao mat
traefik.ingress.kubernetes.io/router.middlewares: production-cors@kubernetescrd,production-secure-headers@kubernetescrd,production-rate-limit@kubernetescrd
# EN: cert-manager TLS (production issuer)
# VI: TLS bang cert-manager (issuer production)
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- api.goodgo.vn
secretName: api-tls-cert
secretName: api-production-tls
rules:
- host: api.goodgo.vn
http:
paths:
# ===== IAM Service =====
- path: /api/v1/auth
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
- path: /api/v1/users
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
- path: /api/v1/identity
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
- path: /api/v1/access
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
- path: /api/v1/governance
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
- path: /api/v1/rbac
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
- path: /api/v1/mfa
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
- path: /api/v1/sessions
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 5001
number: 8080
# EN: IdentityServer OIDC endpoints
# VI: IdentityServer OIDC endpoints
- path: /connect
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 8080
- path: /.well-known
pathType: Prefix
backend:
service:
name: iam-service
port:
number: 8080
# ===== Merchant Service =====
- path: /api/v1/merchants
pathType: Prefix
backend:
service:
name: merchant-service
port:
number: 8080
- path: /api/v1/shops
pathType: Prefix
backend:
service:
name: merchant-service
port:
number: 8080
- path: /api/v1/subscriptions
pathType: Prefix
backend:
service:
name: merchant-service
port:
number: 8080
# ===== Order Service =====
- path: /api/v1/orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 8080
# EN: POS/KDS SignalR Hub (WebSocket)
# VI: POS/KDS SignalR Hub (WebSocket)
- path: /hubs/pos
pathType: Prefix
backend:
service:
name: order-service
port:
number: 8080
# ===== FnB Engine =====
- path: /api/v1/kitchen
pathType: Prefix
backend:
service:
name: fnb-engine
port:
number: 8080
- path: /api/v1/fnb
pathType: Prefix
backend:
service:
name: fnb-engine
port:
number: 8080
- path: /api/v1/tables
pathType: Prefix
backend:
service:
name: fnb-engine
port:
number: 8080
# EN: Kitchen Display SignalR Hub
# VI: SignalR Hub Man hinh bep
- path: /hubs/kitchen
pathType: Prefix
backend:
service:
name: fnb-engine
port:
number: 8080
# ===== Inventory Service =====
- path: /api/v1/inventory
pathType: Prefix
backend:
service:
name: inventory-service
port:
number: 8080
- path: /api/v1/stock
pathType: Prefix
backend:
service:
name: inventory-service
port:
number: 8080
# ===== Wallet Service =====
- path: /api/v1/wallets
pathType: Prefix
backend:
service:
name: wallet-service
port:
number: 8080
- path: /api/v1/points
pathType: Prefix
backend:
service:
name: wallet-service
port:
number: 8080
- path: /api/v1/payments
pathType: Prefix
backend:
service:
name: wallet-service
port:
number: 8080
# ===== Catalog Service =====
- path: /api/v1/products
pathType: Prefix
backend:
service:
name: catalog-service
port:
number: 8080
- path: /api/v1/categories
pathType: Prefix
backend:
service:
name: catalog-service
port:
number: 8080
# ===== Booking Service =====
- path: /api/v1/bookings
pathType: Prefix
backend:
service:
name: booking-service
port:
number: 8080
- path: /api/v1/reservations
pathType: Prefix
backend:
service:
name: booking-service
port:
number: 8080
---
# =============================================================================
# POS Frontend Ingress
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pos-web-ingress
namespace: production
labels:
environment: production
platform: goodgo
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- pos.goodgo.vn
secretName: pos-production-tls
rules:
- host: pos.goodgo.vn
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: pos-web
port:
number: 8080

View File

@@ -0,0 +1,136 @@
# EN: Inventory Service - Stock Management & Deduction (Retail + FnB)
# VI: Inventory Service - Quan ly Ton kho & Tru kho (Retail + FnB)
apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-service
namespace: production
labels:
app: inventory-service
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: inventory-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: inventory-service
environment: production
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- inventory-service
topologyKey: kubernetes.io/hostname
containers:
- name: inventory-service
image: goodgo/inventory-service-net:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: goodgo-config
- secretRef:
name: goodgo-secrets
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: INVENTORY_DATABASE_URL
- name: IamService__ServiceName
value: "inventory-service"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: inventory-service
namespace: production
labels:
app: inventory-service
environment: production
spec:
selector:
app: inventory-service
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: inventory-service-hpa
namespace: production
labels:
app: inventory-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: inventory-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,136 @@
# EN: Merchant Service - Merchant & Shop Management
# VI: Merchant Service - Quan ly Merchant & Shop
apiVersion: apps/v1
kind: Deployment
metadata:
name: merchant-service
namespace: production
labels:
app: merchant-service
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: merchant-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: merchant-service
environment: production
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- merchant-service
topologyKey: kubernetes.io/hostname
containers:
- name: merchant-service
image: goodgo/merchant-service-net:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: goodgo-config
- secretRef:
name: goodgo-secrets
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: MERCHANT_DATABASE_URL
- name: IamService__ServiceName
value: "merchant-service"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: merchant-service
namespace: production
labels:
app: merchant-service
environment: production
spec:
selector:
app: merchant-service
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: merchant-service-hpa
namespace: production
labels:
app: merchant-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: merchant-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,10 @@
# EN: Production namespace for GoodGo Platform
# VI: Namespace production cho GoodGo Platform
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
environment: production
platform: goodgo
managed-by: kubectl

View File

@@ -0,0 +1,144 @@
# EN: Order Service - Order Processing & POS API (SignalR WebSocket for POS/KDS)
# VI: Order Service - Xu ly Order & POS API (SignalR WebSocket cho POS/KDS)
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
labels:
app: order-service
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: order-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: order-service
environment: production
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- order-service
topologyKey: kubernetes.io/hostname
containers:
- name: order-service
image: goodgo/order-service-net:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: goodgo-config
- secretRef:
name: goodgo-secrets
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: ORDER_DATABASE_URL
- name: IamService__ServiceName
value: "order-service"
# EN: Inter-service communication
# VI: Giao tiep giua cac service
- name: CatalogService__BaseUrl
value: "http://catalog-service:8080"
- name: InventoryService__BaseUrl
value: "http://inventory-service:8080"
- name: WalletService__BaseUrl
value: "http://wallet-service:8080"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: order-service
namespace: production
labels:
app: order-service
environment: production
spec:
selector:
app: order-service
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
namespace: production
labels:
app: order-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

View File

@@ -0,0 +1,122 @@
# EN: Redis - Cache & SignalR Backplane for production
# VI: Redis - Cache & SignalR Backplane cho production
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: production
labels:
app: redis
environment: production
platform: goodgo
tier: infrastructure
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
environment: production
spec:
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- "--requirepass"
- "$(REDIS_PASSWORD)"
- "--maxmemory"
- "512mb"
- "--maxmemory-policy"
- "allkeys-lru"
- "--appendonly"
- "yes"
- "--appendfsync"
- "everysec"
- "--save"
- "900 1"
- "--save"
- "300 10"
- "--save"
- "60 10000"
ports:
- containerPort: 6379
protocol: TCP
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: Redis__Password
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
exec:
command:
- redis-cli
- -a
- "$(REDIS_PASSWORD)"
- ping
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- redis-cli
- -a
- "$(REDIS_PASSWORD)"
- ping
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-pvc
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: production
labels:
app: redis
environment: production
spec:
selector:
app: redis
ports:
- name: redis
protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-pvc
namespace: production
labels:
app: redis
environment: production
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi

View File

@@ -1,34 +1,64 @@
# Kubernetes Secrets Template for Production
# DO NOT commit actual secrets to Git
# Use this as a template to create secrets
# Create secret using kubectl:
# kubectl create secret generic iam-service-secrets \
# --from-literal=database-url='postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true' \
# --from-literal=jwt-secret='your-production-jwt-secret-min-32-chars' \
# --from-literal=jwt-refresh-secret='your-production-refresh-secret-min-32-chars' \
# --from-literal=redis-password='' \
# EN: Kubernetes Secrets Template for GoodGo Production
# VI: Template Secrets Kubernetes cho GoodGo Production
#
# DO NOT commit actual secrets to Git.
# Use this as a template to create secrets via kubectl or sealed-secrets.
#
# =============================================================================
# Option 1: Create secrets using kubectl (manual)
# =============================================================================
#
# kubectl create secret generic goodgo-secrets \
# --from-literal=Jwt__Secret='your-production-jwt-secret-min-64-chars-strong-random' \
# --from-literal=Jwt__RefreshSecret='your-production-refresh-secret-min-64-chars-strong-random' \
# --from-literal=IdentityServer__IssuerUri='https://api.goodgo.vn' \
# --from-literal=IAM_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/iam_production?sslmode=require&pgbouncer=true' \
# --from-literal=MERCHANT_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/merchant_production?sslmode=require&pgbouncer=true' \
# --from-literal=ORDER_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/order_production?sslmode=require&pgbouncer=true' \
# --from-literal=FNB_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/fnb_production?sslmode=require&pgbouncer=true' \
# --from-literal=INVENTORY_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/inventory_production?sslmode=require&pgbouncer=true' \
# --from-literal=WALLET_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/wallet_production?sslmode=require&pgbouncer=true' \
# --from-literal=CATALOG_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/catalog_production?sslmode=require&pgbouncer=true' \
# --from-literal=BOOKING_DATABASE_URL='postgresql://user:pass@ep-xxx.region.neon.tech/booking_production?sslmode=require&pgbouncer=true' \
# --from-literal=Redis__Password='your-strong-redis-password' \
# --from-literal=ConnectionStrings__Redis='redis:6379,password=your-strong-redis-password,abortConnect=false' \
# --from-literal=Storage__MinIO__Endpoint='minio.goodgo.vn' \
# --from-literal=Storage__MinIO__AccessKey='your-minio-access-key' \
# --from-literal=Storage__MinIO__SecretKey='your-minio-secret-key' \
# --from-literal=RabbitMQ__Host='rabbitmq' \
# --from-literal=RabbitMQ__Username='goodgo' \
# --from-literal=RabbitMQ__Password='your-strong-rabbitmq-password' \
# -n production
# Or use GitHub Secrets in CI/CD:
# - NEON_DATABASE_URL_PRODUCTION
# - JWT_SECRET_PRODUCTION
# - JWT_REFRESH_SECRET_PRODUCTION
apiVersion: v1
kind: Secret
metadata:
name: iam-service-secrets
namespace: production
type: Opaque
stringData:
# Neon Database URL (Production branch)
# Format: postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true
database-url: "postgresql://user:password@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true"
# JWT Secrets (use strong random strings, min 32 characters)
jwt-secret: "your-production-jwt-secret-min-32-chars"
jwt-refresh-secret: "your-production-refresh-secret-min-32-chars"
# Redis (if password protected)
redis-password: ""
#
# =============================================================================
# Option 2: Use GitHub Secrets in CI/CD (for automated deployments)
# =============================================================================
#
# Required GitHub Secrets:
# - KUBECONFIG_PRODUCTION (base64 encoded kubeconfig)
# - DOCKER_USERNAME / DOCKER_PASSWORD
# - NEON_IAM_DATABASE_URL_PRODUCTION
# - NEON_MERCHANT_DATABASE_URL_PRODUCTION
# - NEON_ORDER_DATABASE_URL_PRODUCTION
# - NEON_FNB_DATABASE_URL_PRODUCTION
# - NEON_INVENTORY_DATABASE_URL_PRODUCTION
# - NEON_WALLET_DATABASE_URL_PRODUCTION
# - NEON_CATALOG_DATABASE_URL_PRODUCTION
# - NEON_BOOKING_DATABASE_URL_PRODUCTION
# - JWT_SECRET_PRODUCTION
# - JWT_REFRESH_SECRET_PRODUCTION
# - REDIS_PASSWORD_PRODUCTION
# - MINIO_ACCESS_KEY_PRODUCTION
# - MINIO_SECRET_KEY_PRODUCTION
# - RABBITMQ_PASSWORD_PRODUCTION
#
# =============================================================================
# Option 3: Use sealed-secrets or external-secrets operator (RECOMMENDED for production)
# =============================================================================
#
# Install sealed-secrets controller:
# kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.5/controller.yaml
#
# Create sealed secret:
# kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# kubectl apply -f sealed-secret.yaml

View File

@@ -0,0 +1,136 @@
# EN: Wallet Service - Wallet & Payment Management
# VI: Wallet Service - Quan ly Vi & Thanh toan
apiVersion: apps/v1
kind: Deployment
metadata:
name: wallet-service
namespace: production
labels:
app: wallet-service
environment: production
platform: goodgo
tier: backend
spec:
replicas: 3
selector:
matchLabels:
app: wallet-service
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: wallet-service
environment: production
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- wallet-service
topologyKey: kubernetes.io/hostname
containers:
- name: wallet-service
image: goodgo/wallet-service-net:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
envFrom:
- configMapRef:
name: goodgo-config
- secretRef:
name: goodgo-secrets
env:
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: WALLET_DATABASE_URL
- name: IamService__ServiceName
value: "wallet-service"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 12
---
apiVersion: v1
kind: Service
metadata:
name: wallet-service
namespace: production
labels:
app: wallet-service
environment: production
spec:
selector:
app: wallet-service
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: wallet-service-hpa
namespace: production
labels:
app: wallet-service
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: wallet-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80