バリのパイプラインをすでに作っていて、新規作成したイメージを本番環境やテスト環境に押し込む方法を知っているといいのだけれど。
さて、ここで疑問が生じる。Dockerコンテナを、どうやってテストするのだろうか?
僕たちが取れるテスト戦略はいくつかある。この記事では、それらのメリットとデメリットをそれぞれ紹介したいと思う。
「簡単な」方法
これは、一般の人向けのデフォルトのやり方だ。これは、作業をするCIサーバに依存する。この方法を取る場合、開発者はjar/rpm/deb を使った方法よりも良い選択肢であるDockerをパッケージマネジャとして使っている。CIサーバはアプリケーションコードをコンパイルし、テスト(単体、サービス、機能、その他)を実行する。ビルド成果物はDocker buildで再利用され、新規イメージが作られる。これは、中心的なデプロイ成果物となる。作られたイメージには、アプリケーションの「バイナリ」だけでなく、あらゆる依存関係やアプリケーション設定などの必須ランタイムも含まれている。
アプリケーションの移植性を得られるようになった反面、開発やテストの移植性は失われつつある。僕たちは、CIの外部で全く同じ開発環境やテスト環境を再現することができない。新しいテスト環境を作るには、テストツール(正しいバージョンとプラグイン)をセットアップし、ランタイムやOSの設定を行い、同じバージョンのテストスクリプトと、恐らくテストデータを用意する必要がある。
こうした問題を解決することで、次に進むことができる。
アプリケーション&テストコンテナを使った方法
ここで、必要なパッケージ、テストツール(特定のバージョン)、テストツールプラグイン、テストスクリプト、テスト環境など全ての必須パッケージを含む、アプリケーションの「バイナリ」を一つにまとめたものを作ってみる。
この方法のメリット:
- CI、開発、ステージング、本番環境において、再現性のあるテスト環境を持てる(同じテストツールを使って全く同じテストを実行できる)
- ある特定の時点でテストスクリプトをキャプチャするため、どのような環境でも常にそれらを再現することができる
- テストツールのセットアップや設定をする必要がない(これらはイメージの一部であるため)
この方法には重大な欠点がある:
- イメージのサイズが大きくなる(テストツール、必須パッケージ、テストスクリプト、そして恐らくテストデータをも含むため)
- 特定の設定テストでイメージのランタイム環境を汚染し、不必要な依存関係をもたらすことすらある(結合テストに必要とされる)
- テスト結果とログをどうするか(どこにどうやってエクスポートするかなど)も決めなければならない
次の簡略化した[Dockerfile]が、この方法を説明している。
FROM "":""
WORKDIR ""
\# install packages required to run app and tests
RUN apt-get update && apt-get install -y \
" and " \ # add app runtime and required packages
" and " \ # add testing tools and required packages
&& rm -rf /var/lib/apt/lists/*
\# copy app files
COPY app app
COPY run.sh run.sh
\# copy test scripts
COPY tests tests
\# copy "main" test command
COPY test.sh test.sh
\# ... EXPOSE, RUN, ADD ... for app and test environment
\# main app command
CMD [run.sh, ""]
\# it's not possible to have multiple CMD commands, but this is the "main" test command
\# CMD [/test.sh, ""]
コンテナ内テストにはもっと良い方法が必要だし、実際ある。
テストを意識したコンテナを使った方法
現在、Dockerのお約束は「Build -> Ship -> Run」である。イメージを作成し、いくつかのレジストリに送り、どこでも実行する。僕のつたない意見だが、これには重大な欠落したステップがある。テストだ。正しく、そして完全な順序は、Build -> Test -> Ship -> Runであるべきだ。
「テスト・フレンドリー」なDockerfileの構文とDockerコマンドの拡張を見てみよう。この大切なステップは、ネイティブにサポートされている場合もある。これは実際の構文ではないが、ちょっと我慢して見ていて欲しい。これから「理想的な」バージョンを定義し、それに非常に近いものを実装する方法をお見せしよう。
ONTEST [INSTRUCTION]
既存の[ONBUILD]命令に似た、特別な[ONTEST]命令を定義しよう。[ONTEST]命令は、後にイメージがテストされる時、実行されるトリガー命令をイメージに追加する。build命令はどれもトリガーとして登録される。
ONTEST命令は新規[docker test]コマンドに認識されるはずだ。
docker test [OPTIONS] IMAGE [COMMAND] [ARG...]
[docker test]コマンド構文は[docker run]コマンドに似ているが、一つの決定的な違いは、新規の「テスト可能な」イメージは自動生成され、: ‐testタグ(「test」という語が元のイメージタグに追加される)でタグ付けされることだ。この「テスト可能な」イメージは、アプリケーションイメージから([FROM])生成され、全ての命令を実行し、ONTESTコマンドの後に定義され、ONTEST CMD(またはONTEST ENTRYPOINT)を実行する。この[docker test]コマンドは、何らかのテストが失敗すると、0以外のコードを返す。テスト結果は、/var/tests/resultsフォルダを指す、自動生成されたVOLUMEに書きこまれるはずだ。
下の修正したDockerfileを見てみよう。提案された新規ONTEST命令を含んでいる。
FROM "":""
WORKDIR ""
\# install packages required to run app
RUN apt-get update && apt-get install -y \
" and " \ # add app runtime and required packages
&& rm -rf /var/lib/apt/lists/*
\# install packages required to run tests
ONTEST RUN apt-get update && apt-get install -y \
" and " \ # add testing tools and required packages
&& rm -rf /var/lib/apt/lists/*
\# copy app files
COPY app app
COPY run.sh run.sh
\# copy test scripts
ONTEST COPY tests tests
\# copy "main" test command
ONTEST COPY test.sh test.sh
\# auto-generated volume for test results
\# ONTEST VOLUME "/var/tests/results"
\# ... EXPOSE, RUN, ADD ... for app and test environment
\# main app command
CMD [run.sh, ""]
\# main test command
ONTEST CMD [/test.sh, ""]
「テストアウェアコンテナ」の実現
Dockerは、[docker-test]をコンテナ管理のライフサイクルの一部にするべきだと僕らは考えている。現在は簡単な作業ソリューションを持つ必要があり、これから理想的な状態に非常に近いものを紹介しようと思う。
前述したとおり、Dockerには非常に便利な[ONBUILD]命令がある。この命令は、後に続くbuildに、もう一つのbuild命令をトリガーできる。基本的な考え方は、[docker-test]実行時に[ONBUILD]命令を使うことだ。
[docker-test]コマンドの実行フロー:
- [docker-test]はアプリケーション[Dockerfile]で[ONBUILD]命令を探し・・・
- 元の[Dockerfile]から一時的なDockerfile.testを生成する
- [docker build]コマンドにサポートされた追加オプションとともに[docker build -f Dockerfile.test [OPTIONS] PATH]を実行する(-test がtagオプションに自動付加される)
- buildが成功すれば、[docker run -v ./tests/results:/var/tests/results [OPTIONS] IMAGE:TAG-test [COMMAND] [ARG…]]を実行する
- Dockerfile.testファイルを消す
なぜONBUIOLD命令なしで新規Dockerfile.testを作成しないのか?
なぜなら、正しいイメージ(そしてタグ)をテストするためには、image:tagにFROMを常に更新している必要があるからだ。これは簡単なことではない。
前述のやり方には限界がある。Maven:onbuildのように、[onbuild]のイメージには適さないのだ(アプリを自動でbuildするのに使用されるイメージ)
[docker-test]コマンドの簡単な実装を見てみよう。ここでは概念に焦点を当てている。[docker-test]コマンドはbuildとrunコマンドオプションを処理し、エラーを適切に処理できるべきだ。
\#!/bin/bash
image="app"
tag="latest"
echo "FROM ${image}:${tag}" > Dockerfile.test &&
docker build -t "${image}:${tag}-test" -f Dockerfile.test . &&
docker run -it --rm -v $(pwd)/tests/results:/var/tests/results "${image}:${tag}-test" &&
rm Dockerfile.test
一番面白い該当箇所に焦点を当ててみよう。
結合テストコンテナ
何十、何百のマイクロサービスから作られたアプリケーションがあるとしよう。各マイクロサービスが作られた場所に自動化されたCI/CDパイプラインがあり、CIによってテストされ、作成とテストが終わった後、何らかの環境(テスト、ステージングまたは本番)にデプロイされるとする。かなりすごいでしょう?僕たちのCIテストは、単体テスト、サービステスト(またはAPIコントラクトテスト)を実行し、各マイクロサービスを単独でテストできる。マイクロ結合テスト(サブシステム上で実行するテスト)でさえ、アドホックのやり方で作成される(例えば、[docker compose]ヘルプを使って)。
これにより、対処すべき課題が生じる。
- 実際の結合テストや長期のテスト(パフォーマンスやストレスなど)はどうなるのか?
- 耐性テスト(「chaos monkey」などを使ったテスト)はどうなるのか?
- セキュリティスキャンは?
- 時間のかかるテストやスキャンアクティビティはどうなるのか?そして、完全に運用システム上で実行されるべきなのか?
ただ単に新規マイクロサービスバージョンを本番に放り込んでみっちりモニターするのではなく、もっと良い方法があってもいい。
特別な結合テストコンテナがあってもいい。これらのコンテナは、テストツールとテスト成果物(つまりテストスクリプト、テストデータ、テスト環境設定など)のみを含んでいる。このようなコンテナのオーケストレーションや自動化を単純化するには、いくつかの約束事を定義してそれに従い、メタデータラベル(DockerfileのLABEL命令)を使わなければならない。
結合テストラベル
- test.type – テストタイプ;デフォルトの[integration];は、[integration], [performance], security], [chaos]または何らかの文字のうちの一つである場合が多い。 このラベルにより、これが「結合テストコンテナ」であると宣言している。
- test.results - テスト結果の[VOLUME];default /var/tests/results
- test.XXX – メタデータに関するその他のテスト;ラベル名には**test.**という接頭辞を使うだけでいい
結合テストコンテナ
結合テストコンテナは、ただの普通のDockerコンテナだ。アプリケーションロジックやコードは特に含まれていない。その唯一の目的は、再現性があってポータブルなテストを作成することだ。結合テストコンテナの中身におススメなのは…:
- テストツール:Phantom.js, Selenium, Chakram, Gatling, …
- テストツールランタイム:Node.js, JVM, Python, Ruby, …
- テスト環境設定:環境変数、設定ファイル、bootstrapスクリプト・・・
- テスト:コンパイル済みパッケージまたはスクリプトファイルとして
- テストデータ:テストに使われるいかなる種類のデータファイル(json, csv, txt, xml, …)
- テストスタートアップスクリプト:テストを実行するための「主要な」スタートアップスクリプト[test.sh]を作り、そこからテストツールを立ち上げればよい。
結合テストコンテナは、全てのマイクロサービスがデプロイされる運用環境(テスト、ステージング、本番環境)で実行するべきだ。これらのコンテナはその他全てのサービスとまさに同じようにデプロイされることがある。これらは同じネットワーク層をつかっているため、選択されたサービス発見方法(通常はDNS)を使って、複数のサービスにアクセスすることがある。複数のサービスにアクセスするには、実際の結合テストが必要になる。システムが複数の場所でどう動くかシミュレートし、確認する必要がある。結合テストをアプリケーションサービス内部に留めるのは、コンテナのフットプリントを増やすだけでなく、複数のサービス間に不要な依存関係を作ることになる。僕たちはこれら依存関係を、すべて結合テストコンテナのレベルに留めておく。テスト(またはテストツール)が一度コンテナ内にパッケージ化されれば、いつでも同じテストを、開発機を含むどのような環境においても再実行できる。あなたはいつでも、時間をさかのぼって結合テストコンテナの特定のバージョンを再実行できるというわけだ。
あなたはどう思うだろうか?特に[docker-test]コマンド標準化に関するフィードバックをもらえると非常にありがたい。
原文:http://blog.terranillius.com/post/docker_testing/(2016-6-29) ※元記事の筆者には直接翻訳の許可を頂いて、翻訳・公開しております。