🧱

複数リポジトリが密結合のシステムでCodespaceを使う(疑似モノリポ編)

に公開

概要

GitHub Codespacesは、GitHubによるクラウドベースの開発環境です。Dev Containerをベースに、コンテナをGitHub側のクラウド環境で動かすことで、各自のマシンに依存しない開発体験が可能となります。

ただ、Codespacesは1リポジトリ単体で動作確認・開発が行えるシステムを主な対象としたつくりとなっているため、複数リポジトリが相互に依存していて単独で動作確認が難しいシステム[1]でCodespacesを利用したい場合は、ひと工夫が必要となります。

この記事では、そうした複数リポジトリにまたがるシステムでCodespacesをとりあえず利用する場合の設計の一案として、1コンテナ上で各リポジトリをcloneしてくる疑似モノリポ構成を紹介します。

対象読者

  • Dev Containers・GitHub Codespacesの基礎を理解しており、簡単なプロジェクトでの使用経験がある
  • 複数リポジトリが密に依存しあっているシステムで、相互依存はいったん解消しないまま、Dev ContainersやCodespacesをとりあえず使用したい。

例に使うシステムの構成

筆者の個人的な興味と好みから、今回の検討では以下の構成のシステムを例として用いています。
もっとも、今回はコンテナの話が主なので、アプリケーション部分の内容はあまり関係ありません。

要点

忙しい人のために要点だけ先にまとめておきます。
以下ではこの要点についてひとつずつ説明していきます。

構成概要

  • Codespace専用のリポジトリを用意する
  • Docker Composeを使用した複数コンテナ構成
    • DBがない場合や、別クラウド上のDBを使う場合は単一コンテナのほうが楽
  • データベースだけ別コンテナ、アプリケーションコードは全て1コンテナ
  • 各リポジトリはDev Container作成時に内部でcloneする
    • Submoduleは使わない

👍利点

  • 初期設定が比較的単純。
  • 利用方法も比較的単純。

🤔難点

  • 起動に時間がかかる。
  • Codespaceの生存期間が長いと、各リポジトリが古くなる。
  • 本番環境とコンテナ構成を揃えられない場合がある。

ほか注意点

  • アプリケーションコンテナはコマンドで起きたままにしておく

詳説

Codespace専用のリポジトリを用意する

https://github.com/mi759/haikucodespace

単独リポジトリ構成のアプリケーションであれば、アプリケーションのリポジトリ自体に.devcontainersを作成してCodespaceを利用するのが普通かと思います。
(というより、Dev Containerの本来想定された使い方がそれだと思います)

今回のように複数リポジトリをまたぐ場合も、どれか1つのリポジトリに.devcontainersを置いてCodespaceを開く方法は可能です。その場合、他のリポジトリは後述の通りSubmodule等として引っ張ってくることになります。
リポジトリ間に「これがメインで、他は個別サービス」のような明確な主従関係があるならば、メインのリポジトリ上に設定するのもよいでしょう。

ただ、今回は下記の理由から、Codespaceの設定専用のリポジトリを用意し、そこからCodespaceを開きます。

  • フロントエンドとバックエンドのどちらが主・従というわけでもない
  • 開発環境(Codespace)の設定は、各アプリケーションのソースとは別の塊なので、履歴を分けて管理するのは理に適っている

Docker Composeを使用した複数コンテナ構成

コンテナ構成は devcontainer.json に記述します。

Codespacesのチュートリアル等では imagefeatures を指定した単一コンテナの構成が多いですが、代わりに dockerComposeFile を指定することで複数コンテナを作成することもできます。

正直、featuresを使う方がライブラリの導入やリポジトリのcloneが格段に楽なので、特に動機がなければ単一コンテナのほうが楽です。

ただ、今回はデータベースのコンテナを分けたいという事情があるため、複数コンテナとしました。

なお、Docker Composeは各コンテナをデフォルトで1つのネットワークに接続してくれるため、特に設定しなくともコンテナ間の通信が可能です。

