Skip to content
Closed
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
10 changes: 7 additions & 3 deletions .github/workflows/01-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
push:
branches:
- main

pull_request:
branches:
- main

# Manual trigger for the workflow
workflow_dispatch:
Expand Down Expand Up @@ -83,10 +87,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"

Expand Down Expand Up @@ -142,7 +146,7 @@ jobs:
steps:

- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Login to Azure
uses: azure/login@v2
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/02-deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 15 additions & 13 deletions .github/workflows/04-deploy-production.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const Login = () => {

return (
<Container maxWidth="sm">
<Card sx={{ mt: 10 }}>
<Card sx={{ mt: 10, bgcolor: "#f0a835" }}> {/* background colour change */}
<CardContent sx={{ p: 4 }}>
<Typography
variant="h4"
Expand All @@ -100,7 +100,7 @@ const Login = () => {
color="text.secondary"
sx={{ mb: 3 }}
>
Sign in to continue
Sign in to continue (v2 - released by Continuous Deployment) {/* text change */}
</Typography>

{error && (
Expand Down
5 changes: 5 additions & 0 deletions terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.terraform/
*.tfstate
*.tfstate.*
.terraform.lock.hcl
terraform.tfvars
15 changes: 15 additions & 0 deletions terraform/container_registry.tf
Original file line number Diff line number Diff line change
@@ -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
}
)
}
15 changes: 15 additions & 0 deletions terraform/kubernetes_manifest.tf
Original file line number Diff line number Diff line change
@@ -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)
}
34 changes: 34 additions & 0 deletions terraform/kubernetes_serivce.tf
Original file line number Diff line number Diff line change
@@ -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
}
57 changes: 57 additions & 0 deletions terraform/output.tf
Original file line number Diff line number Diff line change
@@ -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}"
}
11 changes: 11 additions & 0 deletions terraform/resource_group.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location

tags = merge(
var.tags,
{
Environment = var.environment
}
)
}
30 changes: 30 additions & 0 deletions terraform/storage_account.tf
Original file line number Diff line number Diff line change
@@ -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"
}
83 changes: 83 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading