Skip to content

chore(examples/templates/aws-devcontainer): update to use envbuilder provider` #14831

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
chore(examples/templates/aws-devcontainer): update to use envbuilder …
…provider
  • Loading branch information
johnstcn committed Sep 26, 2024
commit 8c8a109ffae5df1e155bf096b313d6b2c16bd96e
15 changes: 15 additions & 0 deletions examples/templates/aws-devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ Coder uses `aws_ec2_instance_state` to start and stop the VM. This example templ
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.

## Caching

To speed up your builds, you can use a container registry as a cache.
When creating the template, set the parameter `cache_repo` to a valid Docker repository in the form `host.tld/path/to/repo`.

See the [Envbuilder Terraform Provider Examples](https://github.com/coder/terraform-provider-envbuilder/blob/main/examples/resources/envbuilder_cached_image/envbuilder_cached_image_resource.tf/) for a more complete example of how the provider works.

> [!NOTE] We recommend using a registry cache with authentication enabled.
> To allow Envbuilder to authenticate with a registry cache hosted on ECR, specify an IAM instance
> profile that has read and write access to the given registry. For more information, see the
> [AWS documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html).
>
> Alternatively, you can specify the variable `cache_repo_docker_config_path`
> with the path to a Docker config `.json` on disk containing valid credentials for the registry.

## code-server

`code-server` is installed via the [`code-server`](https://registry.coder.com/modules/code-server) registry module. For a list of all modules and templates pplease check [Coder Registry](https://registry.coder.com).
272 changes: 218 additions & 54 deletions examples/templates/aws-devcontainer/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ terraform {
aws = {
source = "hashicorp/aws"
}
envbuilder = {
source = "coder/envbuilder"
}
}
}

Expand All @@ -14,6 +17,45 @@ module "aws_region" {
default = "us-east-1"
}

provider "aws" {
region = module.aws_region.value
}

variable "cache_repo" {
default = ""
description = "(Optional) Use a container registry as a cache to speed up builds. Example: host.tld/path/to/repo."
type = string
}

variable "cache_repo_docker_config_path" {
default = ""
description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required. This will depend on your Coder setup. Example: `/home/coder/.docker/config.json`."
sensitive = true
type = string
}

variable "iam_instance_profile" {
default = ""
description = "(Optional) Name of an IAM instance profile to assign to the instance."
type = string
}

data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}

data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}

data "coder_parameter" "instance_type" {
name = "instance_type"
display_name = "Instance type"
Expand Down Expand Up @@ -46,25 +88,26 @@ data "coder_parameter" "instance_type" {
}
}

provider "aws" {
region = module.aws_region.value
}

data "coder_workspace" "me" {
data "coder_parameter" "fallback_image" {
default = "codercom/enterprise-base:ubuntu"
description = "This image runs if the devcontainer fails to build."
display_name = "Fallback Image"
mutable = true
name = "fallback_image"
order = 3
}
data "coder_workspace_owner" "me" {}

data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
data "coder_parameter" "devcontainer_builder" {
description = <<-EOF
Image that will build the devcontainer.
Find the latest version of Envbuilder here: https://ghcr.io/coder/envbuilder
Be aware that using the `:latest` tag may expose you to breaking changes.
EOF
display_name = "Devcontainer Builder"
mutable = true
name = "devcontainer_builder"
default = "ghcr.io/coder/envbuilder:latest"
order = 4
}

data "coder_parameter" "repo_url" {
Expand All @@ -75,39 +118,66 @@ data "coder_parameter" "repo_url" {
mutable = true
}

resource "coder_agent" "dev" {
count = data.coder_workspace.me.start_count
arch = "amd64"
auth = "token"
os = "linux"
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}"
connection_timeout = 0
data "coder_parameter" "ssh_pubkey" {
name = "ssh_pubkey"
display_name = "SSH Public Key"
default = ""
description = "(Optional) Add an SSH public key to the `coder` user's authorized_keys. Useful for troubleshooting. You may need to add a security group to the instance."
mutable = false
}

metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
data "local_sensitive_file" "cache_repo_dockerconfigjson" {
count = var.cache_repo_docker_config_path == "" ? 0 : 1
filename = var.cache_repo_docker_config_path
}

module "code-server" {
count = data.coder_workspace.me.start_count
source = "https://registry.coder.com/modules/code-server"
agent_id = coder_agent.dev[0].id
data "aws_iam_instance_profile" "vm_instance_profile" {
count = var.iam_instance_profile == "" ? 0 : 1
name = var.iam_instance_profile
}

# Be careful when modifying the below locals!
locals {
linux_user = "coder"
user_data = <<-EOT
# TODO: provide a way to pick the availability zone.
aws_availability_zone = "${module.aws_region.value}a"
linux_user = "coder"
# Name the container after the workspace and owner.
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
# The devcontainer builder image is the image that will build the devcontainer.
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value
# We may need to authenticate with a registry. If so, the user will provide a path to a docker config.json.
docker_config_json_base64 = try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, "")
# The envbuilder provider requires a key-value map of environment variables. Build this here.
envbuilder_env = {
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider
# if the cache repo is enabled.
"ENVBUILDER_GIT_URL" : data.coder_parameter.repo_url.value,
# The agent token is required for the agent to connect to the Coder platform.
"CODER_AGENT_TOKEN" : try(coder_agent.dev.0.token, ""),
# The agent URL is required for the agent to connect to the Coder platform.
"CODER_AGENT_URL" : data.coder_workspace.me.access_url,
# The agent init script is required for the agent to start up. We base64 encode it here
# to avoid quoting issues.
"ENVBUILDER_INIT_SCRIPT" : "echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh",
"ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""),
# The fallback image is the image that will run if the devcontainer fails to build.
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
# The following are used to push the image to the cache repo, if defined.
"ENVBUILDER_CACHE_REPO" : var.cache_repo,
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true",
# You can add other required environment variables here.
# See: https://github.com/coder/envbuilder/?tab=readme-ov-file#environment-variables
}
# If we have a cached image, use the cached image's environment variables. Otherwise, just use
# the environment variables we've defined above.
docker_env_input = try(envbuilder_cached_image.cached.0.env_map, local.envbuilder_env)
# Convert the above to the list of arguments for the Docker run command.
# The startup script will write this to a file, which the Docker run command will reference.
docker_env_list_base64 = base64encode(join("\n", [for k, v in local.docker_env_input : "${k}=${v}"]))
# Builder image will either be the builder image parameter, or the cached image, if cache is provided.
builder_image = try(envbuilder_cached_image.cached[0].image, data.coder_parameter.devcontainer_builder.value)
# User data to start the workspace.
user_data = <<-EOT
Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0

Expand All @@ -125,6 +195,8 @@ locals {
- name: ${local.linux_user}
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
ssh_authorized_keys:
- "${data.coder_parameter.ssh_pubkey.value}"

--//
Content-Type: text/x-shellscript; charset="us-ascii"
Expand All @@ -144,24 +216,57 @@ locals {
echo "Docker is already installed."
fi

# Set up Docker credentials
mkdir -p "/home/${local.linux_user}/.docker"
if [ -n "${local.docker_config_json_base64}" ]; then
# Write the Docker config JSON to disk if it is provided.
printf "%s" "${local.docker_config_json_base64}" | base64 -d | tee "/home/${local.linux_user}/.docker/config.json"
else
# Assume that we're going to use the instance IAM role to pull from the cache repo if we need to.
# Set up the ecr credential helper.
apt-get update -y && apt-get install -y amazon-ecr-credential-helper
mkdir -p .docker
printf '{"credsStore": "ecr-login"}' | tee "/home/${local.linux_user}/.docker/config.json"
fi
chown -R ${local.linux_user}:${local.linux_user} "/home/${local.linux_user}/.docker"

# Write the container env to disk.
printf "%s" "${local.docker_env_list_base64}" | base64 -d | tee "/home/${local.linux_user}/env.txt"

# Start envbuilder
docker run --rm \
sudo -u coder docker run \
--rm \
--net=host \
-h ${lower(data.coder_workspace.me.name)} \
-v /home/${local.linux_user}/envbuilder:/workspaces \
-e CODER_AGENT_TOKEN="${try(coder_agent.dev[0].token, "")}" \
-e CODER_AGENT_URL="${data.coder_workspace.me.access_url}" \
-e GIT_URL="${data.coder_parameter.repo_url.value}" \
-e INIT_SCRIPT="echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh" \
-e FALLBACK_IMAGE="codercom/enterprise-base:ubuntu" \
ghcr.io/coder/envbuilder
-v /var/run/docker.sock:/var/run/docker.sock \
--env-file /home/${local.linux_user}/env.txt \
${local.builder_image}
--//--
EOT
}

# Check for the presence of a prebuilt image in the cache repo
# that we can use instead.
resource "envbuilder_cached_image" "cached" {
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count
builder_image = local.devcontainer_builder_image
git_url = data.coder_parameter.repo_url.value
cache_repo = var.cache_repo
extra_env = local.envbuilder_env
}

# This is useful for debugging the startup script. Left here for reference.
# resource local_file "startup_script" {
# content = local.user_data
# filename = "${path.module}/user_data.txt"
# }

resource "aws_instance" "vm" {
ami = data.aws_ami.ubuntu.id
availability_zone = "${module.aws_region.value}a"
instance_type = data.coder_parameter.instance_type.value
ami = data.aws_ami.ubuntu.id
availability_zone = local.aws_availability_zone
instance_type = data.coder_parameter.instance_type.value
iam_instance_profile = try(data.aws_iam_instance_profile.vm_instance_profile[0].name, null)
root_block_device {
volume_size = 30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also make the root disk size a parameter?

Copy link
Member Author

@johnstcn johnstcn Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep this a separate change.
Edit: #14833

}
Expand All @@ -181,3 +286,62 @@ resource "aws_ec2_instance_state" "vm" {
instance_id = aws_instance.vm.id
state = data.coder_workspace.me.transition == "start" ? "running" : "stopped"
}

resource "coder_agent" "dev" {
count = data.coder_workspace.me.start_count
arch = "amd64"
auth = "token"
os = "linux"
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}"
connection_timeout = 0

metadata {
key = "cpu"
display_name = "CPU Usage"
interval = 5
timeout = 5
script = "coder stat cpu"
}
metadata {
key = "memory"
display_name = "Memory Usage"
interval = 5
timeout = 5
script = "coder stat mem"
}
}

resource "coder_metadata" "info" {
count = data.coder_workspace.me.start_count
resource_id = coder_agent.dev[0].id
item {
key = "ami"
value = aws_instance.vm.ami
}
item {
key = "availability_zone"
value = local.aws_availability_zone
}
item {
key = "instance_type"
value = data.coder_parameter.instance_type.value
}
item {
key = "ssh_pubkey"
value = data.coder_parameter.ssh_pubkey.value
}
item {
key = "repo_url"
value = data.coder_parameter.repo_url.value
}
item {
key = "devcontainer_builder"
value = data.coder_parameter.devcontainer_builder.value
}
}

module "code-server" {
count = data.coder_workspace.me.start_count
source = "https://registry.coder.com/modules/code-server"
agent_id = coder_agent.dev[0].id
}
Loading