Compare commits

...

17 Commits

Author SHA1 Message Date
DevCats eddcfb6627 Update MODULE.md
Co-authored-by: Atif Ali <atif@coder.com>
2025-06-25 19:44:19 -05:00
DevCats 6bcb1ac50b Update MODULE.md
Co-authored-by: Atif Ali <atif@coder.com>
2025-06-25 19:44:07 -05:00
DevCats 4bef92b1f6 Update MODULE.md
Co-authored-by: Atif Ali <atif@coder.com>
2025-06-25 19:43:57 -05:00
DevelopmentCats 6763fdbbbb feat: Add Documentation for Coder Modules 2025-06-24 20:12:39 +00:00
DevCats e5ccf74ccc feat: claude-code workspace persistence (#154)
## Description

Add Tmux Plugin Manager with resurrect and continuum plugins. Add
functionality to be able to enable workspace persistence to save the
tmux session automatically so that it can persist through workspace
restarts.

---

## Type of Change

- [ ] New module
- [ ] Bug fix
- [X] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Module Information

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v1.4.0`  
**Breaking change:** [ ] Yes [X] No

---

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [X] Changes tested locally

---

## Related Issues

Closes [#29](https://github.com/coder/registry/issues/29)
2025-06-19 16:46:18 -05:00
DevCats a47ff911e1 fix: Version-Bump Workflow/Script - Formatting before diff (#155)
## Description

Set up Pre-Req's, and ensure that formatting is done before checking
diff since it likes to not respect prettier formatting.

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun run fmt`)
- [ ] Changes tested locally

---------

