Terraform을 사용하여 Cloud Run 인스턴스에 OpenClaw를 배포하는 방법

1. 소개

개요

이 실습에서는 선언적 코드형 인프라(Terraform)를 사용하여 오픈소스 AI 에이전트 프레임워크인 OpenClaw의 완전한 영구 프로덕션 등급 인스턴스를 Cloud Run 인스턴스에 배포합니다. google 제공업체의 google_cloud_run_v2_instance 리소스를 사용하여 Cloud Run 인스턴스를 프로비저닝하고, Cloud Storage FUSE를 통해 Google Cloud Storage를 마운트하여 에이전트 작업공간 상태를 유지하고, Google Cloud Secret Manager를 사용하여 API 사용자 인증 정보를 안전하게 관리하고, 안전한 기기 페어링을 위해 Cloud Run 인스턴스 네이티브 SSH를 사용하여 OpenClaw의 내장 웹 UI를 통해 AI 에이전트와 상호작용합니다.

시작하기 전에 OpenClaw 문서를 살펴보고 OpenClaw의 아키텍처, 도구, 에이전트 워크플로를 숙지하세요.

실습할 내용

  • Terraform(main.tf)에서 API, 서비스 계정, IAM 바인딩, 보안 비밀, 스토리지 버킷, Cloud Run 인스턴스를 포함한 전체 인프라를 선언적으로 정의합니다.
  • google_cloud_run_v2_instance를 사용하여 OpenClaw 2.0 이상(ghcr.io/openclaw/openclaw:latest)을 배포합니다.
  • Secret Manager에서 보안 게이트웨이 사용자 인증 정보를 자동으로 생성하고 저장합니다.
  • Cloud Storage FUSE를 통해 Cloud Storage 버킷을 마운트하고 영구 상태, 작업공간, SQLite 지속성을 위해 /home/node/.openclaw를 동기화합니다.
  • gcloud beta run instances ssh을 사용하여 OpenClaw Control 웹 UI에 대한 브라우저 기기 페어링 요청을 인증하고 승인합니다.
  • (선택사항) 메시지 채널(Telegram 또는 WhatsApp)을 구성합니다.
  • (선택사항) Cloud Storage에 커스텀 기술을 추가하여 에이전트의 기능을 확장합니다.

학습할 내용

  • Terraform을 사용하여 Cloud Run 인스턴스를 선언하고 배포하는 방법
  • Terraform에서 Cloud Storage FUSE 볼륨 마운트를 구성하는 방법
  • Secret Manager 보안 비밀을 환경 변수로 Cloud Run 인스턴스에 안전하게 삽입하는 방법
  • Cloud Run 인스턴스 네이티브 SSH를 사용하여 OpenClaw 2.0 기기 인증을 관리하는 방법
  • Cloud Run 인스턴스에서 장기 실행되는 영구 에이전트 워크로드를 실행하는 방법

2. 설정 및 요구사항

GCP 프로젝트 설정

  1. Google Cloud 콘솔에 로그인합니다.
  2. Google Cloud 프로젝트를 만들거나 선택합니다.
  3. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

Cloud Shell 열기

Cloud 콘솔의 상단 툴바에서 Google Cloud Shell을 활성화합니다. Cloud Shell에는 Terraform과 Google Cloud SDK가 사전 설치되어 있습니다.

프로젝트 및 리전 설정

프로젝트 ID와 리전을 환경 변수로 설정하고 gcloud를 구성합니다.

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

gcloud config set project $PROJECT_ID
gcloud config get project

gcloud 베타 구성요소 설치

beta 구성요소가 설치되어 있는지 확인합니다(Cloud Run 인스턴스 SSH 및 로그 스트리밍에 필요).

gcloud components install beta --quiet
gcloud components update

Terraform 확인

Terraform이 설치되어 있고 버전 요구사항을 충족하는지 확인합니다.

terraform version

Terraform v1.5.0 이상이 표시되어야 합니다.

3. (선택사항) 메시지 통합 설정(Telegram 또는 WhatsApp)

OpenClaw를 Telegram 또는 WhatsApp에 연결할 수 있습니다. 아래에서 옵션 A 또는 옵션 B를 선택합니다.

옵션 A: 텔레그램 봇 설정

  1. Telegram을 열고 @BotFather를 검색합니다.
  2. /newbot 명령어를 전송하고 메시지에 따라 봇 이름과 사용자 이름을 지정합니다.
  3. 생성된 HTTP API 토큰(예: 123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ)을 복사합니다.
  4. Telegram에서 @userinfobot를 검색하고 /start를 전송한 후 숫자 사용자 ID(예: 8035936176)를 복사합니다.

이러한 값은 terraform.tfvars 파일에서 Terraform 입력 변수로 제공됩니다.

