Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.git
Dockerfile
.env.local
.env
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: CI
run-name: '[PR] Running pre-merge checks for PR#${{ github.event.number }}'

on:
push:
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Deploy Staging to Cloud Run
run-name: '[STAGING] Running staging release workflow #${{ github.run_number }}'

on:
push:
branches:
- dev

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

env:
PROJECT_ID: student-progress-staging
REGION: europe-west3
REPOSITORY: student-progress-api-repo
SERVICE: student-progress-api
IMAGE_URI: europe-west3-docker.pkg.dev/student-progress-staging/student-progress-api-repo/student-progress-api

jobs:
deploy:
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

- name: Set up gcloud
uses: google-github-actions/setup-gcloud@v3

- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push image
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: true
tags: |
${{ env.IMAGE_URI }}:${{ github.sha }}
${{ env.IMAGE_URI }}:latest

- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v3
with:
service: ${{ env.SERVICE }}
region: ${{ env.REGION }}
image: ${{ env.IMAGE_URI }}:${{ github.sha }}
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ Cloud Run app → service account IAM DB user
Admin tasks → separate admin user / controlled IAM access
```

**The API runs on Cloud Run and connects to private resources via Direct VPC egress.**

```txt
Client
Cloud Run service
Direct VPC egress
VPC Network
Private resources (Cloud SQL / Redis)
```

### API Scope

This project intentionally implements a small set of representative endpoints rather than a complete CRUD API.
Expand Down Expand Up @@ -278,6 +292,28 @@ gcloud run deploy student-progress-api \

Note: Replace `<REDIS_HOST>` with the Memorystore private IP.

Subsequent deployments are handled with GitHub Actions.

### 4. GitHub Actions Authentication (OIDC)

GitHub Actions authenticates to GCP using Workload Identity Federation (OIDC) instead of long-lived JSON service account keys.

Terraform provisions:

- a dedicated GitHub Actions deployer service account
- a Workload Identity Pool
- a GitHub OIDC provider
- IAM bindings allowing the repository to impersonate the deployer service account

Apply the Terraform configuration.

Add these Terraform outputs as GitHub Actions repository secrets:

| **Secret** | **Terraform output** |
| ------------------------------ | -------------------------------------- |
| GCP_WORKLOAD_IDENTITY_PROVIDER | github_workload_identity_provider_name |
| GCP_SERVICE_ACCOUNT | github_deployer_service_account_email |

## One-off Local Development Setup

### 1. **Install Auth Proxy & update dev script**
Expand Down
5 changes: 4 additions & 1 deletion infra/apis.tf
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
resource "google_project_service" "apis" {
for_each = toset([
"artifactregistry.googleapis.com",
"iam.googleapis.com",
"iamcredentials.googleapis.com",
"run.googleapis.com", # for deploying to Cloud Run
"redis.googleapis.com",
"secretmanager.googleapis.com",
"sqladmin.googleapis.com"
"sts.googleapis.com",
"sqladmin.googleapis.com",

])

project = var.project_id
Expand Down
66 changes: 66 additions & 0 deletions infra/iam.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Runtime service account used by the application (Cloud Run runtime identity for Cloud SQL, Secret Manager, etc.)
resource "google_service_account" "app" {
account_id = "student-progress-app-sa"
display_name = "Student Progress App"
Expand All @@ -15,3 +16,68 @@ resource "google_project_iam_member" "app_roles" {

member = "serviceAccount:${google_service_account.app.email}"
}


# GitHub Actions deployer service account
resource "google_service_account" "github_deployer" {
account_id = "github-deployer-sa"
display_name = "GitHub Actions Deployer"
}

# Permissions for deploying to Cloud Run
resource "google_project_iam_member" "github_deployer_roles" {

for_each = toset([
"roles/run.admin",
"roles/artifactregistry.writer",
"roles/iam.serviceAccountUser"
])

project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.github_deployer.email}"
}

# Workload Identity Pool used for GitHub Actions authentication
resource "google_iam_workload_identity_pool" "github_actions" {
workload_identity_pool_id = "github-pool"

display_name = "GitHub Actions Pool"
description = "Workload Identity Pool for GitHub Actions"
}

# GitHub OIDC provider used to authenticate GitHub Actions to GCP
resource "google_iam_workload_identity_pool_provider" "github_provider" {
workload_identity_pool_id = google_iam_workload_identity_pool.github_actions.workload_identity_pool_id
workload_identity_pool_provider_id = "github-provider"

display_name = "GitHub Provider"

attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.actor" = "assertion.actor"
"attribute.repository" = "assertion.repository"
"attribute.repository_owner" = "assertion.repository_owner"
"attribute.ref" = "assertion.ref"
}

attribute_condition = "assertion.repository == '${var.github_org}/${var.github_repo}' && assertion.ref == 'refs/heads/main'"

oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
}

# Allow GitHub Actions identities from this repository to impersonate the deployer service account
resource "google_service_account_iam_member" "github_actions_workload_identity" {
service_account_id = google_service_account.github_deployer.name

role = "roles/iam.workloadIdentityUser"

member = "principalSet://iam.googleapis.com/projects/${data.google_project.current.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.github_actions.workload_identity_pool_id}/attribute.repository/${var.github_org}/${var.github_repo}"
}

# Current GCP project metadata used for IAM bindings and resource references
data "google_project" "current" {
project_id = var.project_id
}
11 changes: 11 additions & 0 deletions infra/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,14 @@ output "redis_host" {
output "redis_port" {
value = google_redis_instance.cache.port
}


# GitHub deployer service account email used by GitHub Actions authentication
output "github_deployer_service_account_email" {
value = google_service_account.github_deployer.email
}

# Workload Identity Provider resource name used by GitHub Actions authentication
output "github_workload_identity_provider_name" {
value = google_iam_workload_identity_pool_provider.github_provider.name
}
2 changes: 2 additions & 0 deletions infra/staging.tfvars
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
project_id = "student-progress-staging"
environment = "staging"
github_org = "aga87"
github_repo = "student-progress-api"
8 changes: 8 additions & 0 deletions infra/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ variable "db_name" {
description = "Application database name"
default = "student_progress"
}

variable "github_org" {
type = string
}

variable "github_repo" {
type = string
}