infra/staging/main.tf

58e6347eeb72 · 23 KB

locals {
  labels = merge(var.labels, {
    application = "openagents"
    environment = "staging"
    managed_by  = "terraform"
  })

  nodes = {
    "openagents-fleet-1" = "10.42.0.11"
    "openagents-fleet-2" = "10.42.0.12"
    "openagents-fleet-3" = "10.42.0.13"
  }

  required_services = toset([
    "artifactregistry.googleapis.com",
    "compute.googleapis.com",
    "dns.googleapis.com",
    "iam.googleapis.com",
    "logging.googleapis.com",
    "monitoring.googleapis.com",
    "run.googleapis.com",
    "secretmanager.googleapis.com",
    "servicenetworking.googleapis.com",
    "sqladmin.googleapis.com",
    "storage.googleapis.com"
  ])

  runtime_secrets = toset([
    "openagents-staging-web-config",
    "openagents-staging-fleet-config",
    "openagents-staging-builder-config",
    "openagents-staging-database-url",
    "openagents-staging-fleet-database-url",
    "openagents-staging-secret-key-base",
    "openagents-staging-github-client-secret",
    "openagents-staging-github-vault-active",
    "openagents-staging-github-vault-previous",
    "openagents-staging-openai-api-key",
    "openagents-staging-voice-recording-key",
    "openagents-staging-content-vault-active",
    "openagents-staging-forge-operator-token",
    "openagents-staging-release-cookie",
    "openagents-staging-scv-codex-operator-1",
    "openagents-staging-scv-codex-operator-2"
  ])

  scv_codex_credential_secrets = toset([
    "openagents-staging-scv-codex-operator-1",
    "openagents-staging-scv-codex-operator-2"
  ])

  application_secrets = toset([
    "openagents-staging-secret-key-base",
    "openagents-staging-github-client-secret",
    "openagents-staging-github-vault-active",
    "openagents-staging-github-vault-previous",
    "openagents-staging-openai-api-key",
    "openagents-staging-voice-recording-key",
    "openagents-staging-content-vault-active",
    "openagents-staging-forge-operator-token",
    "openagents-staging-release-cookie"
  ])
}

resource "google_project_service" "required" {
  for_each = local.required_services

  project            = var.staging_project_id
  service            = each.value
  disable_on_destroy = false
}

resource "google_compute_network" "staging" {
  name                    = "openagents-staging"
  auto_create_subnetworks = false
  routing_mode            = "REGIONAL"

  depends_on = [google_project_service.required]
}

resource "google_compute_subnetwork" "staging" {
  name                     = "openagents-staging-${var.region}"
  ip_cidr_range            = var.network_cidr
  region                   = var.region
  network                  = google_compute_network.staging.id
  private_ip_google_access = true

  log_config {
    aggregation_interval = "INTERVAL_5_SEC"
    flow_sampling        = 0.5
    metadata             = "INCLUDE_ALL_METADATA"
  }
}

resource "google_compute_global_address" "private_services" {
  name          = "openagents-staging-private-services"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = 20
  network       = google_compute_network.staging.id
}

resource "google_service_networking_connection" "private_services" {
  network                 = google_compute_network.staging.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_services.name]

  depends_on = [google_project_service.required]
}

resource "google_compute_router" "staging" {
  name    = "openagents-staging"
  region  = var.region
  network = google_compute_network.staging.id
}

resource "google_compute_router_nat" "staging" {
  name                               = "openagents-staging"
  router                             = google_compute_router.staging.name
  region                             = var.region
  nat_ip_allocate_option             = "AUTO_ONLY"
  source_subnetwork_ip_ranges_to_nat = "LIST_OF_SUBNETWORKS"

  subnetwork {
    name                    = google_compute_subnetwork.staging.id
    source_ip_ranges_to_nat = ["ALL_IP_RANGES"]
  }

  log_config {
    enable = true
    filter = "ERRORS_ONLY"
  }
}

resource "google_dns_managed_zone" "staging_private" {
  name        = "openagents-staging-internal"
  dns_name    = "staging.internal."
  description = "Private service discovery for isolated OpenAgents staging."
  visibility  = "private"

  private_visibility_config {
    networks {
      network_url = google_compute_network.staging.id
    }
  }

  depends_on = [google_project_service.required]
}

