Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions terraform/gcp_cloudrun/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 2 additions & 28 deletions terraform/gcp_cloudrun/r2.tf
Original file line number Diff line number Diff line change
@@ -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).
22 changes: 15 additions & 7 deletions terraform/gcp_cloudrun/rclone-sync.sh
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +8 to +18
# 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
Expand All @@ -19,13 +27,13 @@ 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

# ── 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
26 changes: 26 additions & 0 deletions terraform/gcp_cloudrun/secrets.tf
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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 ~}
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading