DEVOPS-C-01: Replace hardcoded :latest with IMAGE_TAG placeholder in all 8
production K8s manifests. Update deploy-production.yml to sed-replace
IMAGE_TAG with commit SHA before kubectl apply (remove now-redundant
kubectl set image step).
DEVOPS-C-02: Configure Alertmanager — create alertmanager.yml with Slack +
email receivers (critical/warning/infra routes, inhibition rules). Add
alertmanager:v0.27.0 service to both docker-compose.observability.yml and
deployments/local/docker-compose.yml. Enable prometheus.yml target
(alertmanager:9093).
DEVOPS-C-03: Remove :latest from docker-build.yml main branch push. Now
only SHA tag is pushed for main; :staging+SHA for develop.
DEVOPS-C-04: Add 4 mkt-* services to deployments/local/docker-compose.yml
with unique host ports (facebook:5021, whatsapp:5022, x:5023, zalo:5024)
to eliminate port 5000 conflicts. Add corresponding Traefik routers and
load-balancer entries in infra/traefik/dynamic/routes.yml
(/api/v1/mkt/{facebook,whatsapp,x,zalo}).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
413 lines
17 KiB
YAML
413 lines
17 KiB
YAML
# 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
|