🧪

VS CodeでQuakus + Firestore向けのポータブルな開発環境を作る

2022/08/23に公開

はじめに

Javaの開発環境としてはNetBeansを良く利用していたのですが、最近はVSCodeのdevcontainerが非常に便利なので良く利用しています。今回、Quakus + Firestore向けの開発環境をdevcontainerで構築したので備忘を兼ねて残しておきます。

TL;DR

  • devcointer + docker-composeで開発環境をポータブルに
  • Docker in Dockerの代わりにPodman in Dockerをすると簡単
  • GOOGLE_APPLICATION_CREDENTIALSの偽装を忘れずに

VSCode Dev Containerとは?

Dev ContainerはWSLやSSH、VS Code ServerなどのVS Code Remote Developmentファミリの一つです。

Dockerコンテナに開発環境を入れそこに接続する事で、ポータブルで再現性の高い開発環境を簡単に構築する事が出来ます。自前で似たような環境を作ったりしてましたし、Docker DesktopにもDev Environmentsという似た機能がありますが、VS Codeに完全に統合されているのは良いですよね。

具体的な使い方としては開発プロジェクトのルートディレクトリに.devcontainer/devcontainer.jsonを作成して、そこに開発環境となるコンテナの情報を記述します。典型的なJavaのプロジェクトだと例えば以下のようなディレクトリ構成となります。

├── .git
├── .devcontainer/
│   ├── devcontainer.json
│   ├── Dockerfile
│   ├── docker-compose.yaml
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src

このフォルダ構成で、VSCodeを開くと以下のようにコンテナに入るかという問合せが来るので選択する事でdevcontainer.jsonに記載されたコンテナで開発する事が出来ます。

devcontainer.json内にはDockerイメージ、Dockerfile、docker-compose.yamlのいずれかを指定する事が出来、今回のように「開発用DBを起動したい」というようなケースやフロントとバックエンドの両方を開発する必要がある時などにはDockerfileを直で指定するより、docker-compose.yamlを指定することで複数のコンテナを利用できて便利です。以下に、マイクロソフトが主要な開発環境向けのサンプルを作っているので、こちらを改造するのが手っ取り早いと思います。

podmanによるnative-imageのビルド

Quarkusを使う魅力の一つはGraalVMのnative-imageを使ったネイティブファイルの作成でしょう。通常は以下のようにビルドできます。

$ ./mvnw package -Pnative

ただ、これ一見ふつうのMavenコマンドに見えますが、GraalVMなどをDockerイメージとして取得してビルドするような作りになっています。そのため、Dockerコンテナであるdevcontainer上でQuarkusのネイティブビルドを実行しようとするとDocker in Docker(dind)をやDocker outside of Docker(DooD)をの設定するのも何かと面倒。
なので、daemonlessなコンテナエンジンが無いかなー、と呟いていたところ以下のようにPodmanが良いのでは? と教えて頂きました。
https://twitter.com/bootjp/status/1561683788966805504
https://twitter.com/orimanabu/status/1561689779095617536

言われてみればPodman in Dockerであれば特権を与える必要はありますが、Docker in DockerよりシンプルにContainer in Containerが実現出来そうです。実際試してみたらふつうに動きました。

vscode ➜ /workspace (main ✗) $ podman run -it debian bash
WARN[0000] Failed to detect the owner for the current cgroup: stat /sys/fs/cgroup/systemd/docker/6d6278fb73a9c9d82078e5c89ec81b0bffc73b5a52c72b59e7a397be0c9d0efc: no such file or directory 
ERRO[0000] unable to write pod event: "write unixgram @001a3->/run/systemd/journal/socket: sendmsg: no such file or directory" 
ERRO[0000] unable to write pod event: "write unixgram @001a3->/run/systemd/journal/socket: sendmsg: no such file or directory" 
ERRO[0000] unable to write pod event: "write unixgram @001a3->/run/systemd/journal/socket: sendmsg: no such file or directory" 
root@6f1cadfb2ce6:/#

ただ、動作はするのですがsendmsg: no such file or directoryというエラーが出ているのでこちらの記事を参考に$HOME/.config/containers/containers.conに以下の内容を指定しました。

[engine]
events_logger="file"

これで無事にエラーも無くなります。Quarkusのビルドはpodmanにも対応しているので、特にaliasでdockerコマンドに偽装するといった事をしなくても正常にビルドをする事ができました。

