diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3457426..5b95dea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,23 @@ name: CI Pipeline on: + # Trigger the pipeline when a Pull Request targets main + pull_request: + branches: + - main + + # Trigger the pipeline when code is pushed to main + push: + branches: + - main + # Manual trigger for the workflow workflow_dispatch: +# Required for Azure OIDC authentication +permissions: + id-token: write + contents: read jobs: @@ -78,10 +92,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" @@ -97,6 +111,41 @@ jobs: pytest -v + # ========================================================= + # Frontend Tests + # ========================================================= + frontend-test: + name: Test frontend + runs-on: ubuntu-latest + + env: + VITE_USER_SERVICE_URL: /api/user-service + VITE_STUDENT_SERVICE_URL: /api/student-service + VITE_LECTURER_SERVICE_URL: /api/lecturer-service + VITE_COURSE_SERVICE_URL: /api/course-service + VITE_ENROLLMENT_SERVICE_URL: /api/enrollment-service + + steps: + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "20" + + - name: Install frontend dependencies + working-directory: frontend + run: | + npm ci + + - name: Run frontend tests + working-directory: frontend + run: | + npm run test:run + + # ========================================================= # Build and Push Docker Images # ========================================================= @@ -104,11 +153,12 @@ jobs: name: Build and Push ${{ matrix.image }} runs-on: ubuntu-latest - # All backend tests must pass before this job starts + # Tests must pass before images are built needs: - backend-test + - frontend-test - # Build and push when code is pushed to main or manually triggered + # Only build/push for main pushes or manual runs if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' strategy: @@ -137,17 +187,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 + # ===================================================== + # Azure OIDC Login + # ===================================================== - name: Login to Azure - uses: azure/login@v2 + uses: azure/login@v3 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + # ===================================================== + # Azure Container Registry Login + # ===================================================== - name: Login to Azure Container Registry run: | az acr login --name ${{ vars.ACR_NAME }} + # ===================================================== + # Build Docker Image + # ===================================================== - name: Build Docker image run: | docker build \ @@ -156,12 +217,115 @@ jobs: -t ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:latest \ ./${{ matrix.service }} + # ===================================================== + # Push SHA Tagged Image + # ===================================================== - name: Push Docker image with commit SHA run: | docker push \ ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:${{ github.sha }} + # ===================================================== + # Push Latest Tagged Image + # ===================================================== - name: Push Docker image with latest tag run: | docker push \ ${{ vars.ACR_LOGIN_SERVER }}/${{ matrix.image }}:latest + + + # ========================================================= + # Continuous Deployment to Azure Kubernetes Service + # ========================================================= + deploy-to-aks: + name: Deploy to AKS + runs-on: ubuntu-latest + + # Deploy only after all Docker images are successfully pushed + needs: + - build-and-push + + # Only deploy for pushes to main or manual runs + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + + steps: + + # ===================================================== + # Checkout Repository + # ===================================================== + - name: Checkout repository + uses: actions/checkout@v6 + + # ===================================================== + # Azure OIDC Login + # ===================================================== + - name: Login to Azure + uses: azure/login@v3 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + # ===================================================== + # Configure kubectl for AKS + # ===================================================== + - name: Set AKS context + uses: azure/aks-set-context@v4 + with: + resource-group: koalatech-week07-rg + cluster-name: koalatech-week07-aks + + - name: Set up kubectl + uses: azure/setup-kubectl@v4 + + # ===================================================== + # Apply Kubernetes Manifests + # ===================================================== + - name: Deploy Kubernetes manifests + run: | + kubectl apply -f k8s/databases.yaml + kubectl apply -f k8s/apps.yaml + kubectl apply -f k8s/frontend.yaml + + # ===================================================== + # Update Deployments to Exact Git Commit Image + # ===================================================== + - name: Deploy images + run: | + kubectl set image deployment/user-service \ + user-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-user-service:${{ github.sha }} + + kubectl set image deployment/student-service \ + student-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-student-service:${{ github.sha }} + + kubectl set image deployment/lecturer-service \ + lecturer-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-lecturer-service:${{ github.sha }} + + kubectl set image deployment/course-service \ + course-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-course-service:${{ github.sha }} + + kubectl set image deployment/enrollment-service \ + enrollment-service=${{ vars.ACR_LOGIN_SERVER }}/koalatech-enrollment-service:${{ github.sha }} + + kubectl set image deployment/frontend \ + frontend=${{ vars.ACR_LOGIN_SERVER }}/koalatech-frontend:${{ github.sha }} + + # ===================================================== + # Wait for Successful Rollout + # ===================================================== + - name: Verify deployments + run: | + kubectl rollout status deployment/user-service --timeout=180s + kubectl rollout status deployment/student-service --timeout=180s + kubectl rollout status deployment/lecturer-service --timeout=180s + kubectl rollout status deployment/course-service --timeout=180s + kubectl rollout status deployment/enrollment-service --timeout=180s + kubectl rollout status deployment/frontend --timeout=180s + + # ===================================================== + # Verify Kubernetes Pods and Services + # ===================================================== + - name: Verify Kubernetes pods + run: | + kubectl get pods + kubectl get services \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 1132741..884b57d 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -93,7 +93,7 @@ const Login = () => { variant="h4" gutterBottom > - KoalaTech University + KoalaTech University - Continuous Deployment Demo { ); }; -export default Login; +export default Login; \ No newline at end of file diff --git a/k8s/apps.yaml b/k8s/apps.yaml new file mode 100644 index 0000000..4c77844 --- /dev/null +++ b/k8s/apps.yaml @@ -0,0 +1,308 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-service +spec: + replicas: 1 + selector: + matchLabels: + app: user-service + template: + metadata: + labels: + app: user-service + spec: + containers: + - name: user-service + image: koalatechweek07acr2.azurecr.io/koalatech-user-service:latest + ports: + - containerPort: 8000 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: users + - name: POSTGRES_HOST + value: user-db + - name: POSTGRES_PORT + value: "5432" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: user-service +spec: + selector: + app: user-service + ports: + - port: 8000 + targetPort: 8000 + type: ClusterIP + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: student-service +spec: + replicas: 1 + selector: + matchLabels: + app: student-service + template: + metadata: + labels: + app: student-service + spec: + containers: + - name: student-service + image: koalatechweek07acr2.azurecr.io/koalatech-student-service:latest + ports: + - containerPort: 8000 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: students + - name: POSTGRES_HOST + value: student-db + - name: POSTGRES_PORT + value: "5432" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: student-service +spec: + selector: + app: student-service + ports: + - port: 8000 + targetPort: 8000 + type: ClusterIP + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lecturer-service +spec: + replicas: 1 + selector: + matchLabels: + app: lecturer-service + template: + metadata: + labels: + app: lecturer-service + spec: + containers: + - name: lecturer-service + image: koalatechweek07acr2.azurecr.io/koalatech-lecturer-service:latest + ports: + - containerPort: 8000 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: lecturers + - name: POSTGRES_HOST + value: lecturer-db + - name: POSTGRES_PORT + value: "5432" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: lecturer-service +spec: + selector: + app: lecturer-service + ports: + - port: 8000 + targetPort: 8000 + type: ClusterIP + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: course-service +spec: + replicas: 1 + selector: + matchLabels: + app: course-service + template: + metadata: + labels: + app: course-service + spec: + containers: + - name: course-service + image: koalatechweek07acr2.azurecr.io/koalatech-course-service:latest + ports: + - containerPort: 8000 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: courses + - name: POSTGRES_HOST + value: course-db + - name: POSTGRES_PORT + value: "5432" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: course-service +spec: + selector: + app: course-service + ports: + - port: 8000 + targetPort: 8000 + type: ClusterIP + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: enrollment-service +spec: + replicas: 1 + selector: + matchLabels: + app: enrollment-service + template: + metadata: + labels: + app: enrollment-service + spec: + containers: + - name: enrollment-service + image: koalatechweek07acr2.azurecr.io/koalatech-enrollment-service:latest + ports: + - containerPort: 8000 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: enrollments + - name: POSTGRES_HOST + value: enrollment-db + - name: POSTGRES_PORT + value: "5432" + readinessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: enrollment-service +spec: + selector: + app: enrollment-service + ports: + - port: 8000 + targetPort: 8000 + type: ClusterIP \ No newline at end of file diff --git a/k8s/databases.yaml b/k8s/databases.yaml new file mode 100644 index 0000000..273bf5a --- /dev/null +++ b/k8s/databases.yaml @@ -0,0 +1,343 @@ +apiVersion: v1 +kind: Secret +metadata: + name: postgres-secret +type: Opaque +stringData: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: user-db-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-db +spec: + replicas: 1 + selector: + matchLabels: + app: user-db + template: + metadata: + labels: + app: user-db + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: users + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: user-db-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: user-db +spec: + selector: + app: user-db + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: student-db-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: student-db +spec: + replicas: 1 + selector: + matchLabels: + app: student-db + template: + metadata: + labels: + app: student-db + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: students + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: student-db-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: student-db +spec: + selector: + app: student-db + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: lecturer-db-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lecturer-db +spec: + replicas: 1 + selector: + matchLabels: + app: lecturer-db + template: + metadata: + labels: + app: lecturer-db + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: lecturers + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: lecturer-db-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: lecturer-db +spec: + selector: + app: lecturer-db + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: course-db-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: course-db +spec: + replicas: 1 + selector: + matchLabels: + app: course-db + template: + metadata: + labels: + app: course-db + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: courses + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: course-db-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: course-db +spec: + selector: + app: course-db + ports: + - port: 5432 + targetPort: 5432 + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: enrollment-db-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: enrollment-db +spec: + replicas: 1 + selector: + matchLabels: + app: enrollment-db + template: + metadata: + labels: + app: enrollment-db + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_USER + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: postgres-secret + key: POSTGRES_PASSWORD + - name: POSTGRES_DB + value: enrollments + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: postgres-data + mountPath: /var/lib/postgresql/data + volumes: + - name: postgres-data + persistentVolumeClaim: + claimName: enrollment-db-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: enrollment-db +spec: + selector: + app: enrollment-db + ports: + - port: 5432 + targetPort: 5432 \ No newline at end of file diff --git a/k8s/frontend.yaml b/k8s/frontend.yaml new file mode 100644 index 0000000..c3b3d3a --- /dev/null +++ b/k8s/frontend.yaml @@ -0,0 +1,31 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + containers: + - name: frontend + image: koalatechweek07acr2.azurecr.io/koalatech-frontend:latest + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: frontend +spec: + type: LoadBalancer + selector: + app: frontend + ports: + - port: 80 + targetPort: 80 \ No newline at end of file