How to Deploy OpenClaw on Cloud Run Instances with Terraform

1. Introduction

Overview

In this lab, you will deploy a fully persistent, production-grade instance of OpenClaw—an open-source AI agent framework—to Cloud Run Instances using declarative Infrastructure as Code (Terraform). You will provision Cloud Run Instances using the google_cloud_run_v2_instance resource from the google provider, mount Google Cloud Storage via Cloud Storage FUSE to persist agent workspace state, manage API credentials securely using Google Cloud Secret Manager, and interact with your AI agent through OpenClaw's built-in Web UI using Cloud Run Instances native SSH for secure device pairing.

Before getting started, you can explore the OpenClaw Documentation to familiarize yourself with OpenClaw's architecture, tools, and agent workflows.

What you'll do

  • Define your entire infrastructure declaratively in Terraform (main.tf), including APIs, service accounts, IAM bindings, secrets, storage buckets, and Cloud Run Instances.
  • Deploy OpenClaw 2.0+ (ghcr.io/openclaw/openclaw:latest) using google_cloud_run_v2_instance.
  • Automatically generate and store secure gateway credentials in Secret Manager.
  • Mount a Cloud Storage bucket via Cloud Storage FUSE and synchronize /home/node/.openclaw for durable state, workspace, and SQLite persistence.
  • Use gcloud beta run instances ssh to authenticate and approve browser device pairing requests into the OpenClaw Control Web UI.
  • (Optional) Configure a messaging channel (Telegram or WhatsApp).
  • (Optional) Extend your agent's capabilities by adding custom Skills to Cloud Storage.

What you'll learn

  • How to declare and deploy Cloud Run Instances using Terraform.
  • How to configure Cloud Storage FUSE volume mounts in Terraform.
  • How to securely inject Secret Manager secrets as environment variables into Cloud Run Instances.
  • How to manage OpenClaw 2.0 device authentication using Cloud Run Instances native SSH.
  • How to run long-running, persistent agent workloads on Cloud Run Instances.

2. Setup and Requirements

GCP Project Setup

  1. Sign in to the Google Cloud Console.
  2. Create or select a Google Cloud Project.
  3. Ensure billing is enabled for your Google Cloud project.

Open Cloud Shell

Activate Google Cloud Shell from the top toolbar of the Cloud Console. Cloud Shell includes Terraform and the Google Cloud SDK pre-installed.

Set Project & Region

Set your project ID and region as environment variables and configure gcloud:

export PROJECT_ID=<YOUR_PROJECT_ID>
export REGION=<YOUR_REGION>

gcloud config set project $PROJECT_ID
gcloud config get project

Install gcloud beta Components

Ensure the beta component is installed (required for Cloud Run Instances SSH and log streaming):

gcloud components install beta --quiet
gcloud components update

Verify Terraform

Confirm that Terraform is installed and meets version requirements:

terraform version

You should see Terraform v1.5.0 or higher.

3. (Optional) Set Up Messaging Integrations (Telegram or WhatsApp)

You can connect OpenClaw to Telegram or WhatsApp. Choose Option A or Option B below.

Option A: Telegram Bot Setup

  1. Open Telegram and search for @BotFather.
  2. Send the /newbot command and follow the prompts to specify a bot name and username.
  3. Copy the generated HTTP API Token (e.g., 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ).
  4. Search for @userinfobot on Telegram, send /start, and copy your numeric User ID (e.g., 8035936176).

You will supply these values as Terraform input variables in your terraform.tfvars file.

Option B: WhatsApp Setup

  1. Obtain your personal WhatsApp phone number in international format without spaces or symbols (e.g., +15551234567).
  2. OpenClaw connects via the WhatsApp channel allowlist policy.

4. Define Infrastructure in Terraform

We will declare our entire infrastructure in Terraform. This includes enabling required Google Cloud APIs, creating the dedicated service account and IAM roles, generating and storing secrets in Secret Manager, provisioning the Cloud Storage state bucket, generating the openclaw.json configuration, and deploying the Cloud Run Instance.