resource "google_compute_address" "fleet" {
  for_each = local.nodes

  name         = each.key
  region       = var.region
  address_type = "INTERNAL"
  subnetwork   = google_compute_subnetwork.staging.id
  address      = each.value
}

resource "google_dns_record_set" "fleet_nodes" {
  for_each = local.nodes

  name         = "${each.key}.staging.internal."
  type         = "A"
  ttl          = 30
  managed_zone = google_dns_managed_zone.staging_private.name
  rrdatas      = [google_compute_address.fleet[each.key].address]
}

resource "google_dns_record_set" "fleet_discovery" {
  name         = "openagents-fleet.staging.internal."
  type         = "A"
  ttl          = 30
  managed_zone = google_dns_managed_zone.staging_private.name
  rrdatas      = [for node in sort(keys(local.nodes)) : google_compute_address.fleet[node].address]
}

resource "google_dns_record_set" "deployer" {
  name         = "openagents-deployer.staging.internal."
  type         = "A"
  ttl          = 30
  managed_zone = google_dns_managed_zone.staging_private.name
  rrdatas      = [google_compute_address.deployer.address]
}

resource "google_compute_firewall" "fleet_internal" {
  name      = "openagents-staging-fleet-internal"
  network   = google_compute_network.staging.name
  direction = "INGRESS"
  priority  = 900

  source_ranges = [var.network_cidr]
  target_tags   = ["openagents-staging-fleet", "openagents-staging-controller"]

  allow {
    protocol = "tcp"
    ports    = ["4000", "4369", "9100-9115"]
  }

  log_config {
    metadata = "INCLUDE_ALL_METADATA"
  }
}

resource "google_compute_firewall" "iap_ssh" {
  name      = "openagents-staging-iap-ssh"
  network   = google_compute_network.staging.name
  direction = "INGRESS"
  priority  = 900

  source_ranges = ["35.235.240.0/20"]
  target_tags   = ["openagents-staging-fleet", "openagents-staging-controller"]

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  log_config {
    metadata = "INCLUDE_ALL_METADATA"
  }
}

resource "google_service_account" "web" {
  account_id   = "openagents-staging-web"
  display_name = "OpenAgents staging web"
  description  = "Runs only the staging web acceptance lane."
}

resource "google_service_account" "fleet" {
  account_id   = "openagents-staging-fleet"
  display_name = "OpenAgents staging fleet"
  description  = "Runs only the three staging BEAM nodes."
}

resource "google_service_account" "deployer" {
  account_id   = "openagents-staging-deployer"
  display_name = "OpenAgents staging deployer"
  description  = "Performs one-node-at-a-time replacement in staging."
}

resource "google_project_iam_member" "web" {
  for_each = toset([
    "roles/cloudsql.client",
    "roles/logging.logWriter",
    "roles/monitoring.metricWriter"
  ])

  project = var.staging_project_id
  role    = each.value
  member  = google_service_account.web.member
}

resource "google_project_iam_member" "fleet" {
  for_each = toset([
    "roles/artifactregistry.reader",
    "roles/cloudsql.client",
    "roles/logging.logWriter",
    "roles/monitoring.metricWriter"
  ])

  project = var.staging_project_id
  role    = each.value
  member  = google_service_account.fleet.member
}

resource "google_project_iam_custom_role" "deployer" {
  role_id     = "openagentsStagingDeployer"
  title       = "OpenAgents staging deployer"
  description = "Resets one staging fleet instance after updating its exact image metadata."
  stage       = "GA"

  permissions = [
    "compute.instances.get",
    "compute.instances.reset",
    "compute.instances.setMetadata",
    "compute.zoneOperations.get"
  ]
}

resource "google_project_iam_member" "deployer" {
  project = var.staging_project_id
  role    = google_project_iam_custom_role.deployer.id
  member  = google_service_account.deployer.member
}

# Compute requires actAs on an instance's attached service account when the
# deployer replaces metadata. Scope that permission to the staging fleet
# identity; the deployer has no instance-creation or service-account mutation
# permission.
resource "google_service_account_iam_member" "deployer_fleet_act_as" {
  service_account_id = google_service_account.fleet.name
  role               = "roles/iam.serviceAccountUser"
  member             = google_service_account.deployer.member
}

