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
76 changes: 76 additions & 0 deletions .github/workflows/00-terraform.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: 00 - Terraform

on:
pull_request:
paths:
- "terraform/**"
push:
branches:
- main
paths:
- "terraform/**"
workflow_dispatch:

permissions:
contents: read

env:
TF_IN_AUTOMATION: "true"
TF_WORKING_DIR: terraform

jobs:
terraform:
name: Terraform validate, plan and apply
runs-on: ubuntu-latest

defaults:
run:
working-directory: terraform

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

- name: Login to Azure
uses: azure/login@v3
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

# The azurerm provider reads ARM_* variables; azure/login does not
# export them, so pull them out of the same service principal JSON.
- name: Export ARM credentials for Terraform
env:
CREDS: ${{ secrets.AZURE_CREDENTIALS }}
run: |
for key in clientId clientSecret tenantId subscriptionId; do
echo "::add-mask::$(echo "$CREDS" | jq -r .$key)"
done
echo "ARM_CLIENT_ID=$(echo "$CREDS" | jq -r .clientId)" >> "$GITHUB_ENV"
echo "ARM_CLIENT_SECRET=$(echo "$CREDS" | jq -r .clientSecret)" >> "$GITHUB_ENV"
echo "ARM_TENANT_ID=$(echo "$CREDS" | jq -r .tenantId)" >> "$GITHUB_ENV"
echo "ARM_SUBSCRIPTION_ID=$(echo "$CREDS" | jq -r .subscriptionId)" >> "$GITHUB_ENV"

- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~1.9"

- name: Terraform format check
run: terraform fmt -check -diff

- name: Terraform init
run: |
terraform init \
-backend-config="resource_group_name=${{ vars.TFSTATE_RESOURCE_GROUP }}" \
-backend-config="storage_account_name=${{ vars.TFSTATE_STORAGE_ACCOUNT }}"

- name: Terraform validate
run: terraform validate

- name: Terraform plan
run: terraform plan -input=false -out=tfplan

# Only changes merged to main are applied. Pull requests stop at plan.
- name: Terraform apply
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
run: terraform apply -input=false -auto-approve tfplan
30 changes: 30 additions & 0 deletions .github/workflows/01-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,36 @@ jobs:
-t ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} \
./${{ matrix.service }}

- name: Build Docker image
run: |
docker build \
--platform linux/amd64 \
-t ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} \
./${{ matrix.service }}

- name: Login to Docker Hub (for Docker Scout)
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PAT }}

- name: Docker Scout scan
uses: docker/scout-action@v1
with:
command: quickview,cves,recommendations
image: local://${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }}
only-severities: critical,high
sarif-file: scout-${{ matrix.service }}.sarif
# Report-only until the first findings are fixed, then flip to true
exit-code: false

- name: Upload Scout report
uses: actions/upload-artifact@v4
if: always()
with:
name: scout-${{ matrix.service }}
path: scout-${{ matrix.service }}.sarif

- name: Push Docker image with commit SHA
run: |
docker push \
Expand Down
30 changes: 18 additions & 12 deletions .github/workflows/04-deploy-production.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
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

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}

- name: Login to Azure
uses: azure/login@v3
Expand Down Expand Up @@ -69,37 +75,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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,10 @@ cython_debug/
.cursorignore
.cursorindexingignore

node_modules/
node_modules/

# terraform
terraform/.terraform/
terraform/*.tfstate
terraform/*.tfstate.*
terraform/tfplan
2 changes: 1 addition & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KoalaTech University</title>
<title>KoalaTech University - Live</title>
</head>

<body>
Expand Down
43 changes: 43 additions & 0 deletions terraform/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
}
)
}
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
}
)
}
Loading