1. Create a Terraform Directory

In Cloud Shell, create a directory for your Terraform files and navigate into it:

mkdir openclaw-terraform && cd openclaw-terraform

2. Create main.tf

Create a file named main.tf. This configuration uses the Google provider to define all infrastructure components:

terraform {
  required_version = ">= 1.5.0"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 8.8.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.5.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

# 1. Enable Required APIs
resource "google_project_service" "services" {
  for_each = toset([
    "run.googleapis.com",
    "secretmanager.googleapis.com",
    "storage.googleapis.com",
    "generativelanguage.googleapis.com",
    "compute.googleapis.com"
  ])
  project            = var.project_id
  service            = each.key
  disable_on_destroy = false
}

# 2. Dedicated Service Account
resource "google_service_account" "openclaw_sa" {
  account_id   = "openclaw-sa"
  display_name = "OpenClaw Service Account"
  depends_on   = [google_project_service.services]
}

# 3. Secret Manager: Gateway Password
resource "random_password" "gateway_password" {
  length  = 32
  special = false
}

resource "google_secret_manager_secret" "gateway_password" {
  secret_id = "openclaw-gateway-password"
  replication {
    auto {}
  }
  depends_on = [google_project_service.services]
}

resource "google_secret_manager_secret_version" "gateway_password" {
  secret      = google_secret_manager_secret.gateway_password.id
  secret_data = random_password.gateway_password.result
}

resource "google_secret_manager_secret_iam_member" "sa_gateway_password" {
  secret_id = google_secret_manager_secret.gateway_password.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.openclaw_sa.email}"
}

# 4. Secret Manager: Gemini API Key
resource "google_secret_manager_secret" "gemini_api_key" {
  secret_id = "gemini-api-key"
  replication {
    auto {}
  }
  depends_on = [google_project_service.services]
}

resource "google_secret_manager_secret_version" "gemini_api_key" {
  secret      = google_secret_manager_secret.gemini_api_key.id
  secret_data = var.gemini_api_key
}

resource "google_secret_manager_secret_iam_member" "sa_gemini_api_key" {
  secret_id = google_secret_manager_secret.gemini_api_key.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.openclaw_sa.email}"
}

# 5. Optional Secret Manager: Telegram Bot Token
resource "google_secret_manager_secret" "telegram_bot_token" {
  count     = var.telegram_bot_token != "" ? 1 : 0
  secret_id = "telegram-bot-token"
  replication {
    auto {}
  }
  depends_on = [google_project_service.services]
}

resource "google_secret_manager_secret_version" "telegram_bot_token" {
  count       = var.telegram_bot_token != "" ? 1 : 0
  secret      = google_secret_manager_secret.telegram_bot_token[0].id
  secret_data = var.telegram_bot_token
}

resource "google_secret_manager_secret_iam_member" "sa_telegram_bot_token" {
  count     = var.telegram_bot_token != "" ? 1 : 0
  secret_id = google_secret_manager_secret.telegram_bot_token[0].id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.openclaw_sa.email}"
}

# 6. Cloud Storage Bucket for State Persistence
resource "google_storage_bucket" "openclaw_state" {
  name                        = "openclaw-state-${var.project_id}"
  location                    = var.region
  uniform_bucket_level_access = true
  force_destroy               = true
  depends_on                  = [google_project_service.services]
}

resource "google_storage_bucket_iam_member" "sa_storage" {
  bucket = google_storage_bucket.openclaw_state.name
  role   = "roles/storage.objectUser"
  member = "serviceAccount:${google_service_account.openclaw_sa.email}"
}

