はじめに
こんにちは。データ推進室データテクノロジーラボ部アーキグループ(以下アーキ G)所属の舛谷(ますたに)です。アーキ G では、これまで AI/ML アプリケーションの稼働環境を Kubernetes で構築・運用・管理してきました。
コンテナ技術が普及しつつありますが、アプリケーションはコンテナ上で動かしていても、開発環境は仮想サーバで運用している方も多いのではないでしょうか。今回は、認証サービスである Okta の導入にあたり( 関連記事参照 )、コストを最適化するために仮想サーバの数を減らすことを目指しました。この機会に開発環境のコンテナ化に取り組み、多くの学びがありましたので、この記事でご紹介します。
先に結論をお伝えすると、20 台ほどあった開発用仮想サーバは接続用のサーバ 1 台に削減できました。
取り組みの前提と方針
前提
我々の運用環境では、複数のプロダクトが 1 つの Kubernetes クラスター内で Namespace ごとに稼働しています。プロダクトごとに開発資材などを分離する必要があるため、開発サーバはプロダクトごとに用意しています。開発サーバで行う作業は主に、コンテナイメージのビルド&プッシュ、Kubernetes クラスターに対する操作、およびアプリケーションのデバッグ等です。
方針
仮想サーバの数を減らすために、全てのプロダクト用に用意している開発環境をコンテナに移行します。ただ移行するだけでなく、コンテナのメリットを最大限活かすために改善も行います。主な改善点は以下の通りです。
-
開発者ごとに開発環境を構築できる。
これまではプロダクトごとに環境を用意していました。そのため、複数の開発者が同じサーバを利用していました。コンテナ化することで、Kubernetes クラスター内に必要なときに必要なだけコンテナを起動できるようになり、開発者ごとに専用の環境を提供しやすくなります。 -
カスタマイズが可能になる。
一人一つの開発環境を提供することで、個別のカスタマイズも可能になります。ベースイメージを提供し、それを各開発者が好みに応じて変更できるようにします。
システム設計
アーキテクチャ
本システムのアーキテクチャを以下の図に示します。
踏み台サーバは、複数の環境に接続するための入り口となる既存サーバです。Launch サーバには、実際に開発コンテナの起動や接続などを実行する機能を持たせています(我々の環境の都合でこのようにしていますが、踏み台サーバから直接接続する構成でも問題ありません)。開発者はまず Launch サーバに SSH 接続し、その後、開発コンテナに接続します。
コンテナ
データの永続化のために外部ストレージをマウントします。我々は Alibaba Cloud を利用しているので、マネージドサービス File Storage の NAS を利用して Persistent Volume(PV) としてマウントします。 コンテナ内でコンテナイメージをビルドする必要があるため、DinD(Docker in Docker)が必要になります。 後述しますが、NAS の領域を Docker のデータ領域にすると通信のオーバーヘッドでビルドが極端に遅くなるため、Pod のローカル領域を利用するようにします。
マニフェスト
Kubernetes 上にデプロイするためには、マニフェストの用意が必要です。今回は開発者それぞれに個別の環境を提供することが求められます。このために、StatefulSet を使用してコンテナ(Pod)とボリュームの同一性を担保し、常に同じ Pod に同じ PV がマウントされるようにします。
マニフェストはテンプレート化した上で、リソース名などをユーザごとにユニークにするために、Helm を利用します。後述する CLI と連携して、ユーザごとにチャートを作成し、ユニークな StatefulSet をデプロイします。
さらに、コンテナの立ち上げっぱなしを防止するために、Alibaba Cloud が提供する拡張機能である CronHPA を導入します。これにより、時間に応じたスケーリングが可能になります。
CLI
Helm や Kubernetes に対する操作には学習コストがかかります。開発環境として利用するにあたっては、開発者に余計な学習を強いることなく、必要な操作をシンプルなコマンドで実行できる CLI を提供しました。
詳細は割愛しますが、主に以下の操作を実現しています。
1. ユーザごとにユニークなHelm Chartのインストール
2. コンテナの起動
3. コンテナの停止
課題と解決
課題 ① コンテナイメージのビルドがめちゃくちゃ遅い
内容
コンテナ化を進める中で最初に直面した問題は、Docker ビルドが極端に遅くなったことでした。この原因は、Docker がアクセスするデータ領域を NAS に設定していたことにありました。コンテナ内でビルドする、いわゆる DinD(Docker in Docker)を利用する場合、ストレージドライバーは VFS となります(参考: VFS driver )。
VFS を利用すると、ビルドレイヤごとに DeepCopy が取られるため、NAS へのアクセスが頻繁に発生し、その結果として I/O のオーバーヘッドがかかり、ビルドが極端に遅くなってしまいました。
解決策
Docker がアクセスする領域をコンテナのローカルディスクに変更しました。これにより、DeepCopy 時のオーバーヘッドが減り、大幅にビルド時間を削減することができました。Dockerfile の設定にも依存しますが、我々のユースケースだと 30 分ほどかかっていたビルドが 1 分 30 秒程度まで短縮できました。
課題 ② ビルドキャッシュが利用できない
内容
これは課題 ① を解決した際の副作用です。Docker が利用するデータをローカルディスクに保存すると、コンテナを停止するたびにキャッシュが削除されてしまいます。つまり、再起動後の初回ビルドはキャッシュなしで実行されるため、時間がかかります。
解決策
この問題を解決するために、Inline Cache を利用しました(参考: Inline cache )。この方法を用いると、初回ビルドしたイメージを 2 回目以降のビルドでキャッシュとして利用することができます。この結果、ローカルにキャッシュを持たなくても過去のビルドを参照できるため、ビルド時間を短縮することができました。
課題 ③ ビルド時にストレージの容量がとんでもなく消費される。
内容
DinD を利用する際、ストレージドライバーが VFS になるため、ビルドレイヤごとに DeepCopy が発生することを前述しました。これにより、ローカルディスクの使用量が大幅に増加します。Dockerfile 内で実行するライブラリなどのインストールの内容も DeepCopy の場合、その後のすべてのレイヤに引き継がれます。我々の環境では、最終的なイメージサイズの 10 倍近いディスク容量を必要とすることもありました。
解決策
ストレージドライバーに fuse-overlayfs を利用することができます。fuse-overlayfs は、VFS と比較してストレージの効率が高く、レイヤごとの DeepCopy が発生しないため、ビルド時のストレージ使用量を大幅に削減できます。この方法により、ビルドプロセス全体でのストレージ消費を抑えることが可能となり、ローカルディスクの負荷も軽減されます。実際のイメージの内容にも依存する部分ですが我々の検証範囲では、ビルドに必要なディスクの空き容量を 90%も削減することができました。
まとめ
ここまで、コンテナで開発環境を提供した方法とそこでぶつかった課題についてご紹介しました。マイクロサービスやプラットフォームエンジニアリングなど様々な文脈で Kubernetes を採用しそれ以下の環境を抽象化していく取り組みがなされてきています。
全てが Kubernetes 上で完結する世界線もそのうち来るのではないかと思うと、今回は仮想サーバ上の環境をコンテナ化する上での課題や注意点について知ることができ、学びの多い取り組みとなったと思います。
クラウドエンジニア
舛谷友一
2022年9月リクルート入社。DTL部アーキG所属。趣味はコーヒー。コーヒー向けアプリ開発中。