옵션 B: WhatsApp 설정

  1. 공백이나 기호 없이 국가 코드 포함 형식으로 개인 WhatsApp 전화번호를 가져옵니다(예: +15551234567).
  2. OpenClaw는 WhatsApp 채널 허용 목록 정책을 통해 연결됩니다.

4. Terraform에서 인프라 정의

Terraform에서 전체 인프라를 선언합니다. 여기에는 필요한 Google Cloud API 사용 설정, 전용 서비스 계정 및 IAM 역할 만들기, Secret Manager에서 보안 비밀 생성 및 저장, Cloud Storage 상태 버킷 프로비저닝, openclaw.json 구성 생성, Cloud Run 인스턴스 배포가 포함됩니다.

1. Terraform 디렉터리 만들기

Cloud Shell에서 Terraform 파일의 디렉터리를 만들고 해당 디렉터리로 이동합니다.

mkdir openclaw-terraform && cd openclaw-terraform

2. main.tf 만들기

main.tf 파일을 만듭니다. 이 구성은 Google 제공업체를 사용하여 모든 인프라 구성요소를 정의합니다.

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. variables.tf 만들기

variables.tf라는 파일을 만들어 입력 변수를 정의합니다.

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. outputs.tf 만들기

배포된 인스턴스 URL, 생성된 게이트웨이 비밀번호, 스토리지 버킷 이름을 노출하는 outputs.tf이라는 파일을 만듭니다.

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. terraform.tfvars 만들기

Gemini API 키를 가져오려면 다음 안내를 따르세요.

  1. Google AI Studio로 이동하여 Google 계정으로 로그인합니다.
  2. API 키 만들기를 클릭하고 Google Cloud 프로젝트를 선택합니다.
  3. 생성된 API 키를 복사합니다.

프로젝트 좌표를 사용하여 terraform.tfvars이라는 파일을 만듭니다.

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. Terraform을 사용하여 인프라 배포

1. 초기화 및 적용

Terraform 작업 디렉터리를 초기화하고 제공업체 플러그인을 다운로드합니다.

terraform init

인프라를 배포합니다.

terraform apply

메시지가 표시되면 계획된 리소스를 검토하고 yes를 입력하여 확인합니다.

2. 배포 출력 추출

Terraform 상태에서 인스턴스 URL과 생성된 게이트웨이 비밀번호를 내보냅니다.

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. 웹 UI를 통한 상호작용 및 Cloud Run SSH를 통한 인증

OpenClaw 2.0 이상에서는 암호화 기기 인증을 시행합니다. 새 브라우저에서 연결하면 게이트웨이가 페어링 요청을 생성하며, 이 요청은 Cloud Run 인스턴스 네이티브 SSH를 통해 직접 승인할 수 있습니다.

1. OpenClaw Control UI 열기

  1. 웹브라우저에서 $INSTANCE_URL을 엽니다.
  2. 인증을 요청하는 메시지가 표시되면 게이트웨이 비밀번호($GATEWAY_PASSWORD)를 입력합니다.
  3. 새 브라우저 세션이므로 OpenClaw Control UI에 요청 ID(예: b56143fc-1654-49da-a1d8-5d40479ddc66)가 포함된 대기 중인 기기 페어링 프롬프트가 표시됩니다. 이 요청 ID를 복사합니다.

2. Cloud Run 인스턴스에 SSH로 연결

Cloud Shell에서 실행 중인 인스턴스에 대한 보안 대화형 셸을 시작합니다.

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

3. 기기 페어링 승인

SSH 세션 내에서 브라우저 요청을 승인합니다.

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

4. 에이전트에 프롬프트 입력(실제 인스턴스)

승인되면 브라우저가 OpenClaw Control Dashboard에 즉시 연결됩니다.

  1. 채팅 UI에서 "Hello"과 같은 프롬프트를 전송하여 OpenClaw 에이전트를 테스트합니다.
  2. Cloud Run 인스턴스의 장기 수명 주기와 영구 스토리지를 테스트하려면 OpenClaw 에이전트에게 예약된 자율 작업을 수행하라는 프롬프트를 표시합니다. 예를 들어 GitHub 저장소에서 출시를 모니터링하고 요약을 작성하도록 할 수 있습니다.
    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. OpenClaw 상태, 작업공간 파일, SQLite 데이터베이스는 Cloud Storage FUSE를 통해 Cloud Storage에 직접 유지되므로 예약된 작업, 대화 기록, 생성된 요약 파일은 인스턴스 다시 시작 및 컨테이너 업그레이드 후에도 유지됩니다.

7. (선택사항) 메시지 채널 통합 확인