# 7. Upload openclaw.json to Cloud Storage Root
resource "google_storage_bucket_object" "openclaw_config" {
  name   = "openclaw.json"
  bucket = google_storage_bucket.openclaw_state.name
  content = jsonencode({
    gateway = {
      mode           = "local"
      port           = 18789
      trustedProxies = ["127.0.0.1", "::1", "169.254.0.0/16", "10.0.0.0/8"]
      bind           = "lan"
      auth = {
        password = "$${OPENCLAW_GATEWAY_PASSWORD}"
      }
      controlUi = {
        allowedOrigins = ["*"]
        enabled        = true
      }
    }
    memory = {
      search = {
        provider = "gemini"
      }
    }
    agents = {
      defaults = {
        model = {
          primary = "google/gemini-3.1-pro-preview"
        }
        sandbox = {
          mode = "off"
        }
      }
    }
    skills = {
      load = {
        extraDirs = ["/mnt/openclaw/skills"]
      }
    }
    channels = {
      telegram = {
        enabled        = var.telegram_bot_token != ""
        defaultAccount = "default"
        accounts = {
          default = {
            enabled   = var.telegram_bot_token != ""
            dmPolicy  = "allowlist"
            allowFrom = var.telegram_user_id != "" ? [var.telegram_user_id] : []
          }
        }
      }
    }
    plugins = {
      entries = {
        google   = { enabled = true }
        telegram = { enabled = var.telegram_bot_token != "" }
      }
    }
  })
}

# 8. Cloud Run Instance
resource "google_cloud_run_v2_instance" "openclaw" {
  name     = "openclaw-instance"
  location = var.region

  ingress              = "INGRESS_TRAFFIC_ALL"
  invoker_iam_disabled = true
  restart_policy       = "ON_FAILURE"
  service_account      = google_service_account.openclaw_sa.email

  containers {
    image = "ghcr.io/openclaw/openclaw:latest"
    args = [
      "/bin/sh",
      "-c",
      "cp /mnt/openclaw/openclaw.json /home/node/.openclaw/ && exec node /app/openclaw.mjs gateway"
    ]

    ports {
      container_port = 18789
    }

    resources {
      limits = {
        cpu    = "4"
        memory = "4Gi"
      }
    }

    env {
      name = "GEMINI_API_KEY"
      value_source {
        secret_key_ref {
          secret  = google_secret_manager_secret.gemini_api_key.secret_id
          version = "latest"
        }
      }
    }

    env {
      name = "OPENCLAW_GATEWAY_PASSWORD"
      value_source {
        secret_key_ref {
          secret  = google_secret_manager_secret.gateway_password.secret_id
          version = "latest"
        }
      }
    }

    dynamic "env" {
      for_each = toset(var.telegram_bot_token != "" ? ["telegram"] : [])
      content {
        name = "TELEGRAM_BOT_TOKEN"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.telegram_bot_token[0].secret_id
            version = "latest"
          }
        }
      }
    }

    volume_mounts {
      name       = "openclaw-storage"
      mount_path = "/mnt/openclaw"
    }
  }

  volumes {
    name = "openclaw-storage"
    gcs {
      bucket        = google_storage_bucket.openclaw_state.name
      read_only     = false
      mount_options = ["uid=1000", "gid=1000", "file-mode=0700", "dir-mode=0700"]
    }
  }

  depends_on = [
    google_project_service.services,
    google_storage_bucket_object.openclaw_config,
    google_secret_manager_secret_version.gateway_password,
    google_secret_manager_secret_version.gemini_api_key,
    google_secret_manager_secret_iam_member.sa_gateway_password,
    google_secret_manager_secret_iam_member.sa_gemini_api_key,
    google_storage_bucket_iam_member.sa_storage
  ]
}

3. Create variables.tf

Create a file named variables.tf to define the input variables:

variable "project_id" {
  type        = string
  description = "Google Cloud Project ID"
}

variable "region" {
  type        = string
  default     = "us-west1"
  description = "Google Cloud Region for Cloud Run and Storage"
}

variable "gemini_api_key" {
  type        = string
  sensitive   = true
  description = "Gemini API Key from Google AI Studio"
}

variable "telegram_bot_token" {
  type        = string
  default     = ""
  sensitive   = true
  description = "(Optional) Telegram Bot HTTP API Token"
}

variable "telegram_user_id" {
  type        = string
  default     = ""
  description = "(Optional) Telegram User ID for allowlist policy"
}

4. Create outputs.tf

