coder: initial import of deployed stack (v2.35.3 compose, docker template, SSOT vault)
This commit is contained in:
423
templates/docker/main.tf
Normal file
423
templates/docker/main.tf
Normal file
@@ -0,0 +1,423 @@
|
||||
# Coder workspace template: Docker workspaces on the loca operator host.
|
||||
#
|
||||
# The provisioner is the `coder` server container itself (compose project
|
||||
# /home/loca/dev/coder), which is the only thing on this host that holds
|
||||
# /var/run/docker.sock. Workspaces created here are plain Docker containers on
|
||||
# that same daemon.
|
||||
#
|
||||
# Every construct below is derived from sources cited in README.md, not from
|
||||
# memory. Read that file before changing anything.
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
# coder/coder 2.x requires Coder Server >= v2.18.0 (provider "Version 2
|
||||
# Upgrade Guide"). This deployment runs v2.35.3, so the 2.x line applies.
|
||||
# 2.18.0 is the latest release published on registry.terraform.io.
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 2.18"
|
||||
}
|
||||
|
||||
# 4.5.0 is the latest release published on registry.terraform.io.
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
version = "~> 4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
# No `host` argument: the provisioner runs inside the `coder` container, which
|
||||
# already has /var/run/docker.sock bind-mounted, so the provider's default
|
||||
# socket is the right one. Nothing needs to be exposed over TCP.
|
||||
provider "docker" {}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parameters
|
||||
#
|
||||
# The upstream Docker example ships no parameters; the upstream Kubernetes
|
||||
# example does, and this follows that idiom. Defaults are deliberately modest:
|
||||
# the host has 10 cores and ~22 GiB available RAM, but its 15 GiB of swap is
|
||||
# already ~97% consumed, so workspaces must not be sized to lean on swap.
|
||||
#
|
||||
# There is intentionally NO disk-size parameter. The home volume uses Docker's
|
||||
# `local` driver on an overlay2/extfs root (verified with `docker info`), which
|
||||
# cannot enforce a size quota without XFS project quotas. A disk parameter here
|
||||
# would be decorative and would mislead the operator; home disk usage is
|
||||
# reported through agent metadata instead.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
data "coder_parameter" "cpu" {
|
||||
name = "cpu"
|
||||
display_name = "CPU"
|
||||
description = "CPU cores available to the workspace. This is a hard ceiling, not a reservation."
|
||||
type = "number"
|
||||
default = "2"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = true
|
||||
order = 1
|
||||
|
||||
option {
|
||||
name = "1 core"
|
||||
value = "1"
|
||||
}
|
||||
option {
|
||||
name = "2 cores (default)"
|
||||
value = "2"
|
||||
}
|
||||
option {
|
||||
name = "4 cores"
|
||||
value = "4"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "memory" {
|
||||
name = "memory"
|
||||
display_name = "Memory (GB)"
|
||||
description = "Memory ceiling in GB. Container swap is pinned to this value, so a workspace can never push the host further into swap."
|
||||
type = "number"
|
||||
default = "2"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = true
|
||||
order = 2
|
||||
|
||||
option {
|
||||
name = "2 GB (default)"
|
||||
value = "2"
|
||||
}
|
||||
option {
|
||||
name = "4 GB"
|
||||
value = "4"
|
||||
}
|
||||
option {
|
||||
name = "8 GB"
|
||||
value = "8"
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
username = data.coder_workspace_owner.me.name
|
||||
|
||||
# Matches the image used by the upstream Docker example at the tag this
|
||||
# server runs (v2.35.3). Upstream `main` has since renamed the same image to
|
||||
# codercom/example-base:ubuntu.
|
||||
workspace_image = "codercom/enterprise-base:ubuntu"
|
||||
|
||||
memory_mb = tonumber(data.coder_parameter.memory.value) * 1024
|
||||
|
||||
# Hostname the workspace must reach to find the Coder server, read from the
|
||||
# access URL (https://code.loca.zone on this deployment) instead of hardcoded.
|
||||
# regexall, not regex: regex throws a build-time error on no match, and an
|
||||
# unset access URL must degrade to "don't add a host entry", not fail.
|
||||
access_url_match = regexall("^[a-zA-Z][a-zA-Z0-9+.-]*://([^/:]+)", data.coder_workspace.me.access_url)
|
||||
access_host = length(local.access_url_match) > 0 ? local.access_url_match[0][0] : ""
|
||||
|
||||
# Pin that hostname to the Docker gateway so agent -> server traffic reaches
|
||||
# the host's nginx directly rather than depending on hairpin NAT out to the
|
||||
# public IP and back. Verified reachable: an HTTPS request to /healthz with
|
||||
# code.loca.zone resolved to 172.17.0.1 returns 200 OK with a valid
|
||||
# certificate. Skipped for a bare IPv4 access URL, which the entrypoint
|
||||
# rewrite further down already handles.
|
||||
pin_access_host = local.access_host != "" && length(regexall("^[0-9]+(\\.[0-9]+){3}$", local.access_host)) == 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
|
||||
# "blocking" is the behaviour the provider docs recommend: the workspace is
|
||||
# not advertised as ready until the script below exits.
|
||||
startup_script_behavior = "blocking"
|
||||
|
||||
# Deliberately no `set -e`. Every step here is optional, idempotent, and must
|
||||
# never fail the workspace build; failures are logged and stepped over. The
|
||||
# script ends with an explicit `exit 0` for the same reason.
|
||||
startup_script = <<-EOT
|
||||
set -u
|
||||
|
||||
log() { printf '[startup] %s\n' "$*"; }
|
||||
|
||||
# 1. Seed the home directory on first boot only. The home volume is
|
||||
# persistent, so this must not clobber an existing home.
|
||||
if [ ! -f "$HOME/.init_done" ]; then
|
||||
if [ -d /etc/skel ]; then
|
||||
cp -rT /etc/skel "$HOME" 2>/dev/null || log "skel copy skipped"
|
||||
fi
|
||||
touch "$HOME/.init_done" 2>/dev/null || log "could not mark init_done"
|
||||
fi
|
||||
|
||||
# 2. code-server, installed into the persistent home so a workspace restart
|
||||
# does not re-download it. Skipped cleanly if curl or the network is
|
||||
# unavailable; the code-server app then just reports unhealthy.
|
||||
CS_DIR="$HOME/.local/share/code-server-dist"
|
||||
CS_BIN="$CS_DIR/bin/code-server"
|
||||
if [ ! -x "$CS_BIN" ]; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
log "installing code-server into $CS_DIR"
|
||||
curl -fsSL --max-time 180 https://code-server.dev/install.sh \
|
||||
| sh -s -- --method=standalone --prefix="$CS_DIR" \
|
||||
|| log "code-server install failed; skipping"
|
||||
else
|
||||
log "curl not present; skipping code-server install"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Start code-server unless something is already serving that port.
|
||||
if [ -x "$CS_BIN" ] && ! curl -fsS --max-time 2 http://localhost:13337/healthz >/dev/null 2>&1; then
|
||||
"$CS_BIN" --auth none --bind-addr 127.0.0.1:13337 \
|
||||
>"$HOME/.code-server.log" 2>&1 &
|
||||
log "code-server started on 127.0.0.1:13337"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
EOT
|
||||
|
||||
# Lets users commit immediately. These take precedence over ~/.gitconfig.
|
||||
env = {
|
||||
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
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = data.coder_workspace_owner.me.email
|
||||
}
|
||||
|
||||
# Observability, mirroring the upstream Docker example. The host rows matter
|
||||
# on this box because the daemon, both wikis, and every workspace share it.
|
||||
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 = "2_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "3_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "4_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "5_swap_host"
|
||||
script = <<-EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f GiB", $3/1024/1024/1024, $2/1024/1024/1024) }'
|
||||
EOT
|
||||
interval = 30
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Apps
|
||||
#
|
||||
# CRITICAL: every coder_app in this template MUST set `subdomain = false`.
|
||||
#
|
||||
# This deployment has no wildcard access URL. Nested names such as
|
||||
# deep.nested.code.loca.zone do resolve, but the certbot lineage covers only
|
||||
# code.loca.zone and wiki.code.loca.zone; a wildcard certificate would require
|
||||
# a DNS-01 challenge with DNS-provider credentials that are not configured on
|
||||
# this host. Subdomain apps would therefore be served with a name-mismatched
|
||||
# certificate and fail in the browser. Path-based apps
|
||||
# (/@user/workspace.main/apps/<slug>) are the only working mode here.
|
||||
#
|
||||
# The upstream example pulls code-server in from the
|
||||
# registry.coder.com/coder/code-server/coder module. This template declares the
|
||||
# app directly instead, for two reasons: a module-owned coder_app is not under
|
||||
# our control and could ship subdomain = true, and modules add a provision-time
|
||||
# dependency on registry.coder.com.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "coder_app" "code_server" {
|
||||
agent_id = coder_agent.main.id
|
||||
slug = "code-server"
|
||||
display_name = "code-server"
|
||||
icon = "/icon/code.svg"
|
||||
url = "http://localhost:13337/?folder=/home/coder"
|
||||
share = "owner"
|
||||
order = 1
|
||||
|
||||
# See the block comment above. Do not change this to true.
|
||||
subdomain = false
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
interval = 5
|
||||
threshold = 12
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Infrastructure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "docker_image" "workspace" {
|
||||
name = local.workspace_image
|
||||
|
||||
# Every workspace on this host shares this image. Deleting one workspace must
|
||||
# not pull it out from under the others.
|
||||
keep_locally = true
|
||||
}
|
||||
|
||||
resource "docker_volume" "home" {
|
||||
# Keyed to the immutable workspace id rather than its name, so stopping,
|
||||
# starting, renaming, or rebuilding a workspace all reattach the same volume
|
||||
# instead of silently discarding /home/coder.
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
|
||||
lifecycle {
|
||||
# Never let an attribute diff (labels, driver options) destroy user data.
|
||||
ignore_changes = all
|
||||
}
|
||||
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
# Goes stale if the workspace is renamed, but useful when hunting dangling
|
||||
# volumes by hand.
|
||||
labels {
|
||||
label = "coder.workspace_name_at_creation"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = docker_image.workspace.image_id
|
||||
|
||||
# lower() because Docker rejects uppercase in container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
# Makes the shell prompt friendly: coder@my-workspace:~$
|
||||
hostname = data.coder_workspace.me.name
|
||||
|
||||
# Rewrites a loopback access URL to the Docker gateway. A no-op on this
|
||||
# deployment, where CODER_ACCESS_URL is https://code.loca.zone.
|
||||
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
|
||||
# Hard ceilings from the parameters above. memory_swap is set equal to memory,
|
||||
# which disables container swap entirely: the host's 15 GiB swap is already
|
||||
# ~97% used, so a workspace must not be allowed to add to it.
|
||||
cpus = tostring(data.coder_parameter.cpu.value)
|
||||
memory = local.memory_mb
|
||||
memory_swap = local.memory_mb
|
||||
|
||||
# SECURITY BOUNDARY: /var/run/docker.sock is deliberately NOT mounted here.
|
||||
# The socket stays with the `coder` server container, which needs it to
|
||||
# provision workspaces. Handing it to a workspace would give anyone inside
|
||||
# that workspace root-equivalent control of the whole host, including the
|
||||
# nginx/certbot stack, the Postgres container behind Coder, and every other
|
||||
# workspace. Docker-in-workspace, if it is ever wanted, must be done with a
|
||||
# sysbox/rootless runtime, not by exposing this socket.
|
||||
volumes {
|
||||
container_path = "/home/coder"
|
||||
volume_name = docker_volume.home.name
|
||||
read_only = false
|
||||
}
|
||||
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
|
||||
# See local.pin_access_host: keeps agent -> server traffic on the host
|
||||
# gateway instead of routing out to the public IP and back.
|
||||
dynamic "host" {
|
||||
for_each = local.pin_access_host ? [local.access_host] : []
|
||||
iterator = h
|
||||
content {
|
||||
host = h.value
|
||||
ip = "host-gateway"
|
||||
}
|
||||
}
|
||||
|
||||
labels {
|
||||
label = "coder.owner"
|
||||
value = data.coder_workspace_owner.me.name
|
||||
}
|
||||
labels {
|
||||
label = "coder.owner_id"
|
||||
value = data.coder_workspace_owner.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_id"
|
||||
value = data.coder_workspace.me.id
|
||||
}
|
||||
labels {
|
||||
label = "coder.workspace_name"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
}
|
||||
|
||||
# Surfaces the limits actually applied, so the dashboard does not require an
|
||||
# operator to shell in and run `docker inspect` to see them.
|
||||
resource "coder_metadata" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = docker_container.workspace[0].id
|
||||
|
||||
item {
|
||||
key = "image"
|
||||
value = local.workspace_image
|
||||
}
|
||||
item {
|
||||
key = "cpu limit"
|
||||
value = "${data.coder_parameter.cpu.value} core(s)"
|
||||
}
|
||||
item {
|
||||
key = "memory limit"
|
||||
value = "${data.coder_parameter.memory.value} GB (swap disabled)"
|
||||
}
|
||||
item {
|
||||
key = "home volume"
|
||||
value = docker_volume.home.name
|
||||
}
|
||||
item {
|
||||
key = "app routing"
|
||||
value = "path-based (`subdomain = false`); no wildcard access URL on this deployment"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user