From 2a69842bfa15196f1e3a87b860951132a98de0c1 Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Fri, 11 Sep 2026 16:35:04 +1000 Subject: [PATCH 1/8] trigger CI --- terraform/.gitignore | 5 ++ terraform/container_registry.tf | 15 ++++++ terraform/kubernetes_manifest.tf | 15 ++++++ terraform/kubernetes_serivce.tf | 34 +++++++++++++ terraform/output.tf | 57 ++++++++++++++++++++++ terraform/resource_group.tf | 11 +++++ terraform/storage_account.tf | 30 ++++++++++++ terraform/variables.tf | 83 ++++++++++++++++++++++++++++++++ terraform/versions.tf | 18 +++++++ 9 files changed, 268 insertions(+) create mode 100644 terraform/.gitignore create mode 100644 terraform/container_registry.tf create mode 100644 terraform/kubernetes_manifest.tf create mode 100644 terraform/kubernetes_serivce.tf create mode 100644 terraform/output.tf create mode 100644 terraform/resource_group.tf create mode 100644 terraform/storage_account.tf create mode 100644 terraform/variables.tf create mode 100644 terraform/versions.tf diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 00000000..ef1394e4 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,5 @@ +.terraform/ +*.tfstate +*.tfstate.* +.terraform.lock.hcl +terraform.tfvars \ No newline at end of file diff --git a/terraform/container_registry.tf b/terraform/container_registry.tf new file mode 100644 index 00000000..dc32f38a --- /dev/null +++ b/terraform/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/terraform/kubernetes_manifest.tf b/terraform/kubernetes_manifest.tf new file mode 100644 index 00000000..3fd07c08 --- /dev/null +++ b/terraform/kubernetes_manifest.tf @@ -0,0 +1,15 @@ +locals { + k8s_dir = "${path.module}/../kubernetes" + + template_vars = { + acr_login_server = azurerm_container_registry.acr.login_server + storage_connection_string = azurerm_storage_account.storage_account.primary_connection_string + } +} + +resource "local_file" "k8s_manifests" { + for_each = fileset(local.k8s_dir, "*.tpl") + + filename = "${local.k8s_dir}/${trimsuffix(each.value, ".tpl")}" + content = templatefile("${local.k8s_dir}/${each.value}", local.template_vars) +} \ No newline at end of file diff --git a/terraform/kubernetes_serivce.tf b/terraform/kubernetes_serivce.tf new file mode 100644 index 00000000..bee30484 --- /dev/null +++ b/terraform/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/terraform/output.tf b/terraform/output.tf new file mode 100644 index 00000000..36e88828 --- /dev/null +++ b/terraform/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/terraform/resource_group.tf b/terraform/resource_group.tf new file mode 100644 index 00000000..ff3255ec --- /dev/null +++ b/terraform/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/terraform/storage_account.tf b/terraform/storage_account.tf new file mode 100644 index 00000000..ce33235f --- /dev/null +++ b/terraform/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/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 00000000..c6e53382 --- /dev/null +++ b/terraform/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/terraform/versions.tf b/terraform/versions.tf new file mode 100644 index 00000000..d70fa886 --- /dev/null +++ b/terraform/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.7.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + local = { + source = "hashicorp/local" + version = "~> 2.5" + } + } +} + +provider "azurerm" { + features {} +} From d58ecb83f2e4761f4d186ee4c488365e6d987c68 Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Fri, 11 Sep 2026 16:37:01 +1000 Subject: [PATCH 2/8] trigger CI From aca0f9e72cf5039933f5715edee41ab8ed93927b Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Fri, 11 Sep 2026 16:46:05 +1000 Subject: [PATCH 3/8] trigger CI --- .github/workflows/01-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 8206db43..b9b070b2 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -83,10 +83,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" @@ -142,7 +142,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Login to Azure uses: azure/login@v2 From d3cfde8687e3f990fc1a58b11129c1a962f7d1de Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Fri, 11 Sep 2026 17:00:47 +1000 Subject: [PATCH 4/8] trigger CI From edeb33b1c10aab910714323c0722e38b95ec0316 Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Fri, 11 Sep 2026 17:07:35 +1000 Subject: [PATCH 5/8] trigger CI From 8c4c53f96d89ecd490b162725b628609e2fa4fc2 Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Fri, 11 Sep 2026 17:15:13 +1000 Subject: [PATCH 6/8] trigger CI From 1b52fa5f117dccbbf62e10f4a5dc177bc1a1b493 Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Thu, 24 Sep 2026 14:55:12 +1000 Subject: [PATCH 7/8] 9.3C: enable Continuous Deployment --- .github/workflows/01-ci.yml | 4 ++++ .github/workflows/02-deploy-staging.yml | 3 ++- .github/workflows/04-deploy-production.yml | 28 ++++++++++++---------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index b9b070b2..37deb194 100644 --- a/.github/workflows/01-ci.yml +++ b/.github/workflows/01-ci.yml @@ -5,6 +5,10 @@ on: push: branches: - main + + pull_request: + branches: + - main # Manual trigger for the workflow workflow_dispatch: diff --git a/.github/workflows/02-deploy-staging.yml b/.github/workflows/02-deploy-staging.yml index 006bb70d..85ffe45b 100644 --- a/.github/workflows/02-deploy-staging.yml +++ b/.github/workflows/02-deploy-staging.yml @@ -15,7 +15,8 @@ jobs: runs-on: ubuntu-latest if: > - ${{ github.event.workflow_run.conclusion == 'success' }} + ${{ github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event != 'pull_request' }} environment: name: staging diff --git a/.github/workflows/04-deploy-production.yml b/.github/workflows/04-deploy-production.yml index 969d646b..92a6a96b 100644 --- a/.github/workflows/04-deploy-production.yml +++ b/.github/workflows/04-deploy-production.yml @@ -1,18 +1,20 @@ 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 runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + environment: name: production @@ -69,37 +71,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 From ede634c9844d1cd6805249a0be94b19acd956b0d Mon Sep 17 00:00:00 2001 From: Aaron Alijani Date: Thu, 24 Sep 2026 15:40:14 +1000 Subject: [PATCH 8/8] Update login page to v2 for CD demo- Change Background --- frontend/src/pages/Login.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 1132741c..d6692eb4 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -87,7 +87,7 @@ const Login = () => { return ( - + {/* background colour change */} { color="text.secondary" sx={{ mb: 3 }} > - Sign in to continue + Sign in to continue (v2 - released by Continuous Deployment) {/* text change */} {error && (