Co-authored-by: Atif Ali <atif@coder.com>
2025-06-19 16:35:10 -05:00
Ben Potter a8e23647c5 feat: add option to disable VS Code Web workspace trust protection (#131)
for admins with certainty about what is installed in the environment,
this is ideal. otherwise, it's best to get user trust

---------

Co-authored-by: DevelopmentCats <christofer@coder.com>
Co-authored-by: Atif Ali <atif@coder.com>
2025-06-16 21:24:51 -05:00
DevCats 960ec18d35 fix: clean up version-bump workflow script output handling (#153)
## Description

Removed unnecessary comments and added commands to reset the working
directory and clean untracked files in the version-bump workflow. This
improves the script's reliability by ensuring a clean state after
executing version checks.

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Related Issues

None
2025-06-16 19:52:56 -05:00
DevCats eae64160bd fix: update GitHub Actions permissions in version-bump workflow (#152)
## Description

update GitHub Actions permissions in version-bump workflow by adding
issues permission for commenting on PR's

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Related Issues

None
2025-06-16 14:32:09 -05:00
Spike Curtis b58bfebcf3 fix: disable UDP connections on windows-rdp module (#149)
## Description

Relates to 

Fixes an issue where RDP doesn't function properly over Coder Connect,
by disabling UDP and relying only on TCP. c.f.
https://github.com/coder/internal/issues/608#issuecomment-2965923672 for
a detailed description of the problem.

---

## Type of Change

- [ ] New module
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

---

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/windows-rdp`  
**New version:** `v1.0.19`  
**Breaking change:** [ ] Yes [x] No

---

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun run fmt`)
- [x] Changes tested locally

---

## Related Issues

https://github.com/coder/internal/issues/608

Closes #

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2025-06-13 06:18:11 +00:00
Benjamin Peinhardt 05124309ee feat: add templates and update icon paths (#144)
This PR copies the templates in coder/coder/examples/templates over to
the registry, so that template contribution can be done through the
registry.
For now, the starter templates in the coder/coder binary and the
templates available in coder/registry will simply be different
constructs, until we find a solution we like around a single source of
truth for templates that doesn't raise hairy semver concerns for
coder/coder:
https://codercom.slack.com/archives/C05T7165ET1/p1749493368773469
2025-06-12 13:06:46 -05:00
blink-so[bot] 6d1e99d6ae feat: add configurable Devolutions Gateway version for windows-rdp module (#148)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: matifali <10648092+matifali@users.noreply.github.com>
2025-06-11 23:14:30 +05:00
blink-so[bot] 01b70dcbaa feat: add group and order inputs to windows-rdp module (#147)
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: matifali <10648092+matifali@users.noreply.github.com>
2025-06-11 22:17:34 +05:00
DevCats e54ceb3b92 fix: replace broken multi-template PR system with unified template (#143)
Unifies the broken Multi-PR Template I introduced.
2025-06-10 21:18:18 -05:00
blink-so[bot] f5bf6687e7 feat: extract version bump logic into reusable script (#140)
# Extract Version Bump Logic into Reusable Script

This PR extracts the version bump logic from the GitHub Actions workflow
(PR #137) into a reusable script and implements the requirements as
requested.

##  What This PR Delivers

### 🔧 **Version Bump Script**: `.github/scripts/version-bump.sh`
- Extracts all version bump logic from the original workflow
- Supports `patch`, `minor`, and `major` version bumps
- Configurable base reference for diff comparison (defaults to
`origin/main`)
- Comprehensive error handling and semantic version validation
- Can be used standalone or in workflows

### 🔍 **Version Check Workflow**: `.github/workflows/version-check.yaml`
- **Required CI check** that runs on all PRs modifying modules
- Verifies that module versions have been properly updated
- Fails if versions need bumping but haven't been updated
- Provides clear instructions on how to fix version issues

### 🚀 **Version Bump Workflow**: `.github/workflows/version-bump.yaml`
- Simplified workflow that uses the extracted script
- Triggered by PR labels (`version:patch`, `version:minor`,
`version:major`)
- Automatically commits version updates and comments on PR

### 📚 **Updated Documentation**: `CONTRIBUTING.md`
- Clear instructions on how to use the version bump script
- Examples for different bump types
- Information about PR labels as an alternative
- Explains that CI will check version updates

## 🎯 Key Features

 **Script Logic Extracted**: All complex bash logic moved from workflow
to reusable script
 **Required CI Check**: Version check workflow ensures versions are
updated
 **Diff Verification**: Script checks git diff to detect modified
modules
 **Contribution Docs Updated**: Clear instructions for contributors  
 **Backward Compatible**: Maintains all original functionality  
 **Error Handling**: Comprehensive validation and clear error messages

## 📖 Usage Examples

```bash
# For bug fixes
./.github/scripts/version-bump.sh patch

# For new features  
./.github/scripts/version-bump.sh minor

# For breaking changes
./.github/scripts/version-bump.sh major
```

## 🔄 Workflow Integration

1. **Developer makes changes** to modules
2. **CI runs version-check** workflow automatically
3. **If versions need updating**, CI fails with instructions
4. **Developer runs script** or adds PR label
5. **Versions get updated** automatically
6. **CI passes** and PR can be merged

## 🧪 Testing

The script has been tested with:
-  Valid and invalid bump types
-  Module detection from git diff
-  Version calculation and validation
-  README version updates
-  Error handling for edge cases

This implementation addresses all the original requirements while making
the logic more maintainable and reusable.

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Co-authored-by: DevelopmentCats <christofer@coder.com>
2025-06-10 21:17:39 -05:00
dependabot[bot] 7cf60c4e59 chore(deps): bump crate-ci/typos from 1.32.0 to 1.33.1 (#142)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 14:00:06 +05:00
Atif Ali e9870049bb chore: deploy only on tag pushes (#139)
Resolves #68
2025-06-05 07:10:13 +02:00
74 changed files with 8171 additions and 163 deletions
+31 -10
View File
@@ -1,18 +1,39 @@
## Choose a PR Template
## Description
Please select the appropriate PR template for your contribution:
- 🆕 [New Module](?template=new_module.md) - Adding a new module to the registry
- 🐛 [Bug Fix](?template=bug_fix.md) - Fixing an existing issue
- ✨ [Feature](?template=feature.md) - Adding new functionality to a module
- 📝 [Documentation](?template=documentation.md) - Improving docs only
<!-- Briefly describe what this PR does and why -->
---
If you've already started your PR, add `?template=TEMPLATE_NAME.md` to your URL.
## Type of Change
### Quick Checklist
- [ ] New module
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
---
## Module Information
<!-- Delete this section if not applicable -->
**Path:** `registry/[namespace]/modules/[module-name]`
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [ ] No
---
## Testing & Validation
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Following contribution guidelines
- [ ] Changes tested locally
---
## Related Issues
<!-- Link related issues or write "None" if not applicable -->
Closes #
-22
View File
@@ -1,22 +0,0 @@
## Bug Fix: [Brief Description]
**Module:** `registry/[namespace]/modules/[module-name]`
**Version:** `v1.2.3``v1.2.4`
### Problem
<!-- What was broken? -->
### Solution
<!-- How did you fix it? -->
### Testing
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Fix verified locally
### Related Issue
<!-- Link to issue if applicable -->
@@ -1,23 +0,0 @@
## Documentation Update
### Description
<!-- What documentation did you improve? -->
### Changes
<!-- What specific changes were made? -->
-
-
### Checklist
- [ ] README validation passes (if applicable)
- [ ] Code examples tested
- [ ] Links verified
- [ ] Formatting consistent
### Context
<!-- Why were these changes needed? -->
-32
View File
@@ -1,32 +0,0 @@
## Feature: [Brief Description]
**Module:** `registry/[namespace]/modules/[module-name]`
**Version:** `v1.2.3``v1.3.0` (or `v2.0.0` for breaking changes)
### Description
<!-- What does this feature add? -->
### Changes
<!-- List key changes -->
-
-
### Breaking Changes
<!-- List any breaking changes (if major version bump) -->
- None / _Remove if not applicable_
### Testing
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] New tests added for feature
- [ ] Backward compatibility maintained
### Documentation
<!-- Did you update the README? -->
@@ -1,25 +0,0 @@
## New Module: [Module Name]
**Module Path:** `registry/[namespace]/modules/[module-name]`
**Initial Version:** `v1.0.0`
### Description
<!-- Brief description of what this module does -->
### Checklist
- [ ] All required files included (`main.tf`, `main.test.ts`, `README.md`)
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] README has proper frontmatter
- [ ] Icon path is valid
- [ ] Namespace has avatar (if first-time contributor)
### Testing
<!-- How did you test this module? -->
### Additional Notes
<!-- Any special setup or dependencies? -->
+238
View File
@@ -0,0 +1,238 @@
#!/bin/bash
# Version Bump Script
# Usage: ./version-bump.sh <bump_type> [base_ref]
# bump_type: patch, minor, or major
# base_ref: base reference for diff (default: origin/main)
set -euo pipefail
usage() {
echo "Usage: $0 <bump_type> [base_ref]"
echo " bump_type: patch, minor, or major"
echo " base_ref: base reference for diff (default: origin/main)"
echo ""
echo "Examples:"
echo " $0 patch # Update versions with patch bump"
echo " $0 minor # Update versions with minor bump"
echo " $0 major # Update versions with major bump"
exit 1
}
validate_version() {
local version="$1"
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Invalid version format: '$version'. Expected X.Y.Z format." >&2
return 1
fi
return 0
}
bump_version() {
local current_version="$1"
local bump_type="$2"
IFS='.' read -r major minor patch <<< "$current_version"
if ! [[ "$major" =~ ^[0-9]+$ ]] || ! [[ "$minor" =~ ^[0-9]+$ ]] || ! [[ "$patch" =~ ^[0-9]+$ ]]; then
echo "❌ Version components must be numeric: major='$major' minor='$minor' patch='$patch'" >&2
return 1
fi
case "$bump_type" in
"patch")
echo "$major.$minor.$((patch + 1))"
;;
"minor")
echo "$major.$((minor + 1)).0"
;;
"major")
echo "$((major + 1)).0.0"
;;
*)
echo "❌ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2
return 1
;;
esac
}
update_readme_version() {
local readme_path="$1"
local namespace="$2"
local module_name="$3"
local new_version="$4"
if [ ! -f "$readme_path" ]; then
return 1
fi
local module_source="registry.coder.com/${namespace}/${module_name}/coder"
if grep -q "source.*${module_source}" "$readme_path"; then
echo "Updating version references for $namespace/$module_name in $readme_path"
awk -v module_source="$module_source" -v new_version="$new_version" '
/source.*=.*/ {
if ($0 ~ module_source) {
in_target_module = 1
} else {
in_target_module = 0
}
}
/version.*=.*"/ {
if (in_target_module) {
gsub(/version[[:space:]]*=[[:space:]]*"[^"]*"/, "version = \"" new_version "\"")
in_target_module = 0
}
}
{ print }
' "$readme_path" > "${readme_path}.tmp" && mv "${readme_path}.tmp" "$readme_path"
return 0
elif grep -q 'version\s*=\s*"' "$readme_path"; then
echo "⚠️ Found version references but no module source match for $namespace/$module_name"
return 1
fi
return 1
}
main() {
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
usage
fi
local bump_type="$1"
local base_ref="${2:-origin/main}"
case "$bump_type" in
"patch" | "minor" | "major") ;;
*)
echo "❌ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2
exit 1
;;
esac
echo "🔍 Detecting modified modules..."
local changed_files
changed_files=$(git diff --name-only "${base_ref}"...HEAD)
local modules
modules=$(echo "$changed_files" | grep -E '^registry/[^/]+/modules/[^/]+/' | cut -d'/' -f1-4 | sort -u)
if [ -z "$modules" ]; then
echo "❌ No modules detected in changes"
exit 1
fi
echo "Found modules:"
echo "$modules"
echo ""
local bumped_modules=""
local updated_readmes=""
local untagged_modules=""
local has_changes=false
while IFS= read -r module_path; do
if [ -z "$module_path" ]; then continue; fi
local namespace
namespace=$(echo "$module_path" | cut -d'/' -f2)
local module_name
module_name=$(echo "$module_path" | cut -d'/' -f4)
echo "📦 Processing: $namespace/$module_name"
local latest_tag
latest_tag=$(git tag -l "release/${namespace}/${module_name}/v*" | sort -V | tail -1)
local readme_path="$module_path/README.md"
local current_version
if [ -z "$latest_tag" ]; then
if [ -f "$readme_path" ] && grep -q 'version\s*=\s*"' "$readme_path"; then
local readme_version
readme_version=$(grep 'version\s*=\s*"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/')
echo "No git tag found, but README shows version: $readme_version"
if ! validate_version "$readme_version"; then
echo "Starting from v1.0.0 instead"
current_version="1.0.0"
else
current_version="$readme_version"
untagged_modules="$untagged_modules\n- $namespace/$module_name (README: v$readme_version)"
fi
else
echo "No existing tags or version references found for $namespace/$module_name, starting from v1.0.0"
current_version="1.0.0"
fi
else
current_version=$(echo "$latest_tag" | sed 's/.*\/v//')
echo "Found git tag: $latest_tag (v$current_version)"
fi
echo "Current version: $current_version"
if ! validate_version "$current_version"; then
exit 1
fi
local new_version
new_version=$(bump_version "$current_version" "$bump_type")
echo "New version: $new_version"
if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then
updated_readmes="$updated_readmes\n- $namespace/$module_name"
has_changes=true
fi
bumped_modules="$bumped_modules\n- $namespace/$module_name: v$current_version → v$new_version"
echo ""
done <<< "$modules"
# Always run formatter to ensure consistent formatting
echo "🔧 Running formatter to ensure consistent formatting..."
if command -v bun >/dev/null 2>&1; then
bun fmt >/dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
else
echo "⚠️ Warning: bun not found, skipping formatting"
fi
echo ""
echo "📋 Summary:"
echo "Bump Type: $bump_type"
echo ""
echo "Modules Updated:"
echo -e "$bumped_modules"
echo ""
if [ -n "$updated_readmes" ]; then
echo "READMEs Updated:"
echo -e "$updated_readmes"
echo ""
fi
if [ -n "$untagged_modules" ]; then
echo "⚠️ Modules Without Git Tags:"
echo -e "$untagged_modules"
echo "These modules were versioned based on README content. Consider creating proper release tags after merging."
echo ""
fi
if [ "$has_changes" = true ]; then
echo "✅ Version bump completed successfully!"
echo "📝 README files have been updated with new versions."
echo ""
echo "Next steps:"
echo "1. Review the changes: git diff"
echo "2. Commit the changes: git add . && git commit -m 'chore: bump module versions ($bump_type)'"
echo "3. Push the changes: git push"
exit 0
else
echo "️ No README files were updated (no version references found matching module sources)."
echo "Version calculations completed, but no files were modified."
exit 0
fi
}
main "$@"
+4 -1
View File
@@ -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
+1 -1
View File
@@ -48,7 +48,7 @@ jobs:
- name: Validate formatting
run: bun fmt:ci
- name: Check for typos
uses: crate-ci/typos@v1.32.0
uses: crate-ci/typos@v1.33.1
with:
config: .github/typos.toml
validate-readme-files:
-2
View File
@@ -2,8 +2,6 @@ name: deploy-registry
on:
push:
branches:
- main
tags:
# Matches release/<namespace>/<resource_name>/<semantic_version>
# (e.g., "release/whizus/exoscale-zone/v1.0.13")
+120
View File
@@ -0,0 +1,120 @@
name: Version Bump
on:
pull_request:
types: [labeled]
paths:
- "registry/**/modules/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
version-bump:
if: github.event.label.name == 'version:patch' || github.event.label.name == 'version:minor' || github.event.label.name == 'version:major'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@main
- name: Install dependencies
run: bun install
- name: Extract bump type from label
id: bump-type
run: |
case "${{ github.event.label.name }}" in
"version:patch")
echo "type=patch" >> $GITHUB_OUTPUT
;;
"version:minor")
echo "type=minor" >> $GITHUB_OUTPUT
;;
"version:major")
echo "type=major" >> $GITHUB_OUTPUT
;;
*)
echo "Invalid version label: ${{ github.event.label.name }}"
exit 1
;;
esac
- name: Check version bump requirements
id: version-check
run: |
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"
else
echo "Script failed"
cat "$output_file"
exit 1
fi
{
echo "output<<EOF"
cat "$output_file"
echo "EOF"
} >> $GITHUB_OUTPUT
cat "$output_file"
if git diff --quiet; then
echo "versions_up_to_date=true" >> $GITHUB_OUTPUT
echo "✅ All module versions are already up to date"
else
echo "versions_up_to_date=false" >> $GITHUB_OUTPUT
echo "❌ Module versions need to be updated"
echo "Files that would be changed:"
git diff --name-only
echo ""
echo "Diff preview:"
git diff
git checkout .
git clean -fd
exit 1
fi
- name: Comment on PR - Failure
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 }}`;
let comment = `## ❌ Version Bump Validation Failed\n\n`;
comment += `**Bump Type:** \`${bumpType}\`\n\n`;
comment += `Module versions need to be updated but haven't been bumped yet.\n\n`;
comment += `**Required Actions:**\n`;
comment += `1. Run the version bump script locally: \`./.github/scripts/version-bump.sh ${bumpType}\`\n`;
comment += `2. Commit the changes: \`git add . && git commit -m "chore: bump module versions (${bumpType})"\`\n`;
comment += `3. Push the changes: \`git push\`\n\n`;
comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`;
comment += `> Please update the module versions and push the changes to continue.`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

+21
View File
@@ -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

+2
View File
@@ -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

+24 -2
View File
@@ -258,13 +258,35 @@ All README files must follow these rules:
## Versioning Guidelines
After your PR is merged, maintainers will handle the release. Understanding version numbers helps you describe the impact of your changes:
When you modify a module, you need to update its version number in the README. Understanding version numbers helps you describe the impact of your changes:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
**Important**: Always specify the version change in your PR (e.g., `v1.2.3 → v1.2.4`). This helps maintainers create the correct release tag.
### Updating Module Versions
If your changes require a version bump, use the version bump script:
```bash
# For bug fixes
./.github/scripts/version-bump.sh patch
# For new features
./.github/scripts/version-bump.sh minor
# For breaking changes
./.github/scripts/version-bump.sh major
```
The script will:
1. Detect which modules you've modified
2. Calculate the new version number
3. Update all version references in the module's README
4. Show you a summary of changes
**Important**: Only run the version bump script if your changes require a new release. Documentation-only changes don't need version updates.
---
+1200
View File
File diff suppressed because it is too large Load Diff
+26 -3
View File
@@ -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.3.1"
version = "1.4.0"
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.3.1"
version = "1.4.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
@@ -100,6 +100,29 @@ module "claude-code" {
}
```
## Session Persistence (Experimental)
Enable automatic session persistence to maintain Claude Code sessions across workspace restarts:
```tf
module "claude-code" {
count = data.coder_workspace.me.start_count
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
# Enable tmux with session persistence
experiment_use_tmux = true
experiment_tmux_session_persistence = true
experiment_tmux_session_save_interval = "10" # Save every 10 minutes
experiment_report_tasks = true
}
```
Session persistence automatically saves and restores your Claude Code environment, including working directory and command history.
## Run standalone
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI.
@@ -107,7 +130,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.3.1"
version = "1.4.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
+124 -25
View File
@@ -84,6 +84,18 @@ variable "experiment_post_install_script" {
default = null
}
variable "experiment_tmux_session_persistence" {
type = bool
description = "Whether to enable tmux session persistence across workspace restarts."
default = false
}
variable "experiment_tmux_session_save_interval" {
type = string
description = "How often to save tmux sessions in minutes."
default = "15"
}
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) : ""
@@ -98,12 +110,28 @@ resource "coder_script" "claude_code" {
#!/bin/bash
set -e
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if the specified folder exists
install_tmux() {
echo "Installing tmux..."
if command_exists apt-get; then
sudo apt-get update && sudo apt-get install -y tmux
elif command_exists yum; then
sudo yum install -y tmux
elif command_exists dnf; then
sudo dnf install -y tmux
elif command_exists pacman; then
sudo pacman -S --noconfirm tmux
elif command_exists apk; then
sudo apk add tmux
else
echo "Error: Unable to install tmux automatically. Package manager not recognized."
exit 1
fi
}
if [ ! -d "${var.folder}" ]; then
echo "Warning: The specified folder '${var.folder}' does not exist."
echo "Creating the folder..."
@@ -112,8 +140,6 @@ resource "coder_script" "claude_code" {
mkdir -p "${var.folder}"
echo "Folder created successfully."
fi
# Run pre-install script if provided
if [ -n "${local.encoded_pre_install_script}" ]; then
echo "Running pre-install script..."
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
@@ -121,11 +147,30 @@ resource "coder_script" "claude_code" {
/tmp/pre_install.sh
fi
# Install Claude Code if enabled
if [ "${var.install_claude_code}" = "true" ]; then
if ! command_exists npm; then
echo "Error: npm is not installed. Please install Node.js and npm first."
exit 1
echo "npm not found, checking for Node.js installation..."
if ! command_exists node; then
echo "Node.js not found, installing Node.js via NVM..."
export NVM_DIR="$HOME/.nvm"
if [ ! -d "$NVM_DIR" ]; then
mkdir -p "$NVM_DIR"
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
else
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi
nvm install --lts
nvm use --lts
nvm alias default node
echo "Node.js installed: $(node --version)"
echo "npm installed: $(npm --version)"
else
echo "Node.js is installed but npm is not available. Please install npm manually."
exit 1
fi
fi
echo "Installing Claude Code..."
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
@@ -136,7 +181,6 @@ resource "coder_script" "claude_code" {
coder exp mcp configure claude-code ${var.folder}
fi
# Run post-install script if provided
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
@@ -144,46 +188,97 @@ resource "coder_script" "claude_code" {
/tmp/post_install.sh
fi
# Handle terminal multiplexer selection (tmux or screen)
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
echo "Please set only one of them to true."
exit 1
fi
# Run with tmux if enabled
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Claude Code in the background with tmux..."
if [ "${var.experiment_tmux_session_persistence}" = "true" ] && [ "${var.experiment_use_tmux}" != "true" ]; then
echo "Error: Session persistence requires tmux to be enabled."
echo "Please set experiment_use_tmux = true when using session persistence."
exit 1
fi
# Check if tmux is installed
if [ "${var.experiment_use_tmux}" = "true" ]; then
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
install_tmux
fi
touch "$HOME/.claude-code.log"
if [ "${var.experiment_tmux_session_persistence}" = "true" ]; then
echo "Setting up tmux session persistence..."
if ! command_exists git; then
echo "Git not found, installing git..."
if command_exists apt-get; then
sudo apt-get update && sudo apt-get install -y git
elif command_exists yum; then
sudo yum install -y git
elif command_exists dnf; then
sudo dnf install -y git
elif command_exists pacman; then
sudo pacman -S --noconfirm git
elif command_exists apk; then
sudo apk add git
else
echo "Error: Unable to install git automatically. Package manager not recognized."
echo "Please install git manually to enable session persistence."
exit 1
fi
fi
mkdir -p ~/.tmux/plugins
if [ ! -d ~/.tmux/plugins/tpm ]; then
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
fi
cat > ~/.tmux.conf << EOF
# Claude Code tmux persistence configuration
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
# Configure session persistence
set -g @resurrect-processes ':all:'
set -g @resurrect-capture-pane-contents 'on'
set -g @resurrect-save-bash-history 'on'
set -g @continuum-restore 'on'
set -g @continuum-save-interval '${var.experiment_tmux_session_save_interval}'
set -g @continuum-boot 'on'
set -g @continuum-save-on 'on'
# Initialize plugin manager
run '~/.tmux/plugins/tpm/tpm'
EOF
~/.tmux/plugins/tpm/scripts/install_plugins.sh
fi
echo "Running Claude Code in the background with tmux..."
touch "$HOME/.claude-code.log"
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\""
if [ "${var.experiment_tmux_session_persistence}" = "true" ]; then
sleep 3
if ! tmux has-session -t claude-code 2>/dev/null; then
# Only create a new session if one doesn't exist
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
fi
else
if ! tmux has-session -t claude-code 2>/dev/null; then
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
fi
fi
fi
# Run with screen if enabled
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Claude Code in the background..."
# Check if screen is installed
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
fi
touch "$HOME/.claude-code.log"
# Ensure the screenrc exists
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log"
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
@@ -198,6 +293,7 @@ resource "coder_script" "claude_code" {
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
@@ -207,7 +303,6 @@ resource "coder_script" "claude_code" {
exec bash
'
else
# Check if claude is installed before running
if ! command_exists claude; then
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
exit 1
@@ -231,6 +326,10 @@ resource "coder_app" "claude_code" {
if [ "${var.experiment_use_tmux}" = "true" ]; then
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"
# If Claude isn't running in the session, start it without the prompt
if ! tmux list-panes -t claude-code -F '#{pane_current_command}' | grep -q "claude"; then
tmux send-keys -t claude-code "cd ${var.folder} && claude -c --dangerously-skip-permissions" C-m
fi
tmux attach-session -t claude-code
else
echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
+5 -5
View File
@@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -31,7 +31,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@@ -45,7 +45,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
@@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -78,7 +78,7 @@ By default, this module installs the latest. To pin a specific version, retrieve
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.3.0"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
@@ -121,6 +121,12 @@ variable "use_cached" {
default = false
}
variable "disable_trust" {
type = bool
description = "Disables workspace trust protection for VS Code Web."
default = false
}
variable "extensions_dir" {
type = string
description = "Override the directory to store extensions in."
@@ -169,6 +175,7 @@ resource "coder_script" "vscode-web" {
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
OFFLINE : var.offline,
USE_CACHED : var.use_cached,
DISABLE_TRUST : var.disable_trust,
EXTENSIONS_DIR : var.extensions_dir,
FOLDER : var.folder,
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
+8 -2
View File
@@ -16,10 +16,16 @@ if [ -n "${SERVER_BASE_PATH}" ]; then
SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}"
fi
# Set disable workspace trust
DISABLE_TRUST_ARG=""
if [ "${DISABLE_TRUST}" = true ]; then
DISABLE_TRUST_ARG="--disable-workspace-trust"
fi
run_vscode_web() {
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG $DISABLE_TRUST_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
echo "Check logs at ${LOG_PATH}!"
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
}
# Check if the settings file exists...
+16 -3
View File
@@ -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.0.18"
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.0.18"
version = "1.2.1"
agent_id = resource.coder_agent.main.id
resource_id = resource.aws_instance.dev.id
}
@@ -46,12 +46,25 @@ module "windows_rdp" {
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
version = "1.0.18"
version = "1.2.1"
agent_id = resource.coder_agent.main.id
resource_id = resource.google_compute_instance.dev[0].id
}
```
### With Custom Devolutions Gateway Version
```tf
module "windows_rdp" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windows-rdp/coder"
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
}
```
## Roadmap
- [ ] Test on Microsoft Azure.
+24 -3
View File
@@ -9,6 +9,18 @@ terraform {
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "share" {
type = string
default = "owner"
@@ -39,14 +51,21 @@ variable "admin_password" {
sensitive = true
}
variable "devolutions_gateway_version" {
type = string
default = "2025.2.1"
description = "Version of Devolutions Gateway to install. Defaults to the latest available version."
}
resource "coder_script" "windows-rdp" {
agent_id = var.agent_id
display_name = "windows-rdp"
icon = "/icon/desktop.svg"
script = templatefile("${path.module}/powershell-installation-script.tftpl", {
admin_username = var.admin_username
admin_password = var.admin_password
admin_username = var.admin_username
admin_password = var.admin_password
devolutions_gateway_version = var.devolutions_gateway_version
# Wanted to have this be in the powershell template file, but Terraform
# doesn't allow recursive calls to the templatefile function. Have to feed
@@ -68,6 +87,8 @@ resource "coder_app" "windows-rdp" {
url = "http://localhost:7171"
icon = "/icon/desktop.svg"
subdomain = true
order = var.order
group = var.group
healthcheck {
url = "http://localhost:7171"
@@ -78,7 +99,7 @@ resource "coder_app" "windows-rdp" {
resource "coder_app" "rdp-docs" {
agent_id = var.agent_id
display_name = "Local RDP"
display_name = "Local RDP Docs"
slug = "rdp-docs"
icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg"
url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop"
@@ -16,12 +16,17 @@ 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 {
# Define the module name and version
$moduleName = "DevolutionsGateway"
$moduleVersion = "2024.1.5"
$moduleVersion = "${devolutions_gateway_version}"
# Install the module with the specified version for all users
# This requires administrator privileges
@@ -0,0 +1,111 @@
---
display_name: AWS EC2 (Devcontainer)
description: Provision AWS EC2 VMs with a devcontainer as Coder workspaces
icon: ../../../../.icons/aws.svg
maintainer_github: coder
verified: true
tags: [vm, linux, aws, persistent, devcontainer]
---
# Remote Development on AWS EC2 VMs using a Devcontainer
Provision AWS EC2 VMs as [Coder workspaces](https://coder.com/docs) with this example template.
![Architecture Diagram](./architecture.svg)
<!-- 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).
File diff suppressed because one or more lines are too long

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}'
+296
View File
@@ -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
}
}
+48
View File
@@ -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.
+220
View File
@@ -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
![Architecture Diagram](./architecture.svg)
## 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.
File diff suppressed because one or more lines are too long

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`.
+184
View File
@@ -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"
}
}
+51
View File
@@ -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!
+317
View File
@@ -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.
+345
View File
@@ -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.
+66
View File
@@ -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
}
+3 -3
View File
@@ -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