Skip to content

Commit 91c337a

Browse files
authored
feat: use hashicorp/cloud-init provider in AWS devcontainer template (coder#15050)
This PR makes templates uses the [hashicorp/cloud-init](https://registry.terraform.io/providers/hashicorp/cloudinit/latest/docs) provider instead of hardcoding a cloud-init config.
1 parent 9308331 commit 91c337a

File tree

3 files changed

+103
-88
lines changed

3 files changed

+103
-88
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#cloud-config
2+
cloud_final_modules:
3+
- [scripts-user, always]
4+
hostname: ${hostname}
5+
users:
6+
- name: ${linux_user}
7+
sudo: ALL=(ALL) NOPASSWD:ALL
8+
shell: /bin/bash
9+
ssh_authorized_keys:
10+
- "${ssh_pubkey}"
11+
# Automatically grow the partition
12+
growpart:
13+
mode: auto
14+
devices: ['/']
15+
ignore_growroot_disabled: false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/bash
2+
# Install Docker
3+
if ! command -v docker &> /dev/null
4+
then
5+
echo "Docker not found, installing..."
6+
curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh 2>&1 >/dev/null
7+
usermod -aG docker ${linux_user}
8+
newgrp docker
9+
else
10+
echo "Docker is already installed."
11+
fi
12+
13+
# Set up Docker credentials
14+
mkdir -p "/home/${linux_user}/.docker"
15+
16+
if [ -n "${docker_config_json_base64}" ]; then
17+
# Write the Docker config JSON to disk if it is provided.
18+
printf "%s" "${docker_config_json_base64}" | base64 -d | tee "/home/${linux_user}/.docker/config.json"
19+
else
20+
# Assume that we're going to use the instance IAM role to pull from the cache repo if we need to.
21+
# Set up the ecr credential helper.
22+
apt-get update -y && apt-get install -y amazon-ecr-credential-helper
23+
mkdir -p .docker
24+
printf '{"credsStore": "ecr-login"}' | tee "/home/${linux_user}/.docker/config.json"
25+
fi
26+
chown -R ${linux_user}:${linux_user} "/home/${linux_user}/.docker"
27+
28+
# Start envbuilder
29+
sudo -u coder docker run \
30+
--rm \
31+
--net=host \
32+
-h ${hostname} \
33+
-v /home/${linux_user}/envbuilder:/workspaces \
34+
%{ for key, value in environment ~}
35+
-e ${key}="${value}" \
36+
%{ endfor ~}
37+
${builder_image}

examples/templates/aws-devcontainer/main.tf

+51-88
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ terraform {
66
aws = {
77
source = "hashicorp/aws"
88
}
9+
cloudinit = {
10+
source = "hashicorp/cloudinit"
11+
}
912
envbuilder = {
1013
source = "coder/envbuilder"
1114
}
@@ -153,13 +156,16 @@ data "aws_iam_instance_profile" "vm_instance_profile" {
153156
locals {
154157
# TODO: provide a way to pick the availability zone.
155158
aws_availability_zone = "${module.aws_region.value}a"
156-
linux_user = "coder"
157-
# Name the container after the workspace and owner.
158-
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
159+
160+
hostname = lower(data.coder_workspace.me.name)
161+
linux_user = "coder"
162+
159163
# The devcontainer builder image is the image that will build the devcontainer.
160164
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
165+
161166
# We may need to authenticate with a registry. If so, the user will provide a path to a docker config.json.
162167
docker_config_json_base64 = try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, "")
168+
163169
# The envbuilder provider requires a key-value map of environment variables. Build this here.
164170
envbuilder_env = {
165171
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
@@ -172,7 +178,7 @@ locals {
172178
# The agent init script is required for the agent to start up. We base64 encode it here
173179
# to avoid quoting issues.
174180
"ENVBUILDER_INIT_SCRIPT" : "echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh",
175-
"ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""),
181+
"ENVBUILDER_DOCKER_CONFIG_BASE64" : local.docker_config_json_base64,
176182
# The fallback image is the image that will run if the devcontainer fails to build.
177183
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
178184
# The following are used to push the image to the cache repo, if defined.
@@ -181,87 +187,6 @@ locals {
181187
# You can add other required environment variables here.
182188
# See: https://github.com/coder/envbuilder/?tab=readme-ov-file#environment-variables
183189
}
184-
# If we have a cached image, use the cached image's environment variables. Otherwise, just use
185-
# the environment variables we've defined above.
186-
docker_env_input = try(envbuilder_cached_image.cached.0.env_map, local.envbuilder_env)
187-
# Convert the above to the list of arguments for the Docker run command.
188-
# The startup script will write this to a file, which the Docker run command will reference.
189-
docker_env_list_base64 = base64encode(join("\n", [for k, v in local.docker_env_input : "${k}=${v}"]))
190-
# Builder image will either be the builder image parameter, or the cached image, if cache is provided.
191-
builder_image = try(envbuilder_cached_image.cached[0].image, data.coder_parameter.devcontainer_builder.value)
192-
# User data to start the workspace.
193-
user_data = <<-EOT
194-
Content-Type: multipart/mixed; boundary="//"
195-
MIME-Version: 1.0
196-
197-
--//
198-
Content-Type: text/cloud-config; charset="us-ascii"
199-
MIME-Version: 1.0
200-
Content-Transfer-Encoding: 7bit
201-
Content-Disposition: attachment; filename="cloud-config.txt"
202-
203-
#cloud-config
204-
cloud_final_modules:
205-
- [scripts-user, always]
206-
hostname: ${lower(data.coder_workspace.me.name)}
207-
users:
208-
- name: ${local.linux_user}
209-
sudo: ALL=(ALL) NOPASSWD:ALL
210-
shell: /bin/bash
211-
ssh_authorized_keys:
212-
- "${data.coder_parameter.ssh_pubkey.value}"
213-
# Automatically grow the partition
214-
growpart:
215-
mode: auto
216-
devices: ['/']
217-
ignore_growroot_disabled: false
218-
219-
--//
220-
Content-Type: text/x-shellscript; charset="us-ascii"
221-
MIME-Version: 1.0
222-
Content-Transfer-Encoding: 7bit
223-
Content-Disposition: attachment; filename="userdata.txt"
224-
225-
#!/bin/bash
226-
# Install Docker
227-
if ! command -v docker &> /dev/null
228-
then
229-
echo "Docker not found, installing..."
230-
curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh 2>&1 >/dev/null
231-
usermod -aG docker ${local.linux_user}
232-
newgrp docker
233-
else
234-
echo "Docker is already installed."
235-
fi
236-
237-
# Set up Docker credentials
238-
mkdir -p "/home/${local.linux_user}/.docker"
239-
if [ -n "${local.docker_config_json_base64}" ]; then
240-
# Write the Docker config JSON to disk if it is provided.
241-
printf "%s" "${local.docker_config_json_base64}" | base64 -d | tee "/home/${local.linux_user}/.docker/config.json"
242-
else
243-
# Assume that we're going to use the instance IAM role to pull from the cache repo if we need to.
244-
# Set up the ecr credential helper.
245-
apt-get update -y && apt-get install -y amazon-ecr-credential-helper
246-
mkdir -p .docker
247-
printf '{"credsStore": "ecr-login"}' | tee "/home/${local.linux_user}/.docker/config.json"
248-
fi
249-
chown -R ${local.linux_user}:${local.linux_user} "/home/${local.linux_user}/.docker"
250-
251-
# Write the container env to disk.
252-
printf "%s" "${local.docker_env_list_base64}" | base64 -d | tee "/home/${local.linux_user}/env.txt"
253-
254-
# Start envbuilder
255-
sudo -u coder docker run \
256-
--rm \
257-
--net=host \
258-
-h ${lower(data.coder_workspace.me.name)} \
259-
-v /home/${local.linux_user}/envbuilder:/workspaces \
260-
-v /var/run/docker.sock:/var/run/docker.sock \
261-
--env-file /home/${local.linux_user}/env.txt \
262-
${local.builder_image}
263-
--//--
264-
EOT
265190
}
266191

267192
# Check for the presence of a prebuilt image in the cache repo
@@ -274,9 +199,47 @@ resource "envbuilder_cached_image" "cached" {
274199
extra_env = local.envbuilder_env
275200
}
276201

202+
data "cloudinit_config" "user_data" {
203+
gzip = false
204+
base64_encode = false
205+
206+
boundary = "//"
207+
208+
part {
209+
filename = "cloud-config.yaml"
210+
content_type = "text/cloud-config"
211+
212+
content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", {
213+
hostname = local.hostname
214+
linux_user = local.linux_user
215+
216+
ssh_pubkey = data.coder_parameter.ssh_pubkey.value
217+
})
218+
}
219+
220+
part {
221+
filename = "userdata.sh"
222+
content_type = "text/x-shellscript"
223+
224+
content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", {
225+
hostname = local.hostname
226+
linux_user = local.linux_user
227+
228+
# If we have a cached image, use the cached image's environment variables.
229+
# Otherwise, just use the environment variables we've defined in locals.
230+
environment = try(envbuilder_cached_image.cached[0].env_map, local.envbuilder_env)
231+
232+
# Builder image will either be the builder image parameter, or the cached image, if cache is provided.
233+
builder_image = try(envbuilder_cached_image.cached[0].image, data.coder_parameter.devcontainer_builder.value)
234+
235+
docker_config_json_base64 = local.docker_config_json_base64
236+
})
237+
}
238+
}
239+
277240
# This is useful for debugging the startup script. Left here for reference.
278241
# resource local_file "startup_script" {
279-
# content = local.user_data
242+
# content = data.cloudinit_config.user_data.rendered
280243
# filename = "${path.module}/user_data.txt"
281244
# }
282245

@@ -289,9 +252,9 @@ resource "aws_instance" "vm" {
289252
volume_size = data.coder_parameter.root_volume_size_gb.value
290253
}
291254

292-
user_data = local.user_data
255+
user_data = data.cloudinit_config.user_data.rendered
293256
tags = {
294-
Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
257+
Name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
295258
# Required if you are using our example policy, see template README
296259
Coder_Provisioned = "true"
297260
}

0 commit comments

Comments
 (0)