この記事は AWS Advent Calendar 2021 14日目の記事です。
概要
AWSでCI/CDパイプラインを構築する場合、マネージドサービスであるCodeシリーズを利用することが多々あります。
その中でも、ビルドを実行するCodeBuildでは自動テストのツールを組み込むことが可能です。
この記事では、CodeBuildでREST APIの自動テストを実行する方法を紹介します。記事の開発言語はPythonですが、REST APIのテストは言語に依存しないメリットがあるので、他の言語やフレームワークを使っている方にも参考になればと思います。
REST APIの自動テストツールとしてはKarateを利用します。
前提知識のある方は「CodeBuildの設定」だけ読んでいただければ、大まかな内容は掴めると思います。
2021/12/15 追記
利用したソースコードはgithubに上げています。
環境
- Python 3.8
- Karate 1.1.0
- OpenJDK Corretto-11.0.13.8.1
前提知識
まず、登場するAWSの各サービスおよびKarateを簡単に紹介します。記事の内容に深く関連する部分は手厚く記載しています。
既に知識のある方は読み飛ばしていただいてかまいません。
CodePipeline
- AWSが提供するマネージドなCI/CDパイプライン構築のサービス。リポジトリ→ビルド→自動テスト→デプロイといった一連のパイプラインを構築することが出来ます。
- Codeシリーズとの統合が容易ですが、Github等の外部サービスも利用が可能です。
CodeCommit
- AWSが提供するマネージドなリポジトリサービス。他のリポジトリサービスと比較して機能がシンプルな印象です。
- Gitの認証方法としてHTTPS(AWSのIAM認証情報を利用する方法)とSSH(公開鍵による認証方法)が利用可能です。
CodeBuild
- AWSが提供するマネージドな環境でビルドを行えるサービス。リポジトリで管理されるソースをローカルへ展開せずに、クラウド上でビルドすることができます。
- サービスの中身を平たく言うと、Amazon LinuxやUbuntu等の環境がAWSのマネージドなVPCで立ち上がり、利用者が「ビルド」として定義したコマンドやその準備コマンドを実行してくれるサービスです。
- 詳細な設定やコマンドはbuildspec.ymlファイルに定義されます。
- ビルドした成果物(Artifact)はS3やEC2に保存できます。
CodeDeploy
- AWSが提供するマネージドなデプロイのサービス。
- EC2へのデプロイを例にあげると、CodeDeployはEC2にインストールされたCodeDeployエージェントと連動して動作します。具体的には、CodeDeployはEC2にインストールされたCodeDeployエージェントに指示出しを行い、実際のデプロイ作業はエージェントが実行します。
- エージェントに実行して欲しいコマンド(例えばアプリサーバーの起動/停止など)は、スクリプトファイルに記載してArtifactに含める必要があります。スクリプトファイルの名前はappspec.ymlファイルに定義します。
Karate
- REST APIの自動テストツール。
- 指定したAPIに対して、任意のリクエストを送ったり想定されるレスポンス内容を検証することができます。
- テストシナリオをDSL(Domain Specific Language)という独自のスクリプトで記載します。言語自体はわかりやすく、複雑なシナリオはJavaScriptの関数を定義して実行することも可能です。
※画像はGithubから引用
DSLは上記のように記載されます。
この例だとhttp://myhost.com/v1/cats
というURLに対して、以下のテストを行っています。
-
{'name': 'Billie'}
をペイロードとしたPOSTリクエスト - POSTリクエストで返却されたIDをパスに渡したGETリクエスト
前提知識で登場するサービスについての参考記事
上記で登場するサービス・ツールの詳細な内容や基本的な使い方は、以下のハンズオンや記事がとても参考になりました。
まだいずれにも触れたことが無い方は、こちらの内容を読むのが良いと思います。
- Amazon Web Services ブログ Code シリーズ入門ハンズオンを公開しました!- Monthly AWS Hands-on for Beginners 2020年8月号
- APIテスト自動化ツール「Karate」のまとめ
- KarateによるAPIのシナリオテスト自動化 #01 Quick Start
ベースとなるアプリケーションとパイプラインの全体感
前提が長くなりましたがここから内容に入ります。
Pythonアプリケーション
CI/CDのベースとしては、PythonのFlask, gunicornを使ったシンプルなAPIアプリケーションです。
※Webサーバ(Nginx)は参考に置いてるだけで、今回のCI/CDとは直接関係しません。
import json
from flask import Flask
app = Flask(__name__)
@app.route('/students', methods=['GET'])
def get_students():
res = json.dumps({'name': 'Alice'})
return res
if __name__ == '__main__':
app.run()
$ curl 127.0.0.1:5000/students
{"name": "Alice"}
Karateの構成
├── pom.xml
├── src/test/java
│ ├── examples
│ │ ├── ExamplesTest.java
│ │ └── students
│ │ ├── StudentsRunner.java # テスト対象データのために作成
│ │ └── students.feature # DSL:APIのURLや想定される結果を記載
│ ├── karate-config.js
│ └── logback-test.xml
└── target
├── # 省略
コメントに記載の通り、studentsフォルダと配下のファイルを今回用意しています。
package examples.students;
import com.intuit.karate.junit5.Karate;
class StudentsRunner {
@Karate.Test
Karate testStudents() {
return Karate.run("students").relativeTo(getClass());
}
}
Feature: sample karate test script
for help, see: https://github.com/intuit/karate/wiki/IDE-Support
Background:
* url 'http://127.0.0.1:5000'
Scenario: get all students
Given path 'students'
When method get
Then status 200
And match response ==
"""
{
"name": "Alice"
}
"""
試しにローカルでテストを実行すると以下の通りです。
なお、Karate自体はJavaのツールなのでJavaの実行環境が必要です。
$ mvn clean test -Dtest=StudentsRunner
[INFO] Scanning for projects...
# 省略
[INFO]
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.410 s
[INFO] Finished at: 2021-12-14T14:08:36+09:00
[INFO] ------------------------------------------------------------------------
CI/CDパイプラインの概要
これらを組み合わせて、Codeシリーズで以下のようなCI/CDパイプラインを構築します。
- CodeCommitへのpushをトリガーとしてCodePipelineが発火
- CodeBuildでKarateを実行してREST APIのテストを実施
- CodeDeploy及びCodeDeployエージェントにより、ArtifactがEC2に展開
パイプラインの構築
各サービスのポイントとなる部分を記載しています。
詳細な手順はドキュメントや上記で紹介したリンクを確認ください。
CodeCommitの設定
基本は、ドキュメント等に従えばリポジトリを作成できると思います。
ポイントとして、アプリ用のリポジトリとKarate用のリポジトリの2つを作成します。今回は前者をpython-sample
, 後者をpython-karate-sample
という名前にしています。
この理由として、KarateもDSLなどのコードを含む以上はGit管理されるべきという思想からです。これによりテストコードに変更があった場合、チーム間での作業が可能になります。
アプリ用のリポジトリのディレクトリ構成は以下の通りです。Karateのリポジトリは先程と同様です。
├── buildspec.yml # CodeBuildで利用
└── src
├── appspec.yml # CodeDeployで利用
├── scripts # CodeDeployで実行する各種スクリプト
│ ├── change_permissions.sh
│ ├── install_dependencies.sh
│ ├── start_server.sh
│ ├── stop_server.sh
│ └── validate_server.sh
├── conf # WSGIとして利用するgunicornの設定ファイル
│ └── gunicorn_conf.py
├── requirements.txt
└── server.py # アプリのソース本体
CodeBuildの設定
設定手順としては以下のドキュメントもありますが、ソースプロパイダがS3になっているため注意が必要です。
前提知識で紹介したAWS公式のハンズオンの方がわかりやすいかと思います。
設定内容のサマリとしては以下の通りですが、簡素化のためあまり複雑な設定はしていません。
- 追加のソース2を設定
- イメージにはデフォルトの
aws/codebuild/amazonlinux2-x86_64-standard:3.0
を利用 - VPC、ファイルシステム、バッチ設定は無し
特殊な設定をしている部分としては、ソース2の設定とbuildspec.ymlになります。
ソース2の設定
CodeBuildの作成時に「ソースの追加」を押すと複数のリポジトリを指定することが可能になります。
ソース1 - プライマリにはPythonアプリのリポジトリを指定します。
ソース2には、Karateのリポジトリを設定します。
ソース識別子は任意の文字列で問題ありません。今回はKARATE_CONFIG
とします。
buildspec.ymlの設定
buildspec.ymlを作成するにあたって、まずはビルドプロセスを設計します。
Pythonにコンパイルはありませんので、ビルドで実行するのはライブラリのインストールと静的解析、自動テスト程度です。
CodeBuildでのKarateの実行に関しては、CodeBuildのビルドインスタンス上でアプリサーバーを起動して、localhostに対してKarateからリクエストを送る、という方法で実現します。(力技感があるので、もっといいやり方ありましたらご指摘ください...)
CodeBuildではフェーズという概念でビルドプロセスを詳細化します。フェーズごとの実行内容と実際のbuildspec.ymlは以下の通りです。
フェーズ | 実行内容 |
---|---|
install | ・各種依存関係をインストール |
pre_build | ・flake8およびbanditによる静的解析 ・アプリサーバーの起動とKarateの実行 |
build | ・Artifactのzipファイルを生成 |
post_build | ・ArtifactをS3にアップロード |
version: 0.2
env:
shell: bash
variables:
ENV: "build"
phases:
install:
runtime-versions:
python: 3.8
java: corretto11 # Karate実行のため、AWSの提供するOpenJDK(corretto)をインストール
commands:
- pip install -r src/requirements.txt
pre_build:
commands:
- flake8 src/server.py # 静的解析(flake8)の実行
- bandit src/server.py # 静的解析(bandit)の実行
- gunicorn src.server:app -c src/conf/gunicorn_conf.py --daemon # アプリサーバーをデーモン起動
- sleep 1
- cd $CODEBUILD_SRC_DIR_KARATE_CONFIG # Karateの展開ディレクトリに移動
- mvn clean test -Dtest=StudentsRunner # Karateによるテスト実行
build:
commands:
- zip -r artifact.zip src
post_build:
commands:
- aws s3 cp artifact.zip s3://{BUCKET_NAME} # BUCKET_NAMEは任意のバケット名
artifacts:
files:
- '**/*'
base-directory: src
buildspec.ymlの補足として、ソース2の展開先はCODEBUILD_SRC_DIR_{ソース識別子}
という環境変数に設定されます。今回で言えば、KARATE_CONFIG
というソース識別子を設定したので、CODEBUILD_SRC_DIR_KARATE_CONFIG
という環境変数名です。このあたりの仕様はドキュメントに記載されています。
CodeDeployの設定
アプリケーションの作成>デプロイグループの作成という順で設定を行います。
CodeBuildと同様に簡素化のため以下のような設定としています。
- デプロイタイプはインプレース、デプロイ設定にAllAtOnce
- Load balancerは無効
わかりにくいところは、CodeDeployエージェントのインストールとIAMロール関連かと思います。
appspec.ymlと合わせて補足します。
appspec.ymlの設定内容
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user
# アプリ用リポジトリのsrc/scriptsに含まれる各種スクリプト実行
hooks:
ApplicationStop:
- location: scripts/stop_server.sh
timeout: 60
runas: root
BeforeInstall:
- location: scripts/install_dependencies.sh
timeout: 60
runas: root
AfterInstall:
- location: scripts/change_permissions.sh
timeout: 60
runas: root
ApplicationStart:
- location: scripts/start_server.sh
timeout: 60
runas: root
ValidateService:
- location: scripts/validate_server.sh
timeout: 60
runas: root
CodeDeployエージェントのインストール
事前にデプロイ先のEC2へインストールしておきます。
公式ドキュメントは翻訳の都合もあってインストール方法がわかりづらいため、以下のようなクラスメソッドさんの記事が参考になります。
IAMロールの設定
前提知識で紹介したCodeDeployの仕組み上、以下のIAMロール設定が必要になります。
- CodeDeployからEC2へののアクセス権限
- CodeDeployからEC2にインストールされたエージェントに指示出しを行うため
- S3への読み込み権限のあるIAMロールをEC2に付与
- EC2にインストールされたエージェントがS3のArtifactを取得するため
特にEC2の方はEC2のコンソール or CLIからの作業が必要になるため忘れがちです。
CodePipelineの設定
こちらもコンソールから作成していきます。
ソースステージでは、Pythonアプリのリポジトリを指定します。これによりアプリのリポジトリにpushがあった場合に、パイプラインが発火します。
順にさきほど作成したCodeBuild, CodeDeployのプロジェクトも指定すると、以下のようなパイプラインが構築されます。
パイプラインの実行
ようやく実行まで来ました。
アプリのリポジトリへのPushすると以下のようにパイプラインが起動します。
Karateによるテストをパスすると以下のようにPOST_BUILDフェーズが完了します。
依存関係のインストールもありCodeBuildのログがかなり長くなりますが、最後の方に出力されます。
CodeDeployまで完了すると、無事アプリがEC2にデプロイされてAPIとして利用可能になります。
$ curl http://192.168.XXX.XXX/students
{"name": "Alice"}
また、試しにKarateのテストを失敗させてみると、想定通りCodeBuildで失敗してパイプラインが停止されます。
この例では、Response Bodyを{"name": "Bob"}
に変えてテストを失敗させています。
以上で、REST APIの自動テストをパスしたアプリのみがデプロイされる、というパイプラインを構築することができました。
パス関係で上手くいかない場合
ここからはハマったポイントを紹介します。1つ目はパス関連です。
今回の方法では、CodeBuildのインスタンスやCodeDeployから展開先となるEC2など、複数の場所でアプリサーバーを起動します。
そのためサーバーの実行パスや設定ファイルのパスが異なってアプリサーバーが起動しない、といったことが何回かありました。
これに対しては、以下のようなポイントを修正して対応しています。
ただ、CodeBuildでカスタムのDockerイメージ使ったほうが簡単に対応できるかもしれませんね...
- appspec.ymlのHooksで指定する
runas
(実行ユーザー) - 実行するスクリプト内でパスを設定
- gunicornの設定ファイル
conf/gunicorn_conf.py
対応方法は言語やフレームワークに依存するかと思いますが、いずれにせよパスへの考慮は必要になると考えられます。
CodeDeployで上手くいかない場合
CodeBuildはログがコンソールに出るのでデバッグしやすいのですが、CodeDeployはエージェントも絡むのでデバッグに手間がかかりました。エージェントのログや実行されたスクリプトファイルの標準出力は以下に出力されます。
/var/log/aws/codedeploy-agent/codedeploy-agent.log
/opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
詳細はこちらの記事で解説されています。
また、上手くログ出力されていない場合はエージェントを再起動したら上手くいくことがあります。
これらの記事が助けになりましたので、同じようにハマった方は参考にしてみてください。
補足1:Karateのテスト結果をブラウザで見れるようにする
Karateの特徴として、テスト結果がHTMLファイルで生成されます。
CodeBuildの実行結果はCLIでしか確認できませんが、せっかくのHTMLファイルなのでブラウザから確認したいところです。
というわけで、Karateから生成されたHTMLファイルをブラウザから確認できるようにします。
やり方は単純で、生成されたHTMLファイルをあらかじめ用意したS3バケットにアップロードするだけです。
buildspec.ymlの修正箇所は以下のとおりです。
pre_build:
commands:
- flake8 src/server.py
- bandit src/server.py
- gunicorn src.server:app -c src/conf/gunicorn_conf.py --daemon
- sleep 1
- cd $CODEBUILD_SRC_DIR_KARATE_CONFIG
- export RESULT_PAGE=target/surefire-reports/examples.students.students.html # 生成されたHTMLファイル名の設定
- mvn clean test -Dtest=StudentsRunner
finally: # REST APIでのテストに失敗した場合でもアップロードが行われるようfinallyセクションを利用
- aws s3 cp $RESULT_PAGE s3://{HOSTING_BUCKET_NAME}/index.html # HOSTING_BUCKET_NAMEは任意のバケット名
公開したS3バケットのURLにアクセスすると、このようにブラウザからテスト結果が確認できます。これにより、開発者がパイプラインでのテスト結果を簡単に確認できるようになります。
なお、この場合はS3に対して接続元のIPアドレス制限やBasic認証をかけるなど、何かしらセキュリティ対策は行ったほうが良いでしょう。
補足2:REST APIのテストをデータベース込みで行う
実際のAPIはデータベースにアクセスすることがほとんどです。
AWSの場合、RDSやAuroraも込みでKarateのテストを行いたい場合があると考えられますが、こちらもCodeBuildから実施可能です。
変更点としては以下の2点です。
-
CodeBuildの設定から、VPCでビルドのインスタンスが起動するように変更
-
APIのソース修正とライブラリの追加インストール
- DBアクセスするためにソースの修正と、CodeBuildで関連するライブラリのインストールが必要になります。yumでのインストールや、アプリのライブラリについては
requirements.txt
への修正を行います。
- DBアクセスするためにソースの修正と、CodeBuildで関連するライブラリのインストールが必要になります。yumでのインストールや、アプリのライブラリについては
さいごに
ざっくりですが、CodeシリーズとKarateを組み合わせることでREST APIのテスト自動化を行うことができました。
今回はEC2上に作成したAPIでしたが、サーバレスやコンテナ(LambdaやECS)を使うことが最近は多いと思いますので、同じくテストの自動化を検討できればと考えています。
また、Karateも今回は「触ってみた」レベルなので、より複雑なテストシナリオに活用できればと思います。
なお、CodeBuildではビルド環境として自分で作成したDockerイメージの利用が可能です。今回は依存関係のライブラリをbuildspec.ymlのコマンドでインストールしていますが、Dockerイメージから起動することでそれらの手間を省いてビルドすることができます。
CodeBuildを利用される方は、ぜひ活用を検討してみてください。
以上、誰かのお役に立てば幸いです。