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

on:
# Plan automatically whenever the infrastructure code changes
push:
branches:
- main
paths:
- "terraform/**"
- ".github/workflows/00-terraform.yml"

pull_request:
paths:
- "terraform/**"

# Manual run: "plan" (drift check) or "apply" (make the change)
workflow_dispatch:
inputs:
action:
description: "Terraform action to run"
required: true
type: choice
default: plan
options:
- plan
- apply

permissions:
contents: read

env:
TF_IN_AUTOMATION: "true"
TF_INPUT: "false"

jobs:

# =========================================================
# Format, validate and plan
# =========================================================
plan:
name: Terraform fmt / validate / plan
runs-on: ubuntu-latest

defaults:
run:
working-directory: terraform

env:
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
ARM_ACCESS_KEY: ${{ secrets.TF_STATE_ACCESS_KEY }}

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

- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

- name: Export service principal for Terraform
run: |
CLIENT_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .clientId)
CLIENT_SECRET=$(echo "$AZURE_CREDENTIALS" | jq -r .clientSecret)
TENANT_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .tenantId)
SUBSCRIPTION_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .subscriptionId)
echo "::add-mask::$CLIENT_SECRET"
echo "ARM_CLIENT_ID=$CLIENT_ID" >> "$GITHUB_ENV"
echo "ARM_CLIENT_SECRET=$CLIENT_SECRET" >> "$GITHUB_ENV"
echo "ARM_TENANT_ID=$TENANT_ID" >> "$GITHUB_ENV"
echo "ARM_SUBSCRIPTION_ID=$SUBSCRIPTION_ID" >> "$GITHUB_ENV"

- name: Terraform fmt
run: terraform fmt -check -recursive

- name: Terraform init
run: terraform init

- name: Terraform validate
run: terraform validate

- name: Terraform plan
id: plan
run: |
set +e
terraform plan -no-color -detailed-exitcode -out=tfplan > plan.txt
EXIT_CODE=$?
set -e
cat plan.txt
{
echo "## Terraform plan"
echo ""
if [ "$EXIT_CODE" -eq 0 ]; then
echo "No changes - live Azure infrastructure matches the code."
elif [ "$EXIT_CODE" -eq 2 ]; then
echo "Changes detected:"
fi
echo '```'
grep -E "Plan:|No changes|# azurerm|will be|must be" plan.txt || true
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
if [ "$EXIT_CODE" -eq 1 ]; then
exit 1
fi

# =========================================================
# Apply (manual only)
# =========================================================
apply:
name: Terraform apply
runs-on: ubuntu-latest
needs:
- plan

if: github.event_name == 'workflow_dispatch' && inputs.action == 'apply'

# Add a required reviewer on this environment for an approval gate
environment:
name: infrastructure

defaults:
run:
working-directory: terraform

env:
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
ARM_ACCESS_KEY: ${{ secrets.TF_STATE_ACCESS_KEY }}

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

- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

- name: Export service principal for Terraform
run: |
CLIENT_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .clientId)
CLIENT_SECRET=$(echo "$AZURE_CREDENTIALS" | jq -r .clientSecret)
TENANT_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .tenantId)
SUBSCRIPTION_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .subscriptionId)
echo "::add-mask::$CLIENT_SECRET"
echo "ARM_CLIENT_ID=$CLIENT_ID" >> "$GITHUB_ENV"
echo "ARM_CLIENT_SECRET=$CLIENT_SECRET" >> "$GITHUB_ENV"
echo "ARM_TENANT_ID=$TENANT_ID" >> "$GITHUB_ENV"
echo "ARM_SUBSCRIPTION_ID=$SUBSCRIPTION_ID" >> "$GITHUB_ENV"

- name: Terraform init
run: terraform init

- name: Terraform apply
run: terraform apply -auto-approve -no-color
54 changes: 54 additions & 0 deletions .github/workflows/00b-terraform-apply.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: 00b - Terraform Apply (manual)

on:
workflow_dispatch:

permissions:
contents: read

env:
TF_IN_AUTOMATION: "true"
TF_INPUT: "false"

jobs:
apply:
name: Terraform plan + apply
runs-on: ubuntu-latest

environment:
name: infrastructure

defaults:
run:
working-directory: terraform

env:
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}
ARM_ACCESS_KEY: ${{ secrets.TF_STATE_ACCESS_KEY }}

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

- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false

- name: Export service principal for Terraform
run: |
echo "ARM_CLIENT_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .clientId)" >> "$GITHUB_ENV"
SECRET=$(echo "$AZURE_CREDENTIALS" | jq -r .clientSecret)
echo "::add-mask::$SECRET"
echo "ARM_CLIENT_SECRET=$SECRET" >> "$GITHUB_ENV"
echo "ARM_TENANT_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .tenantId)" >> "$GITHUB_ENV"
echo "ARM_SUBSCRIPTION_ID=$(echo "$AZURE_CREDENTIALS" | jq -r .subscriptionId)" >> "$GITHUB_ENV"

- name: Terraform init
run: terraform init

- name: Terraform plan
run: terraform plan -no-color -out=tfplan

- name: Terraform apply
run: terraform apply -auto-approve -no-color tfplan
20 changes: 20 additions & 0 deletions .github/workflows/01-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,26 @@ jobs:
-t ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} \
./${{ matrix.service }}

# ---------------------------------------------------------
# DevSecOps: scan the freshly built image BEFORE it is pushed
# ---------------------------------------------------------
- name: Login to Docker Hub (required by Docker Scout)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Docker Scout - quickview, CVEs and recommendations
uses: docker/scout-action@v1
with:
command: quickview,cves,recommendations
image: local://${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }}
only-severities: critical,high
# Report-only for now; set to true to block the push on findings
exit-code: false
write-comment: false
summary: true

- name: Push Docker image with commit SHA
run: |
docker push \
Expand Down
39 changes: 29 additions & 10 deletions .github/workflows/04-deploy-production.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
name: 04 - Deploy to Production

on:
# Automatic: runs after "03 - Test Staging" completes on main
workflow_run:
workflows:
- "03 - Test Staging"
types:
- completed
branches:
- main

# Manual fallback / rollback
workflow_dispatch:
inputs:
image_tag:
description: "Tested image SHA to deploy"
required: true
description: "Image SHA to deploy (manual runs only)"
required: false
type: string

jobs:
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest

if: >
${{ github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success' }}

environment:
name: production

env:
IMAGE_TAG: ${{ inputs.image_tag || github.event.workflow_run.head_sha }}

steps:
- name: Checkout repository
- name: Checkout tested commit
uses: actions/checkout@v4
with:
ref: ${{ env.IMAGE_TAG }}

- name: Login to Azure
uses: azure/login@v3
Expand Down Expand Up @@ -69,37 +88,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:${{ env.IMAGE_TAG }} \
-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:${{ env.IMAGE_TAG }} \
-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:${{ env.IMAGE_TAG }} \
-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:${{ env.IMAGE_TAG }} \
-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:${{ env.IMAGE_TAG }} \
-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:${{ env.IMAGE_TAG }} \
-n production

- name: Wait for frontend rollout
Expand Down Expand Up @@ -142,4 +161,4 @@ jobs:
run: |
kubectl get pods -n production
kubectl get services -n production
kubectl get pvc -n production
kubectl get pvc -n production
Loading