Skip to content

Commit 84d895e

Browse files
committed
Deploy DO Droplet for OpenClaw
1 parent fb13caf commit 84d895e

5 files changed

Lines changed: 222 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is a Terraform-based infrastructure-as-code (IaC) project that provisions and manages a Kubernetes cluster on DigitalOcean with supporting services, using a GitOps approach via ArgoCD.
8+
9+
## Common Commands
10+
11+
### Terraform Format & Validate
12+
13+
```bash
14+
# Format check
15+
terraform -chdir=environments/prod/infra fmt -check
16+
terraform -chdir=environments/prod/k8s fmt -check
17+
18+
# Auto-format
19+
terraform -chdir=environments/prod/infra fmt -recursive
20+
terraform -chdir=environments/prod/k8s fmt -recursive
21+
22+
# Validate
23+
terraform -chdir=environments/prod/infra validate
24+
terraform -chdir=environments/prod/k8s validate
25+
```
26+
27+
### Terraform Plan & Apply
28+
29+
```bash
30+
# Init (required after module changes or first run)
31+
terraform -chdir=environments/prod/infra init
32+
terraform -chdir=environments/prod/k8s init
33+
34+
# Plan
35+
terraform -chdir=environments/prod/infra plan
36+
terraform -chdir=environments/prod/k8s plan
37+
38+
# Apply (infra must be applied before k8s)
39+
terraform -chdir=environments/prod/infra apply
40+
terraform -chdir=environments/prod/k8s apply -var="kubeconfig=<kubeconfig_path>"
41+
```
42+
43+
## Architecture
44+
45+
### Two-Layer Terraform Structure
46+
47+
The project is split into two sequentially-dependent layers under `environments/prod/`:
48+
49+
1. **`infra/`** — Provisions DigitalOcean infrastructure: VPC (10.0.4.0/22), reserved IP, SSH keys, and the K8s control plane droplet (Ubuntu 22.04, 2vCPU/4GB, SFO2). Outputs a kubeconfig used by the k8s layer.
50+
51+
2. **`k8s/`** — Deploys all Kubernetes workloads using Kubernetes, Helm, kubectl, GitHub, and Cloudflare providers. Depends on the kubeconfig output from the infra layer. This layer also manages Cloudflare DNS records.
52+
53+
The k8s layer receives the kubeconfig by reading infra state output (automated in CI via `terraform -chdir=environments/prod/infra output -raw kubeconfig`).
54+
55+
### Reusable Modules (`modules/`)
56+
57+
All infrastructure logic lives in reusable modules. The k8s environment wires them together:
58+
59+
| Module | Purpose |
60+
|--------|---------|
61+
| `controlplane` | Self-managed K8s via cloud-init on a DigitalOcean droplet |
62+
| `argocd` | GitOps CD platform (Helm chart), syncs from `m8rmclaren/infra-gitops` repo |
63+
| `argo_helm_app` | Generic wrapper for deploying any Helm app via ArgoCD AppProject |
64+
| `auth` | Ory Kratos (identity) + Ory Hydra (OAuth2/OIDC) with Apple SIWA support |
65+
| `cert_manager` | Let's Encrypt via cert-manager with Cloudflare DNS-01 challenge |
66+
| `database` | PostgreSQL with replication via Helm/ArgoCD |
67+
| `domain` | Cloudflare DNS A record management |
68+
| `external_dns` | Automatic DNS updates from K8s service annotations |
69+
| `gateway` | Kubernetes Gateway API config |
70+
| `ghcr_pull_secret` | GHCR image pull secrets for private container images |
71+
| `health_auto_export_server` | Health metrics auto-export server |
72+
| `istio` | Service mesh |
73+
| `website` | Website deployment via ArgoCD |
74+
75+
### GitOps Pattern
76+
77+
- **App definitions** live in this repo (Terraform manages ArgoCD `Application` resources)
78+
- **App manifests/Helm values** live in `m8rmclaren/infra-gitops` (synced by ArgoCD)
79+
- This means Terraform provisions the ArgoCD `Application` CRD, ArgoCD handles actual K8s deployment
80+
81+
### State Management
82+
83+
- Backend: AWS S3 (`m8rmclaren-terraform-state-infra`, us-west-1) with encryption
84+
- Separate state files for `infra` and `k8s` layers
85+
- Auth: GitHub Actions uses AWS OIDC (`arn:aws:iam::691595548805:role/github_actions_full_admin`)
86+
87+
### CI/CD
88+
89+
- **`plan.yml`**: Runs on PRs — formats, validates, and plans both layers; posts results as sticky PR comments
90+
- **`apply.yml`**: Runs on push to `main` — applies infra first, then k8s. Includes fresh-cluster detection logic that taints resources on first apply (checks namespace count to determine if cluster is new)
91+
92+
### Key Secrets (managed in GitHub Actions environment `prod`)
93+
94+
`DIGITALOCEAN_TOKEN`, `CLOUDFLARE_API_TOKEN`, `GH_PAT`, `EMAIL`, `DOMAIN`, `POSTGRES_ADMIN_PASSWORD`, `POSTGRES_REPLICATION_PASSWORD`, `HYDRA_DATABASE_PASSWORD`, `HYDRA_SYSTEM_SECRET`, `HYDRA_COOKIE_SECRET`, `KRATOS_DATABASE_PASSWORD`, `APPLE_DEVELOPER_TEAM_ID`, `CHAT_PRIMARY_APP_ID`
95+
96+
## Bootstrap
97+
98+
To initialize the S3 state backend (one-time setup):
99+
100+
```bash
101+
aws s3api create-bucket \
102+
--bucket m8rmclaren-terraform-state-infra \
103+
--region us-west-1 \
104+
--create-bucket-configuration LocationConstraint=us-west-1
105+
```
106+
107+
If the bucket name or region is changed, update both `backend.tf` files accordingly.