resource "google_project_iam_member" "deployer_runtime" {
  for_each = toset([
    "roles/artifactregistry.reader",
    "roles/logging.logWriter",
    "roles/monitoring.metricWriter"
  ])

  project = var.staging_project_id
  role    = each.value
  member  = google_service_account.deployer.member
}

resource "google_project_iam_member" "iap_tunnel" {
  for_each = var.iap_ssh_members

  project = var.staging_project_id
  role    = "roles/iap.tunnelResourceAccessor"
  member  = each.value
}

resource "google_project_iam_member" "os_login" {
  for_each = var.iap_ssh_members

  project = var.staging_project_id
  role    = "roles/compute.osLogin"
  member  = each.value
}

resource "google_secret_manager_secret" "runtime" {
  for_each = local.runtime_secrets

  secret_id = each.value
  labels    = local.labels

  replication {
    auto {}
  }

  depends_on = [google_project_service.required]
}

resource "google_secret_manager_secret_iam_member" "web_env" {
  secret_id = google_secret_manager_secret.runtime["openagents-staging-web-config"].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.web.member
}

resource "google_secret_manager_secret_iam_member" "web_database" {
  secret_id = google_secret_manager_secret.runtime["openagents-staging-database-url"].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.web.member
}

resource "google_secret_manager_secret_iam_member" "fleet_env" {
  secret_id = google_secret_manager_secret.runtime["openagents-staging-fleet-config"].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.fleet.member
}

resource "google_secret_manager_secret_iam_member" "fleet_database" {
  secret_id = google_secret_manager_secret.runtime["openagents-staging-fleet-database-url"].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.fleet.member
}

resource "google_secret_manager_secret_iam_member" "builder_env" {
  secret_id = google_secret_manager_secret.runtime["openagents-staging-builder-config"].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.fleet.member
}

resource "google_secret_manager_secret_iam_member" "web_secrets" {
  for_each = local.application_secrets

  secret_id = google_secret_manager_secret.runtime[each.value].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.web.member
}

resource "google_secret_manager_secret_iam_member" "fleet_secrets" {
  for_each = local.application_secrets

  secret_id = google_secret_manager_secret.runtime[each.value].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.fleet.member
}

resource "google_secret_manager_secret_iam_member" "deployer_cookie" {
  secret_id = google_secret_manager_secret.runtime["openagents-staging-release-cookie"].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.deployer.member
}

resource "google_secret_manager_secret_iam_member" "scv_codex_credential_add" {
  for_each = local.scv_codex_credential_secrets

  secret_id = google_secret_manager_secret.runtime[each.value].id
  role      = "roles/secretmanager.secretVersionAdder"
  member    = google_service_account.web.member
}

resource "google_secret_manager_secret_iam_member" "scv_codex_credential_read" {
  for_each = local.scv_codex_credential_secrets

  secret_id = google_secret_manager_secret.runtime[each.value].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.web.member
}

resource "google_secret_manager_secret_iam_member" "fleet_scv_codex_credential_add" {
  for_each = local.scv_codex_credential_secrets

  secret_id = google_secret_manager_secret.runtime[each.value].id
  role      = "roles/secretmanager.secretVersionAdder"
  member    = google_service_account.fleet.member
}

resource "google_secret_manager_secret_iam_member" "fleet_scv_codex_credential_read" {
  for_each = local.scv_codex_credential_secrets

  secret_id = google_secret_manager_secret.runtime[each.value].id
  role      = "roles/secretmanager.secretAccessor"
  member    = google_service_account.fleet.member
}

resource "google_artifact_registry_repository" "openagents" {
  location      = var.region
  repository_id = "openagents-staging"
  description   = "Immutable OpenAgents staging release and builder images."
  format        = "DOCKER"
  labels        = local.labels

  cleanup_policy_dry_run = true
  deletion_policy        = "PREVENT"

  docker_config {
    immutable_tags = true
  }

  depends_on = [google_project_service.required]
}

resource "google_storage_bucket" "artifacts" {
  name                        = "${var.staging_project_id}-openagents-artifacts"
  location                    = upper(var.region)
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = false
  labels                      = local.labels

  versioning {
    enabled = true
  }

  soft_delete_policy {
    retention_duration_seconds = 604800
  }

  depends_on = [google_project_service.required]
}

