infra/main.tf (295 lines of code) (raw):
/**
* 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.
*/
locals {
nextauth_url = "http://${google_compute_global_address.default.address}"
has_default_firestore_database = length(data.google_cloud_asset_resources_search_all.default_firestore_database.results) > 0 ? true : false
}
### GCS bucket ###
resource "random_id" "bucket_prefix" {
byte_length = 6
}
resource "random_id" "service_account_prefix" {
byte_length = 3
}
resource "google_storage_bucket" "default" {
project = var.project_id
name = "${var.deployment_name}-bucket-${random_id.bucket_prefix.hex}"
location = "US"
storage_class = "STANDARD"
uniform_bucket_level_access = true
force_destroy = true
labels = var.labels
}
resource "google_storage_bucket_iam_member" "default" {
bucket = google_storage_bucket.default.name
role = "roles/storage.objectViewer"
member = "allUsers"
}
resource "google_storage_bucket_object" "icons" {
for_each = fileset(path.module, "google-cloud-icons/*.svg")
name = each.value
source = "${path.module}/${each.value}"
content_type = "image/svg+xml"
bucket = google_storage_bucket.default.name
}
resource "google_compute_backend_bucket" "default" {
project = var.project_id
name = "${var.deployment_name}-backend-bucket"
description = "Backend bucket for ${var.deployment_name}"
bucket_name = google_storage_bucket.default.name
enable_cdn = true
cdn_policy {
cache_mode = "CACHE_ALL_STATIC"
client_ttl = 3600
default_ttl = 3600
max_ttl = 86400
negative_caching = true
serve_while_stale = 86400
}
depends_on = [
time_sleep.project_services,
time_sleep.cloud_run_v2_service
]
}
### Secret Manager resources ###
resource "random_id" "nextauth_secret" {
byte_length = 32
}
resource "google_secret_manager_secret" "nextauth_secret" {
project = var.project_id
secret_id = "${var.deployment_name}-nextauth-secret"
replication {
auto {}
}
labels = var.labels
depends_on = [
time_sleep.project_services
]
}
resource "google_secret_manager_secret_version" "nextauth_secret" {
secret = google_secret_manager_secret.nextauth_secret.id
secret_data = random_id.nextauth_secret.b64_std
depends_on = [
google_secret_manager_secret.nextauth_secret
]
}
resource "time_sleep" "nextauth_secret" {
depends_on = [
google_secret_manager_secret_version.nextauth_secret
]
create_duration = "15s"
}
resource "google_secret_manager_secret_iam_binding" "nextauth_secret" {
project = var.project_id
secret_id = google_secret_manager_secret.nextauth_secret.secret_id
role = "roles/secretmanager.secretAccessor"
members = [
"serviceAccount:${google_service_account.cloud_run.email}",
]
depends_on = [
google_secret_manager_secret.nextauth_secret
]
}
### Cloud Run service resources and network endpoint group ###
#### Service Account
resource "google_service_account" "cloud_run" {
project = var.project_id
account_id = "run-service-account-${random_id.service_account_prefix.hex}"
display_name = "${var.deployment_name} Cloud Run service Service Account."
depends_on = [
time_sleep.project_services
]
}
#### Cloud Run IAM
resource "google_project_iam_member" "run_datastore_owner" {
project = var.project_id
role = "roles/datastore.owner"
member = "serviceAccount:${google_service_account.cloud_run.email}"
}
resource "google_cloud_run_v2_service" "default" {
project = var.project_id
name = var.deployment_name
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
deletion_protection = false
template {
containers {
image = var.initial_run_image
env {
name = "PROJECT_ID"
value = var.project_id
}
env {
name = "NEXTAUTH_SECRET"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.nextauth_secret.secret_id
version = "latest"
}
}
}
env {
name = "NEXTAUTH_URL"
value = local.nextauth_url
}
}
service_account = google_service_account.cloud_run.email
}
labels = var.labels
depends_on = [
time_sleep.nextauth_secret
]
}
resource "time_sleep" "cloud_run_v2_service" {
depends_on = [
google_cloud_run_v2_service.default
]
create_duration = "45s"
}
data "google_iam_policy" "noauth" {
binding {
role = "roles/run.invoker"
members = [
"allUsers",
]
}
}
resource "google_cloud_run_service_iam_policy" "noauth" {
project = google_cloud_run_v2_service.default.project
location = google_cloud_run_v2_service.default.location
service = google_cloud_run_v2_service.default.name
policy_data = data.google_iam_policy.noauth.policy_data
depends_on = [
time_sleep.cloud_run_v2_service
]
}
resource "google_compute_region_network_endpoint_group" "default" {
project = var.project_id
region = var.region
name = "${var.deployment_name}-network-endpoint-group"
network_endpoint_type = "SERVERLESS"
cloud_run {
service = google_cloud_run_v2_service.default.name
}
depends_on = [
time_sleep.project_services,
time_sleep.cloud_run_v2_service
]
}
### External loadbalancer ###
resource "google_compute_global_address" "default" {
project = var.project_id
name = "${var.deployment_name}-reserved-ip"
depends_on = [
time_sleep.project_services
]
}
resource "google_compute_url_map" "default" {
project = var.project_id
name = "${var.deployment_name}-http-load-balancer"
default_service = google_compute_backend_service.default.id
host_rule {
hosts = [google_compute_global_address.default.address]
path_matcher = "ip4addr"
}
path_matcher {
name = "ip4addr"
default_service = google_compute_backend_service.default.id
path_rule {
paths = ["/google-cloud-icons/*"]
service = google_compute_backend_bucket.default.id
}
}
depends_on = [
time_sleep.cloud_run_v2_service
]
}
resource "google_compute_backend_service" "default" {
project = var.project_id
name = "${var.deployment_name}-run-backend-service"
port_name = "http"
protocol = "HTTP"
load_balancing_scheme = "EXTERNAL_MANAGED"
enable_cdn = true
backend {
group = google_compute_region_network_endpoint_group.default.id
}
log_config {
enable = true
sample_rate = 1
}
cdn_policy {
cache_mode = "CACHE_ALL_STATIC"
client_ttl = "3600"
default_ttl = "3600"
max_ttl = "86400"
negative_caching = true
serve_while_stale = "86400"
signed_url_cache_max_age_sec = 0
cache_key_policy {
include_host = true
include_http_headers = []
include_named_cookies = []
include_protocol = true
include_query_string = true
query_string_blacklist = []
query_string_whitelist = []
}
}
}
resource "google_compute_target_http_proxy" "default" {
project = var.project_id
name = "${var.deployment_name}-http-proxy"
url_map = google_compute_url_map.default.id
}
resource "google_compute_global_forwarding_rule" "http" {
project = var.project_id
name = "${var.deployment_name}-http-forwarding-rule"
load_balancing_scheme = "EXTERNAL_MANAGED"
port_range = "80"
target = google_compute_target_http_proxy.default.id
ip_address = google_compute_global_address.default.id
labels = var.labels
}
# It may take more than 2 minutes for the newly provisioned load balancer
# to forward requests to the Cloud Run service. The following data source
# allows for terraform apply to finish running when the end-point resolves
data "http" "load_balancer_warm_up" {
url = "http://${google_compute_global_address.default.address}/"
# Attempt retry every 20 seconds 17 times, totaling to a 6 minute timeout.
retry {
attempts = 17
max_delay_ms = 20000
min_delay_ms = 20000
}
# Begin trying after load balancer resources are created.
depends_on = [
google_compute_global_address.default,
google_compute_url_map.default,
google_compute_backend_service.default,
google_compute_target_http_proxy.default,
google_compute_global_forwarding_rule.http
]
}
### Firestore ###
# The following checks Asset Inventory for an existing Firestore database
data "google_cloud_asset_resources_search_all" "default_firestore_database" {
provider = google-beta
scope = "projects/${var.project_id}"
query = "displayName:(default)"
asset_types = [
"firestore.googleapis.com/Database"
]
}
# If a Firestore database exists on the project, Terraform will skip this resource
resource "google_firestore_database" "database" {
count = var.init_firestore && !local.has_default_firestore_database ? 1 : 0
project = var.project_id
name = "(default)"
location_id = "nam5"
type = "FIRESTORE_NATIVE"
concurrency_mode = "PESSIMISTIC"
app_engine_integration_mode = "DISABLED"
depends_on = [
time_sleep.project_services
]
}