environments/prod/infra/main.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,30 @@ resource "digitalocean_vpc" "k8s" {
3838
ip_range = "10.0.4.0/22"
3939
}
4040

41+
resource "digitalocean_vpc" "main" {
42+
name = "main"
43+
region = local.do_region
44+
ip_range = "10.0.4.0/22"
45+
}
46+
4147
resource "digitalocean_reserved_ip" "primary" {
4248
region = local.do_region
4349
}
4450

51+
resource "digitalocean_reserved_ip" "openclaw" {
52+
region = local.do_region
53+
}
54+
55+
module "openclaw" {
56+
source = "../../../modules/openclaw"
57+
region = local.do_region
58+
droplet_size = "s-1vcpu-1gb"
59+
ssh_key_id = digitalocean_ssh_key.primary.id
60+
ssh_key = tls_private_key.primary.private_key_openssh
61+
vpc_uuid = digitalocean_vpc.main.id
62+
public_ip = digitalocean_reserved_ip.openclaw.ip_address
63+
}
64+
4565
module "controlplane" {
4666
source = "../../../modules/controlplane"
4767
region = local.do_region
@@ -62,3 +82,7 @@ output "kubeconfig" {
6282
output "public_ip" {
6383
value = digitalocean_reserved_ip.primary.ip_address
6484
}
85+
86+
output "openclaw_public_ip" {
87+
value = digitalocean_reserved_ip.openclaw.ip_address
88+
}

modules/openclaw/main.tf

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
terraform {
2+
required_providers {
3+
digitalocean = {
4+
source = "digitalocean/digitalocean"
5+
version = "~> 2.0"
6+
}
7+
}
8+
}
9+
10+
resource "digitalocean_droplet" "openclaw" {
11+
name = "openclaw"
12+
image = "ubuntu-22-04-x64"
13+
region = var.region
14+
size = var.droplet_size
15+
16+
vpc_uuid = var.vpc_uuid
17+
18+
ssh_keys = [var.ssh_key_id]
19+
20+
monitoring = true
21+
22+
tags = ["openclaw"]
23+
}
24+
25+
# Wait for droplet to come online
26+
resource "null_resource" "wait_for_droplet" {
27+
connection {
28+
type = "ssh"
29+
host = digitalocean_droplet.openclaw.ipv4_address
30+
user = "root"
31+
private_key = var.ssh_key
32+
}
33+
34+
provisioner "remote-exec" {
35+
inline = ["echo Droplet is ready"]
36+
}
37+
38+
depends_on = [digitalocean_droplet.openclaw]
39+
40+
triggers = {
41+
droplet_id = digitalocean_droplet.openclaw.id
42+
droplet_ip = digitalocean_droplet.openclaw.ipv4_address
43+
droplet_status = digitalocean_droplet.openclaw.status
44+
}
45+
}
46+
47+
# Bind static IP
48+
resource "digitalocean_reserved_ip_assignment" "public_ip" {
49+
droplet_id = digitalocean_droplet.openclaw.id
50+
ip_address = var.public_ip
51+
52+
depends_on = [null_resource.wait_for_droplet]
53+
}

modules/openclaw/outputs.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
output "ipv4_address" {
2+
value = digitalocean_droplet.openclaw.ipv4_address
3+
}
4+
5+
output "public_ip" {
6+
value = var.public_ip
7+
}

modules/openclaw/variables.tf

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
variable "region" {
2+
type = string
3+
default = "sfo2"
4+
description = "DO region"
5+
}
6+
7+
variable "droplet_size" {
8+
type = string
9+
default = "s-1vcpu-1gb"
10+
}
11+
12+
variable "ssh_key_id" {
13+
type = string
14+
description = "DO SSH key ID that was already created in DO"
15+
}
16+
17+
variable "ssh_key" {
18+
type = string
19+
sensitive = true
20+
description = "SSH private key associated with ssh_key_id"
21+
}
22+
23+
variable "vpc_uuid" {
24+
type = string
25+
description = "DO VPC UUID"
26+
}
27+
28+
variable "public_ip" {
29+
type = string
30+
description = "DO reserved IP address"
31+
}

0 commit comments

Comments
 (0)