infra/run.tf
Rama main · réplica de lectura del repositorio original.
locals {
public_urls = {
API_URL = "https://api.${local.domain}"
MAIN_URL = "https://${local.domain}"
}
web_service = {
account = "tamery-web"
memory = "512Mi"
min = 0
max = 5
concurrency = 80
cpu_idle = true
timeout = "300s"
egress = null
tags = []
env = {}
secrets = []
health = null
}
services = {
app = local.web_service
main = local.web_service
docs = local.web_service
share = local.web_service
# Holds transactions and the cancel map in memory, so it stays one instance and takes every user's queries.
proxy = merge(local.web_service, {
account = "tamery-proxy"
memory = "1Gi"
max = 1
concurrency = 500
cpu_idle = false
timeout = "3600s"
egress = "ALL_TRAFFIC"
tags = ["tamery-proxy"]
env = local.public_urls
secrets = ["PROXY_SHARED_SECRET"]
})
api = merge(local.web_service, {
account = "tamery-api"
memory = "1Gi"
min = 1
# Each open client holds up to seven sync streams, and each one counts.
concurrency = 1000
cpu_idle = false
timeout = "3600s"
egress = "PRIVATE_RANGES_ONLY"
env = merge(local.public_urls, { REDIS_CA = google_redis_instance.tamery.server_ca_certs[0].cert })
secrets = local.api_secrets
# A revision that cannot reach its database must never take traffic.
health = "/health"
})
}
jobs = {
api-migrate = {
account = "tamery-jobs"
command = ["pnpm"]
args = ["--filter", "@tamery/db", "run", "drizzle:migrate"]
timeout = "600s"
retries = 0
env = {}
secrets = ["DATABASE_URL"]
}
api-purge-deleted-records = {
account = "tamery-jobs"
command = ["pnpm"]
args = ["--filter", "@tamery/db", "purge-deleted-records"]
timeout = "1800s"
retries = 1
env = { NODE_ENV = "production" }
secrets = ["DATABASE_URL"]
}
api-rotate = {
account = "tamery-api"
command = ["bun"]
args = ["run", "rotate:encryption-secret"]
timeout = "3600s"
retries = 0
env = { NODE_ENV = "production" }
secrets = ["DATABASE_URL", "ENCRYPTION_SECRET", "USER_KEY_SECRET"]
}
}
}
resource "google_cloud_run_v2_service" "this" {
for_each = local.services
name = each.key
location = local.region
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
# Public by design, so all three must stay together: the load balancer's mTLS is the only way in.
invoker_iam_disabled = true
default_uri_disabled = true
template {
service_account = google_service_account.this[each.value.account].email
timeout = each.value.timeout
max_instance_request_concurrency = each.value.concurrency
scaling {
min_instance_count = each.value.min
max_instance_count = each.value.max
}
dynamic "vpc_access" {
for_each = each.value.egress == null ? [] : [each.value.egress]
content {
egress = vpc_access.value
network_interfaces {
network = google_compute_network.tamery.name
subnetwork = google_compute_subnetwork.tamery.name
tags = each.value.tags
}
}
}
containers {
image = "${local.registry}/${each.key}:bootstrap"
resources {
limits = {
cpu = "1"
memory = each.value.memory
}
cpu_idle = each.value.cpu_idle
startup_cpu_boost = true
}
ports {
name = "http1"
container_port = 8080
}
dynamic "env" {
for_each = each.value.env
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = toset(each.value.secrets)
content {
name = env.value
value_source {
secret_key_ref {
secret = env.value
version = "latest"
}
}
}
}
dynamic "startup_probe" {
for_each = each.value.health == null ? [] : [each.value.health]
content {
timeout_seconds = 5
period_seconds = 5
failure_threshold = 24
http_get {
path = startup_probe.value
port = 8080
}
}
}
}
}
lifecycle {
# deploy.yml owns the image, and SECRETS.md names a revision to pick up a new secret value.
ignore_changes = [client, client_version, template[0].revision, template[0].containers[0].image]
}
depends_on = [google_secret_manager_secret_iam_member.access]
}
resource "google_cloud_run_v2_job" "this" {
for_each = local.jobs
name = each.key
location = local.region
template {
template {
service_account = google_service_account.this[each.value.account].email
timeout = each.value.timeout
max_retries = each.value.retries
vpc_access {
egress = "PRIVATE_RANGES_ONLY"
network_interfaces {
network = google_compute_network.tamery.name
subnetwork = google_compute_subnetwork.tamery.name
}
}
containers {
image = "${local.registry}/api:bootstrap"
command = each.value.command
args = each.value.args
resources {
limits = {
cpu = "1000m"
memory = "1Gi"
}
}
dynamic "env" {
for_each = each.value.env
content {
name = env.key
value = env.value
}
}
dynamic "env" {
for_each = toset(each.value.secrets)
content {
name = env.value
value_source {
secret_key_ref {
secret = env.value
version = "latest"
}
}
}
}
}
}
}
lifecycle {
ignore_changes = [client, client_version, template[0].template[0].containers[0].image]
}
depends_on = [google_secret_manager_secret_iam_member.access]
}
resource "google_cloud_run_v2_job_iam_member" "scheduler_runs_purge" {
name = google_cloud_run_v2_job.this["api-purge-deleted-records"].name
location = local.region
role = "roles/run.jobsExecutor"
member = google_service_account.this["tamery-scheduler"].member
}
resource "google_cloud_scheduler_job" "purge_deleted_records" {
name = "api-purge-deleted-records"
region = local.region
schedule = "0 3 * * *"
time_zone = "Etc/UTC"
paused = false
http_target {
http_method = "POST"
uri = "https://run.googleapis.com/v2/${google_cloud_run_v2_job.this["api-purge-deleted-records"].id}:run"
oauth_token {
service_account_email = google_service_account.this["tamery-scheduler"].email
}
}
depends_on = [google_cloud_run_v2_job_iam_member.scheduler_runs_purge]
}