diff --git a/.ci-test.txt b/.ci-test.txt new file mode 100644 index 00000000..3ad72172 --- /dev/null +++ b/.ci-test.txt @@ -0,0 +1 @@ +ci test Wed 1 Oct 2025 23:20:42 AEST diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml index 6035ed15..2d443679 100644 --- a/.github/workflows/backend-cd.yml +++ b/.github/workflows/backend-cd.yml @@ -3,99 +3,225 @@ 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: + # leave empty to use the 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 + 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 + 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 }} + + # 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" || true + + - name: Ensure namespace (optional) + if: env.NAMESPACE != '' + run: | + kubectl get ns "$NAMESPACE" || kubectl create ns "$NAMESPACE" + + # --- Ensure Postgres backends exist (creates or updates) --- + - name: Ensure product-db run: | - az aks get-credentials --resource-group ${{ github.event.inputs.aks_resource_group }} --name ${{ github.event.inputs.aks_cluster_name }} --overwrite-existing + 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: Attach ACR + - name: Ensure order-db 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 }} + 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 - - name: Deploy Backend Infrastructure (Namespace, ConfigMaps, Secrets, Databases) + # --- Ensure product-service + order-service exist (creates or updates) --- + - name: Ensure product-service deployment & service 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 ${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: | - echo "Deploying backend microservices..." - cd k8s/ - kubectl apply -f product-service.yaml - kubectl apply -f order-service.yaml - - - name: Wait for Backend LoadBalancer IPs + ns_flag="" + [ -n "$NAMESPACE" ] && ns_flag="-n $NAMESPACE" + PRODUCT_IMG="${ACR_LOGIN_SERVER}/product_service:${TAG}" + ORDER_IMG="${ACR_LOGIN_SERVER}/order_service:${TAG}" + + 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 + + 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 6 minutes) + id: capture_ips run: | - echo "Waiting for Product, Order LoadBalancer IPs to be assigned (up to 5 minutes)..." + ns_flag="" + [ -n "$NAMESPACE" ] && ns_flag="-n $NAMESPACE" + 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 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" + 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 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 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/frontend-cd.yml b/.github/workflows/frontend-cd.yml index 0a0879c8..11b84a92 100644 --- a/.github/workflows/frontend-cd.yml +++ b/.github/workflows/frontend-cd.yml @@ -1,93 +1,82 @@ # week08/.github/workflows/frontend-cd.yml - 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 + - 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 ${{ secrets.AZURE_CONTAINER_REGISTRY }} + # --- 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 - - name: Inject Backend IPs into Frontend main.js + # --- Build & push the frontend image to ACR --- + - name: Login to ACR + env: + ACR_FQDN: ${{ secrets.ACR_LOGIN_SERVER }} # e.g. sit722acr12345.azurecr.io 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_FQDN" | cut -d. -f1) + az acr login --name "$ACR_NAME" - # Build and Push Docker image for Frontend - - name: Build and Push Frontend Image + - name: Build & Push Frontend Image + env: + ACR_FQDN: ${{ secrets.ACR_LOGIN_SERVER }} + TAG: ${{ github.event.inputs.image_tag || 'latest' }} run: | - docker build -t ${{ secrets.AZURE_CONTAINER_REGISTRY }}/frontend:latest ./frontend/ - docker push ${{ secrets.AZURE_CONTAINER_REGISTRY }}/frontend:latest + docker build -t "${ACR_FQDN}/frontend:${TAG}" ./frontend + docker push "${ACR_FQDN}/frontend:${TAG}" - - name: Set Kubernetes context (get AKS credentials) + # --- Set AKS context & (re)apply k8s objects --- + - name: Set AKS context uses: azure/aks-set-context@v3 with: - resource-group: ${{ inputs.aks_resource_group }} - cluster-name: ${{ inputs.aks_cluster_name }} + resource-group: ${{ secrets.AKS_RG }} + cluster-name: ${{ secrets.AKS_CLUSTER }} - - name: Deploy Frontend to AKS + - name: Deploy/Update k8s resources 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 + kubectl ${NAMESPACE:+-n $NAMESPACE} apply -f k8s/frontend.yaml - - name: Logout from Azure (AKS deployment) - run: az logout + - name: Roll to latest image + env: + ACR_FQDN: ${{ secrets.ACR_LOGIN_SERVER }} + TAG: ${{ github.event.inputs.image_tag || 'latest' }} + run: | + 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 + echo + echo "Frontend IP:" + kubectl ${NAMESPACE:+-n $NAMESPACE} get svc frontend -o jsonpath='{.status.loadBalancer.ingress[0].ip}'; echo diff --git a/.github/workflows/frontend_ci.yml b/.github/workflows/frontend_ci.yml index 9f9e76d9..3b5ad1ed 100644 --- a/.github/workflows/frontend_ci.yml +++ b/.github/workflows/frontend_ci.yml @@ -3,24 +3,15 @@ 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 + ACR_LOGIN_SERVER: ${{ secrets.ACR_LOGIN_SERVER }} IMAGE_TAG: ${{ github.sha }}-${{ github.run_id }} jobs: @@ -28,26 +19,42 @@ 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 + + # 🔧 Always write the backend IPs into config.js (no heredoc, no sed) + - name: Write backend IPs into config.js + run: | + mkdir -p frontend/src + 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 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Login to ACR + run: | + ACR_NAME=$(echo "${{ env.ACR_LOGIN_SERVER }}" | cut -d. -f1) + 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: | + 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" + + - name: Logout from Azure + if: always() + run: az logout 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 new file mode 100644 index 00000000..38981fe5 --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1,2 @@ +export const PRODUCT_API_BASE = "http://4.237.211.110/api"; +export const ORDER_API_BASE = "http://4.254.28.11/api"; diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml index 1948536d..2bb75daf 100644 --- a/k8s/frontend.yaml +++ b/k8s/frontend.yaml @@ -4,36 +4,31 @@ 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: - - name: frontend-container - image: durgeshsamariya.azurecr.io/frontend:latest - imagePullPolicy: Always - ports: - - containerPort: 80 + - name: frontend + 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 - labels: - app: frontend + name: frontend + labels: { app: frontend } spec: - selector: - app: frontend + type: LoadBalancer + selector: { app: frontend } 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 +