From 6fb537e8012808314c1885b7c980dc4d554452bf Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 19 Jul 2023 14:37:08 +1000 Subject: [PATCH 1/6] feat: migrate postdeployment to data http --- app/init/Dockerfile | 17 +++ app/init/README.md | 10 ++ app/init/init-execute.sh | 56 +++++++ app/init/init-image.cloudbuild.yaml | 36 +++++ app/placeholder/Dockerfile | 2 +- app/placeholder/placeholder-deploy.sh | 23 ++- infra/containers.tf | 3 +- infra/iam.tf | 14 +- infra/jobs.tf | 29 ---- infra/postdeployment.tf | 202 +++++++++++++------------- 10 files changed, 253 insertions(+), 139 deletions(-) create mode 100644 app/init/Dockerfile create mode 100644 app/init/README.md create mode 100755 app/init/init-execute.sh create mode 100644 app/init/init-image.cloudbuild.yaml diff --git a/app/init/Dockerfile b/app/init/Dockerfile new file mode 100644 index 00000000..bbe83037 --- /dev/null +++ b/app/init/Dockerfile @@ -0,0 +1,17 @@ +# Copyright 2023 Google LLC +# +# 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. + +FROM gcr.io/google.com/cloudsdktool/cloud-sdk:slim +COPY init-execute.sh . +ENTRYPOINT ./init-execute.sh diff --git a/app/init/README.md b/app/init/README.md new file mode 100644 index 00000000..0082b0df --- /dev/null +++ b/app/init/README.md @@ -0,0 +1,10 @@ +# Init + +This folder contains the source for a container image that runs the tasks required to setup the application on first deployment. + +The container is designed to be executed as a Cloud Run job, with the `roles/run.developer` role, to run the `init-execute.sh` script: + + * execute the `setup` job, [primes the the database](https://github.com/GoogleCloudPlatform/avocano/blob/main/server/scripts/prime_database.sh) script + * execute the `client` job, that runs the [client deployment](https://github.com/GoogleCloudPlatform/avocano/blob/main/client/docker-deploy.sh) + * purges cache and warms API. + diff --git a/app/init/init-execute.sh b/app/init/init-execute.sh new file mode 100755 index 00000000..ed2f9bf2 --- /dev/null +++ b/app/init/init-execute.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Copyright 2023 Google LLC +# +# 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. + +# Script to assist in Dockerfile-based deployments. +# any errors? exit immediately. +set -e + +# escape if project_id not defined (mandatory, required later) +if [[ -z $PROJECT_ID ]]; then + echo "PROJECT_ID not defined. Cannot deploy. Exiting." + exit 1 +fi + +# escape if firebase_url not defined (mandatory, required later) +if [[ -z $FIREBASE_URL ]]; then + echo "FIREBASE_URL not defined. Cannot deploy. Exiting." + exit 1 +fi + +# Define common defaults, all overrideable. +REGION="${REGION:-us-central1}" +SETUP_JOB="${SETUP_JOB:-setup}" +CLIENT_JOB="${CLIENT_JOB:-client}" + +echo "*** Executing initization job ***" +echo "PROJECT_ID: $PROJECT_ID" +echo "REGION: $REGION" +echo "SETUP JOB: $SETUP_JOB" +echo "CLIENT JOB: $CLIENT_JOB" +echo "FIREBASE URL: $FIREBASE_URL" +echo "SERVER URL: $SERVER_URL" +echo "" + +echo "Running init database migration..." +gcloud run jobs execute $SETUP_JOB --wait --project $PROJECT_ID --region $REGION + +echo "Running client deploy..." +gcloud run jobs execute $CLIENT_JOB --wait --project $PROJECT_ID --region $REGION + +echo "Purge Firebase cache" +echo curl -X PURGE "${FIREBASE_URL}/" + +echo "Warm up API" +curl "${SERVER_URL}/api/products/?warmup" diff --git a/app/init/init-image.cloudbuild.yaml b/app/init/init-image.cloudbuild.yaml new file mode 100644 index 00000000..7b854213 --- /dev/null +++ b/app/init/init-image.cloudbuild.yaml @@ -0,0 +1,36 @@ +# Copyright 2023 Google LLC +# +# 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. + +# Build placeholder site code into container + +steps: + - id: build + name: "gcr.io/cloud-builders/docker" + dir: app/init + args: + [ + "build", + "-t", + "gcr.io/$PROJECT_ID/$_IMAGE_NAME", + ".", + ] + +images: + - gcr.io/$PROJECT_ID/$_IMAGE_NAME + +substitutions: + _IMAGE_NAME: avocano-init + +options: + dynamic_substitutions: true diff --git a/app/placeholder/Dockerfile b/app/placeholder/Dockerfile index 81870466..5bc8abe6 100644 --- a/app/placeholder/Dockerfile +++ b/app/placeholder/Dockerfile @@ -17,7 +17,7 @@ ARG PROJECT_ID=YOURPROJECTID FROM gcr.io/$PROJECT_ID/firebase -RUN apk add gettext +RUN apk add gettext curl RUN npm install -g json COPY . ./ diff --git a/app/placeholder/placeholder-deploy.sh b/app/placeholder/placeholder-deploy.sh index c3ce9941..09005ada 100755 --- a/app/placeholder/placeholder-deploy.sh +++ b/app/placeholder/placeholder-deploy.sh @@ -17,6 +17,21 @@ # any errors? exit immediately. set -e +# escape if firebase_url not defined (mandatory, required later) +if [[ -z $FIREBASE_URL ]]; then + echo "FIREBASE_URL not defined. Cannot deploy. Exiting." + exit 1 +fi + +# Only run the placeholder script if the site has been deployed before. +# Check if the firebase url has "Site Not Found" (the pre-deployment state) +if curl $FIREBASE_URL | grep -q "Site Not Found"; then + echo "Firebase site $FIREBASE_URL hasn't been deployed before, so it needs a placeholder." +else + echo "Firebase site $FIREBASE_URL has been deployed before. Not going to deploy placeholder. Exiting." + exit 0 +fi + # if deploying with a suffix (from infra/jobs.tf), adjust the config to suit the custom site # https://firebase.google.com/docs/hosting/multisites#set_up_deploy_targets if [[ -n $SUFFIX ]]; then @@ -24,7 +39,7 @@ if [[ -n $SUFFIX ]]; then UPDATED=true # Use template file to generate configuration - envsubst < firebaserc.tmpl > .firebaserc + envsubst .firebaserc echo "Customised .firebaserc created to support site." cat .firebaserc fi @@ -35,6 +50,10 @@ if [[ -n $UPDATED ]]; then cat firebase.json fi +# Finally, deploy the application echo "Deploying placeholder to Firebase..." - firebase deploy --project "$PROJECT_ID" --only hosting + +# Setup for greater chances of success by explicitly purging cache +echo "Purging firebase cache" +curl -X PURGE "${FIREBASE_URL}/" diff --git a/infra/containers.tf b/infra/containers.tf index 1fefdade..92056d66 100644 --- a/infra/containers.tf +++ b/infra/containers.tf @@ -19,5 +19,6 @@ locals { server_image = "gcr.io/${var.server_image_host}/server:${var.image_version}" client_image = "gcr.io/${var.client_image_host}/client:${var.image_version}" - placeholder_image = "gcr.io/hsa-public/avocano-placeholder:latest" + placeholder_image = "gcr.io/hsa-public/avocano-placeholder:postjsscurl" # TODO(glasnt): revert to tag "latest" + init_image = "gcr.io/hsa-public/avocano-init:postjsscurl" # TODO(glasnt): revert to tag "latest" } diff --git a/infra/iam.tf b/infra/iam.tf index 2513e9eb..7dd2f0fd 100644 --- a/infra/iam.tf +++ b/infra/iam.tf @@ -41,9 +41,9 @@ resource "google_service_account" "automation" { depends_on = [module.project_services] } -resource "google_service_account" "compute" { - account_id = var.random_suffix ? "compute-startup-${random_id.suffix.hex}" : "compute-startup" - display_name = "Head Start App Compute Instance SA" +resource "google_service_account" "init" { + account_id = var.random_suffix ? "init-startup-${random_id.suffix.hex}" : "init-startup" + display_name = "Jump Start App Init SA" depends_on = [module.project_services] count = var.init ? 1 : 0 } @@ -88,12 +88,12 @@ resource "google_project_iam_member" "client_permissions" { depends_on = [google_service_account.client] } -# GCE instance needs access to start Jobs -resource "google_project_iam_member" "computestartup_permissions" { +# Init process needs access to start Jobs +resource "google_project_iam_member" "initstartup_permissions" { project = var.project_id role = "roles/run.developer" - member = "serviceAccount:${google_service_account.compute[0].email}" - depends_on = [google_service_account.compute] + member = "serviceAccount:${google_service_account.init[0].email}" + depends_on = [google_service_account.init] count = var.init ? 1 : 0 } diff --git a/infra/jobs.tf b/infra/jobs.tf index 74583cdc..bed42a35 100644 --- a/infra/jobs.tf +++ b/infra/jobs.tf @@ -144,32 +144,3 @@ resource "google_cloud_run_v2_job" "client" { ] } - -resource "google_cloud_run_v2_job" "placeholder" { - name = var.random_suffix ? "placeholder-${random_id.suffix.hex}" : "placeholder" - location = var.region - - labels = var.labels - - template { - template { - service_account = google_service_account.client.email - containers { - image = local.placeholder_image - env { - name = "PROJECT_ID" - value = var.project_id - } - - env { - name = "SUFFIX" - value = var.random_suffix ? random_id.suffix.hex : "" - } - } - } - } - - depends_on = [ - module.project_services - ] -} diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index 0ca6b1dd..3a181276 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -14,126 +14,130 @@ * limitations under the License. */ -resource "google_compute_network" "gce_init" { - count = var.init ? 1 : 0 - - name = var.random_suffix ? "gce-init-network-${random_id.suffix.hex}" : "gce-init-network" - auto_create_subnetworks = false - routing_mode = "GLOBAL" - project = var.project_id - delete_default_routes_on_create = false - mtu = 0 - depends_on = [module.project_services] +# used to collect access token, for authenticated POST commands +data "google_client_config" "current" { } -resource "google_compute_subnetwork" "gce_init" { - count = var.init ? 1 : 0 - - name = var.random_suffix ? "subnet-gce-init-${random_id.suffix.hex}" : "subnet-gce-init" - network = google_compute_network.gce_init[0].id - ip_cidr_range = "10.10.10.0/24" - region = var.region +# Job that uses pre-built docker image to deploy a placeholder website. +resource "google_cloud_run_v2_job" "placeholder" { + name = var.random_suffix ? "placeholder-${random_id.suffix.hex}" : "placeholder" + location = var.region + + labels = var.labels + + template { + template { + service_account = google_service_account.client.email + max_retries = 1 + containers { + image = local.placeholder_image + + # Variables consumed by /app/placeholder/placeholder-deploy.sh + env { + name = "PROJECT_ID" + value = var.project_id + } + env { + name = "SUFFIX" + value = var.random_suffix ? random_id.suffix.hex : "" + } + env { + name = "FIREBASE_URL" + value = local.firebase_url + } + } + } + } - depends_on = [module.project_services] + depends_on = [ + module.project_services + ] } -resource "google_compute_instance" "gce_init" { - count = var.init ? 1 : 0 + +# execute the job by calling the API directly. +data "http" "execute_placeholder_job" { + url = "https://${var.region}-run.googleapis.com/v2/projects/${var.project_id}/locations/${var.region}/jobs/${google_cloud_run_v2_job.placeholder.name}:run" + method = "POST" + request_headers = { + Accept = "application/json" + Authorization = "Bearer ${data.google_client_config.current.access_token}" } depends_on = [ module.project_services, - google_sql_database_instance.postgres, - google_cloud_run_v2_job.setup, - google_cloud_run_v2_job.client, + google_cloud_run_v2_job.placeholder, ] +} - name = var.random_suffix ? "head-start-initialize-${random_id.suffix.hex}" : "head-start-initialize" - machine_type = "n1-standard-1" - zone = var.zone - desired_status = "RUNNING" # https://github.com/GoogleCloudPlatform/terraform-dynamic-python-webapp/pull/75#issuecomment-1547198414 - - allow_stopping_for_update = true - - boot_disk { - initialize_params { - image = "debian-cloud/debian-11" - } - } - - network_interface { - network = google_compute_network.gce_init[0].self_link - subnetwork = google_compute_subnetwork.gce_init[0].self_link - access_config { - // Ephemeral public IP +resource "google_cloud_run_v2_job" "init" { + name = var.random_suffix ? "init-${random_id.suffix.hex}" : "init" + location = var.region + + labels = var.labels + + template { + template { + service_account = google_service_account.init[0].email + max_retries = 1 + containers { + image = local.init_image + + # Variables consumed by /app/init/init-execute.sh + env { + name = "PROJECT_ID" + value = var.project_id + } + env { + name = "SUFFIX" + value = var.random_suffix ? random_id.suffix.hex : "" + } + env { + name = "REGION" + value = var.region + } + env { + name = "SETUP_JOB" + value = google_cloud_run_v2_job.setup.name + } + env { + name = "CLIENT_JOB" + value = google_cloud_run_v2_job.client.name + } + env { + name = "FIREBASE_URL" + value = local.firebase_url + } + env { + name = "SERVER_URL" + value = google_cloud_run_v2_service.server.uri + } + + } } } - service_account { - email = google_service_account.compute[0].email - scopes = ["cloud-platform"] # TODO: Restrict?? - } - - metadata_startup_script = < Date: Wed, 19 Jul 2023 15:11:20 +1000 Subject: [PATCH 2/6] lint --- app/init/README.md | 6 +++--- app/init/init-execute.sh | 4 ++-- app/placeholder/placeholder-deploy.sh | 2 +- infra/containers.tf | 2 +- infra/postdeployment.tf | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/init/README.md b/app/init/README.md index 0082b0df..72de895a 100644 --- a/app/init/README.md +++ b/app/init/README.md @@ -1,10 +1,10 @@ # Init -This folder contains the source for a container image that runs the tasks required to setup the application on first deployment. +This folder contains the source for a container image that runs the tasks required to setup the application on first deployment. -The container is designed to be executed as a Cloud Run job, with the `roles/run.developer` role, to run the `init-execute.sh` script: +The container is designed to be executed as a Cloud Run job, with the `roles/run.developer` role, to run the `init-execute.sh` script: * execute the `setup` job, [primes the the database](https://github.com/GoogleCloudPlatform/avocano/blob/main/server/scripts/prime_database.sh) script * execute the `client` job, that runs the [client deployment](https://github.com/GoogleCloudPlatform/avocano/blob/main/client/docker-deploy.sh) - * purges cache and warms API. + * purges cache and warms API. diff --git a/app/init/init-execute.sh b/app/init/init-execute.sh index ed2f9bf2..6c99e9dc 100755 --- a/app/init/init-execute.sh +++ b/app/init/init-execute.sh @@ -44,10 +44,10 @@ echo "SERVER URL: $SERVER_URL" echo "" echo "Running init database migration..." -gcloud run jobs execute $SETUP_JOB --wait --project $PROJECT_ID --region $REGION +gcloud run jobs execute "$SETUP_JOB" --wait --project "$PROJECT_ID" --region "$REGION" echo "Running client deploy..." -gcloud run jobs execute $CLIENT_JOB --wait --project $PROJECT_ID --region $REGION +gcloud run jobs execute "$CLIENT_JOB" --wait --project "$PROJECT_ID" --region "$REGION" echo "Purge Firebase cache" echo curl -X PURGE "${FIREBASE_URL}/" diff --git a/app/placeholder/placeholder-deploy.sh b/app/placeholder/placeholder-deploy.sh index 09005ada..46cc3118 100755 --- a/app/placeholder/placeholder-deploy.sh +++ b/app/placeholder/placeholder-deploy.sh @@ -25,7 +25,7 @@ fi # Only run the placeholder script if the site has been deployed before. # Check if the firebase url has "Site Not Found" (the pre-deployment state) -if curl $FIREBASE_URL | grep -q "Site Not Found"; then +if curl "$FIREBASE_URL" | grep -q "Site Not Found"; then echo "Firebase site $FIREBASE_URL hasn't been deployed before, so it needs a placeholder." else echo "Firebase site $FIREBASE_URL has been deployed before. Not going to deploy placeholder. Exiting." diff --git a/infra/containers.tf b/infra/containers.tf index 92056d66..ee70766c 100644 --- a/infra/containers.tf +++ b/infra/containers.tf @@ -20,5 +20,5 @@ locals { server_image = "gcr.io/${var.server_image_host}/server:${var.image_version}" client_image = "gcr.io/${var.client_image_host}/client:${var.image_version}" placeholder_image = "gcr.io/hsa-public/avocano-placeholder:postjsscurl" # TODO(glasnt): revert to tag "latest" - init_image = "gcr.io/hsa-public/avocano-init:postjsscurl" # TODO(glasnt): revert to tag "latest" + init_image = "gcr.io/hsa-public/avocano-init:postjsscurl" # TODO(glasnt): revert to tag "latest" } diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index 3a181276..b3ee3ba2 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -19,7 +19,7 @@ data "google_client_config" "current" { } -# Job that uses pre-built docker image to deploy a placeholder website. +# Job that uses pre-built docker image to deploy a placeholder website. resource "google_cloud_run_v2_job" "placeholder" { name = var.random_suffix ? "placeholder-${random_id.suffix.hex}" : "placeholder" location = var.region @@ -56,7 +56,7 @@ resource "google_cloud_run_v2_job" "placeholder" { } -# execute the job by calling the API directly. +# execute the job by calling the API directly. data "http" "execute_placeholder_job" { url = "https://${var.region}-run.googleapis.com/v2/projects/${var.project_id}/locations/${var.region}/jobs/${google_cloud_run_v2_job.placeholder.name}:run" method = "POST" @@ -124,7 +124,7 @@ resource "google_cloud_run_v2_job" "init" { } -# execute the job, once it and other dependencies exit. +# execute the job, once it and other dependencies exit. data "http" "execute_init_job" { count = var.init ? 1 : 0 From 2e552502dc0301a12a343bfe13e2c7fad5cc7144 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Wed, 19 Jul 2023 16:02:51 +1000 Subject: [PATCH 3/6] lint: add ignore to side-effect 'unused' data sources --- infra/postdeployment.tf | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index b3ee3ba2..adc8bd9e 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -56,7 +56,8 @@ resource "google_cloud_run_v2_job" "placeholder" { } -# execute the job by calling the API directly. +# execute the job by calling the API directly. Intended side-effect +# tflint-ignore: terraform_unused_declarations data "http" "execute_placeholder_job" { url = "https://${var.region}-run.googleapis.com/v2/projects/${var.project_id}/locations/${var.region}/jobs/${google_cloud_run_v2_job.placeholder.name}:run" method = "POST" @@ -124,7 +125,8 @@ resource "google_cloud_run_v2_job" "init" { } -# execute the job, once it and other dependencies exit. +# execute the job, once it and other dependencies exit. Intended side-effect. +# tflint-ignore: terraform_unused_declarations data "http" "execute_init_job" { count = var.init ? 1 : 0 From 17dea670c123097b2608ec693c63b27213f0bca7 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 20 Jul 2023 17:27:01 +1000 Subject: [PATCH 4/6] remove debugging single-try job --- infra/postdeployment.tf | 2 -- 1 file changed, 2 deletions(-) diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index adc8bd9e..98a03fc5 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -29,7 +29,6 @@ resource "google_cloud_run_v2_job" "placeholder" { template { template { service_account = google_service_account.client.email - max_retries = 1 containers { image = local.placeholder_image @@ -81,7 +80,6 @@ resource "google_cloud_run_v2_job" "init" { template { template { service_account = google_service_account.init[0].email - max_retries = 1 containers { image = local.init_image From 7af2799978cabb9db68f4cd4b826ed41dbc9b432 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 20 Jul 2023 17:27:16 +1000 Subject: [PATCH 5/6] workaround: don't delete jobs --- infra/postdeployment.tf | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infra/postdeployment.tf b/infra/postdeployment.tf index 98a03fc5..8c2d8dad 100644 --- a/infra/postdeployment.tf +++ b/infra/postdeployment.tf @@ -52,6 +52,11 @@ resource "google_cloud_run_v2_job" "placeholder" { depends_on = [ module.project_services ] + + # work around, b/292021282 + lifecycle { + prevent_destroy = true + } } @@ -120,6 +125,11 @@ resource "google_cloud_run_v2_job" "init" { depends_on = [ module.project_services ] + + # work around, b/292021282 + lifecycle { + prevent_destroy = true + } } From 551c6dddd4639e8ac2ceb6abd09bb1462f163fa3 Mon Sep 17 00:00:00 2001 From: Katie McLaughlin Date: Thu, 20 Jul 2023 17:27:38 +1000 Subject: [PATCH 6/6] workaround: ignore unused variable --- infra/variables.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/variables.tf b/infra/variables.tf index b7845fc9..51794201 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -29,6 +29,7 @@ variable "region" { # HSA +# tflint-ignore: terraform_unused_declarations variable "zone" { type = string description = "GCP zone for provisioning zonal resources."