Repository: google/exposure-notifications-server
Pull request #474 (2020-05-28 16:08:00)

for e2e testing, allow acceptance of keys valid till the end of the U…
…TC day

google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/474#" title="Author self-approved">mikehelmick</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [mikehelmick]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/474#pullrequestreview-420271327" title="Approved">gurayAlsac</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/474#" title="Author self-approved">mikehelmick</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [mikehelmick]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
sethvargo: /lgtm
/approve
google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/474#pullrequestreview-420271327" title="Approved">gurayAlsac</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/474#" title="Author self-approved">mikehelmick</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/474#issuecomment-635448074" title="Approved">sethvargo</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [mikehelmick,sethvargo]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bbe779e..cf39c14 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,7 +32,7 @@ All submissions will be reviewed before merging. Submissions are reviewed using
 
 ### Source code layout
 
-Common code is in the `/pkg` folder.
+Common code is in the `/internal` folder.
 
 Each binary will have its `main.go` file in a `/cmd/[bin-name]` folder.
 
diff --git a/internal/generate/handler.go b/internal/generate/handler.go
index 88751f8..8277313 100644
--- a/internal/generate/handler.go
+++ b/internal/generate/handler.go
@@ -37,7 +37,7 @@ func NewHandler(ctx context.Context, config *Config, env *serverenv.ServerEnv) (
 		return nil, fmt.Errorf("missing database in server environment")
 	}
 
-	transformer, err := model.NewTransformer(config.MaxKeysOnPublish, config.MaxIntervalAge, config.TruncateWindow)
+	transformer, err := model.NewTransformer(config.MaxKeysOnPublish, config.MaxIntervalAge, config.TruncateWindow, false)
 	if err != nil {
 		return nil, fmt.Errorf("model.NewTransformer: %w", err)
 	}
