diff --git a/.github/typos.toml b/.github/typos.toml index 5889c7d7c..f27257a23 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -1,4 +1,7 @@ [default.extend-words] muc = "muc" # For Munich location code Hashi = "Hashi" -HashiCorp = "HashiCorp" \ No newline at end of file +HashiCorp = "HashiCorp" + +[files] +extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/.icons/1f4e6.png b/.icons/1f4e6.png new file mode 100644 index 000000000..ed5cbdcf3 Binary files /dev/null and b/.icons/1f4e6.png differ diff --git a/.icons/do.png b/.icons/do.png new file mode 100644 index 000000000..827bcaa64 Binary files /dev/null and b/.icons/do.png differ diff --git a/.icons/docker.png b/.icons/docker.png new file mode 100644 index 000000000..f07559cbc Binary files /dev/null and b/.icons/docker.png differ diff --git a/.icons/k8s.png b/.icons/k8s.png new file mode 100644 index 000000000..7a9e3c2b8 Binary files /dev/null and b/.icons/k8s.png differ diff --git a/.icons/lxc.svg b/.icons/lxc.svg new file mode 100644 index 000000000..0e8e118f7 --- /dev/null +++ b/.icons/lxc.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.icons/nomad.svg b/.icons/nomad.svg new file mode 100644 index 000000000..b4dc91c7b --- /dev/null +++ b/.icons/nomad.svg @@ -0,0 +1,2 @@ + + diff --git a/registry/coder/templates/aws-devcontainer/README.md b/registry/coder/templates/aws-devcontainer/README.md new file mode 100644 index 000000000..1f9440399 --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/README.md @@ -0,0 +1,111 @@ +--- +display_name: AWS EC2 (Devcontainer) +description: Provision AWS EC2 VMs with a devcontainer as Coder workspaces +icon: ../../../../.icons/aws.svg +maintainer_github: coder +verified: true +tags: [vm, linux, aws, persistent, devcontainer] +--- + +# Remote Development on AWS EC2 VMs using a Devcontainer + +Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template. +![Architecture Diagram](./architecture.svg) + + + +## Prerequisites + +### Authentication + +By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration). + +The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`. + +To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template. + +## Required permissions / policy + +The following sample policy allows Coder to create EC2 instances and modify +instances provisioned by Coder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ], + "Resource": "*" + }, + { + "Sid": "CoderResources", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ], + "Resource": "arn:aws:ec2:*:*:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/Coder_Provisioned": "true" + } + } + } + ] +} +``` + +## Architecture + +This template provisions the following resources: + +- AWS Instance + +Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **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). diff --git a/registry/coder/templates/aws-devcontainer/architecture.svg b/registry/coder/templates/aws-devcontainer/architecture.svg new file mode 100644 index 000000000..be66c3f1e --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/architecture.svg @@ -0,0 +1,8 @@ +AWSAWSHostingHostingVirtual MachineVirtual MachineLinux HardwareLinux HardwareCoder WorkspaceCoder WorkspaceDevcontainerDevcontainerenvbuilder created filesystemenvbuilder created filesystemA Clone of your repoA Clone of your repoSource codeSource codeLanguagesLanguagesPython. Go, etcPython. Go, etcToolingToolingExtensions, linting, formatting, etcExtensions, linting, formatting, etcCPUsCPUsDisk StorageDisk StorageCode EditorCode EditorVS Code DesktopVS Code DesktopLocal InstallationLocal InstallationVS Code DesktopVS Code DesktopLocal InstallationLocal Installationcode-servercode-serverA web IDEA web IDEJetBrains GatewayJetBrains GatewayLocal InstallationLocal InstallationCommand LineCommand LineSSH via Coder CLISSH via Coder CLI \ No newline at end of file diff --git a/registry/coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl b/registry/coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 000000000..af6b35171 --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/cloud-init/cloud-config.yaml.tftpl @@ -0,0 +1,15 @@ +#cloud-config +cloud_final_modules: + - [scripts-user, always] +hostname: ${hostname} +users: + - name: ${linux_user} + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash + ssh_authorized_keys: + - "${ssh_pubkey}" +# Automatically grow the partition +growpart: + mode: auto + devices: ['/'] + ignore_growroot_disabled: false diff --git a/registry/coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl b/registry/coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl new file mode 100644 index 000000000..67c166cb6 --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/cloud-init/userdata.sh.tftpl @@ -0,0 +1,37 @@ +#!/bin/bash +# Install Docker +if ! command -v docker &> /dev/null +then + echo "Docker not found, installing..." + curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh 2>&1 >/dev/null + usermod -aG docker ${linux_user} + newgrp docker +else + echo "Docker is already installed." +fi + +# Set up Docker credentials +mkdir -p "/home/${linux_user}/.docker" + +if [ -n "${docker_config_json_base64}" ]; then + # Write the Docker config JSON to disk if it is provided. + printf "%s" "${docker_config_json_base64}" | base64 -d | tee "/home/${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/${linux_user}/.docker/config.json" +fi +chown -R ${linux_user}:${linux_user} "/home/${linux_user}/.docker" + +# Start envbuilder +sudo -u coder docker run \ + --rm \ + --net=host \ + -h ${hostname} \ + -v /home/${linux_user}/envbuilder:/workspaces \ + %{ for key, value in environment ~} + -e ${key}="${value}" \ + %{ endfor ~} + ${builder_image} diff --git a/registry/coder/templates/aws-devcontainer/main.tf b/registry/coder/templates/aws-devcontainer/main.tf new file mode 100644 index 000000000..b23b9a65a --- /dev/null +++ b/registry/coder/templates/aws-devcontainer/main.tf @@ -0,0 +1,331 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + aws = { + source = "hashicorp/aws" + } + cloudinit = { + source = "hashicorp/cloudinit" + } + envbuilder = { + source = "coder/envbuilder" + } + } +} + +module "aws_region" { + source = "https://registry.coder.com/modules/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" + description = "What instance type should your workspace use?" + default = "t3.micro" + mutable = false + option { + name = "2 vCPU, 1 GiB RAM" + value = "t3.micro" + } + option { + name = "2 vCPU, 2 GiB RAM" + value = "t3.small" + } + option { + name = "2 vCPU, 4 GiB RAM" + value = "t3.medium" + } + option { + name = "2 vCPU, 8 GiB RAM" + value = "t3.large" + } + option { + name = "4 vCPU, 16 GiB RAM" + value = "t3.xlarge" + } + option { + name = "8 vCPU, 32 GiB RAM" + value = "t3.2xlarge" + } +} + +data "coder_parameter" "root_volume_size_gb" { + name = "root_volume_size_gb" + display_name = "Root Volume Size (GB)" + description = "How large should the root volume for the instance be?" + default = 30 + type = "number" + mutable = true + validation { + min = 1 + monotonic = "increasing" + } +} + +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_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" { + name = "repo_url" + display_name = "Repository URL" + default = "https://github.com/coder/envbuilder-starter-devcontainer" + description = "Repository URL" + mutable = true +} + +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 +} + +data "local_sensitive_file" "cache_repo_dockerconfigjson" { + count = var.cache_repo_docker_config_path == "" ? 0 : 1 + filename = var.cache_repo_docker_config_path +} + +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 { + # TODO: provide a way to pick the availability zone. + aws_availability_zone = "${module.aws_region.value}a" + + hostname = lower(data.coder_workspace.me.name) + linux_user = "coder" + + # 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" : local.docker_config_json_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 + } +} + +# 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 +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = false + + boundary = "//" + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + + ssh_pubkey = data.coder_parameter.ssh_pubkey.value + }) + } + + part { + filename = "userdata.sh" + content_type = "text/x-shellscript" + + content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + + # If we have a cached image, use the cached image's environment variables. + # Otherwise, just use the environment variables we've defined in locals. + environment = try(envbuilder_cached_image.cached[0].env_map, local.envbuilder_env) + + # 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) + + docker_config_json_base64 = local.docker_config_json_base64 + }) + } +} + +# This is useful for debugging the startup script. Left here for reference. +# resource local_file "startup_script" { +# content = data.cloudinit_config.user_data.rendered +# filename = "${path.module}/user_data.txt" +# } + +resource "aws_instance" "vm" { + 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 = data.coder_parameter.root_volume_size_gb.value + } + + user_data = data.cloudinit_config.user_data.rendered + tags = { + Name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + # Required if you are using our example policy, see template README + Coder_Provisioned = "true" + } + lifecycle { + ignore_changes = [ami] + } +} + +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 + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + agent_id = coder_agent.dev[0].id +} diff --git a/registry/coder/templates/aws-linux/README.md b/registry/coder/templates/aws-linux/README.md new file mode 100644 index 000000000..248fbea1a --- /dev/null +++ b/registry/coder/templates/aws-linux/README.md @@ -0,0 +1,94 @@ +--- +display_name: AWS EC2 (Linux) +description: Provision AWS EC2 VMs as Coder workspaces +icon: ../../../../.icons/aws.svg +maintainer_github: coder +verified: true +tags: [vm, linux, aws, persistent-vm] +--- + +# Remote Development on AWS EC2 VMs (Linux) + +Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + +## Prerequisites + +### Authentication + +By default, this template authenticates to AWS using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration). + +The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`. + +To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template. + +## Required permissions / policy + +The following sample policy allows Coder to create EC2 instances and modify +instances provisioned by Coder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ], + "Resource": "*" + }, + { + "Sid": "CoderResources", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ], + "Resource": "arn:aws:ec2:*:*:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/Coder_Provisioned": "true" + } + } + } + ] +} +``` + +## Architecture + +This template provisions the following resources: + +- AWS Instance + +Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. diff --git a/registry/coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl b/registry/coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 000000000..14da76945 --- /dev/null +++ b/registry/coder/templates/aws-linux/cloud-init/cloud-config.yaml.tftpl @@ -0,0 +1,8 @@ +#cloud-config +cloud_final_modules: + - [scripts-user, always] +hostname: ${hostname} +users: + - name: ${linux_user} + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/bash diff --git a/registry/coder/templates/aws-linux/cloud-init/userdata.sh.tftpl b/registry/coder/templates/aws-linux/cloud-init/userdata.sh.tftpl new file mode 100644 index 000000000..2070bc4df --- /dev/null +++ b/registry/coder/templates/aws-linux/cloud-init/userdata.sh.tftpl @@ -0,0 +1,2 @@ +#!/bin/bash +sudo -u '${linux_user}' sh -c '${init_script}' diff --git a/registry/coder/templates/aws-linux/main.tf b/registry/coder/templates/aws-linux/main.tf new file mode 100644 index 000000000..bf59dadc6 --- /dev/null +++ b/registry/coder/templates/aws-linux/main.tf @@ -0,0 +1,296 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + cloudinit = { + source = "hashicorp/cloudinit" + } + aws = { + source = "hashicorp/aws" + } + } +} + +# Last updated 2023-03-14 +# aws ec2 describe-regions | jq -r '[.Regions[].RegionName] | sort' +data "coder_parameter" "region" { + name = "region" + display_name = "Region" + description = "The region to deploy the workspace in." + default = "us-east-1" + mutable = false + option { + name = "Asia Pacific (Tokyo)" + value = "ap-northeast-1" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "Asia Pacific (Seoul)" + value = "ap-northeast-2" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Osaka)" + value = "ap-northeast-3" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "Asia Pacific (Mumbai)" + value = "ap-south-1" + icon = "/emojis/1f1ee-1f1f3.png" + } + option { + name = "Asia Pacific (Singapore)" + value = "ap-southeast-1" + icon = "/emojis/1f1f8-1f1ec.png" + } + option { + name = "Asia Pacific (Sydney)" + value = "ap-southeast-2" + icon = "/emojis/1f1e6-1f1fa.png" + } + option { + name = "Canada (Central)" + value = "ca-central-1" + icon = "/emojis/1f1e8-1f1e6.png" + } + option { + name = "EU (Frankfurt)" + value = "eu-central-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Stockholm)" + value = "eu-north-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Ireland)" + value = "eu-west-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (London)" + value = "eu-west-2" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Paris)" + value = "eu-west-3" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "South America (São Paulo)" + value = "sa-east-1" + icon = "/emojis/1f1e7-1f1f7.png" + } + option { + name = "US East (N. Virginia)" + value = "us-east-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US East (Ohio)" + value = "us-east-2" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (N. California)" + value = "us-west-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (Oregon)" + value = "us-west-2" + icon = "/emojis/1f1fa-1f1f8.png" + } +} + +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance type" + description = "What instance type should your workspace use?" + default = "t3.micro" + mutable = false + option { + name = "2 vCPU, 1 GiB RAM" + value = "t3.micro" + } + option { + name = "2 vCPU, 2 GiB RAM" + value = "t3.small" + } + option { + name = "2 vCPU, 4 GiB RAM" + value = "t3.medium" + } + option { + name = "2 vCPU, 8 GiB RAM" + value = "t3.large" + } + option { + name = "4 vCPU, 16 GiB RAM" + value = "t3.xlarge" + } + option { + name = "8 vCPU, 32 GiB RAM" + value = "t3.2xlarge" + } +} + +provider "aws" { + region = data.coder_parameter.region.value +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "aws_ami" "ubuntu" { + most_recent = true + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } + owners = ["099720109477"] # Canonical +} + +resource "coder_agent" "dev" { + count = data.coder_workspace.me.start_count + arch = "amd64" + auth = "aws-instance-identity" + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + + 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" + } + metadata { + key = "disk" + display_name = "Disk Usage" + interval = 600 # every 10 minutes + timeout = 30 # df can take a while on large filesystems + script = "coder stat disk --path $HOME" + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/code-server/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.dev[0].id + order = 1 +} + +# See https://registry.coder.com/modules/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/home/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.dev[0].id + agent_name = "dev" + order = 2 +} + +locals { + hostname = lower(data.coder_workspace.me.name) + linux_user = "coder" +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = false + + boundary = "//" + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + }) + } + + part { + filename = "userdata.sh" + content_type = "text/x-shellscript" + + content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", { + linux_user = local.linux_user + + init_script = try(coder_agent.dev[0].init_script, "") + }) + } +} + +resource "aws_instance" "dev" { + ami = data.aws_ami.ubuntu.id + availability_zone = "${data.coder_parameter.region.value}a" + instance_type = data.coder_parameter.instance_type.value + + user_data = data.cloudinit_config.user_data.rendered + tags = { + Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + # Required if you are using our example policy, see template README + Coder_Provisioned = "true" + } + lifecycle { + ignore_changes = [ami] + } +} + +resource "coder_metadata" "workspace_info" { + resource_id = aws_instance.dev.id + item { + key = "region" + value = data.coder_parameter.region.value + } + item { + key = "instance type" + value = aws_instance.dev.instance_type + } + item { + key = "disk" + value = "${aws_instance.dev.root_block_device[0].volume_size} GiB" + } +} + +resource "aws_ec2_instance_state" "dev" { + instance_id = aws_instance.dev.id + state = data.coder_workspace.me.transition == "start" ? "running" : "stopped" +} diff --git a/registry/coder/templates/aws-windows/README.md b/registry/coder/templates/aws-windows/README.md new file mode 100644 index 000000000..0ebd075d6 --- /dev/null +++ b/registry/coder/templates/aws-windows/README.md @@ -0,0 +1,96 @@ +--- +display_name: AWS EC2 (Windows) +description: Provision AWS EC2 VMs as Coder workspaces +icon: ../../../../.icons/aws.svg +maintainer_github: coder +verified: true +tags: [vm, windows, aws] +--- + +# Remote Development on AWS EC2 VMs (Windows) + +Provision AWS EC2 Windows VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + + + +## Prerequisites + +### Authentication + +By default, this template authenticates to AWS with using the provider's default [authentication methods](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration). + +The simplest way (without making changes to the template) is via environment variables (e.g. `AWS_ACCESS_KEY_ID`) or a [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-format). If you are running Coder on a VM, this file must be in `/home/coder/aws/credentials`. + +To use another [authentication method](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication), edit the template. + +## Required permissions / policy + +The following sample policy allows Coder to create EC2 instances and modify +instances provisioned by Coder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "ec2:GetDefaultCreditSpecification", + "ec2:DescribeIamInstanceProfileAssociations", + "ec2:DescribeTags", + "ec2:DescribeInstances", + "ec2:DescribeInstanceTypes", + "ec2:DescribeInstanceStatus", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:DescribeInstanceCreditSpecifications", + "ec2:DescribeImages", + "ec2:ModifyDefaultCreditSpecification", + "ec2:DescribeVolumes" + ], + "Resource": "*" + }, + { + "Sid": "CoderResources", + "Effect": "Allow", + "Action": [ + "ec2:DescribeInstanceAttribute", + "ec2:UnmonitorInstances", + "ec2:TerminateInstances", + "ec2:StartInstances", + "ec2:StopInstances", + "ec2:DeleteTags", + "ec2:MonitorInstances", + "ec2:CreateTags", + "ec2:RunInstances", + "ec2:ModifyInstanceAttribute", + "ec2:ModifyInstanceCreditSpecification" + ], + "Resource": "arn:aws:ec2:*:*:instance/*", + "Condition": { + "StringEquals": { + "aws:ResourceTag/Coder_Provisioned": "true" + } + } + } + ] +} +``` + +## Architecture + +This template provisions the following resources: + +- AWS Instance + +Coder uses `aws_ec2_instance_state` to start and stop the VM. This example template is fully persistent, meaning the full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. diff --git a/registry/coder/templates/aws-windows/main.tf b/registry/coder/templates/aws-windows/main.tf new file mode 100644 index 000000000..167b1b69f --- /dev/null +++ b/registry/coder/templates/aws-windows/main.tf @@ -0,0 +1,214 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + aws = { + source = "hashicorp/aws" + } + } +} + +# Last updated 2023-03-14 +# aws ec2 describe-regions | jq -r '[.Regions[].RegionName] | sort' +data "coder_parameter" "region" { + name = "region" + display_name = "Region" + description = "The region to deploy the workspace in." + default = "us-east-1" + mutable = false + option { + name = "Asia Pacific (Tokyo)" + value = "ap-northeast-1" + icon = "/emojis/1f1ef-1f1f5.png" + } + option { + name = "Asia Pacific (Seoul)" + value = "ap-northeast-2" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Osaka-Local)" + value = "ap-northeast-3" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Mumbai)" + value = "ap-south-1" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Singapore)" + value = "ap-southeast-1" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Asia Pacific (Sydney)" + value = "ap-southeast-2" + icon = "/emojis/1f1f0-1f1f7.png" + } + option { + name = "Canada (Central)" + value = "ca-central-1" + icon = "/emojis/1f1e8-1f1e6.png" + } + option { + name = "EU (Frankfurt)" + value = "eu-central-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Stockholm)" + value = "eu-north-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Ireland)" + value = "eu-west-1" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (London)" + value = "eu-west-2" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "EU (Paris)" + value = "eu-west-3" + icon = "/emojis/1f1ea-1f1fa.png" + } + option { + name = "South America (São Paulo)" + value = "sa-east-1" + icon = "/emojis/1f1e7-1f1f7.png" + } + option { + name = "US East (N. Virginia)" + value = "us-east-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US East (Ohio)" + value = "us-east-2" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (N. California)" + value = "us-west-1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "US West (Oregon)" + value = "us-west-2" + icon = "/emojis/1f1fa-1f1f8.png" + } +} + +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance type" + description = "What instance type should your workspace use?" + default = "t3.micro" + mutable = false + option { + name = "2 vCPU, 1 GiB RAM" + value = "t3.micro" + } + option { + name = "2 vCPU, 2 GiB RAM" + value = "t3.small" + } + option { + name = "2 vCPU, 4 GiB RAM" + value = "t3.medium" + } + option { + name = "2 vCPU, 8 GiB RAM" + value = "t3.large" + } + option { + name = "4 vCPU, 16 GiB RAM" + value = "t3.xlarge" + } + option { + name = "8 vCPU, 32 GiB RAM" + value = "t3.2xlarge" + } +} + +provider "aws" { + region = data.coder_parameter.region.value +} + +data "coder_workspace" "me" { +} +data "coder_workspace_owner" "me" {} + +data "aws_ami" "windows" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["Windows_Server-2019-English-Full-Base-*"] + } +} + +resource "coder_agent" "main" { + arch = "amd64" + auth = "aws-instance-identity" + os = "windows" +} + +locals { + + # User data is used to stop/start AWS instances. See: + # https://github.com/hashicorp/terraform-provider-aws/issues/22 + + user_data_start = < +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +${coder_agent.main.init_script} + +true +EOT + + user_data_end = < +shutdown /s + +true +EOT +} + +resource "aws_instance" "dev" { + ami = data.aws_ami.windows.id + availability_zone = "${data.coder_parameter.region.value}a" + instance_type = data.coder_parameter.instance_type.value + + user_data = data.coder_workspace.me.transition == "start" ? local.user_data_start : local.user_data_end + tags = { + Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + # Required if you are using our example policy, see template README + Coder_Provisioned = "true" + } + lifecycle { + ignore_changes = [ami] + } +} + +resource "coder_metadata" "workspace_info" { + resource_id = aws_instance.dev.id + item { + key = "region" + value = data.coder_parameter.region.value + } + item { + key = "instance type" + value = aws_instance.dev.instance_type + } + item { + key = "disk" + value = "${aws_instance.dev.root_block_device[0].volume_size} GiB" + } +} diff --git a/registry/coder/templates/azure-linux/README.md b/registry/coder/templates/azure-linux/README.md new file mode 100644 index 000000000..9b5ba079e --- /dev/null +++ b/registry/coder/templates/azure-linux/README.md @@ -0,0 +1,64 @@ +--- +display_name: Azure VM (Linux) +description: Provision Azure VMs as Coder workspaces +icon: ../../../../.icons/azure.svg +maintainer_github: coder +verified: true +tags: [vm, linux, azure] +--- + +# Remote Development on Azure VMs (Linux) + +Provision Azure Linux VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + + + +## Prerequisites + +### Authentication + +This template assumes that coderd is run in an environment that is authenticated +with Azure. For example, run `az login` then `az account set --subscription=` +to import credentials on the system and user running coderd. For other ways to +authenticate, [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure). + +## Architecture + +This template provisions the following resources: + +- Azure VM (ephemeral, deleted on stop) +- Managed disk (persistent, mounted to `/home/coder`) + +This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles. + +> [!NOTE] +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +### Persistent VM + +> [!IMPORTANT] +> This approach requires the [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli#install) to be present in the PATH of your Coder Provisioner. +> You will have to do this installation manually as it is not included in our official images. + +It is possible to make the VM persistent (instead of ephemeral) by removing the `count` attribute in the `azurerm_linux_virtual_machine` resource block as well as adding the following snippet: + +```hcl +# Stop the VM +resource "null_resource" "stop_vm" { + count = data.coder_workspace.me.transition == "stop" ? 1 : 0 + depends_on = [azurerm_linux_virtual_machine.main] + provisioner "local-exec" { + # Use deallocate so the VM is not charged + command = "az vm deallocate --ids ${azurerm_linux_virtual_machine.main.id}" + } +} + +# Start the VM +resource "null_resource" "start" { + count = data.coder_workspace.me.transition == "start" ? 1 : 0 + depends_on = [azurerm_linux_virtual_machine.main] + provisioner "local-exec" { + command = "az vm start --ids ${azurerm_linux_virtual_machine.main.id}" + } +} +``` diff --git a/registry/coder/templates/azure-linux/cloud-init/cloud-config.yaml.tftpl b/registry/coder/templates/azure-linux/cloud-init/cloud-config.yaml.tftpl new file mode 100644 index 000000000..750067783 --- /dev/null +++ b/registry/coder/templates/azure-linux/cloud-init/cloud-config.yaml.tftpl @@ -0,0 +1,56 @@ +#cloud-config +cloud_final_modules: +- [scripts-user, always] +bootcmd: + # work around https://github.com/hashicorp/terraform-provider-azurerm/issues/6117 + - until [ -e /dev/disk/azure/scsi1/lun10 ]; do sleep 1; done +device_aliases: + homedir: /dev/disk/azure/scsi1/lun10 +disk_setup: + homedir: + table_type: gpt + layout: true +fs_setup: + - label: coder_home + filesystem: ext4 + device: homedir.1 +mounts: + - ["LABEL=coder_home", "/home/${username}"] +hostname: ${hostname} +users: + - name: ${username} + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + groups: sudo + shell: /bin/bash +packages: + - git +write_files: + - path: /opt/coder/init + permissions: "0755" + encoding: b64 + content: ${init_script} + - path: /etc/systemd/system/coder-agent.service + permissions: "0644" + content: | + [Unit] + Description=Coder Agent + After=network-online.target + Wants=network-online.target + + [Service] + User=${username} + ExecStart=/opt/coder/init + Restart=always + RestartSec=10 + TimeoutStopSec=90 + KillMode=process + + OOMScoreAdjust=-900 + SyslogIdentifier=coder-agent + + [Install] + WantedBy=multi-user.target +runcmd: + - chown ${username}:${username} /home/${username} + - systemctl enable coder-agent + - systemctl start coder-agent diff --git a/registry/coder/templates/azure-linux/main.tf b/registry/coder/templates/azure-linux/main.tf new file mode 100644 index 000000000..687c8cae2 --- /dev/null +++ b/registry/coder/templates/azure-linux/main.tf @@ -0,0 +1,325 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + azurerm = { + source = "hashicorp/azurerm" + } + cloudinit = { + source = "hashicorp/cloudinit" + } + } +} + +# See https://registry.coder.com/modules/coder/azure-region +module "azure_region" { + source = "registry.coder.com/coder/azure-region/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + default = "eastus" +} + +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance type" + description = "What instance type should your workspace use?" + default = "Standard_B4ms" + icon = "/icon/azure.png" + mutable = false + option { + name = "Standard_B1ms (1 vCPU, 2 GiB RAM)" + value = "Standard_B1ms" + } + option { + name = "Standard_B2ms (2 vCPU, 8 GiB RAM)" + value = "Standard_B2ms" + } + option { + name = "Standard_B4ms (4 vCPU, 16 GiB RAM)" + value = "Standard_B4ms" + } + option { + name = "Standard_B8ms (8 vCPU, 32 GiB RAM)" + value = "Standard_B8ms" + } + option { + name = "Standard_B12ms (12 vCPU, 48 GiB RAM)" + value = "Standard_B12ms" + } + option { + name = "Standard_B16ms (16 vCPU, 64 GiB RAM)" + value = "Standard_B16ms" + } + option { + name = "Standard_D2as_v5 (2 vCPU, 8 GiB RAM)" + value = "Standard_D2as_v5" + } + option { + name = "Standard_D4as_v5 (4 vCPU, 16 GiB RAM)" + value = "Standard_D4as_v5" + } + option { + name = "Standard_D8as_v5 (8 vCPU, 32 GiB RAM)" + value = "Standard_D8as_v5" + } + option { + name = "Standard_D16as_v5 (16 vCPU, 64 GiB RAM)" + value = "Standard_D16as_v5" + } + option { + name = "Standard_D32as_v5 (32 vCPU, 128 GiB RAM)" + value = "Standard_D32as_v5" + } +} + +data "coder_parameter" "home_size" { + name = "home_size" + display_name = "Home volume size" + description = "How large would you like your home volume to be (in GB)?" + default = 20 + type = "number" + icon = "/icon/azure.png" + mutable = false + validation { + min = 1 + max = 1024 + } +} + +provider "azurerm" { + features {} +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + arch = "amd64" + os = "linux" + auth = "azure-instance-identity" + + metadata { + key = "cpu" + display_name = "CPU Usage" + interval = 5 + timeout = 5 + script = <<-EOT + #!/bin/bash + set -e + top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}' + EOT + } + metadata { + key = "memory" + display_name = "Memory Usage" + interval = 5 + timeout = 5 + script = <<-EOT + #!/bin/bash + set -e + free -m | awk 'NR==2{printf "%.2f%%\t", $3*100/$2 }' + EOT + } + metadata { + key = "disk" + display_name = "Disk Usage" + interval = 600 # every 10 minutes + timeout = 30 # df can take a while on large filesystems + script = <<-EOT + #!/bin/bash + set -e + df /home/coder | awk '$NF=="/"{printf "%s", $5}' + EOT + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + order = 1 +} + +# See https://registry.coder.com/modules/coder/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/home/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + agent_name = "main" + order = 2 +} + +locals { + prefix = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = true + + boundary = "//" + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + username = "coder" # Ensure this user/group does not exist in your VM image + init_script = base64encode(coder_agent.main.init_script) + hostname = lower(data.coder_workspace.me.name) + }) + } +} + +resource "azurerm_resource_group" "main" { + name = "${local.prefix}-resources" + location = module.azure_region.value + + tags = { + Coder_Provisioned = "true" + } +} + +// Uncomment here and in the azurerm_network_interface resource to obtain a public IP +#resource "azurerm_public_ip" "main" { +# name = "publicip" +# resource_group_name = azurerm_resource_group.main.name +# location = azurerm_resource_group.main.location +# allocation_method = "Static" +# +# tags = { +# Coder_Provisioned = "true" +# } +#} + +resource "azurerm_virtual_network" "main" { + name = "network" + address_space = ["10.0.0.0/24"] + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + + tags = { + Coder_Provisioned = "true" + } +} + +resource "azurerm_subnet" "internal" { + name = "internal" + resource_group_name = azurerm_resource_group.main.name + virtual_network_name = azurerm_virtual_network.main.name + address_prefixes = ["10.0.0.0/29"] +} + +resource "azurerm_network_interface" "main" { + name = "nic" + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + + ip_configuration { + name = "internal" + subnet_id = azurerm_subnet.internal.id + private_ip_address_allocation = "Dynamic" + // Uncomment for public IP address as well as azurerm_public_ip resource above + //public_ip_address_id = azurerm_public_ip.main.id + } + + tags = { + Coder_Provisioned = "true" + } +} + +resource "azurerm_managed_disk" "home" { + create_option = "Empty" + location = azurerm_resource_group.main.location + name = "home" + resource_group_name = azurerm_resource_group.main.name + storage_account_type = "StandardSSD_LRS" + disk_size_gb = data.coder_parameter.home_size.value +} + +// azurerm requires an SSH key (or password) for an admin user or it won't start a VM. However, +// cloud-init overwrites this anyway, so we'll just use a dummy SSH key. +resource "tls_private_key" "dummy" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "azurerm_linux_virtual_machine" "main" { + count = data.coder_workspace.me.start_count + name = "vm" + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + size = data.coder_parameter.instance_type.value + // cloud-init overwrites this, so the value here doesn't matter + admin_username = "adminuser" + admin_ssh_key { + public_key = tls_private_key.dummy.public_key_openssh + username = "adminuser" + } + + network_interface_ids = [ + azurerm_network_interface.main.id, + ] + computer_name = lower(data.coder_workspace.me.name) + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + source_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-focal" + sku = "20_04-lts-gen2" + version = "latest" + } + user_data = data.cloudinit_config.user_data.rendered + + tags = { + Coder_Provisioned = "true" + } +} + +resource "azurerm_virtual_machine_data_disk_attachment" "home" { + count = data.coder_workspace.me.transition == "start" ? 1 : 0 + managed_disk_id = azurerm_managed_disk.home.id + virtual_machine_id = azurerm_linux_virtual_machine.main[0].id + lun = "10" + caching = "ReadWrite" +} + +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = azurerm_linux_virtual_machine.main[0].id + + item { + key = "type" + value = azurerm_linux_virtual_machine.main[0].size + } +} + +resource "coder_metadata" "home_info" { + resource_id = azurerm_managed_disk.home.id + + item { + key = "size" + value = "${data.coder_parameter.home_size.value} GiB" + } +} diff --git a/registry/coder/templates/azure-windows/FirstLogonCommands.xml b/registry/coder/templates/azure-windows/FirstLogonCommands.xml new file mode 100644 index 000000000..ac4a9d807 --- /dev/null +++ b/registry/coder/templates/azure-windows/FirstLogonCommands.xml @@ -0,0 +1,12 @@ + + + cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\Initialize.ps1" + Copy Initialize.ps1 to file from CustomData + 3 + + + powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\Initialize.ps1 *> C:\AzureData\Initialize.log" + Execute Initialize.ps1 script + 4 + + diff --git a/registry/coder/templates/azure-windows/Initialize.ps1.tftpl b/registry/coder/templates/azure-windows/Initialize.ps1.tftpl new file mode 100644 index 000000000..ae1bdef7b --- /dev/null +++ b/registry/coder/templates/azure-windows/Initialize.ps1.tftpl @@ -0,0 +1,73 @@ +# This script gets run once when the VM is first created. + +# Initialize the data disk & home directory. +$disk = Get-Disk -Number 2 +if ($disk.PartitionStyle -Eq 'RAW') +{ + "Initializing data disk" + $disk | Initialize-Disk +} else { + "data disk already initialized" +} + +$partitions = Get-Partition -DiskNumber $disk.Number | Where-Object Type -Ne 'Reserved' +if ($partitions.Count -Eq 0) { + "Creating partition on data disk" + $partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize +} else { + $partition = $partitions[0] + $s = "data disk already has partition of size {0:n1} GiB" -f ($partition.Size / 1073741824) + Write-Output $s +} + +$volume = Get-Volume -Partition $partition +if ($volume.FileSystemType -Eq 'Unknown') +{ + "Formatting data disk" + Format-Volume -InputObject $volume -FileSystem NTFS -Confirm:$false +} else { + "data disk is already formatted" +} + +# Mount the partition +Add-PartitionAccessPath -InputObject $partition -AccessPath "F:" + +# Enable RDP +Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0 +# Enable RDP through Windows Firewall +Enable-NetFirewallRule -DisplayGroup "Remote Desktop" +# Disable Network Level Authentication (NLA) +# Clients will connect via Coder's tunnel +(Get-WmiObject -class "Win32_TSGeneralSetting" -Namespace root\cimv2\terminalservices -ComputerName $env:COMPUTERNAME -Filter "TerminalName='RDP-tcp'").SetUserAuthenticationRequired(0) + +# Install Chocolatey package manager +Set-ExecutionPolicy Bypass -Scope Process -Force +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 +iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +# Reload path so sessions include "choco" and "refreshenv" +$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + +# Install Git and reload path +choco install -y git +$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + +# Set protocol to TLS1.2 for agent download +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Set Coder Agent to run immediately, and on each restart +$init_script = @' +${init_script} +'@ +Out-File -FilePath "C:\AzureData\CoderAgent.ps1" -InputObject $init_script +$task = @{ + TaskName = 'CoderAgent' + Action = (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 *>> C:\AzureData\CoderAgent.log"') + Trigger = (New-ScheduledTaskTrigger -AtStartup), (New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(15)) + Settings = (New-ScheduledTaskSettingsSet -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::FromDays(3650)) -Compatibility Win8) + Principal = (New-ScheduledTaskPrincipal -UserId "$env:COMPUTERNAME\$env:USERNAME" -RunLevel Highest -LogonType S4U) +} +Register-ScheduledTask @task -Force + +# Additional Chocolatey package installs (optional, uncomment to enable) +# choco feature enable -n=allowGlobalConfirmation +# choco install visualstudio2022community --package-parameters "--add=Microsoft.VisualStudio.Workload.ManagedDesktop;includeRecommended --passive --locale en-US" diff --git a/registry/coder/templates/azure-windows/README.md b/registry/coder/templates/azure-windows/README.md new file mode 100644 index 000000000..1fb1bfe1f --- /dev/null +++ b/registry/coder/templates/azure-windows/README.md @@ -0,0 +1,64 @@ +--- +display_name: Azure VM (Windows) +description: Provision Azure VMs as Coder workspaces +icon: ../../../../.icons/azure.svg +maintainer_github: coder +verified: true +tags: [vm, windows, azure] +--- + +# Remote Development on Azure VMs (Windows) + +Provision Azure Windows VMs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + + + +## Prerequisites + +### Authentication + +This template assumes that coderd is run in an environment that is authenticated +with Azure. For example, run `az login` then `az account set --subscription=` +to import credentials on the system and user running coderd. For other ways to +authenticate, [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure). + +## Architecture + +This template provisions the following resources: + +- Azure VM (ephemeral, deleted on stop) +- Managed disk (persistent, mounted to `F:`) + +This means, when the workspace restarts, any tools or files outside of the data directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). + +> [!NOTE] +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +### Persistent VM + +> [!IMPORTANT] +> This approach requires the [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli#install) to be present in the PATH of your Coder Provisioner. +> You will have to do this installation manually as it is not included in our official images. + +It is possible to make the VM persistent (instead of ephemeral) by removing the `count` attribute in the `azurerm_windows_virtual_machine` resource block as well as adding the following snippet: + +```hcl +# Stop the VM +resource "null_resource" "stop_vm" { + count = data.coder_workspace.me.transition == "stop" ? 1 : 0 + depends_on = [azurerm_windows_virtual_machine.main] + provisioner "local-exec" { + # Use deallocate so the VM is not charged + command = "az vm deallocate --ids ${azurerm_windows_virtual_machine.main.id}" + } +} + +# Start the VM +resource "null_resource" "start" { + count = data.coder_workspace.me.transition == "start" ? 1 : 0 + depends_on = [azurerm_windows_virtual_machine.main] + provisioner "local-exec" { + command = "az vm start --ids ${azurerm_windows_virtual_machine.main.id}" + } +} +``` diff --git a/registry/coder/templates/azure-windows/main.tf b/registry/coder/templates/azure-windows/main.tf new file mode 100644 index 000000000..65447a777 --- /dev/null +++ b/registry/coder/templates/azure-windows/main.tf @@ -0,0 +1,210 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + azurerm = { + source = "hashicorp/azurerm" + } + } +} + +provider "azurerm" { + features {} +} + +provider "coder" {} +data "coder_workspace" "me" {} + +# See https://registry.coder.com/modules/coder/azure-region +module "azure_region" { + source = "registry.coder.com/coder/azure-region/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + default = "eastus" +} + +# See https://registry.coder.com/modules/coder/windows-rdp +module "windows_rdp" { + source = "registry.coder.com/coder/windows-rdp/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + admin_username = local.admin_username + admin_password = random_password.admin_password.result + + agent_id = resource.coder_agent.main.id + resource_id = null # Unused, to be removed in a future version +} + +data "coder_parameter" "data_disk_size" { + description = "Size of your data (F:) drive in GB" + display_name = "Data disk size" + name = "data_disk_size" + default = 20 + mutable = "false" + type = "number" + validation { + min = 5 + max = 5000 + } +} + +resource "coder_agent" "main" { + arch = "amd64" + auth = "azure-instance-identity" + os = "windows" +} + +resource "random_password" "admin_password" { + length = 16 + special = true + # https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements#reference + # we remove characters that require special handling in XML, as this is how we pass it to the VM; we also remove the powershell escape character + # namely: <>&'`" + override_special = "~!@#$%^*_-+=|\\(){}[]:;,.?/" +} + +locals { + prefix = "coder-win" + admin_username = "coder" +} + +resource "azurerm_resource_group" "main" { + name = "${local.prefix}-${data.coder_workspace.me.id}" + location = module.azure_region.value + tags = { + Coder_Provisioned = "true" + } +} + +// Uncomment here and in the azurerm_network_interface resource to obtain a public IP +#resource "azurerm_public_ip" "main" { +# name = "publicip" +# resource_group_name = azurerm_resource_group.main.name +# location = azurerm_resource_group.main.location +# allocation_method = "Static" +# tags = { +# Coder_Provisioned = "true" +# } +#} +resource "azurerm_virtual_network" "main" { + name = "network" + address_space = ["10.0.0.0/24"] + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + tags = { + Coder_Provisioned = "true" + } +} +resource "azurerm_subnet" "internal" { + name = "internal" + resource_group_name = azurerm_resource_group.main.name + virtual_network_name = azurerm_virtual_network.main.name + address_prefixes = ["10.0.0.0/29"] +} +resource "azurerm_network_interface" "main" { + name = "nic" + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + ip_configuration { + name = "internal" + subnet_id = azurerm_subnet.internal.id + private_ip_address_allocation = "Dynamic" + // Uncomment for public IP address as well as azurerm_public_ip resource above + # public_ip_address_id = azurerm_public_ip.main.id + } + tags = { + Coder_Provisioned = "true" + } +} +# Create storage account for boot diagnostics +resource "azurerm_storage_account" "my_storage_account" { + name = "diag${random_id.storage_id.hex}" + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + account_tier = "Standard" + account_replication_type = "LRS" +} +# Generate random text for a unique storage account name +resource "random_id" "storage_id" { + keepers = { + # Generate a new ID only when a new resource group is defined + resource_group = azurerm_resource_group.main.name + } + byte_length = 8 +} + +resource "azurerm_managed_disk" "data" { + name = "data_disk" + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = data.coder_parameter.data_disk_size.value +} + +# Create virtual machine +resource "azurerm_windows_virtual_machine" "main" { + count = data.coder_workspace.me.start_count + name = "vm" + admin_username = local.admin_username + admin_password = random_password.admin_password.result + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + network_interface_ids = [azurerm_network_interface.main.id] + size = "Standard_DS1_v2" + custom_data = base64encode( + templatefile("${path.module}/Initialize.ps1.tftpl", { init_script = coder_agent.main.init_script }) + ) + os_disk { + name = "myOsDisk" + caching = "ReadWrite" + storage_account_type = "Premium_LRS" + } + source_image_reference { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2022-datacenter-azure-edition" + version = "latest" + } + additional_unattend_content { + content = "${random_password.admin_password.result}true1${local.admin_username}" + setting = "AutoLogon" + } + additional_unattend_content { + content = file("${path.module}/FirstLogonCommands.xml") + setting = "FirstLogonCommands" + } + boot_diagnostics { + storage_account_uri = azurerm_storage_account.my_storage_account.primary_blob_endpoint + } + tags = { + Coder_Provisioned = "true" + } +} + +resource "coder_metadata" "rdp_login" { + count = data.coder_workspace.me.start_count + resource_id = azurerm_windows_virtual_machine.main[0].id + item { + key = "Username" + value = local.admin_username + } + item { + key = "Password" + value = random_password.admin_password.result + sensitive = true + } +} + +resource "azurerm_virtual_machine_data_disk_attachment" "main_data" { + count = data.coder_workspace.me.start_count + managed_disk_id = azurerm_managed_disk.data.id + virtual_machine_id = azurerm_windows_virtual_machine.main[0].id + lun = "10" + caching = "ReadWrite" +} diff --git a/registry/coder/templates/digitalocean-linux/README.md b/registry/coder/templates/digitalocean-linux/README.md new file mode 100644 index 000000000..5acddc316 --- /dev/null +++ b/registry/coder/templates/digitalocean-linux/README.md @@ -0,0 +1,52 @@ +--- +display_name: DigitalOcean Droplet (Linux) +description: Provision DigitalOcean Droplets as Coder workspaces +icon: ../../../../.icons/do.png +maintainer_github: coder +verified: true +tags: [vm, linux, digitalocean] +--- + +# Remote Development on DigitalOcean Droplets + +Provision DigitalOcean Droplets as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. + + + +## Prerequisites + +To deploy workspaces as DigitalOcean Droplets, you'll need: + +- DigitalOcean [personal access token (PAT)](https://docs.digitalocean.com/reference/api/create-personal-access-token) + +- DigitalOcean project ID (you can get your project information via the `doctl` CLI by running `doctl projects list`) + + - Remove the following sections from the `main.tf` file if you don't want to + associate your workspaces with a project: + + - `variable "project_uuid"` + - `resource "digitalocean_project_resources" "project"` + +- **Optional:** DigitalOcean SSH key ID (obtain via the `doctl` CLI by running + `doctl compute ssh-key list`) + + - Note that this is only required for Fedora images to work. + +### Authentication + +This template assumes that the Coder Provisioner is run in an environment that is authenticated with Digital Ocean. + +Obtain a [Digital Ocean Personal Access Token](https://cloud.digitalocean.com/account/api/tokens) and set the `DIGITALOCEAN_TOKEN` environment variable to the access token. +For other ways to authenticate [consult the Terraform provider's docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs). + +## Architecture + +This template provisions the following resources: + +- DigitalOcean VM (ephemeral, deleted on stop) +- Managed disk (persistent, mounted to `/home/coder`) + +This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the VM image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). + +> [!NOTE] +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. diff --git a/registry/coder/templates/digitalocean-linux/cloud-config.yaml.tftpl b/registry/coder/templates/digitalocean-linux/cloud-config.yaml.tftpl new file mode 100644 index 000000000..a2400b546 --- /dev/null +++ b/registry/coder/templates/digitalocean-linux/cloud-config.yaml.tftpl @@ -0,0 +1,46 @@ +#cloud-config +users: + - name: ${username} + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + groups: sudo + shell: /bin/bash +packages: + - git +mounts: + - [ + "LABEL=${home_volume_label}", + "/home/${username}", + auto, + "defaults,uid=1000,gid=1000", + ] +write_files: + - path: /opt/coder/init + permissions: "0755" + encoding: b64 + content: ${init_script} + - path: /etc/systemd/system/coder-agent.service + permissions: "0644" + content: | + [Unit] + Description=Coder Agent + After=network-online.target + Wants=network-online.target + + [Service] + User=${username} + ExecStart=/opt/coder/init + Environment=CODER_AGENT_TOKEN=${coder_agent_token} + Restart=always + RestartSec=10 + TimeoutStopSec=90 + KillMode=process + + OOMScoreAdjust=-900 + SyslogIdentifier=coder-agent + + [Install] + WantedBy=multi-user.target +runcmd: + - chown ${username}:${username} /home/${username} + - systemctl enable coder-agent + - systemctl start coder-agent diff --git a/registry/coder/templates/digitalocean-linux/main.tf b/registry/coder/templates/digitalocean-linux/main.tf new file mode 100644 index 000000000..4daf4b8b8 --- /dev/null +++ b/registry/coder/templates/digitalocean-linux/main.tf @@ -0,0 +1,361 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + digitalocean = { + source = "digitalocean/digitalocean" + } + } +} + +provider "coder" {} + +variable "project_uuid" { + type = string + description = <<-EOF + DigitalOcean project ID + + $ doctl projects list + EOF + sensitive = true + + validation { + # make sure length of alphanumeric string is 36 (UUIDv4 size) + condition = length(var.project_uuid) == 36 + error_message = "Invalid Digital Ocean Project ID." + } + +} + +variable "ssh_key_id" { + type = number + description = <<-EOF + DigitalOcean SSH key ID (some Droplet images require an SSH key to be set): + + Can be set to "0" for no key. + + Note: Setting this to zero will break Fedora images and notify root passwords via email. + + $ doctl compute ssh-key list + EOF + sensitive = true + default = 0 + + validation { + condition = var.ssh_key_id >= 0 + error_message = "Invalid Digital Ocean SSH key ID, a number is required." + } +} + +data "coder_parameter" "droplet_image" { + name = "droplet_image" + display_name = "Droplet image" + description = "Which Droplet image would you like to use?" + default = "ubuntu-22-04-x64" + type = "string" + mutable = false + option { + name = "AlmaLinux 9" + value = "almalinux-9-x64" + icon = "/icon/almalinux.svg" + } + option { + name = "AlmaLinux 8" + value = "almalinux-8-x64" + icon = "/icon/almalinux.svg" + } + option { + name = "Fedora 39" + value = "fedora-39-x64" + icon = "/icon/fedora.svg" + } + option { + name = "Fedora 38" + value = "fedora-38-x64" + icon = "/icon/fedora.svg" + } + option { + name = "CentOS Stream 9" + value = "centos-stream-9-x64" + icon = "/icon/centos.svg" + } + option { + name = "CentOS Stream 8" + value = "centos-stream-8-x64" + icon = "/icon/centos.svg" + } + option { + name = "Debian 12" + value = "debian-12-x64" + icon = "/icon/debian.svg" + } + option { + name = "Debian 11" + value = "debian-11-x64" + icon = "/icon/debian.svg" + } + option { + name = "Debian 10" + value = "debian-10-x64" + icon = "/icon/debian.svg" + } + option { + name = "Rocky Linux 9" + value = "rockylinux-9-x64" + icon = "/icon/rockylinux.svg" + } + option { + name = "Rocky Linux 8" + value = "rockylinux-8-x64" + icon = "/icon/rockylinux.svg" + } + option { + name = "Ubuntu 22.04 (LTS)" + value = "ubuntu-22-04-x64" + icon = "/icon/ubuntu.svg" + } + option { + name = "Ubuntu 20.04 (LTS)" + value = "ubuntu-20-04-x64" + icon = "/icon/ubuntu.svg" + } +} + +data "coder_parameter" "droplet_size" { + name = "droplet_size" + display_name = "Droplet size" + description = "Which Droplet configuration would you like to use?" + default = "s-1vcpu-1gb" + type = "string" + icon = "/icon/memory.svg" + mutable = false + # s-1vcpu-512mb-10gb is unsupported in tor1, blr1, lon1, sfo2, and nyc3 regions + # s-8vcpu-16gb access requires a support ticket with Digital Ocean + option { + name = "1 vCPU, 1 GB RAM" + value = "s-1vcpu-1gb" + } + option { + name = "1 vCPU, 2 GB RAM" + value = "s-1vcpu-2gb" + } + option { + name = "2 vCPU, 2 GB RAM" + value = "s-2vcpu-2gb" + } + option { + name = "2 vCPU, 4 GB RAM" + value = "s-2vcpu-4gb" + } + option { + name = "4 vCPU, 8 GB RAM" + value = "s-4vcpu-8gb" + } +} + +data "coder_parameter" "home_volume_size" { + name = "home_volume_size" + display_name = "Home volume size" + description = "How large would you like your home volume to be (in GB)?" + type = "number" + default = "20" + mutable = false + validation { + min = 1 + max = 100 # Sizes larger than 100 GB require a support ticket with Digital Ocean + } +} + +data "coder_parameter" "region" { + name = "region" + display_name = "Region" + description = "This is the region where your workspace will be created." + icon = "/emojis/1f30e.png" + type = "string" + default = "ams3" + mutable = false + # nyc1, sfo1, and ams2 regions were excluded because they do not support volumes, which are used to persist data while decreasing cost + option { + name = "Canada (Toronto)" + value = "tor1" + icon = "/emojis/1f1e8-1f1e6.png" + } + option { + name = "Germany (Frankfurt)" + value = "fra1" + icon = "/emojis/1f1e9-1f1ea.png" + } + option { + name = "India (Bangalore)" + value = "blr1" + icon = "/emojis/1f1ee-1f1f3.png" + } + option { + name = "Netherlands (Amsterdam)" + value = "ams3" + icon = "/emojis/1f1f3-1f1f1.png" + } + option { + name = "Singapore" + value = "sgp1" + icon = "/emojis/1f1f8-1f1ec.png" + } + option { + name = "United Kingdom (London)" + value = "lon1" + icon = "/emojis/1f1ec-1f1e7.png" + } + option { + name = "United States (California - 2)" + value = "sfo2" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "United States (California - 3)" + value = "sfo3" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "United States (New York - 1)" + value = "nyc1" + icon = "/emojis/1f1fa-1f1f8.png" + } + option { + name = "United States (New York - 3)" + value = "nyc3" + icon = "/emojis/1f1fa-1f1f8.png" + } +} + +# Configure the DigitalOcean Provider +provider "digitalocean" { + # Recommended: use environment variable DIGITALOCEAN_TOKEN with your personal access token when starting coderd + # alternatively, you can pass the token via a variable. +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + + 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" + } + metadata { + key = "home" + display_name = "Home Usage" + interval = 600 # every 10 minutes + timeout = 30 # df can take a while on large filesystems + script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}" + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + order = 1 +} + +# See https://registry.coder.com/modules/coder/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/home/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + agent_name = "main" + order = 2 +} + +resource "digitalocean_volume" "home_volume" { + region = data.coder_parameter.region.value + name = "coder-${data.coder_workspace.me.id}-home" + size = data.coder_parameter.home_volume_size.value + initial_filesystem_type = "ext4" + initial_filesystem_label = "coder-home" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } +} + +resource "digitalocean_droplet" "workspace" { + region = data.coder_parameter.region.value + count = data.coder_workspace.me.start_count + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}" + image = data.coder_parameter.droplet_image.value + size = data.coder_parameter.droplet_size.value + + volume_ids = [digitalocean_volume.home_volume.id] + user_data = templatefile("cloud-config.yaml.tftpl", { + username = lower(data.coder_workspace_owner.me.name) + home_volume_label = digitalocean_volume.home_volume.initial_filesystem_label + init_script = base64encode(coder_agent.main.init_script) + coder_agent_token = coder_agent.main.token + }) + # Required to provision Fedora. + ssh_keys = var.ssh_key_id > 0 ? [var.ssh_key_id] : [] +} + +resource "digitalocean_project_resources" "project" { + project = var.project_uuid + # Workaround for terraform plan when using count. + resources = length(digitalocean_droplet.workspace) > 0 ? [ + digitalocean_volume.home_volume.urn, + digitalocean_droplet.workspace[0].urn + ] : [ + digitalocean_volume.home_volume.urn + ] +} + +resource "coder_metadata" "workspace-info" { + count = data.coder_workspace.me.start_count + resource_id = digitalocean_droplet.workspace[0].id + + item { + key = "region" + value = digitalocean_droplet.workspace[0].region + } + item { + key = "image" + value = digitalocean_droplet.workspace[0].image + } +} + +resource "coder_metadata" "volume-info" { + resource_id = digitalocean_volume.home_volume.id + + item { + key = "size" + value = "${digitalocean_volume.home_volume.size} GiB" + } +} diff --git a/registry/coder/templates/docker-devcontainer/README.md b/registry/coder/templates/docker-devcontainer/README.md new file mode 100644 index 000000000..0c07d5ca6 --- /dev/null +++ b/registry/coder/templates/docker-devcontainer/README.md @@ -0,0 +1,77 @@ +--- +display_name: Docker (Devcontainer) +description: Provision envbuilder containers as Coder workspaces +icon: ../../../../.icons/docker.png +maintainer_github: coder +verified: true +tags: [container, docker, devcontainer] +--- + +# Remote Development on Docker Containers (with Devcontainers) + +Provision Devcontainers as [Coder workspaces](https://coder.com/docs/workspaces) in Docker with this example template. + +## Prerequisites + +### Infrastructure + +Coder must have access to a running Docker socket, and the `coder` user must be a member of the `docker` group: + +```shell +# Add coder user to Docker group +sudo usermod -aG docker coder + +# Restart Coder server +sudo systemctl restart coder + +# Test Docker +sudo -u coder docker ps +``` + +## Architecture + +Coder supports Devcontainers via [envbuilder](https://github.com/coder/envbuilder), an open source project. Read more about this in [Coder's documentation](https://coder.com/docs/templates/dev-containers). + +This template provisions the following resources: + +- Envbuilder cached image (conditional, persistent) using [`terraform-provider-envbuilder`](https://github.com/coder/terraform-provider-envbuilder) +- Docker image (persistent) using [`envbuilder`](https://github.com/coder/envbuilder) +- Docker container (ephemeral) +- Docker volume (persistent on `/workspaces`) + +The Git repository is cloned inside the `/workspaces` volume if not present. +Any local changes to the Devcontainer files inside the volume will be applied when you restart the workspace. +Keep in mind that any tools or files outside of `/workspaces` or not added as part of the Devcontainer specification are not persisted. +Edit the `devcontainer.json` instead! + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## Docker-in-Docker + +See the [Envbuilder documentation](https://github.com/coder/envbuilder/blob/main/docs/docker.md) for information on running Docker containers inside a devcontainer built by Envbuilder. + +## 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. + +For example, you can run a local registry: + +```shell +docker run --detach \ + --volume registry-cache:/var/lib/registry \ + --publish 5000:5000 \ + --name registry-cache \ + --net=host \ + registry:2 +``` + +Then, when creating the template, enter `localhost:5000/devcontainer-cache` for the parameter `cache_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 the registry cache, specify the variable `cache_repo_docker_config_path` +> with the path to a Docker config `.json` on disk containing valid credentials for the registry. diff --git a/registry/coder/templates/docker-devcontainer/main.tf b/registry/coder/templates/docker-devcontainer/main.tf new file mode 100644 index 000000000..2765874f8 --- /dev/null +++ b/registry/coder/templates/docker-devcontainer/main.tf @@ -0,0 +1,372 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 2.0" + } + docker = { + source = "kreuzwerker/docker" + } + envbuilder = { + source = "coder/envbuilder" + } + } +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +provider "coder" {} +provider "docker" { + # Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default + host = var.docker_socket != "" ? var.docker_socket : null +} +provider "envbuilder" {} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "repo" { + description = "Select a repository to automatically clone and start working with a devcontainer." + display_name = "Repository (auto)" + mutable = true + name = "repo" + option { + name = "vercel/next.js" + description = "The React Framework" + value = "https://github.com/vercel/next.js" + } + option { + name = "home-assistant/core" + description = "🏡 Open source home automation that puts local control and privacy first." + value = "https://github.com/home-assistant/core" + } + option { + name = "discourse/discourse" + description = "A platform for community discussion. Free, open, simple." + value = "https://github.com/discourse/discourse" + } + option { + name = "denoland/deno" + description = "A modern runtime for JavaScript and TypeScript." + value = "https://github.com/denoland/deno" + } + option { + name = "microsoft/vscode" + icon = "/icon/code.svg" + description = "Code editing. Redefined." + value = "https://github.com/microsoft/vscode" + } + option { + name = "Custom" + icon = "/emojis/1f5c3.png" + description = "Specify a custom repo URL below" + value = "custom" + } + order = 1 +} + +data "coder_parameter" "custom_repo_url" { + default = "" + description = "Optionally enter a custom repository URL, see [awesome-devcontainers](https://github.com/manekinekko/awesome-devcontainers)." + display_name = "Repository URL (custom)" + name = "custom_repo_url" + mutable = true + order = 2 +} + +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_parameter" "devcontainer_builder" { + description = <<-EOF +Image that will build the devcontainer. +We highly recommend using a specific release as the `:latest` tag will change. +Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder +EOF + display_name = "Devcontainer Builder" + mutable = true + name = "devcontainer_builder" + default = "ghcr.io/coder/envbuilder:latest" + order = 4 +} + +variable "cache_repo" { + default = "" + description = "(Optional) Use a container registry as a cache to speed up builds." + type = string +} + +variable "insecure_cache_repo" { + default = false + description = "Enable this option if your cache registry does not serve HTTPS." + type = bool +} + +variable "cache_repo_docker_config_path" { + default = "" + description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required." + sensitive = true + type = string +} + +locals { + container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value + 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 + repo_url = data.coder_parameter.repo.value == "custom" ? data.coder_parameter.custom_repo_url.value : data.coder_parameter.repo.value + # The envbuilder provider requires a key-value map of environment variables. + envbuilder_env = { + # ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider + # if the cache repo is enabled. + "ENVBUILDER_GIT_URL" : local.repo_url, + "ENVBUILDER_CACHE_REPO" : var.cache_repo, + "CODER_AGENT_TOKEN" : coder_agent.main.token, + # Use the docker gateway if the access URL is 127.0.0.1 + "CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + # Use the docker gateway if the access URL is 127.0.0.1 + "ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + "ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, + "ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""), + "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true", + "ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}", + } + # Convert the above map to the format expected by the docker provider. + docker_env = [ + for k, v in local.envbuilder_env : "${k}=${v}" + ] +} + +data "local_sensitive_file" "cache_repo_dockerconfigjson" { + count = var.cache_repo_docker_config_path == "" ? 0 : 1 + filename = var.cache_repo_docker_config_path +} + +resource "docker_image" "devcontainer_builder_image" { + name = local.devcontainer_builder_image + keep_locally = true +} + +resource "docker_volume" "workspaces" { + name = "coder-${data.coder_workspace.me.id}" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } + # Add labels in Docker to keep track of orphan resources. + 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 + } + # This field becomes outdated if the workspace is renamed but can + # be useful for debugging or cleaning out dangling volumes. + labels { + label = "coder.workspace_name_at_creation" + value = data.coder_workspace.me.name + } +} + +# 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 = local.repo_url + cache_repo = var.cache_repo + extra_env = local.envbuilder_env + insecure = var.insecure_cache_repo +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = data.coder_workspace.me.name + # Use the environment specified by the envbuilder provider, if available. + env = var.cache_repo == "" ? local.docker_env : envbuilder_cached_image.cached.0.env + # network_mode = "host" # Uncomment if testing with a registry running on `localhost`. + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/workspaces" + volume_name = docker_volume.workspaces.name + read_only = false + } + # Add labels in Docker to keep track of orphan resources. + 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 + } +} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + dir = "/workspaces" + + # These environment variables allow you to make Git commits right away after creating a + # workspace. Note that they take precedence over configuration defined in ~/.gitconfig! + # You can remove this block if you'd prefer to configure Git manually or using + # dotfiles. (see docs/dotfiles.md) + env = { + GIT_AUTHOR_NAME = local.git_author_name + GIT_AUTHOR_EMAIL = local.git_author_email + GIT_COMMITTER_NAME = local.git_author_name + GIT_COMMITTER_EMAIL = local.git_author_email + } + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + 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 = "3_home_disk" + script = "coder stat disk --path $HOME" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = < + +## Prerequisites + +### Infrastructure + +The VM you run Coder on must have a running Docker socket and the `coder` user must be added to the Docker group: + +```sh +# Add coder user to Docker group +sudo adduser coder docker + +# Restart Coder server +sudo systemctl restart coder + +# Test Docker +sudo -u coder docker ps +``` + +## Architecture + +This template provisions the following resources: + +- Docker image (built by Docker socket and kept locally) +- Docker container pod (ephemeral) +- Docker volume (persistent on `/home/coder`) + +This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +### Editing the image + +Edit the `Dockerfile` and run `coder templates push` to update workspaces. diff --git a/registry/coder/templates/docker/main.tf b/registry/coder/templates/docker/main.tf new file mode 100644 index 000000000..234c43382 --- /dev/null +++ b/registry/coder/templates/docker/main.tf @@ -0,0 +1,220 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + docker = { + source = "kreuzwerker/docker" + } + } +} + +locals { + username = data.coder_workspace_owner.me.name +} + +variable "docker_socket" { + default = "" + description = "(Optional) Docker socket URI" + type = string +} + +provider "docker" { + # Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default + host = var.docker_socket != "" ? var.docker_socket : null +} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + set -e + + # Prepare user home with default files on first start. + if [ ! -f ~/.init_done ]; then + cp -rT /etc/skel ~ + touch ~/.init_done + fi + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + + # These environment variables allow you to make Git commits right away after creating a + # workspace. Note that they take precedence over configuration defined in ~/.gitconfig! + # You can remove this block if you'd prefer to configure Git manually or using + # dotfiles. (see docs/dotfiles.md) + 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}" + } + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + 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 = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = < **Create new key**. + +1. Generate a **JSON private key**, which will be what you provide to Coder + during the setup process. + +## Architecture + +This template provisions the following resources: + +- Envbuilder cached image (conditional, persistent) using [`terraform-provider-envbuilder`](https://github.com/coder/terraform-provider-envbuilder) +- GCP VM (persistent) with a running Docker daemon +- GCP Disk (persistent, mounted to root) +- [Envbuilder container](https://github.com/coder/envbuilder) inside the GCP VM + +Coder persists the root volume. The full filesystem is preserved when the workspace restarts. +When the GCP VM starts, a startup script runs that ensures a running Docker daemon, and starts +an Envbuilder container using this Docker daemon. The Docker socket is also mounted inside the container to allow running Docker containers inside the workspace. + +> **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 the registry cache, 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. Please check [Coder Registry](https://registry.coder.com) for a list of all modules and templates. diff --git a/registry/coder/templates/gcp-devcontainer/architecture.svg b/registry/coder/templates/gcp-devcontainer/architecture.svg new file mode 100644 index 000000000..c3dfd6455 --- /dev/null +++ b/registry/coder/templates/gcp-devcontainer/architecture.svg @@ -0,0 +1,8 @@ +GCPGCPHostingHostingVirtual MachineVirtual MachineLinux HardwareLinux HardwareCoder WorkspaceCoder WorkspaceDevcontainerDevcontainerenvbuilder created filesystemenvbuilder created filesystemA Clone of your repoA Clone of your repoSource codeSource codeLanguagesLanguagesPython. Go, etcPython. Go, etcToolingToolingExtensions, linting, formatting, etcExtensions, linting, formatting, etcCPUsCPUsDisk StorageDisk StorageCode EditorCode EditorVS Code DesktopVS Code DesktopLocal InstallationLocal InstallationVS Code DesktopVS Code DesktopLocal InstallationLocal Installationcode-servercode-serverA web IDEA web IDEJetBrains GatewayJetBrains GatewayLocal InstallationLocal InstallationCommand LineCommand LineSSH via Coder CLISSH via Coder CLI \ No newline at end of file diff --git a/registry/coder/templates/gcp-devcontainer/main.tf b/registry/coder/templates/gcp-devcontainer/main.tf new file mode 100644 index 000000000..317a22fcc --- /dev/null +++ b/registry/coder/templates/gcp-devcontainer/main.tf @@ -0,0 +1,341 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + google = { + source = "hashicorp/google" + } + envbuilder = { + source = "coder/envbuilder" + } + } +} + +provider "coder" {} + +provider "google" { + zone = module.gcp_region.value + project = var.project_id +} + +data "google_compute_default_service_account" "default" {} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" +} + +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 +} + +# See https://registry.coder.com/modules/coder/gcp-region +module "gcp_region" { + source = "registry.coder.com/coder/gcp-region/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + regions = ["us", "europe"] +} + +data "coder_parameter" "instance_type" { + name = "instance_type" + display_name = "Instance Type" + description = "Select an instance type for your workspace." + type = "string" + mutable = false + order = 2 + default = "e2-micro" + option { + name = "e2-micro (2C, 1G)" + value = "e2-micro" + } + option { + name = "e2-small (2C, 2G)" + value = "e2-small" + } + option { + name = "e2-medium (2C, 2G)" + value = "e2-medium" + } +} + +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_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" { + name = "repo_url" + display_name = "Repository URL" + default = "https://github.com/coder/envbuilder-starter-devcontainer" + description = "Repository URL" + mutable = true +} + +data "local_sensitive_file" "cache_repo_dockerconfigjson" { + count = var.cache_repo_docker_config_path == "" ? 0 : 1 + filename = var.cache_repo_docker_config_path +} + +# Be careful when modifying the below locals! +locals { + # Ensure Coder username is a valid Linux username + linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32)) + # 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) + + # The GCP VM needs a startup script to set up the environment and start the container. Defining this here. + # NOTE: make sure to test changes by uncommenting the local_file resource at the bottom of this file + # and running `terraform apply` to see the generated script. You should also run shellcheck on the script + # to ensure it is valid. + startup_script = <<-META + #!/usr/bin/env sh + set -eux + + # If user does not exist, create it and set up passwordless sudo + if ! id -u "${local.linux_user}" >/dev/null 2>&1; then + useradd -m -s /bin/bash "${local.linux_user}" + echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user + fi + + # Check for Docker, install if not present + if ! command -v docker >/dev/null 2>&1; then + echo "Docker not found, installing..." + curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh >/dev/null 2>&1 + sudo usermod -aG docker ${local.linux_user} + newgrp docker + else + echo "Docker is already installed." + fi + + # Write the Docker config JSON to disk if it is provided. + if [ -n "${local.docker_config_json_base64}" ]; then + mkdir -p "/home/${local.linux_user}/.docker" + printf "%s" "${local.docker_config_json_base64}" | base64 -d | tee "/home/${local.linux_user}/.docker/config.json" + chown -R ${local.linux_user}:${local.linux_user} "/home/${local.linux_user}/.docker" + fi + + # 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 \ + --net=host \ + -h ${lower(data.coder_workspace.me.name)} \ + -v /home/${local.linux_user}/envbuilder:/workspaces \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --env-file /home/${local.linux_user}/env.txt \ + ${local.builder_image} + META +} + +# Create a persistent disk to store the workspace data. +resource "google_compute_disk" "root" { + name = "coder-${data.coder_workspace.me.id}-root" + type = "pd-ssd" + image = "debian-cloud/debian-12" + lifecycle { + ignore_changes = all + } +} + +# 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.startup_script +# filename = "${path.module}/startup_script.sh" +# } + +# Create a VM where the workspace will run. +resource "google_compute_instance" "vm" { + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root" + machine_type = data.coder_parameter.instance_type.value + # data.coder_workspace_owner.me.name == "default" is a workaround to suppress error in the terraform plan phase while creating a new workspace. + desired_status = (data.coder_workspace_owner.me.name == "default" || data.coder_workspace.me.start_count == 1) ? "RUNNING" : "TERMINATED" + + network_interface { + network = "default" + access_config { + // Ephemeral public IP + } + } + + boot_disk { + auto_delete = false + source = google_compute_disk.root.name + } + + service_account { + email = data.google_compute_default_service_account.default.email + scopes = ["cloud-platform"] + } + + metadata = { + # The startup script runs as root with no $HOME environment set up, so instead of directly + # running the agent init script, create a user (with a homedir, default shell and sudo + # permissions) and execute the init script as that user. + startup-script = local.startup_script + } +} + +# Create a Coder agent to manage the workspace. +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" + } + metadata { + key = "disk" + display_name = "Disk Usage" + interval = 5 + timeout = 5 + script = "coder stat disk" + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + order = 1 +} + +# See https://registry.coder.com/modules/coder/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/workspaces" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + agent_name = "main" + order = 2 +} + +# Create metadata for the workspace and home disk. +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = google_compute_instance.vm.id + + item { + key = "type" + value = google_compute_instance.vm.machine_type + } + + item { + key = "zone" + value = module.gcp_region.value + } +} + +resource "coder_metadata" "home_info" { + resource_id = google_compute_disk.root.id + + item { + key = "size" + value = "${google_compute_disk.root.size} GiB" + } +} diff --git a/registry/coder/templates/gcp-linux/README.md b/registry/coder/templates/gcp-linux/README.md new file mode 100644 index 000000000..60191040e --- /dev/null +++ b/registry/coder/templates/gcp-linux/README.md @@ -0,0 +1,64 @@ +--- +display_name: Google Compute Engine (Linux) +description: Provision Google Compute Engine instances as Coder workspaces +icon: ../../../../.icons/gcp.svg +maintainer_github: coder +verified: true +tags: [vm, linux, gcp] +--- + +# Remote Development on Google Compute Engine (Linux) + +## Prerequisites + +### Authentication + +This template assumes that coderd is run in an environment that is authenticated +with Google Cloud. For example, run `gcloud auth application-default login` to +import credentials on the system and user running coderd. For other ways to +authenticate [consult the Terraform +docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials). + +Coder requires a Google Cloud Service Account to provision workspaces. To create +a service account: + +1. Navigate to the [CGP + console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create), + and select your Cloud project (if you have more than one project associated + with your account) + +1. Provide a service account name (this name is used to generate the service + account ID) + +1. Click **Create and continue**, and choose the following IAM roles to grant to + the service account: + + - Compute Admin + - Service Account User + + Click **Continue**. + +1. Click on the created key, and navigate to the **Keys** tab. + +1. Click **Add key** > **Create new key**. + +1. Generate a **JSON private key**, which will be what you provide to Coder + during the setup process. + +## Architecture + +This template provisions the following resources: + +- GCP VM (ephemeral) +- GCP Disk (persistent, mounted to root) + +Coder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. diff --git a/registry/coder/templates/gcp-linux/main.tf b/registry/coder/templates/gcp-linux/main.tf new file mode 100644 index 000000000..286db4e41 --- /dev/null +++ b/registry/coder/templates/gcp-linux/main.tf @@ -0,0 +1,184 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + google = { + source = "hashicorp/google" + } + } +} + +provider "coder" {} + +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" +} + +# See https://registry.coder.com/modules/coder/gcp-region +module "gcp_region" { + source = "registry.coder.com/coder/gcp-region/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + regions = ["us", "europe"] + default = "us-central1-a" +} + +provider "google" { + zone = module.gcp_region.value + project = var.project_id +} + +data "google_compute_default_service_account" "default" {} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "google_compute_disk" "root" { + name = "coder-${data.coder_workspace.me.id}-root" + type = "pd-ssd" + zone = module.gcp_region.value + image = "debian-cloud/debian-11" + lifecycle { + ignore_changes = [name, image] + } +} + +resource "coder_agent" "main" { + auth = "google-instance-identity" + arch = "amd64" + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + + metadata { + key = "cpu" + display_name = "CPU Usage" + interval = 5 + timeout = 5 + script = <<-EOT + #!/bin/bash + set -e + top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "%"}' + EOT + } + metadata { + key = "memory" + display_name = "Memory Usage" + interval = 5 + timeout = 5 + script = <<-EOT + #!/bin/bash + set -e + free -m | awk 'NR==2{printf "%.2f%%\t", $3*100/$2 }' + EOT + } + metadata { + key = "disk" + display_name = "Disk Usage" + interval = 600 # every 10 minutes + timeout = 30 # df can take a while on large filesystems + script = <<-EOT + #!/bin/bash + set -e + df /home/coder | awk '$NF=="/"{printf "%s", $5}' + EOT + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + order = 1 +} + +# See https://registry.coder.com/modules/coder/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/home/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + agent_name = "main" + order = 2 +} + +resource "google_compute_instance" "dev" { + zone = module.gcp_region.value + count = data.coder_workspace.me.start_count + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root" + machine_type = "e2-medium" + network_interface { + network = "default" + access_config { + // Ephemeral public IP + } + } + boot_disk { + auto_delete = false + source = google_compute_disk.root.name + } + service_account { + email = data.google_compute_default_service_account.default.email + scopes = ["cloud-platform"] + } + # The startup script runs as root with no $HOME environment set up, so instead of directly + # running the agent init script, create a user (with a homedir, default shell and sudo + # permissions) and execute the init script as that user. + metadata_startup_script = </dev/null 2>&1; then + useradd -m -s /bin/bash "${local.linux_user}" + echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user +fi + +exec sudo -u "${local.linux_user}" sh -c '${coder_agent.main.init_script}' +EOMETA +} + +locals { + # Ensure Coder username is a valid Linux username + linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32)) +} + +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = google_compute_instance.dev[0].id + + item { + key = "type" + value = google_compute_instance.dev[0].machine_type + } +} + +resource "coder_metadata" "home_info" { + resource_id = google_compute_disk.root.id + + item { + key = "size" + value = "${google_compute_disk.root.size} GiB" + } +} diff --git a/registry/coder/templates/gcp-vm-container/README.md b/registry/coder/templates/gcp-vm-container/README.md new file mode 100644 index 000000000..83704ee25 --- /dev/null +++ b/registry/coder/templates/gcp-vm-container/README.md @@ -0,0 +1,65 @@ +--- +display_name: Google Compute Engine (VM Container) +description: Provision Google Compute Engine instances as Coder workspaces +icon: ../../../../.icons/gcp.svg +maintainer_github: coder +verified: true +tags: [vm-container, linux, gcp] +--- + +# Remote Development on Google Compute Engine (VM Container) + +## Prerequisites + +### Authentication + +This template assumes that coderd is run in an environment that is authenticated +with Google Cloud. For example, run `gcloud auth application-default login` to +import credentials on the system and user running coderd. For other ways to +authenticate [consult the Terraform +docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials). + +Coder requires a Google Cloud Service Account to provision workspaces. To create +a service account: + +1. Navigate to the [CGP + console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create), + and select your Cloud project (if you have more than one project associated + with your account) + +1. Provide a service account name (this name is used to generate the service + account ID) + +1. Click **Create and continue**, and choose the following IAM roles to grant to + the service account: + + - Compute Admin + - Service Account User + + Click **Continue**. + +1. Click on the created key, and navigate to the **Keys** tab. + +1. Click **Add key** > **Create new key**. + +1. Generate a **JSON private key**, which will be what you provide to Coder + during the setup process. + +## Architecture + +This template provisions the following resources: + +- GCP VM (ephemeral, deleted on stop) + - Container in VM +- Managed disk (persistent, mounted to `/home/coder` in container) + +This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image, or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. diff --git a/registry/coder/templates/gcp-vm-container/main.tf b/registry/coder/templates/gcp-vm-container/main.tf new file mode 100644 index 000000000..b259b4b22 --- /dev/null +++ b/registry/coder/templates/gcp-vm-container/main.tf @@ -0,0 +1,136 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + google = { + source = "hashicorp/google" + } + } +} + +provider "coder" {} + +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" +} + +# https://registry.coder.com/modules/coder/gcp-region/coder +module "gcp_region" { + source = "registry.coder.com/coder/gcp-region/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + regions = ["us", "europe"] +} + +provider "google" { + zone = module.gcp_region.value + project = var.project_id +} + +data "google_compute_default_service_account" "default" {} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + auth = "google-instance-identity" + arch = "amd64" + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + order = 1 +} + +# See https://registry.coder.com/modules/coder/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = "/home/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + agent_id = coder_agent.main.id + agent_name = "main" + order = 2 +} + +# See https://registry.terraform.io/modules/terraform-google-modules/container-vm +module "gce-container" { + source = "terraform-google-modules/container-vm/google" + version = "3.0.0" + + container = { + image = "codercom/enterprise-base:ubuntu" + command = ["sh"] + args = ["-c", coder_agent.main.init_script] + securityContext = { + privileged : true + } + } +} + +resource "google_compute_instance" "dev" { + zone = module.gcp_region.value + count = data.coder_workspace.me.start_count + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}" + machine_type = "e2-medium" + network_interface { + network = "default" + access_config { + // Ephemeral public IP + } + } + boot_disk { + initialize_params { + image = module.gce-container.source_image + } + } + service_account { + email = data.google_compute_default_service_account.default.email + scopes = ["cloud-platform"] + } + metadata = { + "gce-container-declaration" = module.gce-container.metadata_value + } + labels = { + container-vm = module.gce-container.vm_container_label + } +} + +resource "coder_agent_instance" "dev" { + count = data.coder_workspace.me.start_count + agent_id = coder_agent.main.id + instance_id = google_compute_instance.dev[0].instance_id +} + +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = google_compute_instance.dev[0].id + + item { + key = "image" + value = module.gce-container.container.image + } +} diff --git a/registry/coder/templates/gcp-windows/README.md b/registry/coder/templates/gcp-windows/README.md new file mode 100644 index 000000000..ac717e414 --- /dev/null +++ b/registry/coder/templates/gcp-windows/README.md @@ -0,0 +1,64 @@ +--- +display_name: Google Compute Engine (Windows) +description: Provision Google Compute Engine instances as Coder workspaces +icon: ../../../../.icons/gcp.svg +maintainer_github: coder +verified: true +tags: [vm, windows, gcp] +--- + +# Remote Development on Google Compute Engine (Windows) + +## Prerequisites + +### Authentication + +This template assumes that coderd is run in an environment that is authenticated +with Google Cloud. For example, run `gcloud auth application-default login` to +import credentials on the system and user running coderd. For other ways to +authenticate [consult the Terraform +docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/getting_started#adding-credentials). + +Coder requires a Google Cloud Service Account to provision workspaces. To create +a service account: + +1. Navigate to the [CGP + console](https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create), + and select your Cloud project (if you have more than one project associated + with your account) + +1. Provide a service account name (this name is used to generate the service + account ID) + +1. Click **Create and continue**, and choose the following IAM roles to grant to + the service account: + + - Compute Admin + - Service Account User + + Click **Continue**. + +1. Click on the created key, and navigate to the **Keys** tab. + +1. Click **Add key** > **Create new key**. + +1. Generate a **JSON private key**, which will be what you provide to Coder + during the setup process. + +## Architecture + +This template provisions the following resources: + +- GCP VM (ephemeral) +- GCP Disk (persistent, mounted to root) + +Coder persists the root volume. The full filesystem is preserved when the workspace restarts. See this [community example](https://github.com/bpmct/coder-templates/tree/main/aws-linux-ephemeral) of an ephemeral AWS instance. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## code-server + +`code-server` is installed via the `startup_script` argument in the `coder_agent` +resource block. The `coder_app` resource is defined to access `code-server` through +the dashboard UI over `localhost:13337`. diff --git a/registry/coder/templates/gcp-windows/main.tf b/registry/coder/templates/gcp-windows/main.tf new file mode 100644 index 000000000..aea409eee --- /dev/null +++ b/registry/coder/templates/gcp-windows/main.tf @@ -0,0 +1,96 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + google = { + source = "hashicorp/google" + } + } +} + +provider "coder" {} + +variable "project_id" { + description = "Which Google Compute Project should your workspace live in?" +} + +# See https://registry.coder.com/modules/coder/gcp-region +module "gcp_region" { + source = "registry.coder.com/coder/gcp-region/coder" + + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + + regions = ["us", "europe"] + default = "us-central1-a" +} + +provider "google" { + zone = module.gcp_region.value + project = var.project_id +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "google_compute_default_service_account" "default" {} + +resource "google_compute_disk" "root" { + name = "coder-${data.coder_workspace.me.id}-root" + type = "pd-ssd" + zone = module.gcp_region.value + image = "projects/windows-cloud/global/images/windows-server-2022-dc-core-v20220215" + lifecycle { + ignore_changes = [name, image] + } +} + +resource "coder_agent" "main" { + auth = "google-instance-identity" + arch = "amd64" + os = "windows" +} + +resource "google_compute_instance" "dev" { + zone = module.gcp_region.value + count = data.coder_workspace.me.start_count + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}" + machine_type = "e2-medium" + network_interface { + network = "default" + access_config { + // Ephemeral public IP + } + } + boot_disk { + auto_delete = false + source = google_compute_disk.root.name + } + service_account { + email = data.google_compute_default_service_account.default.email + scopes = ["cloud-platform"] + } + metadata = { + windows-startup-script-ps1 = coder_agent.main.init_script + serial-port-enable = "TRUE" + } +} +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = google_compute_instance.dev[0].id + + item { + key = "type" + value = google_compute_instance.dev[0].machine_type + } +} + +resource "coder_metadata" "home_info" { + resource_id = google_compute_disk.root.id + + item { + key = "size" + value = "${google_compute_disk.root.size} GiB" + } +} diff --git a/registry/coder/templates/incus/README.md b/registry/coder/templates/incus/README.md new file mode 100644 index 000000000..def594cdb --- /dev/null +++ b/registry/coder/templates/incus/README.md @@ -0,0 +1,51 @@ +--- +display_name: Incus System Container with Docker +description: Develop in an Incus System Container with Docker using incus +icon: ../../../../.icons/lxc.svg +maintainer_github: coder +verified: true +tags: [local, incus, lxc, lxd] +--- + +# Incus System Container with Docker + +Develop in an Incus System Container and run nested Docker containers using Incus on your local infrastructure. + +## Prerequisites + +1. Install [Incus](https://linuxcontainers.org/incus/) on the same machine as Coder. +2. Allow Coder to access the Incus socket. + + - If you're running Coder as system service, run `sudo usermod -aG incus-admin coder` and restart the Coder service. + - If you're running Coder as a Docker Compose service, get the group ID of the `incus-admin` group by running `getent group incus-admin` and add the following to your `compose.yaml` file: + + ```yaml + services: + coder: + volumes: + - /var/lib/incus/unix.socket:/var/lib/incus/unix.socket + group_add: + - 996 # Replace with the group ID of the `incus-admin` group + ``` + +3. Create a storage pool named `coder` and `btrfs` as the driver by running `incus storage create coder btrfs`. + +## Usage + +> **Note:** this template requires using a container image with cloud-init installed such as `ubuntu/jammy/cloud/amd64`. + +1. Run `coder templates init -id incus` +1. Select this template +1. Follow the on-screen instructions + +## Extending this template + +See the [lxc/incus](https://registry.terraform.io/providers/lxc/incus/latest/docs) Terraform provider documentation to +add the following features to your Coder template: + +- HTTPS incus host +- Volume mounts +- Custom networks +- More + +We also welcome contributions! diff --git a/registry/coder/templates/incus/main.tf b/registry/coder/templates/incus/main.tf new file mode 100644 index 000000000..95e10a6d2 --- /dev/null +++ b/registry/coder/templates/incus/main.tf @@ -0,0 +1,317 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + incus = { + source = "lxc/incus" + } + } +} + +data "coder_provisioner" "me" {} + +provider "incus" {} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "image" { + name = "image" + display_name = "Image" + description = "The container image to use. Be sure to use a variant with cloud-init installed!" + default = "ubuntu/jammy/cloud/amd64" + icon = "/icon/image.svg" + mutable = true +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPUs to allocate to the workspace (1-8)" + type = "number" + default = "1" + icon = "https://raw.githubusercontent.com/matifali/logos/main/cpu-3.svg" + mutable = true + validation { + min = 1 + max = 8 + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory to allocate to the workspace in GB (up to 16GB)" + type = "number" + default = "2" + icon = "/icon/memory.svg" + mutable = true + validation { + min = 1 + max = 16 + } +} + +data "coder_parameter" "git_repo" { + type = "string" + name = "Git repository" + default = "https://github.com/coder/coder" + description = "Clone a git repo into [base directory]" + mutable = true +} + +data "coder_parameter" "repo_base_dir" { + type = "string" + name = "Repository Base Directory" + default = "~" + description = "The directory specified will be created (if missing) and the specified repo will be cloned into [base directory]/{repo}🪄." + mutable = true +} + +resource "coder_agent" "main" { + count = data.coder_workspace.me.start_count + arch = data.coder_provisioner.me.arch + os = "linux" + dir = "/home/${local.workspace_user}" + env = { + CODER_WORKSPACE_ID = data.coder_workspace.me.id + } + + 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 = "3_home_disk" + script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}" + interval = 60 + timeout = 1 + } +} + +# https://registry.coder.com/modules/coder/git-clone +module "git-clone" { + source = "registry.coder.com/coder/git-clone/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + agent_id = local.agent_id + url = data.coder_parameter.git_repo.value + base_dir = local.repo_base_dir +} + +# https://registry.coder.com/modules/coder/code-server +module "code-server" { + source = "registry.coder.com/coder/code-server/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + agent_id = local.agent_id + folder = local.repo_base_dir +} + +# https://registry.coder.com/modules/coder/filebrowser +module "filebrowser" { + source = "registry.coder.com/coder/filebrowser/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + agent_id = local.agent_id +} + +# https://registry.coder.com/modules/coder/coder-login +module "coder-login" { + source = "registry.coder.com/coder/coder-login/coder" + # This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = "~> 1.0" + agent_id = local.agent_id +} + +resource "incus_volume" "home" { + name = "coder-${data.coder_workspace.me.id}-home" + pool = local.pool +} + +resource "incus_volume" "docker" { + name = "coder-${data.coder_workspace.me.id}-docker" + pool = local.pool +} + +resource "incus_cached_image" "image" { + source_remote = "images" + source_image = data.coder_parameter.image.value +} + +resource "incus_instance_file" "agent_token" { + count = data.coder_workspace.me.start_count + instance = incus_instance.dev.name + content = < **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`. + +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 the registry cache, specify the variable `cache_repo_dockerconfig_secret` +> with the name of a Kubernetes secret in the same namespace as Coder. The secret must contain the key `.dockerconfigjson`. diff --git a/registry/coder/templates/kubernetes-devcontainer/main.tf b/registry/coder/templates/kubernetes-devcontainer/main.tf new file mode 100644 index 000000000..8fc79fa25 --- /dev/null +++ b/registry/coder/templates/kubernetes-devcontainer/main.tf @@ -0,0 +1,464 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + envbuilder = { + source = "coder/envbuilder" + } + } +} + +provider "coder" {} +provider "kubernetes" { + # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} +provider "envbuilder" {} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + default = "default" + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace." +} + +variable "cache_repo" { + default = "" + description = "Use a container registry as a cache to speed up builds." + type = string +} + +variable "insecure_cache_repo" { + default = false + description = "Enable this option if your cache registry does not serve HTTPS." + type = bool +} + +data "coder_parameter" "cpu" { + type = "number" + name = "cpu" + display_name = "CPU" + description = "CPU limit (cores)." + default = "2" + icon = "/emojis/1f5a5.png" + mutable = true + validation { + min = 1 + max = 99999 + } + order = 1 +} + +data "coder_parameter" "memory" { + type = "number" + name = "memory" + display_name = "Memory" + description = "Memory limit (GiB)." + default = "2" + icon = "/icon/memory.svg" + mutable = true + validation { + min = 1 + max = 99999 + } + order = 2 +} + +data "coder_parameter" "workspaces_volume_size" { + name = "workspaces_volume_size" + display_name = "Workspaces volume size" + description = "Size of the `/workspaces` volume (GiB)." + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } + order = 3 +} + +data "coder_parameter" "repo" { + description = "Select a repository to automatically clone and start working with a devcontainer." + display_name = "Repository (auto)" + mutable = true + name = "repo" + order = 4 + type = "string" +} + +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 = 6 +} + +data "coder_parameter" "devcontainer_builder" { + description = <<-EOF +Image that will build the devcontainer. +We highly recommend using a specific release as the `:latest` tag will change. +Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder +EOF + display_name = "Devcontainer Builder" + mutable = true + name = "devcontainer_builder" + default = "ghcr.io/coder/envbuilder:latest" + order = 7 +} + +variable "cache_repo_secret_name" { + default = "" + description = "Path to a docker config.json containing credentials to the provided cache repo, if required." + sensitive = true + type = string +} + +data "kubernetes_secret" "cache_repo_dockerconfig_secret" { + count = var.cache_repo_secret_name == "" ? 0 : 1 + metadata { + name = var.cache_repo_secret_name + namespace = var.namespace + } +} + +locals { + deployment_name = "coder-${lower(data.coder_workspace.me.id)}" + devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value + 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 + repo_url = data.coder_parameter.repo.value + # The envbuilder provider requires a key-value map of environment variables. + envbuilder_env = { + "CODER_AGENT_TOKEN" : coder_agent.main.token, + # Use the docker gateway if the access URL is 127.0.0.1 + "CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + # ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider + # if the cache repo is enabled. + "ENVBUILDER_GIT_URL" : var.cache_repo == "" ? local.repo_url : "", + # Use the docker gateway if the access URL is 127.0.0.1 + "ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + "ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, + "ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")), + "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true" + # You may need to adjust this if you get an error regarding deleting files when building the workspace. + # For example, when testing in KinD, it was necessary to set `/product_name` and `/product_uuid` in + # addition to `/var/run`. + # "ENVBUILDER_IGNORE_PATHS": "/product_name,/product_uuid,/var/run", + } +} + +# 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 = local.repo_url + cache_repo = var.cache_repo + extra_env = local.envbuilder_env + insecure = var.insecure_cache_repo +} + +resource "kubernetes_persistent_volume_claim" "workspaces" { + metadata { + name = "coder-${lower(data.coder_workspace.me.id)}-workspaces" + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-${lower(data.coder_workspace.me.id)}-workspaces" + "app.kubernetes.io/instance" = "coder-${lower(data.coder_workspace.me.id)}-workspaces" + "app.kubernetes.io/part-of" = "coder" + //Coder-specific labels. + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + annotations = { + "com.coder.user.email" = data.coder_workspace_owner.me.email + } + } + wait_until_bound = false + spec { + access_modes = ["ReadWriteOnce"] + resources { + requests = { + storage = "${data.coder_parameter.workspaces_volume_size.value}Gi" + } + } + # storage_class_name = "local-path" # Configure the StorageClass to use here, if required. + } +} + +resource "kubernetes_deployment" "main" { + count = data.coder_workspace.me.start_count + depends_on = [ + kubernetes_persistent_volume_claim.workspaces + ] + wait_for_rollout = false + metadata { + name = local.deployment_name + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = local.deployment_name + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + annotations = { + "com.coder.user.email" = data.coder_workspace_owner.me.email + } + } + + spec { + replicas = 1 + selector { + match_labels = { + "app.kubernetes.io/name" = "coder-workspace" + } + } + strategy { + type = "Recreate" + } + + template { + metadata { + labels = { + "app.kubernetes.io/name" = "coder-workspace" + } + } + spec { + security_context {} + + container { + name = "dev" + image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image + image_pull_policy = "Always" + security_context {} + + # Set the environment using cached_image.cached.0.env if the cache repo is enabled. + # Otherwise, use the local.envbuilder_env. + # You could alternatively write the environment variables to a ConfigMap or Secret + # and use that as `env_from`. + dynamic "env" { + for_each = nonsensitive(var.cache_repo == "" ? local.envbuilder_env : envbuilder_cached_image.cached.0.env_map) + content { + name = env.key + value = env.value + } + } + + resources { + requests = { + "cpu" = "250m" + "memory" = "512Mi" + } + limits = { + "cpu" = "${data.coder_parameter.cpu.value}" + "memory" = "${data.coder_parameter.memory.value}Gi" + } + } + volume_mount { + mount_path = "/workspaces" + name = "workspaces" + read_only = false + } + } + + volume { + name = "workspaces" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim.workspaces.metadata.0.name + read_only = false + } + } + + affinity { + // This affinity attempts to spread out all workspace pods evenly across + // nodes. + pod_anti_affinity { + preferred_during_scheduling_ignored_during_execution { + weight = 1 + pod_affinity_term { + topology_key = "kubernetes.io/hostname" + label_selector { + match_expressions { + key = "app.kubernetes.io/name" + operator = "In" + values = ["coder-workspace"] + } + } + } + } + } + } + } + } + } +} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + dir = "/workspaces" + + # These environment variables allow you to make Git commits right away after creating a + # workspace. Note that they take precedence over configuration defined in ~/.gitconfig! + # You can remove this block if you'd prefer to configure Git manually or using + # dotfiles. (see docs/dotfiles.md) + env = { + GIT_AUTHOR_NAME = local.git_author_name + GIT_AUTHOR_EMAIL = local.git_author_email + GIT_COMMITTER_NAME = local.git_author_name + GIT_COMMITTER_EMAIL = local.git_author_email + } + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + 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 = "Workspaces Disk" + key = "3_workspaces_disk" + script = "coder stat disk --path /workspaces" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = < + +## Prerequisites + +### Infrastructure + +**Cluster**: This template requires an existing Kubernetes cluster + +**Container Image**: This template uses the [codercom/enterprise-base:ubuntu image](https://github.com/coder/enterprise-images/tree/main/images/base) with some dev tools preinstalled. To add additional tools, extend this image or build it yourself. + +### Authentication + +This template authenticates using a `~/.kube/config`, if present on the server, or via built-in authentication if the Coder provisioner is running on Kubernetes with an authorized ServiceAccount. To use another [authentication method](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs#authentication), edit the template. + +## Architecture + +This template provisions the following resources: + +- Kubernetes pod (ephemeral) +- Kubernetes persistent volume claim (persistent on `/home/coder`) + +This means, when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace (e.g. `python3`), modify the container image. Alternatively, individual developers can [personalize](https://coder.com/docs/dotfiles) their workspaces with dotfiles. + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. diff --git a/registry/coder/templates/kubernetes/main.tf b/registry/coder/templates/kubernetes/main.tf new file mode 100644 index 000000000..e1fdb12cb --- /dev/null +++ b/registry/coder/templates/kubernetes/main.tf @@ -0,0 +1,345 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + } +} + +provider "coder" { +} + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace." +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 Cores" + value = "2" + } + option { + name = "4 Cores" + value = "4" + } + option { + name = "6 Cores" + value = "6" + } + option { + name = "8 Cores" + value = "8" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_parameter" "home_disk_size" { + name = "home_disk_size" + display_name = "Home disk size" + description = "The size of the home disk in GB" + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } +} + +provider "kubernetes" { + # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + startup_script = <<-EOT + set -e + + # Install the latest code-server. + # Append "--version x.x.x" to install a specific version of code-server. + curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server + + # Start code-server in the background. + /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 & + EOT + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + 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 = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = < + +> **Note** +> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case. + +## Prerequisites + +- [Nomad](https://www.nomadproject.io/downloads) +- [Docker](https://docs.docker.com/get-docker/) + +## Setup + +### 1. Start the CSI Host Volume Plugin + +The CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace. + +1. Login to the Nomad server using SSH. + +2. Append the following stanza to your Nomad server configuration file and restart the nomad service. + + ```tf + plugin "docker" { + config { + allow_privileged = true + } + } + ``` + + ```shell + sudo systemctl restart nomad + ``` + +3. Create a file `hostpath.nomad` with following content: + + ```tf + job "hostpath-csi-plugin" { + datacenters = ["dc1"] + type = "system" + + group "csi" { + task "plugin" { + driver = "docker" + + config { + image = "registry.k8s.io/sig-storage/hostpathplugin:v1.10.0" + + args = [ + "--drivername=csi-hostpath", + "--v=5", + "--endpoint=${CSI_ENDPOINT}", + "--nodeid=node-${NOMAD_ALLOC_INDEX}", + ] + + privileged = true + } + + csi_plugin { + id = "hostpath" + type = "monolith" + mount_dir = "/csi" + } + + resources { + cpu = 256 + memory = 128 + } + } + } + } + ``` + +4. Run the job: + + ```shell + nomad job run hostpath.nomad + ``` + +### 2. Setup the Nomad Template + +1. Create the template by running the following command: + + ```shell + coder template init nomad-docker + cd nomad-docker + coder template push + ``` + +2. Set up Nomad server address and optional authentication: + +3. Create a new workspace and start developing. diff --git a/registry/coder/templates/nomad-docker/main.tf b/registry/coder/templates/nomad-docker/main.tf new file mode 100644 index 000000000..9fc508930 --- /dev/null +++ b/registry/coder/templates/nomad-docker/main.tf @@ -0,0 +1,193 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + nomad = { + source = "hashicorp/nomad" + } + } +} + +variable "nomad_provider_address" { + type = string + description = "Nomad provider address. e.g., http://IP:PORT" + default = "http://localhost:4646" +} + +variable "nomad_provider_http_auth" { + type = string + description = "Nomad provider http_auth in the form of `user:password`" + sensitive = true + default = "" +} + +provider "coder" {} + +provider "nomad" { + address = var.nomad_provider_address + http_auth = var.nomad_provider_http_auth == "" ? null : var.nomad_provider_http_auth + + # Fix reading the NOMAD_NAMESPACE and the NOMAD_REGION env var from the coder's allocation. + ignore_env_vars = { + "NOMAD_NAMESPACE" = true + "NOMAD_REGION" = true + } +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "1" + icon = "/icon/memory.svg" + mutable = true + option { + name = "1 Cores" + value = "1" + } + option { + name = "2 Cores" + value = "2" + } + option { + name = "3 Cores" + value = "3" + } + option { + name = "4 Cores" + value = "4" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + startup_script = <<-EOT + set -e + # install and start code-server + curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server + /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 & + EOT + + metadata { + display_name = "Load Average (Host)" + key = "load_host" + # get load avg scaled by number of cores + script = <