@@ -6,16 +6,49 @@ terraform {
6
6
google = {
7
7
source = " hashicorp/google"
8
8
}
9
+ envbuilder = {
10
+ source = " coder/envbuilder"
11
+ }
9
12
}
10
13
}
11
14
12
15
provider "coder" {
13
16
}
14
17
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
+
15
29
variable "project_id" {
16
30
description = " Which Google Compute Project should your workspace live in?"
17
31
}
18
32
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
+
19
52
data "coder_parameter" "zone" {
20
53
name = " zone"
21
54
display_name = " Zone"
@@ -24,6 +57,7 @@ data "coder_parameter" "zone" {
24
57
icon = " /emojis/1f30e.png"
25
58
default = " us-central1-a"
26
59
mutable = false
60
+ order = 1
27
61
option {
28
62
name = " North America (Northeast)"
29
63
value = " northamerica-northeast1-a"
@@ -51,25 +85,48 @@ data "coder_parameter" "zone" {
51
85
}
52
86
}
53
87
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
+ }
60
108
}
61
109
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
63
117
}
64
- data "coder_workspace_owner" "me" {}
65
118
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
73
130
}
74
131
75
132
data "coder_parameter" "repo_url" {
@@ -80,46 +137,122 @@ data "coder_parameter" "repo_url" {
80
137
mutable = true
81
138
}
82
139
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
+ }
90
144
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 } " ,
104
174
}
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
111
232
}
112
233
}
113
234
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
118
244
}
119
245
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.
120
253
resource "google_compute_instance" "vm" {
121
254
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
123
256
# data.coder_workspace_owner.me.name == "default" is a workaround to suppress error in the terraform plan phase while creating a new workspace.
124
257
desired_status = (data. coder_workspace_owner . me . name == " default" || data. coder_workspace . me . start_count == 1 ) ? " RUNNING" : " TERMINATED"
125
258
@@ -144,45 +277,50 @@ resource "google_compute_instance" "vm" {
144
277
# The startup script runs as root with no $HOME environment set up, so instead of directly
145
278
# running the agent init script, create a user (with a homedir, default shell and sudo
146
279
# 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
+ }
150
283
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
156
292
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"
178
313
}
179
314
}
180
315
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
184
321
}
185
322
323
+ # Create metadata for the workspace and home disk.
186
324
resource "coder_metadata" "workspace_info" {
187
325
count = data. coder_workspace . me . start_count
188
326
resource_id = google_compute_instance. vm . id
0 commit comments