Kubernetesを用いたGithub Codespacesのようなエフェメラルな開発環境の構築① ~コンテナの中で開発するために~

# はじめに

[Github Codepsaces](https://github.co.jp/features/codespaces)を用いると、事前に定義されたコンテナを用いて開発をすぐに開発を始める事ができます。これは必要な時にすぐに開発環境を用意し、不要になればすぐに廃棄でき、いつでもクリーンな環境で必要なツールが揃った状態で開発ができます。こういった環境だと簡単に構築・破棄できるため、環境を汚してしまうような操作も気楽に実施できます。Github CodespaceはVisual Studio Codeとセットで利用されるイメージが強いかもしれませんが、SSH接続することができ、Vim/NeovimやEmacsなどのCUIツールを用いて開発することもできます。私自信Neovimユーザであるため、こういった使い方をGithub Codespaceではしていました。こういったコンテナ内で開発が実施できる環境をCodespaceではdev containerと呼んでいます。

しかし、Codespaceは社内サービスだったり、自宅ネットワークと直接接続できないため、それらのネットワーク内のサービスと連携するような何かを開発する時には不便です。Codespacesの[dev containerをローカルで実行するような仕組み](https://code.visualstudio.com/blogs/2022/05/18/dev-container-cli)もありますが、自分のやりたいことに対して少々仕様が大きかったり依存が多かったりしたため、自分にあった方法が欲しいと考えていました。
また常にこのエフェメラルな開発環境を利用するのではなく、時には完全にローカルなマシンを使って開発をしたい場合もあり、エフェメラルな開発環境とローカルの開発環境をどのように環境を揃えるかという問題も抱えていました。

最終的にKubernetesを用いてCodespacesのような開発体験ができる仕組みを構築し、そのエフェメラルな開発環境とローカルマシンの開発環境で同じツールを使えるように整え、それらの開発環境を普段の開発で利用しています。この記事がどういった方にささるかわかりませんが、誰かに面白いと思っていただければ幸いです。

## この記事のスコープ

この記事ではまず、エフェメラルな開発環境をコンテナを用いてどのように構築・利用しているかを紹介します。
次回以降に、こちらで紹介したコンテナイメージをKubernetesでどう利用・管理しているかについてご紹介する予定です。

## コンテナを用いた開発環境

Kubernetes上に環境を構築する前から、シンプルにDockerを用いてコンテナ上に構築したエフェメラルな開発環境の中で開発をしていました。そもそもコンテナを用いて開発環境を構築しようと考えたのは、Fatih Arslanさんの[Using the iPad Pro as my development machine](https://arslan.io/2019/01/07/using-the-ipad-pro-as-my-development-machine/)という記事がきっかけでした。同じ時期にiPadを自分も購入しており、この記事に参考にしながら自分なりにDockerを用いて開発環境を構築して、過去に[記事にしています](https://qiita.com/uesyn/items/de999f43cd15c0064b93)。
上記の記事執筆時点から時代は流れ、Docker以外にもコンテナを実行する環境が整ってきました。特にKubernetesは以前より身近になった気がします。私が所属するZ LabではプライベートKubernetes as a Serviceを開発しているため、湯水のようにクラスタを作成できます。そこでDockerへ依存していた開発環境を、Kubernetes上へ引っ越すことにしました。

## 開発環境に利用するコンテナイメージ

普段Webブラウザ以外はターミナル内で可能な限り開発が完結するようにしていました。理由としては仕事ではMac、プライベートではWindows or Mac、寝る前や外出先ではiPadからSSHしたLinuxなど様々な環境から開発をする事があり、特定のOSでしか動かないアプリケーションであったり、iPadで動かないMac/PC用のアプリケーションは避けているからです。

そのためコンテナ内に自分の必要なもの全てを詰め込みたいのですが、イメージサイズがあまり大きくなるとpullに時間がかかってしまい、開発環境をたちあげるまでに時間がかかってしまいます。イメージサイズは気にせず[lazy pulling](https://medium.com/nttlabs/lazy-pulling-estargz-ef35812d73de)に頼るという手段も取れるとは思いますが、自分が利用するコンテナ実行環境全てで利用できるとは限らないため、それに頼ることはやめました。
イメージサイズを削減するために、利用する言語ごとにコンテナイメージを分けて、サイズの肥大化を抑えてる方法も考えられます。しかし開発に用いる言語毎にイメージを変更する必要が出てしまいます。言語ごとに依存するツールが異なり、扱う言語が増えればその度にイメージが増えていくのもなんとなく嫌だったので、可能な限り全部入りイメージにしたいと思いました。
そのため、自分が利用する言語で必要となる最低限のアプリケーションのみをコンテナイメージに入れて、コンテナをVM的な使い方をすることとしました。そして開発に必要なツールは必要な時に都度ダウンロードするようにしました。これについては後ほど説明します。

ベースイメージは[こちら](https://github.com/uesyn/dotfiles/tree/main/images/devbox)で管理しています。言語に関わらずほ開発で必須となるようなツールをインストールし、dockerをデーモンとして動かして、dotfilesをgithubからpullしてbootstrapするような雑なベースイメージです。今のところコンテナ内で実行するサービスとしてdockerしかありませんが、今後増やせるようにsupervisordでdockerは管理しています。
ちなみにdocker in dockerは[こちらのスクリプト](https://github.com/moby/moby/blob/master/hack/dind)を参考にしました。

私のdotfilesにはbootstrapスクリプトを用意しており、dotfilesを配置したりベースイメージにないツールをセットアップしたりします。
こちらのbootstrapスクリプトはMacでもLinuxでも動くようにしてあり、このエフェメラルな開発環境以外でも動くように作っています。これはネットワークの関係上、完全にローカルな環境を使いたい場合も、このコンテナの開発環境とほぼ同じ環境を再現できるようにするためです。

"Kubernetesを用いた"とタイトルに付けていますが、こちらのイメージはdockerでも実行できます。
以下のコマンドで誰でも試すことが可能です。

```sh
$ docker run --name devbox --privileged -d -v docker:/var/lib/docker ghcr.io/uesyn/devbox
# 起動後、少しbootstrap処理を待つ必要があります。
$ docker exec -it devbox zsh
```

## 宣言的なツールのバージョン管理

ツールを都度ダウンロードする場合に、どうやってダウンロードしてくるのか？バージョンはどう管理するのか？といったような事を気にする必要があります。
それには[aqua](https://aquaproj.github.io/)を利用しています。これを利用すると、ツールのバージョンを宣言的に管理する事ができるため、同じ設定ファイルを使えば、同じツールのバージョンを簡単に揃えることができます。Kubernetesを普段触っていると宣言的に管理できるという気持ち良さは素晴らしいです。

またaquaでは[lazy install](https://aquaproj.github.io/blog/2021/12/04/aqua-overview/#lazy-install)ができます。lazy installされたコマンドは実行時に初めて実際にインストールされるという素晴らしい機能です。しかし、普段使わないツールはlazy installで良いのですが、ほぼ利用することが確定しているものは、あらかじめダウンロードして欲しいものです。
aquaにはlazy installするものとそうでないものをタグにより指定できます。([こちら](https://github.com/aquaproj/aqua/pull/1336)のPRで対応していただきました！)以下は実際に私が利用している[aquaの設定の一部](https://github.com/uesyn/dotfiles/blob/737e3b9d2cb97746e5830b36722687154caa3dd4/aqua/aqua.yaml)です。インストールするものをpackageとして定義します。そして自分の場合予めインストールして欲しいものには`precede`タグをつけています。

```yaml
packages:
- name: ahmetb/kubectx@v0.9.4
- name: ahmetb/kubens@v0.9.4
- name: stedolan/jq@jq-1.6
  tags:
  - precede
```

以下のようにインストールコマンドを実行することで、precedeタグがついたものはインストールし、それ以外はlazy installによりインストールされます。

```sh
$ aqua install --tags precede
```

上記コマンドはdotfilesのbootstrapスクリプト内で実行しており、これによりベースイメージに存在しない必要最低限のツールをコンテナ実行時にインストールしつつ起動を可能な限り高速にできるようにしています。またコンテナのイメージにツールの全てインストールせずaquaを利用している別の理由としては、aquaはMacでも動作するため、これらのツールのセットアップはコンテナ以外でも同様に動作してくれるため、エフェメラルな環境だけでなくLinux/Macの様々な環境で同じバージョンのツールをすぐに用意できるからです。

## neovimのプラグインの管理

私はneovimユーザで、いくつかのプラグインを利用しています。ツールのバージョンをaquaにより宣言的に管理し、同じバージョンが動くようにしているのと同様に、neovimのプラグインも開発環境の構築毎にバージョンが異なるのは避けたいと考えています。自分の場合、neovimが動かないと仕事はほぼできないと言っても過言ではないため、安定したneovimの稼働を保証するためにバージョンの固定は必須でした。
neovim/vimのプラグインのほとんどはgithub上で公開されており、その多くはcloneして適切な場所に配置し、neovim/vim上からロードする設定を書くだけ利用できます。そのためバージョンを固定するためにプラグインマネージャーは利用せず、dotfilesのリポジトリ内で[git submoduleによりプラグインのコミットを固定して](https://github.com/uesyn/dotfiles/tree/main/nvim/pack/submodules/opt)利用しています。有名なプラグインマネージャーの一つである[vim-plug](https://github.com/junegunn/vim-plug)ではバージョンを固定する機能がありましたが、自分はそれほど利用しているプラグインが多くありません。そのためプラグインの管理を上手にするよりも、シンプルにプラグインを管理したいと考えました。そのためneovimのために特別なbootstrap処理がなく、dotfilesをcloneするだけでneovimのbootstrapが完結できるこの管理方法を選択しました。これによりプラグインに何かしら不都合があった場合でも、revertするだけで想定通りに動作したバージョンに戻すことができます。

## clipboardの同期

コマンドラインからテキストデータをクリップボードへリダイレクトしたいことはよくあるかと思います。例えばMacを利用している場合だとpbcopyを、Windowsではclip.exeを利用することでできます。しかし開発用のコンテナはリモート環境にあるKubernetesやdockerコンテナとして実行されているため、ローカルマシンのこれらのコマンドを実行することはできません。
そのため、クリップボード設定用制御文字列であるOSC52を利用しています。OSC52を利用すると、対応しているターミナルであれば、この制御文字列によりテキストデータをシステムのclipboardへ保存してくれます。有名なTerminalでは対応しているものが多くあり、iTerm2やWindows terminal、自分が愛用しているweztermやiPad用のSSHクライアントであるBlink shellなんかでも対応しています。
OSC52をpbcopyやclip.exeのようなコマンドで操作できるようにするために、以下の様なシェルスクリプトでを用意して利用しています。

```shell
#!/usr/bin/env bash

if [[ $# == 0 ]]; then
  payload=$(cat -)
else
  payload=$(echo -n "$1")
fi
b64_payload=$(printf "%s" "$payload" | base64 -w0)

# OSC52
printf "\e]52;c;%s\a" "$b64_payload"
```

またneovim上でも編集している内容をclipboardへ保存したくなる時があるため、OSC52を扱うための[ojroques/vim-oscyank](https://github.com/ojroques/vim-oscyank)というプラグインを利用しています。

## credentialの管理

この開発用コンテナでは、何らかの外部サービスに対してアクセスするためにそれらのcredentialが必要となるときが多々あります。開発のための環境であるため、特にgithub上のリポジトリのpush/pullのtokenはほぼ必須といっても過言ではありません。これらcredentialはコンテナへexec時に環境変数として渡し、可能な限りディスク上に保存しないように運用しています。これはセキュリティを考えてとかそういうものではなく、自分は頻繁にtokenを更新するので、その更新をホストOS側で実施すれば、更新作業が完結するようにしたかったからです。

GithubやGitlabのtokenに限って言えば、この環境変数で渡したtokenを使ってpush/pullできるように、簡単な[git credential](https://github.com/uesyn/dotfiles/blob/737e3b9d2cb97746e5830b36722687154caa3dd4/bin/git-credential-env)を実装しています。
gitにはpush/pullなどの操作時に利用するcredentialを引っ張ってくるプラグインが簡単に実装できるようになっています。詳細は公式のドキュメントの[7.14 Git のさまざまなツール - 認証情報の保存](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%81%AE%E4%BF%9D%E5%AD%98)を参照ください。
下記のスクリプトはgetアクションのみを実装してあり、合致するホスト名の場合、それ用のusername/passwordを環境変数経由で渡すようにしています。

```sh
#!/usr/bin/env bash

action=$1
if [[ "${action}" != "get" ]]; then
  exit 0
fi

host=""
protocol=""

OLF_IFS=$IFS
IFS="="
while read key value; do
  case $key in
    "protocol" ) protocol=$value ;;
    "host" ) host=$value ;;
  esac
  if [[ -n "$protocol" ]] && [[ -n "$host" ]]; then
    break
  fi
done
IFS=$OLD_IFS

if [[ "${protocol}" != "https" ]]; then
  exit 1
fi

case "${host}" in
  "github.com" )
    echo "username=x-access-token"
    echo "password=${GITHUB_TOKEN}"
    ;;
  "gitlab.com" )
    echo "username=oauth2"
    echo "password=${GITLAB_TOKEN}"
    ;;
  "${GITHUB_ENTERPRISE_HOST:-${GH_ENTERPRISE_HOST}}" )
    echo "username=x-access-token"
    echo "password=${GITHUB_ENTERPRISE_TOKEN:-${GH_ENTERPRISE_TOKEN}}"
    ;;
  "${GITLAB_ENTERPRISE_HOST}" )
    echo "username=oauth2"
    echo "password=${GITLAB_ENTERPRISE_TOKEN}"
    ;;
esac

```

このスクリプトを実行可能なファイルとしてPATHが通った場所に`git-credential-env`という名前で配置し、gitの設定ファイル内で以下のように設定すれば利用できます。

```
[credential]
        helper = env
```

## 最後に

他にも工夫がいくつかありますが、主に今回ご紹介したようにコンテナで私は開発環境を構築しています。
またコンテナイメージだけでなく、MacやコンテナではないLinuxでも、同じ環境をすぐに用意できるようにしています。
次回はこのコンテナイメージをKubernetes上でどのように管理しているかご紹介します。