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/.github/workflows/01-ci.yml b/.github/workflows/01-ci.yml index 8206db43..29fc022c 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: @@ -101,6 +102,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 # ========================================================= # Build and Push Docker Images @@ -112,6 +145,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' @@ -160,6 +194,18 @@ 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 + env: + 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: | docker push \ 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: 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/.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..1a5f350f --- /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/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 = 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..205d1188 --- /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