From aec5cb88c4e3a4d9039c90df5daadd51c8a42a57 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 09:49:18 +1000 Subject: [PATCH 01/25] Update backend_ci.yml --- .github/workflows/backend_ci.yml | 130 ++++++++++++++----------------- 1 file changed, 58 insertions(+), 72 deletions(-) diff --git a/.github/workflows/backend_ci.yml b/.github/workflows/backend_ci.yml index d69725aa..6e34fd7c 100644 --- a/.github/workflows/backend_ci.yml +++ b/.github/workflows/backend_ci.yml @@ -1,91 +1,70 @@ # week08/.github/workflows/backend_ci.yml +# week08/.github/workflows/backend_ci.yml +name: Backend CI — Test, Build and Push Images to ACR -name: Backend CI - Test, Build and Push Images to ACR - -# Trigger the workflow on pushes to the 'main' branch -# You can also add 'pull_request:' to run on PRs on: - # Manual trigger workflow_dispatch: - - # Automatically on pushes to main branch push: - branches: - - main - paths: # Only trigger if changes are in backend directories + branches: [ main ] + paths: - 'backend/**' - - '.github/workflows/backend_ci.yml' # Trigger if this workflow file changes + - '.github/workflows/backend_ci.yml' -# Define global environment variables that can be used across jobs env: - # ACR Login Server (e.g., myregistry.azurecr.io) - # This needs to be set as a GitHub Repository Secret - ACR_LOGIN_SERVER: ${{ secrets.AZURE_CONTAINER_REGISTRY }} - # Dynamically generate image tags based on Git SHA and GitHub Run ID - # This provides unique, traceable tags for each image build + # Use the secret you created in Step 3 (FQDN like sit722acr12345.azurecr.io) + ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }} + # Unique tag per run for traceability IMAGE_TAG: ${{ github.sha }}-${{ github.run_id }} jobs: - # Job 1: Run tests and linting for all backend services test_and_lint_backends: - runs-on: ubuntu-latest # Use a GitHub-hosted runner + runs-on: ubuntu-latest services: - # Product DB container product_db: image: postgres:15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: products - # Make pg_isready available so the service is healthy before tests run options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 - ports: - - 5432:5432 - - # Order DB + ports: [ "5432:5432" ] + order_db: image: postgres:15 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: orders - ports: - - 5433:5432 options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 + ports: [ "5433:5432" ] steps: - # 1. Checkout the repository code to the runner - name: Checkout repository - uses: actions/checkout@v4 # Action to check out your repository code + uses: actions/checkout@v4 - # 2. Set up Python environment - name: Set up Python 3.10 - uses: actions/setup-python@v5 # Action to set up Python environment + uses: actions/setup-python@v5 with: python-version: '3.10' - # 3. Install dependencies and run code quality checks - name: Install dependencies - run: | # Use a multi-line script to install pip dependencies + run: | pip install --upgrade pip - # Loop through each backend service folder for req in backend/*/requirements.txt; do echo "Installing $req" pip install -r "$req" done - # Install CI tools pip install pytest httpx - # 5. Run tests for product service - name: Run product_service tests working-directory: backend/product_service env: @@ -94,10 +73,8 @@ jobs: POSTGRES_DB: products POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - run: | - pytest tests --maxfail=1 --disable-warnings -q - - # 6. Run tests for order service + run: pytest tests --maxfail=1 --disable-warnings -q + - name: Run order_service tests working-directory: backend/order_service env: @@ -106,41 +83,50 @@ jobs: POSTGRES_DB: orders POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - run: | - pytest tests --maxfail=1 --disable-warnings -q + run: pytest tests --maxfail=1 --disable-warnings -q - # Job 2: Build and Push Docker Images (runs only if tests pass) build_and_push_images: runs-on: ubuntu-latest needs: test_and_lint_backends steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Azure login using a Service Principal secret - - name: Azure Login - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} # Needs to be set as a GitHub Secret (Service Principal JSON) - - # Login to Azure Container Registry (ACR) - - name: Login to Azure Container Registry - run: az acr login --name ${{ env.ACR_LOGIN_SERVER }} - - # Build and Push Docker image for Product Service - - name: Build and Push Product Service Image - run: | - docker build -t ${{ env.ACR_LOGIN_SERVER }}/product_service:latest ./backend/product_service/ - docker push ${{ env.ACR_LOGIN_SERVER }}/product_service:latest - - # Build and Push Docker image for Order Service - - name: Build and Push Order Service Image - run: | - docker build -t ${{ env.ACR_LOGIN_SERVER }}/order_service:latest ./backend/order_service/ - docker push ${{ env.ACR_LOGIN_SERVER }}/order_service:latest - - # Logout from Azure for security (runs even if image push fails) - - name: Logout from Azure - run: az logout - if: always() + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Azure login + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + # ACR login must use the short registry name (no .azurecr.io) + - name: Login to Azure Container Registry + run: | + ACR_NAME=$(echo "${{ env.ACR_LOGIN_SERVER }}" | cut -d. -f1) + az acr login --name "$ACR_NAME" + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + # Build & push product_service + - name: Build & push product_service + env: + IMG_BASE: ${{ env.ACR_LOGIN_SERVER }}/product_service + run: | + docker build -t "$IMG_BASE:${{ env.IMAGE_TAG }}" ./backend/product_service + docker tag "$IMG_BASE:${{ env.IMAGE_TAG }}" "$IMG_BASE:latest" + docker push "$IMG_BASE:${{ env.IMAGE_TAG }}" + docker push "$IMG_BASE:latest" + + # Build & push order_service + - name: Build & push order_service + env: + IMG_BASE: ${{ env.ACR_LOGIN_SERVER }}/order_service + run: | + docker build -t "$IMG_BASE:${{ env.IMAGE_TAG }}" ./backend/order_service + docker tag "$IMG_BASE:${{ env.IMAGE_TAG }}" "$IMG_BASE:latest" + docker push "$IMG_BASE:${{ env.IMAGE_TAG }}" + docker push "$IMG_BASE:latest" + + - name: Logout from Azure + if: always() + run: az logout From b63ec07ff78a185f9e6c045ea946d5199bf93182 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 09:49:59 +1000 Subject: [PATCH 02/25] fix(ci): ACR login + dual tags; tests preserved --- backend/ci-trigger.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/ci-trigger.txt diff --git a/backend/ci-trigger.txt b/backend/ci-trigger.txt new file mode 100644 index 00000000..49c8f34d --- /dev/null +++ b/backend/ci-trigger.txt @@ -0,0 +1 @@ +trigger Sat 20 Sep 2025 09:49:59 AEST From 1fb67204b5a7b2e015403e56a7e861a7f116f49c Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 09:57:42 +1000 Subject: [PATCH 03/25] chore: ignore azure_creds.json --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cfa76a7f..7c2ddd04 100644 --- a/.gitignore +++ b/.gitignore @@ -194,4 +194,4 @@ cython_debug/ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data # refer to https://docs.cursor.com/context/ignore-files .cursorignore -.cursorindexingignore \ No newline at end of file +.cursorindexingignoreazure_creds.json From d3955367052977467f5f854313e9ac1532a5b1dd Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 11:38:53 +1000 Subject: [PATCH 04/25] Update backend-cd.yml --- .github/workflows/backend-cd.yml | 165 ++++++++++++++++++------------- 1 file changed, 99 insertions(+), 66 deletions(-) diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index 6035ed15..d77dc262 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -3,99 +3,132 @@ name: CD - Deploy Backend Services to AKS on: workflow_dispatch: inputs: - aks_cluster_name: - description: 'Name of the AKS Cluster to deploy to' - required: true - default: '' - aks_resource_group: - description: 'Resource Group of the AKS Cluster' - required: true - default: '' - aks_acr_name: - description: 'Name of ACR' - required: true - default: '' + image_tag: + description: "Image tag to deploy (default: latest)" + required: false + default: "latest" + +env: + # Change if your manifests use a different namespace (empty = default namespace) + NAMESPACE: "" jobs: deploy_backend: runs-on: ubuntu-latest environment: Production - + outputs: - PRODUCT_API_IP: ${{ steps.get_product_ip.outputs.external_ip }} - ORDER_API_IP: ${{ steps.get_order_ip.outputs.external_ip }} + PRODUCT_API_IP: ${{ steps.capture_ips.outputs.product_ip }} + ORDER_API_IP: ${{ steps.capture_ips.outputs.order_ip }} steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Log in to Azure - uses: azure/login@v1 + - name: Azure login + uses: azure/login@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - enable-AzPSSession: true - - name: Set Kubernetes context (get AKS credentials) + - name: Derive ACR vars + id: acr run: | - az aks get-credentials --resource-group ${{ github.event.inputs.aks_resource_group }} --name ${{ github.event.inputs.aks_cluster_name }} --overwrite-existing + echo "ACR_LOGIN_SERVER=${{ secrets.ACR_LOGIN_SERVER }}" >> $GITHUB_ENV + echo "ACR_NAME=$(echo '${{ secrets.ACR_LOGIN_SERVER }}' | cut -d. -f1)" >> $GITHUB_ENV + echo "TAG=${{ github.event.inputs.image_tag || 'latest' }}" >> $GITHUB_ENV + + - name: Set AKS context + uses: azure/aks-set-context@v4 + with: + resource-group: ${{ secrets.AKS_RG }} + cluster-name: ${{ secrets.AKS_CLUSTER }} - - name: Attach ACR + # harmless if already attached + - name: Attach ACR permissions run: | - az aks update --name ${{ github.event.inputs.aks_cluster_name }} --resource-group ${{ github.event.inputs.aks_resource_group }} --attach-acr ${{ github.event.inputs.aks_acr_name }} + az aks update -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --attach-acr "$ACR_NAME" - - name: Deploy Backend Infrastructure (Namespace, ConfigMaps, Secrets, Databases) + - name: Create namespace if set (optional) + if: env.NAMESPACE != '' run: | - echo "Deploying backend infrastructure..." - cd k8s/ - kubectl apply -f configmaps.yaml - kubectl apply -f secrets.yaml - kubectl apply -f product-db.yaml - kubectl apply -f order-db.yaml - - - name: Deploy Backend Microservices (Product, Order) + kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" + + - name: Apply backend manifests run: | - echo "Deploying backend microservices..." - cd k8s/ - kubectl apply -f product-service.yaml - kubectl apply -f order-service.yaml - - - name: Wait for Backend LoadBalancer IPs + set -e + # If your k8s yaml files are elsewhere, adjust these paths. + if [ -d k8s ]; then + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/ + elif [ -d kubernetes/backend ]; then + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f kubernetes/backend/ + else + echo "::warning::No 'k8s' folder found. Continuing to set images/rollout." + fi + + - name: Update images & rollout + run: | + set -e + ns_flag="" + [ -n "$NAMESPACE" ] && ns_flag="-n $NAMESPACE" + + # Image repos match CI pushes: product_service & order_service + PRODUCT_IMG="${ACR_LOGIN_SERVER}/product_service:${TAG}" + ORDER_IMG="${ACR_LOGIN_SERVER}/order_service:${TAG}" + + # Try both common deployment names and both container names (dash/underscore) + for dep in product-service-w08e1 product-service; do + kubectl $ns_flag set image deploy/$dep product-service=$PRODUCT_IMG --record || true + kubectl $ns_flag set image deploy/$dep product_service=$PRODUCT_IMG --record || true + kubectl $ns_flag rollout status deploy/$dep --timeout=180s || true + done + + for dep in order-service-w08e1 order-service; do + kubectl $ns_flag set image deploy/$dep order-service=$ORDER_IMG --record || true + kubectl $ns_flag set image deploy/$dep order_service=$ORDER_IMG --record || true + kubectl $ns_flag rollout status deploy/$dep --timeout=180s || true + done + + - name: Wait for LoadBalancer IPs (up to 5 minutes) + id: capture_ips run: | - echo "Waiting for Product, Order LoadBalancer IPs to be assigned (up to 5 minutes)..." + set -e + ns_flag="" + [ -n "$NAMESPACE" ] && ns_flag="-n $NAMESPACE" + + svc_names_product=("product-service-w08e1" "product-service") + svc_names_order=("order-service-w08e1" "order-service") + PRODUCT_IP="" ORDER_IP="" - - for i in $(seq 1 60); do - echo "Attempt $i/60 to get IPs..." - PRODUCT_IP=$(kubectl get service product-service-w08e1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}') - ORDER_IP=$(kubectl get service order-service-w08e1 -o jsonpath='{.status.loadBalancer.ingress[0].ip}') - if [[ -n "$PRODUCT_IP" && -n "$ORDER_IP" ]]; then - echo "All backend LoadBalancer IPs assigned!" + for i in $(seq 1 60); do + echo "Attempt $i/60 to get external IPs ..." + for s in "${svc_names_product[@]}"; do + ip=$(kubectl $ns_flag get svc "$s" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + [ -n "$ip" ] && PRODUCT_IP="$ip" && break + done + for s in "${svc_names_order[@]}"; do + ip=$(kubectl $ns_flag get svc "$s" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + [ -n "$ip" ] && ORDER_IP="$ip" && break + done + if [ -n "$PRODUCT_IP" ] && [ -n "$ORDER_IP" ]; then echo "Product Service IP: $PRODUCT_IP" - echo "Order Service IP: $ORDER_IP" + echo "Order Service IP: $ORDER_IP" break fi - sleep 5 # Wait 5 seconds before next attempt + sleep 5 done - - if [[ -z "$PRODUCT_IP" || -z "$ORDER_IP" ]]; then - echo "Error: One or more LoadBalancer IPs not assigned after timeout." - exit 1 # Fail the job if IPs are not obtained + + if [ -z "$PRODUCT_IP" ] || [ -z "$ORDER_IP" ]; then + echo "::error::One or more LoadBalancer IPs not assigned after timeout." + exit 1 fi - - # These are environment variables for subsequent steps in the *same job* - # And used to set the job outputs + + echo "product_ip=$PRODUCT_IP" >> $GITHUB_OUTPUT + echo "order_ip=$ORDER_IP" >> $GITHUB_OUTPUT echo "PRODUCT_IP=$PRODUCT_IP" >> $GITHUB_ENV - echo "ORDER_IP=$ORDER_IP" >> $GITHUB_ENV - - - name: Capture Product Service IP for Workflow Output - id: get_product_ip - run: echo "external_ip=${{ env.PRODUCT_IP }}" >> $GITHUB_OUTPUT - - - name: Capture Order Service IP for Workflow Output - id: get_order_ip - run: echo "external_ip=${{ env.ORDER_IP }}" >> $GITHUB_OUTPUT - - - name: Logout from Azure - run: az logout + echo "ORDER_IP=$ORDER_IP" >> $GITHUB_ENV + + - name: Show services + run: | + kubectl ${NAMESPACE:+-n $NAMESPACE} get svc -o wide From e6bc22067dae7a1e66be0248ea06a8dca3fbbb79 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 11:45:53 +1000 Subject: [PATCH 05/25] Update backend-cd.yml --- .github/workflows/backend-cd.yml | 195 +++++++++++++++++++++++-------- 1 file changed, 144 insertions(+), 51 deletions(-) diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index d77dc262..2d443679 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -9,7 +9,7 @@ on: default: "latest" env: - # Change if your manifests use a different namespace (empty = default namespace) + # leave empty to use the default namespace NAMESPACE: "" jobs: @@ -30,8 +30,7 @@ jobs: with: creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: Derive ACR vars - id: acr + - name: Derive ACR + tag run: | echo "ACR_LOGIN_SERVER=${{ secrets.ACR_LOGIN_SERVER }}" >> $GITHUB_ENV echo "ACR_NAME=$(echo '${{ secrets.ACR_LOGIN_SERVER }}' | cut -d. -f1)" >> $GITHUB_ENV @@ -43,74 +42,169 @@ jobs: resource-group: ${{ secrets.AKS_RG }} cluster-name: ${{ secrets.AKS_CLUSTER }} - # harmless if already attached - - name: Attach ACR permissions + # harmless if already attached; won't fail the job + - name: Attach ACR permissions (best effort) run: | - az aks update -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --attach-acr "$ACR_NAME" + az aks update -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --attach-acr "$ACR_NAME" || true - - name: Create namespace if set (optional) + - name: Ensure namespace (optional) if: env.NAMESPACE != '' run: | kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" - - name: Apply backend manifests + # --- Ensure Postgres backends exist (creates or updates) --- + - name: Ensure product-db run: | - set -e - # If your k8s yaml files are elsewhere, adjust these paths. - if [ -d k8s ]; then - kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/ - elif [ -d kubernetes/backend ]; then - kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f kubernetes/backend/ - else - echo "::warning::No 'k8s' folder found. Continuing to set images/rollout." - fi - - - name: Update images & rollout + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - <<'YAML' + apiVersion: apps/v1 + kind: Deployment + metadata: { name: product-db } + spec: + replicas: 1 + selector: { matchLabels: { app: product-db } } + template: + metadata: { labels: { app: product-db } } + spec: + containers: + - name: postgres + image: postgres:15 + env: + - { name: POSTGRES_PASSWORD, value: "postgres" } + - { name: POSTGRES_DB, value: "products" } + ports: [ { containerPort: 5432 } ] + --- + apiVersion: v1 + kind: Service + metadata: { name: product-db } + spec: + selector: { app: product-db } + ports: [ { port: 5432, targetPort: 5432 } ] + YAML + + - name: Ensure order-db + run: | + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - <<'YAML' + apiVersion: apps/v1 + kind: Deployment + metadata: { name: order-db } + spec: + replicas: 1 + selector: { matchLabels: { app: order-db } } + template: + metadata: { labels: { app: order-db } } + spec: + containers: + - name: postgres + image: postgres:15 + env: + - { name: POSTGRES_PASSWORD, value: "postgres" } + - { name: POSTGRES_DB, value: "orders" } + ports: [ { containerPort: 5432 } ] + --- + apiVersion: v1 + kind: Service + metadata: { name: order-db } + spec: + selector: { app: order-db } + ports: [ { port: 5432, targetPort: 5432 } ] + YAML + + # --- Ensure product-service + order-service exist (creates or updates) --- + - name: Ensure product-service deployment & service + run: | + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - <<'YAML' + apiVersion: apps/v1 + kind: Deployment + metadata: { name: product-service } + spec: + replicas: 1 + selector: { matchLabels: { app: product-service } } + template: + metadata: { labels: { app: product-service } } + spec: + containers: + - name: product-service + image: replace-later + imagePullPolicy: Always + env: + - { name: POSTGRES_HOST, value: "product-db" } + - { name: POSTGRES_PORT, value: "5432" } + - { name: POSTGRES_DB, value: "products" } + - { name: POSTGRES_USER, value: "postgres" } + - { name: POSTGRES_PASSWORD, value: "postgres" } + ports: [ { containerPort: 3000 } ] + readinessProbe: { httpGet: { path: /health, port: 3000 }, initialDelaySeconds: 5, periodSeconds: 10 } + livenessProbe: { httpGet: { path: /health, port: 3000 }, initialDelaySeconds: 10, periodSeconds: 20 } + --- + apiVersion: v1 + kind: Service + metadata: { name: product-service } + spec: + type: LoadBalancer + selector: { app: product-service } + ports: [ { port: 80, targetPort: 3000 } ] + YAML + + - name: Ensure order-service deployment & service + run: | + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - <<'YAML' + apiVersion: apps/v1 + kind: Deployment + metadata: { name: order-service } + spec: + replicas: 1 + selector: { matchLabels: { app: order-service } } + template: + metadata: { labels: { app: order-service } } + spec: + containers: + - name: order-service + image: replace-later + imagePullPolicy: Always + env: + - { name: POSTGRES_HOST, value: "order-db" } + - { name: POSTGRES_PORT, value: "5432" } + - { name: POSTGRES_DB, value: "orders" } + - { name: POSTGRES_USER, value: "postgres" } + - { name: POSTGRES_PASSWORD, value: "postgres" } + ports: [ { containerPort: 3001 } ] + readinessProbe: { httpGet: { path: /health, port: 3001 }, initialDelaySeconds: 5, periodSeconds: 10 } + livenessProbe: { httpGet: { path: /health, port: 3001 }, initialDelaySeconds: 10, periodSeconds: 20 } + --- + apiVersion: v1 + kind: Service + metadata: { name: order-service } + spec: + type: LoadBalancer + selector: { app: order-service } + ports: [ { port: 80, targetPort: 3001 } ] + YAML + + - name: Set images to ACR tag and rollout run: | - set -e ns_flag="" [ -n "$NAMESPACE" ] && ns_flag="-n $NAMESPACE" - - # Image repos match CI pushes: product_service & order_service PRODUCT_IMG="${ACR_LOGIN_SERVER}/product_service:${TAG}" ORDER_IMG="${ACR_LOGIN_SERVER}/order_service:${TAG}" - # Try both common deployment names and both container names (dash/underscore) - for dep in product-service-w08e1 product-service; do - kubectl $ns_flag set image deploy/$dep product-service=$PRODUCT_IMG --record || true - kubectl $ns_flag set image deploy/$dep product_service=$PRODUCT_IMG --record || true - kubectl $ns_flag rollout status deploy/$dep --timeout=180s || true - done + kubectl $ns_flag set image deploy/product-service product-service="$PRODUCT_IMG" --record || true + kubectl $ns_flag set image deploy/order-service order-service="$ORDER_IMG" --record || true - for dep in order-service-w08e1 order-service; do - kubectl $ns_flag set image deploy/$dep order-service=$ORDER_IMG --record || true - kubectl $ns_flag set image deploy/$dep order_service=$ORDER_IMG --record || true - kubectl $ns_flag rollout status deploy/$dep --timeout=180s || true - done + kubectl $ns_flag rollout status deploy/product-service --timeout=240s || true + kubectl $ns_flag rollout status deploy/order-service --timeout=240s || true - - name: Wait for LoadBalancer IPs (up to 5 minutes) + - name: Wait for LoadBalancer IPs (up to 6 minutes) id: capture_ips run: | - set -e ns_flag="" [ -n "$NAMESPACE" ] && ns_flag="-n $NAMESPACE" - svc_names_product=("product-service-w08e1" "product-service") - svc_names_order=("order-service-w08e1" "order-service") - PRODUCT_IP="" ORDER_IP="" - - for i in $(seq 1 60); do - echo "Attempt $i/60 to get external IPs ..." - for s in "${svc_names_product[@]}"; do - ip=$(kubectl $ns_flag get svc "$s" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) - [ -n "$ip" ] && PRODUCT_IP="$ip" && break - done - for s in "${svc_names_order[@]}"; do - ip=$(kubectl $ns_flag get svc "$s" -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) - [ -n "$ip" ] && ORDER_IP="$ip" && break - done + for i in $(seq 1 72); do + echo "Attempt $i/72 to get external IPs ..." + PRODUCT_IP=$(kubectl $ns_flag get svc product-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + ORDER_IP=$(kubectl $ns_flag get svc order-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) if [ -n "$PRODUCT_IP" ] && [ -n "$ORDER_IP" ]; then echo "Product Service IP: $PRODUCT_IP" echo "Order Service IP: $ORDER_IP" @@ -130,5 +224,4 @@ jobs: echo "ORDER_IP=$ORDER_IP" >> $GITHUB_ENV - name: Show services - run: | - kubectl ${NAMESPACE:+-n $NAMESPACE} get svc -o wide + run: kubectl ${NAMESPACE:+-n $NAMESPACE} get svc -o wide From 0887c11b85396ee75c15031fede20c650afd82a6 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 12:32:11 +1000 Subject: [PATCH 06/25] frontend: point API to AKS backends --- frontend/src/config.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 frontend/src/config.js diff --git a/frontend/src/config.js b/frontend/src/config.js new file mode 100644 index 00000000..47590c91 --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1,2 @@ +export const PRODUCT_API_BASE = "http://4.237.211.110"; +export const ORDER_API_BASE = "http://4.254.28.11"; From ca42364ed553480582c26c7f4e7d7186c984f3b4 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 12:52:07 +1000 Subject: [PATCH 07/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 133 +++++++++++++++--------------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index 0a0879c8..db68def6 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -2,92 +2,95 @@ name: CD - Deploy Frontend to AKS -# This workflow can be called by other workflows and takes inputs. -# Or it can be run manually if you provide the IPs. on: workflow_dispatch: inputs: - product_api_ip: - description: 'External IP of Product Service' - required: true - default: 'http://:8000' - order_api_ip: - description: 'External IP of Order Service (e.g., http://Y.Y.Y.Y:8001)' - required: true - default: 'http://:8001' - aks_cluster_name: - description: 'Name of the AKS Cluster to deploy to' - required: true - default: '' - aks_resource_group: - description: 'Resource Group of the AKS Cluster' - required: true - default: '<' + image_tag: + description: "Image tag to deploy (default: latest)" + required: false + default: "latest" - workflow_call: - inputs: - product_api_ip: - required: true - type: string - order_api_ip: - required: true - type: string - aks_cluster_name: - required: true - type: string - aks_resource_group: - required: true - type: string +env: + # leave empty to use the default namespace + NAMESPACE: "" jobs: deploy_frontend: runs-on: ubuntu-latest - environment: Production steps: - name: Checkout repository uses: actions/checkout@v4 - # Azure login using a Service Principal secret - - name: Azure Login - uses: azure/login@v1 + - name: Azure login + uses: azure/login@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - # Login to Azure Container Registry (ACR) - - name: Login to Azure Container Registry - run: az acr login --name ${{ secrets.AZURE_CONTAINER_REGISTRY }} + - name: Derive ACR + tag + run: | + echo "ACR_LOGIN_SERVER=${{ secrets.ACR_LOGIN_SERVER }}" >> $GITHUB_ENV + echo "TAG=${{ github.event.inputs.image_tag || 'latest' }}" >> $GITHUB_ENV + + - name: Set AKS context + uses: azure/aks-set-context@v4 + with: + resource-group: ${{ secrets.AKS_RG }} + cluster-name: ${{ secrets.AKS_CLUSTER }} - - name: Inject Backend IPs into Frontend main.js + # Harmless if already attached; don't fail the job if it is. + - name: Attach ACR permissions (best effort) run: | - echo "Injecting IPs into frontend/static/js/main.js" - # Ensure frontend/main.js is directly in the path for sed - sed -i "s|_PRODUCT_API_URL_|${{ inputs.product_api_ip }}|g" frontend/main.js - sed -i "s|_ORDER_API_URL_|${{ inputs.order_api_ip }}|g" frontend/main.js - - # Display the modified file content for debugging - echo "--- Modified main.js content ---" - cat frontend/main.js - echo "---------------------------------" + ACR_NAME=$(echo "${ACR_LOGIN_SERVER}" | cut -d. -f1) + az aks update -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --attach-acr "$ACR_NAME" || true - # Build and Push Docker image for Frontend - - name: Build and Push Frontend Image + - name: Ensure namespace (optional) + if: env.NAMESPACE != '' run: | - docker build -t ${{ secrets.AZURE_CONTAINER_REGISTRY }}/frontend:latest ./frontend/ - docker push ${{ secrets.AZURE_CONTAINER_REGISTRY }}/frontend:latest + kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" - - name: Set Kubernetes context (get AKS credentials) - uses: azure/aks-set-context@v3 - with: - resource-group: ${{ inputs.aks_resource_group }} - cluster-name: ${{ inputs.aks_cluster_name }} + # Apply a manifest if present; otherwise create minimal Deployment/Service inline + - name: Apply frontend manifests (if repo has them) + run: | + set -e + if [ -f k8s/frontend.yaml ]; then + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend.yaml + elif [ -d k8s/frontend ]; then + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend/ + else + echo "::notice::No k8s/frontend* found; creating minimal resources inline." + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - <<'YAML' + apiVersion: apps/v1 + kind: Deployment + metadata: { name: frontend } + spec: + replicas: 1 + selector: { matchLabels: { app: frontend } } + template: + metadata: { labels: { app: frontend } } + spec: + containers: + - name: frontend + image: nginx:alpine # replaced below with your ACR image + ports: [ { containerPort: 80 } ] + readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } + livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } + --- + apiVersion: v1 + kind: Service + metadata: { name: frontend } + spec: + type: LoadBalancer + selector: { app: frontend } + ports: [ { port: 80, targetPort: 80 } ] + YAML + fi - - name: Deploy Frontend to AKS + - name: Set image to ACR tag and rollout run: | - echo "Deploying frontend with latest tag to AKS cluster: ${{ inputs.aks_cluster_name }}" - cd k8s/ - # Ensure frontend-service.yaml is configured with your ACR - kubectl apply -f frontend.yaml + IMG="${ACR_LOGIN_SERVER}/frontend:${TAG}" + kubectl ${NAMESPACE:+-n $NAMESPACE} set image deploy/frontend frontend="$IMG" --record || true + kubectl ${NAMESPACE:+-n $NAMESPACE} rollout status deploy/frontend --timeout=240s || true - - name: Logout from Azure (AKS deployment) - run: az logout + - name: Show services + run: kubectl ${NAMESPACE:+-n $NAMESPACE} get svc -o wide From c18a4eeca0f6477bfe7a738ec976acfb9751a30b Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 13:00:26 +1000 Subject: [PATCH 08/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index db68def6..733fe488 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -11,8 +11,7 @@ on: default: "latest" env: - # leave empty to use the default namespace - NAMESPACE: "" + NAMESPACE: "" # leave empty to use default namespace jobs: deploy_frontend: @@ -38,7 +37,7 @@ jobs: resource-group: ${{ secrets.AKS_RG }} cluster-name: ${{ secrets.AKS_CLUSTER }} - # Harmless if already attached; don't fail the job if it is. + # Harmless if already attached; don't fail if it is. - name: Attach ACR permissions (best effort) run: | ACR_NAME=$(echo "${ACR_LOGIN_SERVER}" | cut -d. -f1) @@ -49,16 +48,14 @@ jobs: run: | kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" - # Apply a manifest if present; otherwise create minimal Deployment/Service inline - - name: Apply frontend manifests (if repo has them) + # Apply repo manifests if present; otherwise create minimal resources inline + - name: Ensure frontend Deployment & Service run: | - set -e if [ -f k8s/frontend.yaml ]; then kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend.yaml elif [ -d k8s/frontend ]; then kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend/ else - echo "::notice::No k8s/frontend* found; creating minimal resources inline." kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - <<'YAML' apiVersion: apps/v1 kind: Deployment @@ -71,7 +68,7 @@ jobs: spec: containers: - name: frontend - image: nginx:alpine # replaced below with your ACR image + image: nginx:alpine # replaced below ports: [ { containerPort: 80 } ] readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } From e841c33470dcb3dc8576b3d0d9a5ddc102a7e84c Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 13:09:06 +1000 Subject: [PATCH 09/25] Update frontend_ci.yml --- .github/workflows/frontend_ci.yml | 85 ++++++++++++++++++------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/.github/workflows/frontend_ci.yml b/.github/workflows/frontend_ci.yml index 9f9e76d9..66932c64 100644 --- a/.github/workflows/frontend_ci.yml +++ b/.github/workflows/frontend_ci.yml @@ -3,24 +3,16 @@ name: Frontend CI - Build & Push Image on: - # Manual trigger workflow_dispatch: - - # Automatically on pushes to main branch push: - branches: - - main - paths: # Only trigger if changes are in the frontend directory + branches: [ main ] + paths: - 'frontend/**' - - '.github/workflows/frontend_ci.yml' # Trigger if this workflow file changes + - '.github/workflows/frontend_ci.yml' -# Define global environment variables that can be used across jobs env: - # ACR Login Server (e.g., myregistry.azurecr.io) - # This needs to be set as a GitHub Repository Secret - ACR_LOGIN_SERVER: ${{ secrets.AZURE_CONTAINER_REGISTRY }} - # Dynamically generate image tags based on Git SHA and GitHub Run ID - # This provides unique, traceable tags for each image build + # Use the secret you actually created + ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }} IMAGE_TAG: ${{ github.sha }}-${{ github.run_id }} jobs: @@ -28,26 +20,47 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Azure login using a Service Principal secret - - name: Azure Login - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - # Login to Azure Container Registry (ACR) - - name: Login to Azure Container Registry - run: az acr login --name ${{ env.ACR_LOGIN_SERVER }} - - # Build and Push Docker image for Frontend - - name: Build and Push Frontend Image - run: | - docker build -t ${{ env.ACR_LOGIN_SERVER }}/frontend:latest ./frontend/ - docker push ${{ env.ACR_LOGIN_SERVER }}/frontend:latest - - # Logout from Azure for security (runs even if image push fails) - - name: Logout from Azure - run: az logout - if: always() + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node (optional build step) + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install & build frontend if package.json exists + run: | + if [ -f frontend/package.json ]; then + npm ci --prefix frontend + npm --prefix frontend run build --if-present || true + else + echo "No package.json found in frontend/. Skipping npm build." + fi + + - name: Azure login + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to ACR + run: | + # az acr login expects the ACR NAME, not the FQDN + ACR_NAME=$(echo "${{ env.ACR_LOGIN_SERVER }}" | cut -d. -f1) + echo "Logging in to ACR: $ACR_NAME (from ${ACR_LOGIN_SERVER})" + az acr login --name "$ACR_NAME" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build & push frontend image + env: + IMG_BASE: ${{ env.ACR_LOGIN_SERVER }}/frontend + run: | + # Requires a Dockerfile in ./frontend (see note below) + docker build -t "$IMG_BASE:${{ env.IMAGE_TAG }}" -t "$IMG_BASE:latest" ./frontend + docker push "$IMG_BASE:${{ env.IMAGE_TAG }}" + docker push "$IMG_BASE:latest" + + - name: Logout from Azure + if: always() + run: az logout From 1540699eeda5c4c915b72b56234dfcb4d28eb708 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 13:29:19 +1000 Subject: [PATCH 10/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index 733fe488..5708a67c 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -37,7 +37,7 @@ jobs: resource-group: ${{ secrets.AKS_RG }} cluster-name: ${{ secrets.AKS_CLUSTER }} - # Harmless if already attached; don't fail if it is. + # Harmless if already attached; don't fail the job if it is. - name: Attach ACR permissions (best effort) run: | ACR_NAME=$(echo "${ACR_LOGIN_SERVER}" | cut -d. -f1) @@ -48,7 +48,6 @@ jobs: run: | kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" - # Apply repo manifests if present; otherwise create minimal resources inline - name: Ensure frontend Deployment & Service run: | if [ -f k8s/frontend.yaml ]; then @@ -68,7 +67,7 @@ jobs: spec: containers: - name: frontend - image: nginx:alpine # replaced below + image: nginx:alpine # replaced below with your ACR image ports: [ { containerPort: 80 } ] readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } From 498a71b49d8b0382dbb5061715d93e705dffb82f Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 13:39:00 +1000 Subject: [PATCH 11/25] Update frontend.yaml --- k8s/frontend.yaml | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml index 1948536d..c4f7a398 100644 --- a/k8s/frontend.yaml +++ b/k8s/frontend.yaml @@ -17,23 +17,35 @@ spec: app: frontend spec: containers: - - name: frontend-container - image: durgeshsamariya.azurecr.io/frontend:latest - imagePullPolicy: Always - ports: - - containerPort: 80 + # MUST be 'frontend' so the CD step "kubectl set image deploy/frontend frontend=..." works + - name: frontend + # Placeholder image; the CD workflow will replace this with: + # /frontend:latest + image: nginx:alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + readinessProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 10 + periodSeconds: 20 --- apiVersion: v1 kind: Service metadata: - name: frontend-w08e1 # Service name matches + # MUST be 'frontend' to match the CD logs/S23 screenshot + name: frontend labels: app: frontend spec: selector: app: frontend + type: LoadBalancer ports: - - protocol: TCP - port: 80 # The port the service listens on inside the cluster - targetPort: 80 # The port on the Pod (containerPort where Nginx runs) - type: LoadBalancer # Exposes the service on a port on each Node's IP + - name: http + port: 80 + targetPort: 80 From c93a3595ac26b0b9002811835d75a7b7a016a26d Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 13:49:42 +1000 Subject: [PATCH 12/25] Update frontend.yaml --- k8s/frontend.yaml | 41 ++++++++++++----------------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml index c4f7a398..2b8a3cdf 100644 --- a/k8s/frontend.yaml +++ b/k8s/frontend.yaml @@ -1,50 +1,33 @@ # week08/k8s/frontend.yaml +# week08/k8s/frontend.yaml apiVersion: apps/v1 kind: Deployment metadata: name: frontend - labels: - app: frontend + labels: { app: frontend } spec: replicas: 1 - selector: - matchLabels: - app: frontend + selector: { matchLabels: { app: frontend } } template: - metadata: - labels: - app: frontend + metadata: { labels: { app: frontend } } spec: containers: - # MUST be 'frontend' so the CD step "kubectl set image deploy/frontend frontend=..." works - - name: frontend - # Placeholder image; the CD workflow will replace this with: - # /frontend:latest - image: nginx:alpine + - name: frontend # <- must be 'frontend' + image: nginx:alpine # placeholder; CD sets ACR image imagePullPolicy: IfNotPresent - ports: - - containerPort: 80 - readinessProbe: - httpGet: { path: /, port: 80 } - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - httpGet: { path: /, port: 80 } - initialDelaySeconds: 10 - periodSeconds: 20 + ports: [ { containerPort: 80 } ] + readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } + livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } --- apiVersion: v1 kind: Service metadata: - # MUST be 'frontend' to match the CD logs/S23 screenshot - name: frontend - labels: - app: frontend + name: frontend # <- must be 'frontend' + labels: { app: frontend } spec: - selector: - app: frontend type: LoadBalancer + selector: { app: frontend } ports: - name: http port: 80 From 1886b3f401f7a8dd3a3791eccf24ef8a46825b79 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 14:05:55 +1000 Subject: [PATCH 13/25] Update frontend.yaml --- k8s/frontend.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml index 2b8a3cdf..2bb75daf 100644 --- a/k8s/frontend.yaml +++ b/k8s/frontend.yaml @@ -1,6 +1,5 @@ # week08/k8s/frontend.yaml -# week08/k8s/frontend.yaml apiVersion: apps/v1 kind: Deployment metadata: @@ -13,8 +12,8 @@ spec: metadata: { labels: { app: frontend } } spec: containers: - - name: frontend # <- must be 'frontend' - image: nginx:alpine # placeholder; CD sets ACR image + - name: frontend + image: nginx:alpine imagePullPolicy: IfNotPresent ports: [ { containerPort: 80 } ] readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } @@ -23,7 +22,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: frontend # <- must be 'frontend' + name: frontend labels: { app: frontend } spec: type: LoadBalancer @@ -32,3 +31,4 @@ spec: - name: http port: 80 targetPort: 80 + From 65020a2dc58e4ea79ef35a24add05a402f1a98b0 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 14:21:33 +1000 Subject: [PATCH 14/25] frontend: use in-cluster service URLs (ClusterIP) --- frontend/src/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/config.js b/frontend/src/config.js index 47590c91..392883a1 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -1,2 +1,2 @@ -export const PRODUCT_API_BASE = "http://4.237.211.110"; -export const ORDER_API_BASE = "http://4.254.28.11"; +export const PRODUCT_API_BASE = "http://product-service"; +export const ORDER_API_BASE = "http://order-service"; From 9c47c68f4f92f60995e533722b59c251a2e3ab66 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 17:38:10 +1000 Subject: [PATCH 15/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 55 ++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index 5708a67c..f29b3a74 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -26,10 +26,14 @@ jobs: with: creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: Derive ACR + tag + - name: Derive env (ACR, TAG, node RG) + shell: bash run: | echo "ACR_LOGIN_SERVER=${{ secrets.ACR_LOGIN_SERVER }}" >> $GITHUB_ENV echo "TAG=${{ github.event.inputs.image_tag || 'latest' }}" >> $GITHUB_ENV + NODE_RG=$(az aks show -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --query nodeResourceGroup -o tsv) + echo "NODE_RG=$NODE_RG" >> $GITHUB_ENV + echo "Node RG: $NODE_RG" - name: Set AKS context uses: azure/aks-set-context@v4 @@ -37,8 +41,9 @@ jobs: resource-group: ${{ secrets.AKS_RG }} cluster-name: ${{ secrets.AKS_CLUSTER }} - # Harmless if already attached; don't fail the job if it is. - - name: Attach ACR permissions (best effort) + # If ACR was never attached to the cluster, attach it (no-op otherwise). + - name: Attach ACR to AKS (best effort) + shell: bash run: | ACR_NAME=$(echo "${ACR_LOGIN_SERVER}" | cut -d. -f1) az aks update -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --attach-acr "$ACR_NAME" || true @@ -49,16 +54,17 @@ jobs: kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" - name: Ensure frontend Deployment & Service + shell: bash run: | if [ -f k8s/frontend.yaml ]; then kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend.yaml elif [ -d k8s/frontend ]; then kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend/ else - kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - <<'YAML' + cat <<'YAML' | kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - apiVersion: apps/v1 kind: Deployment - metadata: { name: frontend } + metadata: { name: frontend, labels: { app: frontend } } spec: replicas: 1 selector: { matchLabels: { app: frontend } } @@ -67,14 +73,14 @@ jobs: spec: containers: - name: frontend - image: nginx:alpine # replaced below with your ACR image + image: nginx:alpine ports: [ { containerPort: 80 } ] readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } --- apiVersion: v1 kind: Service - metadata: { name: frontend } + metadata: { name: frontend, labels: { app: frontend } } spec: type: LoadBalancer selector: { app: frontend } @@ -82,11 +88,44 @@ jobs: YAML fi + # Free public IP quota by making backends internal (no error if already ClusterIP) + - name: Make backend services ClusterIP (free PIPs) – best effort + shell: bash + run: | + kubectl patch svc product-service -p '{"spec":{"type":"ClusterIP"}}' || true + kubectl patch svc order-service -p '{"spec":{"type":"ClusterIP"}}' || true + + # Make sure AKS knows the node RG where PIPs live + - name: Annotate frontend Service with node RG + shell: bash + run: | + kubectl annotate svc frontend service.beta.kubernetes.io/azure-load-balancer-resource-group="$NODE_RG" --overwrite || true + - name: Set image to ACR tag and rollout + shell: bash run: | IMG="${ACR_LOGIN_SERVER}/frontend:${TAG}" kubectl ${NAMESPACE:+-n $NAMESPACE} set image deploy/frontend frontend="$IMG" --record || true kubectl ${NAMESPACE:+-n $NAMESPACE} rollout status deploy/frontend --timeout=240s || true - - name: Show services + - name: Wait for frontend EXTERNAL-IP (soft wait, no fail) + shell: bash + run: | + echo "Waiting up to 5 minutes for an external IP..." + for i in {1..60}; do + IP=$(kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + if [ -n "$IP" ]; then + echo "EXTERNAL-IP=$IP" + echo "FRONTEND_IP=$IP" >> $GITHUB_ENV + break + fi + sleep 5 + done + kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o wide || true + + - name: Describe frontend Service (diagnostics) + if: env.FRONTEND_IP == '' + run: kubectl ${NAMESPACE:+-n $NAMESPACE} describe svc frontend || true + + - name: Show services (final) run: kubectl ${NAMESPACE:+-n $NAMESPACE} get svc -o wide From d04c512870bb64100ec8f45278b392c934f43fe7 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 17:46:49 +1000 Subject: [PATCH 16/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 63 +++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index f29b3a74..35acf621 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -1,5 +1,6 @@ # week08/.github/workflows/frontend-cd.yml +# .github/workflows/frontend-cd.yml name: CD - Deploy Frontend to AKS on: @@ -11,14 +12,15 @@ on: default: "latest" env: - NAMESPACE: "" # leave empty to use default namespace + NAMESPACE: "" # keep default namespace + PIP_NAME: w08-frontend-pip jobs: deploy_frontend: runs-on: ubuntu-latest steps: - - name: Checkout repository + - name: Checkout uses: actions/checkout@v4 - name: Azure login @@ -41,7 +43,6 @@ jobs: resource-group: ${{ secrets.AKS_RG }} cluster-name: ${{ secrets.AKS_CLUSTER }} - # If ACR was never attached to the cluster, attach it (no-op otherwise). - name: Attach ACR to AKS (best effort) shell: bash run: | @@ -53,7 +54,7 @@ jobs: run: | kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" - - name: Ensure frontend Deployment & Service + - name: Ensure frontend Deployment & Service exist shell: bash run: | if [ -f k8s/frontend.yaml ]; then @@ -88,44 +89,66 @@ jobs: YAML fi - # Free public IP quota by making backends internal (no error if already ClusterIP) - - name: Make backend services ClusterIP (free PIPs) – best effort + # 1) Make sure we have ONE static PIP in the node RG (create if missing) + - name: Ensure/allocate static Public IP shell: bash run: | - kubectl patch svc product-service -p '{"spec":{"type":"ClusterIP"}}' || true - kubectl patch svc order-service -p '{"spec":{"type":"ClusterIP"}}' || true + if ! az network public-ip show -g "$NODE_RG" -n "$PIP_NAME" >/dev/null 2>&1; then + az network public-ip create -g "$NODE_RG" -n "$PIP_NAME" --sku Standard --allocation-method static + fi + PIP=$(az network public-ip show -g "$NODE_RG" -n "$PIP_NAME" --query ipAddress -o tsv) + echo "FRONTEND_PIP=$PIP" >> $GITHUB_ENV + echo "Using PIP: $PIP" - # Make sure AKS knows the node RG where PIPs live - - name: Annotate frontend Service with node RG + # 2) Ensure the frontend Service uses that PIP and the correct RG + - name: Bind PIP to Service (annotation + loadBalancerIP) shell: bash run: | kubectl annotate svc frontend service.beta.kubernetes.io/azure-load-balancer-resource-group="$NODE_RG" --overwrite || true + # try patch; if immutable, recreate just the Service with the fixed spec + if ! kubectl patch svc frontend -p '{"spec":{"type":"LoadBalancer","loadBalancerIP":"'"$FRONTEND_PIP"'"}}'; then + echo "Recreating Service with static PIP…" + kubectl delete svc frontend --ignore-not-found + cat </dev/null || true) if [ -n "$IP" ]; then echo "EXTERNAL-IP=$IP" - echo "FRONTEND_IP=$IP" >> $GITHUB_ENV break fi sleep 5 done - kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o wide || true + kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o wide - - name: Describe frontend Service (diagnostics) - if: env.FRONTEND_IP == '' + - name: Describe Service (diagnostics if still failing) + if: ${{ failure() }} run: kubectl ${NAMESPACE:+-n $NAMESPACE} describe svc frontend || true - - - name: Show services (final) - run: kubectl ${NAMESPACE:+-n $NAMESPACE} get svc -o wide From 7b9c8034002a8d1f6671dc4fc4e0c94f0910a2ab Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 17:55:38 +1000 Subject: [PATCH 17/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 105 +++++++++++++++--------------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index 35acf621..d98ad6d4 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -1,6 +1,4 @@ # week08/.github/workflows/frontend-cd.yml - -# .github/workflows/frontend-cd.yml name: CD - Deploy Frontend to AKS on: @@ -12,8 +10,8 @@ on: default: "latest" env: - NAMESPACE: "" # keep default namespace - PIP_NAME: w08-frontend-pip + NAMESPACE: "" # keep default namespace + PIP_NAME: w08-frontend-pip # static PIP to (re)use in the node RG jobs: deploy_frontend: @@ -54,7 +52,7 @@ jobs: run: | kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" - - name: Ensure frontend Deployment & Service exist + - name: Ensure frontend Deployment & Service shell: bash run: | if [ -f k8s/frontend.yaml ]; then @@ -62,34 +60,39 @@ jobs: elif [ -d k8s/frontend ]; then kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend/ else - cat <<'YAML' | kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f - - apiVersion: apps/v1 - kind: Deployment - metadata: { name: frontend, labels: { app: frontend } } - spec: - replicas: 1 - selector: { matchLabels: { app: frontend } } - template: - metadata: { labels: { app: frontend } } - spec: - containers: - - name: frontend - image: nginx:alpine - ports: [ { containerPort: 80 } ] - readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } - livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } - --- - apiVersion: v1 - kind: Service - metadata: { name: frontend, labels: { app: frontend } } - spec: - type: LoadBalancer - selector: { app: frontend } - ports: [ { port: 80, targetPort: 80 } ] - YAML + # write a minimal known-good manifest and apply it + cat >/tmp/frontend.yaml <<'YAML' +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: { app: frontend } +spec: + replicas: 1 + selector: { matchLabels: { app: frontend } } + template: + metadata: { labels: { app: frontend } } + spec: + containers: + - name: frontend + image: nginx:alpine # will be set to your ACR image below + ports: [ { containerPort: 80 } ] + readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } + livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: { app: frontend } +spec: + type: LoadBalancer + selector: { app: frontend } + ports: [ { port: 80, targetPort: 80 } ] +YAML + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f /tmp/frontend.yaml fi - # 1) Make sure we have ONE static PIP in the node RG (create if missing) - name: Ensure/allocate static Public IP shell: bash run: | @@ -100,32 +103,31 @@ jobs: echo "FRONTEND_PIP=$PIP" >> $GITHUB_ENV echo "Using PIP: $PIP" - # 2) Ensure the frontend Service uses that PIP and the correct RG - name: Bind PIP to Service (annotation + loadBalancerIP) shell: bash run: | kubectl annotate svc frontend service.beta.kubernetes.io/azure-load-balancer-resource-group="$NODE_RG" --overwrite || true - # try patch; if immutable, recreate just the Service with the fixed spec + # Try patch first; if Service was created with different type/spec and patch fails, recreate Service with our settings. if ! kubectl patch svc frontend -p '{"spec":{"type":"LoadBalancer","loadBalancerIP":"'"$FRONTEND_PIP"'"}}'; then - echo "Recreating Service with static PIP…" kubectl delete svc frontend --ignore-not-found - cat </tmp/frontend-svc.yaml < Date: Sat, 20 Sep 2025 18:14:16 +1000 Subject: [PATCH 18/25] frontend: point API to backend public LB IPs --- frontend/src/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/config.js b/frontend/src/config.js index 392883a1..47590c91 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -1,2 +1,2 @@ -export const PRODUCT_API_BASE = "http://product-service"; -export const ORDER_API_BASE = "http://order-service"; +export const PRODUCT_API_BASE = "http://4.237.211.110"; +export const ORDER_API_BASE = "http://4.254.28.11"; From d3929ff245dd5487c452fae1aefe60ce3d7f7837 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 21:23:17 +1000 Subject: [PATCH 19/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 141 ++++-------------------------- 1 file changed, 17 insertions(+), 124 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index d98ad6d4..0d3f2305 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -10,148 +10,41 @@ on: default: "latest" env: - NAMESPACE: "" # keep default namespace - PIP_NAME: w08-frontend-pip # static PIP to (re)use in the node RG + # leave empty to use default namespace + NAMESPACE: "" jobs: deploy_frontend: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v4 - name: Azure login - uses: azure/login@v2 + uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: Derive env (ACR, TAG, node RG) - shell: bash - run: | - echo "ACR_LOGIN_SERVER=${{ secrets.ACR_LOGIN_SERVER }}" >> $GITHUB_ENV - echo "TAG=${{ github.event.inputs.image_tag || 'latest' }}" >> $GITHUB_ENV - NODE_RG=$(az aks show -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --query nodeResourceGroup -o tsv) - echo "NODE_RG=$NODE_RG" >> $GITHUB_ENV - echo "Node RG: $NODE_RG" - - name: Set AKS context - uses: azure/aks-set-context@v4 + uses: azure/aks-set-context@v3 with: resource-group: ${{ secrets.AKS_RG }} cluster-name: ${{ secrets.AKS_CLUSTER }} - - name: Attach ACR to AKS (best effort) - shell: bash - run: | - ACR_NAME=$(echo "${ACR_LOGIN_SERVER}" | cut -d. -f1) - az aks update -n "${{ secrets.AKS_CLUSTER }}" -g "${{ secrets.AKS_RG }}" --attach-acr "$ACR_NAME" || true - - - name: Ensure namespace (optional) - if: env.NAMESPACE != '' + - name: Deploy/Update k8s resources run: | - kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" + # Apply your existing manifests (Deployment + Service) + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend.yaml - - name: Ensure frontend Deployment & Service - shell: bash + - name: Roll to latest ACR image + env: + ACR: ${{ secrets.ACR_LOGIN_SERVER }} + TAG: ${{ github.event.inputs.image_tag || 'latest' }} run: | - if [ -f k8s/frontend.yaml ]; then - kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend.yaml - elif [ -d k8s/frontend ]; then - kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend/ - else - # write a minimal known-good manifest and apply it - cat >/tmp/frontend.yaml <<'YAML' -apiVersion: apps/v1 -kind: Deployment -metadata: - name: frontend - labels: { app: frontend } -spec: - replicas: 1 - selector: { matchLabels: { app: frontend } } - template: - metadata: { labels: { app: frontend } } - spec: - containers: - - name: frontend - image: nginx:alpine # will be set to your ACR image below - ports: [ { containerPort: 80 } ] - readinessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 5, periodSeconds: 10 } - livenessProbe: { httpGet: { path: /, port: 80 }, initialDelaySeconds: 10, periodSeconds: 20 } ---- -apiVersion: v1 -kind: Service -metadata: - name: frontend - labels: { app: frontend } -spec: - type: LoadBalancer - selector: { app: frontend } - ports: [ { port: 80, targetPort: 80 } ] -YAML - kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f /tmp/frontend.yaml - fi - - - name: Ensure/allocate static Public IP - shell: bash - run: | - if ! az network public-ip show -g "$NODE_RG" -n "$PIP_NAME" >/dev/null 2>&1; then - az network public-ip create -g "$NODE_RG" -n "$PIP_NAME" --sku Standard --allocation-method static - fi - PIP=$(az network public-ip show -g "$NODE_RG" -n "$PIP_NAME" --query ipAddress -o tsv) - echo "FRONTEND_PIP=$PIP" >> $GITHUB_ENV - echo "Using PIP: $PIP" - - - name: Bind PIP to Service (annotation + loadBalancerIP) - shell: bash - run: | - kubectl annotate svc frontend service.beta.kubernetes.io/azure-load-balancer-resource-group="$NODE_RG" --overwrite || true - # Try patch first; if Service was created with different type/spec and patch fails, recreate Service with our settings. - if ! kubectl patch svc frontend -p '{"spec":{"type":"LoadBalancer","loadBalancerIP":"'"$FRONTEND_PIP"'"}}'; then - kubectl delete svc frontend --ignore-not-found - cat >/tmp/frontend-svc.yaml </dev/null || true) - if [ -n "$IP" ]; then - echo "EXTERNAL-IP=$IP" - break - fi - sleep 5 - done - kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o wide - - - name: Describe Service (diagnostics if still failing) - if: ${{ failure() }} - run: kubectl ${NAMESPACE:+-n $NAMESPACE} describe svc frontend || true + IMG="${ACR}/frontend:${TAG}" + kubectl ${NAMESPACE:+-n $NAMESPACE} set image deploy/frontend frontend="$IMG" --record + kubectl ${NAMESPACE:+-n $NAMESPACE} rollout status deploy/frontend --timeout=300s + - name: Show service + run: kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o wide From 33f3f6add8fa1b5284b0adc6a4ae5a02775c6708 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 22:56:01 +1000 Subject: [PATCH 20/25] Update frontend-cd.yml --- .github/workflows/frontend-cd.yml | 44 ++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/.github/workflows/frontend-cd.yml b/.github/workflows/frontend-cd.yml index 0d3f2305..11b84a92 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -10,7 +10,7 @@ on: default: "latest" env: - # leave empty to use default namespace + # Leave empty to use the default namespace NAMESPACE: "" jobs: @@ -26,6 +26,35 @@ jobs: with: creds: ${{ secrets.AZURE_CREDENTIALS }} + # --- Inject backend IPs into the built JS before building the image --- + # Adjust the path to the file that contains the placeholders if needed. + - name: Inject Backend IPs into frontend bundle + run: | + set -e + echo "Replacing placeholders in frontend/main.js ..." + # Use / at the end to make URL-safe with new URL(...) + sed -i "s|_PRODUCT_API_URL_|http://4.237.211.110/|g" frontend/main.js + sed -i "s|_ORDER_API_URL_|http://4.254.28.11/|g" frontend/main.js + echo "Preview of main.js lines containing http://" + grep -n "http://" frontend/main.js || true + + # --- Build & push the frontend image to ACR --- + - name: Login to ACR + env: + ACR_FQDN: ${{ secrets.ACR_LOGIN_SERVER }} # e.g. sit722acr12345.azurecr.io + run: | + ACR_NAME=$(echo "$ACR_FQDN" | cut -d. -f1) + az acr login --name "$ACR_NAME" + + - name: Build & Push Frontend Image + env: + ACR_FQDN: ${{ secrets.ACR_LOGIN_SERVER }} + TAG: ${{ github.event.inputs.image_tag || 'latest' }} + run: | + docker build -t "${ACR_FQDN}/frontend:${TAG}" ./frontend + docker push "${ACR_FQDN}/frontend:${TAG}" + + # --- Set AKS context & (re)apply k8s objects --- - name: Set AKS context uses: azure/aks-set-context@v3 with: @@ -34,17 +63,20 @@ jobs: - name: Deploy/Update k8s resources run: | - # Apply your existing manifests (Deployment + Service) kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend.yaml - - name: Roll to latest ACR image + - name: Roll to latest image env: - ACR: ${{ secrets.ACR_LOGIN_SERVER }} + ACR_FQDN: ${{ secrets.ACR_LOGIN_SERVER }} TAG: ${{ github.event.inputs.image_tag || 'latest' }} run: | - IMG="${ACR}/frontend:${TAG}" + IMG="${ACR_FQDN}/frontend:${TAG}" kubectl ${NAMESPACE:+-n $NAMESPACE} set image deploy/frontend frontend="$IMG" --record kubectl ${NAMESPACE:+-n $NAMESPACE} rollout status deploy/frontend --timeout=300s - name: Show service - run: kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o wide + run: | + kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o wide + echo + echo "Frontend IP:" + kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o jsonpath='{.status.loadBalancer.ingress[0].ip}'; echo From 1fe6ab652c254c3b177457aa4fea7bd8e1971cc2 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 23:24:26 +1000 Subject: [PATCH 21/25] Update frontend_ci.yml --- .github/workflows/frontend_ci.yml | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/frontend_ci.yml b/.github/workflows/frontend_ci.yml index 66932c64..842493b0 100644 --- a/.github/workflows/frontend_ci.yml +++ b/.github/workflows/frontend_ci.yml @@ -11,7 +11,7 @@ on: - '.github/workflows/frontend_ci.yml' env: - # Use the secret you actually created + # FQDN of your ACR, e.g. sit722acr1870.azurecr.io ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }} IMAGE_TAG: ${{ github.sha }}-${{ github.run_id }} @@ -23,6 +23,28 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + # ⬇️ Patch backend IPs into the source BEFORE building the image + - name: Patch frontend/src/config.js with backend IPs + run: | + set -euo pipefail + FILE="frontend/src/config.js" + mkdir -p frontend/src + # Create if missing, otherwise replace existing values + if [ ! -f "$FILE" ]; then + cat > "$FILE" <<'EOF' +export const PRODUCT_API_BASE = "http://4.237.211.110"; +export const ORDER_API_BASE = "http://4.254.28.11"; +EOF + else + sed -E -i 's|export const PRODUCT_API_BASE[[:space:]]*=.*;|export const PRODUCT_API_BASE = "http://4.237.211.110";|g' "$FILE" + sed -E -i 's|export const ORDER_API_BASE[[:space:]]*=.*;|export const ORDER_API_BASE = "http://4.254.28.11";|g' "$FILE" + grep -q 'PRODUCT_API_BASE' "$FILE" || echo 'export const PRODUCT_API_BASE = "http://4.237.211.110";' >> "$FILE" + grep -q 'ORDER_API_BASE' "$FILE" || echo 'export const ORDER_API_BASE = "http://4.254.28.11";' >> "$FILE" + fi + echo "===== resulting config.js =====" + cat "$FILE" + + # Optional local (non-Docker) build if you keep a Node app here. - name: Setup Node (optional build step) uses: actions/setup-node@v4 with: @@ -44,7 +66,7 @@ jobs: - name: Login to ACR run: | - # az acr login expects the ACR NAME, not the FQDN + # az acr login expects the ACR NAME (left part of FQDN) ACR_NAME=$(echo "${{ env.ACR_LOGIN_SERVER }}" | cut -d. -f1) echo "Logging in to ACR: $ACR_NAME (from ${ACR_LOGIN_SERVER})" az acr login --name "$ACR_NAME" @@ -56,7 +78,6 @@ jobs: env: IMG_BASE: ${{ env.ACR_LOGIN_SERVER }}/frontend run: | - # Requires a Dockerfile in ./frontend (see note below) docker build -t "$IMG_BASE:${{ env.IMAGE_TAG }}" -t "$IMG_BASE:latest" ./frontend docker push "$IMG_BASE:${{ env.IMAGE_TAG }}" docker push "$IMG_BASE:latest" From e2a06d622fc84db6792561706c918dd5f9077c28 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 23:28:59 +1000 Subject: [PATCH 22/25] Update config.js --- frontend/src/config.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/config.js b/frontend/src/config.js index 47590c91..1bdb7653 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -1,2 +1,9 @@ +- name: Write backend IPs into config.js + run: | + mkdir -p frontend/src + cat > frontend/src/config.js <<'EOF' export const PRODUCT_API_BASE = "http://4.237.211.110"; export const ORDER_API_BASE = "http://4.254.28.11"; +EOF + echo "==== config.js ====" + cat frontend/src/config.js From 9b8aab3054835df26b896b94de6e98dbc5fd3cdf Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Sat, 20 Sep 2025 23:32:01 +1000 Subject: [PATCH 23/25] Update frontend_ci.yml --- .github/workflows/frontend_ci.yml | 45 +++++++------------------------ 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/.github/workflows/frontend_ci.yml b/.github/workflows/frontend_ci.yml index 842493b0..3b5ad1ed 100644 --- a/.github/workflows/frontend_ci.yml +++ b/.github/workflows/frontend_ci.yml @@ -11,7 +11,6 @@ on: - '.github/workflows/frontend_ci.yml' env: - # FQDN of your ACR, e.g. sit722acr1870.azurecr.io ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }} IMAGE_TAG: ${{ github.sha }}-${{ github.run_id }} @@ -23,41 +22,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # ⬇️ Patch backend IPs into the source BEFORE building the image - - name: Patch frontend/src/config.js with backend IPs + # 🔧 Always write the backend IPs into config.js (no heredoc, no sed) + - name: Write backend IPs into config.js run: | - set -euo pipefail - FILE="frontend/src/config.js" mkdir -p frontend/src - # Create if missing, otherwise replace existing values - if [ ! -f "$FILE" ]; then - cat > "$FILE" <<'EOF' -export const PRODUCT_API_BASE = "http://4.237.211.110"; -export const ORDER_API_BASE = "http://4.254.28.11"; -EOF - else - sed -E -i 's|export const PRODUCT_API_BASE[[:space:]]*=.*;|export const PRODUCT_API_BASE = "http://4.237.211.110";|g' "$FILE" - sed -E -i 's|export const ORDER_API_BASE[[:space:]]*=.*;|export const ORDER_API_BASE = "http://4.254.28.11";|g' "$FILE" - grep -q 'PRODUCT_API_BASE' "$FILE" || echo 'export const PRODUCT_API_BASE = "http://4.237.211.110";' >> "$FILE" - grep -q 'ORDER_API_BASE' "$FILE" || echo 'export const ORDER_API_BASE = "http://4.254.28.11";' >> "$FILE" - fi - echo "===== resulting config.js =====" - cat "$FILE" - - # Optional local (non-Docker) build if you keep a Node app here. - - name: Setup Node (optional build step) - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Install & build frontend if package.json exists - run: | - if [ -f frontend/package.json ]; then - npm ci --prefix frontend - npm --prefix frontend run build --if-present || true - else - echo "No package.json found in frontend/. Skipping npm build." - fi + printf '%s\n' \ + 'export const PRODUCT_API_BASE = "http://4.237.211.110";' \ + 'export const ORDER_API_BASE = "http://4.254.28.11";' \ + > frontend/src/config.js + echo "----- frontend/src/config.js -----" + cat frontend/src/config.js - name: Azure login uses: azure/login@v2 @@ -66,9 +40,7 @@ EOF - name: Login to ACR run: | - # az acr login expects the ACR NAME (left part of FQDN) ACR_NAME=$(echo "${{ env.ACR_LOGIN_SERVER }}" | cut -d. -f1) - echo "Logging in to ACR: $ACR_NAME (from ${ACR_LOGIN_SERVER})" az acr login --name "$ACR_NAME" - name: Set up Docker Buildx @@ -78,6 +50,7 @@ EOF env: IMG_BASE: ${{ env.ACR_LOGIN_SERVER }}/frontend run: | + test -f frontend/Dockerfile || { echo "Missing frontend/Dockerfile"; exit 1; } docker build -t "$IMG_BASE:${{ env.IMAGE_TAG }}" -t "$IMG_BASE:latest" ./frontend docker push "$IMG_BASE:${{ env.IMAGE_TAG }}" docker push "$IMG_BASE:latest" From 294ba18e385f9798f30cd3e9b37345d44edba6da Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Wed, 1 Oct 2025 23:19:31 +1000 Subject: [PATCH 24/25] fix: resolve merge conflict in frontend/src/config.js (keep remote) --- .ci-test.txt | 1 + .github/workflows/deploy-dev.yml | 29 ++++++ .github/workflows/deploy-prod.yml | 29 ++++++ .github/workflows/pr-ci.yml | 33 +++++++ .github/workflows/reusable-deploy.yml | 127 ++++++++++++++++++++++++++ frontend/main.js | 6 +- frontend/src/config.js | 11 +-- 7 files changed, 225 insertions(+), 11 deletions(-) create mode 100644 .ci-test.txt create mode 100644 .github/workflows/deploy-dev.yml create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 .github/workflows/pr-ci.yml create mode 100644 .github/workflows/reusable-deploy.yml diff --git a/.ci-test.txt b/.ci-test.txt new file mode 100644 index 00000000..f065b702 --- /dev/null +++ b/.ci-test.txt @@ -0,0 +1 @@ +ci test Wed 1 Oct 2025 23:14:23 AEST diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 00000000..3c66a3fa --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,29 @@ +name: Build & Deploy (development) + +on: + push: + branches: [ development ] + +permissions: + contents: read + deployments: write + +jobs: + product: + uses: ./.github/workflows/reusable-deploy.yml + with: + service_name: product-service + docker_context: example-1/backend/product_Service + image_name: product-service + environment: dev + namespace: dev + + frontend: + uses: ./.github/workflows/reusable-deploy.yml + needs: product + with: + service_name: frontend + docker_context: example-1/frontend + image_name: frontend + environment: dev + namespace: dev diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 00000000..480faaa0 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,29 @@ +name: Release to Production (main) + +on: + push: + branches: [ main ] + +permissions: + contents: read + deployments: write + +jobs: + product: + uses: ./.github/workflows/reusable-deploy.yml + with: + service_name: product-service + docker_context: example-1/backend/product_Service + image_name: product-service + environment: prod + namespace: prod + + frontend: + uses: ./.github/workflows/reusable-deploy.yml + needs: product + with: + service_name: frontend + docker_context: example-1/frontend + image_name: frontend + environment: prod + namespace: prod diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml new file mode 100644 index 00000000..a3f12f71 --- /dev/null +++ b/.github/workflows/pr-ci.yml @@ -0,0 +1,33 @@ +name: PR CI + +on: + pull_request: + branches: [ development, main ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + concurrency: + group: pr-ci-${{ github.event.pull_request.number }} + cancel-in-progress: true + strategy: + matrix: + include: + - service_name: product-service + docker_context: example-1/backend/product_Service + - service_name: frontend + docker_context: example-1/frontend + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build images (no push) + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.docker_context }} + push: false + tags: temp/${{ matrix.service_name }}:test + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/reusable-deploy.yml b/.github/workflows/reusable-deploy.yml new file mode 100644 index 00000000..65c260a0 --- /dev/null +++ b/.github/workflows/reusable-deploy.yml @@ -0,0 +1,127 @@ +name: Reusable Build & Deploy + +on: + workflow_call: + inputs: + service_name: { required: true, type: string } + docker_context: { required: true, type: string } + dockerfile: { required: false, type: string, default: Dockerfile } + image_name: { required: true, type: string } + environment: { required: true, type: string } # dev or prod + namespace: { required: true, type: string } + deploy: { required: false, type: boolean, default: true } + +permissions: + contents: read + deployments: write + +env: + ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }} + AKS_RESOURCE_GROUP: ${{ secrets.AKS_RG }} + AKS_CLUSTER_NAME: ${{ secrets.AKS_CLUSTER_NAME }} + +jobs: + build: + name: Build & Push ${{ inputs.service_name }} + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + concurrency: + group: deploy-${{ inputs.environment }}-${{ inputs.service_name }} + cancel-in-progress: true + outputs: + image_tag: ${{ steps.meta.outputs.tag }} + + steps: + - uses: actions/checkout@v4 + + - name: Azure login + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Compute image tag + id: meta + run: echo "tag=${GITHUB_SHA}-${GITHUB_RUN_NUMBER}" >> $GITHUB_OUTPUT + + - name: ACR login + shell: bash + run: | + ACR_NAME="${ACR_LOGIN_SERVER%%.*}" + echo "Logging into $ACR_NAME" + az acr login --name "$ACR_NAME" + + - name: Build & push image (with cache) + uses: docker/build-push-action@v6 + with: + context: ${{ inputs.docker_context }} + file: ${{ inputs.docker_context }}/${{ inputs.dockerfile }} + push: true + tags: | + ${{ env.ACR_LOGIN_SERVER }}/${{ inputs.image_name }}:${{ steps.meta.outputs.tag }} + ${{ env.ACR_LOGIN_SERVER }}/${{ inputs.image_name }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + + deploy: + name: Deploy ${{ inputs.service_name }} to ${{ inputs.environment }} + needs: build + if: ${{ inputs.deploy }} + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + steps: + - name: Azure login + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: AKS set context + uses: Azure/aks-set-context@v4 + with: + resource-group: ${{ env.AKS_RESOURCE_GROUP }} + cluster-name: ${{ env.AKS_CLUSTER_NAME }} + + - name: Ensure namespace exists + run: kubectl create namespace "${{ inputs.namespace }}" || true + + - name: Apply/Update deployment + shell: bash + run: | + set -e + DEPLOY="${{ inputs.service_name }}" + IMAGE="${{ env.ACR_LOGIN_SERVER }}/${{ inputs.image_name }}:${{ needs.build.outputs.image_tag }}" + if kubectl -n "${{ inputs.namespace }}" get deploy "$DEPLOY" >/dev/null 2>&1; then + kubectl -n "${{ inputs.namespace }}" set image "deployment/$DEPLOY" "$DEPLOY=$IMAGE" --record + else + cat < { // API endpoints for the Product and Order services. // These ports (30000 for Product, 30001 for Order) are mapped // from the Docker containers to the host machine in docker-compose.yml for Example 2. - const PRODUCT_API_BASE_URL = '_PRODUCT_API_URL_'; - const ORDER_API_BASE_URL = '_ORDER_API_URL_'; + // frontend/src/config.js + const PRODUCT_API_BASE = "http://4.237.211.110/api"; + const ORDER_API_BASE = "http://4.254.28.11/api"; + // Product Service is named 'product-service-w04e2' and exposes port 8000 internally. //const PRODUCT_API_BASE_URL = 'http://product-service-w04e2:8000'; diff --git a/frontend/src/config.js b/frontend/src/config.js index 1bdb7653..38981fe5 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -1,9 +1,2 @@ -- name: Write backend IPs into config.js - run: | - mkdir -p frontend/src - cat > frontend/src/config.js <<'EOF' -export const PRODUCT_API_BASE = "http://4.237.211.110"; -export const ORDER_API_BASE = "http://4.254.28.11"; -EOF - echo "==== config.js ====" - cat frontend/src/config.js +export const PRODUCT_API_BASE = "http://4.237.211.110/api"; +export const ORDER_API_BASE = "http://4.254.28.11/api"; From 88ef4283cb498ee4e80629eb827a234b21ce3af2 Mon Sep 17 00:00:00 2001 From: himanshudhaka17 Date: Wed, 1 Oct 2025 23:20:43 +1000 Subject: [PATCH 25/25] test: trigger PR CI --- .ci-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci-test.txt b/.ci-test.txt index f065b702..3ad72172 100644 --- a/.ci-test.txt +++ b/.ci-test.txt @@ -1 +1 @@ -ci test Wed 1 Oct 2025 23:14:23 AEST +ci test Wed 1 Oct 2025 23:20:42 AEST