위의 선택적 설정 단계에서 Telegram 또는 WhatsApp을 구성한 경우 메시지 전송을 확인할 수 있습니다.

  1. Cloud Shell에서 인스턴스 로그를 확인합니다.
    gcloud beta run instances logs read openclaw-instance --region ${REGION} --limit 20
    
  2. Telegram(또는 WhatsApp)을 열고 메시지(예: /start 또는 Hello OpenClaw!)를 보냅니다.
  3. 봇은 openclaw.json의 허용 목록을 기준으로 사용자 ID를 인증하고 Gemini를 사용하여 응답합니다.

8. 인스턴스 로그 모니터링 및 검사

Cloud Run 인스턴스는 컨테이너에서 stdout 및 stderr을 캡처하고 Google Cloud Logging으로 직접 스트리밍합니다. Cloud Shell 또는 Cloud 콘솔을 사용하여 게이트웨이 트래픽을 모니터링하고 에이전트 도구 실행을 실시간으로 검사할 수 있습니다.

1. 최근 인스턴스 로그 읽기

Cloud Shell에서 인스턴스의 최근 로그 항목을 가져오려면 다음을 실행합니다.

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

2. 관찰할 주요 로그 서명

실행 중인 에이전트를 관찰할 때는 다음과 같은 주요 런타임 로그 이벤트를 확인하세요.

  • [gateway] ready: OpenClaw HTTP 및 WebSocket 게이트웨이가 초기화되었으며 포트 18789에서 수신 대기 중임을 확인합니다.
  • [gateway] agent model: ...: 활성 Gemini 모델(예: google/gemini-3.1-pro-preview 또는 google/gemini-3.5-flash)을 표시합니다.
  • [agents/tool-policy]: 정책에 따라 허용되거나 제한되는 도구를 표시합니다.
  • [agent/embedded]: 에이전트의 실행 및 도구 호출을 추적합니다.

3. Google Cloud 콘솔에서 로그 검사

Cloud 콘솔의 로그 탐색기를 사용하여 로그를 보고 필터링할 수도 있습니다.

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

9. (선택사항) 스킬로 에이전트 확장

OpenClaw는 에이전트에게 특정 워크플로, 특화된 CLI 도구, 도메인별 요청 사항을 가르치는 모듈식 기능 패키지인 스킬을 지원합니다.

스킬의 작동 방식

모든 기능은 SKILL.md 파일이 포함된 디렉터리입니다. 메타데이터(name 및 description)를 지정하는 YAML 프런트매터로 시작하고 그 뒤에 마크다운 요청 사항이 이어집니다.

---
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: 기술 식별자입니다(슬래시 명령어로 직접 호출할 수도 있음, 예: /summarize-logs).
  • description: 자연스러운 대화에서 이 스킬을 자동으로 호출할 시점을 OpenClaw 모델에 알려줍니다.

Cloud Storage 버킷에 커스텀 기술 추가

Cloud Storage 버킷이 /mnt/openclaw에 마운트되고 skills.load.extraDirs에 구성되어 있으므로 gs://${BUCKET_NAME}/skills/ 아래에 배치된 기술은 인스턴스 재시작 시 자동으로 로드되고 유지됩니다.

  1. 버킷 이름 검색:
    export BUCKET_NAME=$(terraform output -raw storage_bucket)
    
  2. 로컬 스킬 디렉터리 만들기:
    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. Cloud Storage에 기술 업로드:
    gcloud storage cp -r my-skill gs://${BUCKET_NAME}/skills/
    
  4. 채팅에서 기술 사용:
    • OpenClaw Control 웹 UI로 돌아갑니다.
    • 이제 /summarize-logs를 사용하여 에이전트에 프롬프트를 입력하거나 기술 설명과 일치하는 자연어 질문을 할 수 있습니다.

10. 삭제

이 Codelab에서 사용한 리소스의 비용이 Google Cloud 계정에 계속 청구되지 않도록 하려면 Terraform을 사용하여 프로비저닝된 모든 인프라를 한 번에 삭제하면 됩니다.

  1. openclaw-terraform 디렉터리에 있는지 확인합니다.
    cd openclaw-terraform
    
  2. 모든 리소스를 폐기합니다.
    terraform destroy
    
  3. 메시지가 표시되면 삭제할 리소스를 검토하고 yes를 입력하여 확인합니다. Terraform은 Cloud Run 인스턴스, Secret Manager 보안 비밀, IAM 정책 바인딩, 서비스 계정, Cloud Storage 버킷을 완전히 삭제합니다.

11. 결론

수고하셨습니다 Cloud Storage 상태 지속성, Secret Manager 사용자 인증 정보 보안, 안전한 기기 인증을 위한 Cloud Run 인스턴스 네이티브 SSH를 기반으로 선언적 Terraform을 사용하여 Cloud Run 인스턴스에 안전하고 완전히 영구적인 OpenClaw 인스턴스를 배포했습니다.