$ ./mvnw package -Pnative
[INFO] Scanning for projects...
[INFO] 
~略
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildRunner] podman run --env LANG=C --rm --user 1000:1000 --userns=keep-id -v /workspace/target/example-serverless-ee-1.0.0-SNAPSHOT-native-image-source-jar:/project:z --name build-native-PLiaR quay.io/quarkus/ubi-quarkus-native-image:22.1-java17 -J-
~略
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:56 min
[INFO] Finished at: 2022-08-22T16:26:50Z
[INFO] ------------------------------------------------------------------------

$ podman build -t koduki/app -f src/main/docker/Dockerfile.native-micro  .
~略
STEP 7: CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
STEP 8: COMMIT koduki/app
--> 6c7d9007a82
6c7d9007a82391eb7fba3e607d31de0183af7163fc004798ffaf733f078efd75

Firesotreをローカルで動作確認する

Emulatorの利用

Cloud FirestoreCloud Spannerと同一の環境に構築されたNoSQLです。mBaaSであったFirabase DBとGCPのNoSQLであったDataStoreを統合する形で生まれたDBとなります。無料枠もあるので、フルスクラッチなど制約が少ない時にはとりあえず選ぶようにしています。

完全にクラウドネイティブなDBであるため、ローカルでの開発時にOSS版をインストールする、と言ったことは出来ませんが、幸いGoogleがエミュレータを提供しています。
https://cloud.google.com/sdk/gcloud/reference/beta/emulators?hl=ja

こちらを利用して以下のようにローカルで動作させる事が出来ます。

$ gcloud beta emulators firestore start --project dummy --host-port=localhost:6641

以下のコマンドで疎通確認ができます。OKと出れば起動しています。

$ curl localhost:6641
Ok

quarkusから利用する場合は以下のように環境変数を定義して実行します。

$ FIRESTORE_EMULATOR_HOST=localhost:6641
$ FIRESTORE_PROJECT_ID=dummy
$ mvn quarkus:dev

これでOK! と言いたい所なのですが、どうやらQuarkusのFierstoreクライアントの実装は必ずGOOGLE_APPLICATION_CREDENTIALSの内容をチェックしてるようでローカルを向いているのに以下のエラーが出てしまいます。

