diff --git a/.github/workflows/gbfs-validator-deployer.yml b/.github/workflows/gbfs-validator-deployer.yml new file mode 100644 index 0000000..5ae7c31 --- /dev/null +++ b/.github/workflows/gbfs-validator-deployer.yml @@ -0,0 +1,241 @@ +# +# MobilityData 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Reusable workflow: builds the Docker image and deploys via Terraform. +# Called by gbfs-validator-staging.yml and gbfs-validator-prod.yml. +name: GBFS Validator Terraform Deployment + +on: + workflow_call: + secrets: + GCP_GBFS_VALIDATOR_SA_KEY: + description: GCP service account JSON key for deployment + required: true + inputs: + ENVIRONMENT: + description: Deployment environment (dev, qa, or prod) + required: true + type: string + BUCKET_NAME: + description: Full GCS bucket name for Terraform remote state + required: true + type: string + PROJECT_ID: + description: GCP project ID + required: true + type: string + REGION: + description: GCP region + required: true + type: string + DEPLOYER_SERVICE_ACCOUNT: + description: Service account email used by Terraform for impersonation + required: true + type: string + ARTIFACT_REGISTRY_REPO: + description: Artifact Registry repository name (e.g. gbfs-validator-staging) + required: true + type: string + GBFS_API_IMAGE_VERSION: + description: Docker image version tag to build and deploy (empty = latest from Maven Central) + required: false + type: string + default: '' + TF_APPLY: + description: Whether to apply Terraform changes (set false to plan-only) + required: true + type: boolean + +jobs: + docker-build-publish: + runs-on: ubuntu-latest + permissions: write-all + outputs: + resolved_version: ${{ steps.download_jar.outputs.resolved_version }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_GBFS_VALIDATOR_SA_KEY }} + + - name: GCloud Setup + uses: google-github-actions/setup-gcloud@v2 + + - name: Login to Google Artifact Registry + # Use _json_key (not _json_key_base64) because the secret stores raw JSON. + uses: docker/login-action@v3 + with: + registry: ${{ inputs.REGION }}-docker.pkg.dev + username: _json_key + password: ${{ secrets.GCP_GBFS_VALIDATOR_SA_KEY }} + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'zulu' + + - name: Download validator JAR from Maven Central + id: download_jar + # Uses Maven dependency:copy via the pom in gbfs-validator/. + # Maven handles release/snapshot resolution, checksum verification, + # and snapshot timestamp mapping natively. + run: | + VERSION_ARG="${{ inputs.GBFS_API_IMAGE_VERSION }}" + RESOLVED="${VERSION_ARG:-RELEASE}" + + echo "⬇️ Downloading gbfs-validator-java-api version=${RESOLVED} ..." + + # output.directory is relative to pom basedir (gbfs-validator/). + # stripVersion defaults to false, so the jar keeps its version suffix. + mvn -f gbfs-validator/pom.xml dependency:copy \ + -Dartifact.version="$RESOLVED" \ + -Doutput.directory=. \ + -Dmdep.overWriteIfNewer=true \ + -B -ntp + + # Find the versioned jar and extract the actual version from its name + JAR_NAME=$(ls gbfs-validator/gbfs-validator-java-api-*.jar 2>/dev/null | head -1) + if [[ -z "$JAR_NAME" ]]; then + echo "❌ Download failed — no JAR found" + exit 1 + fi + ACTUAL_VERSION=$(basename "$JAR_NAME" | sed 's/gbfs-validator-java-api-//;s/\.jar$//') + + # Rename to fixed name expected by Dockerfile + mv "$JAR_NAME" gbfs-validator/gbfs-validator-java-api.jar + + FILE_SIZE=$(wc -c < gbfs-validator/gbfs-validator-java-api.jar | tr -d ' ') + echo "✅ Downloaded ${ACTUAL_VERSION} (${FILE_SIZE} bytes)" + echo "resolved_version=${ACTUAL_VERSION}" >> "$GITHUB_OUTPUT" + + - name: Create Artifact Registry repo if not exists + run: | + REPO="${{ inputs.ARTIFACT_REGISTRY_REPO }}" + if ! gcloud artifacts repositories describe "$REPO" \ + --project="${{ inputs.PROJECT_ID }}" \ + --location="${{ inputs.REGION }}" &>/dev/null; then + echo "Creating Artifact Registry repo: $REPO" + gcloud artifacts repositories create "$REPO" \ + --repository-format=docker \ + --location="${{ inputs.REGION }}" \ + --project="${{ inputs.PROJECT_ID }}" \ + --description="GBFS Validator Docker images" + else + echo "Repo $REPO already exists, skipping." + fi + + - name: Build & Push Docker Image + # -repo_name is the shared AR repo; -environment is the subdirectory within it. + run: | + scripts/docker-build-validator.sh \ + --push \ + -project_id ${{ inputs.PROJECT_ID }} \ + -region ${{ inputs.REGION }} \ + -repo_name ${{ inputs.ARTIFACT_REGISTRY_REPO }} \ + -environment ${{ inputs.ENVIRONMENT }} \ + -version ${{ steps.download_jar.outputs.resolved_version }} + + terraform-deploy: + runs-on: ubuntu-latest + permissions: write-all + needs: docker-build-publish + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_GBFS_VALIDATOR_SA_KEY }} + + - name: GCloud Setup + uses: google-github-actions/setup-gcloud@v2 + + - name: Set Variables + # Export workflow inputs as env vars for replace-variables.sh. + run: | + echo "BUCKET_NAME=${{ inputs.BUCKET_NAME }}" >> $GITHUB_ENV + echo "PROJECT_ID=${{ inputs.PROJECT_ID }}" >> $GITHUB_ENV + echo "REGION=${{ inputs.REGION }}" >> $GITHUB_ENV + echo "ENVIRONMENT=${{ inputs.ENVIRONMENT }}" >> $GITHUB_ENV + echo "DEPLOYER_SERVICE_ACCOUNT=${{ inputs.DEPLOYER_SERVICE_ACCOUNT }}" >> $GITHUB_ENV + echo "GBFS_API_IMAGE_VERSION=${{ needs.docker-build-publish.outputs.resolved_version }}" >> $GITHUB_ENV + echo "ARTIFACT_REGISTRY_REPO=${{ inputs.ARTIFACT_REGISTRY_REPO }}" >> $GITHUB_ENV + + - name: Populate Terraform Variables + # Generates backend.conf and vars.tfvars from *.rename_me templates by + # substituting {{VARIABLE}} placeholders with env var values. + run: | + scripts/replace-variables.sh \ + -in_file infra/backend.conf.rename_me \ + -out_file infra/backend.conf \ + -no_quotes \ + -variables BUCKET_NAME,ENVIRONMENT + scripts/replace-variables.sh \ + -in_file infra/vars.tfvars.rename_me \ + -out_file infra/vars.tfvars \ + -variables PROJECT_ID,REGION,ENVIRONMENT,DEPLOYER_SERVICE_ACCOUNT,GBFS_API_IMAGE_VERSION,ARTIFACT_REGISTRY_REPO + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.5.3 + terraform_wrapper: false + + - name: Terraform Init + run: | + cd infra + terraform init -backend-config=backend.conf + + - name: Terraform Plan + run: | + cd infra + terraform plan -var-file=vars.tfvars -out=tf.plan + terraform show -no-color tf.plan > terraform-plan.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Persist Terraform Plan + uses: actions/upload-artifact@v4 + with: + name: terraform-plan.txt + path: infra/terraform-plan.txt + overwrite: true + + - name: Terraform Apply + if: ${{ inputs.TF_APPLY }} + run: | + cd infra + terraform apply -auto-approve tf.plan + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Print API endpoint + if: ${{ inputs.TF_APPLY }} + run: | + echo "" + echo "==========================================" + echo " ✅ ${{ inputs.ENVIRONMENT }} environment deployed" + if [[ "${{ inputs.ENVIRONMENT }}" == "prod" ]]; then + echo " 🔗 https://gbfs.api.mobilitydatabase.org/validate" + else + echo " 🔗 https://${{ inputs.ENVIRONMENT }}.gbfs.api.mobilitydatabase.org/validate" + fi + echo "==========================================" diff --git a/.github/workflows/gbfs-validator-prod.yml b/.github/workflows/gbfs-validator-prod.yml new file mode 100644 index 0000000..76d6431 --- /dev/null +++ b/.github/workflows/gbfs-validator-prod.yml @@ -0,0 +1,43 @@ +# +# MobilityData 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Deploys the GBFS Validator API to the PROD environment. +# Triggered manually. If app_version is omitted, the latest release from +# Maven Central is downloaded automatically. +name: Deploy GBFS Validator - PROD + +on: + workflow_dispatch: + inputs: + app_version: + description: "gbfs-validator-java version to deploy (e.g. 2.0.68). Leave empty for latest." + required: false + type: string + +jobs: + gbfs-validator-deployment: + uses: ./.github/workflows/gbfs-validator-deployer.yml + with: + ENVIRONMENT: prod + BUCKET_NAME: ${{ vars.PROD_GBFS_VALIDATOR_TF_STATE_BUCKET }} + PROJECT_ID: ${{ vars.PROD_GBFS_VALIDATOR_PROJECT_ID }} + REGION: ${{ vars.GBFS_VALIDATOR_REGION }} + DEPLOYER_SERVICE_ACCOUNT: ${{ vars.PROD_GBFS_VALIDATOR_DEPLOYER_SA }} + GBFS_API_IMAGE_VERSION: ${{ inputs.app_version }} + ARTIFACT_REGISTRY_REPO: gbfs-validator + TF_APPLY: true + secrets: + GCP_GBFS_VALIDATOR_SA_KEY: ${{ secrets.PROD_GCP_GBFS_VALIDATOR_SA_KEY }} diff --git a/.github/workflows/gbfs-validator-staging.yml b/.github/workflows/gbfs-validator-staging.yml new file mode 100644 index 0000000..56ead1a --- /dev/null +++ b/.github/workflows/gbfs-validator-staging.yml @@ -0,0 +1,81 @@ +# +# MobilityData 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Deploys to the gbfs-validator-staging GCP project. +# Environment is resolved from the trigger: +# - pull_request → dev +# - push to main → qa +# - workflow_dispatch → user-specified (dev or qa) +# All staging environments share the same GCP project, SA key, and TF state bucket. +# Each environment gets its own state prefix and uniquely-named GCP resources. +name: Deploy GBFS Validator - Staging + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + inputs: + target_environment: + description: "Environment to deploy to" + type: choice + default: dev + options: + - dev + - qa + app_version: + description: "Version to deploy (e.g. 2.0.68-SNAPSHOT or 2.0.68). Leave empty for latest release." + required: false + type: string + +jobs: + # Resolves the environment name from the trigger context + # Mapping: + # pull_request → "dev" (feature-branch validation) + # push to main → "qa" (post-merge integration) + # workflow_dispatch → value chosen by the user (dev or qa) + resolve-environment: + runs-on: ubuntu-latest + outputs: + environment: ${{ steps.resolve.outputs.environment }} + steps: + - name: Resolve environment from trigger + id: resolve + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "environment=dev" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "push" ]]; then + echo "environment=qa" >> $GITHUB_OUTPUT + else + echo "environment=${{ inputs.target_environment }}" >> $GITHUB_OUTPUT + fi + echo "Resolved environment: $(grep '^environment=' $GITHUB_OUTPUT | cut -d= -f2)" + + deploy: + needs: resolve-environment + uses: ./.github/workflows/gbfs-validator-deployer.yml + with: + ENVIRONMENT: ${{ needs.resolve-environment.outputs.environment }} + BUCKET_NAME: ${{ vars.STAGING_GBFS_VALIDATOR_TF_STATE_BUCKET }} + PROJECT_ID: ${{ vars.STAGING_GBFS_VALIDATOR_PROJECT_ID }} + REGION: ${{ vars.GBFS_VALIDATOR_REGION }} + DEPLOYER_SERVICE_ACCOUNT: ${{ vars.STAGING_GBFS_VALIDATOR_DEPLOYER_SA }} + GBFS_API_IMAGE_VERSION: ${{ inputs.app_version }} + ARTIFACT_REGISTRY_REPO: gbfs-validator-staging + TF_APPLY: true + secrets: + GCP_GBFS_VALIDATOR_SA_KEY: ${{ secrets.STAGING_GCP_GBFS_VALIDATOR_SA_KEY }} diff --git a/.gitignore b/.gitignore index eb8b428..111aef7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# IntelliJ IDEA project files +.idea/ + # Local .terraform directories .terraform/ @@ -39,3 +42,6 @@ terraform.rc # Ignore terraform local files backend.conf backend-*.conf + +# Maven/CI temporary download directory +gbfs-validator/tmp-download/ diff --git a/README.md b/README.md index 6516169..531d2ef 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,65 @@ Access to any MobilityData-managed Google Cloud environment is **restricted** un --- +## Deploying + +Deployments are fully automated via GitHub Actions. There is nothing to build or compile manually — the CI pipeline handles everything. + +| Environment | GCP Project | Trigger | URL | +|---|---|---|---| +| `dev` | `gbfs-validator-staging` | PR opened/updated, or manual (see below) | `https://dev.gbfs.api.mobilitydatabase.org/validate` | +| `qa` | `gbfs-validator-staging` | Push to `main`, or manual (see below) | `https://qa.gbfs.api.mobilitydatabase.org/validate` | +| `prod` | `gbfs-validator-prod` | Manual only (see below) | `https://gbfs.api.mobilitydatabase.org/validate` | + +> **Note:** `dev` and `qa` share the same GCP project (`gbfs-validator-staging`). Each environment gets its own Cloud Run service, Artifact Registry path, runtime service account, and Terraform state. `prod` lives in a separate dedicated project. + +### Manual deployment + +The staging and prod workflows can be triggered manually from the [GitHub Actions UI](https://github.com/MobilityData/gbfs-validator-java-infra/actions). + +**Staging** (`gbfs-validator-staging.yml`): +- `target_environment` — choose `dev` (default) or `qa` +- `app_version` — optional. The version of `gbfs-validator-java` to deploy (e.g. `2.0.68`). Can also be a snapshot version (e.g. `2.0.69-SNAPSHOT`). Leave empty to deploy the latest release from Maven Central. + +**Prod** (`gbfs-validator-prod.yml`): +- `app_version` — optional, same as above. + +### How a deployment works + +Both workflows call the shared reusable deployer (`gbfs-validator-deployer.yml`) which: +1. Authenticates to GCP using the deployer service account JSON key +2. Builds and pushes a Docker image to Artifact Registry +3. Runs `terraform apply` to deploy or update the Cloud Run service + +### Terraform State Isolation + +Each environment's state is stored under its own prefix in a shared bucket: +- Staging: `mobilitydata-gbfs-validator-state-staging/{env}/terraform/state` +- Prod: `mobilitydata-gbfs-validator-state-prod/prod/terraform/state` + +This means `terraform apply` for one environment cannot affect another. + +### Required GitHub Actions Secrets + +| Secret | Description | +|---|---| +| `STAGING_GCP_GBFS_VALIDATOR_SA_KEY` | Deployer SA JSON key for staging (all staging envs) | +| `PROD_GCP_GBFS_VALIDATOR_SA_KEY` | Deployer SA JSON key for prod | + +### Required GitHub Actions Variables + +| Variable | Value | Description | +|---|---|---| +| `GBFS_VALIDATOR_REGION` | `northamerica-northeast1` | GCP region (shared) | +| `STAGING_GBFS_VALIDATOR_PROJECT_ID` | `gbfs-validator-staging` | Staging GCP project ID | +| `STAGING_GBFS_VALIDATOR_TF_STATE_BUCKET` | `mobilitydata-gbfs-validator-state-staging` | Staging TF state bucket (shared) | +| `STAGING_GBFS_VALIDATOR_DEPLOYER_SA` | `gbfs-deployer-service-account@gbfs-validator-staging.iam.gserviceaccount.com` | Staging deployer SA | +| `PROD_GBFS_VALIDATOR_PROJECT_ID` | `gbfs-validator-prod` | Prod GCP project ID | +| `PROD_GBFS_VALIDATOR_TF_STATE_BUCKET` | `mobilitydata-gbfs-validator-state-prod` | Prod TF state bucket | +| `PROD_GBFS_VALIDATOR_DEPLOYER_SA` | `gbfs-deployer-service-account@gbfs-validator-prod.iam.gserviceaccount.com` | Prod deployer SA | + +--- + ## Setting Up a New GCP Environment > _"All roads lead to Rome!"_ @@ -17,7 +76,8 @@ For more information, refer to the [Google Cloud Platform documentation](https:/ ## Initial Project and Remote State Setup > _These instructions apply when creating a **new** environment._ -> For illustration purposes, the examples below assume the GCP project is `gbfs-validator-staging` and the application environment is `dev`. +> `dev` and `qa` already exist in `gbfs-validator-staging`. These steps apply when setting up `prod` (project `gbfs-validator-prod`) or any future environment. +> For illustration purposes, the examples below use `gbfs-validator-staging` as the project and `dev` as the environment — substitute accordingly. ### 1. Create a GCP Project @@ -40,7 +100,8 @@ These credentials will be passed as Terraform variables. ### 5. Create SSL Certificates -Configure Google-managed or self-managed certificates for the HTTPS Load Balancer. +Google-managed SSL certificates are created automatically by `scripts/setup-environment.sh` (step 11 below). +They provision once the DNS A/AAAA records are in place and the load balancer is deployed. ### 6. Enable and Configure Identity Platform @@ -69,42 +130,74 @@ gcloud storage buckets create gs://mobilitydata-gbfs-validator-state-staging \ ### 10. Configure the Terraform Backend -Copy the file `backend.conf.rename_me` and rename it to: +Copy `infra/backend.conf.rename_me` to `infra/backend.conf` and populate it. +`BUCKET_NAME` is the full GCS bucket name (e.g. `mobilitydata-gbfs-validator-state-staging`). +`ENVIRONMENT` is the environment name (e.g. `dev`, `qa`) — it becomes the state prefix to isolate each environment's state within the shared bucket. + +### 11. Run the Environment Setup Script ```bash -backend-dev.conf +# Staging (dev or qa): +scripts/setup-environment.sh gbfs-validator-staging dev + +# Prod (uses a separate AR repo): +scripts/setup-environment.sh gbfs-validator-prod prod northamerica-northeast1 gbfs-validator ``` -Populate it with valid values matching the GCP project and bucket. +This script: +- Creates the deployer service account and grants IAM roles +- Enables required Google APIs +- Creates (or verifies) the shared Artifact Registry repository +- Creates global static IPv4 and IPv6 addresses for the load balancer (`{env}-lb-ipv4`, `{env}-lb-ipv6`) +- Creates a Google-managed SSL certificate for `{env}.gbfs.api.mobilitydatabase.org` -### 11. Create the Deployer Service Account +At the end it prints the IP addresses you will need for DNS. -```bash -gcloud iam service-accounts create gbfs-deployer-service-account \ - --display-name="GBFS Terraform Deployer" -``` +### 11a. Create DNS Records -### 12. Run the Environment Setup Script +After running the setup script, ask your DNS administrator (Cloudflare) to create: + +| Type | Name | Value | +|---|---|---| +| `A` | `{env}.gbfs.api.mobilitydatabase.org` | IPv4 printed by the script | +| `AAAA` | `{env}.gbfs.api.mobilitydatabase.org` | IPv6 printed by the script | + +> **Note:** DNS proxy must be **disabled** (grey cloud in Cloudflare) to allow Google-managed cert provisioning. +> The SSL certificate will complete provisioning automatically once DNS resolves to the load balancer IP. + +### 12. Generate a Deployer Service Account Key (for CI) ```bash -../scripts/setup-environment.sh gbfs-validator-staging dev +gcloud iam service-accounts keys create deployer-key.json \ + --iam-account=gbfs-deployer-service-account@gbfs-validator-staging.iam.gserviceaccount.com \ + --project=gbfs-validator-staging ``` -### 13. Initialize Terraform +Store the contents of `deployer-key.json` as the GitHub secret `DEV_GCP_GBFS_VALIDATOR_SA_KEY`. Delete the local file after. + +### 13. Configure Terraform Variables + +Copy `infra/vars.tfvars.rename_me` to `infra/vars.tfvars` and populate it for local runs. +In CI, this is done automatically by `scripts/replace-variables.sh`. + +### 14. Initialize Terraform ```bash -terraform init -backend-config=backend-dev.conf +cd infra +terraform init -backend-config=backend.conf ``` -### 14. Build and push the GBFS API docker +### 15. Build and push the GBFS API Docker image + ```bash -../scripts/docker-build-validator.sh --push -version dev-$(($(date +%s)/60)) +scripts/docker-build-validator.sh --push -version dev-$(($(date +%s)/60)) ``` -### 14. Apply the Terraform Plan +### 16. Apply the Terraform Plan ```bash -terraform apply -var="environment=dev" -var=-"gbfs_api_image_version=<>" +cd infra +terraform apply -var-file=vars.tfvars ``` --- @@ -113,11 +206,11 @@ terraform apply -var="environment=dev" -var=-"gbfs_api_image_version=< + + + 4.0.0 + + org.mobilitydata.infra + gbfs-validator-jar-downloader + 1.0.0 + pom + + Downloads the gbfs-validator-java-api fat JAR for Docker packaging + + + + RELEASE + org.mobilitydata + gbfs-validator-java-api + ${project.basedir} + + + + + + maven-cent1ral-snapshots + https://central.sonatype.com/repository/maven-snapshots + + false + + + true + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + ${artifact.groupId}:${artifact.artifactId}:${artifact.version}:jar + ${output.directory} + false + + + + + diff --git a/infra/backend.conf.rename_me b/infra/backend.conf.rename_me index 987ef5e..a32d4bd 100644 --- a/infra/backend.conf.rename_me +++ b/infra/backend.conf.rename_me @@ -23,6 +23,14 @@ # - Execute: terraform init -backend-conf=backend.conf # - Enjoy coding! # More info: https://developer.hashicorp.com/terraform/language/state/remote +# +# CI/CD: {{BUCKET_NAME}} is the full GCS bucket name for Terraform remote state. +# Staging uses one shared bucket (e.g. mobilitydata-gbfs-validator-state-staging); +# prod uses its own (e.g. mobilitydata-gbfs-validator-state-prod). +# {{ENVIRONMENT}} isolates each environment's state within the bucket +# (e.g. dev/terraform/state, qa/terraform/state). +# Both are substituted at runtime by scripts/replace-variables.sh using values +# from GitHub Actions variables. -bucket = mobilitydata-gbfs-validator-state-{{BUCKET_NAME}} -prefix = {{OBJECT_PREFIX}} +bucket = "{{BUCKET_NAME}}" +prefix = "{{ENVIRONMENT}}/terraform/state" diff --git a/infra/cloud-run-service/cloud-run-main.tf b/infra/cloud-run-service/cloud-run-main.tf index 014fbfc..524f993 100644 --- a/infra/cloud-run-service/cloud-run-main.tf +++ b/infra/cloud-run-service/cloud-run-main.tf @@ -28,8 +28,8 @@ terraform { } locals { - gbfs_validator_config = jsondecode(file("${path.module}/../../gbfs-validator/function_config.json")) - artifact_registry_repo = "gbfs-validator-${var.environment}" + gbfs_validator_config = jsondecode(file("${path.module}/../../gbfs-validator/function_config.json")) + artifact_registry_repo = "${var.artifact_registry_repo}/${var.environment}" } provider "google" { @@ -45,9 +45,9 @@ data "archive_file" "source_zip" { } resource "google_cloud_run_v2_service" "gbfs_validator_api" { - name = "${var.environment}-${local.gbfs_validator_config.name_suffix}" + name = "${var.environment}-${local.gbfs_validator_config.name_suffix}" location = var.gcp_region - ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" + ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" template { service_account = var.gbfs_validator_service_account_email diff --git a/infra/cloud-run-service/outputs.tf b/infra/cloud-run-service/outputs.tf index 8f788a6..f520d43 100644 --- a/infra/cloud-run-service/outputs.tf +++ b/infra/cloud-run-service/outputs.tf @@ -21,5 +21,5 @@ output "function_url" { output "cloud_run_service_name" { description = "Name of the cloud run resource" - value = google_cloud_run_v2_service.gbfs_validator_api.name + value = google_cloud_run_v2_service.gbfs_validator_api.name } \ No newline at end of file diff --git a/infra/cloud-run-service/vars.tf b/infra/cloud-run-service/vars.tf index a6c76ad..61d1dbd 100644 --- a/infra/cloud-run-service/vars.tf +++ b/infra/cloud-run-service/vars.tf @@ -17,13 +17,13 @@ variable "project_id" { type = string description = "GCP project ID" - default = "gbfs-validator-staging" + default = "gbfs-validator-staging" } variable "gcp_region" { type = string description = "GCP region" - default = "northamerica-northeast1" + default = "northamerica-northeast1" } variable "environment" { @@ -32,35 +32,41 @@ variable "environment" { } variable "gbfs_validator_app_name" { - type = string + type = string description = "App name for better resource management and cost tracking" - default = "gbfs_validator" + default = "gbfs_validator" } variable "gbfs_api_service" { - type = string + type = string description = "GBFS API service name as defined in the artifact registry" - default = "gbfs_validator_api" + default = "gbfs_validator_api" } variable "gbfs_api_image_version" { - type = string + type = string description = "GBFS API image version" } variable "java_runtime" { - type = string + type = string description = "Java function runtime" - default = "java17" + default = "java17" } # This is a temporary patch until the publising of the Java jar is defined variable "jar_file_name" { - type = string - default = "gbfs-validator-java-api.jar" + type = string + default = "gbfs-validator-java-api.jar" } variable "gbfs_validator_service_account_email" { - type = string - description = "Service account use for runtime operations" + type = string + description = "Service account use for runtime operations" +} + +variable "artifact_registry_repo" { + type = string + description = "Artifact Registry repository name (e.g. gbfs-validator-staging)" + default = "gbfs-validator-staging" } \ No newline at end of file diff --git a/infra/load-balancer/load-balancer-main.tf b/infra/load-balancer/load-balancer-main.tf index faf8e28..9d41767 100644 --- a/infra/load-balancer/load-balancer-main.tf +++ b/infra/load-balancer/load-balancer-main.tf @@ -17,12 +17,12 @@ # Global static IP data "google_compute_global_address" "lb_ipv6" { project = var.project_id - name = "${var.environment}-lb-ipv6" + name = "${var.environment}-lb-ipv6" } data "google_compute_global_address" "lb_ipv4" { project = var.project_id - name = "${var.environment}-lb-ipv4" + name = "${var.environment}-lb-ipv4" } # Serverless NEG for Cloud Run @@ -39,8 +39,8 @@ resource "google_compute_region_network_endpoint_group" "neg" { # Cloud Armor Security Policy (rate limiting) resource "google_compute_security_policy" "armor" { - project = var.project_id - name = "${var.environment}-rate-limit" + project = var.project_id + name = "${var.environment}-rate-limit" description = "Rate limiting for public access" rule { @@ -60,14 +60,14 @@ resource "google_compute_security_policy" "armor" { count = var.rate_limit_count interval_sec = var.rate_limit_interval_sec } - ban_duration_sec = var.ban_duration_sec + ban_duration_sec = var.ban_duration_sec } } } # Backend service resource "google_compute_backend_service" "lb_backend" { - project = var.project_id + project = var.project_id name = "${var.environment}-gbfs-api-backend" protocol = "HTTP" port_name = "http" @@ -81,7 +81,7 @@ resource "google_compute_backend_service" "lb_backend" { # URL map resource "google_compute_url_map" "lb_url_map" { - project = var.project_id + project = var.project_id name = "${var.environment}-url-map" default_service = google_compute_backend_service.lb_backend.id } @@ -96,7 +96,7 @@ resource "google_compute_target_https_proxy" "lb_https_proxy" { # Forwarding rule resource "google_compute_global_forwarding_rule" "https_forwarding_rule_ipv4" { - project = var.project_id + project = var.project_id name = "${var.environment}-https-rule-ipv4" ip_address = data.google_compute_global_address.lb_ipv4.address port_range = "443" @@ -104,7 +104,7 @@ resource "google_compute_global_forwarding_rule" "https_forwarding_rule_ipv4" { load_balancing_scheme = "EXTERNAL" } resource "google_compute_global_forwarding_rule" "https_forwarding_rule_ipv6" { - project = var.project_id + project = var.project_id name = "${var.environment}-https-rule-ipv6" ip_address = data.google_compute_global_address.lb_ipv6.address port_range = "443" @@ -114,6 +114,6 @@ resource "google_compute_global_forwarding_rule" "https_forwarding_rule_ipv6" { # Reference manually created SSL certificate data "google_compute_ssl_certificate" "cert" { - project = var.project_id - name = "${var.environment}-gbfs-api-mobilitydatabase-org" + project = var.project_id + name = "${var.environment}-gbfs-api-mobilitydatabase-org" } diff --git a/infra/load-balancer/vars.tf b/infra/load-balancer/vars.tf index a734f6b..91fd07e 100644 --- a/infra/load-balancer/vars.tf +++ b/infra/load-balancer/vars.tf @@ -17,13 +17,13 @@ variable "project_id" { type = string description = "GCP project ID" - default = "gbfs-validator-staging" + default = "gbfs-validator-staging" } variable "gcp_region" { type = string description = "GCP region" - default = "northamerica-northeast1" + default = "northamerica-northeast1" } variable "environment" { diff --git a/infra/main.tf b/infra/main.tf index ae1c569..8bc0082 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -15,28 +15,38 @@ # limitations under the License. # -# Service account to execute the cloud functions -resource "google_service_account" "gbfs_validator_service_account" { - project = var.project_id - account_id = "gbfs-validator-service-account" - display_name = "GBFS Validator Service Account" +# GCS remote state backend. Configuration is supplied via -backend-config=backend.conf at init time. +terraform { + backend "gcs" {} } -# Service account to deploy all resources -data "google_service_account" "gbfs_deployer_service_account" { - project = var.project_id - account_id = "gbfs-deployer-service-account" +# Service account to execute the cloud functions. +# CI/CD: account_id is suffixed with the environment (e.g. gbfs-validator-dev-service-account) +# because dev and qa share the same GCP project (gbfs-validator-staging). Without the suffix +# both TF states would attempt to manage the same SA resource, causing conflicts on apply. +resource "google_service_account" "gbfs_validator_service_account" { + project = var.project_id + account_id = "gbfs-validator-sa-${var.environment}" + display_name = "GBFS Validator Service Account (${var.environment})" } +# CI/CD: The deployer service account data source was removed. The deployer SA email +# is now passed in via var.deployer_service_account (populated from vars.tfvars at +# runtime), which allows each environment to use its own project's SA without +# changing Terraform code. provider.tf references the variable directly for impersonation. module "cloud_run" { - source = "./cloud-run-service" - environment = var.environment + source = "./cloud-run-service" + project_id = var.project_id + environment = var.environment gbfs_validator_service_account_email = google_service_account.gbfs_validator_service_account.email - gbfs_api_image_version = var.gbfs_api_image_version + gbfs_api_image_version = var.gbfs_api_image_version + artifact_registry_repo = var.artifact_registry_repo } module "load_balancer" { - source = "./load-balancer" - environment = var.environment + source = "./load-balancer" + project_id = var.project_id + environment = var.environment cloud_run_service_name = module.cloud_run.cloud_run_service_name } + diff --git a/infra/provider.tf b/infra/provider.tf index e14e297..57f8b8f 100644 --- a/infra/provider.tf +++ b/infra/provider.tf @@ -20,16 +20,19 @@ # More info: https://cloud.google.com/blog/topics/developers-practitioners/using-google-cloud-service-account-impersonation-your-terraform-code provider "google" { - alias = "impersonation" - scopes = [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/userinfo.email", - ] + alias = "impersonation" + scopes = [ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/userinfo.email", + ] } data "google_service_account_access_token" "default" { - provider = google.impersonation - target_service_account = data.google_service_account.gbfs_deployer_service_account.email - scopes = ["userinfo-email", "cloud-platform"] - lifetime = "1200s" + provider = google.impersonation + # CI/CD: target_service_account is now supplied via var.deployer_service_account + # (passed from vars.tfvars) rather than looked up via a data source. This allows + # each environment to specify its own deployer SA without hardcoding the account ID. + target_service_account = var.deployer_service_account + scopes = ["userinfo-email", "cloud-platform"] + lifetime = "1200s" } diff --git a/infra/vars.tf b/infra/vars.tf index dd02ae7..cee82e5 100644 --- a/infra/vars.tf +++ b/infra/vars.tf @@ -17,13 +17,13 @@ variable "project_id" { type = string description = "GCP project ID" - default = "gbfs-validator-staging" + default = "gbfs-validator-staging" } variable "gcp_region" { type = string description = "GCP region" - default = "northamerica-northeast1" + default = "northamerica-northeast1" } variable "environment" { @@ -32,37 +32,45 @@ variable "environment" { } variable "gbfs_validator_app_name" { - type = string + type = string description = "App name for better resource management and cost tracking" - default = "gbfs_validator" + default = "gbfs_validator" } variable "gbfs_api_service" { - type = string + type = string description = "GBFS API service name as defined in the artifact registry" - default = "gbfs_validator_api" + default = "gbfs_validator_api" } variable "gbfs_api_image_version" { - type = string + type = string description = "GBFS API image version" - default = "latest" + default = "latest" } variable "java_runtime" { - type = string + type = string description = "Java function runtime" - default = "java17" + default = "java17" } # This is a temporary patch until the publising of the Java jar is defined variable "jar_file_name" { - type = string - default = "gbfs-validator-java-api.jar" + type = string + default = "gbfs-validator-java-api.jar" } variable "deployer_service_account" { + type = string + # CI/CD: No default — this must be supplied explicitly via vars.tfvars (or -var flag) + # so that each environment uses its own project's deployer SA. The value is the + # full SA email, e.g. gbfs-deployer-service-account@.iam.gserviceaccount.com. + description = "Service account email used to deploy resources via impersonation" +} + +variable "artifact_registry_repo" { type = string - description = "Service account used to deploy resources using impersonation" - default = "gbfs_deployer_service_account" + description = "Artifact Registry repository name shared across staging environments (e.g. gbfs-validator-staging)" + default = "gbfs-validator-staging" } diff --git a/infra/vars.tfvars.rename_me b/infra/vars.tfvars.rename_me new file mode 100644 index 0000000..789e32d --- /dev/null +++ b/infra/vars.tfvars.rename_me @@ -0,0 +1,29 @@ +# +# MobilityData 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# This file is a template for populating Terraform variables. +# To use locally: +# - Copy this file to vars.tfvars (git-ignored) +# - Replace {{VARIABLE}} placeholders with real values +# - Run: terraform plan -var-file=vars.tfvars +# In CI, this file is populated automatically by scripts/replace-variables.sh. + +project_id = {{PROJECT_ID}} +gcp_region = {{REGION}} +environment = {{ENVIRONMENT}} +gbfs_api_image_version = {{GBFS_API_IMAGE_VERSION}} +deployer_service_account = {{DEPLOYER_SERVICE_ACCOUNT}} +artifact_registry_repo = {{ARTIFACT_REGISTRY_REPO}} diff --git a/scripts/docker-build-validator.sh b/scripts/docker-build-validator.sh index 8ef7ee9..eaf3878 100755 --- a/scripts/docker-build-validator.sh +++ b/scripts/docker-build-validator.sh @@ -32,6 +32,7 @@ # -project_id GCP project ID (default: gbfs-validator-staging) # -region GCP region (default: northamerica-northeast1) # -repo_name Artifact Registry Docker repo name (default: gbfs-validator) +# -environment Deployment environment (default: dev) # -service Service name (default: gbfs_validator_api) # -version Image version tag (default: latest) # --test Build and run the container locally @@ -75,6 +76,7 @@ display_usage() { echo " -project_id GCP project ID (default: $PROJECT_ID)" echo " -region GCP region (default: $REGION)" echo " -repo_name Artifact Registry Docker repo name (default: $REPO_NAME_PREFIX)" + echo " -environment Deployment environment (default: $ENVIRONMENT)" echo " -service Service name (default: $SERVICE)" echo " -version Image version tag (default: $VERSION)" echo " --test Build and run the container locally" @@ -90,6 +92,10 @@ while [[ $# -gt 0 ]]; do -project_id) PROJECT_ID="$2"; shift 2 ;; -service) SERVICE="$2"; shift 2 ;; -repo_name) REPO_NAME_PREFIX="$2"; shift 2 ;; + # CI/CD: -environment is used as a subdirectory within the shared Artifact Registry repo + # (e.g. gbfs-validator-staging/dev/). Must match the environment Terraform will deploy to, + # otherwise the image ref in Cloud Run won't resolve. + -environment) ENVIRONMENT="$2"; shift 2 ;; -region) REGION="$2"; shift 2 ;; -version) VERSION="$2"; shift 2 ;; --test) TEST_MODE="true"; shift ;; @@ -104,7 +110,7 @@ done SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" JAR_PATH="$SCRIPT_PATH/../gbfs-validator/gbfs-validator-java-api.jar" DOCKERFILE="$SCRIPT_PATH/../gbfs-validator/Dockerfile" -IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME_PREFIX}-${ENVIRONMENT}/${SERVICE}:${VERSION}" +IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME_PREFIX}/${ENVIRONMENT}/${SERVICE}:${VERSION}" LOCAL_TAG="${SERVICE}:local" # === Validation === diff --git a/scripts/replace-variables.sh b/scripts/replace-variables.sh new file mode 100755 index 0000000..bcfe6cd --- /dev/null +++ b/scripts/replace-variables.sh @@ -0,0 +1,149 @@ +#!/bin/bash +# +# +# MobilityData 2023 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# + +# This script replace variables on an input file creating an output file with the substituted content. +# The script receives the name of the variables as parameters. The variables values are read from the environment. +# The input file must contain the variables in the format {{variable_name}}. +# For an example of a valid input file, check `../infra/vars.tfvars.rename_me`. +# All variables need to be set to the environment previous running the script. +# Parameters: +# -variables Comma separated list of REQUIRED variable names. +# -optional_variables Comma separated list of OPTIONAL variable names (may be unset or empty). +# -in_file Full path and file name of the input file. +# -out_file Full path and file name of the output file. +# -no_quotes Option to disable enclosing variable in double quotes during substitution + +display_usage() { + printf "\nThis script replaces variables from an input file creating/overriding the content of on an output file" + printf "\nScript Usage:\n" + echo "Usage: $0 [options]" + echo "Options:" + echo " -variables Comma separated list of REQUIRED variable names." + echo " -optional_variables Comma separated list of OPTIONAL variable names." + echo " -in_file Full path and file name of the input file." + echo " -out_file Full path and file name of the output file." + echo " -no_quotes Do not enclose variable values with quotes." + echo " -help Display help content." + exit 1 +} + +VARIABLES="" +OPTIONAL_VARIABLES="" +INPUT_FILE="" +OUT_FILE="" +ADD_QUOTES="true" + +while [[ $# -gt 0 ]]; do + key="$1" + + case $key in + -variables) + VARIABLES="$2" + shift # past argument + shift # past value + ;; + -optional_variables) + OPTIONAL_VARIABLES="$2" + shift # past argument + shift # past value + ;; + -in_file) + IN_FILE="$2" + shift # past argument + shift # past value + ;; + -out_file) + OUT_FILE="$2" + shift # past argument + shift # past value + ;; + -no_quotes) + ADD_QUOTES="false" + shift # past argument + ;; + -h|--help) + display_usage + ;; + *) # unknown option + shift # past argument + ;; + esac +done + +if [[ -z "${VARIABLES}" || -z "${IN_FILE}" || -z "${OUT_FILE}" ]]; then + echo "Missing required parameters." + display_usage +fi + +if [[ ! -f $IN_FILE ]] +then + echo "Input file does not exist, name: $IN_FILE" + echo "Bye for now." + exit 1 +fi + +if [[ -f $OUT_FILE ]] +then + echo "Warn: Output file does exist and will be overriden, name: $OUT_FILE" +fi + +list=$(echo "$VARIABLES" | tr "," "\n") +optional_list=$(echo "$OPTIONAL_VARIABLES" | tr "," "\n") + +# Check required variables (optional ones may be unset or empty) +for varname in $list; do + if [[ -z "${!varname+x}" ]]; then + echo "Missing required variable (unset) with name: $varname." + echo "Script will not execute variables replacement, bye for now." + exit 1 + fi + if [[ -z "${!varname}" ]]; then + echo "Missing required variable (empty value) with name: $varname." + echo "Script will not execute variables replacement, bye for now." + exit 1 + fi +done + +# Reads from input setting the first version of the output. +output=$(<"$IN_FILE") + +# Replace variables and create output file +for varname in $list; do + # Required variables are guaranteed non-empty here + value="${!varname}" + # shellcheck disable=SC2001 + # shellcheck disable=SC2016 + if [[ "$ADD_QUOTES" == "true" ]]; then + output=$(echo "$output" | sed 's|{{'"$varname"'}}|'"\"$value\""'|g') + else + output=$(echo "$output" | sed 's|{{'"$varname"'}}|'"$value"'|g') + fi +done + +# Substitute optional variables +for varname in $optional_list; do + value="${!varname}" + if [[ "$ADD_QUOTES" == "true" ]]; then + output=$(echo "$output" | sed 's|{{'"$varname"'}}|'"\"$value\""'|g') + else + output=$(echo "$output" | sed 's|{{'"$varname"'}}|'"$value"'|g') + fi +done + +echo "$output" > "$OUT_FILE" diff --git a/scripts/setup-deployer-sa-permissions.sh b/scripts/setup-deployer-sa-permissions.sh new file mode 100755 index 0000000..ed5e298 --- /dev/null +++ b/scripts/setup-deployer-sa-permissions.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# +# MobilityData 2025 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################### +# Deployer SA Permissions Script +# +# Grants the deployer service account all IAM roles needed to run +# Terraform apply for the GBFS Validator infrastructure. +# +# This is a one-time setup per GCP project. Run it manually before +# the first CI/CD deployment. +# +# USAGE: +# ./setup-deployer-sa-permissions.sh +# +# EXAMPLE: +# ./setup-deployer-sa-permissions.sh gbfs-validator-staging +# +############################################################################### + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " + exit 1 +fi + +PROJECT_ID="$1" +SA="gbfs-deployer-service-account@${PROJECT_ID}.iam.gserviceaccount.com" + +echo "Granting deployer SA permissions on project: ${PROJECT_ID}" +echo "Service account: ${SA}" +echo "" + +ROLES=( + # Artifact Registry — push/manage Docker images + roles/artifactregistry.admin + + # Cloud Run — create/update/delete services + roles/run.admin + + # Compute — load balancer, NEG, URL maps, forwarding rules + roles/compute.loadBalancerAdmin + + # Compute — Cloud Armor security policies + roles/compute.securityAdmin + + # IAM — create service accounts (for Cloud Run runtime SA) + roles/iam.serviceAccountAdmin + + # IAM — impersonate service accounts + roles/iam.serviceAccountTokenCreator + + # IAM — attach SAs to Cloud Run services + roles/iam.serviceAccountUser + + # IAM — set IAM policies (e.g. allUsers invoker on Cloud Run) + roles/resourcemanager.projectIamAdmin + + # Storage — read/write Terraform state bucket + roles/storage.admin +) + +for ROLE in "${ROLES[@]}"; do + echo " Granting ${ROLE}..." + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:${SA}" \ + --role="$ROLE" \ + --quiet --no-user-output-enabled +done + +echo "" +echo "✅ All roles granted to ${SA}" diff --git a/scripts/setup-environment.sh b/scripts/setup-environment.sh index c5ab193..f4f142b 100755 --- a/scripts/setup-environment.sh +++ b/scripts/setup-environment.sh @@ -46,14 +46,19 @@ # - IAM Policy Binding for deployer to impersonate gbfs-validator-service-account # # Usage: -# ./bootstrap.sh [] +# ./setup-environment.sh [] [] # -# GCP_PROJECT_ID - Required. Your Google Cloud project ID. -# ENVIRONMENT - Required. Deployment environment (e.g., dev, qa, prod). -# REGION - Optional. GCP region (default: northamerica-northeast1). +# GCP_PROJECT_ID - Required. Your Google Cloud project ID. +# ENVIRONMENT - Required. Deployment environment (e.g., dev, qa, prod). +# REGION - Optional. GCP region (default: northamerica-northeast1). +# ARTIFACT_REGISTRY_REPO - Optional. Shared AR repo name (default: gbfs-validator-staging). +# Use "gbfs-validator" for prod. # -# Example: -# ./bootstrap.sh gbfs-validator-dev dev +# Example (staging): +# ./setup-environment.sh gbfs-validator-staging qa +# +# Example (prod): +# ./setup-environment.sh gbfs-validator prod northamerica-northeast1 gbfs-validator # # Notes: # - Requires `gcloud` CLI installed and authenticated. @@ -67,7 +72,7 @@ set -euo pipefail PROJECT_ID="${1:?Usage: $0 }" ENVIRONMENT="${2:?Usage: $0 }" REGION="${3:-northamerica-northeast1}" # Default to Montréal -REPO_NAME="gbfs-validator-$ENVIRONMENT" +REPO_NAME="${4:-gbfs-validator-staging}" # Shared repo; use "gbfs-validator" for prod LOCAL_USER_EMAIL=$(gcloud config get-value account) PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format="value(projectNumber)") @@ -112,17 +117,24 @@ done # === Impersonation for local user === echo "👤 Allowing local user $LOCAL_USER_EMAIL to impersonate $DEPLOYER_SA" gcloud iam service-accounts add-iam-policy-binding "$DEPLOYER_SA" \ + --project="$PROJECT_ID" \ --member="user:${LOCAL_USER_EMAIL}" \ --role="roles/iam.serviceAccountTokenCreator" || echo "⚠️ Already allowed or failed" # === Allow deployer to impersonate the validator service account === +# Note: gbfs-validator-service-account is created by Terraform on first deploy. +# This binding is applied only if that SA already exists. VALIDATOR_SA="gbfs-validator-service-account@${PROJECT_ID}.iam.gserviceaccount.com" -echo "🔐 Granting 'iam.serviceAccountUser' to $DEPLOYER_SA on $VALIDATOR_SA" - -gcloud iam service-accounts add-iam-policy-binding "$VALIDATOR_SA" \ - --member="serviceAccount:${DEPLOYER_SA}" \ - --role="roles/iam.serviceAccountUser" \ - --project="$PROJECT_ID" +if gcloud iam service-accounts describe "$VALIDATOR_SA" --project="$PROJECT_ID" &>/dev/null; then + echo "🔐 Granting 'iam.serviceAccountUser' to $DEPLOYER_SA on $VALIDATOR_SA" + gcloud iam service-accounts add-iam-policy-binding "$VALIDATOR_SA" \ + --member="serviceAccount:${DEPLOYER_SA}" \ + --role="roles/iam.serviceAccountUser" \ + --project="$PROJECT_ID" +else + echo "⚠️ $VALIDATOR_SA does not exist yet — Terraform will create it on first deploy." + echo " Re-run this script after the first successful deployment to apply this binding." +fi # === Enable Artifact Registry API === echo "🛰️ Enabling Artifact Registry API..." @@ -207,6 +219,7 @@ REQUIRED_SERVICES=( run.googleapis.com certificatemanager.googleapis.com compute.googleapis.com + cloudresourcemanager.googleapis.com ) echo "🛰️ Enabling required Google APIs..." @@ -225,3 +238,58 @@ for SERVICE in "${REQUIRED_SERVICES[@]}"; do done echo "🎉 Setup complete for project $PROJECT_ID" + +# === Global LB IP addresses === +IPV4_NAME="${ENVIRONMENT}-lb-ipv4" +IPV6_NAME="${ENVIRONMENT}-lb-ipv6" + +if gcloud compute addresses describe "${IPV4_NAME}" --project="${PROJECT_ID}" --global &>/dev/null; then + IPV4=$(gcloud compute addresses describe "${IPV4_NAME}" --project="${PROJECT_ID}" --global --format="value(address)") + echo "✅ IPv4 already exists: ${IPV4_NAME} → ${IPV4}" +else + echo "➕ Creating global IPv4: ${IPV4_NAME} ..." + gcloud compute addresses create "${IPV4_NAME}" --project="${PROJECT_ID}" --ip-version=IPV4 --global + IPV4=$(gcloud compute addresses describe "${IPV4_NAME}" --project="${PROJECT_ID}" --global --format="value(address)") + echo "✅ Created: ${IPV4_NAME} → ${IPV4}" +fi + +if gcloud compute addresses describe "${IPV6_NAME}" --project="${PROJECT_ID}" --global &>/dev/null; then + IPV6=$(gcloud compute addresses describe "${IPV6_NAME}" --project="${PROJECT_ID}" --global --format="value(address)") + echo "✅ IPv6 already exists: ${IPV6_NAME} → ${IPV6}" +else + echo "➕ Creating global IPv6: ${IPV6_NAME} ..." + gcloud compute addresses create "${IPV6_NAME}" --project="${PROJECT_ID}" --ip-version=IPV6 --global + IPV6=$(gcloud compute addresses describe "${IPV6_NAME}" --project="${PROJECT_ID}" --global --format="value(address)") + echo "✅ Created: ${IPV6_NAME} → ${IPV6}" +fi + +# === Google-managed SSL certificate === +# Prod uses the bare domain; all other envs use {env}.domain +if [[ "${ENVIRONMENT}" == "prod" ]]; then + DOMAIN="gbfs.api.mobilitydatabase.org" +else + DOMAIN="${ENVIRONMENT}.gbfs.api.mobilitydatabase.org" +fi +CERT_NAME="${ENVIRONMENT}-gbfs-api-mobilitydatabase-org" + +if gcloud compute ssl-certificates describe "${CERT_NAME}" --project="${PROJECT_ID}" --global &>/dev/null; then + echo "✅ SSL certificate already exists: ${CERT_NAME}" +else + echo "➕ Creating Google-managed SSL certificate: ${CERT_NAME} ..." + gcloud compute ssl-certificates create "${CERT_NAME}" \ + --project="${PROJECT_ID}" \ + --domains="${DOMAIN}" \ + --global + echo "✅ Created: ${CERT_NAME} (provisioning pending DNS)" +fi + +echo "" +echo "==========================================" +echo " ✅ Bootstrap complete for: ${ENVIRONMENT}" +echo "" +echo " Next: create DNS records for ${DOMAIN}:" +echo " A ${IPV4}" +echo " AAAA ${IPV6}" +echo "" +echo " SSL cert provisions automatically once DNS resolves." +echo "=========================================="