resource "google_storage_bucket" "wal" {
  name                        = "${var.staging_project_id}-openagents-forge-wal"
  location                    = upper(var.region)
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = false
  labels                      = local.labels

  versioning {
    enabled = true
  }

  soft_delete_policy {
    retention_duration_seconds = 604800
  }

  depends_on = [google_project_service.required]
}

resource "google_storage_bucket" "recordings" {
  name                        = "${var.staging_project_id}-openagents-recordings"
  location                    = upper(var.region)
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = false
  labels                      = local.labels

  lifecycle_rule {
    condition {
      age = 30
    }
    action {
      type = "Delete"
    }
  }

  soft_delete_policy {
    retention_duration_seconds = 604800
  }

  depends_on = [google_project_service.required]
}

resource "google_storage_bucket" "evidence" {
  name                        = "${var.staging_project_id}-openagents-evidence"
  location                    = upper(var.region)
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = false
  labels                      = local.labels

  versioning {
    enabled = true
  }

  retention_policy {
    retention_period = 2592000
    is_locked        = false
  }

  soft_delete_policy {
    retention_duration_seconds = 604800
  }

  depends_on = [google_project_service.required]
}

resource "google_storage_bucket_iam_member" "fleet_artifacts" {
  bucket = google_storage_bucket.artifacts.name
  role   = "roles/storage.objectAdmin"
  member = google_service_account.fleet.member
}

resource "google_storage_bucket_iam_member" "fleet_wal" {
  bucket = google_storage_bucket.wal.name
  role   = "roles/storage.objectAdmin"
  member = google_service_account.fleet.member
}

resource "google_storage_bucket_iam_member" "web_wal" {
  bucket = google_storage_bucket.wal.name
  role   = "roles/storage.objectViewer"
  member = google_service_account.web.member
}

resource "google_storage_bucket_iam_member" "web_recordings" {
  bucket = google_storage_bucket.recordings.name
  role   = "roles/storage.objectAdmin"
  member = google_service_account.web.member
}

resource "google_storage_bucket_iam_member" "fleet_recordings" {
  bucket = google_storage_bucket.recordings.name
  role   = "roles/storage.objectAdmin"
  member = google_service_account.fleet.member
}

resource "google_storage_bucket_iam_member" "deployer_evidence" {
  bucket = google_storage_bucket.evidence.name
  role   = "roles/storage.objectCreator"
  member = google_service_account.deployer.member
}

resource "google_sql_database_instance" "staging" {
  name                = "openagents-staging-postgres"
  region              = var.region
  database_version    = "POSTGRES_17"
  deletion_protection = true

  settings {
    edition                     = "ENTERPRISE"
    tier                        = var.database_tier
    availability_type           = "ZONAL"
    disk_type                   = "PD_SSD"
    disk_size                   = 20
    disk_autoresize             = true
    deletion_protection_enabled = true
    user_labels                 = local.labels

    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
      start_time                     = "05:00"
      transaction_log_retention_days = 7

      backup_retention_settings {
        retained_backups = 7
        retention_unit   = "COUNT"
      }
    }

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.staging.id
      ssl_mode        = "ENCRYPTED_ONLY"
    }

    insights_config {
      query_insights_enabled  = true
      query_string_length     = 1024
      record_application_tags = true
    }

    maintenance_window {
      day          = 7
      hour         = 6
      update_track = "stable"
    }
  }

  depends_on = [
    google_project_service.required,
    google_service_networking_connection.private_services
  ]
}

resource "google_sql_database" "openagents" {
  name            = "openagents_staging"
  instance        = google_sql_database_instance.staging.name
  deletion_policy = "ABANDON"
}

resource "google_sql_user" "openagents" {
  name                = "openagents_staging"
  instance            = google_sql_database_instance.staging.name
  password_wo         = var.database_password
  password_wo_version = var.database_password_version
  deletion_policy     = "ABANDON"
}

data "google_compute_image" "cos" {
  family  = "cos-stable"
  project = "cos-cloud"
}

resource "google_compute_disk" "fleet_state" {
  for_each = local.nodes

  name   = "${each.key}-state"
  type   = "pd-balanced"
  zone   = var.zone
  size   = var.fleet_state_disk_gib
  labels = local.labels

  depends_on = [google_project_service.required]
}

