diff --git a/.github/workflows/00-infra.yml b/.github/workflows/00-infra.yml new file mode 100644 index 00000000..3afe1696 --- /dev/null +++ b/.github/workflows/00-infra.yml @@ -0,0 +1,50 @@ +name: 00 - Infrastructure (Terraform) + +# Runs Terraform validation and planning in CI whenever the Terraform code +# changes, and can be triggered manually. This is the "Terraform integrated into +# the pipeline" evidence. `apply` is performed locally with elevated credentials +# (least privilege: the CI service principal is Contributor only). + +on: + push: + branches: [ main ] + paths: + - "terraform/**" + - ".github/workflows/00-infra.yml" + pull_request: + branches: [ main ] + paths: + - "terraform/**" + workflow_dispatch: + +jobs: + terraform: + name: Terraform validate & plan + runs-on: ubuntu-latest + defaults: + run: + working-directory: terraform + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + + - name: Terraform init (no backend) + run: terraform init -backend=false + + - name: Terraform format check + run: terraform fmt -check -recursive + continue-on-error: true + + - name: Terraform validate + run: terraform validate + + - name: Terraform plan + run: terraform plan -input=false \ No newline at end of file diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 8206db43..2144c1d2 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -5,7 +5,7 @@ on: push: branches: - main - + # Manual trigger for the workflow workflow_dispatch: @@ -103,7 +103,7 @@ jobs: # ========================================================= - # Build and Push Docker Images + # Build, Scan and Push Docker Images # ========================================================= build-and-push: name: Build and Push ${{ matrix.image }} @@ -160,7 +160,31 @@ jobs: -t ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} \ ./${{ matrix.service }} + # --------------------------------------------------------------- + # SECURITY SCANNING (Docker Scout) — runs AFTER build, BEFORE push + # so a vulnerable image is analysed before it enters the registry. + # Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repo secrets. + # --------------------------------------------------------------- + - name: Scan image with Docker Scout + uses: docker/scout-action@v1 + continue-on-error: true # report-only: never blocks the pipeline + with: + command: cves,recommendations + image: ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} + only-severities: critical,high + sarif-file: scout-${{ matrix.image }}.sarif + dockerhub-user: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN }} + # exit-code: true # uncomment to FAIL the build on critical/high CVEs + + - name: Upload Docker Scout SARIF (optional) + if: always() + uses: github/codeql-action/upload-sarif@v3 + continue-on-error: true + with: + sarif_file: scout-${{ matrix.image }}.sarif + - name: Push Docker image with commit SHA run: | docker push \ - ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} + ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/04-deploy-production.yml b/.github/workflows/04-deploy-production.yml index 969d646b..84a6c9d6 100644 --- a/.github/workflows/04-deploy-production.yml +++ b/.github/workflows/04-deploy-production.yml @@ -1,6 +1,16 @@ name: 04 - Deploy to Production on: + # Continuous Deployment: run automatically after staging tests complete + workflow_run: + workflows: + - "03 - Test Staging" + types: + - completed + branches: + - main + + # Manual trigger kept for redeploying or rolling back a specific version workflow_dispatch: inputs: image_tag: @@ -8,15 +18,30 @@ on: required: true type: string +# Prevent two production deployments from running at the same time +concurrency: + group: production + cancel-in-progress: false + jobs: deploy-production: name: Deploy to Production runs-on: ubuntu-latest + # Only deploy if the staging tests passed, or if run manually + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' + environment: name: production + # Use the SHA tested in staging, or the manual input if run by hand + env: + IMAGE_TAG: ${{ inputs.image_tag || github.event.workflow_run.head_sha }} + steps: + - name: Show image tag being deployed + run: echo "Deploying image tag ${{ env.IMAGE_TAG }} to production" + - name: Checkout repository uses: actions/checkout@v4 @@ -69,37 +94,37 @@ jobs: - name: Update frontend image run: | kubectl set image deployment/frontend \ - frontend=${{ vars.ACR_LOGIN_SERVER }}/koalatech-frontend:${{ inputs.image_tag }} \ + frontend=${{ vars.ACR_LOGIN_SERVER }}/koalatech-frontend:${{ env.IMAGE_TAG }} \ -n production - name: Update user-service image run: | kubectl set image deployment/user-service \ - user-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-user-service:${{ inputs.image_tag }} \ + user-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-user-service:${{ env.IMAGE_TAG }} \ -n production - name: Update student-service image run: | kubectl set image deployment/student-service \ - student-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-student-service:${{ inputs.image_tag }} \ + student-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-student-service:${{ env.IMAGE_TAG }} \ -n production - name: Update lecturer-service image run: | kubectl set image deployment/lecturer-service \ - lecturer-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-lecturer-service:${{ inputs.image_tag }} \ + lecturer-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-lecturer-service:${{ env.IMAGE_TAG }} \ -n production - name: Update course-service image run: | kubectl set image deployment/course-service \ - course-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-course-service:${{ inputs.image_tag }} \ + course-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-course-service:${{ env.IMAGE_TAG }} \ -n production - name: Update enrollment-service image run: | kubectl set image deployment/enrollment-service \ - enrollment-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-enrollment-service:${{ inputs.image_tag }} \ + enrollment-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-enrollment-service:${{ env.IMAGE_TAG }} \ -n production - name: Wait for frontend rollout @@ -142,4 +167,46 @@ jobs: run: | kubectl get pods -n production kubectl get services -n production - kubectl get pvc -n production \ No newline at end of file + kubectl get pvc -n production + + # ========================================================= + # MONITORING & OBSERVABILITY (Prometheus + Grafana) + # Runs after a successful production deployment. + # ========================================================= + monitoring: + name: Deploy and validate monitoring + runs-on: ubuntu-latest + needs: deploy-production + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Get AKS credentials + run: | + az aks get-credentials \ + --resource-group ${{ vars.AKS_RESOURCE_GROUP }} \ + --name ${{ vars.AKS_CLUSTER_NAME }} \ + --overwrite-existing + + - name: Install / upgrade kube-prometheus-stack + run: | + helm repo add prometheus-community https://prometheus-community.github.io/helm-charts + helm repo update + helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \ + --namespace monitoring --create-namespace \ + -f monitoring/values-monitoring.yaml \ + --wait --timeout 10m + + - name: Apply application ServiceMonitor + run: kubectl apply -f monitoring/servicemonitor-user-service.yaml + + - name: Validate monitoring is running + run: | + kubectl get pods -n monitoring + kubectl get servicemonitors -A \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 1132741c..1ebf13a5 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -103,6 +103,14 @@ const Login = () => { Sign in to continue + + Release 2.0 – deployed automatically via Continuous Deployment + + {error && ( = 3 && + length(var.storage_account_name) <= 24 && + can(regex("^[a-z0-9]+$", var.storage_account_name)) + ) + + error_message = "The storage account name must contain 3–24 lowercase letters and numbers." + } +} + +variable "aks_cluster_name" { + description = "Name of the Azure Kubernetes Service cluster" + type = string +} + +variable "aks_dns_prefix" { + description = "DNS prefix used by the AKS cluster" + type = string +} + +variable "aks_node_count" { + description = "Number of nodes in the default AKS node pool" + type = number + + # Week 08 requires 3 nodes: staging and production both run + # persistent PostgreSQL workloads on the same cluster. + default = 3 + + validation { + condition = var.aks_node_count >= 1 + error_message = "The AKS node count must be at least 1." + } +} + +variable "aks_node_vm_size" { + description = "Virtual machine size used by the AKS nodes" + type = string + default = "Standard_D2s_v3" +} + +variable "environment" { + description = "Environment name applied to resource tags" + type = string + default = "development" +} + +variable "kubernetes_version" { + description = "AKS Kubernetes version. Leave null to let Azure choose the default for the region. Check available versions with: az aks get-versions --location australiaeast --output table" + type = string + default = null +} + +variable "tags" { + description = "Tags applied to Azure resources" + type = map(string) + + default = { + Project = "KoalaTech Course Platform" + ManagedBy = "Terraform" + Practical = "Week08" + } +} +variable "subscription_id" { + description = "Azure subscription ID" + type = string +} \ No newline at end of file diff --git a/terraform/versions.tf b/terraform/versions.tf new file mode 100644 index 00000000..85cb5a9a --- /dev/null +++ b/terraform/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +provider "azurerm" { + features {} + subscription_id = var.subscription_id +} \ No newline at end of file