DevPod is Wonderful!!
DevPod: the best way to run development workstations in the cloud (and outside of it), a way to set it up and some ideas on how to make it even better.
- Introduction
- Google Cloud Platform Setup
- Authentication
- Context
- Provider: GCloud
- Devcontainer
- IDE: IntelliJ
- Workspace
- Machine
- Appendix
Introduction
Some time in 2021, I looked into developing programs in the cloud instead of my local machine. In the end, I stayed with my local machine, since cloud-based setup comparable in power to my desktop is too expensive, and affordable alternatives too slow - decision that I may reconsider if the prices go down and performance goes up ;)
Even though I normally do not need access to my development environment from anywhere other than my desk, there is something appealing about the cloud-based development environment - if using is is convenient; and here is another (real?) reason I did not switch to cloud-based development in 2021: it is very involved to set it up - and to use…
I perused various recipes that floated around at the time (for instance: How I’ve slashed the cost of my DEV environments by 90%) and came up with my own approach. My notes are in the Appendix: The Manual Way; witness the number of GUI actions to perform, commands to run and unanswered questions mentioned ;)
Spinning up transient, fully configured cloud development environments manually is just too clunky!
I did look into approaches that automate away the complexity, like:
- GitHub Codespaces, which does not seem to support my IDE of choice, IntelliJ Idea natively;
- Google Cloud Workstations, which, with the complexity of running a Kubernetes cluster, seems geared towards enterprises rather than individual developers;
- GitPod… and stayed on my desktop ;)
In July 2024 I stumbled onto DevPod - and it is a game-changer!
TODO
- Open Source
- Client Only
- Unopinionated
Google Cloud Platform Setup
TODO Submit a pull request documenting all this in the provider code repository - and open an issue to do this for the other providers.
To use devpod
with a cloud provider, some resources need to be prepared in that provider. For the Google Cloud Platform (GCP), the prerequisites are:
Project
Create a GCP project for running DevPod workspaces.
Enable Compute Engine API
("compute"
) for the project.
Service Account for devpod
Although it is possible to run devpod
on your machine using your personal GCP account, Principle of least privilege dictates that a separate service account be used for this purpose - and so does GCP documentation: Dedicated Service Accounts, Using IAM Securely. So:
Create a service account (devpod
) and grant to it the following IAM roles on the project:
Role | Needed for |
---|---|
serviceusage.serviceUsageConsumer | Compute Engine billing |
compute.instanceAdmin.v1 | Compute Engine instance operations |
iam.serviceAccountUser | attaching service accounts to virtual machines |
Service Account for Virtual Machines
If access to GCP services from within the virtual machines is needed, create a service account (devpod-vm
) that will be attached to the virtual machines - and grant to it required IAM roles; see https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances.
If this is not needed, iam.serviceAccountUser
role does not need to be granted to the devpod
service account ;)
Authentication
Application Default Credentials (ADC) https://cloud.google.com/docs/authentication/application-default-credentials
TODO not global; no key for non-service account.
Generate, retrieve and stash the JSON key for the account you use to run devpod
on your machine; this can be done using Google Cloud Console UI - or with a gcloud
CLI command:
$ gcloud iam service-accounts keys create \
./devpod.json
--iam-account=<service account email>
TODO gcloud auth application-default login –impersonate-service-account SERVICE_ACCT_EMAIL
Set GOOGLE_APPLICATION_CREDENTIALS
environment variable to the path to the key to the service account for running devpod
on your machine (alternatively, GCLOUD_JSON_AUTH
environment variable can be set to the JSON key itself?).
TODO file an issue
If started from the command line in a shell where GOOGLE_APPLICATION_CREDENTIALS
environment variable is set correctly, DevPod GUI works for the gcloud
provider - but not the workspaces created with it ;)
TODO file an issue When a non-ssh GIT URL is used, but there is no GIT token in the environment, error message from the container does not make this clear…
Context
TODO Precedence:
- explicitly supplied command-line option;
- environment variable;
- option set permanently;
- option default - if one exists, in which case it needs to be specified both in the documentation and in the command-line tool’s help
- ask the user
TODO
- motivation!
- traditional approach: application-scoped environment variables have the highest precedence and override corresponding “permanent” settings - see
GIT_
,GCLOUD_*
,GOOGLE_*
, etc. -
devpod context
: no documentation, no UI - everything scoped by
- I use DirEnv
.envrc
files to set the environment variables. - quote such a file
- supplying the context on each command is tedious and error-prone; using
devpod context use
is global; better approach would bedevpod
looking atDEVPOD_CONTEXT
environment variable… -
devpod-context-create
,devpod-context-use
TODO https://containers.dev/implementors/spec/#merge-logic TODO devpod breaks when VPN is active…
Provider: GCloud
It turns out that setting environment variables does not work as a way to override provider options; this is by (in my opinion - unfortunate :)) design. The easiest way to set the options is to supply them when adding the provider:
devpod provider add gcloud \
-o PROJECT=<project> \
-o ZONE=<zone> \
-o SERVICE_ACCOUNT=<email of the service account for running VMs> \
-o MACHINE_TYPE="c2-standard-8" \
-o DISK_SIZE=50
devpod provider options gcloud
devpod provider use gcloud
To reuse the same virtual machine for all workspaces:
$ devpod provider use gcloud --single-machine
To update the provider:
$ devpod provider update gcloud
TODO document this in the provider code repository (and then - everywhere :)).
Devcontainer
Code repository of the workspace should contain .devcontainer/devcontainer.json
file;
for JVM projects, the minimum is:
{
"image": "mcr.microsoft.com/devcontainers/java:1-21"
}
If additional features are needed:
"features": {
"ghcr.io/devcontainers/features/java:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
}
To expose - say - Jekyll web server running in the container to a local browser:
"forwardPorts": [4000]
I am not sure that Java feature is needed when Java image is used, but with non-Java image (e.g., Jekyll), IntelliJ starts but displays an error:
Unable to find JDK and set project SDK on current Docker image. Please change Docker image definition to proceed with project setup.
So it seems Java should be present in the container regardless ;)
IntelliJ has a Devcontainer plugin. JetBrains also seems to be actively working on defining customizations specific to its IDEs.
TODO “works” in #1 - will Idea eventually self-apply its customizations directly from devcontainer.json
?
IDE: IntelliJ
Until this issue is resolved, the way I install plugins in IntelliJ is by running the following script with the GIT repository URL as parameter:
PLUGIN_SCALA="org.intellij.scala"
PLUGIN_DRACULA_THEME="com.vermouthx.idea"
PLUGINS="$PLUGIN_SCALA $PLUGIN_DRACULA_THEME"
REPOSITORY_URL=$1
# create workspace but do not start the IDE;
# use SSH to check out the repository
devpod up $REPOSITORY_URL --ide intellij --open-ide=false
# install plugins - works, but devpod prints:
# Error tunneling to container:
# wait: remote command exited without exit status or exit signal
# see https://www.jetbrains.com/help/idea/work-inside-remote-project.html#plugins
REPO=`echo $REPOSITORY_URL | awk -F/ '{print $NF}'`
WORKSPACE=`basename $REPO .git`
COMMAND="/home/vscode/.cache/JetBrains/RemoteDev/dist/intellij/bin/remote-dev-server.sh"
devpod ssh $WORKSPACE --command "$COMMAND installPlugins /workspaces/$WORKSPACE $PLUGINS"
TODO does not work!
TODO document this in the code repository.
TODO does DevPod set IntelliJ options correctly or do I need to override the memory limits? devpod up ... --ide-option
? Idea command line options; devops
facility to transfer IDE configuration files and run commands… dot-files?
TODO Ideally, there should be a way to add personal stuff to the devcontainer.json
!
Workspace
To start the IDE and connect to it:
$ devpod up <workspace>
To recreate workspace to reflect all changes to its configuration:
$ devpod up <workspace> --recreate
TODO Does it update the IDE?
Secrets and other environment variables can be made available within the workspace by putting them into a key=value
file(s) and running:
$ devpod up <workspace> --workspace-env-file ...
Even though my SSH key is on a Yubikey token, since devpod
sets up SSH agent forwarding, it works for git push
in the workspace! Straight SSH from the workspace works only with explicitly supplied user name, since the USER
in the workspace is vscode
; I guess GitHub ignores the username and just looks at the key ;)
(SSH key can be specified to the devpod up
command using a hidden ssh-key
option - but I do not see any reason to…)
Machine
Until this issue is resolved, machines do not stop automatically after configured inactivity period elapses, and need to be stopped manually with devpod machine stop <name>
!
Until this issue is resolved, the only way to verify what values are actually used by a virtual machine instance is to use a non-devpod
command specific to the provider, for instance, for GCloud:
$ gcloud compute instances describe devpod-<machine name> --project=<project> --zone=<zone>
where:
-
<machine name>
is copied from the output ofdevpod list
(note thatdevpod-
has to be pre-pended to it); -
<project>
is the PROJECT that was used when the machine was created; -
<zone>
is the ZONE that was used when the machine was created.
Appendix
The Manual Way
Set Up Virtual Machine
# Project
$ gcloud auth login <admin>@<domain>.org
$ gcloud projects create <project>
$ gcloud config set project <project>
$ gcloud config set compute/region us-east1
$ gcloud config set compute/zone us-east1-b
# Compute
## in GCP console, enable billing
$ gcloud services enable compute.googleapis.com # enable Compute Engine
$ gcloud compute project-info add-metadata --metadata enable-oslogin=TRUE
$ gcloud compute project-info describe
# SSH Key
$ gcloud compute os-login ssh-keys add --key-file=<ssh key>.pub --project=<project>
# in the BROWSER!!!, in API Explorer, use Directory to set POSIX user name and home directory
# https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/update
$ gcloud compute os-login describe-profile
# Static IP Address
# QUESTION: maybe use dynamic DNS update client instead?
# or instance options --hostname=box.podval.org --public-ptr --public-ptr-domain [DOMAIN_NAME]
$ gcloud compute addresses create box-ip --network-tier=STANDARD
$ gcloud compute addresses describe box-ip
$ BOX_IP=$(gcloud compute addresses list \
--filter="name:box-ip" \
--format="value(address_range())"
)
$ echo $BOX_IP
# startup-script:
$ sudo dnf -y install wget git mc java-11-openjdk
$ sudo dnf -y install xorg-x11-server-Xwayland xorg-x11-xauth libXrender libXtst fontconfig
$ sudo dnf -y update
# see results:
$ sudo journalctl -u google-startup-scripts.service
# re-run:
$ sudo google_metadata_script_runner startup
# Create VM instance (in ~/box) until I switch to --metadata start-script-url=...
$ gcloud compute instances create box \
--network-tier=STANDARD \
--address=$BOX_IP \
--subnet=default \
--machine-type=e2-highcpu-8 \
--image-project "rocky-linux-cloud" \
--image-family "rocky-linux-8" \
--boot-disk-size=20GB \
--boot-disk-type=pd-standard \
--boot-disk-device-name=box \
--disk=auto-delete=no,name=home,device-name=home \
--metadata-from-file startup-script=startup-script.sh
$ gcloud compute instances list
$ gcloud compute instances describe|start|stop|delete box
# when not running:
$ gcloud compute instances set-machine-type box --machine-type <type>
# In ~/.ssh/config:
Host box
HostName <IP_ADDRESS_DEV_MACHINE>
User dub
ForwardAgent yes
# /home
$ gcloud compute disks create home --size 20GB --type pd-standard
$ gcloud compute instances attach-disk box --disk home
# resize
$ gcloud compute disks resize home --size 20GB
## in the box:
$ lsblk
# format - when new :)
$ sudo mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb
# mount
$ sudo mkdir /mnt/home
$ sudo mount -o discard,defaults /dev/sdb /mnt/home
$ sudo blkid /dev/sdb
## add to /etc/fstab: UUID=UUID_VALUE /mnt/home ext4 discard,defaults,nofail 0 2
## link /mnt/home into /home
# resize
$ sudo df -Th
$ sudo lsblk
$ sudo resize2fs /dev/sdb
Set Up JetBrains Gateway
# install Dracula theme on the gateway client
# VM options are in ~/.cache/JetBrains/RemoteDev/dist/.../bin/idea64.vmoptions
# script is in ~/.cache/JetBrains/RemoteDev/dist/.../bin/remote-dev-server.sh
$ .../remote-dev-server.sh --help
# install Scala plugin on the host:
$ .../remote-dev-server.sh installPlugins ~/Projects/run org.intellij.scala
# server-to-client workflow:
$ .../remote-dev-server.sh run <path/to/project> --ssh-link-host <host>
The DevPod Way
$ gcloud auth login <EMAIL>
$ gcloud auth application-default login
# set up GCloud project, services and roles (once)
$ gcloud projects create <PROJECT ID>
$ gcloud services enable compute --project <PROJECT ID>
$ gcloud projects add-iam-policy-binding <PROJECT ID> --member=user:<EMAIL> --role=roles/compute.instanceAdmin.v1
$ gcloud projects add-iam-policy-binding <PROJECT ID> --member=user:<EMAIL> --role=roles/serviceusage.serviceUsageConsumer
# set up GCloud DevPod provider (once)
$ devpod provider add gcloud -o PROJECT=<PROJECT ID> -o ZONE=<ZONE>
# spin up a workspace
$ devpod up <GIT REPOSITORY URL>
Speed and Price
OpenTorah local: Gradle build ~2 min; Idea: 12 sec
type CPUs Gb Gradle Idea $/month
mem time
e2-standard-2 2 8 ~5.5 min $ 49
e2-highcpu-4 4 4 ~4 min ~70% $ 74
e2-highcpu-8 8 8 ~3 min ~40% 23s $142
e2-standard-4 4 16 $ 98
e2-standard-8 8 32 $196
e2-highcpu-16 16 16 $289
TODO re-test with e2
and c4
machines…