Introduction

Inspiration: https://itnext.io/how-ive-slashed-the-cost-of-my-dev-environments-by-90-9c1082ad1baf

IntelliJ has a Devcontainer plugin.

Cloud: Google Cloud Platform

Basics:

  • create a GCP Project for running DevPod workspaces;
  • enable Compute Engine API ("compute") for the project;
  • create a service account for running devpod on your machine - unless you intend to run it using your personal account;
  • create a service account for running the virtual machines with workspaces on GCP - unless you intend to run them using your personal account.

Assign to the account used to run devpod on your machine roles on the project:

  • serviceusage.serviceUsageConsumer: for the Compute Engine billing (serviceusage.services.use permission);
  • compute.admin: for the Compute Engine operations;
  • iam.serviceAccountUser: for running the virtual machines as a service account.

Authentication - if you are using a service account to run devpod on your machine:

  • generate, retrieve and stash the JSON key for the service account;
  • set GOOGLE_APPLICATION_CREDENTIALS environment variable to the path to the key (alternatively, GCLOUD_JSON_AUTH environment variable can be set to the JSON key itself).

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 ;)

I use DirEnv .envrc files to set the environment variables.

TODO document this in the provider code repository - and in the help or init messages of the gcloud and other providers.

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 ;)

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 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 of devpod list (note that devpod- 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>

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