mirror of https://github.com/openclaw/openclaw.git
docs: add Kubernetes install guide, setup script, and manifests (#34492)
* add docs and manifests for k8s install Signed-off-by: sallyom <somalley@redhat.com> * changelog Signed-off-by: sallyom <somalley@redhat.com> --------- Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
parent
4f620bebe5
commit
8e0e4f736a
|
|
@ -18,6 +18,8 @@ Docs: https://docs.openclaw.ai
|
|||
|
||||
### Changes
|
||||
|
||||
- Docs/Kubernetes: Add a starter K8s install path with raw manifests, Kind setup, and deployment docs. Thanks @sallyom @dzianisv @egkristi
|
||||
|
||||
### Fixes
|
||||
|
||||
- Cron/proactive delivery: keep isolated direct cron sends out of the write-ahead resend queue so transient-send retries do not replay duplicate proactive messages after restart. (#40646) Thanks @openperf and @vincentkoc.
|
||||
|
|
|
|||
|
|
@ -876,6 +876,7 @@
|
|||
"group": "Hosting and deployment",
|
||||
"pages": [
|
||||
"vps",
|
||||
"install/kubernetes",
|
||||
"install/fly",
|
||||
"install/hetzner",
|
||||
"install/gcp",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
summary: "Deploy OpenClaw Gateway to a Kubernetes cluster with Kustomize"
|
||||
read_when:
|
||||
- You want to run OpenClaw on a Kubernetes cluster
|
||||
- You want to test OpenClaw in a Kubernetes environment
|
||||
title: "Kubernetes"
|
||||
---
|
||||
|
||||
# OpenClaw on Kubernetes
|
||||
|
||||
A minimal starting point for running OpenClaw on Kubernetes — not a production-ready deployment. It covers the core resources and is meant to be adapted to your environment.
|
||||
|
||||
## Why not Helm?
|
||||
|
||||
OpenClaw is a single container with some config files. The interesting customization is in agent content (markdown files, skills, config overrides), not infrastructure templating. Kustomize handles overlays without the overhead of a Helm chart. If your deployment grows more complex, a Helm chart can be layered on top of these manifests.
|
||||
|
||||
## What you need
|
||||
|
||||
- A running Kubernetes cluster (AKS, EKS, GKE, k3s, kind, OpenShift, etc.)
|
||||
- `kubectl` connected to your cluster
|
||||
- An API key for at least one model provider
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Replace with your provider: ANTHROPIC, GEMINI, OPENAI, or OPENROUTER
|
||||
export <PROVIDER>_API_KEY="..."
|
||||
./scripts/k8s/deploy.sh
|
||||
|
||||
kubectl port-forward svc/openclaw 18789:18789 -n openclaw
|
||||
open http://localhost:18789
|
||||
```
|
||||
|
||||
Retrieve the gateway token and paste it into the Control UI:
|
||||
|
||||
```bash
|
||||
kubectl get secret openclaw-secrets -n openclaw -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d
|
||||
```
|
||||
|
||||
For local debugging, `./scripts/k8s/deploy.sh --show-token` prints the token after deploy.
|
||||
|
||||
## Local testing with Kind
|
||||
|
||||
If you don't have a cluster, create one locally with [Kind](https://kind.sigs.k8s.io/):
|
||||
|
||||
```bash
|
||||
./scripts/k8s/create-kind.sh # auto-detects docker or podman
|
||||
./scripts/k8s/create-kind.sh --delete # tear down
|
||||
```
|
||||
|
||||
Then deploy as usual with `./scripts/k8s/deploy.sh`.
|
||||
|
||||
## Step by step
|
||||
|
||||
### 1) Deploy
|
||||
|
||||
**Option A** — API key in environment (one step):
|
||||
|
||||
```bash
|
||||
# Replace with your provider: ANTHROPIC, GEMINI, OPENAI, or OPENROUTER
|
||||
export <PROVIDER>_API_KEY="..."
|
||||
./scripts/k8s/deploy.sh
|
||||
```
|
||||
|
||||
The script creates a Kubernetes Secret with the API key and an auto-generated gateway token, then deploys. If the Secret already exists, it preserves the current gateway token and any provider keys not being changed.
|
||||
|
||||
**Option B** — create the secret separately:
|
||||
|
||||
```bash
|
||||
export <PROVIDER>_API_KEY="..."
|
||||
./scripts/k8s/deploy.sh --create-secret
|
||||
./scripts/k8s/deploy.sh
|
||||
```
|
||||
|
||||
Use `--show-token` with either command if you want the token printed to stdout for local testing.
|
||||
|
||||
### 2) Access the gateway
|
||||
|
||||
```bash
|
||||
kubectl port-forward svc/openclaw 18789:18789 -n openclaw
|
||||
open http://localhost:18789
|
||||
```
|
||||
|
||||
## What gets deployed
|
||||
|
||||
```
|
||||
Namespace: openclaw (configurable via OPENCLAW_NAMESPACE)
|
||||
├── Deployment/openclaw # Single pod, init container + gateway
|
||||
├── Service/openclaw # ClusterIP on port 18789
|
||||
├── PersistentVolumeClaim # 10Gi for agent state and config
|
||||
├── ConfigMap/openclaw-config # openclaw.json + AGENTS.md
|
||||
└── Secret/openclaw-secrets # Gateway token + API keys
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Agent instructions
|
||||
|
||||
Edit the `AGENTS.md` in `scripts/k8s/manifests/configmap.yaml` and redeploy:
|
||||
|
||||
```bash
|
||||
./scripts/k8s/deploy.sh
|
||||
```
|
||||
|
||||
### Gateway config
|
||||
|
||||
Edit `openclaw.json` in `scripts/k8s/manifests/configmap.yaml`. See [Gateway configuration](/gateway/configuration) for the full reference.
|
||||
|
||||
### Add providers
|
||||
|
||||
Re-run with additional keys exported:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="..."
|
||||
export OPENAI_API_KEY="..."
|
||||
./scripts/k8s/deploy.sh --create-secret
|
||||
./scripts/k8s/deploy.sh
|
||||
```
|
||||
|
||||
Existing provider keys stay in the Secret unless you overwrite them.
|
||||
|
||||
Or patch the Secret directly:
|
||||
|
||||
```bash
|
||||
kubectl patch secret openclaw-secrets -n openclaw \
|
||||
-p '{"stringData":{"<PROVIDER>_API_KEY":"..."}}'
|
||||
kubectl rollout restart deployment/openclaw -n openclaw
|
||||
```
|
||||
|
||||
### Custom namespace
|
||||
|
||||
```bash
|
||||
OPENCLAW_NAMESPACE=my-namespace ./scripts/k8s/deploy.sh
|
||||
```
|
||||
|
||||
### Custom image
|
||||
|
||||
Edit the `image` field in `scripts/k8s/manifests/deployment.yaml`:
|
||||
|
||||
```yaml
|
||||
image: ghcr.io/openclaw/openclaw:2026.3.1
|
||||
```
|
||||
|
||||
### Expose beyond port-forward
|
||||
|
||||
The default manifests bind the gateway to loopback inside the pod. That works with `kubectl port-forward`, but it does not work with a Kubernetes `Service` or Ingress path that needs to reach the pod IP.
|
||||
|
||||
If you want to expose the gateway through an Ingress or load balancer:
|
||||
|
||||
- Change the gateway bind in `scripts/k8s/manifests/configmap.yaml` from `loopback` to a non-loopback bind that matches your deployment model
|
||||
- Keep gateway auth enabled and use a proper TLS-terminated entrypoint
|
||||
- Configure the Control UI for remote access using the supported web security model (for example HTTPS/Tailscale Serve and explicit allowed origins when needed)
|
||||
|
||||
## Re-deploy
|
||||
|
||||
```bash
|
||||
./scripts/k8s/deploy.sh
|
||||
```
|
||||
|
||||
This applies all manifests and restarts the pod to pick up any config or secret changes.
|
||||
|
||||
## Teardown
|
||||
|
||||
```bash
|
||||
./scripts/k8s/deploy.sh --delete
|
||||
```
|
||||
|
||||
This deletes the namespace and all resources in it, including the PVC.
|
||||
|
||||
## Architecture notes
|
||||
|
||||
- The gateway binds to loopback inside the pod by default, so the included setup is for `kubectl port-forward`
|
||||
- No cluster-scoped resources — everything lives in a single namespace
|
||||
- Security: `readOnlyRootFilesystem`, `drop: ALL` capabilities, non-root user (UID 1000)
|
||||
- The default config keeps the Control UI on the safer local-access path: loopback bind plus `kubectl port-forward` to `http://127.0.0.1:18789`
|
||||
- If you move beyond localhost access, use the supported remote model: HTTPS/Tailscale plus the appropriate gateway bind and Control UI origin settings
|
||||
- Secrets are generated in a temp directory and applied directly to the cluster — no secret material is written to the repo checkout
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
scripts/k8s/
|
||||
├── deploy.sh # Creates namespace + secret, deploys via kustomize
|
||||
├── create-kind.sh # Local Kind cluster (auto-detects docker/podman)
|
||||
└── manifests/
|
||||
├── kustomization.yaml # Kustomize base
|
||||
├── configmap.yaml # openclaw.json + AGENTS.md
|
||||
├── deployment.yaml # Pod spec with security hardening
|
||||
├── pvc.yaml # 10Gi persistent storage
|
||||
└── service.yaml # ClusterIP on 18789
|
||||
```
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env bash
|
||||
# ============================================================================
|
||||
# KIND CLUSTER BOOTSTRAP SCRIPT
|
||||
# ============================================================================
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/k8s/create-kind.sh # Create with auto-detected engine
|
||||
# ./scripts/k8s/create-kind.sh --name mycluster
|
||||
# ./scripts/k8s/create-kind.sh --delete
|
||||
#
|
||||
# After creation, deploy with:
|
||||
# export <AI_PROVIDER>_API_KEY="..." && ./scripts/k8s/deploy.sh
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Defaults
|
||||
CLUSTER_NAME="openclaw"
|
||||
CONTAINER_CMD=""
|
||||
DELETE=false
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[0;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
fail() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
|
||||
Options:
|
||||
--name NAME Cluster name (default: openclaw)
|
||||
--delete Delete the cluster instead of creating it
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # Create cluster (auto-detect engine)
|
||||
$(basename "$0") --delete # Delete the cluster
|
||||
$(basename "$0") --name dev --delete # Delete a cluster named "dev"
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name)
|
||||
[[ -z "${2:-}" ]] && fail "--name requires a value"
|
||||
CLUSTER_NAME="$2"; shift 2 ;;
|
||||
--delete)
|
||||
DELETE=true; shift ;;
|
||||
-h|--help)
|
||||
usage ;;
|
||||
*)
|
||||
fail "Unknown option: $1 (see --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Container engine detection
|
||||
# ---------------------------------------------------------------------------
|
||||
provider_installed() {
|
||||
command -v "$1" &>/dev/null
|
||||
}
|
||||
|
||||
provider_responsive() {
|
||||
case "$1" in
|
||||
docker)
|
||||
docker info &>/dev/null
|
||||
;;
|
||||
podman)
|
||||
podman info &>/dev/null
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_provider() {
|
||||
local candidate
|
||||
|
||||
for candidate in podman docker; do
|
||||
if provider_installed "$candidate" && provider_responsive "$candidate"; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
for candidate in podman docker; do
|
||||
if provider_installed "$candidate"; then
|
||||
case "$candidate" in
|
||||
podman)
|
||||
fail "Podman is installed but not responding, and no responsive Docker daemon was found. Ensure the podman machine is running (podman machine start) or start Docker."
|
||||
;;
|
||||
docker)
|
||||
fail "Docker is installed but not running, and no responsive Podman machine was found. Start Docker or start Podman."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
fail "Neither podman nor docker found. Install one to use Kind."
|
||||
}
|
||||
|
||||
CONTAINER_CMD=$(detect_provider)
|
||||
info "Auto-detected container engine: $CONTAINER_CMD"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prerequisites
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! command -v kind &>/dev/null; then
|
||||
fail "kind is not installed. Install it from https://kind.sigs.k8s.io/"
|
||||
fi
|
||||
|
||||
if ! command -v kubectl &>/dev/null; then
|
||||
fail "kubectl is not installed. Install it before creating or managing a Kind cluster."
|
||||
fi
|
||||
|
||||
# Verify the container engine is responsive
|
||||
if ! provider_responsive "$CONTAINER_CMD"; then
|
||||
if [[ "$CONTAINER_CMD" == "docker" ]]; then
|
||||
fail "Docker daemon is not running. Start it and try again."
|
||||
elif [[ "$CONTAINER_CMD" == "podman" ]]; then
|
||||
fail "Podman is not responding. Ensure the podman machine is running (podman machine start)."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Delete mode
|
||||
# ---------------------------------------------------------------------------
|
||||
if $DELETE; then
|
||||
info "Deleting Kind cluster '$CLUSTER_NAME'..."
|
||||
if KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind get clusters 2>/dev/null | grep -qx "$CLUSTER_NAME"; then
|
||||
KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind delete cluster --name "$CLUSTER_NAME"
|
||||
success "Cluster '$CLUSTER_NAME' deleted."
|
||||
else
|
||||
warn "Cluster '$CLUSTER_NAME' does not exist."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check if cluster already exists
|
||||
# ---------------------------------------------------------------------------
|
||||
if KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind get clusters 2>/dev/null | grep -qx "$CLUSTER_NAME"; then
|
||||
warn "Cluster '$CLUSTER_NAME' already exists."
|
||||
info "To recreate it, run: $0 --name \"$CLUSTER_NAME\" --delete && $0 --name \"$CLUSTER_NAME\""
|
||||
info "Switching kubectl context to kind-$CLUSTER_NAME..."
|
||||
kubectl config use-context "kind-$CLUSTER_NAME" &>/dev/null && success "Context set." || warn "Could not switch context."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Create cluster
|
||||
# ---------------------------------------------------------------------------
|
||||
info "Creating Kind cluster '$CLUSTER_NAME' (provider: $CONTAINER_CMD)..."
|
||||
|
||||
KIND_EXPERIMENTAL_PROVIDER="$CONTAINER_CMD" kind create cluster \
|
||||
--name "$CLUSTER_NAME" \
|
||||
--config - <<'KINDCFG'
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
nodes:
|
||||
- role: control-plane
|
||||
labels:
|
||||
openclaw.dev/role: control-plane
|
||||
# Uncomment to expose services on host ports:
|
||||
# extraPortMappings:
|
||||
# - containerPort: 30080
|
||||
# hostPort: 8080
|
||||
# protocol: TCP
|
||||
# - containerPort: 30443
|
||||
# hostPort: 8443
|
||||
# protocol: TCP
|
||||
KINDCFG
|
||||
|
||||
success "Kind cluster '$CLUSTER_NAME' created."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wait for readiness
|
||||
# ---------------------------------------------------------------------------
|
||||
info "Waiting for cluster to be ready..."
|
||||
kubectl --context "kind-$CLUSTER_NAME" wait --for=condition=Ready nodes --all --timeout=120s >/dev/null
|
||||
success "All nodes are Ready."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "---------------------------------------------------------------"
|
||||
echo " Kind cluster '$CLUSTER_NAME' is ready"
|
||||
echo "---------------------------------------------------------------"
|
||||
echo ""
|
||||
echo " kubectl cluster-info --context kind-$CLUSTER_NAME"
|
||||
echo ""
|
||||
echo ""
|
||||
echo " export <AI_PROVIDER>_API_KEY=\"...\" && ./scripts/k8s/deploy.sh"
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
#!/usr/bin/env bash
|
||||
# Deploy OpenClaw to Kubernetes.
|
||||
#
|
||||
# Secrets are generated in a temp directory and applied server-side.
|
||||
# No secret material is ever written to the repo checkout.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/k8s/deploy.sh # Deploy (requires API key in env or secret already in cluster)
|
||||
# ./scripts/k8s/deploy.sh --create-secret # Create or update the K8s Secret from env vars
|
||||
# ./scripts/k8s/deploy.sh --show-token # Print the gateway token after deploy
|
||||
# ./scripts/k8s/deploy.sh --delete # Tear down
|
||||
#
|
||||
# Environment:
|
||||
# OPENCLAW_NAMESPACE Kubernetes namespace (default: openclaw)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
MANIFESTS="$SCRIPT_DIR/manifests"
|
||||
NS="${OPENCLAW_NAMESPACE:-openclaw}"
|
||||
|
||||
# Check prerequisites
|
||||
for cmd in kubectl openssl; do
|
||||
command -v "$cmd" &>/dev/null || { echo "Missing: $cmd" >&2; exit 1; }
|
||||
done
|
||||
kubectl cluster-info &>/dev/null || { echo "Cannot connect to cluster. Check kubeconfig." >&2; exit 1; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# -h / --help
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
cat <<'HELP'
|
||||
Usage: ./scripts/k8s/deploy.sh [OPTION]
|
||||
|
||||
(no args) Deploy OpenClaw (creates secret from env if needed)
|
||||
--create-secret Create or update the K8s Secret from env vars without deploying
|
||||
--show-token Print the gateway token after deploy or secret creation
|
||||
--delete Delete the namespace and all resources
|
||||
-h, --help Show this help
|
||||
|
||||
Environment:
|
||||
Export at least one provider API key:
|
||||
ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY
|
||||
|
||||
OPENCLAW_NAMESPACE Kubernetes namespace (default: openclaw)
|
||||
HELP
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SHOW_TOKEN=false
|
||||
MODE="deploy"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--create-secret)
|
||||
MODE="create-secret"
|
||||
;;
|
||||
--delete)
|
||||
MODE="delete"
|
||||
;;
|
||||
--show-token)
|
||||
SHOW_TOKEN=true
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
echo "Run ./scripts/k8s/deploy.sh --help for usage." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --delete
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "$MODE" == "delete" ]]; then
|
||||
echo "Deleting namespace '$NS' and all resources..."
|
||||
kubectl delete namespace "$NS" --ignore-not-found
|
||||
echo "Done."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Create and apply Secret to the cluster
|
||||
# ---------------------------------------------------------------------------
|
||||
_apply_secret() {
|
||||
local TMP_DIR
|
||||
local EXISTING_SECRET=false
|
||||
local EXISTING_TOKEN=""
|
||||
local ANTHROPIC_VALUE=""
|
||||
local OPENAI_VALUE=""
|
||||
local GEMINI_VALUE=""
|
||||
local OPENROUTER_VALUE=""
|
||||
local TOKEN
|
||||
local SECRET_MANIFEST
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
chmod 700 "$TMP_DIR"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
if kubectl get secret openclaw-secrets -n "$NS" &>/dev/null; then
|
||||
EXISTING_SECRET=true
|
||||
EXISTING_TOKEN="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d)"
|
||||
ANTHROPIC_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.ANTHROPIC_API_KEY}' 2>/dev/null | base64 -d)"
|
||||
OPENAI_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENAI_API_KEY}' 2>/dev/null | base64 -d)"
|
||||
GEMINI_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.GEMINI_API_KEY}' 2>/dev/null | base64 -d)"
|
||||
OPENROUTER_VALUE="$(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENROUTER_API_KEY}' 2>/dev/null | base64 -d)"
|
||||
fi
|
||||
|
||||
TOKEN="${EXISTING_TOKEN:-$(openssl rand -hex 32)}"
|
||||
ANTHROPIC_VALUE="${ANTHROPIC_API_KEY:-$ANTHROPIC_VALUE}"
|
||||
OPENAI_VALUE="${OPENAI_API_KEY:-$OPENAI_VALUE}"
|
||||
GEMINI_VALUE="${GEMINI_API_KEY:-$GEMINI_VALUE}"
|
||||
OPENROUTER_VALUE="${OPENROUTER_API_KEY:-$OPENROUTER_VALUE}"
|
||||
SECRET_MANIFEST="$TMP_DIR/secrets.yaml"
|
||||
|
||||
# Write secret material to temp files so kubectl handles encoding safely.
|
||||
printf '%s' "$TOKEN" > "$TMP_DIR/OPENCLAW_GATEWAY_TOKEN"
|
||||
printf '%s' "$ANTHROPIC_VALUE" > "$TMP_DIR/ANTHROPIC_API_KEY"
|
||||
printf '%s' "$OPENAI_VALUE" > "$TMP_DIR/OPENAI_API_KEY"
|
||||
printf '%s' "$GEMINI_VALUE" > "$TMP_DIR/GEMINI_API_KEY"
|
||||
printf '%s' "$OPENROUTER_VALUE" > "$TMP_DIR/OPENROUTER_API_KEY"
|
||||
chmod 600 \
|
||||
"$TMP_DIR/OPENCLAW_GATEWAY_TOKEN" \
|
||||
"$TMP_DIR/ANTHROPIC_API_KEY" \
|
||||
"$TMP_DIR/OPENAI_API_KEY" \
|
||||
"$TMP_DIR/GEMINI_API_KEY" \
|
||||
"$TMP_DIR/OPENROUTER_API_KEY"
|
||||
|
||||
kubectl create secret generic openclaw-secrets \
|
||||
-n "$NS" \
|
||||
--from-file=OPENCLAW_GATEWAY_TOKEN="$TMP_DIR/OPENCLAW_GATEWAY_TOKEN" \
|
||||
--from-file=ANTHROPIC_API_KEY="$TMP_DIR/ANTHROPIC_API_KEY" \
|
||||
--from-file=OPENAI_API_KEY="$TMP_DIR/OPENAI_API_KEY" \
|
||||
--from-file=GEMINI_API_KEY="$TMP_DIR/GEMINI_API_KEY" \
|
||||
--from-file=OPENROUTER_API_KEY="$TMP_DIR/OPENROUTER_API_KEY" \
|
||||
--dry-run=client \
|
||||
-o yaml > "$SECRET_MANIFEST"
|
||||
chmod 600 "$SECRET_MANIFEST"
|
||||
|
||||
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||
kubectl apply --server-side --field-manager=openclaw -f "$SECRET_MANIFEST" >/dev/null
|
||||
# Clean up any annotation left by older client-side apply runs.
|
||||
kubectl annotate secret openclaw-secrets -n "$NS" kubectl.kubernetes.io/last-applied-configuration- >/dev/null 2>&1 || true
|
||||
rm -rf "$TMP_DIR"
|
||||
trap - EXIT
|
||||
|
||||
if $EXISTING_SECRET; then
|
||||
echo "Secret updated in namespace '$NS'. Existing gateway token preserved."
|
||||
else
|
||||
echo "Secret created in namespace '$NS'."
|
||||
fi
|
||||
|
||||
if $SHOW_TOKEN; then
|
||||
echo "Gateway token: $TOKEN"
|
||||
else
|
||||
echo "Gateway token stored in Secret only."
|
||||
echo "Retrieve it with:"
|
||||
echo " kubectl get secret openclaw-secrets -n $NS -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d && echo"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --create-secret
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "$MODE" == "create-secret" ]]; then
|
||||
HAS_KEY=false
|
||||
for key in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENROUTER_API_KEY; do
|
||||
if [[ -n "${!key:-}" ]]; then
|
||||
HAS_KEY=true
|
||||
echo " Found $key in environment"
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $HAS_KEY; then
|
||||
echo "No API keys found in environment. Export at least one and re-run:"
|
||||
echo " export <PROVIDER>_API_KEY=\"...\" (ANTHROPIC, GEMINI, OPENAI, or OPENROUTER)"
|
||||
echo " ./scripts/k8s/deploy.sh --create-secret"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_apply_secret
|
||||
echo ""
|
||||
echo "Now run:"
|
||||
echo " ./scripts/k8s/deploy.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check that the secret exists in the cluster
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! kubectl get secret openclaw-secrets -n "$NS" &>/dev/null; then
|
||||
HAS_KEY=false
|
||||
for key in ANTHROPIC_API_KEY OPENAI_API_KEY GEMINI_API_KEY OPENROUTER_API_KEY; do
|
||||
[[ -n "${!key:-}" ]] && HAS_KEY=true
|
||||
done
|
||||
|
||||
if $HAS_KEY; then
|
||||
echo "Creating secret from environment..."
|
||||
_apply_secret
|
||||
echo ""
|
||||
else
|
||||
echo "No secret found and no API keys in environment."
|
||||
echo ""
|
||||
echo "Export at least one provider API key and re-run:"
|
||||
echo " export <PROVIDER>_API_KEY=\"...\" (ANTHROPIC, GEMINI, OPENAI, or OPENROUTER)"
|
||||
echo " ./scripts/k8s/deploy.sh"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Deploy
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "Deploying to namespace '$NS'..."
|
||||
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||
kubectl apply -k "$MANIFESTS" -n "$NS"
|
||||
kubectl rollout restart deployment/openclaw -n "$NS" 2>/dev/null || true
|
||||
echo ""
|
||||
echo "Waiting for rollout..."
|
||||
kubectl rollout status deployment/openclaw -n "$NS" --timeout=300s
|
||||
echo ""
|
||||
echo "Done. Access the gateway:"
|
||||
echo " kubectl port-forward svc/openclaw 18789:18789 -n $NS"
|
||||
echo " open http://localhost:18789"
|
||||
echo ""
|
||||
if $SHOW_TOKEN; then
|
||||
echo "Gateway token (paste into Control UI):"
|
||||
echo " $(kubectl get secret openclaw-secrets -n "$NS" -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d)"
|
||||
echo ""
|
||||
fi
|
||||
echo "Retrieve the gateway token with:"
|
||||
echo " kubectl get secret openclaw-secrets -n $NS -o jsonpath='{.data.OPENCLAW_GATEWAY_TOKEN}' | base64 -d && echo"
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: openclaw-config
|
||||
labels:
|
||||
app: openclaw
|
||||
data:
|
||||
openclaw.json: |
|
||||
{
|
||||
"gateway": {
|
||||
"mode": "local",
|
||||
"bind": "loopback",
|
||||
"port": 18789,
|
||||
"auth": {
|
||||
"mode": "token"
|
||||
},
|
||||
"controlUi": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "~/.openclaw/workspace"
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": "default",
|
||||
"name": "OpenClaw Assistant",
|
||||
"workspace": "~/.openclaw/workspace"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cron": { "enabled": false }
|
||||
}
|
||||
AGENTS.md: |
|
||||
# OpenClaw Assistant
|
||||
|
||||
You are a helpful AI assistant running in Kubernetes.
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: openclaw
|
||||
labels:
|
||||
app: openclaw
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: openclaw
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: openclaw
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
initContainers:
|
||||
- name: init-config
|
||||
image: busybox:1.37
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
cp /config/openclaw.json /home/node/.openclaw/openclaw.json
|
||||
mkdir -p /home/node/.openclaw/workspace
|
||||
cp /config/AGENTS.md /home/node/.openclaw/workspace/AGENTS.md
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
resources:
|
||||
requests:
|
||||
memory: 32Mi
|
||||
cpu: 50m
|
||||
limits:
|
||||
memory: 64Mi
|
||||
cpu: 100m
|
||||
volumeMounts:
|
||||
- name: openclaw-home
|
||||
mountPath: /home/node/.openclaw
|
||||
- name: config
|
||||
mountPath: /config
|
||||
containers:
|
||||
- name: gateway
|
||||
image: ghcr.io/openclaw/openclaw:slim
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- node
|
||||
- /app/dist/index.js
|
||||
- gateway
|
||||
- run
|
||||
ports:
|
||||
- name: gateway
|
||||
containerPort: 18789
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: HOME
|
||||
value: /home/node
|
||||
- name: OPENCLAW_CONFIG_DIR
|
||||
value: /home/node/.openclaw
|
||||
- name: NODE_ENV
|
||||
value: production
|
||||
- name: OPENCLAW_GATEWAY_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: openclaw-secrets
|
||||
key: OPENCLAW_GATEWAY_TOKEN
|
||||
- name: ANTHROPIC_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: openclaw-secrets
|
||||
key: ANTHROPIC_API_KEY
|
||||
optional: true
|
||||
- name: OPENAI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: openclaw-secrets
|
||||
key: OPENAI_API_KEY
|
||||
optional: true
|
||||
- name: GEMINI_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: openclaw-secrets
|
||||
key: GEMINI_API_KEY
|
||||
optional: true
|
||||
- name: OPENROUTER_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: openclaw-secrets
|
||||
key: OPENROUTER_API_KEY
|
||||
optional: true
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 250m
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: "1"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- node
|
||||
- -e
|
||||
- "require('http').get('http://127.0.0.1:18789/healthz', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- node
|
||||
- -e
|
||||
- "require('http').get('http://127.0.0.1:18789/readyz', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
volumeMounts:
|
||||
- name: openclaw-home
|
||||
mountPath: /home/node/.openclaw
|
||||
- name: tmp-volume
|
||||
mountPath: /tmp
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumes:
|
||||
- name: openclaw-home
|
||||
persistentVolumeClaim:
|
||||
claimName: openclaw-home-pvc
|
||||
- name: config
|
||||
configMap:
|
||||
name: openclaw-config
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- pvc.yaml
|
||||
- configmap.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: openclaw-home-pvc
|
||||
labels:
|
||||
app: openclaw
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: openclaw
|
||||
labels:
|
||||
app: openclaw
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: openclaw
|
||||
ports:
|
||||
- name: gateway
|
||||
port: 18789
|
||||
targetPort: 18789
|
||||
protocol: TCP
|
||||
Loading…
Reference in New Issue