Create a file named outputs.tf to expose the deployed instance URL, generated gateway password, and storage bucket name:

output "instance_url" {
  description = "Public URL for OpenClaw Cloud Run Instance"
  value       = google_cloud_run_v2_instance.openclaw.urls[0]
}

output "gateway_password" {
  description = "Generated OpenClaw Gateway Password"
  value       = random_password.gateway_password.result
  sensitive   = true
}

output "storage_bucket" {
  description = "Cloud Storage bucket persisting OpenClaw state"
  value       = google_storage_bucket.openclaw_state.name
}

5. Create terraform.tfvars

To obtain a Gemini API key:

  1. Visit Google AI Studio and sign in with your Google account.
  2. Click Create API key and select your Google Cloud project.
  3. Copy the generated API key.

Create a file named terraform.tfvars with your project coordinates:

project_id     = "YOUR_PROJECT_ID"
region         = "YOUR_REGION"
gemini_api_key = "YOUR_GEMINI_API_KEY"

# Optional: Uncomment if configuring Telegram
# telegram_bot_token = "YOUR_TELEGRAM_BOT_TOKEN"
# telegram_user_id   = "YOUR_TELEGRAM_USER_ID"

5. Deploy Infrastructure with Terraform

1. Initialize and Apply

Initialize the Terraform working directory and download provider plugins:

terraform init

Deploy the infrastructure:

terraform apply

When prompted, review the planned resources and type yes to confirm.

2. Extract Deployment Outputs

Export the instance URL and generated gateway password from the Terraform state:

export INSTANCE_URL=$(terraform output -raw instance_url)
export GATEWAY_PASSWORD=$(terraform output -raw gateway_password)

echo "Instance URL: ${INSTANCE_URL}"
echo "Gateway Password: ${GATEWAY_PASSWORD}"

6. Interact via Web UI & Authenticate via Cloud Run SSH

OpenClaw 2.0+ enforces cryptographic device authentication. When connecting from a new browser, the gateway generates a pairing request that you approve directly through Cloud Run Instances native SSH.

1. Open the OpenClaw Control UI

  1. Open the $INSTANCE_URL in your web browser.
  2. When prompted for authentication, enter your gateway password ($GATEWAY_PASSWORD).
  3. Because this is a new browser session, the OpenClaw Control UI displays a pending device pairing prompt with a Request ID (for example: b56143fc-1654-49da-a1d8-5d40479ddc66). Copy this Request ID.

2. SSH into the Cloud Run Instance

In Cloud Shell, start a secure interactive shell into your running instance:

gcloud beta run instances ssh openclaw-instance --region=${REGION}

3. Approve Device Pairing

Inside the SSH session, approve your browser request:

openclaw devices approve <REQUEST_ID> --password ${GATEWAY_PASSWORD}

4. Prompt Your Agent (Instances in Action)

Once approved, your browser immediately connects to the OpenClaw Control Dashboard!

  1. Test your OpenClaw agent by sending a prompt in the chat UI, such as, "Hello".
  2. To test the Cloud Run instance's long-lived lifecycle and persistent storage, prompt your OpenClaw agent to perform a scheduled, autonomous task. For example, have it monitor a GitHub repository for releases and draft a digest:
    Check the GitHub repository openclaw/openclaw every hour for the next 24 hours. Track any new releases, merged pull requests, and breaking changes. Save a structured markdown digest titled `daily-release-digest.md` to your workspace, and notify me in this chat with a summary whenever a new release is detected.
    
  3. Because OpenClaw state, workspace files, and SQLite databases are persisted directly to Cloud Storage via Cloud Storage FUSE, the scheduled task, conversation history, and generated digest files survive instance restarts and container upgrades.

7. (Optional) Verify Messaging Channel Integrations

If you configured Telegram or WhatsApp in the optional setup steps above, you can verify messaging delivery:

  1. Check instance logs in Cloud Shell:
    gcloud beta run instances logs read openclaw-instance --region ${REGION} --limit 20
    
  2. Open Telegram (or WhatsApp) and send a message (e.g. /start or Hello OpenClaw!).
  3. The bot will authenticate your user ID against the allowlist in openclaw.json and respond using Gemini!

