From 22a6b2667f032ff1529894436d956879ca2e300b Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Wed, 9 Sep 2026 21:08:35 +1000 Subject: [PATCH 01/12] Configure Week 08 infrastructure and CI pipeline --- .github/workflows/01-ci.yml | 26 ++++++++++++ .gitignore | 8 +++- .terraform.lock.hcl | 22 ++++++++++ container_registry.tf | 15 +++++++ kubernetes_serivce.tf | 34 +++++++++++++++ output.tf | 57 +++++++++++++++++++++++++ resource_group.tf | 11 +++++ storage_account.tf | 30 ++++++++++++++ variables.tf | 83 +++++++++++++++++++++++++++++++++++++ versions.tf | 14 +++++++ 10 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 .terraform.lock.hcl create mode 100644 container_registry.tf create mode 100644 kubernetes_serivce.tf create mode 100644 output.tf create mode 100644 resource_group.tf create mode 100644 storage_account.tf create mode 100644 variables.tf create mode 100644 versions.tf diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 8206db43..f0ea7712 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -101,6 +101,31 @@ jobs: run: | pytest -v + # ========================================================= + # Frontend Tests + # ========================================================= + frontend-test: + name: Test frontend + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + working-directory: frontend + run: | + npm install + + - name: Run frontend tests + working-directory: frontend + run: | + npm run test:run # ========================================================= # Build and Push Docker Images @@ -112,6 +137,7 @@ jobs: # All backend tests must pass before this job starts needs: - backend-test + - frontend-test # Build and push when code is pushed to main or manually triggered if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' diff --git a/.gitignore b/.gitignore index 4299cd1a..3cb94e6b 100644 --- a/.gitignore +++ b/.gitignore @@ -196,4 +196,10 @@ cython_debug/ .cursorignore .cursorindexingignore -node_modules/ \ No newline at end of file +node_modules/ +# Terraform +.terraform/ +*.tfstate +*.tfstate.* +*.tfvars +*.tfvars.json diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 00000000..1c302601 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "4.81.0" + constraints = "~> 4.0" + hashes = [ + "h1:XhToZua4gtih1Kv8RdStcfND83G4Tmb6GZFT4jEUhDU=", + "zh:0732e7b74264ddfa2b90ba69d01c283d3cbae9f72ed3e506c6ac92529fed7fd3", + "zh:12afb524e232fe4e3d6161927724af5dfa4831d71edd9c174917ca9b7377bfae", + "zh:169d619ae202c4145e02fb706fb7c3679445ab3e3ff722edbf89597517a8c92e", + "zh:6beb95a3ef2f2d9c76abaa48e5450e90686a3fb6a47f1cb0ff7c5e94b6960151", + "zh:705e075fb5ffc4bf66fd7cbabf1a65007a41621e80030a2c158a4c83b6046216", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79a8d17fefe647040fcb9ee8821a4f09f395427c4fd49493489b9a93a9a1038e", + "zh:8cc3f900b3774c0ae37ae42365c4579a199cf9e5edf88e476fdf5ab1048f84ea", + "zh:dec373b9390fa95e257291acd018ed65a7d512b428645d35e22cdbe8b245a08b", + "zh:e60f1e9fb45df6defade2855ed6e68547409ea75d30655c556adb0c08579749b", + "zh:f901d12ec82f3f8b5880a27b5cbcd7bd0d97e60c9367a2d7ed82fdd1157b39ff", + "zh:facf68ea5bf0f2b8ba720e7fba5f86492e1d4c591100460bb91c3f79f391f4b6", + ] +} diff --git a/container_registry.tf b/container_registry.tf new file mode 100644 index 00000000..dc32f38a --- /dev/null +++ b/container_registry.tf @@ -0,0 +1,15 @@ +resource "azurerm_container_registry" "acr" { + name = var.acr_name + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + + sku = "Basic" + admin_enabled = true + + tags = merge( + var.tags, + { + Environment = var.environment + } + ) +} \ No newline at end of file diff --git a/kubernetes_serivce.tf b/kubernetes_serivce.tf new file mode 100644 index 00000000..bee30484 --- /dev/null +++ b/kubernetes_serivce.tf @@ -0,0 +1,34 @@ +resource "azurerm_kubernetes_cluster" "aks" { + name = var.aks_cluster_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + dns_prefix = var.aks_dns_prefix + kubernetes_version = var.kubernetes_version + + default_node_pool { + name = "default" + node_count = var.aks_node_count + vm_size = var.aks_node_vm_size + } + + identity { + type = "SystemAssigned" + } + + tags = merge( + var.tags, + { + Environment = var.environment + } + ) +} + +# +# Grant AKS permission to pull images from your ACR +# +resource "azurerm_role_assignment" "acr_pull" { + principal_id = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id + role_definition_name = "AcrPull" + scope = azurerm_container_registry.acr.id + skip_service_principal_aad_check = true +} \ No newline at end of file diff --git a/output.tf b/output.tf new file mode 100644 index 00000000..36e88828 --- /dev/null +++ b/output.tf @@ -0,0 +1,57 @@ +output "resource_group_name" { + description = "Name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "acr_name" { + description = "Name of the Azure Container Registry" + value = azurerm_container_registry.acr.name +} + +output "acr_login_server" { + description = "Login server of the Azure Container Registry" + value = azurerm_container_registry.acr.login_server +} + +output "storage_account_name" { + description = "Name of the Azure Storage Account" + value = azurerm_storage_account.storage_account.name +} + +output "storage_connection_string" { + description = "Connection string used by the application to access Blob Storage" + value = azurerm_storage_account.storage_account.primary_connection_string + sensitive = true +} + +output "student_profile_container" { + description = "Student profile photo Blob container" + value = azurerm_storage_container.student_profile_photo.name +} + +output "lecturer_profile_container" { + description = "Lecturer profile photo Blob container" + value = azurerm_storage_container.lecturer_profile_photo.name +} + +output "aks_cluster_name" { + description = "Name of the AKS cluster" + value = azurerm_kubernetes_cluster.aks.name +} + +output "aks_get_credentials_command" { + description = "Azure CLI command used to configure kubectl" + value = join(" ", [ + "az aks get-credentials", + "--resource-group", + azurerm_resource_group.rg.name, + "--name", + azurerm_kubernetes_cluster.aks.name, + "--overwrite-existing" + ]) +} + +output "acr_login_command" { + description = "Azure CLI command used to log in to ACR" + value = "az acr login --name ${azurerm_container_registry.acr.name}" +} \ No newline at end of file diff --git a/resource_group.tf b/resource_group.tf new file mode 100644 index 00000000..ff3255ec --- /dev/null +++ b/resource_group.tf @@ -0,0 +1,11 @@ +resource "azurerm_resource_group" "rg" { + name = var.resource_group_name + location = var.location + + tags = merge( + var.tags, + { + Environment = var.environment + } + ) +} \ No newline at end of file diff --git a/storage_account.tf b/storage_account.tf new file mode 100644 index 00000000..ce33235f --- /dev/null +++ b/storage_account.tf @@ -0,0 +1,30 @@ +resource "azurerm_storage_account" "storage_account" { + name = var.storage_account_name + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + + account_tier = "Standard" + account_replication_type = "LRS" + + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + + tags = merge( + var.tags, + { + Environment = var.environment + } + ) +} + +resource "azurerm_storage_container" "student_profile_photo" { + name = "student-profile-photo" + storage_account_id = azurerm_storage_account.storage_account.id + container_access_type = "private" +} + +resource "azurerm_storage_container" "lecturer_profile_photo" { + name = "lecturer-profile-photo" + storage_account_id = azurerm_storage_account.storage_account.id + container_access_type = "private" +} \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 00000000..c6e53382 --- /dev/null +++ b/variables.tf @@ -0,0 +1,83 @@ +variable "location" { + description = "Azure region where the resources will be created" + type = string + default = "Australia East" +} + +variable "resource_group_name" { + description = "Name of the Azure Resource Group" + type = string +} + +variable "acr_name" { + description = "Globally unique name of the Azure Container Registry" + type = string + + validation { + condition = can(regex("^[a-zA-Z0-9]+$", var.acr_name)) + error_message = "The ACR name must contain only alphanumeric characters." + } +} + +variable "storage_account_name" { + description = "Globally unique name of the Azure Storage Account" + type = string + + validation { + condition = ( + length(var.storage_account_name) >= 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 + default = 2 + + 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" { + default = "1.36.1" +} + +variable "tags" { + description = "Tags applied to Azure resources" + type = map(string) + + default = { + Project = "KoalaTech Course Platform" + ManagedBy = "Terraform" + Practical = "Week06" + } +} \ No newline at end of file diff --git a/versions.tf b/versions.tf new file mode 100644 index 00000000..47437408 --- /dev/null +++ b/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.7.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} + +provider "azurerm" { + features {} +} \ No newline at end of file From b316919736ae853d39c85751f174a2fa1196d090 Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Wed, 9 Sep 2026 21:16:53 +1000 Subject: [PATCH 02/12] Fix frontend CI environment variables --- .github/workflows/01-ci.yml | 57 +++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index f0ea7712..825b4925 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -101,31 +101,38 @@ jobs: run: | pytest -v - # ========================================================= - # Frontend Tests - # ========================================================= - frontend-test: - name: Test frontend - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Install dependencies - working-directory: frontend - run: | - npm install - - - name: Run frontend tests - working-directory: frontend - run: | - npm run test:run + # ========================================================= + # Frontend Tests + # ========================================================= + frontend-test: + name: Test frontend + runs-on: ubuntu-latest + + env: + VITE_USER_SERVICE_URL: http://localhost:8001 + VITE_STUDENT_SERVICE_URL: http://localhost:8002 + VITE_LECTURER_SERVICE_URL: http://localhost:8003 + VITE_COURSE_SERVICE_URL: http://localhost:8004 + VITE_ENROLLMENT_SERVICE_URL: http://localhost:8005 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + working-directory: frontend + run: | + npm install + + - name: Run frontend tests + working-directory: frontend + run: | + npm run test:run # ========================================================= # Build and Push Docker Images From 2e54ce6c521cf2a76179b85aa53c9b563e39f61b Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Wed, 9 Sep 2026 21:21:38 +1000 Subject: [PATCH 03/12] Fix frontend CI job indentation --- .github/workflows/01-ci.yml | 64 ++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 825b4925..2855d444 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -101,38 +101,38 @@ jobs: run: | pytest -v - # ========================================================= - # Frontend Tests - # ========================================================= - frontend-test: - name: Test frontend - runs-on: ubuntu-latest - - env: - VITE_USER_SERVICE_URL: http://localhost:8001 - VITE_STUDENT_SERVICE_URL: http://localhost:8002 - VITE_LECTURER_SERVICE_URL: http://localhost:8003 - VITE_COURSE_SERVICE_URL: http://localhost:8004 - VITE_ENROLLMENT_SERVICE_URL: http://localhost:8005 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Install dependencies - working-directory: frontend - run: | - npm install - - - name: Run frontend tests - working-directory: frontend - run: | - npm run test:run + # ========================================================= + # Frontend Tests + # ========================================================= + frontend-test: + name: Test frontend + runs-on: ubuntu-latest + + env: + VITE_USER_SERVICE_URL: http://localhost:8001 + VITE_STUDENT_SERVICE_URL: http://localhost:8002 + VITE_LECTURER_SERVICE_URL: http://localhost:8003 + VITE_COURSE_SERVICE_URL: http://localhost:8004 + VITE_ENROLLMENT_SERVICE_URL: http://localhost:8005 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + working-directory: frontend + run: | + npm install + + - name: Run frontend tests + working-directory: frontend + run: | + npm run test:run # ========================================================= # Build and Push Docker Images From eb4ae0ee75e07828ab1a71d9d935ceba32fab91e Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Thu, 17 Sep 2026 15:23:44 +1000 Subject: [PATCH 04/12] Implement continuous deployment to production --- .github/workflows/04-deploy-production.yml | 30 ++++++++++++---------- frontend/src/components/Header.jsx | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/workflows/04-deploy-production.yml b/.github/workflows/04-deploy-production.yml index 969d646b..26507667 100644 --- a/.github/workflows/04-deploy-production.yml +++ b/.github/workflows/04-deploy-production.yml @@ -1,24 +1,28 @@ name: 04 - Deploy to Production on: - workflow_dispatch: - inputs: - image_tag: - description: "Tested image SHA to deploy" - required: true - type: string + workflow_run: + workflows: + - "03 - Test Staging" + types: + - completed + branches: + - main jobs: deploy-production: name: Deploy to Production + if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest environment: name: production steps: - - name: Checkout repository + - name: Checkout tested commit uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} - name: Login to Azure uses: azure/login@v3 @@ -69,37 +73,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:${{ github.event.workflow_run.head_sha }} \ -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:${{ github.event.workflow_run.head_sha }} \ -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:${{ github.event.workflow_run.head_sha }} \ -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:${{ github.event.workflow_run.head_sha }} \ -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:${{ github.event.workflow_run.head_sha }} \ -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:${{ github.event.workflow_run.head_sha }} \ -n production - name: Wait for frontend rollout diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index d1a76071..f08d1dcb 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -43,7 +43,7 @@ const Header = () => { fontWeight: 600, }} > - KoalaTech University + KoalaTech University — Continuous Deployment Date: Sat, 26 Sep 2026 02:38:46 +1000 Subject: [PATCH 05/12] Add Terraform validation to CI pipeline --- .github/workflows/00-terraform.yml | 59 +++++++++++++++++++ container_registry.tf | 22 +++---- kubernetes_serivce.tf | 46 +++++++-------- resource_group.tf | 16 +++--- storage_account.tf | 38 ++++++------ variables.tf | 92 +++++++++++++++--------------- versions.tf | 14 ++--- 7 files changed, 173 insertions(+), 114 deletions(-) create mode 100644 .github/workflows/00-terraform.yml diff --git a/.github/workflows/00-terraform.yml b/.github/workflows/00-terraform.yml new file mode 100644 index 00000000..2084083a --- /dev/null +++ b/.github/workflows/00-terraform.yml @@ -0,0 +1,59 @@ +name: 00 - Terraform + +on: + push: + branches: + - main + - feature/continuous-deployment + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + terraform: + name: Terraform Validate and Plan + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Verify Azure account + run: | + az account show + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + + - name: Terraform Format Check + run: | + terraform fmt -check -recursive + + - name: Terraform Init + run: | + terraform init -backend=false + + - name: Terraform Validate + run: | + terraform validate + + - name: Terraform Plan + run: | + terraform plan \ + -refresh=false \ + -input=false \ + -var="location=Australia East" \ + -var="resource_group_name=koalatech-week08-rg" \ + -var="acr_name=koalatechweek09acr" \ + -var="storage_account_name=koalatechweek09storage" \ + -var="aks_cluster_name=koalatech-week08-aks" \ + -var="aks_dns_prefix=koalatech" \ + -var="aks_node_count=3" \ + -var="aks_node_vm_size=Standard_D2s_v3" \ + -var="environment=development" diff --git a/container_registry.tf b/container_registry.tf index dc32f38a..1a5f350f 100644 --- a/container_registry.tf +++ b/container_registry.tf @@ -1,15 +1,15 @@ resource "azurerm_container_registry" "acr" { - name = var.acr_name - resource_group_name = azurerm_resource_group.rg.name - location = azurerm_resource_group.rg.location + name = var.acr_name + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location - sku = "Basic" - admin_enabled = true + sku = "Basic" + admin_enabled = true - tags = merge( - var.tags, - { - Environment = var.environment - } - ) + tags = merge( + var.tags, + { + Environment = var.environment + } + ) } \ No newline at end of file diff --git a/kubernetes_serivce.tf b/kubernetes_serivce.tf index bee30484..ebfd7692 100644 --- a/kubernetes_serivce.tf +++ b/kubernetes_serivce.tf @@ -1,34 +1,34 @@ resource "azurerm_kubernetes_cluster" "aks" { - name = var.aks_cluster_name - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - dns_prefix = var.aks_dns_prefix - kubernetes_version = var.kubernetes_version + name = var.aks_cluster_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + dns_prefix = var.aks_dns_prefix + kubernetes_version = var.kubernetes_version - default_node_pool { - name = "default" - node_count = var.aks_node_count - vm_size = var.aks_node_vm_size - } + default_node_pool { + name = "default" + node_count = var.aks_node_count + vm_size = var.aks_node_vm_size + } - identity { - type = "SystemAssigned" - } + identity { + type = "SystemAssigned" + } - tags = merge( - var.tags, - { - Environment = var.environment - } - ) + tags = merge( + var.tags, + { + Environment = var.environment + } + ) } # # Grant AKS permission to pull images from your ACR # resource "azurerm_role_assignment" "acr_pull" { - principal_id = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id - role_definition_name = "AcrPull" - scope = azurerm_container_registry.acr.id - skip_service_principal_aad_check = true + principal_id = azurerm_kubernetes_cluster.aks.kubelet_identity[0].object_id + role_definition_name = "AcrPull" + scope = azurerm_container_registry.acr.id + skip_service_principal_aad_check = true } \ No newline at end of file diff --git a/resource_group.tf b/resource_group.tf index ff3255ec..0fbe75d3 100644 --- a/resource_group.tf +++ b/resource_group.tf @@ -1,11 +1,11 @@ resource "azurerm_resource_group" "rg" { - name = var.resource_group_name - location = var.location + name = var.resource_group_name + location = var.location - tags = merge( - var.tags, - { - Environment = var.environment - } - ) + tags = merge( + var.tags, + { + Environment = var.environment + } + ) } \ No newline at end of file diff --git a/storage_account.tf b/storage_account.tf index ce33235f..8b3a6b6f 100644 --- a/storage_account.tf +++ b/storage_account.tf @@ -1,30 +1,30 @@ resource "azurerm_storage_account" "storage_account" { - name = var.storage_account_name - resource_group_name = azurerm_resource_group.rg.name - location = azurerm_resource_group.rg.location + name = var.storage_account_name + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location - account_tier = "Standard" - account_replication_type = "LRS" + account_tier = "Standard" + account_replication_type = "LRS" - min_tls_version = "TLS1_2" - allow_nested_items_to_be_public = false + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false - tags = merge( - var.tags, - { - Environment = var.environment - } - ) + tags = merge( + var.tags, + { + Environment = var.environment + } + ) } resource "azurerm_storage_container" "student_profile_photo" { - name = "student-profile-photo" - storage_account_id = azurerm_storage_account.storage_account.id - container_access_type = "private" + name = "student-profile-photo" + storage_account_id = azurerm_storage_account.storage_account.id + container_access_type = "private" } resource "azurerm_storage_container" "lecturer_profile_photo" { - name = "lecturer-profile-photo" - storage_account_id = azurerm_storage_account.storage_account.id - container_access_type = "private" + name = "lecturer-profile-photo" + storage_account_id = azurerm_storage_account.storage_account.id + container_access_type = "private" } \ No newline at end of file diff --git a/variables.tf b/variables.tf index c6e53382..d9c95973 100644 --- a/variables.tf +++ b/variables.tf @@ -1,83 +1,83 @@ variable "location" { - description = "Azure region where the resources will be created" - type = string - default = "Australia East" + description = "Azure region where the resources will be created" + type = string + default = "Australia East" } variable "resource_group_name" { - description = "Name of the Azure Resource Group" - type = string + description = "Name of the Azure Resource Group" + type = string } variable "acr_name" { - description = "Globally unique name of the Azure Container Registry" - type = string + description = "Globally unique name of the Azure Container Registry" + type = string - validation { - condition = can(regex("^[a-zA-Z0-9]+$", var.acr_name)) - error_message = "The ACR name must contain only alphanumeric characters." - } + validation { + condition = can(regex("^[a-zA-Z0-9]+$", var.acr_name)) + error_message = "The ACR name must contain only alphanumeric characters." + } } variable "storage_account_name" { - description = "Globally unique name of the Azure Storage Account" - type = string + description = "Globally unique name of the Azure Storage Account" + type = string - validation { - condition = ( - length(var.storage_account_name) >= 3 && - length(var.storage_account_name) <= 24 && - can(regex("^[a-z0-9]+$", var.storage_account_name)) - ) + validation { + condition = ( + length(var.storage_account_name) >= 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." - } + 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 + description = "Name of the Azure Kubernetes Service cluster" + type = string } variable "aks_dns_prefix" { - description = "DNS prefix used by the AKS cluster" - type = string + 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 - default = 2 + description = "Number of nodes in the default AKS node pool" + type = number + default = 2 - validation { - condition = var.aks_node_count >= 1 - error_message = "The AKS node count must be at least 1." - } + 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" + 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" + description = "Environment name applied to resource tags" + type = string + default = "development" } variable "kubernetes_version" { - default = "1.36.1" + default = "1.36.1" } variable "tags" { - description = "Tags applied to Azure resources" - type = map(string) + description = "Tags applied to Azure resources" + type = map(string) - default = { - Project = "KoalaTech Course Platform" - ManagedBy = "Terraform" - Practical = "Week06" - } + default = { + Project = "KoalaTech Course Platform" + ManagedBy = "Terraform" + Practical = "Week06" + } } \ No newline at end of file diff --git a/versions.tf b/versions.tf index 47437408..205d1188 100644 --- a/versions.tf +++ b/versions.tf @@ -1,14 +1,14 @@ terraform { - required_version = ">= 1.7.0" + required_version = ">= 1.7.0" - required_providers { - azurerm = { - source = "hashicorp/azurerm" - version = "~> 4.0" - } + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" } + } } provider "azurerm" { - features {} + features {} } \ No newline at end of file From 70750417c8be80aa8a8ce6ac870862301722dac0 Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Sat, 26 Sep 2026 02:54:10 +1000 Subject: [PATCH 06/12] Fix vulnerable python-multipart dependency --- user-service/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user-service/requirements.txt b/user-service/requirements.txt index 02679ad8..49f680ce 100644 --- a/user-service/requirements.txt +++ b/user-service/requirements.txt @@ -5,7 +5,7 @@ psycopg2-binary==2.9.10 pydantic[email]==2.11.7 PyJWT pwdlib[argon2]==0.2.1 -python-multipart==0.0.20 +python-multipart==0.0.30 pytest==8.4.1 httpx==0.28.1 python-dotenv==1.0.1 \ No newline at end of file From 133edee3b448319f25d1d163a8fce5d50c75add4 Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Sat, 26 Sep 2026 03:04:40 +1000 Subject: [PATCH 07/12] Add Docker Scout security scan to CI --- .github/workflows/01-ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 2855d444..938dbf0a 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -193,6 +193,15 @@ jobs: -t ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} \ ./${{ matrix.service }} + - name: Docker Scout vulnerability scan + uses: docker/scout-action@v1 + with: + command: cves + image: ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} + only-severities: critical,high + exit-code: false + summary: true + - name: Push Docker image with commit SHA run: | docker push \ From 95a4cc969582b13ffb8291cc49337d36a04c9362 Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Sat, 26 Sep 2026 03:15:18 +1000 Subject: [PATCH 08/12] Run CI pipeline on feature branch --- .github/workflows/01-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 938dbf0a..130a2efb 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -5,6 +5,7 @@ on: push: branches: - main + - feature/continuous-deployment # Manual trigger for the workflow workflow_dispatch: From 490f3a068ec5ccf8d41ec7b66cd41e407ecab95d Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Sat, 26 Sep 2026 03:38:48 +1000 Subject: [PATCH 09/12] Authenticate Docker Scout in CI --- .github/workflows/01-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 130a2efb..abfa05e0 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -202,6 +202,8 @@ jobs: only-severities: critical,high exit-code: false summary: true + env: + DOCKER_SCOUT_HUB_TOKEN: ${{ secrets.DOCKER_SCOUT_TOKEN }} - name: Push Docker image with commit SHA run: | From e2f7b9001e0baf26a04711225338ec659b06e61b Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Sat, 26 Sep 2026 03:44:39 +1000 Subject: [PATCH 10/12] Fix Docker Scout authentication --- .github/workflows/01-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index abfa05e0..29fc022c 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -203,7 +203,8 @@ jobs: exit-code: false summary: true env: - DOCKER_SCOUT_HUB_TOKEN: ${{ secrets.DOCKER_SCOUT_TOKEN }} + DOCKER_SCOUT_HUB_USER: ${{ secrets.DOCKER_SCOUT_HUB_USER }} + DOCKER_SCOUT_HUB_PASSWORD: ${{ secrets.DOCKER_SCOUT_TOKEN }} - name: Push Docker image with commit SHA run: | From daa2875a4568b77d1906b479830977429d2f8d0a Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Sat, 26 Sep 2026 04:06:17 +1000 Subject: [PATCH 11/12] Run staging deployment for feature branch CI --- .github/workflows/02-deploy-staging.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/02-deploy-staging.yml b/.github/workflows/02-deploy-staging.yml index 006bb70d..589c4ffa 100644 --- a/.github/workflows/02-deploy-staging.yml +++ b/.github/workflows/02-deploy-staging.yml @@ -8,6 +8,7 @@ on: - completed branches: - main + - feature/continuous-deployment jobs: deploy-staging: From 8e4d72953240a85c5830ef516d704cc1ea19e8cc Mon Sep 17 00:00:00 2001 From: Hannah Rajput Date: Sat, 26 Sep 2026 04:15:20 +1000 Subject: [PATCH 12/12] Trigger CI pipeline