diff --git a/README.md b/README.md index c3c40c2..1314eff 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OpenClaw on DigitalOcean + Azure +# OpenClaw on DigitalOcean + Azure + GCP [![Security Checks](https://github.com/PCBZ/OpenClaw_Docker/actions/workflows/security.yml/badge.svg)](https://github.com/PCBZ/OpenClaw_Docker/actions/workflows/security.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) @@ -6,12 +6,13 @@ [![Terraform](https://img.shields.io/badge/Terraform-%3E%3D1.5-844fba?logo=terraform&logoColor=white)](https://www.terraform.io) [![DigitalOcean](https://img.shields.io/badge/DigitalOcean-Droplet-0080ff?logo=digitalocean&logoColor=white)](https://www.digitalocean.com) [![Azure](https://img.shields.io/badge/Azure-VM-0078d4?logo=microsoft-azure&logoColor=white)](https://azure.microsoft.com) +[![GCP](https://img.shields.io/badge/GCP-ComputeEngine-4285f4?logo=googlecloud&logoColor=white)](https://cloud.google.com) [![OpenRouter](https://img.shields.io/badge/OpenRouter-Free%20Tier-ff6b35?logoColor=white)](https://openrouter.ai) [![OpenClaw](https://img.shields.io/badge/OpenClaw-2026-00e5cc?logoColor=white)](https://openclaw.bot) [![Telegram](https://img.shields.io/badge/Telegram-Bot-26a5e4?logo=telegram&logoColor=white)](https://telegram.org) [![Slack](https://img.shields.io/badge/Slack-Bot-4a154b?logo=slack&logoColor=white)](https://slack.com) -One-command deployment of an [OpenClaw](https://openclaw.bot) AI agent on DigitalOcean or Azure VM with Telegram and Slack support. After `terraform apply`, the bot is fully operational with no manual SSH steps required. +One-command deployment of an [OpenClaw](https://openclaw.bot) AI agent on DigitalOcean, Azure VM, or GCP Compute Engine with Telegram and Slack support. After `terraform apply`, the bot is fully operational with no manual SSH steps required. ## Features @@ -28,6 +29,7 @@ One-command deployment of an [OpenClaw](https://openclaw.bot) AI agent on Digita - SSH key pair - DigitalOcean account + API token (for DO path) - Azure subscription + service principal credentials (for Azure path) +- GCP project + Compute Engine API + credentials (for GCP path) - OpenRouter API key - Telegram bot token (from [@BotFather](https://t.me/BotFather)) - Slack App-Level token (starts with `xapp-`) @@ -96,6 +98,29 @@ swap_size = 2 openclaw_memory_limit_mb = 800 ``` +#### Option C: GCP VM + +```bash +cd terraform/gcp_vm +cp terraform.tfvars.example terraform.tfvars +``` + +Edit `terraform.tfvars` and set your GCP + VM values: + +```hcl +project_id = "your-gcp-project-id" +region = "us-west1" +zone = "us-west1-b" +machine_type = "e2-micro" +boot_disk_size_gb = 30 +ssh_public_key_path = "~/.ssh/id_rsa.pub" +swap_size = 3 +openclaw_memory_limit_mb = 800 + +# Optional: if not using ADC +# gcp_credentials_json = file("path/to/service-account.json") +``` + ### 3. Load secrets via direnv ```bash diff --git a/terraform/gcp_vm/.envrc b/terraform/gcp_vm/.envrc new file mode 100644 index 0000000..b0e4ce5 --- /dev/null +++ b/terraform/gcp_vm/.envrc @@ -0,0 +1,21 @@ +# Auto-load secrets from .env into Terraform variables +# Requires: brew install direnv && eval "$(direnv hook zsh)" >> ~/.zshrc + +dotenv ../../.env + +export TF_VAR_project_id="$GCP_PROJECT_ID" +export TF_VAR_gcp_credentials_json="${GCP_CREDENTIALS_JSON:-}" +if [ -n "${GCP_CREDENTIALS_FILE:-}" ]; then + credentials_file_expanded="${GCP_CREDENTIALS_FILE/#\~/$HOME}" + if [ -f "${credentials_file_expanded}" ]; then + export TF_VAR_gcp_credentials_json="$(cat "${credentials_file_expanded}")" + fi +fi + +export TF_VAR_openrouter_api_key="$OPENROUTER_API_KEY" +export TF_VAR_telegram_bot_token="$TELEGRAM_BOT_TOKEN" +export TF_VAR_openclaw_gateway_token="$OPENCLAW_GATEWAY_TOKEN" +export TF_VAR_brave_api_key="${BRAVE_API_KEY:-}" +export TF_VAR_telegram_owner_id="${TELEGRAM_OWNER_ID:-}" +export TF_VAR_slack_app_token="${SLACK_APP_TOKEN:-}" +export TF_VAR_slack_bot_token="${SLACK_BOT_TOKEN:-}" diff --git a/terraform/gcp_vm/approve_operator_approvals.py b/terraform/gcp_vm/approve_operator_approvals.py new file mode 100644 index 0000000..75a72fb --- /dev/null +++ b/terraform/gcp_vm/approve_operator_approvals.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Grant operator.approvals on paired devices; clear processed pending requests.""" +import json +import time + +PENDING_FILE = "/root/.openclaw/devices/pending.json" +PAIRED_FILE = "/root/.openclaw/devices/paired.json" + +pending = {} +for _ in range(12): + try: + with open(PENDING_FILE) as f: + pending = json.load(f) + if pending: + break + except Exception: + pass + time.sleep(5) + +# Even if no pending requests, approve operator.approvals for all paired devices. +try: + with open(PAIRED_FILE) as f: + paired = json.load(f) +except Exception: + paired = {} + +for request in pending.values(): + device_id = request.get("deviceId") + if device_id in paired: + for key in ("scopes", "approvedScopes"): + if "operator.approvals" not in paired[device_id].get(key, []): + paired[device_id].setdefault(key, []).append("operator.approvals") + print(f"Approved via pending: {device_id}") + +# Ensure all paired operator devices have operator.approvals regardless of pending state +for device_id, device in paired.items(): + if "operator" in device.get("roles", []): + for key in ("scopes", "approvedScopes"): + if "operator.approvals" not in device.get(key, []): + device.setdefault(key, []).append("operator.approvals") + print(f"Granted operator.approvals to {device_id[:16]}...") + +if paired: + with open(PAIRED_FILE, "w") as f: + json.dump(paired, f, indent=2) +if pending: + with open(PENDING_FILE, "w") as f: + json.dump({}, f) diff --git a/terraform/gcp_vm/bootstrap.sh b/terraform/gcp_vm/bootstrap.sh new file mode 100644 index 0000000..1cd300b --- /dev/null +++ b/terraform/gcp_vm/bootstrap.sh @@ -0,0 +1,182 @@ +#!/bin/bash +set -e + +export HOME=/root +export USER=root +export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +# ── 1. System setup ────────────────────────────────────────── +apt-get update -y + +# ── Create swap file ───────────────────────────────────────── +swap_size_gb=${swap_size} +swap_size_mb=$((swap_size_gb * 1024)) +swap_path="/swapfile" + +echo "Setting up $${swap_size_gb}GB swap at $${swap_path}..." +if dd if=/dev/zero of="$${swap_path}" bs=1M count="$${swap_size_mb}"; then + chmod 600 "$${swap_path}" + mkswap "$${swap_path}" + swapon "$${swap_path}" + if ! grep -q "$${swap_path} none swap" /etc/fstab; then + echo "$${swap_path} none swap sw 0 0" >> /etc/fstab + fi +else + echo "WARNING: Swap creation failed; continuing without swap." +fi + +# ── Kernel swappiness tuning ───────────────────────────────── +cat > /etc/sysctl.d/99-swappiness.conf << 'SYSCTLEOF' +vm.swappiness = 20 +vm.overcommit_memory = 1 +SYSCTLEOF +sysctl -p /etc/sysctl.d/99-swappiness.conf + +# ── Install Node.js 24.x ──────────────────────────────────── +curl -fsSL https://deb.nodesource.com/setup_24.x | bash - +apt-get install -y nodejs + +# ── 2. Install OpenClaw ────────────────────────────────────── +export OPENCLAW_ONBOARD_NON_INTERACTIVE=1 +export OPENCLAW_INSTALL_METHOD=npm +curl -fsSL https://openclaw.bot/install.sh | bash -s -- --install-method npm --no-onboard + +npm install -g grammy @grammyjs/runner @grammyjs/transformer-throttler \ + @slack/bolt @slack/socket-mode @slack/web-api + +# ── 3. Write config ────────────────────────────────────────── +mkdir -p /root/.openclaw + +# Write injected openclaw.json (provided by Terraform) +cat > /root/.openclaw/openclaw.json << 'JSONEOF' +${openclaw_json_content} +JSONEOF + +mkdir -p /root/.openclaw/workspace +if ! grep -q "ALWAYS_REPLY_IN_DM" /root/.openclaw/workspace/AGENTS.md 2>/dev/null; then +cat >> /root/.openclaw/workspace/AGENTS.md << 'AGENTSEOF' + +## Channel Output Rule (OpenClaw) + +- ALWAYS_REPLY_IN_DM: For any direct message on Telegram/Slack, always send at least one plain-text assistant message. +- Never end a DM turn with tool calls only, empty payload, or metadata-only output. +- If uncertain, send a brief fallback text: "I can help with that. Could you share a bit more detail?" +AGENTSEOF +fi + +# ── 4. Write .env and export secrets ───────────────────────── +cat > /root/.openclaw/.env << 'ENVEOF' +OPENROUTER_API_KEY=${openrouter_api_key} +TELEGRAM_BOT_TOKEN=${telegram_bot_token} +OPENCLAW_GATEWAY_TOKEN=${openclaw_gateway_token} +BRAVE_API_KEY=${brave_api_key} +OPENCLAW_ONBOARD_NON_INTERACTIVE=1 +ENVEOF + +export OPENROUTER_API_KEY=${openrouter_api_key} +export TELEGRAM_BOT_TOKEN=${telegram_bot_token} +export OPENCLAW_GATEWAY_TOKEN=${openclaw_gateway_token} +export BRAVE_API_KEY=${brave_api_key} + +# ── 5. Onboard ─────────────────────────────────────────────── +openclaw doctor --fix || true + +loginctl enable-linger root +export XDG_RUNTIME_DIR=/run/user/0 +mkdir -p "$XDG_RUNTIME_DIR" +systemctl start user@0.service || true + +openclaw onboard --non-interactive --accept-risk --install-daemon || true + +# Fix models.json baseUrl (onboard may have written wrong /v1 instead of /api/v1) +MODELS_JSON=/root/.openclaw/agents/main/agent/models.json +if [ -f "$MODELS_JSON" ]; then + sed -i 's|https://openrouter.ai/v1|https://openrouter.ai/api/v1|g' "$MODELS_JSON" + echo "Fixed models.json baseUrl: /v1 -> /api/v1" +fi + +# ── Write agent auth-profiles (OpenRouter key) ─────────────── +mkdir -p /root/.openclaw/agents/main/agent +cat > /root/.openclaw/agents/main/agent/auth-profiles.json << 'AUTHEOF' +{ + "openrouter": { + "apiKey": "${openrouter_api_key}" + } +} +AUTHEOF + +openclaw gateway install --force + +# ── 6. Create systemd service override with memory limits ──── +mkdir -p /root/.config/systemd/user/openclaw-gateway.service.d +cat > /root/.config/systemd/user/openclaw-gateway.service.d/override.conf << 'OVERRIDEEOF' +[Service] +TimeoutStartSec=180 +TimeoutStopSec=60 +RestartSec=5 +MemoryLimit=${openclaw_memory_limit_mb}M +OVERRIDEEOF + +systemctl --user daemon-reload +systemctl --user restart openclaw-gateway.service + +# ── 7. Auto-approve operator.approvals scope ───────────────── +echo "Waiting for approval requests..." +sleep 120 + +mkdir -p /root/.openclaw/bootstrap +cat > /root/.openclaw/bootstrap/approve_operator_approvals.py << 'APPROVEPYEOF' +${approve_operator_script} +APPROVEPYEOF +chmod 700 /root/.openclaw/bootstrap/approve_operator_approvals.py +python3 /root/.openclaw/bootstrap/approve_operator_approvals.py + +# ── 8. Setup unattended security updates ───────────────────── +apt-get install -y unattended-upgrades + +cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'UNATTENDEDEOF' +Unattended-Upgrade::Allowed-Origins { + "$${distro_id}:$${distro_codename}-security"; +}; +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; +Unattended-Upgrade::MinimalSteps "true"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +Unattended-Upgrade::Automatic-Reboot "true"; +Unattended-Upgrade::Automatic-Reboot-Time "03:00"; +UNATTENDEDEOF + +# ── 9. Setup cleanup cron jobs ─────────────────────────────── +cat > /etc/cron.d/openclaw-cleanup << 'CRONEOF' +# OpenClaw cleanup and maintenance tasks +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +CRON_TZ=America/Los_Angeles + +# Weekly npm cache cleanup (Sunday 2:00 Pacific) +0 2 * * 0 root npm cache clean --force >> /var/log/openclaw-cleanup.log 2>&1 + +# Daily disk space logging (2:00 Pacific) +0 2 * * * root df -h > /var/log/openclaw-diskspace.log 2>&1 +CRONEOF + +# ── 10. Setup logrotate for OpenClaw logs ──────────────────── +mkdir -p /var/log/openclaw + +cat > /etc/logrotate.d/openclaw << 'LOGROTATEEOF' +/var/log/openclaw-*.log { + daily + rotate 7 + compress + delaycompress + notifempty + create 0644 root root + missingok +} +LOGROTATEEOF + +echo "=== GCP Bootstrap Complete ===" +echo "Swap: $${swap_size_gb}GB at $${swap_path}" +echo "Memory Limit: ${openclaw_memory_limit_mb}MB (systemd cgroup)" +echo "OpenClaw Web: http://localhost:18789/health (test locally)" +echo "Check systemd: systemctl --user status openclaw-gateway" diff --git a/terraform/gcp_vm/main.tf b/terraform/gcp_vm/main.tf new file mode 100644 index 0000000..50c06da --- /dev/null +++ b/terraform/gcp_vm/main.tf @@ -0,0 +1,122 @@ +locals { + ssh_key_entry = "${var.admin_username}:${file(var.ssh_public_key_path)}" + ssh_ipv4_cidrs = [ + for cidr in var.ssh_allowed_cidrs : cidr + if length(regexall(":", cidr)) == 0 + ] + ssh_ipv6_cidrs = [ + for cidr in var.ssh_allowed_cidrs : cidr + if length(regexall(":", cidr)) > 0 + ] + gateway_ipv4_cidrs = [ + for cidr in var.gateway_allowed_cidrs : cidr + if length(regexall(":", cidr)) == 0 + ] + gateway_ipv6_cidrs = [ + for cidr in var.gateway_allowed_cidrs : cidr + if length(regexall(":", cidr)) > 0 + ] + + openclaw_json_content = templatefile("${path.module}/openclaw.json.tpl", { + openclaw_gateway_token = var.openclaw_gateway_token + openrouter_api_key = var.openrouter_api_key + brave_api_key = var.brave_api_key + telegram_bot_token = var.telegram_bot_token + slack_app_token = var.slack_app_token + slack_bot_token = var.slack_bot_token + slack_enabled = var.slack_app_token != "" && var.slack_bot_token != "" + }) + + bootstrap_vars = { + openrouter_api_key = var.openrouter_api_key + telegram_bot_token = var.telegram_bot_token + openclaw_gateway_token = var.openclaw_gateway_token + brave_api_key = var.brave_api_key + swap_size = var.swap_size + openclaw_memory_limit_mb = var.openclaw_memory_limit_mb + approve_operator_script = file("${path.module}/approve_operator_approvals.py") + openclaw_json_content = local.openclaw_json_content + } +} + +resource "google_compute_firewall" "allow_ssh_ipv4" { + count = length(local.ssh_ipv4_cidrs) > 0 ? 1 : 0 + name = "${var.vm_name}-allow-ssh-ipv4" + network = var.network_name + + allow { + protocol = "tcp" + ports = ["22"] + } + + source_ranges = local.ssh_ipv4_cidrs + target_tags = [var.vm_name] +} + +resource "google_compute_firewall" "allow_ssh_ipv6" { + count = length(local.ssh_ipv6_cidrs) > 0 ? 1 : 0 + name = "${var.vm_name}-allow-ssh-ipv6" + network = var.network_name + + allow { + protocol = "tcp" + ports = ["22"] + } + + source_ranges = local.ssh_ipv6_cidrs + target_tags = [var.vm_name] +} + +resource "google_compute_firewall" "allow_gateway_ipv4" { + count = length(local.gateway_ipv4_cidrs) > 0 ? 1 : 0 + name = "${var.vm_name}-allow-gateway-ipv4" + network = var.network_name + + allow { + protocol = "tcp" + ports = ["18789"] + } + + source_ranges = local.gateway_ipv4_cidrs + target_tags = [var.vm_name] +} + +resource "google_compute_firewall" "allow_gateway_ipv6" { + count = length(local.gateway_ipv6_cidrs) > 0 ? 1 : 0 + name = "${var.vm_name}-allow-gateway-ipv6" + network = var.network_name + + allow { + protocol = "tcp" + ports = ["18789"] + } + + source_ranges = local.gateway_ipv6_cidrs + target_tags = [var.vm_name] +} + +resource "google_compute_instance" "main" { + name = var.vm_name + machine_type = var.machine_type + zone = var.zone + tags = [var.vm_name] + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-2204-lts" + size = var.boot_disk_size_gb + type = "pd-balanced" + } + } + + network_interface { + network = var.network_name + access_config {} + } + + metadata = { + ssh-keys = local.ssh_key_entry + } + + metadata_startup_script = templatefile("${path.module}/bootstrap.sh", local.bootstrap_vars) +} diff --git a/terraform/gcp_vm/openclaw.json.tpl b/terraform/gcp_vm/openclaw.json.tpl new file mode 100644 index 0000000..114d76d --- /dev/null +++ b/terraform/gcp_vm/openclaw.json.tpl @@ -0,0 +1,90 @@ +{ + "gateway": { + "bind": "lan", + "auth": { "mode": "token", "token": "${openclaw_gateway_token}" }, + "mode": "local", + "remote": { "token": "${openclaw_gateway_token}" } + }, + "agents": { + "defaults": { + "model": { + "primary": "openrouter/openai/gpt-4o-mini", + "fallbacks": [ + "openrouter/anthropic/claude-haiku-4.5", + "openrouter/meta-llama/llama-3.3-70b-instruct:free", + "openrouter/auto" + ] + }, + "models": { + "openrouter/anthropic/claude-opus-4.6": {"alias": "opus"}, + "openrouter/anthropic/claude-sonnet-4.6": {"alias": "sonnet"}, + "openrouter/anthropic/claude-haiku-4.5": {"alias": "haiku"}, + "openrouter/openai/gpt-5.4": {"alias": "gpt5"}, + "openrouter/openai/gpt-4o": {"alias": "gpt4o"}, + "openrouter/openai/gpt-4o-mini": {"alias": "mini"}, + "openrouter/google/gemini-2.5-pro": {"alias": "gemini-pro"}, + "openrouter/google/gemini-2.5-flash": {"alias": "flash"}, + "openrouter/deepseek/deepseek-r1": {"alias": "r1"}, + "openrouter/mistralai/devstral-small": {"alias": "devstral"}, + "openrouter/meta-llama/llama-3.3-70b-instruct:free": {"alias": "llama"}, + "openrouter/nvidia/nemotron-3-super-120b-a12b:free": {"alias": "nemotron"}, + "openrouter/qwen/qwen3-coder:free": {"alias": "coder"}, + "openrouter/cognitivecomputations/dolphin-mistral-24b-venice-edition:free": {"alias": "uncensored"}, + "openrouter/auto": {"alias": "auto"} + }, + "compaction": { "mode": "safeguard", "reserveTokensFloor": 4000 } + } + }, + "tools": { + "web": { + "search": { "enabled": true, "provider": "brave" }, + "fetch": { + "enabled": true, + "strip_images": true, + "strip_videos": true, + "strip_css": true, + "strip_fonts": true + } + }, + "deny": ["browser"] + }, + "plugins": { + "load": { + "paths": [ + "/usr/lib/node_modules/openclaw/dist/extensions/telegram"%{ if slack_enabled }, + "/usr/lib/node_modules/openclaw/dist/extensions/slack"%{ endif } + ] + }, + "entries": { + "telegram": { "enabled": true }, +%{ if slack_enabled } + "slack": { "enabled": true }, +%{ endif } + "openrouter": { "enabled": true }, + "brave": { + "enabled": true, + "config": { "webSearch": { "apiKey": "${brave_api_key}" } } + } + } + }, + "channels": { + "telegram": { + "enabled": true, + "accounts": { + "default": { + "botToken": "${telegram_bot_token}", + "dmPolicy": "open", + "groupPolicy": "open" + } + } + }%{ if slack_enabled }, + "slack": { + "enabled": true, + "mode": "socket", + "appToken": "${slack_app_token}", + "botToken": "${slack_bot_token}", + "dmPolicy": "open", + "groupPolicy": "open" + }%{ endif } + } +} diff --git a/terraform/gcp_vm/outputs.tf b/terraform/gcp_vm/outputs.tf new file mode 100644 index 0000000..fe7f4c0 --- /dev/null +++ b/terraform/gcp_vm/outputs.tf @@ -0,0 +1,19 @@ +output "vm_public_ip" { + description = "Public IP address of the VM (use this for SSH)" + value = google_compute_instance.main.network_interface[0].access_config[0].nat_ip +} + +output "vm_name" { + description = "Name of the VM" + value = google_compute_instance.main.name +} + +output "ssh_command" { + description = "SSH command to connect to the VM" + value = "ssh ${var.admin_username}@${google_compute_instance.main.network_interface[0].access_config[0].nat_ip}" +} + +output "openclaw_gateway_url" { + description = "OpenClaw gateway health check URL (after bootstrap completes)" + value = "http://${google_compute_instance.main.network_interface[0].access_config[0].nat_ip}:18789/health" +} diff --git a/terraform/gcp_vm/provider.tf b/terraform/gcp_vm/provider.tf new file mode 100644 index 0000000..9af33b5 --- /dev/null +++ b/terraform/gcp_vm/provider.tf @@ -0,0 +1,16 @@ +terraform { + required_version = ">= 1.0" + required_providers { + google = { + source = "hashicorp/google" + version = "~> 5.0" + } + } +} + +provider "google" { + project = var.project_id + region = var.region + zone = var.zone + credentials = var.gcp_credentials_json != "" ? var.gcp_credentials_json : null +} diff --git a/terraform/gcp_vm/terraform.tfvars.example b/terraform/gcp_vm/terraform.tfvars.example new file mode 100644 index 0000000..1c5fd48 --- /dev/null +++ b/terraform/gcp_vm/terraform.tfvars.example @@ -0,0 +1,36 @@ +# Copy to terraform.tfvars and fill in your values +# cp terraform.tfvars.example terraform.tfvars +# +# Secrets: use repo root .env + direnv (terraform/gcp_vm/.envrc -> TF_VAR_*). +# Set GCP_PROJECT_ID and GCP_CREDENTIALS_FILE in .env (see README). + +# ── GCP Core ──────────────────────────────────────────────── +project_id = "your-gcp-project-id" +region = "us-west1" # Oregon +zone = "us-west1-b" + +# Optional: only if not using ADC or GCP_CREDENTIALS_FILE via .env +# gcp_credentials_json = file("path/to/service-account.json") + +# ── VM Configuration ──────────────────────────────────────── +vm_name = "openclaw-e2-micro" +machine_type = "e2-micro" # Always Free tier friendly; use e2-small if OOM +boot_disk_size_gb = 30 + +# ── SSH / Network Access ──────────────────────────────────── +admin_username = "openclaw" +ssh_public_key_path = "~/.ssh/id_rsa.pub" +network_name = "default" + +# Firewall: IPv4 and IPv6 CIDRs are split into separate GCP rules automatically. +# Replace with your public IP/CIDR. For dev-only wide open (not recommended): +# ssh_allowed_cidrs = ["0.0.0.0/0"] +# gateway_allowed_cidrs = ["0.0.0.0/0"] +# Optional IPv6, e.g. your ISP prefix or ::/0 for testing: +# ssh_allowed_cidrs = ["203.0.113.10/32", "2001:db8::/32"] +ssh_allowed_cidrs = ["203.0.113.10/32"] +gateway_allowed_cidrs = ["203.0.113.10/32"] + +# ── Runtime Limits ────────────────────────────────────────── +swap_size = 3 +openclaw_memory_limit_mb = 800 diff --git a/terraform/gcp_vm/variables.tf b/terraform/gcp_vm/variables.tf new file mode 100644 index 0000000..dcb90e7 --- /dev/null +++ b/terraform/gcp_vm/variables.tf @@ -0,0 +1,118 @@ +variable "project_id" { + description = "GCP project ID" + type = string +} + +variable "region" { + description = "GCP region (e.g. us-west1)" + type = string + default = "us-west1" +} + +variable "zone" { + description = "GCP zone (e.g. us-west1-b)" + type = string + default = "us-west1-b" +} + +variable "gcp_credentials_json" { + description = "Optional service account JSON content. Leave empty to use ADC." + type = string + sensitive = true + default = "" +} + +variable "vm_name" { + description = "VM instance name" + type = string + default = "openclaw-e2-micro" +} + +variable "machine_type" { + description = "GCE machine type" + type = string + default = "e2-micro" +} + +variable "boot_disk_size_gb" { + description = "Boot disk size in GB" + type = number + default = 30 +} + +variable "admin_username" { + description = "Linux user for SSH" + type = string + default = "openclaw" +} + +variable "ssh_public_key_path" { + description = "Path to SSH public key (~/.ssh/id_rsa.pub)" + type = string + default = "~/.ssh/id_rsa.pub" +} + +variable "network_name" { + description = "VPC network name (default uses pre-created network)" + type = string + default = "default" +} + +variable "ssh_allowed_cidrs" { + description = "CIDR blocks allowed SSH access (port 22)" + type = list(string) + default = ["0.0.0.0/0", "::/0"] +} + +variable "gateway_allowed_cidrs" { + description = "CIDR blocks allowed OpenClaw gateway access (port 18789)" + type = list(string) + default = ["0.0.0.0/0", "::/0"] +} + +variable "swap_size" { + description = "Swap file size in GB" + type = number + default = 3 +} + +variable "openclaw_memory_limit_mb" { + description = "Hard memory limit for OpenClaw systemd service (MB)" + type = number + default = 800 +} + +variable "openrouter_api_key" { + sensitive = true +} + +variable "telegram_bot_token" { + sensitive = true +} + +variable "openclaw_gateway_token" { + sensitive = true +} + +variable "brave_api_key" { + description = "Brave Search API key (optional)" + sensitive = true + default = "" +} + +variable "telegram_owner_id" { + description = "Telegram numeric user ID for privileged commands" + default = "" +} + +variable "slack_app_token" { + description = "Slack App-Level Token (xapp-...)" + sensitive = true + default = "" +} + +variable "slack_bot_token" { + description = "Slack Bot OAuth Token (xoxb-...)" + sensitive = true + default = "" +}