{
  "details": "Error id 4494c58b-77fe-41cc-9b0f-b53ac6e1c8f8-1, java.io.IOException:
  The Application Default Credentials are not available. They are available if 
  running in Google Compute Engine. Otherwise, the environment variable 
  GOOGLE_APPLICATION_CREDENTIALS must be defined pointing to a file defining 
  the credentials. See https://developers.google.com/accounts/docs/
  application-default-credentials for more information.",

GOOGLE_APPLICATION_CREDENTIALSの偽装

色々試したところ、幸いにも実際にGCPへアクセスしたりしてる分けではなく、単純にvalidなフォーマットのファイルがあればよいようです。なので以下のコマンドで適当なprivate keyを生成してJSONフォーマットで出力します。各項目は見ての通りDummyなのでセキュリティ的には取扱いを気にする必要はりません。

mkdir -p /secret/ && openssl genpkey -out /secret/dummy.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
cat <<- _DOC_ > /secret/credential.json 
  {
    "type": "service_account",
    "project_id": "dummy",
    "private_key_id": "dummy",
    "private_key": "$(cat /secret/dummy.pem | perl -pe 's/\n/\\n/g')",
    "client_email": "dummy",
    "client_id": "dummy",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_x509_cert_url": "dummy"
  }

こちらを/secret/credential.jsonに配置しているので以下のように環境変数を指定します。

GOOGLE_APPLICATION_CREDENTIALS=/secret/credential.json

これで無事にFirestoreエミュレータに接続する事が出来ます。コンテナ経由で利用すれば自動テストを組むときとかにも重宝するかと思います。

実際の各種定義ファイル

ここからは、今までの話を盛り込んだ各種ファイルを見ていきます。

devcontainer.json

.devcontainer/devcontainer.jsonはdockerComposeを指定しています。extentionも必要そうなものは追加してあります。単なるDockerfileだけでやる場合と違い、devcontainerはextentionもポータブルに持ち運べるのが良いですよね。

{
	"name": "Quarkus",
	"dockerComposeFile": "docker-compose.yaml",
	"service": "workspace",
	"workspaceFolder": "/workspace",
	"customizations": {
		"vscode": {
			"settings": {
				"maven.executable.path": "/usr/local/sdkman/candidates/maven/current/bin/mvn"
			},
			"extensions": [
				"vscjava.vscode-java-pack",
				"redhat.vscode-xml",
				"redhat.vscode-quarkus",
				"redhat.fabric8-analytics"
			]
		}
	},
	"remoteUser": "vscode"
}

docker-compose.yaml

.devcontainer/docker-compose.yamは開発環境のworkspaceと、ローカルDBのfirestoreの2つのサービスを定義しています。privileged: trueにしているのはpodmanを動作させるためです。firestoreはgoogle/cloud-sdkイメージを利用しています。こちらのイメージはすでに各種エミュレータも含まれているので自動テストなど色々な場面で重宝しそうです。contextとvolumesは..とcurrentではなく一つ上のディレクトリを指定している事に注意をしてください。そうしないと.devcontainerディレクトリをみてしまうので。

version: "3"

services:
  workspace:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
      args:
        - VARIANT=17-bullseye
        - INSTALL_MAVEN=true
        - MAVEN_VERSION=3.8.5
        - INSTALL_GRADLE=false
        - NODE_VERSION=lts/*
    command: sleep infinity
    privileged: true
    environment:
      - FIRESTORE_EMULATOR_HOST=firestore:6641
      - FIRESTORE_PROJECT_ID=dummy 
      - GOOGLE_APPLICATION_CREDENTIALS=/secret/credential.json
    volumes:
      - ..:/workspace:cached
  firestore:
    platform: linux/amd64
    image: google/cloud-sdk
    ports:
        - 6641:6641
    command:
      - gcloud
      - beta
      - emulators
      - firestore
      - start
      - --project
      - dummy
      - --host-port=firestore:6641
    volumes:
      - ..:/workspace:cached

Dockerfile

最後に.devcontainer/Dockerfileですが、公式のJava向けのものを改造して作成しています。ヒアドキュメントが使いたかったので最新のbuildkitが動くようにsyntax=docker/...を指定しています。その上で、Cloud SDKとquarkus-cliを導入し、先ほどのようにDumyのサービスアカウント鍵ファイルの作成や、podmanのインストールと初期設定を行っています。

# syntax=docker/dockerfile:1.4.1

# See here for image contents: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/java/.devcontainer/base.Dockerfile
# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 8, 11, 17, 8-bullseye, 11-bullseye, 17-bullseye, 8-buster, 11-buster, 17-buster
ARG VARIANT=11-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/java:${VARIANT}

# [Option] Install Maven
ARG INSTALL_MAVEN="false"
ARG MAVEN_VERSION=""
# [Option] Install Gradle
ARG INSTALL_GRADLE="false"
ARG GRADLE_VERSION=""
RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \
    && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi

# Install Google Cloud SDK
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg  add - && apt-get update -y && apt-get install google-cloud-sdk -y
## Gen Dummy Service Account
RUN <<EOF
mkdir -p /secret/ && openssl genpkey -out /secret/dummy.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
cat <<- _DOC_ > /secret/credential.json 
  {
    "type": "service_account",
    "project_id": "dummy",
    "private_key_id": "dummy",
    "private_key": "$(cat /secret/dummy.pem | perl -pe 's/\n/\\n/g')",
    "client_email": "dummy",
    "client_id": "dummy",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_x509_cert_url": "dummy"
  }
_DOC_
EOF

# Install Quarkus
RUN su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install quarkus 2>&1"

# Podman Install
RUN <<EOF
apt -y install podman
mkdir -p /home/vscode/.config/containers
cat << _DOC_ > /home/vscode/.config/containers/containers.conf
[engine]
events_logger="file"
_DOC_
chown -R vscode:vscode /home/vscode/.config/containers
EOF

まとめ

今回はVS Codeのdevcontainerを利用してQuarkus + Firestoreの環境を作成してみました。devcontainerを上手く使えば開発環境の統一がぐっと楽になりそうです。少し課題に感じていたcontainer in containerもpodman in dockerでシンプルに解決ができましたし。現状、CLIはあれどIDEがVS Codeだけなのは少し制約が大きいですが、Java向けとしてさえどんどん進化してるしそろそろ乗り換えもありですね。Ruby向けとかも全部こちらに共通化が出来そうですし。

それではHappy Hacking!

Discussion