Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/00-infra.yml
Original file line number Diff line number Diff line change
@@ -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
30 changes: 27 additions & 3 deletions .github/workflows/01-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
push:
branches:
- main

# Manual trigger for the workflow
workflow_dispatch:

Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 }}
81 changes: 74 additions & 7 deletions .github/workflows/04-deploy-production.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,47 @@
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:
description: "Tested image SHA to deploy"
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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -142,4 +167,46 @@ jobs:
run: |
kubectl get pods -n production
kubectl get services -n production
kubectl get pvc -n production
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
8 changes: 8 additions & 0 deletions frontend/src/pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ const Login = () => {
Sign in to continue
</Typography>

<Alert
severity="success"
variant="filled"
sx={{ mb: 3, fontWeight: "bold" }}
>
Release 2.0 – deployed automatically via Continuous Deployment
</Alert>

{error && (
<Alert
severity="error"
Expand Down
117 changes: 117 additions & 0 deletions monitoring/grafana-dashboard.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"annotations": { "list": [] },
"editable": true,
"graphTooltip": 0,
"schemaVersion": 39,
"style": "dark",
"tags": ["sit722", "week10"],
"templating": {
"list": [
{
"name": "datasource",
"type": "datasource",
"query": "prometheus",
"current": {},
"hide": 0,
"label": "Data source"
},
{
"name": "namespace",
"type": "query",
"datasource": "${datasource}",
"query": "label_values(kube_pod_info, namespace)",
"current": { "text": "default", "value": "default" },
"refresh": 2,
"includeAll": false,
"label": "Namespace"
}
]
},
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"title": "SIT722 Week08 App & Cluster",
"uid": "sit722-week10",
"version": 1,
"panels": [
{
"type": "timeseries",
"title": "CPU usage by pod (cores)",
"datasource": "${datasource}",
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 },
"fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] },
"targets": [
{
"datasource": "${datasource}",
"expr": "sum by (pod) (rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", container!=\"\", container!=\"POD\"}[5m]))",
"legendFormat": "{{pod}}"
}
]
},
{
"type": "timeseries",
"title": "Memory working set by pod (bytes)",
"datasource": "${datasource}",
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 0 },
"fieldConfig": { "defaults": { "unit": "bytes" }, "overrides": [] },
"targets": [
{
"datasource": "${datasource}",
"expr": "sum by (pod) (container_memory_working_set_bytes{namespace=\"$namespace\", container!=\"\", container!=\"POD\"})",
"legendFormat": "{{pod}}"
}
]
},
{
"type": "stat",
"title": "Running pods",
"datasource": "${datasource}",
"gridPos": { "h": 6, "w": 6, "x": 0, "y": 9 },
"fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] },
"targets": [
{
"datasource": "${datasource}",
"expr": "count(kube_pod_status_phase{namespace=\"$namespace\", phase=\"Running\"} == 1)",
"legendFormat": "running"
}
]
},
{
"type": "timeseries",
"title": "Pod restarts (increase, 1h)",
"datasource": "${datasource}",
"gridPos": { "h": 6, "w": 18, "x": 6, "y": 9 },
"fieldConfig": { "defaults": { "unit": "short" }, "overrides": [] },
"targets": [
{
"datasource": "${datasource}",
"expr": "sum by (pod) (increase(kube_pod_container_status_restarts_total{namespace=\"$namespace\"}[1h]))",
"legendFormat": "{{pod}}"
}
]
},
{
"type": "timeseries",
"title": "HTTP request rate (needs app /metrics — optional)",
"datasource": "${datasource}",
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 15 },
"fieldConfig": { "defaults": { "unit": "reqps" }, "overrides": [] },
"targets": [
{
"datasource": "${datasource}",
"expr": "sum by (handler) (rate(http_requests_total{namespace=\"$namespace\"}[5m]))",
"legendFormat": "{{handler}}"
}
]
}
]
}
Loading