devcontainer.json(image+featuresの場合)
{
    "name": "Haiku Codespace",
    "image": "ghcr.io/prulloac/base:bookworm",
    "features": {
        "ghcr.io/devcontainers/features/docker-in-docker:2": {}
    }
    // 後略
devcontainer.json(Docker Composeを使う場合)
{
    "name": "Haiku Codespace",
    "dockerComposeFile": "./docker-compose.yml",
    // 後略
}

データベースだけ別コンテナ、アプリケーションコードは全て1コンテナ

複数コンテナとはいうものの、分けるのはデータベースのコンテナのみで、フロントエンドとバックエンドはどちらもアプリケーションとして1つのコンテナに入れてしまいます。

なぜならば、そのほうがフロントエンドとバックエンドのコードを並行して編集するのが楽だからです。

各リポジトリを別コンテナに分けた場合も、設定次第ではVSCodeの接続先コンテナを分けて作業することは(少なくともDev Containerの機能としては)可能ですが[2]、密に結合した各リポジトリを行ったり来たりしながら開発したい場合、いちいちコマンドパレットから接続先を切り替えるのは手間が大きすぎる感があります。

その点、1コンテナにアプリケーションコードを全てまとめてしまえば、あたかもモノリポ構成のごとく、ファイルシステム上で編集対象を切り替えるだけですみます。

作業イメージ
Codespaceを開いた時の様子。バックエンドhaikuapiとフロントエンドhaikufrontendを同時に編集できる

データベース定義のほうは、(用途にもよりますが)Codespace上から頻繁に編集したいものでもないので、セットアップの利便性を優先して別コンテナとしています。

開発途中でちょくちょくDB定義を変更して試したい!という需要がある場合には、アプリケーションコンテナ側にpsqlクライアントを入れて、そこからSQLを流して一時的に定義変更をする(Codespaceを消すと変更内容も消えるので、残したいものはDB定義リポジトリに反映する)と言うのもありかなと思います。

各リポジトリはDev Container作成時に内部でcloneする

上記で「Codespace専用のリポジトリを用意する」という選択をしたため、そのままだと作成したコンテナの中には何のソースコードもなく、空の状態です。
(アプリケーションのリポジトリからCodespaceを開く場合、ソースコードはコンテナ内の指定ディレクトリに自動でマウントされます)

そのため、コンテナイメージ作成用のDockerfileにて、フロントエンド・バックエンド・データベースの各リポジトリをgit cloneするとともに、必要なライブラリ類もインストールしてあげる必要があります。

対象がPrivateリポジトリの場合にはもう少し気を遣うことが増えるはずですが、今回はいずれもPublicリポジトリなので、愚直に設定するだけです。

まずアプリケーションコンテナの例はこちら
https://github.com/mi759/haikucodespace/blob/6ca8bfe0cde853c187543bf8cfea1783ab0dcb65/.devcontainer/app/Dockerfile

データベースコンテナのほうは、cloneしてきたSQLを流すため、簡易のセットアップシェルを用意し、postgresイメージで設定されている /docker-entrypoint-initdb.d/ (内部のシェルが自動実行される)内に配置しています。
データベースの作成もこの初期化シェルでおこなっています。
https://github.com/mi759/haikucodespace/blob/6ca8bfe0cde853c187543bf8cfea1783ab0dcb65/.devcontainer/db/Dockerfile

https://github.com/mi759/haikucodespace/blob/6ca8bfe0cde853c187543bf8cfea1783ab0dcb65/.devcontainer/db/init/01-init-user-db.sh

https://github.com/mi759/haikucodespace/blob/6ca8bfe0cde853c187543bf8cfea1783ab0dcb65/.devcontainer/db/init/02-apply-definitions.sh

Submoduleは使わない

他のリポジトリを参照するのが目的であれば、Codespace用リポジトリから各リポジトリをSubmoduleとして参照する方法もあります。

実際に、その方法を採用している記事もあるようです。
https://zenn.dev/ruchika/articles/df383b20c72dda
https://zenn.dev/lincwell_inc/articles/f4671ffcda2c0f

ただ、Submoduleはあくまでコミットの参照であるという都合もあり、最新の状態に保つのが地味に大変です。また、利用する側もSubmoduleの概念を理解していないと思わぬ落とし穴にハマる可能性があります。

そのため、今回は敢えてSubmoduleを採用せず、(スマートではないものの)よりシンプルな上記のcloneでの方法を採用しています。

👍利点

初期設定が比較的単純

アプリケーションコンテナ内に各リポジトリのcloneと関連ライブラリのインストールを行うDockerfileを書くだけなので、そこまで深いDev ContainersやCodespacesの知識が無くとも書くことができます。たぶん。

利用方法も比較的単純

Codespaceを起動したら、あとは概ねローカルマシンに各リポジトリをcloneしたときと同じ感覚でアプリの起動・編集が可能なため、Dev ContainersやCodespacesの細かい知識がない利用者でも直感的に利用しやすいと思います。

🤔難点

起動に時間がかかる

Codespace起動のたびにイメージを作ることになるので、起動に数分程度かかります。
解決策としては下記が考えられます。

  • Codespaceの生存期間を長くする
    • 後述の通り、各リポジトリのリモートとの同期がやや面倒になります。
  • イメージを事前にビルドしてどこかにホストしておき、Codespacesではそれを利用する
    • 主にgit等のライブラリのインストールに時間を要するため、その部分を事前に済ませておくことで、起動時間の短縮が期待できます。
    • git cloneは事前ビルドしてしまうとリモートから取り残されてしまうので、事前ビルドには含めない方が良いでしょう。

Codespaceの生存期間が長いと、各リポジトリが古くなる

Codespace起動時にgit cloneしてくるので、何もしないと(ローカル開発環境同様に)各リポジトリはリモートから取り残されていきます。もっとも、定期的にgit pullしてあげれば問題はありません。

本番環境とコンテナ構成を揃えられない場合がある

Codespacesでの利便性のためにアプリケーションを全て1コンテナにまとめる前提のため、たとえ本番では各アプリケーションを別コンテナやマシンに分けていたとしても、それを再現することはできません。
各サービスのコンテナごとに設定を変えたい場合や、サービス間の疎通設定が複雑な場合には問題になり得るでしょう。

ほか注意点

導入時につまずいた点を記載します。

アプリケーションコンテナはコマンドで起きたままにしておく

素直に各種ライブラリのインストールとリポジトリのcloneだけのDockerfileを記述してDev Containerを起動すると、アプリケーションコンテナがすぐに終了してしまいます。
これはコンテナの仕様上、メインプロセス(PID1のプロセス)が終了するとコンテナ自体も終了してしまうためです。
イメージによってはCMDにてbash等を指定してくれていてメインプロセスが残る場合もありますが、今回使用したdebian:bookwormはその限りではないようです。

紛らわしいことに、Docker Composeを使わずにdevcontainer.jsonの image にdebian:bookwormを指定した場合にはこの問題は起きません。これは、 image 指定の場合はDev Container側がデフォルトコマンドを /bin/sh -c "while sleep 1000; do :; done" (待機し続ける)に上書きしてくれるからです。この機能はdevcontainer.jsonの overrideCommand プロパティでオンオフを切り替えることができます(デフォルトはtrue=オン)。

ただ、Docker Composeを使う場合にはこの便利機能は動作しないので、自分で設定してあげる必要があります。今回はdocker-compose.yml上で command プロパティとして指定しています。
https://github.com/mi759/haikucodespace/blob/6ca8bfe0cde853c187543bf8cfea1783ab0dcb65/.devcontainer/docker-compose.yml#L8-L9

感想

一度設定さえしてしまえば、ローカル機のスペックに悩まされずどこからでも快適に開発できるので嬉しいです。さすがにメイン開発環境として使おうとすると費用がかかってしまいますが……。

現状の大きな課題として起動時間の長さがあるので、今後はそこを解消するためのイメージの事前ビルド等を試してみたいと思います。

参考文献

長いので格納
脚注
  1. たとえばフロントエンドとバックエンドAPIが別リポジトリに分かれているケースなどが当てはまるでしょう。 ↩︎

  2. 参照: https://code.visualstudio.com/remote/advancedcontainers/connect-multiple-containers ↩︎

Discussion