Skip to content

Commit 85c86b8

Browse files
committed
feat(examples/templates/gcp-devcontainer): add envbuilder provider
This PR modifies the gcp-devcontainer example template to include support for devcontainer caching using the envbuilder provider.
1 parent d52bc91 commit 85c86b8

File tree

1 file changed

+216
-78
lines changed
  • examples/templates/gcp-devcontainer

1 file changed

+216
-78
lines changed

examples/templates/gcp-devcontainer/main.tf

Lines changed: 216 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,49 @@ terraform {
66
google = {
77
source = "hashicorp/google"
88
}
9+
envbuilder = {
10+
source = "coder/envbuilder"
11+
}
912
}
1013
}
1114

1215
provider "coder" {
1316
}
1417

18+
provider "google" {
19+
zone = data.coder_parameter.zone.value
20+
project = var.project_id
21+
}
22+
23+
data "google_compute_default_service_account" "default" {}
24+
25+
data "coder_workspace" "me" {
26+
}
27+
data "coder_workspace_owner" "me" {}
28+
1529
variable "project_id" {
1630
description = "Which Google Compute Project should your workspace live in?"
1731
}
1832

33+
variable "cache_repo" {
34+
default = ""
35+
description = "(Optional) Use a container registry as a cache to speed up builds."
36+
type = string
37+
}
38+
39+
variable "insecure_cache_repo" {
40+
default = false
41+
description = "Enable this option if your cache registry does not serve HTTPS."
42+
type = bool
43+
}
44+
45+
variable "cache_repo_docker_config_path" {
46+
default = ""
47+
description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required."
48+
sensitive = true
49+
type = string
50+
}
51+
1952
data "coder_parameter" "zone" {
2053
name = "zone"
2154
display_name = "Zone"
@@ -24,6 +57,7 @@ data "coder_parameter" "zone" {
2457
icon = "/emojis/1f30e.png"
2558
default = "us-central1-a"
2659
mutable = false
60+
order = 1
2761
option {
2862
name = "North America (Northeast)"
2963
value = "northamerica-northeast1-a"
@@ -51,25 +85,48 @@ data "coder_parameter" "zone" {
5185
}
5286
}
5387

54-
provider "google" {
55-
zone = data.coder_parameter.zone.value
56-
project = var.project_id
57-
}
58-
59-
data "google_compute_default_service_account" "default" {
88+
data "coder_parameter" "instance_type" {
89+
name = "instance_type"
90+
display_name = "Instance Type"
91+
description = "Select an instance type for your workspace."
92+
type = "string"
93+
mutable = false
94+
order = 2
95+
default = "e2-micro"
96+
option {
97+
name = "e2-micro (2C, 1G)"
98+
value = "e2-micro"
99+
}
100+
option {
101+
name = "e2-small (2C, 2G)"
102+
value = "e2-small"
103+
}
104+
option {
105+
name = "e2-medium (2C, 2G)"
106+
value = "e2-medium"
107+
}
60108
}
61109

62-
data "coder_workspace" "me" {
110+
data "coder_parameter" "fallback_image" {
111+
default = "codercom/enterprise-base:ubuntu"
112+
description = "This image runs if the devcontainer fails to build."
113+
display_name = "Fallback Image"
114+
mutable = true
115+
name = "fallback_image"
116+
order = 3
63117
}
64-
data "coder_workspace_owner" "me" {}
65118

66-
resource "google_compute_disk" "root" {
67-
name = "coder-${data.coder_workspace.me.id}-root"
68-
type = "pd-ssd"
69-
image = "debian-cloud/debian-12"
70-
lifecycle {
71-
ignore_changes = [name, image]
72-
}
119+
data "coder_parameter" "devcontainer_builder" {
120+
description = <<-EOF
121+
Image that will build the devcontainer.
122+
We highly recommend using a specific release as the `:latest` tag will change.
123+
Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder
124+
EOF
125+
display_name = "Devcontainer Builder"
126+
mutable = true
127+
name = "devcontainer_builder"
128+
default = "ghcr.io/coder/envbuilder:latest"
129+
order = 4
73130
}
74131

75132
data "coder_parameter" "repo_url" {
@@ -80,46 +137,122 @@ data "coder_parameter" "repo_url" {
80137
mutable = true
81138
}
82139

83-
resource "coder_agent" "dev" {
84-
count = data.coder_workspace.me.start_count
85-
arch = "amd64"
86-
auth = "token"
87-
os = "linux"
88-
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}"
89-
connection_timeout = 0
140+
data "local_sensitive_file" "cache_repo_dockerconfigjson" {
141+
count = var.cache_repo_docker_config_path == "" ? 0 : 1
142+
filename = var.cache_repo_docker_config_path
143+
}
90144

91-
metadata {
92-
key = "cpu"
93-
display_name = "CPU Usage"
94-
interval = 5
95-
timeout = 5
96-
script = "coder stat cpu"
97-
}
98-
metadata {
99-
key = "memory"
100-
display_name = "Memory Usage"
101-
interval = 5
102-
timeout = 5
103-
script = "coder stat mem"
145+
146+
locals {
147+
# Ensure Coder username is a valid Linux username
148+
linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32))
149+
# Name the container after the workspace and owner.
150+
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
151+
# The devcontainer builder image is the image that will build the devcontainer.
152+
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
153+
# We may need to authenticate with a registry. If so, the user will provide a path to a docker config.json.
154+
docker_config_json_base64 = try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, "")
155+
# The envbuilder provider requires a key-value map of environment variables. Build this here.
156+
envbuilder_env = {
157+
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
158+
# if the cache repo is enabled.
159+
"ENVBUILDER_GIT_URL" : data.coder_parameter.repo_url.value,
160+
# The agent token is required for the agent to connect to the Coder platform.
161+
"CODER_AGENT_TOKEN" : try(coder_agent.dev.0.token, ""),
162+
# The agent URL is required for the agent to connect to the Coder platform.
163+
"CODER_AGENT_URL" : data.coder_workspace.me.access_url,
164+
# The agent init script is required for the agent to start up. We base64 encode it here
165+
# to avoid quoting issues.
166+
"ENVBUILDER_INIT_SCRIPT" : "echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh",
167+
"ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""),
168+
# The fallback image is the image that will run if the devcontainer fails to build.
169+
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
170+
# The following are used to push the image to the cache repo, if defined.
171+
"ENVBUILDER_CACHE_REPO" : var.cache_repo,
172+
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true",
173+
"ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}",
104174
}
105-
metadata {
106-
key = "disk"
107-
display_name = "Disk Usage"
108-
interval = 5
109-
timeout = 5
110-
script = "coder stat disk"
175+
# If we have a cached image, use the cached image's environment variables. Otherwise, just use
176+
# the environment variables we've defined above.
177+
docker_env_input = try(envbuilder_cached_image.cached.0.env_map, local.envbuilder_env)
178+
# Convert the above to the list of arguments for the Docker run command. This is going to end
179+
# up in our startup script metadata. These are all terminated by backslashes.
180+
docker_env_arg_list = [for k, v in local.docker_env_input : " -e \"${k}=${v}\" \\"]
181+
182+
# The GCP VM needs a startup script to set up the environment and start the container. Defining this here.
183+
# NOTE: make sure to test changes by uncommenting the local_file resource at the bottom of this file
184+
# and running `terraform apply` to see the generated script. You should also run shellcheck on the script
185+
# to ensure it is valid.
186+
startup_script = <<-META
187+
#!/usr/bin/env sh
188+
set -eux
189+
190+
# If user does not exist, create it and set up passwordless sudo
191+
if ! id -u "${local.linux_user}" >/dev/null 2>&1; then
192+
useradd -m -s /bin/bash "${local.linux_user}"
193+
echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user
194+
fi
195+
196+
# Check for Docker, install if not present
197+
if ! command -v docker >/dev/null 2>&1; then
198+
echo "Docker not found, installing..."
199+
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh >/dev/null 2>&1
200+
sudo usermod -aG docker ${local.linux_user}
201+
newgrp docker
202+
else
203+
echo "Docker is already installed."
204+
fi
205+
206+
# Write the Docker config JSON to disk if it is provided.
207+
if [ -n "${local.docker_config_json_base64}" ]; then
208+
mkdir -p "/home/${local.linux_user}/.docker"
209+
printf "%s" "${local.docker_config_json_base64}" | base64 -d | tee "/home/${local.linux_user}/.docker/config.json"
210+
chown -R ${local.linux_user}:${local.linux_user} "/home/${local.linux_user}/.docker"
211+
fi
212+
213+
# Start envbuilder.
214+
docker run \
215+
--rm \
216+
--net=host \
217+
-h ${lower(data.coder_workspace.me.name)} \
218+
-v /home/${local.linux_user}/envbuilder:/workspaces \
219+
-v /var/run/docker.sock:/var/run/docker.sock \
220+
${join("\n", local.docker_env_arg_list)}
221+
${data.coder_parameter.devcontainer_builder.value}
222+
META
223+
}
224+
225+
# Create a persistent to store the workspace data.
226+
resource "google_compute_disk" "root" {
227+
name = "coder-${data.coder_workspace.me.id}-root"
228+
type = "pd-ssd"
229+
image = "debian-cloud/debian-12"
230+
lifecycle {
231+
ignore_changes = all
111232
}
112233
}
113234

114-
module "code-server" {
115-
count = data.coder_workspace.me.start_count
116-
source = "https://registry.coder.com/modules/code-server"
117-
agent_id = coder_agent.dev[0].id
235+
# Check for the presence of a prebuilt image in the cache repo
236+
# that we can use instead.
237+
resource "envbuilder_cached_image" "cached" {
238+
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count
239+
builder_image = local.devcontainer_builder_image
240+
git_url = data.coder_parameter.repo_url.value
241+
cache_repo = var.cache_repo
242+
extra_env = local.envbuilder_env
243+
insecure = var.insecure_cache_repo
118244
}
119245

246+
# This is useful for debugging the startup script. Left here for reference.
247+
# resource local_file "startup_script" {
248+
# content = local.startup_script
249+
# filename = "${path.module}/startup_script.sh"
250+
# }
251+
252+
# Create a VM where the workspace will run.
120253
resource "google_compute_instance" "vm" {
121254
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root"
122-
machine_type = "e2-medium"
255+
machine_type = data.coder_parameter.instance_type.value
123256
# data.coder_workspace_owner.me.name == "default" is a workaround to suppress error in the terraform plan phase while creating a new workspace.
124257
desired_status = (data.coder_workspace_owner.me.name == "default" || data.coder_workspace.me.start_count == 1) ? "RUNNING" : "TERMINATED"
125258

@@ -144,45 +277,50 @@ resource "google_compute_instance" "vm" {
144277
# The startup script runs as root with no $HOME environment set up, so instead of directly
145278
# running the agent init script, create a user (with a homedir, default shell and sudo
146279
# permissions) and execute the init script as that user.
147-
startup-script = <<-META
148-
#!/usr/bin/env sh
149-
set -eux
280+
startup-script = local.startup_script
281+
}
282+
}
150283

151-
# If user does not exist, create it and set up passwordless sudo
152-
if ! id -u "${local.linux_user}" >/dev/null 2>&1; then
153-
useradd -m -s /bin/bash "${local.linux_user}"
154-
echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user
155-
fi
284+
# Create a Coder agent to manage the workspace.
285+
resource "coder_agent" "dev" {
286+
count = data.coder_workspace.me.start_count
287+
arch = "amd64"
288+
auth = "token"
289+
os = "linux"
290+
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}"
291+
connection_timeout = 0
156292

157-
# Check for Docker, install if not present
158-
if ! command -v docker &> /dev/null
159-
then
160-
echo "Docker not found, installing..."
161-
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh 2>&1 >/dev/null
162-
sudo usermod -aG docker ${local.linux_user}
163-
newgrp docker
164-
else
165-
echo "Docker is already installed."
166-
fi
167-
# Start envbuilder
168-
docker run --rm \
169-
-h ${lower(data.coder_workspace.me.name)} \
170-
-v /home/${local.linux_user}/envbuilder:/workspaces \
171-
-e CODER_AGENT_TOKEN="${try(coder_agent.dev[0].token, "")}" \
172-
-e CODER_AGENT_URL="${data.coder_workspace.me.access_url}" \
173-
-e GIT_URL="${data.coder_parameter.repo_url.value}" \
174-
-e INIT_SCRIPT="echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh" \
175-
-e FALLBACK_IMAGE="codercom/enterprise-base:ubuntu" \
176-
ghcr.io/coder/envbuilder
177-
META
293+
metadata {
294+
key = "cpu"
295+
display_name = "CPU Usage"
296+
interval = 5
297+
timeout = 5
298+
script = "coder stat cpu"
299+
}
300+
metadata {
301+
key = "memory"
302+
display_name = "Memory Usage"
303+
interval = 5
304+
timeout = 5
305+
script = "coder stat mem"
306+
}
307+
metadata {
308+
key = "disk"
309+
display_name = "Disk Usage"
310+
interval = 5
311+
timeout = 5
312+
script = "coder stat disk"
178313
}
179314
}
180315

181-
locals {
182-
# Ensure Coder username is a valid Linux username
183-
linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32))
316+
# Install code-server via Terraform module.
317+
module "code-server" {
318+
count = data.coder_workspace.me.start_count
319+
source = "https://registry.coder.com/modules/code-server"
320+
agent_id = coder_agent.dev[0].id
184321
}
185322

323+
# Create metadata for the workspace and home disk.
186324
resource "coder_metadata" "workspace_info" {
187325
count = data.coder_workspace.me.start_count
188326
resource_id = google_compute_instance.vm.id

0 commit comments

Comments
 (0)