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>
351 lines
14 KiB
Bash
Executable File
351 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
# EN: Deploy GoodGo Platform production services to Kubernetes production environment
|
|
# VI: Trien khai cac service production cua GoodGo Platform len moi truong production Kubernetes
|
|
#
|
|
# Prerequisites:
|
|
# - kubectl configured with production cluster access (KUBECONFIG env var)
|
|
# - Docker images pushed to Docker Hub (goodgo/*:<commit-sha>)
|
|
# - Secrets created via kubectl or sealed-secrets (see secrets.yaml.example)
|
|
#
|
|
# Usage:
|
|
# export KUBECONFIG=/path/to/kubeconfig
|
|
# ./scripts/deploy/deploy-prod.sh
|
|
# ./scripts/deploy/deploy-prod.sh --service iam-service # Deploy single service
|
|
# ./scripts/deploy/deploy-prod.sh --dry-run # Dry run mode
|
|
# ./scripts/deploy/deploy-prod.sh --rollback iam-service # Rollback service
|
|
# ./scripts/deploy/deploy-prod.sh --migrate # Run DB migrations before deploy
|
|
|
|
set -euo pipefail
|
|
|
|
# EN: Color output helpers
|
|
# VI: Ham ho tro mau output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
MAGENTA='\033[0;35m'
|
|
NC='\033[0m' # No Color
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
DEPLOY_DIR="${ROOT_DIR}/deployments/production/kubernetes"
|
|
NAMESPACE="production"
|
|
DRY_RUN=""
|
|
SINGLE_SERVICE=""
|
|
ROLLBACK_SERVICE=""
|
|
RUN_MIGRATIONS=false
|
|
|
|
# EN: Production services list
|
|
# VI: Danh sach service production
|
|
PROD_SERVICES=(
|
|
"iam-service"
|
|
"merchant-service"
|
|
"catalog-service"
|
|
"order-service"
|
|
"fnb-engine"
|
|
"inventory-service"
|
|
"wallet-service"
|
|
"booking-service"
|
|
)
|
|
|
|
# EN: Parse arguments
|
|
# VI: Phan tich tham so
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--dry-run)
|
|
DRY_RUN="--dry-run=client"
|
|
echo -e "${YELLOW}[DRY RUN] No changes will be applied${NC}"
|
|
shift
|
|
;;
|
|
--service)
|
|
SINGLE_SERVICE="$2"
|
|
shift 2
|
|
;;
|
|
--rollback)
|
|
ROLLBACK_SERVICE="$2"
|
|
shift 2
|
|
;;
|
|
--migrate)
|
|
RUN_MIGRATIONS=true
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
echo "Usage: $0 [OPTIONS]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --dry-run Preview changes without applying"
|
|
echo " --service <name> Deploy a single service"
|
|
echo " --rollback <name> Rollback a service to previous revision"
|
|
echo " --migrate Run EF Core database migrations before deploy"
|
|
echo " -h, --help Show this help"
|
|
echo ""
|
|
echo "Services: ${PROD_SERVICES[*]}"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo -e "${RED}Unknown option: $1${NC}"
|
|
echo "Use --help for usage information"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# =============================================================================
|
|
# EN: Pre-flight checks
|
|
# VI: Kiem tra truoc khi deploy
|
|
# =============================================================================
|
|
|
|
echo -e "${MAGENTA}=== GoodGo Platform - Production Deployment ===${NC}"
|
|
echo ""
|
|
|
|
# EN: Require explicit confirmation for production
|
|
# VI: Yeu cau xac nhan ro rang cho production
|
|
if [ -z "$DRY_RUN" ] && [ -z "$ROLLBACK_SERVICE" ]; then
|
|
echo -e "${RED}WARNING: You are about to deploy to PRODUCTION!${NC}"
|
|
echo -e "${YELLOW}Namespace: ${NAMESPACE}${NC}"
|
|
if [ -n "$SINGLE_SERVICE" ]; then
|
|
echo -e "${YELLOW}Service: ${SINGLE_SERVICE}${NC}"
|
|
else
|
|
echo -e "${YELLOW}Services: ALL (${#PROD_SERVICES[@]} services)${NC}"
|
|
fi
|
|
echo ""
|
|
read -p "Type 'yes' to confirm: " confirm
|
|
if [ "$confirm" != "yes" ]; then
|
|
echo -e "${YELLOW}Deployment cancelled${NC}"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# EN: Verify KUBECONFIG
|
|
# VI: Xac minh KUBECONFIG
|
|
if [ -z "${KUBECONFIG:-}" ]; then
|
|
echo -e "${RED}KUBECONFIG environment variable not set${NC}"
|
|
echo "Usage: export KUBECONFIG=/path/to/kubeconfig && $0"
|
|
exit 1
|
|
fi
|
|
|
|
# EN: Verify kubectl connectivity
|
|
# VI: Xac minh ket noi kubectl
|
|
echo -e "${BLUE}Verifying kubectl connectivity...${NC}"
|
|
if ! kubectl cluster-info &>/dev/null; then
|
|
echo -e "${RED}Cannot connect to Kubernetes cluster. Check KUBECONFIG.${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# EN: Verify namespace exists
|
|
# VI: Xac minh namespace ton tai
|
|
if ! kubectl get namespace "${NAMESPACE}" &>/dev/null; then
|
|
echo -e "${YELLOW}Namespace '${NAMESPACE}' does not exist. Creating...${NC}"
|
|
kubectl apply -f "${DEPLOY_DIR}/namespace.yaml" ${DRY_RUN}
|
|
fi
|
|
|
|
# EN: Verify manifests directory
|
|
# VI: Xac minh thu muc manifests
|
|
if [ ! -d "${DEPLOY_DIR}" ]; then
|
|
echo -e "${RED}Deployment directory not found: ${DEPLOY_DIR}${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "${GREEN}Pre-flight checks passed${NC}"
|
|
echo ""
|
|
|
|
# =============================================================================
|
|
# EN: Handle rollback
|
|
# VI: Xu ly rollback
|
|
# =============================================================================
|
|
|
|
if [ -n "$ROLLBACK_SERVICE" ]; then
|
|
echo -e "${YELLOW}Rolling back ${ROLLBACK_SERVICE}...${NC}"
|
|
|
|
# EN: Show rollout history
|
|
# VI: Hien thi lich su rollout
|
|
echo -e "${BLUE}Rollout history:${NC}"
|
|
kubectl rollout history "deployment/${ROLLBACK_SERVICE}" -n "${NAMESPACE}" 2>/dev/null || {
|
|
echo -e "${RED}Deployment '${ROLLBACK_SERVICE}' not found in namespace '${NAMESPACE}'${NC}"
|
|
exit 1
|
|
}
|
|
|
|
echo ""
|
|
read -p "Rollback to previous revision? (yes/no): " confirm_rollback
|
|
if [ "$confirm_rollback" = "yes" ]; then
|
|
kubectl rollout undo "deployment/${ROLLBACK_SERVICE}" -n "${NAMESPACE}"
|
|
echo -e "${BLUE}Waiting for rollback to complete...${NC}"
|
|
kubectl rollout status "deployment/${ROLLBACK_SERVICE}" -n "${NAMESPACE}" --timeout=180s
|
|
echo -e "${GREEN}Rollback of ${ROLLBACK_SERVICE} completed${NC}"
|
|
else
|
|
echo -e "${YELLOW}Rollback cancelled${NC}"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# =============================================================================
|
|
# EN: Run database migrations (optional)
|
|
# VI: Chay database migrations (tuy chon)
|
|
# =============================================================================
|
|
|
|
if [ "$RUN_MIGRATIONS" = true ]; then
|
|
echo -e "${BLUE}[0/6] Running database migrations...${NC}"
|
|
|
|
# EN: Check dotnet-ef tool
|
|
# VI: Kiem tra dotnet-ef tool
|
|
if ! command -v dotnet &>/dev/null; then
|
|
echo -e "${RED}dotnet SDK not found. Install .NET 10 SDK for migrations.${NC}"
|
|
exit 1
|
|
fi
|
|
dotnet tool install --global dotnet-ef 2>/dev/null || true
|
|
|
|
declare -A MIGRATION_MAP=(
|
|
["iam-service"]="services/iam-service-net/src/IamService.Infrastructure/IamService.Infrastructure.csproj|services/iam-service-net/src/IamService.API/IamService.API.csproj|NEON_IAM_DATABASE_URL_PRODUCTION"
|
|
["merchant-service"]="services/merchant-service-net/src/MerchantService.Infrastructure/MerchantService.Infrastructure.csproj|services/merchant-service-net/src/MerchantService.API/MerchantService.API.csproj|NEON_MERCHANT_DATABASE_URL_PRODUCTION"
|
|
["order-service"]="services/order-service-net/src/OrderService.Infrastructure/OrderService.Infrastructure.csproj|services/order-service-net/src/OrderService.API/OrderService.API.csproj|NEON_ORDER_DATABASE_URL_PRODUCTION"
|
|
["fnb-engine"]="services/fnb-engine-net/src/FnbEngine.Infrastructure/FnbEngine.Infrastructure.csproj|services/fnb-engine-net/src/FnbEngine.API/FnbEngine.API.csproj|NEON_FNB_DATABASE_URL_PRODUCTION"
|
|
["inventory-service"]="services/inventory-service-net/src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj|services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj|NEON_INVENTORY_DATABASE_URL_PRODUCTION"
|
|
["wallet-service"]="services/wallet-service-net/src/WalletService.Infrastructure/WalletService.Infrastructure.csproj|services/wallet-service-net/src/WalletService.API/WalletService.API.csproj|NEON_WALLET_DATABASE_URL_PRODUCTION"
|
|
["catalog-service"]="services/catalog-service-net/src/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj|services/catalog-service-net/src/CatalogService.API/CatalogService.API.csproj|NEON_CATALOG_DATABASE_URL_PRODUCTION"
|
|
["booking-service"]="services/booking-service-net/src/BookingService.Infrastructure/BookingService.Infrastructure.csproj|services/booking-service-net/src/BookingService.API/BookingService.API.csproj|NEON_BOOKING_DATABASE_URL_PRODUCTION"
|
|
)
|
|
|
|
for svc in "${PROD_SERVICES[@]}"; do
|
|
if [ -n "$SINGLE_SERVICE" ] && [ "$SINGLE_SERVICE" != "$svc" ]; then
|
|
continue
|
|
fi
|
|
|
|
IFS='|' read -r infra_proj startup_proj db_env_var <<< "${MIGRATION_MAP[$svc]}"
|
|
infra_path="${ROOT_DIR}/${infra_proj}"
|
|
startup_path="${ROOT_DIR}/${startup_proj}"
|
|
|
|
if [ -f "$infra_path" ] && [ -f "$startup_path" ]; then
|
|
echo -e "${BLUE} Migrating ${svc}...${NC}"
|
|
if [ -n "${!db_env_var:-}" ]; then
|
|
ConnectionStrings__DefaultConnection="${!db_env_var}" \
|
|
dotnet ef database update \
|
|
--project "$infra_path" \
|
|
--startup-project "$startup_path" 2>&1 || {
|
|
echo -e "${RED} Migration failed for ${svc}${NC}"
|
|
read -p "Continue deployment? (yes/no): " continue_deploy
|
|
if [ "$continue_deploy" != "yes" ]; then
|
|
exit 1
|
|
fi
|
|
}
|
|
echo -e "${GREEN} ${svc} migrated${NC}"
|
|
else
|
|
echo -e "${YELLOW} Skipping ${svc} - env var ${db_env_var} not set${NC}"
|
|
fi
|
|
else
|
|
echo -e "${YELLOW} Skipping ${svc} - project files not found${NC}"
|
|
fi
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
# =============================================================================
|
|
# EN: Step 1 - Apply namespace
|
|
# VI: Buoc 1 - Tao namespace
|
|
# =============================================================================
|
|
echo -e "${BLUE}[1/6] Applying namespace...${NC}"
|
|
kubectl apply -f "${DEPLOY_DIR}/namespace.yaml" ${DRY_RUN}
|
|
|
|
# =============================================================================
|
|
# EN: Step 2 - Apply shared configuration
|
|
# VI: Buoc 2 - Ap dung cau hinh chung
|
|
# =============================================================================
|
|
echo -e "${BLUE}[2/6] Applying configuration...${NC}"
|
|
kubectl apply -f "${DEPLOY_DIR}/configmap.yaml" ${DRY_RUN}
|
|
|
|
# =============================================================================
|
|
# EN: Step 3 - Deploy infrastructure (Redis)
|
|
# VI: Buoc 3 - Trien khai ha tang (Redis)
|
|
# =============================================================================
|
|
echo -e "${BLUE}[3/6] Deploying infrastructure...${NC}"
|
|
if [ -z "$SINGLE_SERVICE" ] || [ "$SINGLE_SERVICE" = "redis" ]; then
|
|
kubectl apply -f "${DEPLOY_DIR}/redis.yaml" ${DRY_RUN}
|
|
echo -e "${GREEN} redis deployed${NC}"
|
|
fi
|
|
|
|
# =============================================================================
|
|
# EN: Step 4 - Deploy backend services
|
|
# VI: Buoc 4 - Trien khai cac service backend
|
|
# =============================================================================
|
|
echo -e "${BLUE}[4/6] Deploying services...${NC}"
|
|
|
|
for svc in "${PROD_SERVICES[@]}"; do
|
|
if [ -z "$SINGLE_SERVICE" ] || [ "$SINGLE_SERVICE" = "$svc" ]; then
|
|
if [ -f "${DEPLOY_DIR}/${svc}.yaml" ]; then
|
|
kubectl apply -f "${DEPLOY_DIR}/${svc}.yaml" ${DRY_RUN}
|
|
echo -e "${GREEN} ${svc} deployed${NC}"
|
|
else
|
|
echo -e "${YELLOW} ${svc}.yaml not found, skipping${NC}"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# =============================================================================
|
|
# EN: Step 5 - Apply ingress routing
|
|
# VI: Buoc 5 - Ap dung dinh tuyen ingress
|
|
# =============================================================================
|
|
echo -e "${BLUE}[5/6] Applying ingress routing...${NC}"
|
|
if [ -z "$SINGLE_SERVICE" ]; then
|
|
kubectl apply -f "${DEPLOY_DIR}/ingress.yaml" ${DRY_RUN}
|
|
fi
|
|
|
|
# =============================================================================
|
|
# EN: Step 6 - Wait for rollouts and verify
|
|
# VI: Buoc 6 - Cho rollout va xac minh
|
|
# =============================================================================
|
|
echo ""
|
|
echo -e "${BLUE}[6/6] Waiting for rollouts to complete...${NC}"
|
|
|
|
if [ -z "$DRY_RUN" ]; then
|
|
FAILED=0
|
|
for svc in "${PROD_SERVICES[@]}"; do
|
|
if [ -z "$SINGLE_SERVICE" ] || [ "$SINGLE_SERVICE" = "$svc" ]; then
|
|
echo -n " ${svc}: "
|
|
if kubectl rollout status "deployment/${svc}" -n "${NAMESPACE}" --timeout=180s 2>/dev/null; then
|
|
echo -e "${GREEN}ready${NC}"
|
|
else
|
|
echo -e "${RED}FAILED${NC}"
|
|
FAILED=$((FAILED + 1))
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
|
|
if [ $FAILED -gt 0 ]; then
|
|
echo -e "${RED}WARNING: ${FAILED} service(s) did not complete rollout${NC}"
|
|
echo -e "${YELLOW}Troubleshooting:${NC}"
|
|
echo -e " kubectl get pods -n ${NAMESPACE}"
|
|
echo -e " kubectl describe pod <pod-name> -n ${NAMESPACE}"
|
|
echo -e " kubectl logs -n ${NAMESPACE} -l app=<service-name> --tail=50"
|
|
echo ""
|
|
echo -e "${YELLOW}To rollback a failed service:${NC}"
|
|
echo -e " $0 --rollback <service-name>"
|
|
exit 1
|
|
fi
|
|
|
|
# EN: Print deployment summary
|
|
# VI: In tom tat deployment
|
|
echo -e "${GREEN}=== Production Deployment Summary ===${NC}"
|
|
echo ""
|
|
echo -e "${BLUE}Pods:${NC}"
|
|
kubectl get pods -n "${NAMESPACE}" -o wide --sort-by='.metadata.name' 2>/dev/null
|
|
echo ""
|
|
echo -e "${BLUE}Services:${NC}"
|
|
kubectl get svc -n "${NAMESPACE}" 2>/dev/null
|
|
echo ""
|
|
echo -e "${BLUE}HPAs:${NC}"
|
|
kubectl get hpa -n "${NAMESPACE}" 2>/dev/null
|
|
echo ""
|
|
echo -e "${BLUE}Ingress:${NC}"
|
|
kubectl get ingress -n "${NAMESPACE}" 2>/dev/null
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${GREEN}=== Production deployment completed ===${NC}"
|
|
echo -e "${BLUE}API: https://api.goodgo.vn${NC}"
|
|
echo -e "${BLUE}POS: https://pos.goodgo.vn${NC}"
|
|
echo ""
|
|
echo -e "Verify: kubectl get pods -n ${NAMESPACE}"
|
|
echo -e "Logs: kubectl logs -n ${NAMESPACE} -l app=<service-name> -f"
|
|
echo -e "Rollback: $0 --rollback <service-name>"
|