CI/CD パイプラインを通じてアプリケーションを実行し、すべてのテストにパスしたのに、アプリケーションをターゲット環境にデプロイすると期待どおりに動作しない、という問題にぶつかったことはありませんか。本番環境でのアプリケーションの挙動を正確に予測できないという事態は、実際に多々発生しており、チームの悩みの種となっています。スモークテストは、このようなエラーを早期に発見するために、アプリケーションの重要なコンポーネントや機能だけを大まかに検証するテストです。デプロイ後にアプリケーションが期待どおりに機能することも確認できます。通常、スモークテストはすべてのアプリケーション ビルドで実行されます。より時間のかかる広範なテストに踏み切る前に、必要不可欠な基本機能に問題がないことを検証するわけです。スモークテストを実装すれば、高速なフィードバック ループの構築が促進されるため、ソフトウェア開発ライフ サイクルにおいて大きなメリットが期待できます。
この記事では、CI/CD パイプラインのデプロイ ステージにスモークテストを追加して、デプロイ後のアプリケーションの基本機能をテストする方法について紹介します。
使用するテクノロジー
この記事では次のテクノロジーについて言及します。
- GitHub
- CircleCI
- Docker
- Kubernetes
- Google Kubernetes Engine (GKE)
- Bash
- smoke.sh - asm89 によるオープンソースのスモークテスト フレームワーク
- Pulumi
前提条件
今回は、以前の記事「Infrastructure as Code を使用したパイプラインからのリリースの自動化」で使用した構成とコードを取り扱います。ソースコードの全文はこちらのリポジトリでご確認ください。
スモークテスト
スモークテストは、予期せぬビルド エラーや接続エラーを検出し、新しいリリースをターゲット環境にデプロイした後にサーバーから期待どおりのレスポンスがあるかどうかを検証するのに効果的です。シンプルなスモークテストでは、たとえばアプリケーションがアクセス可能で、特定の応答コード (OK 200
、300
、301
、404
など) を返すかどうかをチェックできます。この記事で紹介するスモークテストでは、デプロイ後のアプリがサーバー コード OK 200
を返すこと、デフォルトのページ コンテンツとして期待どおりのテキストが表示されることを確認します。
スモークテストが追加されていない CI/CD パイプライン
まず、サンプルのパイプライン構成を見てみましょう。単体テストの実行と、Docker イメージのビルドおよび Docker Hub へのプッシュを行うパイプライン構成です。Infrastructure as Code (Pulumi) を使用して、新しい Google Kubernetes Engine (GKE) クラスタをプロビジョニングし、このリリースをクラスタにデプロイします。このサンプル パイプライン構成は、スモークテストを実装していません。このサンプル パイプラインを実行すると、新しい GKE クラスタが作成され、手動で pulumi destroy
コマンド (作成されたすべてのインフラストラクチャを終了) を実行するまで有効になります。
注意: インフラストラクチャを終了しないと、予想外のコストがかかることがあります。
version: 2.1
orbs:
pulumi: pulumi/pulumi@1.0.1
jobs:
build_test:
docker:
- image: circleci/python:3.7.2
environment:
PIPENV_VENV_IN_PROJECT: 'true'
steps:
- checkout
- run:
name: Install Python Dependencies
command: |
pipenv install --skip-lock
- run:
name: Run Tests
command: |
pipenv run pytest
build_push_image:
docker:
- image: circleci/python:3.7.2
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Build and push Docker image
command: |
pipenv install --skip-lock
pipenv run pip install --upgrade 'setuptools<45.0.0'
pipenv run pyinstaller -F hello_world.py
echo 'export TAG=${CIRCLE_SHA1}' >> $BASH_ENV
echo 'export IMAGE_NAME=orb-pulumi-gcp' >> $BASH_ENV
source $BASH_ENV
docker build -t $DOCKER_LOGIN/$IMAGE_NAME -t $DOCKER_LOGIN/$IMAGE_NAME:$TAG .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push $DOCKER_LOGIN/$IMAGE_NAME
deploy_to_gcp:
docker:
- image: circleci/python:3.7.2
environment:
CLOUDSDK_PYTHON: '/usr/bin/python2.7'
GOOGLE_SDK_PATH: '~/google-cloud-sdk/'
steps:
- checkout
- pulumi/login:
access-token: ${PULUMI_ACCESS_TOKEN}
- run:
name: Install dependencies
command: |
cd ~/
sudo pip install --upgrade pip==18.0 && pip install --user -r project/reqs.txt
curl -o gcp-cli.tar.gz https://dl.google.com/dl/cloudsdk/channels/rapid/google-cloud-sdk.tar.gz
tar -xzvf gcp-cli.tar.gz
echo ${GOOGLE_CLOUD_KEYS} | base64 --decode --ignore-garbage > ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json
./google-cloud-sdk/install.sh --quiet
echo 'export PATH=$PATH:~/google-cloud-sdk/bin' >> $BASH_ENV
source $BASH_ENV
gcloud auth activate-service-account --key-file ${HOME}/project/pulumi/gcp/gke/cicd_demo_gcp_creds.json
- pulumi/update:
stack: k8s
working_directory: ${HOME}/project/pulumi/gcp/gke/
workflows:
build_test_deploy:
jobs:
- build_test
- build_push_image:
requires:
- build_test
- deploy_to_gcp:
requires:
- build_push_image
このパイプラインは新しい GKE クラスタに新しいアプリ リリースをデプロイしますが、この自動化が完了した後、アプリケーションが実際に稼働しているかどうかを知る術はありません。アプリケーションのデプロイが完了し、新しい GKE クラスタ内で正しく機能しているかどうかをすばやく確認するにはどうしたらよいでしょうか。ここで CI/CD パイプラインにスモーク テストを実装すると、デプロイ後のアプリケーションのステータスをすばやく簡単に確認することができます。
スモークテストの作成方法
スモークテストを作成するにはまず、アプリケーションの機能の検証に必要なステップを定義するテストケースを開発します。テストケースの開発は、検証したい機能を特定し、そのテスト シナリオを作成するプロセスの一部になっています。今回のチュートリアルでは、意図的にテスト範囲を非常に狭くしました。この場合、デプロイ後のアプリケーションにアクセスできるかどうか、デフォルトのページに期待どおりの静的テキストが表示されるかどうかの検証が最重要事項です。
以下では、このスモークテスト用のテストケースをどのようにして開発したかについて説明します。個人的には、自分の開発スタイルに沿って、テスト対象のアイテムの概要をまとめ、一覧を作成するようにしています。概要には、そのアプリ向けにスモークテストを開発したときに考慮した要素を記載します。
- 言語/テストのフレームワーク
- Bash
- smoke.sh
- テストの実行タイミング
- GKE クラスタの作成後
- テスト対象
- テスト: デプロイ後のアプリケーションにアクセスできるか
- 期待される結果: サーバーがコード
200
を返す
- 期待される結果: サーバーがコード
- テスト: デフォルト ページに「Welcome to CI/CD」と表示されるか
- 期待される結果:
TRUE
- 期待される結果:
- テスト: デフォルト ページに「Version Number:」というテキストが表示されるか
- 期待される結果:
TRUE
- 期待される結果:
- テスト: デプロイ後のアプリケーションにアクセスできるか
- テスト後のアクション (テストにパスしたかどうかにかかわらず実行)
- 標準出力にテスト結果を出力
- GKE クラスタと関連インフラストラクチャを破棄
pulumi destroy
を実行
このチュートリアルのテストケースの概要は完全で、テストしたい項目に関する情報が明白に記載されています。これは「テスト スクリプト」とも呼ばれます。今回は、bash ベースのオープンソースのスモークテスト フレームワーク smoke.sh
(asm89 作成) を使用してスモークテストを作成しますが、お好みの言語またはフレームワークで作成してかまいません。ここで smoke.sh
を選択したのは、実装しやすく、オープンソースのフレームワークだからです。では、smoke.sh
フレームワークを使用して、このテスト スクリプトを作成する方法について見ていきましょう。
smoke.sh を使用してスモークテストを作成する
smoke.sh
フレームワークのドキュメントに、フレームワークの使い方が記載されています。以下には、サンプル コードのリポジトリの test/
ディレクトリにある smoke_test
ファイルを使用する場合のコード例を示します。
#!/bin/bash
. tests/smoke.sh
TIME_OUT=300
TIME_OUT_COUNT=0
PULUMI_STACK="k8s"
PULUMI_CWD="pulumi/gcp/gke/"
SMOKE_IP=$(pulumi stack --stack $PULUMI_STACK --cwd $PULUMI_CWD output app_endpoint_ip)
SMOKE_URL="http://$SMOKE_IP"
while true
do
STATUS=$(curl -s -o /dev/null -w '%{http_code}' $SMOKE_URL)
if [ $STATUS -eq 200 ]; then
smoke_url_ok $SMOKE_URL
smoke_assert_body "Welcome to CI/CD"
smoke_assert_body "Version Number:"
smoke_report
echo "\n\n"
echo 'Smoke Tests Successfully Completed.'
echo 'Terminating the Kubernetes Cluster in 300 second...'
sleep 300
pulumi destroy --stack $PULUMI_STACK --cwd $PULUMI_CWD --yes
break
elif [[ $TIME_OUT_COUNT -gt $TIME_OUT ]]; then
echo "Process has Timed out! Elapsed Timeout Count.. $TIME_OUT_COUNT"
pulumi destroy --stack $PULUMI_STACK --cwd $PULUMI_CWD --yes
exit 1
else
echo "Checking Status on host $SMOKE... $TIME_OUT_COUNT seconds elapsed"
TIME_OUT_COUNT=$((TIME_OUT_COUNT+10))
fi
sleep 10
done
次に、smoke_test ファイルで何が行われているかを説明します。
smoke_test ファイルの解説
ファイルの先頭から見ていきましょう。
#!/bin/bash
. tests/smoke.sh
上のスニペットでは、Bash バイナリを使用するよう指定しています。また、smoke_test
スクリプトにインポートまたは追加する、コアの smoke.sh
フレームワークのファイル パスを指定しています。
TIME_OUT=300
TIME_OUT_COUNT=0
PULUMI_STACK="k8s"
PULUMI_CWD="pulumi/gcp/gke/"
SMOKE_IP=$(pulumi stack --stack $PULUMI_STACK --cwd $PULUMI_CWD output app_endpoint_ip)
SMOKE_URL="http://$SMOKE_IP"
上のスニペットは、smoke_test
スクリプトで使用する環境変数を定義しています。以下に、環境変数の説明を一覧します。
PULUMI_STACK="k8s"
- pulumi が pulumi アプリ スタックを指定するために使用します。PULUMI_CWD="pulumi/gcp/gke/"
- pulumi インフラストラクチャ コードのパス。SMOKE_IP=$(pulumi stack --stack $PULUMI_STACK --cwd $PULUMI_CWD output app_endpoint_ip)
- GKE クラスタ上のアプリケーションのパブリック IP アドレスを取得する Pulumi コマンド。この変数はスクリプト全体で使用します。SMOKE_URL="http://$SMOKE_IP"
- GKE クラスタ上のアプリケーションの url エンドポイントを指定します。
while true
do
STATUS=$(curl -s -o /dev/null -w '%{http_code}' $SMOKE_URL)
if [ $STATUS -eq 200 ]; then
smoke_url_ok $SMOKE_URL
smoke_assert_body "Welcome to CI/CD"
smoke_assert_body "Version Number:"
smoke_report
echo "\n\n"
echo 'Smoke Tests Successfully Completed.'
echo 'Terminating the Kubernetes Cluster in 300 second...'
sleep 300
pulumi destroy --stack $PULUMI_STACK --cwd $PULUMI_CWD --yes
break
elif [[ $TIME_OUT_COUNT -gt $TIME_OUT ]]; then
echo "Process has Timed out! Elapsed Timeout Count.. $TIME_OUT_COUNT"
pulumi destroy --stack $PULUMI_STACK --cwd $PULUMI_CWD --yes
exit 1
else
echo "Checking Status on host $SMOKE... $TIME_OUT_COUNT seconds elapsed"
TIME_OUT_COUNT=$((TIME_OUT_COUNT+10))
fi
sleep 10
done
上のスニペットがすべての魔法の源となる部分です。while
ループは、条件が true になるかスクリプトが終了するまで実行されます。この場合、ループは curl
コマンドを使ってアプリケーションが OK 200
応答コードを返すかどうかをテストします。このパイプラインは新しい GKE クラスタをゼロから作成するので、Google Cloud Platform でトランザクションが発生します。スモークテストを始める前に、この処理が完了するのを待ちましょう。まず GKE クラスタとアプリケーション サービスを稼働させる必要があります。$STATUS
変数には、curl リクエストの結果が入ります。次に、値が 200
かどうかのテストが行われます。それ以外の場合、ループは $TIME_OUT_COUNT
変数を 10 秒インクリメントして、10 秒待ってから curl
リクエストをもう一度実行します。これはアプリケーションが応答するまで繰り返されます。クラスタとアプリケーションが稼働していて、応答を返す場合、STATUS
変数が 200
応答コードを生成し、テストの残りの部分が実行されます。
smoke_assert_body "Welcome to CI/CD"
および smoke_assert_body "Version Number: "
ステートメントで、Web ページに Welcome と Version number のテキストが表示されるかどうかをテストします。結果が false なら、テストは失敗し、パイプラインのエラーになります。結果が true なら、アプリケーションは 200
応答コードを返し、テキスト テストの結果が TRUE
になります。するとスモークテストがパスして、pulumi destroy
コマンドが実行され、このテストケースのために作成されたすべてのインフラストラクチャが終了します。このクラスタはもう必要ないので、テストで作成されたすべてのインフラストラクチャが終了します。
このループには、アプリケーションが $TIME_OUT
値を超過したかどうかを確認する elif
(else if) ステートメントも含まれています。elif
ステートメントは、例外処理の一例で、予期しない結果が発生した場合の制御に使用します。$TIME_OUT_COUNT
値が TIME_OUT 値を超過した場合、pulumi destroy
コマンドが実行され、新しく作成されたインフラストラクチャが終了します。さらに exit 1
コマンドにより、パイプライン ビルド プロセスが失敗します。テスト以外の目的にはこのインフラストラクチャは必要ないため、テスト結果にかかわらず、GKE クラスタは終了します。
パイプラインにスモークテストを追加する
ここまでの手順では、サンプルのスモークテストと、テストケースの開発プロセスについて説明してきました。ここでは、上記の CI/CD パイプライン構成にスモークテストを統合します。deploy_to_gcp
ジョブの pulumi/update
ステップの下に、新しい run
ステップを追加します。
...
- run:
name: Run Smoke Test against GKE
command: |
echo 'Initializing Smoke Tests on the GKE Cluster'
./tests/smoke_test
echo "GKE Cluster Tested & Destroyed"
...
上のスニペットは、既存の CI/CD パイプラインに smoke_test
スクリプトを統合し、実行する方法を示しています。パイプラインに新しい run ブロックを追加すると、各パイプライン ビルドが実行中の GKE クラスタ上のアプリケーションをテストし、アプリケーションがすべてのテストケースをパスしたという検証結果を返すようになります。特定のリリースをテスト済みのターゲット環境 (この例の場合は Google Kubernetes クラスタ) にデプロイすると、名目上実行されます。
まとめ
この記事では、CI/CD パイプラインでスモークテストと Infrastructure as Code を利用して、ターゲット デプロイ環境でビルドをテストする利点について詳しく紹介しました。ターゲット環境でアプリケーションをテストすると、同じターゲット環境にデプロイしたときのアプリケーションの挙動について貴重なインサイトが得られます。CI/CD パイプラインにスモークテストを実装することで、アプリケーション ビルドの信頼性がいっそう高まります。
ご質問、ご意見、ご感想がございましたら、Twitter で @punkdata 宛にメンションしてください。
最後までお読みいただき、ありがとうございました。