resource "google_compute_instance" "fleet" {
  for_each = local.nodes

  name                      = each.key
  zone                      = var.zone
  machine_type              = var.fleet_machine_type
  allow_stopping_for_update = true
  can_ip_forward            = false
  deletion_protection       = true
  tags                      = ["openagents-staging-fleet"]
  labels                    = merge(local.labels, { lane = "distributed" })

  boot_disk {
    auto_delete = true

    initialize_params {
      image = data.google_compute_image.cos.self_link
      size  = 20
      type  = "pd-balanced"
    }
  }

  attached_disk {
    source      = google_compute_disk.fleet_state[each.key].id
    device_name = "openagents-state"
    mode        = "READ_WRITE"
  }

  network_interface {
    subnetwork = google_compute_subnetwork.staging.id
    network_ip = google_compute_address.fleet[each.key].address
  }

  metadata = {
    block-project-ssh-keys    = "TRUE"
    enable-oslogin            = "TRUE"
    openagents-environment    = "staging"
    openagents-image          = ""
    openagents-image-digest   = ""
    openagents-builder-image  = ""
    openagents-builder-digest = ""
    openagents-sha            = ""
    openagents-runtime-secret = google_secret_manager_secret.runtime["openagents-staging-fleet-config"].secret_id
    openagents-builder-secret = google_secret_manager_secret.runtime["openagents-staging-builder-config"].secret_id
    startup-script = templatefile("${path.module}/templates/fleet-startup.sh.tftpl", {
      network_cidr           = var.network_cidr
      project_id             = var.staging_project_id
      region                 = var.region
      forge_mirror_urls_json = var.forge_mirror_urls_json
    })
  }

  service_account {
    email  = google_service_account.fleet.email
    scopes = ["cloud-platform"]
  }

  scheduling {
    automatic_restart   = true
    on_host_maintenance = "MIGRATE"
    provisioning_model  = "STANDARD"
  }

  shielded_instance_config {
    enable_secure_boot          = true
    enable_vtpm                 = true
    enable_integrity_monitoring = true
  }

  depends_on = [
    google_project_service.required,
    google_compute_router_nat.staging,
    google_secret_manager_secret_iam_member.fleet_env
  ]
}

resource "google_compute_address" "deployer" {
  name         = "openagents-staging-deployer"
  region       = var.region
  address_type = "INTERNAL"
  subnetwork   = google_compute_subnetwork.staging.id
  address      = "10.42.0.20"
}

resource "google_compute_instance" "deployer" {
  name                      = "openagents-staging-deployer"
  zone                      = var.zone
  machine_type              = "e2-small"
  allow_stopping_for_update = true
  can_ip_forward            = false
  deletion_protection       = true
  tags                      = ["openagents-staging-controller"]
  labels                    = merge(local.labels, { lane = "deployer" })

  boot_disk {
    auto_delete = true

    initialize_params {
      image = data.google_compute_image.cos.self_link
      size  = 20
      type  = "pd-balanced"
    }
  }

  network_interface {
    subnetwork = google_compute_subnetwork.staging.id
    network_ip = google_compute_address.deployer.address
  }

  metadata = {
    block-project-ssh-keys      = "TRUE"
    enable-oslogin              = "TRUE"
    openagents-environment      = "staging"
    openagents-controller-image = ""
    openagents-controller-sha   = ""
    openagents-cookie-secret    = google_secret_manager_secret.runtime["openagents-staging-release-cookie"].secret_id
    startup-script = templatefile("${path.module}/templates/deployer-startup.sh.tftpl", {
      image_repository       = "${var.region}-docker.pkg.dev/${var.staging_project_id}/openagents-staging/openagents"
      production_project_id  = var.production_project_id
      project_id             = var.staging_project_id
      region                 = var.region
      rolling_instances_json = jsonencode({ for instance, ip in local.nodes : "openagents@${ip}" => instance })
      zone                   = var.zone
    })
  }

  service_account {
    email  = google_service_account.deployer.email
    scopes = ["cloud-platform"]
  }

  scheduling {
    automatic_restart   = true
    on_host_maintenance = "MIGRATE"
    provisioning_model  = "STANDARD"
  }

  shielded_instance_config {
    enable_secure_boot          = true
    enable_vtpm                 = true
    enable_integrity_monitoring = true
  }

  depends_on = [
    google_project_service.required,
    google_compute_router_nat.staging,
    google_project_iam_member.deployer,
    google_secret_manager_secret_iam_member.deployer_cookie
  ]
}