# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # TERRAFORM BLOCK # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ terraform { required_providers { coder = { source = "coder/coder" version = "2.3.0" } kubernetes = { source = "hashicorp/kubernetes" } } } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # PROVIDERS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ provider "coder" {} provider "kubernetes" { # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences config_path = var.use_kubeconfig == true ? "~/.kube/config" : null } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # VARIABLES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ variable "vscode_image" { type = string description = "The VSCode container image to use for workspaces" default = "artifactory.example.com/container-dsl-nonprod/testuser1/vscode-ide:v1.0.0" validation { condition = can(regex("^.+:.+$", var.vscode_image)) error_message = "Image must include a tag (e.g., 'registry/image:tag')." } } variable "namespace" { type = string description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces)" default = "coder" validation { condition = can(regex("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", var.namespace)) || var.namespace == "" error_message = "Namespace must be a valid Kubernetes namespace name." } } variable "use_kubeconfig" { type = bool description = <<-EOF Use host kubeconfig? (true/false) Set this to false if the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to. Set this to true if the Coder host is running outside the Kubernetes cluster for workspaces. A valid "~/.kube/config" must be present on the Coder host. EOF default = false } variable "node_selector_key" { type = string description = "Node selector key for workspace placement (optional)" default = "" } variable "node_selector_value" { type = string description = "Node selector value for workspace placement (optional)" default = "" } variable "default_cpu" { type = string description = "Default CPU allocation for workspaces" default = "2" validation { condition = contains(["1", "2", "4", "6", "8"], var.default_cpu) error_message = "CPU must be one of: 1, 2, 4, 6, 8." } } variable "default_memory" { type = string description = "Default memory allocation for workspaces (in GB)" default = "4" validation { condition = contains(["2", "4", "8", "16"], var.default_memory) error_message = "Memory must be one of: 2, 4, 8, 16." } } variable "default_disk_size" { type = string description = "Default disk size for workspaces (in GB)" default = "20" validation { condition = contains(["10", "20", "50", "100"], var.default_disk_size) error_message = "Disk size must be one of: 10, 20, 50, 100." } } variable "image_pull_secret" { description = "Path to a docker config.json containing credentials to the provided cache repo, if required." sensitive = true type = string default = "artifactory-prod" } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # LOCALS # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ locals { deployment_name = "coder-${lower(data.coder_workspace.me.id)}" git_author_name = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) git_author_email = data.coder_workspace_owner.me.email repo_url = data.coder_parameter.repo_url.value } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # DATA SOURCES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ data "coder_provisioner" "me" {} data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} # User parameters data "coder_parameter" "cpu" { name = "cpu" display_name = "CPU" description = "The number of CPU cores" default = "2" icon = "/icon/memory.svg" mutable = true option { name = "2 Cores" value = "2" } option { name = "4 Cores" value = "4" } option { name = "6 Cores" value = "6" } option { name = "8 Cores" value = "8" } } data "coder_parameter" "memory" { name = "memory" display_name = "Memory" description = "The amount of memory in GB" default = "4" icon = "/icon/memory.svg" mutable = true option { name = "2 GB" value = "2" } option { name = "4 GB" value = "4" } option { name = "8 GB" value = "8" } option { name = "16 GB" value = "16" } } data "coder_parameter" "disk_size" { name = "disk_size" display_name = "Disk Size" description = "The size of the persistent volume in GB" default = "20" icon = "/icon/folder.svg" mutable = false option { name = "10 GB" value = "10" } option { name = "20 GB" value = "20" } option { name = "50 GB" value = "50" } option { name = "100 GB" value = "100" } } data "coder_parameter" "repo_url" { name = "repo_url" display_name = "Repository URL" description = "Git repository to clone into the workspace (optional)" default = "https://tfs.example.com/tfs/EXAMPLE_Git_Collection/CloudEngineering/_git/example-helloworld-app-code" mutable = true icon = "/icon/git.svg" } data "coder_parameter" "repo_branch" { name = "repo_branch" display_name = "Repository Branch" description = "Git branch to checkout (optional, defaults to main/master)" default = "develop" mutable = true icon = "/icon/git.svg" } data "coder_parameter" "git_username" { name = "git_username" display_name = "Git Username" description = "Git username for authentication" default = "" mutable = true icon = "/icon/git.svg" } data "coder_parameter" "git_token" { name = "git_token" display_name = "Git Token/Password" description = "Git personal access token or password" default = "" mutable = true icon = "/icon/git.svg" type = "string" } data "coder_parameter" "git_email" { name = "git_email" display_name = "Git Email" description = "Git email for commits" default = data.coder_workspace_owner.me.email mutable = true icon = "/icon/git.svg" } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # CREATE PERSISTENT STORAGE # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ resource "kubernetes_persistent_volume_claim" "workspaces" { metadata { name = "coder-${lower(data.coder_workspace.me.id)}-workspaces" namespace = "coder" labels = { "app.kubernetes.io/name" = "coder-${lower(data.coder_workspace.me.id)}-workspaces" "app.kubernetes.io/instance" = "coder-${lower(data.coder_workspace.me.id)}-workspaces" "app.kubernetes.io/part-of" = "coder" "com.coder.resource" = "true" "com.coder.workspace.id" = data.coder_workspace.me.id "com.coder.workspace.name" = data.coder_workspace.me.name "com.coder.user.id" = data.coder_workspace_owner.me.id "com.coder.user.username" = data.coder_workspace_owner.me.name } annotations = { "com.coder.user.email" = data.coder_workspace_owner.me.email } } wait_until_bound = false spec { access_modes = ["ReadWriteOnce"] resources { requests = { storage = "${data.coder_parameter.disk_size.value}Gi" } } } } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # CREATE KUBERNETES DEPLOYMENT # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ resource "kubernetes_deployment" "main" { count = data.coder_workspace.me.start_count depends_on = [ kubernetes_persistent_volume_claim.workspaces ] wait_for_rollout = false metadata { name = local.deployment_name namespace = "coder" labels = { "app.kubernetes.io/name" = "coder-workspace" "app.kubernetes.io/instance" = local.deployment_name "app.kubernetes.io/part-of" = "coder" "com.coder.resource" = "true" "com.coder.workspace.id" = data.coder_workspace.me.id "com.coder.workspace.name" = data.coder_workspace.me.name "com.coder.user.id" = data.coder_workspace_owner.me.id "com.coder.user.username" = data.coder_workspace_owner.me.name } annotations = { "com.coder.user.email" = data.coder_workspace_owner.me.email } } spec { replicas = 1 selector { match_labels = { "app.kubernetes.io/name" = "coder-workspace" } } strategy { type = "Recreate" } template { metadata { labels = { "app.kubernetes.io/name" = "coder-workspace" } } spec { security_context { fs_group = 1000 } container { name = "dev" image = var.vscode_image image_pull_policy = "Always" # Use your start-workspace.sh script command = ["/usr/local/bin/start-workspace.sh"] security_context { run_as_user = 1000 run_as_group = 1000 } env { name = "CODER_AGENT_TOKEN" value = coder_agent.main.token } env { name = "REPO_URL" value = data.coder_parameter.repo_url.value } env { name = "REPO_BRANCH" value = data.coder_parameter.repo_branch.value } env { name = "GIT_USERNAME" value = data.coder_parameter.git_username.value } env { name = "GIT_TOKEN" value = data.coder_parameter.git_token.value } env { name = "GIT_EMAIL" value = data.coder_parameter.git_email.value } # Code-server configuration env { name = "PORT" value = "8080" } env { name = "APP_NAME" value = "VS Code" } env { name = "FOLDER" value = "/home/coder/workspace" } env { name = "LOG_PATH" value = "/home/coder/.local/share/code-server/logs/code-server.log" } env { name = "EXTENSIONS_DIR" value = "/home/coder/.local/share/code-server/extensions" } resources { requests = { "cpu" = "250m" "memory" = "512Mi" } limits = { "cpu" = "${data.coder_parameter.cpu.value}" "memory" = "${data.coder_parameter.memory.value}Gi" } } # Mount only workspace directory to preserve image setup volume_mount { mount_path = "/home/coder/workspace" name = "workspace" read_only = false } } image_pull_secrets { name = var.image_pull_secret } volume { name = "workspace" persistent_volume_claim { claim_name = kubernetes_persistent_volume_claim.workspaces.metadata.0.name read_only = false } } affinity { pod_anti_affinity { preferred_during_scheduling_ignored_during_execution { weight = 1 pod_affinity_term { topology_key = "kubernetes.io/hostname" label_selector { match_expressions { key = "app.kubernetes.io/name" operator = "In" values = ["coder-workspace"] } } } } } } } } } } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # CREATE CODER AGENT # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ resource "coder_agent" "main" { arch = data.coder_provisioner.me.arch os = "linux" startup_script_behavior = "blocking" startup_script = <<-EOT #!/bin/bash set -e echo "🚀 Coder agent starting..." # Wait for code-server to be ready echo "⏳ Waiting for code-server to start..." timeout=60 while [ $timeout -gt 0 ]; do if curl -f http://localhost:8080/healthz >/dev/null 2>&1; then echo "✅ Code-server is ready" break fi sleep 2 timeout=$((timeout - 2)) done if [ $timeout -le 0 ]; then echo "⚠️ Code-server health check timeout" # Check if process is running even if health check fails if pgrep -f "code-server" > /dev/null; then echo "✅ Code-server process is running" else echo "❌ Code-server process not found" exit 1 fi fi echo "✅ Agent startup complete" EOT dir = "/home/coder/workspace" env = { GIT_AUTHOR_NAME = data.coder_parameter.git_username.value != "" ? data.coder_parameter.git_username.value : coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) GIT_AUTHOR_EMAIL = data.coder_parameter.git_email.value GIT_COMMITTER_NAME = data.coder_parameter.git_username.value != "" ? data.coder_parameter.git_username.value : coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) GIT_COMMITTER_EMAIL = data.coder_parameter.git_email.value REPO_URL = data.coder_parameter.repo_url.value REPO_BRANCH = data.coder_parameter.repo_branch.value GIT_USERNAME = data.coder_parameter.git_username.value GIT_TOKEN = data.coder_parameter.git_token.value GIT_EMAIL = data.coder_parameter.git_email.value } metadata { display_name = "CPU Usage" key = "0_cpu_usage" script = "coder stat cpu" interval = 10 timeout = 1 } metadata { display_name = "RAM Usage" key = "1_ram_usage" script = "coder stat mem" interval = 10 timeout = 1 } metadata { display_name = "Home Disk" key = "3_home_disk" script = "coder stat disk --path $${HOME}" interval = 60 timeout = 1 } } resource "coder_app" "code-server" { agent_id = coder_agent.main.id slug = "code-server" display_name = "VS Code" url = "http://localhost:8080" icon = "/icon/code.svg" subdomain = false share = "owner" healthcheck { url = "http://localhost:8080/healthz" interval = 5 threshold = 6 } } resource "coder_metadata" "container_info" { count = data.coder_workspace.me.start_count resource_id = coder_agent.main.id item { key = "workspace image" value = var.vscode_image } item { key = "git url" value = local.repo_url } }