diff --git a/terraform/gcp_cloudrun/main.tf b/terraform/gcp_cloudrun/main.tf index da1a5ee..2206b0e 100644 --- a/terraform/gcp_cloudrun/main.tf +++ b/terraform/gcp_cloudrun/main.tf @@ -79,7 +79,7 @@ resource "google_cloud_run_v2_service" "openclaw" { depends_on = ["rclone-sync"] image = local.effective_container_image command = ["/bin/sh"] - args = ["-lc", "openclaw gateway run --bind lan --port \"$${PORT:-8080}\" --allow-unconfigured"] + args = ["-lc", "mkdir -p /home/node/.openclaw/agents/main/agent /home/node/.openclaw/credentials; [ -n \"$OPENCLAW_JSON\" ] && echo \"$OPENCLAW_JSON\" > /home/node/.openclaw/openclaw.json; [ -n \"$TELEGRAM_ALLOW_FROM\" ] && echo \"$TELEGRAM_ALLOW_FROM\" > /home/node/.openclaw/credentials/telegram-allowFrom.json; printf '{\"openrouter\":{\"apiKey\":\"%s\"}}' \"$OPENROUTER_API_KEY\" > /home/node/.openclaw/agents/main/agent/auth-profiles.json; printf '{\"providers\":{\"openrouter\":{\"baseUrl\":\"https://openrouter.ai/api/v1\",\"api\":\"openai-completions\",\"apiKey\":\"OPENROUTER_API_KEY\"}}}' > /home/node/.openclaw/agents/main/agent/models.json; exec openclaw gateway run --bind lan --port \"$${PORT:-8080}\" --allow-unconfigured"] ports { container_port = 8080 @@ -95,7 +95,7 @@ resource "google_cloud_run_v2_service" "openclaw" { volume_mounts { name = "openclaw-runtime" - mount_path = "/tmp/openclaw-state" + mount_path = "/home/node/.openclaw" } env { @@ -105,12 +105,12 @@ resource "google_cloud_run_v2_service" "openclaw" { env { name = "OPENCLAW_STATE_DIR" - value = "/tmp/openclaw-state" + value = "/home/node/.openclaw" } env { name = "OPENCLAW_CONFIG_PATH" - value = "/tmp/openclaw-state/openclaw.json" + value = "/home/node/.openclaw/openclaw.json" } env { @@ -123,6 +123,16 @@ resource "google_cloud_run_v2_service" "openclaw" { value = "8080" } + env { + name = "OPENCLAW_JSON" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.openclaw_json.secret_id + version = "latest" + } + } + } + env { name = "OPENROUTER_API_KEY" value_source { @@ -166,6 +176,19 @@ resource "google_cloud_run_v2_service" "openclaw" { } } + dynamic "env" { + for_each = var.telegram_owner_id != "" ? [1] : [] + content { + name = "TELEGRAM_ALLOW_FROM" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.telegram_allow_from[0].secret_id + version = "latest" + } + } + } + } + env { name = "SLACK_APP_TOKEN" value_source { @@ -196,7 +219,8 @@ resource "google_cloud_run_v2_service" "openclaw" { depends_on = [ google_artifact_registry_repository_iam_member.ghcr_remote_reader, - null_resource.openclaw_json_r2, + google_secret_manager_secret_version.openclaw_json, + google_secret_manager_secret_version.telegram_allow_from, google_project_iam_member.secret_accessor, google_secret_manager_secret_version.r2_access_key_id, google_secret_manager_secret_version.r2_secret_access_key, diff --git a/terraform/gcp_cloudrun/r2.tf b/terraform/gcp_cloudrun/r2.tf index 8411210..2c9b059 100644 --- a/terraform/gcp_cloudrun/r2.tf +++ b/terraform/gcp_cloudrun/r2.tf @@ -1,29 +1,3 @@ # R2 bucket is managed in terraform/shared/ (independent state). -# Cloud Run only uploads openclaw.json to the existing bucket. - -resource "local_sensitive_file" "openclaw_json" { - content = local.openclaw_json_content - filename = "${path.module}/.terraform/tmp/openclaw.json" -} - -resource "null_resource" "openclaw_json_r2" { - triggers = { - content_hash = md5(local.openclaw_json_content) - } - - provisioner "local-exec" { - command = <<-EOT - aws s3 cp "${local_sensitive_file.openclaw_json.filename}" \ - "s3://${var.r2_bucket_name}/openclaw.json" \ - --endpoint-url "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com" \ - --content-type "application/json" - EOT - environment = { - AWS_ACCESS_KEY_ID = var.r2_access_key_id - AWS_SECRET_ACCESS_KEY = var.r2_secret_access_key - AWS_DEFAULT_REGION = "auto" - } - } - - depends_on = [local_sensitive_file.openclaw_json] -} +# openclaw.json is injected via Secret Manager, not stored in R2. +# R2 is used exclusively for persistent memory (workspace files, sessions). diff --git a/terraform/gcp_cloudrun/rclone-sync.sh b/terraform/gcp_cloudrun/rclone-sync.sh index f903ef5..bdca051 100644 --- a/terraform/gcp_cloudrun/rclone-sync.sh +++ b/terraform/gcp_cloudrun/rclone-sync.sh @@ -1,13 +1,21 @@ #!/bin/sh set -e -# openclaw.json is platform-specific (Cloud Run vs VM have different plugin config). -# Exclude it from all sync operations so platforms can share the same R2 bucket -# for soul/memory/sessions without overwriting each other's config. -RCLONE_EXCLUDE="--exclude openclaw.json --exclude openclaw.json.bak" +# Whitelist: only sync memory files shared across platforms. +# Config (openclaw.json, auth) is injected via Secret Manager, never stored in R2. +# Uses a filter file to avoid shell glob expansion of ** patterns. +FILTER_FILE=/tmp/rclone-filter.txt +cat > "$FILTER_FILE" << 'EOF' ++ workspace/MEMORY.md ++ workspace/SOUL.md ++ workspace/USER.md ++ workspace/AGENTS.md ++ agents/main/sessions/** +- * +EOF # ── Restore from R2 on startup ──────────────────────────────── -rclone sync r2:$R2_BUCKET/ /data/ --create-empty-src-dirs $RCLONE_EXCLUDE 2>/dev/null || true +rclone sync r2:$R2_BUCKET/ /data/ --create-empty-src-dirs --filter-from "$FILTER_FILE" 2>/dev/null || true # Fix permissions: rclone runs as root, openclaw runs as node (uid 1000). chmod -R a+rwX /data/ 2>/dev/null || true touch /tmp/rclone-ready @@ -19,7 +27,7 @@ echo "rclone: initial restore complete" # ── Final sync on shutdown (SIGTERM) ───────────────────────── cleanup() { echo "rclone: final sync on shutdown..." - rclone copy /data/ r2:$R2_BUCKET/ --create-empty-src-dirs $RCLONE_EXCLUDE 2>/dev/null || true + rclone copy /data/ r2:$R2_BUCKET/ --create-empty-src-dirs --filter-from "$FILTER_FILE" 2>/dev/null || true exit 0 } trap cleanup TERM INT @@ -27,5 +35,5 @@ trap cleanup TERM INT # ── Periodic sync every 60s ─────────────────────────────────── while true; do sleep 60 - rclone copy /data/ r2:$R2_BUCKET/ --create-empty-src-dirs $RCLONE_EXCLUDE 2>/dev/null || true + rclone copy /data/ r2:$R2_BUCKET/ --create-empty-src-dirs --filter-from "$FILTER_FILE" 2>/dev/null || true done diff --git a/terraform/gcp_cloudrun/secrets.tf b/terraform/gcp_cloudrun/secrets.tf index ecf28c9..c2c68cc 100644 --- a/terraform/gcp_cloudrun/secrets.tf +++ b/terraform/gcp_cloudrun/secrets.tf @@ -1,3 +1,15 @@ +resource "google_secret_manager_secret" "openclaw_json" { + secret_id = "${var.service_name}-openclaw-json" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "openclaw_json" { + secret = google_secret_manager_secret.openclaw_json.id + secret_data = local.openclaw_json_content +} + resource "google_secret_manager_secret" "openrouter_api_key" { secret_id = "${var.service_name}-openrouter-api-key" replication { @@ -72,6 +84,20 @@ resource "google_secret_manager_secret_version" "slack_bot_token" { secret_data = var.slack_bot_token } +resource "google_secret_manager_secret" "telegram_allow_from" { + count = var.telegram_owner_id != "" ? 1 : 0 + secret_id = "${var.service_name}-telegram-allow-from" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "telegram_allow_from" { + count = var.telegram_owner_id != "" ? 1 : 0 + secret = google_secret_manager_secret.telegram_allow_from[0].id + secret_data = jsonencode({ version = 1, allowFrom = [var.telegram_owner_id] }) +} + resource "google_secret_manager_secret" "r2_access_key_id" { secret_id = "${var.service_name}-r2-access-key-id" replication { diff --git a/terraform/gcp_vm/.envrc b/terraform/gcp_compute_engine/.envrc similarity index 100% rename from terraform/gcp_vm/.envrc rename to terraform/gcp_compute_engine/.envrc diff --git a/terraform/gcp_vm/approve_operator_approvals.py b/terraform/gcp_compute_engine/approve_operator_approvals.py similarity index 100% rename from terraform/gcp_vm/approve_operator_approvals.py rename to terraform/gcp_compute_engine/approve_operator_approvals.py diff --git a/terraform/gcp_vm/bootstrap.sh b/terraform/gcp_compute_engine/bootstrap.sh similarity index 94% rename from terraform/gcp_vm/bootstrap.sh rename to terraform/gcp_compute_engine/bootstrap.sh index 2fbff62..b474d64 100644 --- a/terraform/gcp_vm/bootstrap.sh +++ b/terraform/gcp_compute_engine/bootstrap.sh @@ -59,10 +59,17 @@ endpoint = https://${cloudflare_account_id}.r2.cloudflarestorage.com RCLONEEOF echo "Restoring OpenClaw state from R2 (${r2_bucket_name})..." -# Exclude openclaw.json: each platform maintains its own config so the same -# R2 bucket can be shared between Cloud Run and Compute Engine for failover. +# Filter file avoids shell glob expansion of ** patterns. +cat > /etc/rclone-openclaw-filter.txt << 'FILTEREOF' ++ workspace/MEMORY.md ++ workspace/SOUL.md ++ workspace/USER.md ++ workspace/AGENTS.md ++ agents/main/sessions/** +- * +FILTEREOF rclone sync r2:${r2_bucket_name}/ /root/.openclaw/ --create-empty-src-dirs \ - --exclude "openclaw.json" --exclude "openclaw.json.bak" 2>/dev/null || true + --filter-from /etc/rclone-openclaw-filter.txt 2>/dev/null || true chmod -R a+rX /root/.openclaw/ 2>/dev/null || true echo "R2 restore complete" %{ endif ~} @@ -162,7 +169,7 @@ After=network.target Type=simple Restart=always RestartSec=5 -ExecStart=/bin/sh -c 'while true; do rclone copy /root/.openclaw/ r2:${r2_bucket_name}/ --create-empty-src-dirs --exclude "openclaw.json" --exclude "openclaw.json.bak" 2>/dev/null; sleep 60; done' +ExecStart=/bin/sh -c 'while true; do rclone copy /root/.openclaw/ r2:${r2_bucket_name}/ --create-empty-src-dirs --filter-from /etc/rclone-openclaw-filter.txt 2>/dev/null; sleep 60; done' [Install] WantedBy=multi-user.target @@ -178,7 +185,7 @@ After=network.target [Service] Type=oneshot -ExecStart=/usr/bin/rclone copy /root/.openclaw/ r2:${r2_bucket_name}/ --create-empty-src-dirs --exclude "openclaw.json" --exclude "openclaw.json.bak" +ExecStart=/usr/bin/rclone copy /root/.openclaw/ r2:${r2_bucket_name}/ --create-empty-src-dirs --filter-from /etc/rclone-openclaw-filter.txt TimeoutStartSec=30 RemainAfterExit=yes diff --git a/terraform/gcp_vm/main.tf b/terraform/gcp_compute_engine/main.tf similarity index 100% rename from terraform/gcp_vm/main.tf rename to terraform/gcp_compute_engine/main.tf diff --git a/terraform/gcp_vm/outputs.tf b/terraform/gcp_compute_engine/outputs.tf similarity index 100% rename from terraform/gcp_vm/outputs.tf rename to terraform/gcp_compute_engine/outputs.tf diff --git a/terraform/gcp_vm/provider.tf b/terraform/gcp_compute_engine/provider.tf similarity index 100% rename from terraform/gcp_vm/provider.tf rename to terraform/gcp_compute_engine/provider.tf diff --git a/terraform/gcp_vm/terraform.tfvars.example b/terraform/gcp_compute_engine/terraform.tfvars.example similarity index 100% rename from terraform/gcp_vm/terraform.tfvars.example rename to terraform/gcp_compute_engine/terraform.tfvars.example diff --git a/terraform/gcp_vm/variables.tf b/terraform/gcp_compute_engine/variables.tf similarity index 100% rename from terraform/gcp_vm/variables.tf rename to terraform/gcp_compute_engine/variables.tf