# ============================================================================= # 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 } } } } } }