こんにちは。
株式会社ラクスで先行技術検証をしたり、ビジネス部門向けに技術情報を提供する取り組みを行っている「技術推進課」という部署に所属している鈴木(@moomooya)です。
ラクスの開発部ではこれまで社内で利用していなかった技術要素を自社の開発に適合するか検証し、ビジネス要求に対して迅速に応えられるようにそなえる 「技術推進プロジェクト」というプロジェクトがあります。
このプロジェクトで「WEBアプリケーションのDockerコンテナ移行」にまつわる検証を行なったので、その報告を共有しようかと思います。
今回はコンテナ化そのものの話よりも、コンテナ化する際の環境や、対象のアプリケーション設計についてなど、周辺の話が多いです。
ちなみに中間報告時点で公開した記事はこちらになります。
本検証での構成環境
既存のアプリケーション実行環境
- Apache + PHP
- Postfix
- PostgreSQL
- これらは1台のWEBサーバーに相乗りする形でインストール
アプリケーション概要
- PHPで実装されたWEBアプリケーション
- メール関連アプリケーション
- 記録されている最古のコミットが2012年
- 開発開始自体は2001年ごろ=20年以上の長寿サービス
検証した環境
- Apache + PHP + Postfixの相乗りコンテナ
- PostgreSQLコンテナ
- 今回はCIでテストさせたかったのでストレージもコンテナ内
Apache + PHPとPostfixをなんで相乗りさせているの? と思われるかもしれませんが理由は後述。
他にもApache + PHPをphp-fpm使って独立させないのか? という話も出ましたが、コンテナ化「+α」の部分であり、コンテナ化自体に必須ではないので今回はスコープ外としています。 まずはコンテナに乗せることが優先です。
本検証で目指したこと、既存の課題
先述の通り、コンテナ上で動作させることを最優先としています。いわゆるコンテナファースト的な設計への作り直しはスコープ外にしています1。
また、本番運用を視野に入れると考慮しなければならないことが増えてしまうので、今回はコンテナベースのCI、とりわけE2Eテストを可能にするまでです。「本番までコンテナに統一しないと意味ないのでは?」という声もありそうですが、既存の課題として以下のような物があり、開発環境のコンテナ化だけでも恩恵が充分あると判断しています2。
- 個人ごとに仮想マシン(VM)上に開発環境を作っているため環境差異によるトラブルが発生していた
- 新規参加メンバーの環境構築に手間がかかっていた
- 過去バージョン環境の再現が手間
- チーム外メンバーがサポートする際に、動作環境の調達が面倒だった
最大の理由は1つ目ですね。ベースとなるVMイメージは共通のものを使用していますが、VMは状態を保持してしまうので不意に差異が発生しがちです。こまめに再作成すればいいのかもしれませんが、面倒3なのでなかなかそうもいかず、という感じです。
コンテナ化の際に検討および対応したこと
実行環境構築手順の整備
最初に直面したのはOSインストールから始まる環境構築手順が見つからないことでした。
比較的最近開発され始めたサービスであれば大抵の場合は用意されていると思いますが、今回対象にしたアプリケーションではテンプレートとなるVMをコピーして構築する運用だったため、テンプレートVMを解析して手順作成を行うことになりました4。
コンテナ化する際には環境構築手順をDockerfileに書き起こす形で進めていくため、ゼロから構築することができる手順が必要です。
手順はアプリケーションのソースコードレポジトリと一緒に管理しておく、少なくとも手順への参照情報が記載されているとコンテナ化にあたってスムーズに準備が進められるでしょう。手順はコード化されていると理想的ですが、GitHubやGitLabなどにはWiki機能もあるので、そこにまとめておいても良いと思います。
アプリケーション間の連携
1コンテナあたり1つの役割を持たせるのがコンテナとしてはセオリーだと思いますが、現状のアプリケーションは全ての機能が1つになっています。
このアプリケーションの分割を検討するにあたって、PHPアプリケーションとミドルウェアアプリケーションとの連携方法がコンテナを分割できるかどうかの分かれ目となりました。
このアプリケーションに含まれる主だった連携内容には以下のようなものがあります。
- PHPとPostfixの連携
- PHPとPostgreSQLの連携
- PHPからPostgreSQLへのデータ入出力
これらのうち問題となったのは1つ目のPHPとPostfixの連携です。
これらは直接設定ファイルを編集していたり、systemctl postfix reload
といったコマンドで設定の再読み込みを行なっていたり、php -f hogehoge.php
といったコマンドで実行していたりしました。このようにOSを介した連携を行なっている部分は別コンテナにしづらく、HTTPなどのTCP/IPでやりとりするインタフェースを新たに実装する必要があります5。
一方で2つ目のPHPとPostgreSQLの連携はTCP/IPに則っているため、問題なく別コンテナにすることができました。コンテナイメージもPostgreSQLのオフィシャルイメージを使うことができたので構築手順を簡略化できました。
システムコマンドの利用
既存のアプリケーションでは26種類260箇所のシステムコマンド呼び出しがありました。
もし、システムコマンドの呼び出し部分を置き換えるとすると改修に必要な工数の概算は以下のようになります。
- 修正コスト概算
- 1箇所あたりの改修コスト概算 2時間
- 影響調査: 1時間
- 修正:0.5時間
- テスト:0.5時間
- 2時間 x 260箇所 = 520時間 = 3.25人月
- 互換性や依存関係などにより追加のコストが必要になる可能性あり
- 1箇所あたりの改修コスト概算 2時間
これに対して、メリットとデメリットは以下のようになります。
- 得られるメリット
- 余計なコマンドがインストールされないためよりセキュアになる
- コンテナイメージが小さくなる
- 被るデメリット
- 3.25人月の修正コストがかかる
対応しなかった場合のデメリットは以下のようなものです。
- コマンドがインストールされることでセキュリティ的に若干劣る
- 既存の環境では存在しており、使うコマンドだけインストールするため既存よりはセキュアになる
- コンテナイメージが大きくなる
- といってもLinuxコマンド26種類の容量なのでそれほど大きな差にはならない
本来であればコンテナ内にインストールされるコマンドは最小限に抑えるべきでしょうが、修正コストと得られるメリット/デメリットを検討した結果、コンテナイメージをビルドする際に、必要なシステムコマンドをインストールする方法を選択しました。
CI Runnerの不足
CIにはGitLab CIを利用しましたが、Shared RunnerがなかったためCI Runnerを用意するところから準備しました。
CI Runner用のマシンリソースを用意できると良かったのですが、今回は検証期間中のみ使用するため各自のPCにCI Runnerのコンテナを起動し、Shared Runner(実際にはGroup Runner)として登録しました。
このあたりは普段のCI環境整備の一環として用意しておくと楽に進めることが出来ると思います。
ヘッドレスChromeを使ったE2Eテストでは--disable-dev-shm-usage
コンテナ環境では/dev/shm
の容量が小さいため、デフォルトの状態だとクラッシュするようです。
この事象自体はchromiumのIssueにも上がっている問題で、回避策としてはChromeの起動オプションに--disable-dev-shm-usage
を付与して/dev/shm
の代わりに/tmp
を使わせて回避するのが良いようです。
別リポジトリに格納されたE2Eテストコード
今回対象としたアプリケーションではE2Eテスト用のコードがメインのコードベースと別リポジトリで管理されていました。
今回、Docker Composeを利用していましたが、アプリケーションのDockerfileとテストコードの位置はそれぞれ相対パスで指定する必要があります。今回は既存のルールとして特定のパスにそれぞれのリポジトリを配置するルールだったので、リポジトリをまたいで相対パスでの指定が出来ました。
しかし実際にはdocker submodule
などを利用してリポジトリ内のパスとして参照できるようにするか、そもそも同一リポジトリ内で管理するようにしたほうが良いでしょう。
アプリケーションサーバーとDBサーバーが分離していない前提のテストコード
アプリケーション本体はDBサーバー参照先も環境変数などで管理していることが多いと思いますが、テストコードはどちらもローカルホストである前提のコードが書かれていました6。
今回はDBサーバー参照先を地道に書き換えて対応しました。
もしかしたらこんなことも必要かも
ACME対応ローカルCA
弊社では各拠点ごとに外部ネットワークに接続する際、クライアント認証を行っています。そのためdnfコマンドやcomposerコマンドでパブリックなパッケージレジストリからダウンロードする際にはコンテナイメージ内にもクライアント証明書を持つ必要があります。
対処としてはコンテナイメージをビルドするタイミングでクライアント証明書を含める必要があります。しかも東京、大阪など拠点別に証明書は用意されているため、すべての証明書を含めないと手元で起動する際に通信出来ません。 しかし、クライアント証明書を含んだ状態のコンテナイメージをコンテナレジストリに格納するのはセキュリティ上好ましくありません。
今回はコンテナレジストリを使わなかった都合上、各検証メンバーの手元でコンテナビルドをしていたため必要にはなりませんでしたが、本格的に運用していくことを考えると、コンテナ起動時にLet's Encryptのように自動で証明書を取得できるようなローカル認証局を用意するのが良さそうに思えます。
今回は検証スコープ外としましたが、ACMEというプロトコルに対応した認証局を用意することで実現できそうです。
使用しているgitコマンドパスとgitのオプション設定に注意
コンテナというよりもgitの話なのですが、今回の検証でWindows + WSL2環境でDockerを動かしていた環境がありました。この環境上でcloneしたコードでdocker-compose build
がエラーになるという問題が発生しました。
結論としてはgitのautocrlf設定が期待どおりに設定されていなくて、docker-compose.ymlの改行コードが変換されてしまっていた、という話だったのですがこれが起きた経緯が以下のようなものでした。
- WSL2上のgitはautocrlf=falseとなっており、改行コードが変換されることはないはずだった
- PhpStormでgit cloneしていたが、PhpStormはWindows上のgitを参照していた
- Windows上のgitはデフォルト設定のautocrlf=trueでインストールされていた
わかってしまえば単純なミスだったのですが、私自身はWindows環境においてはWSL2上のシェルからコマンドラインでgitを使っていて盲点だったので記録しておきます。
コンテナレジストリの用意
弊社では一部を除いてコンテナを用いた開発を行われていないので社内から使えるプライベートなコンテナレジストリがありませんでした。
今回の検証中ではビルド→E2Eテスト、の間のコンテナイメージの受け渡しをマルチステージビルドで賄ったためなんとか使わずにすみましたが、実際の開発環境で運用する際にはコンテナイメージを共有したいのでコンテナレジストリが欲しくなると思います。
Dockerfileの管轄をどうするか
アプリケーション開発チームとインフラチームが分かれている場合、Dockerfileやdocker-compose.yamlの更新をどちらのチームがどうやるかという問題が発生します。 ここでいうインフラチームはミドルウェア周りを担当しています。
- 両チームが同じファイルを更新する
- インフラチームが作成したコンテナイメージをベースイメージとして、アプリケーション開発チームが仕上げる
という方法が考えられると思います。今回はインフラチームと一緒の検討を行ったわけではないので推測となりますが、以下理由により同じファイルを扱うのが良いと考えています。
- 別ファイルにすると取り込み漏れが発生しそう
- 例えば次期バージョンで必ず入れないといけないミドルウェアのセキュリティアップデートがリリースされない、など
FROM image:latest
のように必ず最新版を参照するようにすれば必ず反映されるが……- リリース直前にベースイメージが変わってしまう可能性がある
- テストがやり直しになる
- ベースイメージはやはりバージョン指定で使いたい
- リリース直前にベースイメージが変わってしまう可能性がある
- 同一ファイルの場合、更新制御にPull Requestなどのバージョン管理システムの機能が利用できる
- 別ファイルでも利用はできるが共通管理じゃないとやりにくそう
まとめ
コンテナはできるなら導入したほうがいい7。
ただし巨大プロジェクトを立ち上げて移行する必要はあまりなく、コンテナ化に向けて障害となる課題を一つずつ解消していく形で進めることができそうです。 そしてそれら一つ一つも開発プロセスや設計の改善となっているため、無駄がないという印象を得られました。
- 直接的に売上が増えるわけでもないので「コンテナ化」だけでそんな予算取れないです。あと一度の改修は出来るだけ小さくしたいし、コンテナ化してCI回しやすくなれば改修の安全性も上がるので後から追加で直していくのが無難だと思っています。↩
- もちろん段階的には本番環境でのコンテナ動作も目指したいとは思っていますが。風呂敷を広げすぎると計画が頓挫していつまで経っても改善出来なかったりするので、分割できる問題はできるだけ小さくしたいです。↩
- 面倒というのはサボるとかそういう話ではなく、手間がかかって非効率という意味合いです。面倒な作業を面倒と認識することは改善する上で重要です。エンジニアの三大美徳である怠惰にも通ずる話ですね。↩
- 「テンプレートとなるVMを構築する手順がないのはおかしいよね」ということで探していたら、テンプレートの解析が終わった頃に参考資料が見つかった。↩
- むりやりsshで実行することも可能だとは思いますが、今後の柔軟性を考えると一般的なRESTなどのTCP/IP通信で連携できるようにしたほうが良いでしょう。↩
- テストコードはなるべく静的なコードにすることが多いと思うので、意外とありがちなのではないかと思っています。↩
- コンテナでの動作を前提としたツールが増えているため、今後の開発プロセスを考えると必要になると思われる。↩