feat: add base templates for all major cloud providers (#26634)

Add 6 new base templates to the template builder, covering all major
cloud providers and platforms:

- **scratch**: Minimal starter template with only `coder_agent` and
metadata
- **aws-windows**: AWS EC2 Windows instances with PowerShell user_data
- **azure-linux**: Azure VMs with cloud-init and managed disk
persistence
- **gcp-linux**: Google Compute Engine Linux instances with persistent
disk
- **gcp-windows**: Google Compute Engine Windows instances
- **digitalocean-linux**: DigitalOcean Linux droplets with persistent
volumes

Each base includes `base.json`, `main.tf.tmpl`, `README.md` (with
prerequisite markers), and any static files (cloud-init configs). Tests
verify all 9 bases load, render without error, and produce valid
single-agent declarations.

`azure-windows` is deferred; it needs to be registered in the `examples`
package first.

Depends on #26633

> [!NOTE]
> This PR was authored by Coder Agents on behalf of @jeremyruppel.

---

NB: This is very much an agent-generated PR and draws completely from
base templates that exist in `examples/templates/`. The base.json files
are new, so review those, but don't spend any brain tokens on the
correctness of the terraform and supporting files: any issues there are
issues with the upstream example template
This commit is contained in:
Jeremy Ruppel
2026-06-30 18:26:31 -04:00
committed by GitHub
parent 6f0b81cfbc
commit c40b3b9bcb
30 changed files with 2837 additions and 25 deletions
@@ -0,0 +1,92 @@
---
display_name: AWS EC2 (Windows)
description: Provision AWS EC2 Windows VMs as Coder workspaces
icon: ../../../site/static/icon/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/user-guides/workspace-management) with this example template.
<!-- prerequisites:start -->
## 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"
}
}
}
]
}
```
<!-- prerequisites:end -->
## Architecture
This template provisions the following resources:
- AWS Instance
This template uses PowerShell user data to start and stop the Coder agent on the Windows VM. The workspace is fully persistent, meaning the full filesystem is preserved when the workspace restarts.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
@@ -0,0 +1,6 @@
{
"id": "aws-windows",
"display_name": "AWS EC2 (Windows)",
"os": "windows",
"default_context": {}
}
@@ -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 = <<EOT
<powershell>
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
${coder_agent.main.init_script}
</powershell>
<persist>true</persist>
EOT
user_data_end = <<EOT
<powershell>
shutdown /s
</powershell>
<persist>true</persist>
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"
}
}
@@ -0,0 +1,50 @@
---
display_name: Azure VM (Linux)
description: Provision Azure VMs as Coder workspaces
icon: ../../../site/static/icon/azure.png
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/user-guides/workspace-management) with this example template.
<!-- prerequisites:start -->
## 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=<id>`
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).
<!-- prerequisites:end -->
## Architecture
This template provisions the following resources:
- Azure VM (ephemeral, deleted on stop)
- Managed disk (persistent, mounted to `/home/coder`)
- Resource group, virtual network, subnet, and network interface (persistent, required by the managed disk and VM)
### What happens on stop
When a workspace is **stopped**, only the VM is destroyed. The managed disk, resource group, virtual network, subnet, and network interface all persist. This is by design. The managed disk retains your `/home/coder` data across workspace restarts, and the other resources remain because the disk depends on them.
This means you will see these Azure resources in your subscription even when a workspace is stopped. This is expected behavior.
### What happens on delete
When a workspace is **deleted**, all resources are destroyed, including the resource group, networking resources, and managed disk.
### Workspace restarts
Since the VM is ephemeral, any tools or files outside of the home directory are not persisted across restarts. 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/user-guides/workspace-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.
@@ -0,0 +1,6 @@
{
"id": "azure-linux",
"display_name": "Azure VM (Linux)",
"os": "linux",
"default_context": {}
}
@@ -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
@@ -0,0 +1,293 @@
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
}
}
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"
}
}
@@ -0,0 +1,54 @@
---
display_name: DigitalOcean Droplet (Linux)
description: Provision DigitalOcean Droplets as Coder workspaces
icon: ../../../site/static/icon/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/user-guides/workspace-management) with this example template.
<!-- prerequisites:start -->
## 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).
<!-- prerequisites:end -->
## 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.
@@ -0,0 +1,6 @@
{
"id": "digitalocean-linux",
"display_name": "DigitalOcean Droplet (Linux)",
"os": "linux",
"default_context": {}
}
@@ -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
@@ -0,0 +1,329 @@
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)}"
}
}
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"
}
}
@@ -0,0 +1,62 @@
---
display_name: Google Compute Engine (Linux)
description: Provision Google Compute Engine instances as Coder workspaces
icon: ../../../site/static/icon/gcp.png
maintainer_github: coder
verified: true
tags: [vm, linux, gcp]
---
# Remote Development on Google Compute Engine (Linux)
<!-- prerequisites:start -->
## 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.
<!-- prerequisites:end -->
## 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.
@@ -0,0 +1,6 @@
{
"id": "gcp-linux",
"display_name": "Google Compute Engine (Linux)",
"os": "linux",
"default_context": {}
}
@@ -0,0 +1,152 @@
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
}
}
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 = <<EOMETA
#!/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
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"
}
}
@@ -0,0 +1,62 @@
---
display_name: Google Compute Engine (Windows)
description: Provision Google Compute Engine instances as Coder workspaces
icon: ../../../site/static/icon/gcp.png
maintainer_github: coder
verified: true
tags: [vm, windows, gcp]
---
# Remote Development on Google Compute Engine (Windows)
<!-- prerequisites:start -->
## 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.
<!-- prerequisites:end -->
## 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.
@@ -0,0 +1,6 @@
{
"id": "gcp-windows",
"display_name": "Google Compute Engine (Windows)",
"os": "windows",
"default_context": {}
}
@@ -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"
}
}
@@ -0,0 +1,32 @@
---
display_name: Scratch
description: A minimal Coder workspace template with just an agent
icon: ../../../site/static/icon/coder.svg
maintainer_github: coder
verified: true
tags: [minimal, scratch]
---
# Scratch Template
A minimal template that provisions a Coder agent with basic metadata. Use this as a starting point when you want full control over the infrastructure and just need the agent scaffolding.
<!-- prerequisites:start -->
## Prerequisites
This template only provisions a Coder agent. You must provide your own compute platform (e.g. Docker, a VM, Kubernetes) for the agent to run on.
<!-- prerequisites:end -->
## Architecture
This template provisions the following resources:
- Coder agent (with CPU and RAM metadata)
- Environment variables for Git configuration
No infrastructure resources are included. Extend the template with your own compute resources.
> **Note**
> This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.
@@ -0,0 +1,6 @@
{
"id": "scratch",
"display_name": "Scratch",
"os": "linux",
"default_context": {}
}
@@ -0,0 +1,40 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = data.coder_provisioner.me.os
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
}
}
# Use this to set environment variables in your workspace
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env
resource "coder_env" "welcome_message" {
agent_id = coder_agent.main.id
name = "WELCOME_MESSAGE"
value = "Welcome to your Coder workspace!"
}
+44 -25
View File
@@ -8,23 +8,40 @@ import (
"github.com/coder/coder/v2/coderd/templatebuilder"
)
// allBaseIDs is the set of base template IDs expected in the catalog.
var allBaseIDs = []string{
"aws-linux",
"aws-windows",
"azure-linux",
"digitalocean-linux",
"docker",
"gcp-linux",
"gcp-windows",
"kubernetes",
"scratch",
}
func TestBaseTemplateOS(t *testing.T) {
t.Parallel()
t.Run("Docker", func(t *testing.T) {
t.Parallel()
require.Equal(t, templatebuilder.BaseOSLinux, templatebuilder.BaseTemplateOS("docker"))
})
linuxBases := []string{
"aws-linux", "azure-linux", "digitalocean-linux",
"docker", "gcp-linux", "kubernetes", "scratch",
}
for _, id := range linuxBases {
t.Run(id, func(t *testing.T) {
t.Parallel()
require.Equal(t, templatebuilder.BaseOSLinux, templatebuilder.BaseTemplateOS(id))
})
}
t.Run("Kubernetes", func(t *testing.T) {
t.Parallel()
require.Equal(t, templatebuilder.BaseOSLinux, templatebuilder.BaseTemplateOS("kubernetes"))
})
t.Run("AWSLinux", func(t *testing.T) {
t.Parallel()
require.Equal(t, templatebuilder.BaseOSLinux, templatebuilder.BaseTemplateOS("aws-linux"))
})
windowsBases := []string{"aws-windows", "gcp-windows"}
for _, id := range windowsBases {
t.Run(id, func(t *testing.T) {
t.Parallel()
require.Equal(t, templatebuilder.BaseOSWindows, templatebuilder.BaseTemplateOS(id))
})
}
t.Run("UnknownReturnsEmpty", func(t *testing.T) {
t.Parallel()
@@ -36,10 +53,10 @@ func TestBaseTemplateIDs(t *testing.T) {
t.Parallel()
ids := templatebuilder.BaseTemplateIDs()
require.Len(t, ids, 3)
require.Contains(t, ids, "docker")
require.Contains(t, ids, "kubernetes")
require.Contains(t, ids, "aws-linux")
require.Len(t, ids, len(allBaseIDs))
for _, id := range allBaseIDs {
require.Contains(t, ids, id)
}
}
func TestDefaultBaseRenderContext(t *testing.T) {
@@ -59,11 +76,16 @@ func TestDefaultBaseRenderContext(t *testing.T) {
require.Nil(t, rc.ImageOptions)
})
t.Run("AWSLinux", func(t *testing.T) {
t.Run("VMBasesHaveNoContainerImage", func(t *testing.T) {
t.Parallel()
rc := templatebuilder.DefaultBaseRenderContext("aws-linux")
require.Empty(t, rc.ContainerImage)
require.Nil(t, rc.ImageOptions)
vmBases := []string{
"aws-linux", "aws-windows", "azure-linux", "azure-windows",
"digitalocean-linux", "gcp-linux", "gcp-windows", "scratch",
}
for _, id := range vmBases {
rc := templatebuilder.DefaultBaseRenderContext(id)
require.Empty(t, rc.ContainerImage, "base %q should have no container image", id)
}
})
t.Run("Unknown", func(t *testing.T) {
@@ -74,12 +96,9 @@ func TestDefaultBaseRenderContext(t *testing.T) {
t.Run("AllBaseTemplatesHaveDefaults", func(t *testing.T) {
t.Parallel()
// Verify that every known base template produces a render
// context via DefaultBaseRenderContext (not just the zero value
// from an unknown ID). This catches forgotten entries.
for _, id := range templatebuilder.BaseTemplateIDs() {
rc := templatebuilder.DefaultBaseRenderContext(id)
_ = rc // existence is the assertion; the value varies per template
_ = rc
}
})
}
+21
View File
@@ -78,6 +78,27 @@ func TestCompose(t *testing.T) {
require.Contains(t, result.ExtraFiles, "cloud-init/userdata.sh.tftpl")
})
t.Run("GCPWindowsBase", func(t *testing.T) {
t.Parallel()
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
BaseTemplateID: "gcp-windows",
RegistryURL: "https://registry.coder.com",
})
require.NoError(t, err)
require.NotEmpty(t, result.MainTF)
require.Contains(t, string(result.MainTF), `resource "coder_agent" "main"`)
})
t.Run("AzureLinuxExtraFiles", func(t *testing.T) {
t.Parallel()
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
BaseTemplateID: "azure-linux",
RegistryURL: "https://registry.coder.com",
})
require.NoError(t, err)
require.Contains(t, result.ExtraFiles, "cloud-init/cloud-config.yaml.tftpl")
})
t.Run("SensitiveVariable", func(t *testing.T) {
t.Parallel()
result, err := templatebuilder.Compose(templatebuilder.ComposeRequest{
+24
View File
@@ -270,6 +270,24 @@ func TestModuleTemplateFS(t *testing.T) {
})
}
func TestAllBasesRenderAndExtractAgent(t *testing.T) {
t.Parallel()
for _, id := range templatebuilder.BaseTemplateIDs() {
t.Run(id, func(t *testing.T) {
t.Parallel()
renderCtx := templatebuilder.DefaultBaseRenderContext(id)
rendered, err := templatebuilder.RenderBaseTemplate(id, "main.tf.tmpl", renderCtx)
require.NoError(t, err, "base %q should render without error", id)
require.NotEmpty(t, rendered)
name, err := templatebuilder.ExtractAgentResourceName(rendered)
require.NoError(t, err, "base %q should have exactly one coder_agent", id)
require.NotEmpty(t, name)
})
}
}
func TestBaseTemplateSnapshot(t *testing.T) {
t.Parallel()
@@ -279,6 +297,12 @@ func TestBaseTemplateSnapshot(t *testing.T) {
{exampleID: "docker"},
{exampleID: "kubernetes"},
{exampleID: "aws-linux"},
{exampleID: "aws-windows"},
{exampleID: "azure-linux"},
{exampleID: "digitalocean-linux"},
{exampleID: "gcp-linux"},
{exampleID: "gcp-windows"},
{exampleID: "scratch"},
}
for _, tc := range tests {
+214
View File
@@ -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 = <<EOT
<powershell>
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
${coder_agent.main.init_script}
</powershell>
<persist>true</persist>
EOT
user_data_end = <<EOT
<powershell>
shutdown /s
</powershell>
<persist>true</persist>
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"
}
}
+293
View File
@@ -0,0 +1,293 @@
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
}
}
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"
}
}
@@ -0,0 +1,329 @@
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)}"
}
}
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"
}
}
+152
View File
@@ -0,0 +1,152 @@
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
}
}
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 = <<EOMETA
#!/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
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"
}
}
+96
View File
@@ -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"
}
}
+40
View File
@@ -0,0 +1,40 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.me.arch
os = data.coder_provisioner.me.os
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
}
}
# Use this to set environment variables in your workspace
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/env
resource "coder_env" "welcome_message" {
agent_id = coder_agent.main.id
name = "WELCOME_MESSAGE"
value = "Welcome to your Coder workspace!"
}
+10
View File
@@ -58,6 +58,16 @@ func TestTemplateBuilderBases(t *testing.T) {
expectedOS: "linux",
hasVariables: false,
},
{
id: "aws-windows",
expectedOS: "windows",
hasVariables: false,
},
{
id: "gcp-windows",
expectedOS: "windows",
hasVariables: false,
},
}
for _, spec := range specs {