# EN: Deploy GoodGo Platform production services to Kubernetes production # VI: Trien khai cac service production cua GoodGo Platform len K8s production name: Deploy to Production on: push: branches: - main paths: - 'services/iam-service-net/**' - 'services/merchant-service-net/**' - 'services/order-service-net/**' - 'services/fnb-engine-net/**' - 'services/inventory-service-net/**' - 'services/wallet-service-net/**' - 'services/catalog-service-net/**' - 'services/booking-service-net/**' - 'apps/web-client-tpos-net/**' - 'deployments/production/**' workflow_dispatch: inputs: service: description: 'Service to deploy (leave empty for all changed)' required: false default: '' type: choice options: - '' - iam-service - merchant-service - order-service - fnb-engine - inventory-service - wallet-service - catalog-service - booking-service - pos-web env: REGISTRY: docker.io NAMESPACE: production jobs: # ========================================================================= # EN: Detect which services changed # VI: Phat hien service nao thay doi # ========================================================================= detect-changes: runs-on: ubuntu-latest outputs: services: ${{ steps.changes.outputs.services }} steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - name: Detect changed services id: changes run: | if [ -n "${{ github.event.inputs.service }}" ]; then echo 'services=["${{ github.event.inputs.service }}"]' >> $GITHUB_OUTPUT exit 0 fi SERVICES=() CHANGED=$(git diff --name-only HEAD~1 HEAD) declare -A SERVICE_MAP=( ["services/iam-service-net"]="iam-service" ["services/merchant-service-net"]="merchant-service" ["services/order-service-net"]="order-service" ["services/fnb-engine-net"]="fnb-engine" ["services/inventory-service-net"]="inventory-service" ["services/wallet-service-net"]="wallet-service" ["services/catalog-service-net"]="catalog-service" ["services/booking-service-net"]="booking-service" ["apps/web-client-tpos-net"]="pos-web" ) for path in "${!SERVICE_MAP[@]}"; do if echo "$CHANGED" | grep -q "^${path}/"; then SERVICES+=("\"${SERVICE_MAP[$path]}\"") fi done # EN: If deployment configs changed, deploy all # VI: Neu cau hinh deployment thay doi, deploy tat ca if echo "$CHANGED" | grep -q "^deployments/production/"; then SERVICES=("\"iam-service\"" "\"merchant-service\"" "\"order-service\"" "\"fnb-engine\"" "\"inventory-service\"" "\"wallet-service\"" "\"catalog-service\"" "\"booking-service\"" "\"pos-web\"") fi if [ ${#SERVICES[@]} -eq 0 ]; then echo 'services=[]' >> $GITHUB_OUTPUT else JOINED=$(IFS=,; echo "${SERVICES[*]}") echo "services=[${JOINED}]" >> $GITHUB_OUTPUT fi # ========================================================================= # EN: Build & Push Docker Images (tagged with commit SHA, never :latest) # VI: Build & Push Docker Images (tag bang commit SHA, khong dung :latest) # ========================================================================= build-and-push: needs: detect-changes if: needs.detect-changes.outputs.services != '[]' runs-on: ubuntu-latest # EN: Require production environment approval # VI: Yeu cau phe duyet moi truong production environment: production strategy: matrix: service: ${{ fromJSON(needs.detect-changes.outputs.services) }} steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Determine build context id: context run: | declare -A CONTEXT_MAP=( ["iam-service"]="./services/iam-service-net" ["merchant-service"]="./services/merchant-service-net" ["order-service"]="./services/order-service-net" ["fnb-engine"]="./services/fnb-engine-net" ["inventory-service"]="./services/inventory-service-net" ["wallet-service"]="./services/wallet-service-net" ["catalog-service"]="./services/catalog-service-net" ["booking-service"]="./services/booking-service-net" ["pos-web"]="./apps/web-client-tpos-net" ) declare -A IMAGE_MAP=( ["iam-service"]="goodgo/iam-service-net" ["merchant-service"]="goodgo/merchant-service-net" ["order-service"]="goodgo/order-service-net" ["fnb-engine"]="goodgo/fnb-engine-net" ["inventory-service"]="goodgo/inventory-service-net" ["wallet-service"]="goodgo/wallet-service-net" ["catalog-service"]="goodgo/catalog-service-net" ["booking-service"]="goodgo/booking-service-net" ["pos-web"]="goodgo/web-client-tpos-net" ) echo "context=${CONTEXT_MAP[${{ matrix.service }}]}" >> $GITHUB_OUTPUT echo "image=${IMAGE_MAP[${{ matrix.service }}]}" >> $GITHUB_OUTPUT # EN: Tag with commit SHA for production (never use :latest in prod) # VI: Tag bang commit SHA cho production (khong bao gio dung :latest trong prod) - name: Build and push ${{ matrix.service }} uses: docker/build-push-action@v5 with: context: ${{ steps.context.outputs.context }} push: true tags: | ${{ steps.context.outputs.image }}:${{ github.sha }} ${{ steps.context.outputs.image }}:production cache-from: type=registry,ref=${{ steps.context.outputs.image }}:buildcache-prod cache-to: type=registry,ref=${{ steps.context.outputs.image }}:buildcache-prod,mode=max # ========================================================================= # EN: Run Database Migrations # VI: Chay Database Migrations # ========================================================================= migrations: needs: [detect-changes, build-and-push] if: needs.detect-changes.outputs.services != '[]' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - name: Install EF Core tools run: dotnet tool install --global dotnet-ef || true - name: Run IAM migrations if: contains(needs.detect-changes.outputs.services, 'iam-service') run: | dotnet ef database update \ --project services/iam-service-net/src/IamService.Infrastructure/IamService.Infrastructure.csproj \ --startup-project services/iam-service-net/src/IamService.API/IamService.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_IAM_DATABASE_URL_PRODUCTION }} - name: Run Merchant migrations if: contains(needs.detect-changes.outputs.services, 'merchant-service') run: | dotnet ef database update \ --project services/merchant-service-net/src/MerchantService.Infrastructure/MerchantService.Infrastructure.csproj \ --startup-project services/merchant-service-net/src/MerchantService.API/MerchantService.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_MERCHANT_DATABASE_URL_PRODUCTION }} - name: Run Order migrations if: contains(needs.detect-changes.outputs.services, 'order-service') run: | dotnet ef database update \ --project services/order-service-net/src/OrderService.Infrastructure/OrderService.Infrastructure.csproj \ --startup-project services/order-service-net/src/OrderService.API/OrderService.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_ORDER_DATABASE_URL_PRODUCTION }} - name: Run FnB Engine migrations if: contains(needs.detect-changes.outputs.services, 'fnb-engine') run: | dotnet ef database update \ --project services/fnb-engine-net/src/FnbEngine.Infrastructure/FnbEngine.Infrastructure.csproj \ --startup-project services/fnb-engine-net/src/FnbEngine.API/FnbEngine.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_FNB_DATABASE_URL_PRODUCTION }} - name: Run Inventory migrations if: contains(needs.detect-changes.outputs.services, 'inventory-service') run: | dotnet ef database update \ --project services/inventory-service-net/src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj \ --startup-project services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_INVENTORY_DATABASE_URL_PRODUCTION }} - name: Run Wallet migrations if: contains(needs.detect-changes.outputs.services, 'wallet-service') run: | dotnet ef database update \ --project services/wallet-service-net/src/WalletService.Infrastructure/WalletService.Infrastructure.csproj \ --startup-project services/wallet-service-net/src/WalletService.API/WalletService.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_WALLET_DATABASE_URL_PRODUCTION }} - name: Run Catalog migrations if: contains(needs.detect-changes.outputs.services, 'catalog-service') run: | dotnet ef database update \ --project services/catalog-service-net/src/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj \ --startup-project services/catalog-service-net/src/CatalogService.API/CatalogService.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_CATALOG_DATABASE_URL_PRODUCTION }} - name: Run Booking migrations if: contains(needs.detect-changes.outputs.services, 'booking-service') run: | dotnet ef database update \ --project services/booking-service-net/src/BookingService.Infrastructure/BookingService.Infrastructure.csproj \ --startup-project services/booking-service-net/src/BookingService.API/BookingService.API.csproj env: ConnectionStrings__DefaultConnection: ${{ secrets.NEON_BOOKING_DATABASE_URL_PRODUCTION }} # ========================================================================= # EN: Deploy to Kubernetes # VI: Trien khai len Kubernetes # ========================================================================= deploy: needs: [detect-changes, build-and-push, migrations] if: needs.detect-changes.outputs.services != '[]' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - name: Setup kubectl uses: azure/setup-kubectl@v3 - name: Configure kubectl run: | echo "${{ secrets.KUBECONFIG_PRODUCTION }}" | base64 -d > kubeconfig echo "KUBECONFIG=$(pwd)/kubeconfig" >> $GITHUB_ENV - name: Apply namespace and config run: | kubectl apply -f deployments/production/kubernetes/namespace.yaml kubectl apply -f deployments/production/kubernetes/configmap.yaml - name: Deploy Redis run: | kubectl apply -f deployments/production/kubernetes/redis.yaml # EN: Update image tags to commit SHA and deploy # VI: Cap nhat image tag bang commit SHA va deploy - name: Deploy services run: | SERVICES='${{ needs.detect-changes.outputs.services }}' declare -A DEPLOY_MAP=( ["iam-service"]="iam-service.yaml" ["merchant-service"]="merchant-service.yaml" ["order-service"]="order-service.yaml" ["fnb-engine"]="fnb-engine.yaml" ["inventory-service"]="inventory-service.yaml" ["wallet-service"]="wallet-service.yaml" ["catalog-service"]="catalog-service.yaml" ["booking-service"]="booking-service.yaml" ["pos-web"]="pos-web.yaml" ) declare -A IMAGE_MAP=( ["iam-service"]="goodgo/iam-service-net" ["merchant-service"]="goodgo/merchant-service-net" ["order-service"]="goodgo/order-service-net" ["fnb-engine"]="goodgo/fnb-engine-net" ["inventory-service"]="goodgo/inventory-service-net" ["wallet-service"]="goodgo/wallet-service-net" ["catalog-service"]="goodgo/catalog-service-net" ["booking-service"]="goodgo/booking-service-net" ["pos-web"]="goodgo/web-client-tpos-net" ) for svc in "${!DEPLOY_MAP[@]}"; do if echo "$SERVICES" | grep -q "\"${svc}\""; then echo "Deploying ${svc}..." # EN: Replace IMAGE_TAG placeholder with commit SHA before applying (never :latest in production) # VI: Thay the IMAGE_TAG bang commit SHA truoc khi apply (khong bao gio dung :latest trong production) MANIFEST="deployments/production/kubernetes/${DEPLOY_MAP[$svc]}" sed "s|IMAGE_TAG|${{ github.sha }}|g" "$MANIFEST" | kubectl apply -f - kubectl rollout restart "deployment/${svc}" -n production fi done - name: Apply ingress run: | kubectl apply -f deployments/production/kubernetes/ingress.yaml - name: Wait for rollouts run: | SERVICES='${{ needs.detect-changes.outputs.services }}' declare -A DEPLOY_NAMES=( ["iam-service"]="iam-service" ["merchant-service"]="merchant-service" ["order-service"]="order-service" ["fnb-engine"]="fnb-engine" ["inventory-service"]="inventory-service" ["wallet-service"]="wallet-service" ["catalog-service"]="catalog-service" ["booking-service"]="booking-service" ["pos-web"]="pos-web" ) FAILED=0 for svc in "${!DEPLOY_NAMES[@]}"; do if echo "$SERVICES" | grep -q "\"${svc}\""; then echo "Waiting for ${svc}..." if ! kubectl rollout status "deployment/${DEPLOY_NAMES[$svc]}" -n production --timeout=180s; then echo "FAILED: ${svc} rollout did not complete in time" FAILED=$((FAILED + 1)) fi fi done if [ $FAILED -gt 0 ]; then echo "ERROR: ${FAILED} service(s) did not complete rollout" kubectl get pods -n production exit 1 fi - name: Verify deployment run: | echo "=== Pods ===" kubectl get pods -n production -o wide echo "" echo "=== Services ===" kubectl get svc -n production echo "" echo "=== HPAs ===" kubectl get hpa -n production echo "" echo "=== Ingress ===" kubectl get ingress -n production # EN: Post-deployment health check # VI: Kiem tra suc khoe sau deploy - name: Health check run: | SERVICES='${{ needs.detect-changes.outputs.services }}' declare -A DEPLOY_NAMES=( ["iam-service"]="iam-service" ["merchant-service"]="merchant-service" ["order-service"]="order-service" ["fnb-engine"]="fnb-engine" ["inventory-service"]="inventory-service" ["wallet-service"]="wallet-service" ["catalog-service"]="catalog-service" ["booking-service"]="booking-service" ) echo "Running post-deployment health checks..." for svc in "${!DEPLOY_NAMES[@]}"; do if echo "$SERVICES" | grep -q "\"${svc}\""; then POD=$(kubectl get pods -n production -l app=${svc} -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) if [ -n "$POD" ]; then HEALTH=$(kubectl exec "$POD" -n production -- curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health/live 2>/dev/null || echo "000") if [ "$HEALTH" = "200" ]; then echo "${svc}: HEALTHY (200)" else echo "${svc}: UNHEALTHY (${HEALTH})" fi fi fi done