From 060c33797fb9301ead02585803888f68b3b42178 Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Tue, 2 Jun 2026 15:20:49 +0100 Subject: [PATCH 1/8] Add terraform backend GCS storage for browse page Using the dev project whilst we lack a common project. --- .gitignore | 4 + cloud/browse/README.md | 13 +++ cloud/browse/tf-backend/.terraform.lock.hcl | 22 +++++ cloud/browse/tf-backend/README.md | 95 +++++++++++++++++++++ cloud/browse/tf-backend/main.tf | 12 +++ cloud/browse/tf-backend/variables.tf | 11 +++ cloud/browse/tf-backend/versions.tf | 10 +++ 7 files changed, 167 insertions(+) create mode 100644 cloud/browse/README.md create mode 100644 cloud/browse/tf-backend/.terraform.lock.hcl create mode 100644 cloud/browse/tf-backend/README.md create mode 100644 cloud/browse/tf-backend/main.tf create mode 100644 cloud/browse/tf-backend/variables.tf create mode 100644 cloud/browse/tf-backend/versions.tf diff --git a/.gitignore b/.gitignore index c0baec31..0518eb84 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ ctrf # playwright test-results + +# terraform +.terraform/ +/cloud/browse/tf-backend/terraform.tfstate* diff --git a/cloud/browse/README.md b/cloud/browse/README.md new file mode 100644 index 00000000..897b2385 --- /dev/null +++ b/cloud/browse/README.md @@ -0,0 +1,13 @@ +# Cloud infrastructure + +This service is hosted on [Google Cloud Platform](https://console.cloud.google.com/). + +## Prerequisites + +1. Install [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) + +## Provisioning + +To provision the cloud infrastructure: + +1. Provision the [Terraform backend](tf-backend/README.md) diff --git a/cloud/browse/tf-backend/.terraform.lock.hcl b/cloud/browse/tf-backend/.terraform.lock.hcl new file mode 100644 index 00000000..142d5132 --- /dev/null +++ b/cloud/browse/tf-backend/.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/google" { + version = "7.30.0" + constraints = "~> 7.30.0" + hashes = [ + "h1:1WM1iHQdoYqqx2LWOgWGQ83JbKwgTwAefdBNZA4tt4I=", + "zh:0cda2cc03f7bf000d9bc66bc0fab621de4c104b329cab348e0ceb6146ab27251", + "zh:2c75a1ea53b21646681e49fbdf0a599817c2f400f1e73d7779f0e3e1d230e6f3", + "zh:34ab9dab67230adaee6a9cd6861cba969555777ca6eb0ae1d2ac7b1f3cb73832", + "zh:45d5d7ee38fb7bf58dd19b774dd637f3cb9caef1d1930dde594467dde7fdea50", + "zh:651ffd36697d8268471d50d0fae664549b7f1e627c03d6e90f80172947f7b1d4", + "zh:8557db0beb201ba8ba70a7a38ba8d1ce9ffb9e98c616b89f6e3c2203e0528803", + "zh:b6a2e53809e0827cb7c47b1279c3511223898f7e3c1536f74ba057d99c72c2e9", + "zh:bf4aea9d1eb663df9d458c974d2e3f9ca7f724280a103706d0a2b1597593c7af", + "zh:c5f0160e0658d75b4a339b18f7a544e721f3750e26b97fc4887e98b8e085ffff", + "zh:e41a215491c64b535eda5585139dab0a32836e631b470cc520ceef4aa9ce7748", + "zh:e490061ab15c6053c651a7996a9c77cacdb83528ed49d0460c9621e93ad7d0cf", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/cloud/browse/tf-backend/README.md b/cloud/browse/tf-backend/README.md new file mode 100644 index 00000000..f28e419d --- /dev/null +++ b/cloud/browse/tf-backend/README.md @@ -0,0 +1,95 @@ +# Terraform backend + +This root module provisions a [Cloud Storage bucket](https://cloud.google.com/storage/docs/buckets) to be used as a +[Terraform backend](https://developer.hashicorp.com/terraform/language/backend) for other root modules. + +As the state for this root module cannot be easily stored within itself, we store it in a shared Bitwarden note instead. + +## Provisioning + +1. Change directory: + + ```bash + cd cloud/tf-backend + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Initialise Terraform: + + ```bash + terraform init + ``` + +1. Fetch the previous Terraform state from Bitwarden, if any: + + ```bash + bw get notes "browse-page-tf-backend-state" > terraform.tfstate + ``` + +1. Apply the changes: + + ```bash + terraform apply + ``` + +1. Store the new Terraform state in Bitwarden as "browse-page-tf-backend-state" + +## Destroying + +1. Change directory: + + ```bash + cd cloud/tf-backend + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Initialise Terraform: + + ```bash + terraform init + ``` + +1. Fetch the previous Terraform state from Bitwarden, if any: + + ```bash + bw get notes "browse-page-tf-backend-state" > terraform.tfstate + ``` + +1. Force deletion of bucket objects by modifying `cloud/tf-backend/main.tf`: + + ```diff + resource "google_storage_bucket" "main" { + + force_destroy = true + ... + } + ``` + +1. Apply the change: + + ```bash + terraform apply + ``` + +1. Revert the modification: + + ```bash + git checkout main.tf + ``` + +1. Delete the resources: + + ```bash + terraform destroy + ``` + +1. Delete the Terraform state "browse-page-tf-backend-state" from Bitwarden diff --git a/cloud/browse/tf-backend/main.tf b/cloud/browse/tf-backend/main.tf new file mode 100644 index 00000000..66790456 --- /dev/null +++ b/cloud/browse/tf-backend/main.tf @@ -0,0 +1,12 @@ +resource "google_storage_bucket" "main" { + name = "${var.project}-tf-backend" + project = var.project + location = var.location + + uniform_bucket_level_access = true + public_access_prevention = "enforced" + + versioning { + enabled = true + } +} diff --git a/cloud/browse/tf-backend/variables.tf b/cloud/browse/tf-backend/variables.tf new file mode 100644 index 00000000..65921a36 --- /dev/null +++ b/cloud/browse/tf-backend/variables.tf @@ -0,0 +1,11 @@ +variable "project" { + description = "GCP project" + type = string + default = "dft-rlg-atip-dev" +} + +variable "location" { + description = "GCP location" + type = string + default = "europe-west1" +} diff --git a/cloud/browse/tf-backend/versions.tf b/cloud/browse/tf-backend/versions.tf new file mode 100644 index 00000000..11959c07 --- /dev/null +++ b/cloud/browse/tf-backend/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = "~> 1.15.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 7.30.0" + } + } +} From 2c5b5d76199d96a4174ad7e7750f112452c26ed0 Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Tue, 2 Jun 2026 15:31:26 +0100 Subject: [PATCH 2/8] Add package.json script for terraform formatting --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e735324d..14145bd4 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,13 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "fmt": "npx prettier --write *.html tests/* src/* backend/*.js", + "fmt": "npm run prettier-format && npm run terraform-format", "test": "npx playwright test", "test-server": "node tests/test-server/server.js", "check": "svelte-check --tsconfig ./tsconfig.json", + "prettier-format": "npx prettier --write *.html tests/* src/* backend/*.js", "setup-govuk": "sass src/style/main.sass src/style/main.css; rm -rf public/assets; mkdir -p public/assets; cp -R node_modules/govuk-frontend/dist/govuk/assets/ public/", + "terraform-format": "terraform -chdir=cloud fmt -recursive", "generate-random-schemes": "npx ts-node --project tsconfig_tsnode.json --esm src/scripts/random_schemes.ts" }, "devDependencies": { From f22e69105ac9a7922651c5b915babc9068816f3d Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Wed, 3 Jun 2026 16:16:05 +0100 Subject: [PATCH 3/8] Add docker repository for browse page --- cloud/browse/README.md | 1 + .../docker-repository/.terraform.lock.hcl | 22 ++++++ cloud/browse/docker-repository/README.md | 68 ++++++++++++++++++ .../github-action-push/main.tf | 22 ++++++ .../github-action-push/outputs.tf | 5 ++ .../github-action-push/variables.tf | 4 ++ cloud/browse/docker-repository/main.tf | 70 +++++++++++++++++++ cloud/browse/docker-repository/outputs.tf | 21 ++++++ cloud/browse/docker-repository/variables.tf | 11 +++ cloud/browse/docker-repository/versions.tf | 10 +++ 10 files changed, 234 insertions(+) create mode 100644 cloud/browse/docker-repository/.terraform.lock.hcl create mode 100644 cloud/browse/docker-repository/README.md create mode 100644 cloud/browse/docker-repository/github-action-push/main.tf create mode 100644 cloud/browse/docker-repository/github-action-push/outputs.tf create mode 100644 cloud/browse/docker-repository/github-action-push/variables.tf create mode 100644 cloud/browse/docker-repository/main.tf create mode 100644 cloud/browse/docker-repository/outputs.tf create mode 100644 cloud/browse/docker-repository/variables.tf create mode 100644 cloud/browse/docker-repository/versions.tf diff --git a/cloud/browse/README.md b/cloud/browse/README.md index 897b2385..4f4b4b52 100644 --- a/cloud/browse/README.md +++ b/cloud/browse/README.md @@ -11,3 +11,4 @@ This service is hosted on [Google Cloud Platform](https://console.cloud.google.c To provision the cloud infrastructure: 1. Provision the [Terraform backend](tf-backend/README.md) +2. Provision the [Docker repository](docker-repository/README.md) diff --git a/cloud/browse/docker-repository/.terraform.lock.hcl b/cloud/browse/docker-repository/.terraform.lock.hcl new file mode 100644 index 00000000..142d5132 --- /dev/null +++ b/cloud/browse/docker-repository/.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/google" { + version = "7.30.0" + constraints = "~> 7.30.0" + hashes = [ + "h1:1WM1iHQdoYqqx2LWOgWGQ83JbKwgTwAefdBNZA4tt4I=", + "zh:0cda2cc03f7bf000d9bc66bc0fab621de4c104b329cab348e0ceb6146ab27251", + "zh:2c75a1ea53b21646681e49fbdf0a599817c2f400f1e73d7779f0e3e1d230e6f3", + "zh:34ab9dab67230adaee6a9cd6861cba969555777ca6eb0ae1d2ac7b1f3cb73832", + "zh:45d5d7ee38fb7bf58dd19b774dd637f3cb9caef1d1930dde594467dde7fdea50", + "zh:651ffd36697d8268471d50d0fae664549b7f1e627c03d6e90f80172947f7b1d4", + "zh:8557db0beb201ba8ba70a7a38ba8d1ce9ffb9e98c616b89f6e3c2203e0528803", + "zh:b6a2e53809e0827cb7c47b1279c3511223898f7e3c1536f74ba057d99c72c2e9", + "zh:bf4aea9d1eb663df9d458c974d2e3f9ca7f724280a103706d0a2b1597593c7af", + "zh:c5f0160e0658d75b4a339b18f7a544e721f3750e26b97fc4887e98b8e085ffff", + "zh:e41a215491c64b535eda5585139dab0a32836e631b470cc520ceef4aa9ce7748", + "zh:e490061ab15c6053c651a7996a9c77cacdb83528ed49d0460c9621e93ad7d0cf", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/cloud/browse/docker-repository/README.md b/cloud/browse/docker-repository/README.md new file mode 100644 index 00000000..64cee10c --- /dev/null +++ b/cloud/browse/docker-repository/README.md @@ -0,0 +1,68 @@ +# Docker repository + +This root module provisions an [Artifact Registry](https://cloud.google.com/artifact-registry/docs/overview) to store +Docker images for this service. + +## Provisioning + +1. Change directory: + + ```bash + cd cloud/docker-repository + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Initialise Terraform: + + ```bash + terraform init + ``` + +1. Apply the changes: + + ```bash + terraform apply + ``` + +## Configuring GitHub Actions + +To configure the [CI workflow](../../.github/workflows/ci.yml) with credentials to push images to the repository: + +1. Obtain the Docker repository service account private key: + + ```bash + terraform output -raw github_action_push_private_key + ``` + +1. [Set the GitHub Actions repository secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository) `GCP_CREDENTIALS_PUSH` to the private key + +## Destroying + +1. Change directory: + + ```bash + cd cloud/docker-repository + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Initialise Terraform: + + ```bash + terraform init + ``` + +1. Delete the resources: + + ```bash + terraform destroy + ``` diff --git a/cloud/browse/docker-repository/github-action-push/main.tf b/cloud/browse/docker-repository/github-action-push/main.tf new file mode 100644 index 00000000..94e18e29 --- /dev/null +++ b/cloud/browse/docker-repository/github-action-push/main.tf @@ -0,0 +1,22 @@ +resource "google_service_account" "main" { + project = var.project + account_id = "github-action-push" + display_name = "Service account for push GitHub Action" +} + +resource "google_project_iam_member" "service_account_token_creator" { + project = var.project + role = "roles/iam.serviceAccountTokenCreator" + member = "serviceAccount:${google_service_account.main.email}" +} + +resource "google_project_iam_member" "artifact_registry_writer" { + project = var.project + role = "roles/artifactregistry.writer" + member = "serviceAccount:${google_service_account.main.email}" +} + +resource "google_service_account_key" "main" { + service_account_id = google_service_account.main.name + public_key_type = "TYPE_X509_PEM_FILE" +} diff --git a/cloud/browse/docker-repository/github-action-push/outputs.tf b/cloud/browse/docker-repository/github-action-push/outputs.tf new file mode 100644 index 00000000..ce262a3e --- /dev/null +++ b/cloud/browse/docker-repository/github-action-push/outputs.tf @@ -0,0 +1,5 @@ +output "private_key" { + description = "Service account key for push GitHub Action service account" + value = google_service_account_key.main.private_key + sensitive = true +} diff --git a/cloud/browse/docker-repository/github-action-push/variables.tf b/cloud/browse/docker-repository/github-action-push/variables.tf new file mode 100644 index 00000000..a90c35ee --- /dev/null +++ b/cloud/browse/docker-repository/github-action-push/variables.tf @@ -0,0 +1,4 @@ +variable "project" { + description = "GCP project" + type = string +} diff --git a/cloud/browse/docker-repository/main.tf b/cloud/browse/docker-repository/main.tf new file mode 100644 index 00000000..74548ce7 --- /dev/null +++ b/cloud/browse/docker-repository/main.tf @@ -0,0 +1,70 @@ +terraform { + backend "gcs" { + bucket = "dft-rlg-atip-dev-tf-backend" + prefix = "docker-repository" + } +} + +locals { + day_in_seconds = 24 * 60 * 60 +} + +resource "google_project_service" "artifact_registry" { + project = var.project + service = "artifactregistry.googleapis.com" +} + +resource "google_project_service" "compute" { + project = var.project + service = "compute.googleapis.com" +} + +resource "google_project_service" "container_scanning" { + project = var.project + service = "containerscanning.googleapis.com" +} + +resource "google_project_service" "iam_credentials" { + project = var.project + service = "iamcredentials.googleapis.com" +} + +resource "google_project_iam_audit_config" "artifact_registry_data_write" { + project = var.project + service = "artifactregistry.googleapis.com" + + audit_log_config { + log_type = "DATA_WRITE" + } +} + +resource "google_artifact_registry_repository" "main" { + project = var.project + repository_id = "docker" + location = var.location + format = "DOCKER" + + cleanup_policies { + id = "delete-untagged" + action = "DELETE" + condition { + tag_state = "UNTAGGED" + } + } + + cleanup_policies { + id = "keep-recent-untagged" + action = "KEEP" + condition { + tag_state = "UNTAGGED" + newer_than = "${7 * local.day_in_seconds}s" + } + } + + depends_on = [google_project_service.artifact_registry] +} + +module "github_action_push" { + source = "./github-action-push" + project = var.project +} diff --git a/cloud/browse/docker-repository/outputs.tf b/cloud/browse/docker-repository/outputs.tf new file mode 100644 index 00000000..e2cac346 --- /dev/null +++ b/cloud/browse/docker-repository/outputs.tf @@ -0,0 +1,21 @@ +output "project" { + description = "GCP project" + value = google_artifact_registry_repository.main.project +} + +output "url" { + description = "Docker repository URL" + value = join("", [ + google_artifact_registry_repository.main.location, + "-docker.pkg.dev/", + google_artifact_registry_repository.main.project, + "/", + google_artifact_registry_repository.main.repository_id + ]) +} + +output "github_action_push_private_key" { + description = "Service account key for push GitHub Action service account" + value = module.github_action_push.private_key + sensitive = true +} diff --git a/cloud/browse/docker-repository/variables.tf b/cloud/browse/docker-repository/variables.tf new file mode 100644 index 00000000..b319ea08 --- /dev/null +++ b/cloud/browse/docker-repository/variables.tf @@ -0,0 +1,11 @@ +variable "project" { + description = "GCP project" + type = string + default = "dft-rlg-atip-dev" +} + +variable "location" { + description = "GCP location" + type = string + default = "europe-west1" +} diff --git a/cloud/browse/docker-repository/versions.tf b/cloud/browse/docker-repository/versions.tf new file mode 100644 index 00000000..9c16084b --- /dev/null +++ b/cloud/browse/docker-repository/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = "~> 1.15.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 7.30.0" + } + } +} From 31fce1c01db9f6639d28cd2ba491bec71681cf49 Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Mon, 8 Jun 2026 15:49:26 +0100 Subject: [PATCH 4/8] Build and push Docker image after full CI workflow --- .github/workflows/ci.yml | 93 ++++++++++++++++++++++++++++++++ .github/workflows/playwright.yml | 52 ------------------ Dockerfile | 12 +++++ README.md | 44 +++++++++++++++ 4 files changed, 149 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/playwright.yml create mode 100644 Dockerfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..8e78c2ec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +env: + VITE_MAPTILER_API_KEY: '${{ secrets.MAPTILER_API_KEY }}' + IMAGE_PROJECT: dft-rlg-atip-dev + IMAGE_LOCATION: europe-west1 + IMAGE_REPOSITORY_ID: docker + IMAGE_NAME: plan + +jobs: + check: + timeout-minutes: 60 + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 22.x + cache: 'npm' + + - name: Install wasm-pack + uses: jetli/wasm-pack-action@v0.4.0 + + - name: Install dependencies + run: npm ci + + - name: Setup GOV.UK + run: npm run setup-govuk + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Run type-checker + run: npm run check + + - name: Run Playwright tests + run: npm test + + - name: Upload test results + run: npx github-actions-ctrf ctrf/ctrf-report.json + if: always() + + - name: Upload test results for historical report + uses: actions/upload-artifact@v4 + with: + name: ctrf-report + path: ctrf/ctrf-report.json + if: always() + + - name: Build svelte app + run: npm run build + + - id: auth + name: Authenticate with Google Cloud + uses: google-github-actions/auth@v3 + with: + token_format: access_token + credentials_json: '${{ secrets.GCP_CREDENTIALS_PUSH }}' + + - name: Login to Docker repository + uses: docker/login-action@v4 + with: + registry: ${{ env.IMAGE_LOCATION }}-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.auth.outputs.access_token }} + + - id: meta + name: Extract metadata + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE_LOCATION }}-docker.pkg.dev/${{ env.IMAGE_PROJECT }}/${{ env.IMAGE_REPOSITORY_ID }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=ref,event=branch,enable={{is_not_default_branch}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push image + uses: docker/build-push-action@v7 + with: + context: . + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml deleted file mode 100644 index b70b48ff..00000000 --- a/.github/workflows/playwright.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Playwright Tests - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -env: - VITE_MAPTILER_API_KEY: '${{ secrets.MAPTILER_API_KEY }}' - VITE_RESOURCE_BASE: 'https://atip.uk' - -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-node@v3 - with: - node-version: 22.x - cache: 'npm' - - - name: Install wasm-pack - uses: jetli/wasm-pack-action@v0.4.0 - - - name: Install dependencies - run: npm ci - - - name: Setup GOV.UK - run: npm run setup-govuk - - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - - name: Run type-checker - run: npm run check - - - name: Run Playwright tests - run: npm test - - - name: Upload test results - run: npx github-actions-ctrf ctrf/ctrf-report.json - if: always() - - - name: Upload test results for historical report - uses: actions/upload-artifact@v4 - with: - name: ctrf-report - path: ctrf/ctrf-report.json - if: always() diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c374b611 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-slim + +ENV PORT=8080 + +WORKDIR /usr/src/app +COPY backend . + +RUN npm install --production + +USER node + +CMD [ "sh", "-c", "npm start" ] diff --git a/README.md b/README.md index a6f68f45..125bed24 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,50 @@ You can also configure the application locally by setting the following environm 1. Open http://localhost:8080 +## Running locally using Docker + +To run the server as a container: + +1. Build the svelte app + + ```bash + npm install && npm run build + ``` + +2. Copy over the built svelte app + + ```bash + cp -r dist backend/ + ``` + +1. Build the Docker image + + ```bash + docker build -t plan . + ``` + +1. Authenticate with Google + + ```bash + gcloud auth application-default login + ``` + +1. Run the Docker image + + ```bash + ADC=~/.config/gcloud/application_default_credentials.json + + docker run --rm -it \ + -e GCS_BUCKET=dft-rlg-atip-dev \ + -e USE_IAP=false \ + -e GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/google_credentials.json \ + -v ${ADC}:/tmp/keys/google_credentials.json:ro \ + -p 8080:8080 \ + plan + ``` + +1. Open http://127.0.0.1:8080 + ### Developing with Vite From 26220f9ca51970772402201c7b4021bc93545dc2 Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Thu, 25 Jun 2026 16:31:09 +0100 Subject: [PATCH 5/8] Import terraform for Cloud Storage bucket This bucket is used for data accessible to plan your active travel schemes. --- cloud/browse/README.md | 1 + .../browse/storage-bucket/.terraform.lock.hcl | 22 +++++ cloud/browse/storage-bucket/README.md | 97 +++++++++++++++++++ cloud/browse/storage-bucket/main.tf | 31 ++++++ cloud/browse/storage-bucket/outputs.tf | 4 + cloud/browse/storage-bucket/variables.tf | 11 +++ cloud/browse/storage-bucket/versions.tf | 10 ++ 7 files changed, 176 insertions(+) create mode 100644 cloud/browse/storage-bucket/.terraform.lock.hcl create mode 100644 cloud/browse/storage-bucket/README.md create mode 100644 cloud/browse/storage-bucket/main.tf create mode 100644 cloud/browse/storage-bucket/outputs.tf create mode 100644 cloud/browse/storage-bucket/variables.tf create mode 100644 cloud/browse/storage-bucket/versions.tf diff --git a/cloud/browse/README.md b/cloud/browse/README.md index 4f4b4b52..19efcc1d 100644 --- a/cloud/browse/README.md +++ b/cloud/browse/README.md @@ -12,3 +12,4 @@ To provision the cloud infrastructure: 1. Provision the [Terraform backend](tf-backend/README.md) 2. Provision the [Docker repository](docker-repository/README.md) +3. Provision the [storage bucket](storage-bucket/README.md) diff --git a/cloud/browse/storage-bucket/.terraform.lock.hcl b/cloud/browse/storage-bucket/.terraform.lock.hcl new file mode 100644 index 00000000..142d5132 --- /dev/null +++ b/cloud/browse/storage-bucket/.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/google" { + version = "7.30.0" + constraints = "~> 7.30.0" + hashes = [ + "h1:1WM1iHQdoYqqx2LWOgWGQ83JbKwgTwAefdBNZA4tt4I=", + "zh:0cda2cc03f7bf000d9bc66bc0fab621de4c104b329cab348e0ceb6146ab27251", + "zh:2c75a1ea53b21646681e49fbdf0a599817c2f400f1e73d7779f0e3e1d230e6f3", + "zh:34ab9dab67230adaee6a9cd6861cba969555777ca6eb0ae1d2ac7b1f3cb73832", + "zh:45d5d7ee38fb7bf58dd19b774dd637f3cb9caef1d1930dde594467dde7fdea50", + "zh:651ffd36697d8268471d50d0fae664549b7f1e627c03d6e90f80172947f7b1d4", + "zh:8557db0beb201ba8ba70a7a38ba8d1ce9ffb9e98c616b89f6e3c2203e0528803", + "zh:b6a2e53809e0827cb7c47b1279c3511223898f7e3c1536f74ba057d99c72c2e9", + "zh:bf4aea9d1eb663df9d458c974d2e3f9ca7f724280a103706d0a2b1597593c7af", + "zh:c5f0160e0658d75b4a339b18f7a544e721f3750e26b97fc4887e98b8e085ffff", + "zh:e41a215491c64b535eda5585139dab0a32836e631b470cc520ceef4aa9ce7748", + "zh:e490061ab15c6053c651a7996a9c77cacdb83528ed49d0460c9621e93ad7d0cf", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/cloud/browse/storage-bucket/README.md b/cloud/browse/storage-bucket/README.md new file mode 100644 index 00000000..3caab99f --- /dev/null +++ b/cloud/browse/storage-bucket/README.md @@ -0,0 +1,97 @@ +# Storage bucket + +This root module provisions a [Cloud Storage bucket](https://cloud.google.com/storage/docs/buckets) to be used to store data used by +the client side application. + +## Environments + +There are multiple environments that replicate the resources which are represented as Terraform workspaces: + +* `dev` +* `test` +* `prod` + +Set the target environment, for example: + +```bash +export ENVIRONMENT=dev +``` + +## Provisioning + +1. Change directory: + + ```bash + cd cloud/browse/storage-bucket + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Initialise Terraform: + + ```bash + terraform init + ``` + +1. Create a Terraform workspace for the environment: + + ```bash + terraform workspace new ${ENVIRONMENT} + ``` + +1. Apply the changes: + + ```bash + terraform apply + ``` + +## Destroying + +1. Change directory: + + ```bash + cd cloud/browse/storage-bucket + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Select the Terraform workspace for the environment: + + ```bash + terraform workspace select ${ENVIRONMENT} + ``` + +1. Disable deletion protection for the service by modifying `cloud/browse/storage-bucket/main.tf`: + + ```diff + resource "google_storage_bucket" "main" { + + force_destroy = true + ... + } + ``` + +1. Apply the change: + + ```bash + terraform apply + ``` + +1. Revert the modification: + + ```bash + git checkout main.tf + ``` + +1. Delete the resources: + + ```bash + terraform destroy + ``` \ No newline at end of file diff --git a/cloud/browse/storage-bucket/main.tf b/cloud/browse/storage-bucket/main.tf new file mode 100644 index 00000000..72ef13ba --- /dev/null +++ b/cloud/browse/storage-bucket/main.tf @@ -0,0 +1,31 @@ +terraform { + backend "gcs" { + bucket = "dft-rlg-atip-dev-tf-backend" + prefix = "storage-bucket" + } +} + +locals { + day_in_seconds = 60 * 60 * 24 + env = terraform.workspace + project = "${var.project_prefix}-${local.env}" + gcs_bucket_name = local.project +} + +resource "google_storage_bucket" "main" { + name = local.gcs_bucket_name + project = local.project + location = var.location + + uniform_bucket_level_access = true + public_access_prevention = "inherited" + + soft_delete_policy { + retention_duration_seconds = 7 * local.day_in_seconds + } +} + +import { + id = "${local.project}/${local.gcs_bucket_name}" + to = google_storage_bucket.main +} diff --git a/cloud/browse/storage-bucket/outputs.tf b/cloud/browse/storage-bucket/outputs.tf new file mode 100644 index 00000000..dfb304d6 --- /dev/null +++ b/cloud/browse/storage-bucket/outputs.tf @@ -0,0 +1,4 @@ +output "bucket_name" { + description = "GCS storage bucket name" + value = google_storage_bucket.main.name +} diff --git a/cloud/browse/storage-bucket/variables.tf b/cloud/browse/storage-bucket/variables.tf new file mode 100644 index 00000000..dc394a56 --- /dev/null +++ b/cloud/browse/storage-bucket/variables.tf @@ -0,0 +1,11 @@ +variable "project_prefix" { + description = "GCP project prefix" + type = string + default = "dft-rlg-atip" +} + +variable "location" { + description = "GCP location" + type = string + default = "europe-west2" +} diff --git a/cloud/browse/storage-bucket/versions.tf b/cloud/browse/storage-bucket/versions.tf new file mode 100644 index 00000000..11959c07 --- /dev/null +++ b/cloud/browse/storage-bucket/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = "~> 1.15.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 7.30.0" + } + } +} From 0260f84b0fd00b154e4de30ebe25df451d094dcd Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Fri, 26 Jun 2026 13:14:31 +0100 Subject: [PATCH 6/8] Add service running in Cloud Run Approximate like-for-like set up to App Engine without IAP. --- cloud/browse/README.md | 1 + cloud/browse/service/.terraform.lock.hcl | 22 +++++ cloud/browse/service/README.md | 96 +++++++++++++++++++ cloud/browse/service/application/main.tf | 63 ++++++++++++ cloud/browse/service/application/outputs.tf | 4 + cloud/browse/service/application/variables.tf | 34 +++++++ cloud/browse/service/main.tf | 59 ++++++++++++ cloud/browse/service/outputs.tf | 4 + cloud/browse/service/variables.tf | 11 +++ cloud/browse/service/versions.tf | 10 ++ 10 files changed, 304 insertions(+) create mode 100644 cloud/browse/service/.terraform.lock.hcl create mode 100644 cloud/browse/service/README.md create mode 100644 cloud/browse/service/application/main.tf create mode 100644 cloud/browse/service/application/outputs.tf create mode 100644 cloud/browse/service/application/variables.tf create mode 100644 cloud/browse/service/main.tf create mode 100644 cloud/browse/service/outputs.tf create mode 100644 cloud/browse/service/variables.tf create mode 100644 cloud/browse/service/versions.tf diff --git a/cloud/browse/README.md b/cloud/browse/README.md index 19efcc1d..cdcc2eea 100644 --- a/cloud/browse/README.md +++ b/cloud/browse/README.md @@ -13,3 +13,4 @@ To provision the cloud infrastructure: 1. Provision the [Terraform backend](tf-backend/README.md) 2. Provision the [Docker repository](docker-repository/README.md) 3. Provision the [storage bucket](storage-bucket/README.md) +4. Provision the [service](service/README.md) diff --git a/cloud/browse/service/.terraform.lock.hcl b/cloud/browse/service/.terraform.lock.hcl new file mode 100644 index 00000000..142d5132 --- /dev/null +++ b/cloud/browse/service/.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/google" { + version = "7.30.0" + constraints = "~> 7.30.0" + hashes = [ + "h1:1WM1iHQdoYqqx2LWOgWGQ83JbKwgTwAefdBNZA4tt4I=", + "zh:0cda2cc03f7bf000d9bc66bc0fab621de4c104b329cab348e0ceb6146ab27251", + "zh:2c75a1ea53b21646681e49fbdf0a599817c2f400f1e73d7779f0e3e1d230e6f3", + "zh:34ab9dab67230adaee6a9cd6861cba969555777ca6eb0ae1d2ac7b1f3cb73832", + "zh:45d5d7ee38fb7bf58dd19b774dd637f3cb9caef1d1930dde594467dde7fdea50", + "zh:651ffd36697d8268471d50d0fae664549b7f1e627c03d6e90f80172947f7b1d4", + "zh:8557db0beb201ba8ba70a7a38ba8d1ce9ffb9e98c616b89f6e3c2203e0528803", + "zh:b6a2e53809e0827cb7c47b1279c3511223898f7e3c1536f74ba057d99c72c2e9", + "zh:bf4aea9d1eb663df9d458c974d2e3f9ca7f724280a103706d0a2b1597593c7af", + "zh:c5f0160e0658d75b4a339b18f7a544e721f3750e26b97fc4887e98b8e085ffff", + "zh:e41a215491c64b535eda5585139dab0a32836e631b470cc520ceef4aa9ce7748", + "zh:e490061ab15c6053c651a7996a9c77cacdb83528ed49d0460c9621e93ad7d0cf", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/cloud/browse/service/README.md b/cloud/browse/service/README.md new file mode 100644 index 00000000..0d8e2b7a --- /dev/null +++ b/cloud/browse/service/README.md @@ -0,0 +1,96 @@ +# Service + +This root module provisions the resources required for the service. + +## Environments + +There are multiple environments that replicate the resources which are represented as Terraform workspaces: + +* `dev` +* `test` +* `prod` + +Set the target environment, for example: + +```bash +export ENVIRONMENT=dev +``` + +## Provisioning + +1. Change directory: + + ```bash + cd cloud/browse/service + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Initialise Terraform: + + ```bash + terraform init + ``` + +1. Create a Terraform workspace for the environment: + + ```bash + terraform workspace new ${ENVIRONMENT} + ``` + +1. Apply the changes: + + ```bash + terraform apply + ``` + +## Destroying + +1. Change directory: + + ```bash + cd cloud/browse/service + ``` + +1. Authenticate with Google Cloud: + + ```bash + gcloud auth application-default login + ``` + +1. Select the Terraform workspace for the environment: + + ```bash + terraform workspace select ${ENVIRONMENT} + ``` + +1. Disable deletion protection for the service by modifying `cloud/browse/service/application/main.tf`: + + ```diff + resource "google_cloud_run_v2_service" "browse_page" { + + deletion_protection = false + ... + } + ``` + +1. Apply the change: + + ```bash + terraform apply + ``` + +1. Revert the modification: + + ```bash + git checkout application/main.tf + ``` + +1. Delete the resources: + + ```bash + terraform destroy + ``` \ No newline at end of file diff --git a/cloud/browse/service/application/main.tf b/cloud/browse/service/application/main.tf new file mode 100644 index 00000000..53c0ff9c --- /dev/null +++ b/cloud/browse/service/application/main.tf @@ -0,0 +1,63 @@ +resource "google_cloud_run_v2_service" "browse_page" { + name = "plan" + project = var.project + location = var.region + ingress = "INGRESS_TRAFFIC_ALL" + + scaling { + min_instance_count = var.keep_idle ? 1 : 0 + max_instance_count = 10 + } + + template { + service_account = google_service_account.cloud_run_browse_page.email + + containers { + image = "${var.docker_repository_url}/plan:latest" + env { + name = "GCS_BUCKET" + value = var.gcs_bucket_name + } + env { + name = "USE_IAP" + value = var.use_iap + } + ports { + container_port = 8080 + } + } + } + + depends_on = [ + google_project_iam_member.cloud_run_artifact_registry_reader + ] +} + +resource "google_service_account" "cloud_run_browse_page" { + account_id = "cloud-run-browse-page" +} + +resource "google_cloud_run_v2_service_iam_binding" "browse_page_run_invoker" { + name = google_cloud_run_v2_service.browse_page.name + project = var.project + location = var.region + + role = "roles/run.invoker" + members = ["allUsers"] +} + +data "google_project" "main" { + project_id = var.project +} + +resource "google_project_iam_member" "cloud_run_artifact_registry_reader" { + project = var.docker_repository_project + role = "roles/artifactregistry.reader" + member = "serviceAccount:service-${data.google_project.main.number}@serverless-robot-prod.iam.gserviceaccount.com" +} + +resource "google_storage_bucket_iam_member" "cloud_run_storage_bucket_reader" { + bucket = var.gcs_bucket_name + role = "roles/storage.objectViewer" + member = "serviceAccount:${google_service_account.cloud_run_browse_page.email}" +} diff --git a/cloud/browse/service/application/outputs.tf b/cloud/browse/service/application/outputs.tf new file mode 100644 index 00000000..ba2da1e8 --- /dev/null +++ b/cloud/browse/service/application/outputs.tf @@ -0,0 +1,4 @@ +output "cloud_run_url" { + description = "Cloud Run URI" + value = google_cloud_run_v2_service.browse_page.uri +} diff --git a/cloud/browse/service/application/variables.tf b/cloud/browse/service/application/variables.tf new file mode 100644 index 00000000..eaba00d5 --- /dev/null +++ b/cloud/browse/service/application/variables.tf @@ -0,0 +1,34 @@ +variable "project" { + description = "GCP project" + type = string +} + +variable "region" { + description = "GCP region" + type = string +} + +variable "docker_repository_project" { + description = "Docker repository GCP project" + type = string +} + +variable "docker_repository_url" { + description = "Docker repository URL" + type = string +} + +variable "keep_idle" { + description = "Whether to keep an instance idle to prevent cold starts" + type = bool +} + +variable "gcs_bucket_name" { + description = "GCS bucket name for assets" + type = string +} + +variable "use_iap" { + description = "Boolean on whether to use IAP or not" + type = bool +} diff --git a/cloud/browse/service/main.tf b/cloud/browse/service/main.tf new file mode 100644 index 00000000..53b2b350 --- /dev/null +++ b/cloud/browse/service/main.tf @@ -0,0 +1,59 @@ +terraform { + backend "gcs" { + bucket = "dft-rlg-atip-dev-tf-backend" + prefix = "service" + } +} + +provider "google" { + project = local.project +} + +locals { + env = terraform.workspace + project = "${var.project_prefix}-${local.env}" + config = { + dev = { + use_iap = false + keep_idle = false + } + } +} + + +data "terraform_remote_state" "docker_repository" { + backend = "gcs" + config = { + bucket = "${var.project_prefix}-dev-tf-backend" + prefix = "docker-repository" + } +} + +data "terraform_remote_state" "storage_bucket" { + backend = "gcs" + config = { + bucket = "${var.project_prefix}-dev-tf-backend" + prefix = "storage-bucket" + } + workspace = local.env +} + +resource "google_project_service" "run" { + project = local.project + service = "run.googleapis.com" +} + +module "application" { + source = "./application" + project = local.project + region = var.location + docker_repository_project = data.terraform_remote_state.docker_repository.outputs.project + docker_repository_url = data.terraform_remote_state.docker_repository.outputs.url + keep_idle = local.config[local.env].keep_idle + gcs_bucket_name = data.terraform_remote_state.storage_bucket.outputs.bucket_name + use_iap = local.config[local.env].use_iap + + depends_on = [ + google_project_service.run, + ] +} diff --git a/cloud/browse/service/outputs.tf b/cloud/browse/service/outputs.tf new file mode 100644 index 00000000..5b8417aa --- /dev/null +++ b/cloud/browse/service/outputs.tf @@ -0,0 +1,4 @@ +output "cloud_run_url" { + description = "Cloud run URI" + value = module.application.cloud_run_url +} diff --git a/cloud/browse/service/variables.tf b/cloud/browse/service/variables.tf new file mode 100644 index 00000000..7ba8fe2d --- /dev/null +++ b/cloud/browse/service/variables.tf @@ -0,0 +1,11 @@ +variable "project_prefix" { + description = "GCP project prefix" + type = string + default = "dft-rlg-atip" +} + +variable "location" { + description = "GCP location" + type = string + default = "europe-west1" +} diff --git a/cloud/browse/service/versions.tf b/cloud/browse/service/versions.tf new file mode 100644 index 00000000..11959c07 --- /dev/null +++ b/cloud/browse/service/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = "~> 1.15.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 7.30.0" + } + } +} From edda0cd485034698239e02e76b179c366b24484f Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Fri, 26 Jun 2026 13:45:48 +0100 Subject: [PATCH 7/8] Deploy new cloud run revision after every CI run to dev --- .github/workflows/ci.yml | 13 +++- .github/workflows/deploy.yml | 61 +++++++++++++++++++ cloud/browse/service/README.md | 13 ++++ cloud/browse/service/application/outputs.tf | 5 ++ .../service/github-action-deploy/main.tf | 34 +++++++++++ .../service/github-action-deploy/outputs.tf | 5 ++ .../service/github-action-deploy/variables.tf | 14 +++++ cloud/browse/service/main.tf | 7 +++ cloud/browse/service/outputs.tf | 6 ++ 9 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 cloud/browse/service/github-action-deploy/main.tf create mode 100644 cloud/browse/service/github-action-deploy/outputs.tf create mode 100644 cloud/browse/service/github-action-deploy/variables.tf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e78c2ec..66955a68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ env: IMAGE_NAME: plan jobs: - check: + build: timeout-minutes: 60 runs-on: ubuntu-22.04 steps: @@ -91,3 +91,14 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} push: true + + deploy: + needs: build + uses: ./.github/workflows/deploy.yml + with: + environment: Dev + tag: ${{ needs.build.outputs.version }} + # only run deploy for CI runs on main + if: github.event_name != 'pull_request' + secrets: + GCP_CREDENTIALS_DEPLOY: ${{ secrets.GCP_CREDENTIALS_DEPLOY }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..01164e9d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,61 @@ +name: Deploy +run-name: 'Deploy ${{ inputs.tag }} to ${{ inputs.environment }}' + +on: + workflow_call: + inputs: + environment: + description: Environment + type: string + required: true + + tag: + description: Docker image tag + type: string + required: true + + secrets: + GCP_CREDENTIALS_DEPLOY: + required: true + + workflow_dispatch: + inputs: + environment: + description: Environment + type: environment + required: true + + tag: + description: Docker image tag + type: string + required: true + default: latest + +permissions: + contents: read + +env: + IMAGE_PROJECT: dft-rlg-atip-dev + IMAGE_LOCATION: europe-west1 + IMAGE_REPOSITORY_ID: docker + IMAGE_NAME: plan + SERVICE_LOCATION: europe-west1 + SERVICE_NAME: plan + +jobs: + + deploy: + runs-on: ubuntu-24.04 + environment: ${{ inputs.environment }} + steps: + - name: Authenticate with Google Cloud + uses: google-github-actions/auth@v3 + with: + credentials_json: '${{ secrets.GCP_CREDENTIALS_DEPLOY }}' + + - name: Deploy image + uses: google-github-actions/deploy-cloudrun@v3 + with: + service: ${{ env.SERVICE_NAME }} + image: ${{ env.IMAGE_LOCATION }}-docker.pkg.dev/${{ env.IMAGE_PROJECT }}/${{ env.IMAGE_REPOSITORY_ID }}/${{ env.IMAGE_NAME }}:${{ inputs.tag }} + region: ${{ env.SERVICE_LOCATION }} diff --git a/cloud/browse/service/README.md b/cloud/browse/service/README.md index 0d8e2b7a..88df99c0 100644 --- a/cloud/browse/service/README.md +++ b/cloud/browse/service/README.md @@ -47,6 +47,19 @@ export ENVIRONMENT=dev ```bash terraform apply ``` +## Configuring GitHub Actions + +For the Dev environment only, configure the [CI workflow](../../.github/workflows/ci.yml) with credentials to deploy +images to the service: + +1. Obtain the Cloud Run service account private key: + + ```bash + terraform output -raw github_action_deploy_private_key + ``` + +1. [Set the GitHub Actions environment secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-an-environment) `GCP_CREDENTIALS_DEPLOY` to the private key + ## Destroying diff --git a/cloud/browse/service/application/outputs.tf b/cloud/browse/service/application/outputs.tf index ba2da1e8..bc0645e1 100644 --- a/cloud/browse/service/application/outputs.tf +++ b/cloud/browse/service/application/outputs.tf @@ -2,3 +2,8 @@ output "cloud_run_url" { description = "Cloud Run URI" value = google_cloud_run_v2_service.browse_page.uri } + +output "service_account_id" { + description = "Cloud Run service account ID" + value = google_service_account.cloud_run_browse_page.id +} diff --git a/cloud/browse/service/github-action-deploy/main.tf b/cloud/browse/service/github-action-deploy/main.tf new file mode 100644 index 00000000..2480dcb2 --- /dev/null +++ b/cloud/browse/service/github-action-deploy/main.tf @@ -0,0 +1,34 @@ +resource "google_service_account" "main" { + project = var.project + account_id = "github-action-deploy" + display_name = "Service account for deploy GitHub Action" +} + +resource "google_project_iam_member" "service_account_token_creator" { + project = var.project + role = "roles/iam.serviceAccountTokenCreator" + member = "serviceAccount:${google_service_account.main.email}" +} + +resource "google_project_iam_member" "run_admin" { + project = var.project + role = "roles/run.admin" + member = "serviceAccount:${google_service_account.main.email}" +} + +resource "google_service_account_iam_member" "service_account_user" { + service_account_id = var.cloud_run_service_account_id + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${google_service_account.main.email}" +} + +resource "google_project_iam_member" "artifact_registry_reader" { + project = var.docker_repository_project + role = "roles/artifactregistry.reader" + member = "serviceAccount:${google_service_account.main.email}" +} + +resource "google_service_account_key" "main" { + service_account_id = google_service_account.main.name + public_key_type = "TYPE_X509_PEM_FILE" +} diff --git a/cloud/browse/service/github-action-deploy/outputs.tf b/cloud/browse/service/github-action-deploy/outputs.tf new file mode 100644 index 00000000..04206a58 --- /dev/null +++ b/cloud/browse/service/github-action-deploy/outputs.tf @@ -0,0 +1,5 @@ +output "private_key" { + description = "Service account key for deploy GitHub Action service account" + value = google_service_account_key.main.private_key + sensitive = true +} diff --git a/cloud/browse/service/github-action-deploy/variables.tf b/cloud/browse/service/github-action-deploy/variables.tf new file mode 100644 index 00000000..f5a88f71 --- /dev/null +++ b/cloud/browse/service/github-action-deploy/variables.tf @@ -0,0 +1,14 @@ +variable "project" { + description = "GCP project" + type = string +} + +variable "docker_repository_project" { + description = "Docker repository GCP project" + type = string +} + +variable "cloud_run_service_account_id" { + description = "Cloud Run service account ID" + type = string +} diff --git a/cloud/browse/service/main.tf b/cloud/browse/service/main.tf index 53b2b350..b2fc09d5 100644 --- a/cloud/browse/service/main.tf +++ b/cloud/browse/service/main.tf @@ -57,3 +57,10 @@ module "application" { google_project_service.run, ] } + +module "github_action_deploy" { + source = "./github-action-deploy" + project = local.project + docker_repository_project = data.terraform_remote_state.docker_repository.outputs.project + cloud_run_service_account_id = module.application.service_account_id +} diff --git a/cloud/browse/service/outputs.tf b/cloud/browse/service/outputs.tf index 5b8417aa..d723c5c7 100644 --- a/cloud/browse/service/outputs.tf +++ b/cloud/browse/service/outputs.tf @@ -2,3 +2,9 @@ output "cloud_run_url" { description = "Cloud run URI" value = module.application.cloud_run_url } + +output "github_action_deploy_private_key" { + description = "Service account key for deploy GitHub Action service account" + value = module.github_action_deploy.private_key + sensitive = true +} From 2a3fb38f6f26656b6edb64f7d73293c2443e1f39 Mon Sep 17 00:00:00 2001 From: Alex Coleman Date: Fri, 26 Jun 2026 15:22:17 +0100 Subject: [PATCH 8/8] Extract a docker tag when CI is triggered by a pull request --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66955a68..a1aa0ca4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,7 @@ jobs: tags: | type=semver,pattern={{version}} type=ref,event=branch,enable={{is_not_default_branch}} + type=ref,event=pr,enable={{is_not_default_branch}} type=raw,value=latest,enable={{is_default_branch}} - name: Build and push image