Files
sdlc-orchestrator/coder/cloudflare-worker/main.tf
T
Daniel SamsonandClaude Opus 4.6 55dee1f2bd Initial commit: SDLC Orchestrator replacing n8n
Lightweight Hono webhook router that receives Gitea events and creates
Coder workspaces for each SDLC stage (analyse, architect, develop,
review, test, rework, release, maintenance). Includes k8s manifests
for deployment and the Coder workspace template moved from babble.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 12:44:28 +01:00

692 lines
18 KiB
Terraform

# =============================================================================
# Cloudflare Worker Development - Coder Template
# =============================================================================
# Lightweight workspace for Cloudflare Workers with Hono, Drizzle, and
# Claude Code automation. Supports automated SDLC via n8n triggers.
# =============================================================================
terraform {
required_providers {
coder = {
source = "coder/coder"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
# =============================================================================
# Data Sources
# =============================================================================
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_provisioner" "me" {}
data "coder_external_auth" "github" {
id = "primary-github"
}
# =============================================================================
# Parameters
# =============================================================================
data "coder_parameter" "cpu" {
name = "cpu"
display_name = "CPU Cores"
description = "Number of CPU cores"
type = "number"
default = "2"
mutable = false
option {
name = "1 Core"
value = "1"
}
option {
name = "2 Cores"
value = "2"
}
option {
name = "4 Cores"
value = "4"
}
}
data "coder_parameter" "memory" {
name = "memory"
display_name = "Memory (MB)"
description = "Amount of memory"
type = "number"
default = "4096"
mutable = false
option {
name = "2048 MB"
value = "2048"
}
option {
name = "4096 MB"
value = "4096"
}
option {
name = "8192 MB"
value = "8192"
}
}
data "coder_parameter" "disk_size" {
name = "disk_size"
display_name = "Disk Size (GB)"
description = "Persistent home directory size"
type = "number"
default = "10"
mutable = false
option {
name = "5 GB"
value = "5"
}
option {
name = "10 GB"
value = "10"
}
option {
name = "20 GB"
value = "20"
}
}
# ─── Automation Parameters ───────────────────────────────────────────────────
data "coder_parameter" "repo_clone_url" {
name = "repo_clone_url"
display_name = "Repository Clone URL"
description = "Git repo URL to clone (leave empty for manual setup)"
type = "string"
default = ""
mutable = false
}
data "coder_parameter" "task_type" {
name = "task_type"
display_name = "Task Type"
description = "Claude Code command to run automatically (leave empty for interactive use)"
type = "string"
default = ""
mutable = false
option {
name = "None (interactive)"
value = ""
}
option {
name = "Analyse"
value = "analyse"
}
option {
name = "Architect"
value = "architect"
}
option {
name = "Develop"
value = "develop"
}
option {
name = "Review Code"
value = "review-code"
}
option {
name = "Test"
value = "test"
}
option {
name = "Fix Bug"
value = "fix-bug"
}
option {
name = "Rework PR"
value = "rework-pr"
}
option {
name = "Release"
value = "release"
}
option {
name = "DevOps"
value = "devops"
}
option {
name = "Create Migration"
value = "create-migration"
}
}
data "coder_parameter" "issue_number" {
name = "issue_number"
display_name = "Issue/PR Number"
description = "Gitea issue or PR number (for automated tasks)"
type = "string"
default = ""
mutable = false
}
data "coder_parameter" "gitea_token" {
name = "gitea_token"
display_name = "Gitea API Token"
description = "API token for reading issues and posting comments"
type = "string"
default = ""
mutable = false
}
data "coder_parameter" "gitea_org" {
name = "gitea_org"
display_name = "Gitea Organization"
description = "Gitea organization name"
type = "string"
default = "claude"
mutable = false
}
data "coder_parameter" "gitea_repo" {
name = "gitea_repo"
display_name = "Gitea Repository"
description = "Gitea repository name"
type = "string"
default = ""
mutable = false
}
data "coder_parameter" "anthropic_api_key" {
name = "anthropic_api_key"
display_name = "Anthropic API Key"
description = "API key for Claude Code"
type = "string"
default = ""
mutable = false
}
data "coder_parameter" "deploy_env" {
name = "deploy_env"
display_name = "Deploy Environment"
description = "Target environment for deploy tasks"
type = "string"
default = ""
mutable = false
option {
name = "None"
value = ""
}
option {
name = "Staging"
value = "staging"
}
option {
name = "Production"
value = "production"
}
}
# =============================================================================
# Locals
# =============================================================================
locals {
namespace = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = data.coder_workspace.me.name
"app.kubernetes.io/managed-by" = "coder"
"coder.workspace.owner" = data.coder_workspace_owner.me.name
}
}
# =============================================================================
# Namespace
# =============================================================================
resource "kubernetes_namespace_v1" "workspace" {
metadata {
name = local.namespace
labels = local.labels
annotations = {
"field.cattle.io/projectId" = "local:p-w2w48"
}
}
}
# =============================================================================
# Secrets - Registry Access
# =============================================================================
data "kubernetes_secret_v1" "registry_source" {
metadata {
name = "registry-samson-media"
namespace = "registry"
}
}
resource "kubernetes_secret_v1" "registry" {
metadata {
name = "registry-samson-media"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
type = "kubernetes.io/dockerconfigjson"
data = {
".dockerconfigjson" = data.kubernetes_secret_v1.registry_source.data[".dockerconfigjson"]
}
}
# =============================================================================
# RBAC
# =============================================================================
resource "kubernetes_service_account_v1" "workspace" {
metadata {
name = "workspace"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
}
resource "kubernetes_role_v1" "workspace" {
metadata {
name = "workspace-admin"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
rule {
api_groups = ["", "apps", "batch", "networking.k8s.io"]
resources = ["*"]
verbs = ["*"]
}
}
resource "kubernetes_role_binding_v1" "workspace" {
metadata {
name = "workspace-admin"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "Role"
name = kubernetes_role_v1.workspace.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account_v1.workspace.metadata[0].name
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
}
}
resource "kubernetes_role_binding_v1" "rancher_user_access" {
metadata {
name = "rancher-user-access"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = "coder-developer"
}
subject {
api_group = "rbac.authorization.k8s.io"
kind = "User"
name = data.coder_workspace_owner.me.name
}
}
# =============================================================================
# Resource Quotas
# =============================================================================
resource "kubernetes_resource_quota_v1" "workspace" {
metadata {
name = "workspace-quota"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
spec {
hard = {
"requests.cpu" = "${tonumber(data.coder_parameter.cpu.value) * 500}m"
"limits.cpu" = "${tonumber(data.coder_parameter.cpu.value) * 1000}m"
"requests.memory" = "${tonumber(data.coder_parameter.memory.value) / 2}Mi"
"limits.memory" = "${data.coder_parameter.memory.value}Mi"
"requests.storage" = "${data.coder_parameter.disk_size.value}Gi"
"persistentvolumeclaims" = "1"
}
}
}
# =============================================================================
# Storage
# =============================================================================
resource "kubernetes_persistent_volume_claim_v1" "home" {
metadata {
name = "home"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
wait_until_bound = false
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = "longhorn-backup"
resources {
requests = {
storage = "${data.coder_parameter.disk_size.value}Gi"
}
}
}
}
# =============================================================================
# Coder Agent
# =============================================================================
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = "linux"
dir = "/home/coder"
startup_script_behavior = "non-blocking"
# Configure auto-stop TTL in Coder template settings:
# Settings > Auto-stop > Default TTL: 1 hour
# This ensures automated workspaces don't run indefinitely.
metadata {
display_name = "Task Type"
key = "task_type"
script = "echo $TASK_TYPE"
interval = 0
}
metadata {
display_name = "Issue Number"
key = "issue_number"
script = "echo $ISSUE_NUMBER"
interval = 0
}
display_apps {
vscode = true
vscode_insiders = false
ssh_helper = true
port_forwarding_helper = true
web_terminal = true
}
env = {
ANTHROPIC_API_KEY = data.coder_parameter.anthropic_api_key.value
GITHUB_TOKEN = data.coder_external_auth.github.access_token
GITEA_TOKEN = data.coder_parameter.gitea_token.value
GITEA_ORG = data.coder_parameter.gitea_org.value
GITEA_REPO = data.coder_parameter.gitea_repo.value
TASK_TYPE = data.coder_parameter.task_type.value
ISSUE_NUMBER = data.coder_parameter.issue_number.value
REPO_CLONE_URL = data.coder_parameter.repo_clone_url.value
DEPLOY_ENV = data.coder_parameter.deploy_env.value
}
startup_script = <<-EOT
#!/bin/bash
set -e
# --- Install Node.js 22 ---
if ! command -v node &> /dev/null; then
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
fi
# --- Install global tools ---
if ! command -v wrangler &> /dev/null; then
sudo npm install -g wrangler @anthropic-ai/claude-code
fi
# --- Install Playwright system dependencies ---
if ! dpkg -s libgbm1 &> /dev/null; then
sudo apt-get update
sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
libcups2t64 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64 libatspi2.0-0 \
|| sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2 libatspi2.0-0
fi
# --- Configure git ---
git config --global user.name "${data.coder_workspace_owner.me.full_name}"
git config --global user.email "${data.coder_workspace_owner.me.email}"
# Configure git to use GITEA_TOKEN for HTTPS push/pull
if [ -n "$GITEA_TOKEN" ]; then
git config --global credential.helper 'store'
echo "https://oauth2:$GITEA_TOKEN@gitea.samson.media" > ~/.git-credentials
chmod 600 ~/.git-credentials
fi
# --- Clone repository ---
if [ -n "$REPO_CLONE_URL" ] && [ ! -d ~/project ]; then
git clone "$REPO_CLONE_URL" ~/project
cd ~/project
# Switch to develop branch if it exists
git fetch origin develop 2>/dev/null && git checkout develop 2>/dev/null || true
# Install backend deps
npm ci --legacy-peer-deps
# Install frontend deps
cd frontend && npm ci && cd ..
# Build frontend (required — vitest fails without frontend/dist)
npm run build:frontend
# Install Playwright Chromium
npx playwright install chromium
# Apply local migrations
npm run db:migrate:local
fi
# --- Automated task execution ---
if [ -n "$TASK_TYPE" ] && [ -d ~/project ]; then
cd ~/project
ARGS="$ISSUE_NUMBER"
if [ "$TASK_TYPE" = "devops" ] && [ -n "$DEPLOY_ENV" ]; then
ARGS="$DEPLOY_ENV"
fi
# Run Claude Code in non-interactive mode
claude --print --dangerously-skip-permissions "/project:$TASK_TYPE $ARGS" 2>&1 | tee ~/task-output.log
echo "Task completed. Output saved to ~/task-output.log"
fi
EOT
}
# =============================================================================
# Main Workspace Deployment
# =============================================================================
resource "kubernetes_deployment_v1" "workspace" {
metadata {
name = "workspace"
namespace = kubernetes_namespace_v1.workspace.metadata[0].name
labels = local.labels
}
wait_for_rollout = false
spec {
replicas = data.coder_workspace.me.start_count
strategy {
type = "Recreate"
}
selector {
match_labels = {
app = "workspace"
}
}
template {
metadata {
labels = merge(local.labels, {
app = "workspace"
})
}
spec {
service_account_name = kubernetes_service_account_v1.workspace.metadata[0].name
security_context {
run_as_user = 1000
run_as_group = 1000
fs_group = 1000
}
image_pull_secrets {
name = kubernetes_secret_v1.registry.metadata[0].name
}
init_container {
name = "fix-permissions"
image = "busybox:latest"
command = ["sh", "-c", <<-EOC
if [ ! -d /home/coder/project ]; then
mkdir -p /home/coder/project
fi
chown -R 1000:1000 /home/coder
EOC
]
volume_mount {
name = "home"
mount_path = "/home/coder"
}
resources {
requests = {
cpu = "5m"
memory = "8Mi"
}
limits = {
cpu = "50m"
memory = "32Mi"
}
}
security_context {
run_as_user = 0
}
}
container {
name = "coder-agent"
image = "codercom/enterprise-base:ubuntu-arm64"
command = ["sh", "-c", coder_agent.main.init_script]
env {
name = "CODER_AGENT_TOKEN"
value = coder_agent.main.token
}
env {
name = "ANTHROPIC_API_KEY"
value = data.coder_parameter.anthropic_api_key.value
}
env {
name = "GITHUB_TOKEN"
value = data.coder_external_auth.github.access_token
}
env {
name = "GITEA_TOKEN"
value = data.coder_parameter.gitea_token.value
}
env {
name = "GITEA_ORG"
value = data.coder_parameter.gitea_org.value
}
env {
name = "GITEA_REPO"
value = data.coder_parameter.gitea_repo.value
}
env {
name = "TASK_TYPE"
value = data.coder_parameter.task_type.value
}
env {
name = "ISSUE_NUMBER"
value = data.coder_parameter.issue_number.value
}
env {
name = "REPO_CLONE_URL"
value = data.coder_parameter.repo_clone_url.value
}
env {
name = "DEPLOY_ENV"
value = data.coder_parameter.deploy_env.value
}
resources {
requests = {
cpu = "${tonumber(data.coder_parameter.cpu.value) * 500}m"
memory = "${tonumber(data.coder_parameter.memory.value) / 2}Mi"
}
limits = {
cpu = "${tonumber(data.coder_parameter.cpu.value) * 1000}m"
memory = "${data.coder_parameter.memory.value}Mi"
}
}
volume_mount {
name = "home"
mount_path = "/home/coder"
}
security_context {
run_as_user = 1000
run_as_group = 1000
}
}
volume {
name = "home"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim_v1.home.metadata[0].name
}
}
}
}
}
}