8. Monitor and Inspect Instance Logs

Cloud Run Instances captures stdout and stderr from your container and streams it directly to Google Cloud Logging. You can use Cloud Shell or the Cloud Console to monitor gateway traffic and inspect agent tool runs in real time.

1. Read Recent Instance Logs

To fetch recent log entries from the instance in Cloud Shell:

gcloud beta run instances logs read openclaw-instance \
  --region ${REGION} \
  --limit 50

2. Key Log Signatures to Observe

When observing your running agent, look for the following key runtime log events:

  • [gateway] ready: Confirms the OpenClaw HTTP and WebSocket gateway has initialized and is listening on port 18789.
  • [gateway] agent model: ...: Displays the active Gemini model (e.g. google/gemini-3.1-pro-preview or google/gemini-3.5-flash).
  • [agents/tool-policy]: Shows the tools permitted or restricted under your policy.
  • [agent/embedded]: Traces execution and tool invocation by the agent.

3. Inspect Logs in Google Cloud Console

You can also view and filter logs using the Logs Explorer in Cloud Console:

resource.type="cloud_run_instance"
resource.labels.instance_name="openclaw-instance"

9. (Optional) Extend Your Agent with Skills

OpenClaw supports Skills—modular capability packages that teach your agent specific workflows, specialized CLI tools, and domain-specific instructions.

How Skills Work

Every skill is a directory containing a SKILL.md file. It begins with YAML frontmatter specifying metadata (name and description), followed by Markdown instructions:

---
name: summarize-logs
description: Summarize Cloud Run error logs into actionable bullet points.
---

# Log Summarizer Skill

When asked to analyze or summarize logs:
1. Parse error stack traces and group similar errors by frequency.
2. Identify root causes such as memory limits, timeouts, or permission errors.
3. Propose concrete remediation steps.
  • name: The skill identifier (also callable directly as a slash command, for example /summarize-logs).
  • description: Tells OpenClaw's model when to automatically invoke this skill in natural conversation.

Add a Custom Skill to Your Cloud Storage Bucket

Because your Cloud Storage bucket is mounted at /mnt/openclaw and configured in skills.load.extraDirs, skills placed under gs://${BUCKET_NAME}/skills/ are automatically loaded and persisted across instance restarts:

  1. Retrieve your Bucket Name:
    export BUCKET_NAME=$(terraform output -raw storage_bucket)
    
  2. Create a Local Skill Directory:
    mkdir -p my-skill
    cat << 'EOF' > my-skill/SKILL.md
    ---
    name: summarize-logs
    description: Summarize Cloud Run error logs into actionable bullet points.
    ---
    
    # Log Summarizer Skill
    
    When asked to analyze or summarize logs, group errors by frequency and suggest actionable fixes.
    EOF
    
  3. Upload the Skill to Cloud Storage:
    gcloud storage cp -r my-skill gs://${BUCKET_NAME}/skills/
    
  4. Use the Skill in Chat:
    • Return to the OpenClaw Control Web UI.
    • You can now prompt your agent using /summarize-logs or ask natural-language questions matching the skill description!

10. Clean Up

To avoid incurring ongoing charges to your Google Cloud account for the resources used in this codelab, you can destroy all provisioned infrastructure in a single step using Terraform:

  1. Ensure you are in your openclaw-terraform directory:
    cd openclaw-terraform
    
  2. Destroy all resources:
    terraform destroy
    
  3. When prompted, review the resources to be removed and type yes to confirm. Terraform will cleanly delete the Cloud Run Instance, Secret Manager secrets, IAM policy bindings, service account, and the Cloud Storage bucket.

11. Conclusion

Congratulations! You have successfully deployed a secure, fully persistent instance of OpenClaw on Cloud Run Instances using declarative Terraform, backed by Cloud Storage state persistence, Secret Manager credential security, and Cloud Run Instances native SSH for secure device authentication!