Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e950669f93 | |||
| 3d78f5e262 | |||
| 960ec18d35 | |||
| eae64160bd | |||
| b58bfebcf3 | |||
| 05124309ee | |||
| 1469373a50 | |||
| 1551c17413 | |||
| eac3e55537 | |||
| c301da7e6b | |||
| 7d64e7ea84 | |||
| b5937c06a9 | |||
| d2b91ae1a8 | |||
| bd05d06a3b | |||
| e340affe95 | |||
| 16892d806e | |||
| 056f4b5a68 | |||
| 1c99c57b6e | |||
| a0c1a051ed |
@@ -1,4 +1,7 @@
|
||||
[default.extend-words]
|
||||
muc = "muc" # For Munich location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
HashiCorp = "HashiCorp"
|
||||
|
||||
[files]
|
||||
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -46,7 +47,6 @@ jobs:
|
||||
- name: Check version bump requirements
|
||||
id: version-check
|
||||
run: |
|
||||
# Run the script to check what versions should be
|
||||
output_file=$(mktemp)
|
||||
if ./.github/scripts/version-bump.sh "${{ steps.bump-type.outputs.type }}" origin/main > "$output_file" 2>&1; then
|
||||
echo "Script completed successfully"
|
||||
@@ -56,17 +56,14 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Store output for PR comment
|
||||
{
|
||||
echo "output<<EOF"
|
||||
cat "$output_file"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
# Show output
|
||||
cat "$output_file"
|
||||
|
||||
# Check if any files would be modified by the script
|
||||
if git diff --quiet; then
|
||||
echo "versions_up_to_date=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ All module versions are already up to date"
|
||||
@@ -78,6 +75,10 @@ jobs:
|
||||
echo ""
|
||||
echo "Diff preview:"
|
||||
git diff
|
||||
|
||||
git checkout .
|
||||
git clean -fd
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -85,6 +86,7 @@ jobs:
|
||||
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const output = `${{ steps.version-check.outputs.output }}`;
|
||||
const bumpType = `${{ steps.bump-type.outputs.type }}`;
|
||||
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg version="1.1" baseProfile="full" width="340" height="310" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="white" stroke-width="1.5">
|
||||
<g transform="rotate(30) skewX(30)">
|
||||
<rect x="110" y="-72" width="175" height="75" fill="#333333" transform="skewX(-50)"/>
|
||||
<rect x="110" y="3" width="87.5" height="75" fill="#CDCDCD" transform="skewX(-50)"/>
|
||||
<rect x="16.5" y="78.9" width="87.5" height="25" fill="#CDCDCD" />
|
||||
<rect x="16.5" y="104.5" width="175" height="25" fill="#888888" />
|
||||
<rect x="16.5" y="130" width="175" height="50" fill="#DD4814" />
|
||||
<rect x="104" y="166" width="89.5" height="25" fill="#CDCDCD" transform="skewY(-40)"/>
|
||||
<rect x="228.3" y="29.5" width="87.5" height="75" fill="#888888" transform="skewX(-50)"/>
|
||||
<rect x="191.8" y="266" width="89.5" height="25" fill="#888888" transform="skewY(-40)"/>
|
||||
<rect x="192" y="291" width="179.5" height="50" fill="#DD4814" transform="skewY(-40)"/>
|
||||
<rect x="282.3" y="240" width="89.1" height="50" fill="#333333" transform="skewY(-40)"/>
|
||||
<rect x="194" y="3.7" width="87.5" height="25" fill="#333333" />
|
||||
</g>
|
||||
<line x1="93" y1="57" x2="93" y2="88" />
|
||||
<line x1="169" y1="131" x2="92" y2="88" />
|
||||
<line x1="92" y1="88" x2="14" y2="128" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#06D092" d="M8 0L1 4v8l7 4 7-4V4L8 0zm3.119 8.797L9.254 9.863 7.001 8.65v2.549l-2.118 1.33v-5.33l1.68-1.018 2.332 1.216V4.794l2.23-1.322-.006 5.325z"/></svg>
|
||||
|
After Width: | Height: | Size: 390 B |
@@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -88,7 +88,7 @@ resource "coder_agent" "main" {
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -107,7 +107,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.4.0"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -117,41 +117,3 @@ module "claude-code" {
|
||||
icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png"
|
||||
}
|
||||
```
|
||||
|
||||
## Enable user memory
|
||||
|
||||
Claude Code supports [user memory](https://docs.anthropic.com/en/docs/claude-code/memory-management) that persists across all projects. This memory is stored in `~/.claude/CLAUDE.md` and contains personal preferences, coding standards, and instructions that apply to all your work.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
|
||||
# Enable user memory
|
||||
enable_user_memory = true
|
||||
user_memory_content = <<-EOT
|
||||
# My Claude Code Preferences
|
||||
|
||||
## Coding Style
|
||||
- Always use descriptive variable names
|
||||
- Prefer explicit over implicit code
|
||||
- Add comments for complex logic
|
||||
|
||||
## Preferred Technologies
|
||||
- Use TypeScript for JavaScript projects
|
||||
- Prefer functional programming patterns
|
||||
- Use modern ES6+ syntax
|
||||
|
||||
## Testing
|
||||
- Write unit tests for all functions
|
||||
- Use descriptive test names
|
||||
- Aim for high test coverage
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
The `user_memory_content` is only used to create the initial `~/.claude/CLAUDE.md` file if it doesn't already exist. Once created, you can edit this file directly to update your preferences, and they will persist across workspace rebuilds since the file lives in the user's home directory outside the workspace.
|
||||
|
||||
@@ -84,22 +84,9 @@ variable "experiment_post_install_script" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "enable_user_memory" {
|
||||
type = bool
|
||||
description = "Whether to enable user memory for Claude Code. This creates ~/.claude/CLAUDE.md for persistent user preferences."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "user_memory_content" {
|
||||
type = string
|
||||
description = "Initial content for the user memory file (~/.claude/CLAUDE.md). Only used if the file doesn't already exist."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
encoded_user_memory_content = var.user_memory_content != null ? base64encode(var.user_memory_content) : ""
|
||||
}
|
||||
|
||||
# Install and Initialize Claude Code
|
||||
@@ -126,28 +113,6 @@ resource "coder_script" "claude_code" {
|
||||
echo "Folder created successfully."
|
||||
fi
|
||||
|
||||
# Set up user memory if enabled
|
||||
if [ "${var.enable_user_memory}" = "true" ]; then
|
||||
echo "Setting up Claude Code user memory..."
|
||||
|
||||
# Create the .claude directory if it doesn't exist
|
||||
if [ ! -d "$HOME/.claude" ]; then
|
||||
echo "Creating ~/.claude directory..."
|
||||
mkdir -p "$HOME/.claude"
|
||||
fi
|
||||
|
||||
# Create the user memory file if it doesn't exist and content is provided
|
||||
if [ ! -f "$HOME/.claude/CLAUDE.md" ] && [ -n "${local.encoded_user_memory_content}" ]; then
|
||||
echo "Creating user memory file ~/.claude/CLAUDE.md..."
|
||||
echo "${local.encoded_user_memory_content}" | base64 -d > "$HOME/.claude/CLAUDE.md"
|
||||
echo "User memory file created successfully."
|
||||
elif [ -f "$HOME/.claude/CLAUDE.md" ]; then
|
||||
echo "User memory file ~/.claude/CLAUDE.md already exists, skipping creation."
|
||||
else
|
||||
echo "User memory enabled but no initial content provided. You can manually create ~/.claude/CLAUDE.md later."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run pre-install script if provided
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
@@ -166,9 +131,14 @@ resource "coder_script" "claude_code" {
|
||||
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
|
||||
fi
|
||||
|
||||
# Hardcoded for now: install AgentAPI
|
||||
wget https://github.com/coder/agentapi/releases/download/preview/agentapi-linux-amd64
|
||||
chmod +x agentapi-linux-amd64
|
||||
sudo mv agentapi-linux-amd64 /usr/local/bin/agentapi
|
||||
|
||||
if [ "${var.experiment_report_tasks}" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
coder exp mcp configure claude-code ${var.folder}
|
||||
coder exp mcp configure claude-code ${var.folder} --ai-agentapi-url http://localhost:3284
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
@@ -201,8 +171,24 @@ resource "coder_script" "claude_code" {
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Create a new tmux session in detached mode
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
|
||||
# use low width to fit in the tasks UI sidebar. height is adjusted to ~match the default 80k (80x1000) characters
|
||||
# visible in the terminal screen.
|
||||
tmux new-session -d -s claude-code-agentapi -c ${var.folder} 'agentapi server --term-width 67 --term-height 1190 -- bash -c "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""; exec bash'
|
||||
echo "Waiting for agentapi server to start on port 3284..."
|
||||
for i in $(seq 1 15); do
|
||||
if lsof -i :3284 | grep -q 'LISTEN'; then
|
||||
echo "agentapi server started on port 3284."
|
||||
break
|
||||
fi
|
||||
echo "Waiting... ($i/15)"
|
||||
sleep 1
|
||||
done
|
||||
if ! lsof -i :3284 | grep -q 'LISTEN'; then
|
||||
echo "Error: agentapi server did not start on port 3284 after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "agentapi attach"
|
||||
|
||||
|
||||
fi
|
||||
|
||||
@@ -252,6 +238,20 @@ resource "coder_script" "claude_code" {
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "claude_code_web" {
|
||||
slug = "claude-code-web"
|
||||
display_name = "Claude Code Web"
|
||||
agent_id = var.agent_id
|
||||
url = "http://localhost:3284/"
|
||||
icon = var.icon
|
||||
subdomain = true
|
||||
healthcheck {
|
||||
url = "http://localhost:3284/status"
|
||||
interval = 5
|
||||
threshold = 3
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_app" "claude_code" {
|
||||
slug = "claude-code"
|
||||
display_name = "Claude Code"
|
||||
@@ -264,12 +264,20 @@ resource "coder_app" "claude_code" {
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
|
||||
if ! tmux has-session -t claude-code-agentapi 2>/dev/null; then
|
||||
echo "Starting a new Claude Code agentapi tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
# use low width to fit in the tasks UI sidebar. height is adjusted to ~match the default 80k (80x1000) characters
|
||||
# visible in the terminal screen.
|
||||
tmux new-session -d -s claude-code-agentapi -c ${var.folder} 'agentapi server --term-width 67 --term-height 1190 -- bash -c "claude --dangerously-skip-permissions"; exec bash'
|
||||
fi
|
||||
|
||||
if tmux has-session -t claude-code 2>/dev/null; then
|
||||
echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux attach-session -t claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
tmux new-session -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions | tee -a \"$HOME/.claude-code.log\"; exec bash"
|
||||
tmux new-session -s claude-code -c ${var.folder} "agentapi attach; exec bash"
|
||||
fi
|
||||
elif [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
if screen -list | grep -q "claude-code"; then
|
||||
@@ -277,7 +285,7 @@ resource "coder_app" "claude_code" {
|
||||
screen -xRR claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code screen session." | tee -a "$HOME/.claude-code.log"
|
||||
screen -S claude-code bash -c 'claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash'
|
||||
screen -S claude-code bash -c 'agentapi attach; exec bash'
|
||||
fi
|
||||
else
|
||||
cd ${var.folder}
|
||||
|
||||
@@ -16,7 +16,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
@@ -34,7 +34,7 @@ module "windows_rdp" {
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
@@ -46,7 +46,7 @@ module "windows_rdp" {
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.google_compute_instance.dev[0].id
|
||||
}
|
||||
@@ -58,7 +58,7 @@ module "windows_rdp" {
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
devolutions_gateway_version = "2025.1.6" # Specify a specific version
|
||||
|
||||
@@ -16,6 +16,11 @@ function Configure-RDP {
|
||||
New-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -PropertyType DWORD -Force
|
||||
# Enable RDP through Windows Firewall
|
||||
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
|
||||
|
||||
# Disable UDP. It doesn't work via `coder port-forward` and is broken due to MTU issues in Coder Connect.
|
||||
# Requires a restart to take effect. c.f. https://github.com/coder/internal/issues/608#issuecomment-2965923672
|
||||
New-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' -Name "SelectTransport" -Value 1 -PropertyType DWORD -Force
|
||||
Restart-Service -Name "TermService" -Force
|
||||
}
|
||||
|
||||
function Install-DevolutionsGateway {
|
||||
|
||||
@@ -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.
|
||||

|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## 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).
|
||||
|
After Width: | Height: | Size: 124 KiB |
@@ -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
|
||||
@@ -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}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
sudo -u '${linux_user}' sh -c '${init_script}'
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## 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`.
|
||||
@@ -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,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.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## 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).
|
||||
|
||||
## 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}"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand>
|
||||
<CommandLine>cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\Initialize.ps1"</CommandLine>
|
||||
<Description>Copy Initialize.ps1 to file from CustomData</Description>
|
||||
<Order>3</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand>
|
||||
<CommandLine>powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\Initialize.ps1 *> C:\AzureData\Initialize.log"</CommandLine>
|
||||
<Description>Execute Initialize.ps1 script</Description>
|
||||
<Order>4</Order>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## 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).
|
||||
|
||||
## 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}"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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 = "<AutoLogon><Password><Value>${random_password.admin_password.result}</Value></Password><Enabled>true</Enabled><LogonCount>1</LogonCount><Username>${local.admin_username}</Username></AutoLogon>"
|
||||
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"
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## 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.
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# 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", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
||||
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
|
||||
}
|
||||
|
||||
resource "coder_metadata" "container_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = coder_agent.main.id
|
||||
item {
|
||||
key = "workspace image"
|
||||
value = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
|
||||
}
|
||||
item {
|
||||
key = "git url"
|
||||
value = local.repo_url
|
||||
}
|
||||
item {
|
||||
key = "cache repo"
|
||||
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
display_name: Docker Containers
|
||||
description: Provision Docker containers as Coder workspaces
|
||||
icon: ../../../../.icons/docker.png
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [docker, container]
|
||||
---
|
||||
|
||||
# Remote Development on Docker Containers
|
||||
|
||||
Provision Docker containers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## 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.
|
||||
@@ -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 = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# 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", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
||||
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 "docker_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = "codercom/enterprise-base:ubuntu"
|
||||
# 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 docker gateway if the access URL is 127.0.0.1
|
||||
entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
volumes {
|
||||
container_path = "/home/coder"
|
||||
volume_name = docker_volume.home_volume.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
display_name: Google Compute Engine (Devcontainer)
|
||||
description: Provision a Devcontainer on Google Compute Engine instances as Coder workspaces
|
||||
icon: ../../../../.icons/gcp.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [vm, linux, gcp, devcontainer]
|
||||
---
|
||||
|
||||
# Remote Development in a Devcontainer on Google Compute Engine
|
||||
|
||||

|
||||
|
||||
## 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:
|
||||
|
||||
- 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.
|
||||
|
After Width: | Height: | Size: 124 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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`.
|
||||
@@ -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 = <<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,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`.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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`.
|
||||
@@ -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,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!
|
||||
@@ -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 = <<EOF
|
||||
CODER_AGENT_TOKEN=${local.agent_token}
|
||||
EOF
|
||||
create_directories = true
|
||||
target_path = "/opt/coder/init.env"
|
||||
}
|
||||
|
||||
resource "incus_instance" "dev" {
|
||||
running = data.coder_workspace.me.start_count == 1
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
image = incus_cached_image.image.fingerprint
|
||||
|
||||
config = {
|
||||
"security.nesting" = true
|
||||
"security.syscalls.intercept.mknod" = true
|
||||
"security.syscalls.intercept.setxattr" = true
|
||||
"boot.autostart" = true
|
||||
"cloud-init.user-data" = <<EOF
|
||||
#cloud-config
|
||||
hostname: ${lower(data.coder_workspace.me.name)}
|
||||
users:
|
||||
- name: ${local.workspace_user}
|
||||
uid: 1000
|
||||
gid: 1000
|
||||
groups: sudo
|
||||
packages:
|
||||
- curl
|
||||
shell: /bin/bash
|
||||
sudo: ['ALL=(ALL) NOPASSWD:ALL']
|
||||
write_files:
|
||||
- path: /opt/coder/init
|
||||
permissions: "0755"
|
||||
encoding: b64
|
||||
content: ${base64encode(local.agent_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=${local.workspace_user}
|
||||
EnvironmentFile=/opt/coder/init.env
|
||||
ExecStart=/opt/coder/init
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutStopSec=90
|
||||
KillMode=process
|
||||
|
||||
OOMScoreAdjust=-900
|
||||
SyslogIdentifier=coder-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
- path: /etc/systemd/system/coder-agent-watcher.service
|
||||
permissions: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Coder Agent Watcher
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/systemctl restart coder-agent.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
- path: /etc/systemd/system/coder-agent-watcher.path
|
||||
permissions: "0644"
|
||||
content: |
|
||||
[Path]
|
||||
PathModified=/opt/coder/init.env
|
||||
Unit=coder-agent-watcher.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
runcmd:
|
||||
- chown -R ${local.workspace_user}:${local.workspace_user} /home/${local.workspace_user}
|
||||
- |
|
||||
#!/bin/bash
|
||||
apt-get update && apt-get install -y curl docker.io
|
||||
usermod -aG docker ${local.workspace_user}
|
||||
newgrp docker
|
||||
- systemctl enable coder-agent.service coder-agent-watcher.service coder-agent-watcher.path
|
||||
- systemctl start coder-agent.service coder-agent-watcher.service coder-agent-watcher.path
|
||||
EOF
|
||||
}
|
||||
|
||||
limits = {
|
||||
cpu = data.coder_parameter.cpu.value
|
||||
memory = "${data.coder_parameter.cpu.value}GiB"
|
||||
}
|
||||
|
||||
device {
|
||||
name = "home"
|
||||
type = "disk"
|
||||
properties = {
|
||||
path = "/home/${local.workspace_user}"
|
||||
pool = local.pool
|
||||
source = incus_volume.home.name
|
||||
}
|
||||
}
|
||||
|
||||
device {
|
||||
name = "docker"
|
||||
type = "disk"
|
||||
properties = {
|
||||
path = "/var/lib/docker"
|
||||
pool = local.pool
|
||||
source = incus_volume.docker.name
|
||||
}
|
||||
}
|
||||
|
||||
device {
|
||||
name = "root"
|
||||
type = "disk"
|
||||
properties = {
|
||||
path = "/"
|
||||
pool = local.pool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
workspace_user = lower(data.coder_workspace_owner.me.name)
|
||||
pool = "coder"
|
||||
repo_base_dir = data.coder_parameter.repo_base_dir.value == "~" ? "/home/${local.workspace_user}" : replace(data.coder_parameter.repo_base_dir.value, "/^~\\//", "/home/${local.workspace_user}/")
|
||||
repo_dir = module.git-clone.repo_dir
|
||||
agent_id = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].id : ""
|
||||
agent_token = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].token : ""
|
||||
agent_init_script = data.coder_workspace.me.start_count == 1 ? coder_agent.main[0].init_script : ""
|
||||
}
|
||||
|
||||
resource "coder_metadata" "info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = incus_instance.dev.name
|
||||
item {
|
||||
key = "memory"
|
||||
value = incus_instance.dev.limits.memory
|
||||
}
|
||||
item {
|
||||
key = "cpus"
|
||||
value = incus_instance.dev.limits.cpu
|
||||
}
|
||||
item {
|
||||
key = "instance"
|
||||
value = incus_instance.dev.name
|
||||
}
|
||||
item {
|
||||
key = "image"
|
||||
value = "${incus_cached_image.image.source_remote}:${incus_cached_image.image.source_image}"
|
||||
}
|
||||
item {
|
||||
key = "image_fingerprint"
|
||||
value = substr(incus_cached_image.image.fingerprint, 0, 12)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
display_name: Kubernetes (Devcontainer)
|
||||
description: Provision envbuilder pods as Coder workspaces
|
||||
icon: ../../../../.icons/k8s.png
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [container, kubernetes, devcontainer]
|
||||
---
|
||||
|
||||
# Remote Development on Kubernetes Pods (with Devcontainers)
|
||||
|
||||
Provision Devcontainers as [Coder workspaces](https://coder.com/docs/workspaces) on Kubernetes with this example template.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Infrastructure
|
||||
|
||||
**Cluster**: This template requires an existing Kubernetes cluster.
|
||||
|
||||
**Container Image**: This template uses the [envbuilder image](https://github.com/coder/envbuilder) to build a Devcontainer from a `devcontainer.json`.
|
||||
|
||||
**(Optional) Cache Registry**: Envbuilder can utilize a Docker registry as a cache to speed up workspace builds. The [envbuilder Terraform provider](https://github.com/coder/terraform-provider-envbuilder) will check the contents of the cache to determine if a prebuilt image exists. In the case of some missing layers in the registry (partial cache miss), Envbuilder can still utilize some of the build cache from the registry.
|
||||
|
||||
### 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
|
||||
|
||||
Coder supports devcontainers with [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:
|
||||
|
||||
- Kubernetes deployment (ephemeral)
|
||||
- Kubernetes persistent volume claim (persistent on `/workspaces`)
|
||||
- Envbuilder cached image (optional, persistent).
|
||||
|
||||
This template will fetch a Git repo containing a `devcontainer.json` specified by the `repo` parameter, and builds it
|
||||
with [`envbuilder`](https://github.com/coder/envbuilder).
|
||||
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.
|
||||
As you might suspect, 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.
|
||||
|
||||
## 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`.
|
||||
@@ -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 = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Swap Usage (Host)"
|
||||
key = "7_swap_host"
|
||||
script = <<EOT
|
||||
free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
|
||||
EOT
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# 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 "coder_metadata" "container_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = coder_agent.main.id
|
||||
item {
|
||||
key = "workspace image"
|
||||
value = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
|
||||
}
|
||||
item {
|
||||
key = "git url"
|
||||
value = local.repo_url
|
||||
}
|
||||
item {
|
||||
key = "cache repo"
|
||||
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
display_name: Kubernetes (Envbox)
|
||||
description: Provision envbox pods as Coder workspaces
|
||||
icon: ../../../../.icons/k8s.png
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [kubernetes, containers, docker-in-docker]
|
||||
---
|
||||
|
||||
# envbox
|
||||
|
||||
## Introduction
|
||||
|
||||
`envbox` is an image that enables creating non-privileged containers capable of running system-level software (e.g. `dockerd`, `systemd`, etc) in Kubernetes.
|
||||
|
||||
It mainly acts as a wrapper for the excellent [sysbox runtime](https://github.com/nestybox/sysbox/) developed by [Nestybox](https://www.nestybox.com/). For more details on the security of `sysbox` containers see sysbox's [official documentation](https://github.com/nestybox/sysbox/blob/master/docs/user-guide/security.md).
|
||||
|
||||
## Envbox Configuration
|
||||
|
||||
The following environment variables can be used to configure various aspects of the inner and outer container.
|
||||
|
||||
| env | usage | required |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| `CODER_INNER_IMAGE` | The image to use for the inner container. | True |
|
||||
| `CODER_INNER_USERNAME` | The username to use for the inner container. | True |
|
||||
| `CODER_AGENT_TOKEN` | The [Coder Agent](https://coder.com/docs/about/architecture#agents) token to pass to the inner container. | True |
|
||||
| `CODER_INNER_ENVS` | The environment variables to pass to the inner container. A wildcard can be used to match a prefix. Ex: `CODER_INNER_ENVS=KUBERNETES_*,MY_ENV,MY_OTHER_ENV` | false |
|
||||
| `CODER_INNER_HOSTNAME` | The hostname to use for the inner container. | false |
|
||||
| `CODER_IMAGE_PULL_SECRET` | The docker credentials to use when pulling the inner container. The recommended way to do this is to create an [Image Pull Secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials) and then reference the secret using an [environment variable](https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-container-environment-variables-using-secret-data). | false |
|
||||
| `CODER_DOCKER_BRIDGE_CIDR` | The bridge CIDR to start the Docker daemon with. | false |
|
||||
| `CODER_MOUNTS` | A list of mounts to mount into the inner container. Mounts default to `rw`. Ex: `CODER_MOUNTS=/home/coder:/home/coder,/var/run/mysecret:/var/run/mysecret:ro` | false |
|
||||
| `CODER_USR_LIB_DIR` | The mountpoint of the host `/usr/lib` directory. Only required when using GPUs. | false |
|
||||
| `CODER_ADD_TUN` | If `CODER_ADD_TUN=true` add a TUN device to the inner container. | false |
|
||||
| `CODER_ADD_FUSE` | If `CODER_ADD_FUSE=true` add a FUSE device to the inner container. | false |
|
||||
| `CODER_ADD_GPU` | If `CODER_ADD_GPU=true` add detected GPUs and related files to the inner container. Requires setting `CODER_USR_LIB_DIR` and mounting in the hosts `/usr/lib/` directory. | false |
|
||||
| `CODER_CPUS` | Dictates the number of CPUs to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false |
|
||||
| `CODER_MEMORY` | Dictates the max memory (in bytes) to allocate the inner container. It is recommended to set this using the Kubernetes [Downward API](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/#use-container-fields-as-values-for-environment-variables). | false |
|
||||
|
||||
## Migrating Existing Envbox Templates
|
||||
|
||||
Due to the [deprecation and removal of legacy parameters](https://coder.com/docs/templates/parameters#legacy)
|
||||
it may be necessary to migrate existing envbox templates on newer versions of
|
||||
Coder. Consult the [migration](https://coder.com/docs/templates/parameters#migration)
|
||||
documentation for details on how to do so.
|
||||
|
||||
To supply values to existing existing Terraform variables you can specify the
|
||||
`-V` flag. For example
|
||||
|
||||
```bash
|
||||
coder templates push envbox --var namespace="mynamespace" --var max_cpus=2 --var min_cpus=1 --var max_memory=4 --var min_memory=1
|
||||
```
|
||||
|
||||
## Version Pinning
|
||||
|
||||
The template sets the image tag as `latest`. We highly recommend pinning the image to a specific release of envbox, as the `latest` tag may change.
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are welcome and can be made against the [envbox repo](https://github.com/coder/envbox).
|
||||
@@ -0,0 +1,322 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "home_disk" {
|
||||
name = "Disk Size"
|
||||
description = "How large should the disk storing the home directory be?"
|
||||
icon = "https://cdn-icons-png.flaticon.com/512/2344/2344147.png"
|
||||
type = "number"
|
||||
default = 10
|
||||
mutable = true
|
||||
validation {
|
||||
min = 10
|
||||
max = 100
|
||||
}
|
||||
}
|
||||
|
||||
variable "use_kubeconfig" {
|
||||
type = bool
|
||||
default = true
|
||||
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
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
variable "namespace" {
|
||||
type = string
|
||||
description = "The namespace to create workspaces in (must exist prior to creating workspaces)"
|
||||
}
|
||||
|
||||
variable "create_tun" {
|
||||
type = bool
|
||||
description = "Add a TUN device to the workspace."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "create_fuse" {
|
||||
type = bool
|
||||
description = "Add a FUSE device to the workspace."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "max_cpus" {
|
||||
type = string
|
||||
description = "Max number of CPUs the workspace may use (e.g. 2)."
|
||||
}
|
||||
|
||||
variable "min_cpus" {
|
||||
type = string
|
||||
description = "Minimum number of CPUs the workspace may use (e.g. .1)."
|
||||
}
|
||||
|
||||
variable "max_memory" {
|
||||
type = string
|
||||
description = "Maximum amount of memory to allocate the workspace (in GB)."
|
||||
}
|
||||
|
||||
variable "min_memory" {
|
||||
type = string
|
||||
description = "Minimum amount of memory to allocate the workspace (in GB)."
|
||||
}
|
||||
|
||||
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
|
||||
#!/bin/bash
|
||||
# home folder can be empty, so copying default bash settings
|
||||
if [ ! -f ~/.profile ]; then
|
||||
cp /etc/skel/.profile $HOME
|
||||
fi
|
||||
if [ ! -f ~/.bashrc ]; then
|
||||
cp /etc/skel/.bashrc $HOME
|
||||
fi
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
namespace = var.namespace
|
||||
}
|
||||
wait_until_bound = false
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
resources {
|
||||
requests = {
|
||||
storage = "${data.coder_parameter.home_disk.value}Gi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_pod" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
namespace = var.namespace
|
||||
}
|
||||
|
||||
spec {
|
||||
restart_policy = "Never"
|
||||
|
||||
container {
|
||||
name = "dev"
|
||||
# We highly recommend pinning this to a specific release of envbox, as the latest tag may change.
|
||||
image = "ghcr.io/coder/envbox:latest"
|
||||
image_pull_policy = "Always"
|
||||
command = ["/envbox", "docker"]
|
||||
|
||||
security_context {
|
||||
privileged = true
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = {
|
||||
"cpu" : "${var.min_cpus}"
|
||||
"memory" : "${var.min_memory}G"
|
||||
}
|
||||
|
||||
limits = {
|
||||
"cpu" : "${var.max_cpus}"
|
||||
"memory" : "${var.max_memory}G"
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_AGENT_TOKEN"
|
||||
value = coder_agent.main.token
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_AGENT_URL"
|
||||
value = data.coder_workspace.me.access_url
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_INNER_IMAGE"
|
||||
value = "index.docker.io/codercom/enterprise-base:ubuntu-20240812"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_INNER_USERNAME"
|
||||
value = "coder"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_BOOTSTRAP_SCRIPT"
|
||||
value = coder_agent.main.init_script
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_MOUNTS"
|
||||
value = "/home/coder:/home/coder"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_ADD_FUSE"
|
||||
value = var.create_fuse
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_INNER_HOSTNAME"
|
||||
value = data.coder_workspace.me.name
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_ADD_TUN"
|
||||
value = var.create_tun
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_CPUS"
|
||||
value_from {
|
||||
resource_field_ref {
|
||||
resource = "limits.cpu"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
env {
|
||||
name = "CODER_MEMORY"
|
||||
value_from {
|
||||
resource_field_ref {
|
||||
resource = "limits.memory"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/home/coder"
|
||||
name = "home"
|
||||
read_only = false
|
||||
sub_path = "home"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/var/lib/coder/docker"
|
||||
name = "home"
|
||||
sub_path = "cache/docker"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/var/lib/coder/containers"
|
||||
name = "home"
|
||||
sub_path = "cache/containers"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/var/lib/sysbox"
|
||||
name = "sysbox"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/var/lib/containers"
|
||||
name = "home"
|
||||
sub_path = "envbox/containers"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/var/lib/docker"
|
||||
name = "home"
|
||||
sub_path = "envbox/docker"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/usr/src"
|
||||
name = "usr-src"
|
||||
}
|
||||
|
||||
volume_mount {
|
||||
mount_path = "/lib/modules"
|
||||
name = "lib-modules"
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "home"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "sysbox"
|
||||
empty_dir {}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "usr-src"
|
||||
host_path {
|
||||
path = "/usr/src"
|
||||
type = ""
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "lib-modules"
|
||||
host_path {
|
||||
path = "/lib/modules"
|
||||
type = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
display_name: Kubernetes (Deployment)
|
||||
description: Provision Kubernetes Deployments as Coder workspaces
|
||||
icon: ../../../../.icons/k8s.png
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [kubernetes, container]
|
||||
---
|
||||
|
||||
# Remote Development on Kubernetes Pods
|
||||
|
||||
Provision Kubernetes Pods as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
## 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.
|
||||
@@ -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 = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# code-server
|
||||
resource "coder_app" "code-server" {
|
||||
agent_id = coder_agent.main.id
|
||||
slug = "code-server"
|
||||
display_name = "code-server"
|
||||
icon = "/icon/code.svg"
|
||||
url = "http://localhost:13337?folder=/home/coder"
|
||||
subdomain = false
|
||||
share = "owner"
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:13337/healthz"
|
||||
interval = 3
|
||||
threshold = 10
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-pvc"
|
||||
"app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}"
|
||||
"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.home_disk_size.value}Gi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
depends_on = [
|
||||
kubernetes_persistent_volume_claim.home
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace.me.id}"
|
||||
namespace = var.namespace
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-workspace"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
|
||||
"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"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
|
||||
"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
|
||||
}
|
||||
}
|
||||
strategy {
|
||||
type = "Recreate"
|
||||
}
|
||||
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
"app.kubernetes.io/name" = "coder-workspace"
|
||||
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
|
||||
"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
|
||||
}
|
||||
}
|
||||
spec {
|
||||
security_context {
|
||||
run_as_user = 1000
|
||||
fs_group = 1000
|
||||
run_as_non_root = true
|
||||
}
|
||||
|
||||
container {
|
||||
name = "dev"
|
||||
image = "codercom/enterprise-base:ubuntu"
|
||||
image_pull_policy = "Always"
|
||||
command = ["sh", "-c", coder_agent.main.init_script]
|
||||
security_context {
|
||||
run_as_user = "1000"
|
||||
}
|
||||
env {
|
||||
name = "CODER_AGENT_TOKEN"
|
||||
value = coder_agent.main.token
|
||||
}
|
||||
resources {
|
||||
requests = {
|
||||
"cpu" = "250m"
|
||||
"memory" = "512Mi"
|
||||
}
|
||||
limits = {
|
||||
"cpu" = "${data.coder_parameter.cpu.value}"
|
||||
"memory" = "${data.coder_parameter.memory.value}Gi"
|
||||
}
|
||||
}
|
||||
volume_mount {
|
||||
mount_path = "/home/coder"
|
||||
name = "home"
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "home"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.home.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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
display_name: Nomad
|
||||
description: Provision Nomad Jobs as Coder workspaces
|
||||
icon: ../../../../.icons/nomad.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [nomad, container]
|
||||
---
|
||||
|
||||
# Remote Development on Nomad
|
||||
|
||||
Provision Nomad Jobs as [Coder workspaces](https://coder.com/docs/workspaces) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.
|
||||
|
||||
<!-- TODO: Add screenshot -->
|
||||
|
||||
> **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.
|
||||
@@ -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 = <<EOT
|
||||
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
|
||||
EOT
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
locals {
|
||||
workspace_tag = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
home_volume_name = "coder_${data.coder_workspace.me.id}_home"
|
||||
}
|
||||
|
||||
resource "nomad_namespace" "coder_workspace" {
|
||||
name = local.workspace_tag
|
||||
description = "Coder workspace"
|
||||
meta = {
|
||||
owner = data.coder_workspace_owner.me.name
|
||||
}
|
||||
}
|
||||
|
||||
data "nomad_plugin" "hostpath" {
|
||||
plugin_id = "hostpath"
|
||||
wait_for_healthy = true
|
||||
}
|
||||
|
||||
resource "nomad_csi_volume" "home_volume" {
|
||||
depends_on = [data.nomad_plugin.hostpath]
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = all
|
||||
}
|
||||
plugin_id = "hostpath"
|
||||
volume_id = local.home_volume_name
|
||||
name = local.home_volume_name
|
||||
namespace = nomad_namespace.coder_workspace.name
|
||||
|
||||
capability {
|
||||
access_mode = "single-node-writer"
|
||||
attachment_mode = "file-system"
|
||||
}
|
||||
|
||||
mount_options {
|
||||
fs_type = "ext4"
|
||||
}
|
||||
}
|
||||
|
||||
resource "nomad_job" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
depends_on = [nomad_csi_volume.home_volume]
|
||||
jobspec = templatefile("${path.module}/workspace.nomad.tpl", {
|
||||
coder_workspace_owner = data.coder_workspace_owner.me.name
|
||||
coder_workspace_name = data.coder_workspace.me.name
|
||||
workspace_tag = local.workspace_tag
|
||||
cores = tonumber(data.coder_parameter.cpu.value)
|
||||
memory_mb = tonumber(data.coder_parameter.memory.value * 1024)
|
||||
coder_init_script = coder_agent.main.init_script
|
||||
coder_agent_token = coder_agent.main.token
|
||||
workspace_name = data.coder_workspace.me.name
|
||||
home_volume_name = local.home_volume_name
|
||||
})
|
||||
deregister_on_destroy = true
|
||||
purge_on_destroy = true
|
||||
}
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = nomad_job.workspace[0].id
|
||||
item {
|
||||
key = "CPU (Cores)"
|
||||
value = data.coder_parameter.cpu.value
|
||||
}
|
||||
item {
|
||||
key = "Memory (GiB)"
|
||||
value = data.coder_parameter.memory.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
job "workspace" {
|
||||
datacenters = ["dc1"]
|
||||
namespace = "${workspace_tag}"
|
||||
type = "service"
|
||||
group "workspace" {
|
||||
volume "home_volume" {
|
||||
type = "csi"
|
||||
source = "${home_volume_name}"
|
||||
read_only = false
|
||||
attachment_mode = "file-system"
|
||||
access_mode = "single-node-writer"
|
||||
}
|
||||
network {
|
||||
port "http" {}
|
||||
}
|
||||
task "workspace" {
|
||||
driver = "docker"
|
||||
config {
|
||||
image = "codercom/enterprise-base:ubuntu"
|
||||
ports = ["http"]
|
||||
labels {
|
||||
name = "${workspace_tag}"
|
||||
managed_by = "coder"
|
||||
}
|
||||
hostname = "${workspace_name}"
|
||||
entrypoint = ["sh", "-c", "sudo chown coder:coder -R /home/coder && echo '${base64encode(coder_init_script)}' | base64 --decode | sh"]
|
||||
}
|
||||
volume_mount {
|
||||
volume = "home_volume"
|
||||
destination = "/home/coder"
|
||||
}
|
||||
resources {
|
||||
cores = ${cores}
|
||||
memory = ${memory_mb}
|
||||
}
|
||||
env {
|
||||
CODER_AGENT_TOKEN = "${coder_agent_token}"
|
||||
}
|
||||
meta {
|
||||
tag = "${workspace_tag}"
|
||||
managed_by = "coder"
|
||||
}
|
||||
}
|
||||
meta {
|
||||
tag = "${workspace_tag}"
|
||||
managed_by = "coder"
|
||||
}
|
||||
}
|
||||
meta {
|
||||
tag = "${workspace_tag}"
|
||||
managed_by = "coder"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
display_name: Scratch
|
||||
description: A minimal starter template for Coder
|
||||
icon: ../../../../.icons/1f4e6.png
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: []
|
||||
---
|
||||
|
||||
# A minimal Scaffolding for a Coder Template
|
||||
|
||||
Use this starter template as a basis to create your own unique template from scratch.
|
||||
@@ -0,0 +1,66 @@
|
||||
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!"
|
||||
}
|
||||
|
||||
# Adds code-server
|
||||
# See all available modules at https://registry.coder.com/modules
|
||||
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
|
||||
}
|
||||
|
||||
# Runs a script at workspace start/stop or on a cron schedule
|
||||
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script
|
||||
resource "coder_script" "startup_script" {
|
||||
agent_id = coder_agent.main.id
|
||||
display_name = "Startup Script"
|
||||
script = <<-EOF
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Run programs at workspace startup
|
||||
EOF
|
||||
run_on_start = true
|
||||
start_blocks_login = true
|
||||
}
|
||||
@@ -19,11 +19,11 @@ main() {
|
||||
# relative to the main script directory
|
||||
local registry_dir="$script_dir/../registry"
|
||||
|
||||
# Get all subdirectories in the registry directory. Code assumes that
|
||||
# Terraform directories won't begin to appear until three levels deep into
|
||||
# Get all module subdirectories in the registry directory. Code assumes that
|
||||
# Terraform module directories won't begin to appear until three levels deep into
|
||||
# the registry (e.g., registry/coder/modules/coder-login, which will then
|
||||
# have a main.tf file inside it)
|
||||
local subdirs=$(find "$registry_dir" -mindepth 3 -type d | sort)
|
||||
local subdirs=$(find "$registry_dir" -mindepth 3 -path "*/modules/*" -type d | sort)
|
||||
|
||||
for dir in $subdirs; do
|
||||
# Skip over any directories that obviously don't have the necessary
|
||||
|
||||