diff --git a/internal/publish/config.go b/internal/publish/config.go
index 5fc7328..f565930 100644
--- a/internal/publish/config.go
+++ b/internal/publish/config.go
@@ -37,7 +37,8 @@ type Config struct {
 	TruncateWindow     time.Duration `envconfig:"TRUNCATE_WINDOW" default:"1h"`
 
 	// Flags for local development and testing.
-	DebugAPIResponses bool `envconfig:"DEBUG_API_RESPONSES"`
+	DebugAPIResponses   bool `envconfig:"DEBUG_API_RESPONSES"`
+	DebugAllowRestOfDay bool `envconfig:"DEBUG_ALLOW_REST_OF_DAY"`
 
 	AuthorizedApp *authorizedapp.Config
 	Database      *database.Config
diff --git a/internal/publish/model/exposure_model.go b/internal/publish/model/exposure_model.go
index 03b3512..ff8467a 100644
--- a/internal/publish/model/exposure_model.go
+++ b/internal/publish/model/exposure_model.go
@@ -176,12 +176,13 @@ type Transformer struct {
 	maxExposureKeys     int
 	maxIntervalStartAge time.Duration // How many intervals old does this server accept?
 	truncateWindow      time.Duration
+	debugAllowRestOfDay bool // raises end time of keys to the end of day, but doesn't embargo. For e2e testing only.
 }
 
 // NewTransformer creates a transformer for turning publish API requests into
 // records for insertion into the database. On the call to TransformPublish
 // all data is validated according to the transformer that is used.
-func NewTransformer(maxExposureKeys int, maxIntervalStartAge time.Duration, truncateWindow time.Duration) (*Transformer, error) {
+func NewTransformer(maxExposureKeys int, maxIntervalStartAge time.Duration, truncateWindow time.Duration, allowRestOfDay bool) (*Transformer, error) {
 	if maxExposureKeys < 0 || maxExposureKeys > maxKeysPerPublish {
 		return nil, fmt.Errorf("maxExposureKeys must be > 0 and <= %v, got %v", maxKeysPerPublish, maxExposureKeys)
 	}
@@ -189,6 +190,7 @@ func NewTransformer(maxExposureKeys int, maxIntervalStartAge time.Duration, trun
 		maxExposureKeys:     maxExposureKeys,
 		maxIntervalStartAge: maxIntervalStartAge,
 		truncateWindow:      truncateWindow,
+		debugAllowRestOfDay: allowRestOfDay,
 	}, nil
 }
 
@@ -266,6 +268,12 @@ func (t *Transformer) TransformPublish(inData *Publish, batchTime time.Time) ([]
 	// And have an interval <= maxInterval (configured allowed clock skew)
 	maxIntervalNumber := IntervalNumber(batchTime)
 
+	// If, for testing, we are accepting keys that are valid the rest of the day:
+	// adjust the maxIntervalNumber to be the end of the UTC day.
+	if t.debugAllowRestOfDay {
+		maxIntervalNumber = IntervalNumber(batchTime.Add(24 * time.Hour).Truncate(24 * time.Hour))
+	}
+
 	// Regions are a multi-value property, uppercase them for storage.
 	// There is no set of "valid" regions overall, but it is defined
 	// elsewhere by what regions an authorized application may write to.
diff --git a/internal/publish/publish.go b/internal/publish/publish.go
index ecc5827..9d3fca0 100644
--- a/internal/publish/publish.go
+++ b/internal/publish/publish.go
@@ -44,7 +44,7 @@ func NewHandler(ctx context.Context, config *Config, env *serverenv.ServerEnv) (
 		return nil, fmt.Errorf("missing AuthorizedApp provider in server environment")
 	}
 
-	transformer, err := model.NewTransformer(config.MaxKeysOnPublish, config.MaxIntervalAge, config.TruncateWindow)
+	transformer, err := model.NewTransformer(config.MaxKeysOnPublish, config.MaxIntervalAge, config.TruncateWindow, config.DebugAllowRestOfDay)
 	if err != nil {
 		return nil, fmt.Errorf("model.NewTransformer: %w", err)
 	}
diff --git a/terraform/database.tf b/terraform/database.tf
index c373746..e00d450 100644
--- a/terraform/database.tf
+++ b/terraform/database.tf
@@ -30,6 +30,16 @@ resource "google_sql_database_instance" "db-inst" {
     disk_size         = var.cloudsql_disk_size_gb
     availability_type = "REGIONAL"
 
+    database_flags {
+      name  = "autovacuum"
+      value = "on"
+    }
+
+    database_flags {
+      name  = "max_connections"
+      value = "100000"
+    }
+
     backup_configuration {
       enabled    = true
       start_time = "02:00"
@@ -63,32 +73,40 @@ resource "google_sql_database_instance" "db-inst" {
   ]
 }
 
-resource "google_sql_ssl_cert" "client_cert" {
-  common_name = "apollo"
+resource "google_sql_database" "db" {
+  project  = data.google_project.project.project_id
+  instance = google_sql_database_instance.db-inst.name
+  name     = "main"
+}
+
+resource "google_sql_ssl_cert" "db-cert" {
+  project     = data.google_project.project.project_id
   instance    = google_sql_database_instance.db-inst.name
+  common_name = "expsoure-notification"
 }
 
-resource "random_password" "userpassword" {
-  length  = 16
+resource "random_password" "db-password" {
+  length  = 64
   special = false
 }
 
 resource "google_sql_user" "user" {
   instance = google_sql_database_instance.db-inst.name
   name     = "notification"
-  password = random_password.userpassword.result
+  password = random_password.db-password.result
 }
 
-resource "google_sql_database" "db" {
-  instance = google_sql_database_instance.db-inst.name
+resource "google_secret_manager_secret" "db-secret" {
+  provider = google-beta
 
-  name    = "main"
-  project = data.google_project.project.project_id
-}
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
 
-resource "google_secret_manager_secret" "db-pwd" {
-  provider  = google-beta
-  secret_id = "dbPassword"
+  secret_id = "db-${each.key}"
 
   replication {
     automatic = true
@@ -99,17 +117,25 @@ resource "google_secret_manager_secret" "db-pwd" {
   ]
 }
 
-resource "google_secret_manager_secret_version" "db-pwd-initial" {
-  provider    = google-beta
-  secret      = google_secret_manager_secret.db-pwd.id
-  secret_data = google_sql_user.user.password
+resource "google_secret_manager_secret_version" "db-secret-version" {
+  provider = google-beta
+
+  for_each = {
+    sslcert     = google_sql_ssl_cert.db-cert.cert
+    sslkey      = google_sql_ssl_cert.db-cert.private_key
+    sslrootcert = google_sql_ssl_cert.db-cert.server_ca_cert
+    password    = google_sql_user.user.password
+  }
+
+  secret      = google_secret_manager_secret.db-secret[each.key].id
+  secret_data = each.value
 }
 
 # Grant Cloud Build the ability to access the database password (required to run
 # migrations).
 resource "google_secret_manager_secret_iam_member" "cloudbuild-db-pwd" {
   provider  = google-beta
-  secret_id = google_secret_manager_secret.db-pwd.id
+  secret_id = google_secret_manager_secret.db-secret["password"].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com"
 
@@ -124,7 +150,9 @@ resource "google_project_iam_member" "cloudbuild-sql" {
   role    = "roles/cloudsql.client"
   member  = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com"
 
-  depends_on = [google_project_service.services["cloudbuild.googleapis.com"]]
+  depends_on = [
+    google_project_service.services["cloudbuild.googleapis.com"]
+  ]
 }
 
 # Migrate runs the initial database migrations.
@@ -133,7 +161,7 @@ resource "null_resource" "migrate" {
     environment = {
       PROJECT_ID     = data.google_project.project.project_id
       DB_CONN        = google_sql_database_instance.db-inst.connection_name
-      DB_PASS_SECRET = google_secret_manager_secret_version.db-pwd-initial.name
+      DB_PASS_SECRET = google_secret_manager_secret_version.db-secret-version["password"].name
       DB_NAME        = google_sql_database.db.name
       DB_USER        = google_sql_user.user.name
       COMMAND        = "up"
@@ -166,5 +194,5 @@ output "db_user" {
 }
 
 output "db_pass_secret" {
-  value = google_secret_manager_secret_version.db-pwd-initial.name
+  value = google_secret_manager_secret_version.db-secret-version["password"].name
 }
diff --git a/terraform/key_management.tf b/terraform/key_management.tf
index da5c387..9ca4696 100644
--- a/terraform/key_management.tf
+++ b/terraform/key_management.tf
@@ -16,6 +16,10 @@ resource "google_kms_key_ring" "export-signing" {
   project  = data.google_project.project.project_id
   name     = "export-signing"
   location = var.region
+
+  depends_on = [
+    google_project_service.services["cloudkms.googleapis.com"],
+  ]
 }
 
 resource "google_kms_crypto_key" "export-signer" {
diff --git a/terraform/main.tf b/terraform/main.tf
index 57b9db9..5d3ac36 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -30,10 +30,6 @@ data "google_project" "project" {
   project_id = var.project
 }
 
-data "google_compute_network" "default" {
-  name = "default"
-}
-
 resource "google_project_service" "services" {
   project = data.google_project.project.project_id
   for_each = toset([
@@ -41,35 +37,53 @@ resource "google_project_service" "services" {
     "cloudkms.googleapis.com",
     "cloudscheduler.googleapis.com",
     "compute.googleapis.com",
+    "containerregistry.googleapis.com",
     "run.googleapis.com",
     "secretmanager.googleapis.com",
     "servicenetworking.googleapis.com",
     "sql-component.googleapis.com",
     "sqladmin.googleapis.com",
     "storage-api.googleapis.com",
+    "vpcaccess.googleapis.com",
   ])
   service            = each.value
   disable_on_destroy = false
 }
 
 resource "google_compute_global_address" "private_ip_address" {
-  provider = google-beta
-
   name          = "private-ip-address"
   purpose       = "VPC_PEERING"
   address_type  = "INTERNAL"
   prefix_length = 16
-  network       = data.google_compute_network.default.self_link
+  network       = "projects/${data.google_project.project.project_id}/global/networks/default"
 
-  depends_on = [google_project_service.services["compute.googleapis.com"]]
+  depends_on = [
+    google_project_service.services["compute.googleapis.com"],
+  ]
 }
 
 resource "google_service_networking_connection" "private_vpc_connection" {
-  provider = google-beta
-
-  network                 = data.google_compute_network.default.self_link
+  network                 = "projects/${data.google_project.project.project_id}/global/networks/default"
   service                 = "servicenetworking.googleapis.com"
   reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
+
+  depends_on = [
+    google_project_service.services["compute.googleapis.com"],
+    google_project_service.services["servicenetworking.googleapis.com"],
+  ]
+}
+
+resource "google_vpc_access_connector" "connector" {
+  project       = data.google_project.project.project_id
+  name          = "serverless-vpc-connector"
+  region        = var.region
+  network       = "default"
+  ip_cidr_range = "10.8.0.0/28"
+
+  depends_on = [
+    google_project_service.services["compute.googleapis.com"],
+    google_project_service.services["vpcaccess.googleapis.com"],
+  ]
 }
 
 # Build creates the container images. It does not deploy or promote them.
@@ -113,27 +127,37 @@ locals {
       value = "10"
     },
     {
-      name  = "DB_PASSWORD"
-      value = "secret://${google_secret_manager_secret_version.db-pwd-initial.name}"
-    },
-    {
-      # NOTE: We disable SSL here because the Cloud Run services use the Cloud
-      # SQL proxy which runs on localhost. The proxy still uses a secure
-      # connection to Cloud SQL.
       name  = "DB_SSLMODE"
-      value = "disable"
+      value = "verify-ca"
     },
     {
       name  = "DB_HOST"
-      value = "/cloudsql/${data.google_project.project.project_id}:${var.region}:${google_sql_database_instance.db-inst.name}"
+      value = google_sql_database_instance.db-inst.private_ip_address
+    },
+    {
+      name  = "DB_NAME"
+      value = google_sql_database.db.name
+    },
+    {
+      name  = "DB_SSLCERT"
+      value = "secret://${google_secret_manager_secret_version.db-secret-version["sslcert"].id}?target=file"
+    },
+
+    {
+      name  = "DB_SSLKEY"
+      value = "secret://${google_secret_manager_secret_version.db-secret-version["sslkey"].id}?target=file"
+    },
+    {
+      name  = "DB_SSLROOTCERT"
+      value = "secret://${google_secret_manager_secret_version.db-secret-version["sslrootcert"].id}?target=file"
     },
     {
       name  = "DB_USER"
       value = google_sql_user.user.name
     },
     {
-      name  = "DB_NAME"
-      value = google_sql_database.db.name
+      name  = "DB_PASSWORD"
+      value = "secret://${google_secret_manager_secret_version.db-secret-version["password"].id}"
     },
   ]
 }
diff --git a/terraform/service_cleanup_export.tf b/terraform/service_cleanup_export.tf
index 375a209..6002848 100644
--- a/terraform/service_cleanup_export.tf
+++ b/terraform/service_cleanup_export.tf
@@ -32,16 +32,17 @@ resource "google_service_account_iam_member" "cloudbuild-deploy-cleanup-export"
   ]
 }
 
-resource "google_project_iam_member" "cleanup-export-cloudsql" {
-  project = data.google_project.project.project_id
-  role    = "roles/cloudsql.client"
-  member  = "serviceAccount:${google_service_account.cleanup-export.email}"
-}
-
-resource "google_secret_manager_secret_iam_member" "cleanup-export-db-pwd" {
+resource "google_secret_manager_secret_iam_member" "cleanup-export-db" {
   provider = google-beta
 
-  secret_id = google_secret_manager_secret.db-pwd.id
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
+
+  secret_id = google_secret_manager_secret.db-secret[each.key].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${google_service_account.cleanup-export.email}"
 }
@@ -82,15 +83,15 @@ resource "google_cloud_run_service" "cleanup-export" {
 
     metadata {
       annotations = {
-        "autoscaling.knative.dev/maxScale" : "1000",
-        "run.googleapis.com/cloudsql-instances" : google_sql_database_instance.db-inst.connection_name
+        "autoscaling.knative.dev/maxScale" : "1",
+        "run.googleapis.com/vpc-access-connector" : google_vpc_access_connector.connector.id
       }
     }
   }
 
   depends_on = [
     google_project_service.services["run.googleapis.com"],
-    google_project_service.services["sqladmin.googleapis.com"],
+    google_secret_manager_secret_iam_member.cleanup-export-db,
     null_resource.build,
   ]
 
@@ -142,5 +143,6 @@ resource "google_cloud_scheduler_job" "cleanup-export-worker" {
   depends_on = [
     google_app_engine_application.app,
     google_cloud_run_service_iam_member.cleanup-export-invoker,
+    google_project_service.services["cloudscheduler.googleapis.com"],
   ]
 }
diff --git a/terraform/service_cleanup_exposure.tf b/terraform/service_cleanup_exposure.tf
index d73c2f8..517e4d1 100644
--- a/terraform/service_cleanup_exposure.tf
+++ b/terraform/service_cleanup_exposure.tf
@@ -32,16 +32,17 @@ resource "google_service_account_iam_member" "cloudbuild-deploy-cleanup-exposure
   ]
 }
 
-resource "google_project_iam_member" "cleanup-exposure-cloudsql" {
-  project = data.google_project.project.project_id
-  role    = "roles/cloudsql.client"
-  member  = "serviceAccount:${google_service_account.cleanup-exposure.email}"
-}
-
-resource "google_secret_manager_secret_iam_member" "cleanup-exposure-db-pwd" {
+resource "google_secret_manager_secret_iam_member" "cleanup-exposure-db" {
   provider = google-beta
 
-  secret_id = google_secret_manager_secret.db-pwd.id
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
+
+  secret_id = google_secret_manager_secret.db-secret[each.key].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${google_service_account.cleanup-exposure.email}"
 }
@@ -76,15 +77,15 @@ resource "google_cloud_run_service" "cleanup-exposure" {
 
     metadata {
       annotations = {
-        "autoscaling.knative.dev/maxScale" : "1000",
-        "run.googleapis.com/cloudsql-instances" : google_sql_database_instance.db-inst.connection_name
+        "autoscaling.knative.dev/maxScale" : "1",
+        "run.googleapis.com/vpc-access-connector" : google_vpc_access_connector.connector.id
       }
     }
   }
 
   depends_on = [
     google_project_service.services["run.googleapis.com"],
-    google_project_service.services["sqladmin.googleapis.com"],
+    google_secret_manager_secret_iam_member.cleanup-exposure-db,
     null_resource.build,
   ]
 
@@ -136,5 +137,6 @@ resource "google_cloud_scheduler_job" "cleanup-exposure-worker" {
   depends_on = [
     google_app_engine_application.app,
     google_cloud_run_service_iam_member.cleanup-exposure-invoker,
+    google_project_service.services["cloudscheduler.googleapis.com"],
   ]
 }
diff --git a/terraform/service_export.tf b/terraform/service_export.tf
index 4d05d4e..4b70dcf 100644
--- a/terraform/service_export.tf
+++ b/terraform/service_export.tf
@@ -32,16 +32,17 @@ resource "google_service_account_iam_member" "cloudbuild-deploy-export" {
   ]
 }
 
-resource "google_project_iam_member" "export-cloudsql" {
-  project = data.google_project.project.project_id
-  role    = "roles/cloudsql.client"
-  member  = "serviceAccount:${google_service_account.export.email}"
-}
-
-resource "google_secret_manager_secret_iam_member" "export-db-pwd" {
+resource "google_secret_manager_secret_iam_member" "export-db" {
   provider = google-beta
 
-  secret_id = google_secret_manager_secret.db-pwd.id
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
+
+  secret_id = google_secret_manager_secret.db-secret[each.key].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${google_service_account.export.email}"
 }
@@ -76,6 +77,14 @@ resource "google_cloud_run_service" "export" {
           }
         }
 
+        dynamic "env" {
+          for_each = local.common_cloudrun_env_vars
+          content {
+            name  = env.value["name"]
+            value = env.value["value"]
+          }
+        }
+
         env {
           name  = "EXPORT_FILE_MAX_RECORDS"
           value = "100"
@@ -85,28 +94,22 @@ resource "google_cloud_run_service" "export" {
           name  = "EXPORT_BUCKET"
           value = google_storage_bucket.export.name
         }
-
-        dynamic "env" {
-          for_each = local.common_cloudrun_env_vars
-          content {
-            name  = env.value["name"]
-            value = env.value["value"]
-          }
-        }
       }
+
+      container_concurrency = 10
     }
 
     metadata {
       annotations = {
-        "autoscaling.knative.dev/maxScale" : "1000",
-        "run.googleapis.com/cloudsql-instances" : google_sql_database_instance.db-inst.connection_name
+        "autoscaling.knative.dev/maxScale" : "10",
+        "run.googleapis.com/vpc-access-connector" : google_vpc_access_connector.connector.id
       }
     }
   }
 
   depends_on = [
     google_project_service.services["run.googleapis.com"],
-    google_project_service.services["sqladmin.googleapis.com"],
+    google_secret_manager_secret_iam_member.export-db,
     null_resource.build,
   ]
 
@@ -158,6 +161,7 @@ resource "google_cloud_scheduler_job" "export-worker" {
   depends_on = [
     google_app_engine_application.app,
     google_cloud_run_service_iam_member.export-invoker,
+    google_project_service.services["cloudscheduler.googleapis.com"],
   ]
 }
 
@@ -183,5 +187,6 @@ resource "google_cloud_scheduler_job" "export-create-batches" {
   depends_on = [
     google_app_engine_application.app,
     google_cloud_run_service_iam_member.export-invoker,
+    google_project_service.services["cloudscheduler.googleapis.com"],
   ]
 }
diff --git a/terraform/service_exposure.tf b/terraform/service_exposure.tf
index 3488578..d7dec02 100644
--- a/terraform/service_exposure.tf
+++ b/terraform/service_exposure.tf
@@ -32,16 +32,17 @@ resource "google_service_account_iam_member" "cloudbuild-deploy-exposure" {
   ]
 }
 
-resource "google_project_iam_member" "exposure-cloudsql" {
-  project = data.google_project.project.project_id
-  role    = "roles/cloudsql.client"
-  member  = "serviceAccount:${google_service_account.exposure.email}"
-}
-
-resource "google_secret_manager_secret_iam_member" "exposure-db-pwd" {
+resource "google_secret_manager_secret_iam_member" "exposure-db" {
   provider = google-beta
 
-  secret_id = google_secret_manager_secret.db-pwd.id
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
+
+  secret_id = google_secret_manager_secret.db-secret[each.key].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${google_service_account.exposure.email}"
 }
@@ -76,15 +77,15 @@ resource "google_cloud_run_service" "exposure" {
 
     metadata {
       annotations = {
-        "autoscaling.knative.dev/maxScale" : "1000",
-        "run.googleapis.com/cloudsql-instances" : google_sql_database_instance.db-inst.connection_name
+        "autoscaling.knative.dev/maxScale" : "500",
+        "run.googleapis.com/vpc-access-connector" : google_vpc_access_connector.connector.id
       }
     }
   }
 
   depends_on = [
     google_project_service.services["run.googleapis.com"],
-    google_project_service.services["sqladmin.googleapis.com"],
+    google_secret_manager_secret_iam_member.exposure-db,
     null_resource.build,
   ]
 
diff --git a/terraform/service_federationin.tf b/terraform/service_federationin.tf
index 46569a4..a35c998 100644
--- a/terraform/service_federationin.tf
+++ b/terraform/service_federationin.tf
@@ -32,16 +32,17 @@ resource "google_service_account_iam_member" "cloudbuild-deploy-federationin" {
   ]
 }
 
-resource "google_project_iam_member" "federationin-cloudsql" {
-  project = data.google_project.project.project_id
-  role    = "roles/cloudsql.client"
-  member  = "serviceAccount:${google_service_account.federationin.email}"
-}
-
-resource "google_secret_manager_secret_iam_member" "federationin-db-pwd" {
+resource "google_secret_manager_secret_iam_member" "federationin" {
   provider = google-beta
 
-  secret_id = google_secret_manager_secret.db-pwd.id
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
+
+  secret_id = google_secret_manager_secret.db-secret[each.key].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${google_service_account.federationin.email}"
 }
@@ -76,15 +77,15 @@ resource "google_cloud_run_service" "federationin" {
 
     metadata {
       annotations = {
-        "autoscaling.knative.dev/maxScale" : "1000",
-        "run.googleapis.com/cloudsql-instances" : google_sql_database_instance.db-inst.connection_name
+        "autoscaling.knative.dev/maxScale" : "3",
+        "run.googleapis.com/vpc-access-connector" : google_vpc_access_connector.connector.id
       }
     }
   }
 
   depends_on = [
     google_project_service.services["run.googleapis.com"],
-    google_project_service.services["sqladmin.googleapis.com"],
+    google_secret_manager_secret_iam_member.federationin,
     null_resource.build,
   ]
 
diff --git a/terraform/service_federationout.tf b/terraform/service_federationout.tf
index 2ae98e6..53fce8d 100644
--- a/terraform/service_federationout.tf
+++ b/terraform/service_federationout.tf
@@ -22,12 +22,6 @@ resource "google_service_account" "federationout" {
   display_name = "Exposure Notification Federation (Out)"
 }
 
-resource "google_project_iam_member" "federationout-cloudsql" {
-  project = data.google_project.project.project_id
-  role    = "roles/cloudsql.client"
-  member  = "serviceAccount:${google_service_account.federationout.email}"
-}
-
 resource "google_service_account_iam_member" "cloudbuild-deploy-federationout" {
   service_account_id = google_service_account.federationout.id
   role               = "roles/iam.serviceAccountUser"
@@ -38,10 +32,17 @@ resource "google_service_account_iam_member" "cloudbuild-deploy-federationout" {
   ]
 }
 
-resource "google_secret_manager_secret_iam_member" "federationout-db-pwd" {
+resource "google_secret_manager_secret_iam_member" "federationout-db" {
   provider = google-beta
 
-  secret_id = google_secret_manager_secret.db-pwd.id
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
+
+  secret_id = google_secret_manager_secret.db-secret[each.key].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${google_service_account.federationout.email}"
 }
@@ -76,15 +77,15 @@ resource "google_cloud_run_service" "federationout" {
 
     metadata {
       annotations = {
-        "autoscaling.knative.dev/maxScale" : "1000",
-        "run.googleapis.com/cloudsql-instances" : google_sql_database_instance.db-inst.connection_name
+        "autoscaling.knative.dev/maxScale" : "5",
+        "run.googleapis.com/vpc-access-connector" : google_vpc_access_connector.connector.id
       }
     }
   }
 
   depends_on = [
     google_project_service.services["run.googleapis.com"],
-    google_project_service.services["sqladmin.googleapis.com"],
+    google_secret_manager_secret_iam_member.federationout-db,
     null_resource.build,
   ]
 
diff --git a/terraform/service_gcr_cleaner.tf b/terraform/service_gcr_cleaner.tf
index d9677ca..4a53f3c 100644
--- a/terraform/service_gcr_cleaner.tf
+++ b/terraform/service_gcr_cleaner.tf
@@ -36,6 +36,10 @@ resource "google_storage_bucket_iam_member" "gcr-cleaner-objectadmin" {
   bucket = "artifacts.${data.google_project.project.project_id}.appspot.com"
   role   = "roles/storage.objectAdmin"
   member = "serviceAccount:${google_service_account.gcr-cleaner.email}"
+
+  depends_on = [
+    google_project_service.services["containerregistry.googleapis.com"],
+  ]
 }
 
 resource "google_cloud_run_service" "gcr-cleaner" {
@@ -124,5 +128,6 @@ resource "google_cloud_scheduler_job" "gcr-cleaner-worker" {
   depends_on = [
     google_app_engine_application.app,
     google_cloud_run_service_iam_member.gcr-cleaner-invoker,
+    google_project_service.services["cloudscheduler.googleapis.com"],
   ]
 }
diff --git a/terraform/service_generate.tf b/terraform/service_generate.tf
index 8becb59..ac6c52c 100644
--- a/terraform/service_generate.tf
+++ b/terraform/service_generate.tf
@@ -32,16 +32,17 @@ resource "google_service_account_iam_member" "cloudbuild-deploy-generate" {
   ]
 }
 
-resource "google_project_iam_member" "generate-cloudsql" {
-  project = data.google_project.project.project_id
-  role    = "roles/cloudsql.client"
-  member  = "serviceAccount:${google_service_account.generate.email}"
-}
-
-resource "google_secret_manager_secret_iam_member" "generate-db-pwd" {
+resource "google_secret_manager_secret_iam_member" "generate-db" {
   provider = google-beta
 
-  secret_id = google_secret_manager_secret.db-pwd.id
+  for_each = toset([
+    "sslcert",
+    "sslkey",
+    "sslrootcert",
+    "password",
+  ])
+
+  secret_id = google_secret_manager_secret.db-secret[each.key].id
   role      = "roles/secretmanager.secretAccessor"
   member    = "serviceAccount:${google_service_account.generate.email}"
 }
@@ -76,15 +77,15 @@ resource "google_cloud_run_service" "generate" {
 
     metadata {
       annotations = {
-        "autoscaling.knative.dev/maxScale" : "1000",
-        "run.googleapis.com/cloudsql-instances" : google_sql_database_instance.db-inst.connection_name
+        "autoscaling.knative.dev/maxScale" : "1",
+        "run.googleapis.com/vpc-access-connector" : google_vpc_access_connector.connector.id
       }
     }
   }
 
   depends_on = [
     google_project_service.services["run.googleapis.com"],
-    google_project_service.services["sqladmin.googleapis.com"],
+    google_secret_manager_secret_iam_member.generate-db,
     null_resource.build,
   ]
 


Test patch:
diff --git a/internal/publish/model/exposure_model_test.go b/internal/publish/model/exposure_model_test.go
index 05625d7..3243f6f 100644
--- a/internal/publish/model/exposure_model_test.go
+++ b/internal/publish/model/exposure_model_test.go
@@ -41,7 +41,7 @@ func TestInvalidNew(t *testing.T) {
 	}
 
 	for i, c := range cases {
-		_, err := NewTransformer(c.maxKeys, time.Hour, time.Hour)
+		_, err := NewTransformer(c.maxKeys, time.Hour, time.Hour, false)
 		if err != nil && errMsg == "" {
 			t.Errorf("%v unexpected error: %v", i, err)
 		} else if err != nil && !strings.Contains(err.Error(), c.message) {
@@ -51,7 +51,7 @@ func TestInvalidNew(t *testing.T) {
 }
 
 func TestInvalidBase64(t *testing.T) {
-	transformer, err := NewTransformer(1, time.Hour*24, time.Hour)
+	transformer, err := NewTransformer(1, time.Hour*24, time.Hour, false)
 	if err != nil {
 		t.Fatalf("error creating transformer: %v", err)
 	}
@@ -108,15 +108,11 @@ func TestPublishValidation(t *testing.T) {
 	currentInterval := IntervalNumber(captureStartTime)
 	minInterval := IntervalNumber(captureStartTime.Add(-1 * maxAge))
 
-	tf, err := NewTransformer(2, maxAge, time.Hour)
-	if err != nil {
-		t.Fatalf("unepected error: %v", err)
-	}
-
 	cases := []struct {
-		name string
-		p    *Publish
-		m    string
+		name    string
+		p       *Publish
+		m       string
+		sameDay bool
 	}{
 		{
 			name: "no keys",
@@ -236,17 +232,39 @@ func TestPublishValidation(t *testing.T) {
 			},
 			m: fmt.Sprintf("interval number %v + interval count %v represents a key that is still valid, must end <= %v", currentInterval-143, 144, currentInterval),
 		},
+		{
+			name: "DEBUG: allow end of current UTC day still valid",
+			p: &Publish{
+				Keys: []ExposureKey{
+					{
+						Key:            encodeKey(generateKey(t)),
+						IntervalNumber: IntervalNumber(captureStartTime.UTC().Truncate(24 * time.Hour)),
+						IntervalCount:  144,
+					},
+				},
+			},
+			sameDay: true,
+		},
 	}
 
 	for _, c := range cases {
-		_, err = tf.TransformPublish(c.p, captureStartTime)
-		if err == nil {
-			t.Errorf("test '%v': want error '%v', got nil", c.name, c.m)
-		} else if !strings.Contains(err.Error(), c.m) {
-			t.Errorf("test '%v': want error '%v', got '%v'", c.name, c.m, err)
-		} else if err != nil && c.m == "" {
-			t.Errorf("test '%v': want error nil, got '%v'", c.name, err)
-		}
+		t.Run(c.name, func(t *testing.T) {
+			tf, err := NewTransformer(2, maxAge, time.Hour, c.sameDay)
+			if err != nil {
+				t.Fatalf("unepected error: %v", err)
+			}
+
+			_, err = tf.TransformPublish(c.p, captureStartTime)
+			if err == nil {
+				if c.m != "" {
+					t.Errorf("want error '%v', got nil", c.m)
+				}
+			} else if !strings.Contains(err.Error(), c.m) {
+				t.Errorf("want error '%v', got '%v'", c.m, err)
+			} else if err != nil && c.m == "" {
+				t.Errorf("want error nil, got '%v'", err)
+			}
+		})
 	}
 }
 
@@ -424,7 +442,7 @@ func TestTransform(t *testing.T) {
 	}
 
 	allowedAge := 14 * 24 * time.Hour
-	transformer, err := NewTransformer(10, allowedAge, time.Hour)
+	transformer, err := NewTransformer(10, allowedAge, time.Hour, false)
 	if err != nil {
 		t.Fatalf("NewTransformer returned unexpected error: %v", err)
 	}
@@ -492,7 +510,7 @@ func TestTransformOverlapping(t *testing.T) {
 		t.Run(c.name, func(t *testing.T) {
 			batchTime := captureStartTime.Add(time.Hour * 24 * 7)
 			allowedAge := 14 * 24 * time.Hour
-			transformer, err := NewTransformer(10, allowedAge, time.Hour)
+			transformer, err := NewTransformer(10, allowedAge, time.Hour, false)
 			if err != nil {
 				t.Fatalf("NewTransformer returned unexpected error: %v", err)
 			}