LoginSignup
8
7

More than 1 year has passed since last update.

CodeBuildでREST APIの自動テストを実行する

Last updated at Posted at 2021-12-14

この記事は 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に保存できます。 image.png

CodeDeploy

  • AWSが提供するマネージドなデプロイのサービス。
  • EC2へのデプロイを例にあげると、CodeDeployはEC2にインストールされたCodeDeployエージェントと連動して動作します。具体的には、CodeDeployはEC2にインストールされたCodeDeployエージェントに指示出しを行い、実際のデプロイ作業はエージェントが実行します。
  • エージェントに実行して欲しいコマンド(例えばアプリサーバーの起動/停止など)は、スクリプトファイルに記載してArtifactに含める必要があります。スクリプトファイルの名前はappspec.ymlファイルに定義します。

image.png

Karate

  • REST APIの自動テストツール。
  • 指定したAPIに対して、任意のリクエストを送ったり想定されるレスポンス内容を検証することができます。
  • テストシナリオをDSL(Domain Specific Language)という独自のスクリプトで記載します。言語自体はわかりやすく、複雑なシナリオはJavaScriptの関数を定義して実行することも可能です。 image.png ※画像はGithubから引用

DSLは上記のように記載されます。
この例だとhttp://myhost.com/v1/catsというURLに対して、以下のテストを行っています。

  • {'name': 'Billie'}をペイロードとしたPOSTリクエスト
  • POSTリクエストで返却されたIDをパスに渡したGETリクエスト

前提知識で登場するサービスについての参考記事

上記で登場するサービス・ツールの詳細な内容や基本的な使い方は、以下のハンズオンや記事がとても参考になりました。
まだいずれにも触れたことが無い方は、こちらの内容を読むのが良いと思います。

ベースとなるアプリケーションとパイプラインの全体感

前提が長くなりましたがここから内容に入ります。

Pythonアプリケーション

CI/CDのベースとしては、PythonのFlask, gunicornを使ったシンプルなAPIアプリケーションです。
※Webサーバ(Nginx)は参考に置いてるだけで、今回のCI/CDとは直接関係しません。
image.png

server.py
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の構成

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フォルダと配下のファイルを今回用意しています。

StudentsRunner.java
package examples.students;

import com.intuit.karate.junit5.Karate;

class StudentsRunner {

    @Karate.Test
    Karate testStudents() {
        return Karate.run("students").relativeTo(getClass());
    }    

}
students.feature
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に展開

image.png

パイプラインの構築

各サービスのポイントとなる部分を記載しています。
詳細な手順はドキュメントや上記で紹介したリンクを確認ください。

CodeCommitの設定

基本は、ドキュメント等に従えばリポジトリを作成できると思います。

ポイントとして、アプリ用のリポジトリKarate用のリポジトリの2つを作成します。今回は前者をpython-sample, 後者をpython-karate-sampleという名前にしています。
この理由として、KarateもDSLなどのコードを含む以上はGit管理されるべきという思想からです。これによりテストコードに変更があった場合、チーム間での作業が可能になります。
image.png

アプリ用のリポジトリのディレクトリ構成は以下の通りです。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アプリのリポジトリを指定します。
image.png
ソース2には、Karateのリポジトリを設定します。
ソース識別子は任意の文字列で問題ありません。今回はKARATE_CONFIGとします。
image.png

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にアップロード
buildspec.yml
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の設定内容

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があった場合に、パイプラインが発火します。
image.png

順にさきほど作成したCodeBuild, CodeDeployのプロジェクトも指定すると、以下のようなパイプラインが構築されます。
image.png

パイプラインの実行

ようやく実行まで来ました。
アプリのリポジトリへのPushすると以下のようにパイプラインが起動します。
image.png

Karateによるテストをパスすると以下のようにPOST_BUILDフェーズが完了します。
依存関係のインストールもありCodeBuildのログがかなり長くなりますが、最後の方に出力されます。
image.png
CodeDeployまで完了すると、無事アプリがEC2にデプロイされてAPIとして利用可能になります。

起動したAPIへのリクエスト(IPアドレスはダミー)
$ curl http://192.168.XXX.XXX/students
{"name": "Alice"}

また、試しにKarateのテストを失敗させてみると、想定通りCodeBuildで失敗してパイプラインが停止されます。
image.png
この例では、Response Bodyを{"name": "Bob"}に変えてテストを失敗させています。
image.png

以上で、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の修正箇所は以下のとおりです。

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認証をかけるなど、何かしらセキュリティ対策は行ったほうが良いでしょう。
image.png

補足2:REST APIのテストをデータベース込みで行う

実際のAPIはデータベースにアクセスすることがほとんどです。
AWSの場合、RDSやAuroraも込みでKarateのテストを行いたい場合があると考えられますが、こちらもCodeBuildから実施可能です。

変更点としては以下の2点です。

  1. CodeBuildの設定から、VPCでビルドのインスタンスが起動するように変更

    • デプロイ先のインスタンスと同じVPC/サブネットで起動するように設定を変更します。また必要に応じてセキュリティグループも設定します。これにより、ビルドの自動テスト時にデータベースにアクセスできるようになります。 image.png
  2. APIのソース修正とライブラリの追加インストール

    • DBアクセスするためにソースの修正と、CodeBuildで関連するライブラリのインストールが必要になります。yumでのインストールや、アプリのライブラリについてはrequirements.txtへの修正を行います。

さいごに

ざっくりですが、CodeシリーズとKarateを組み合わせることでREST APIのテスト自動化を行うことができました。

今回はEC2上に作成したAPIでしたが、サーバレスやコンテナ(LambdaやECS)を使うことが最近は多いと思いますので、同じくテストの自動化を検討できればと考えています。
また、Karateも今回は「触ってみた」レベルなので、より複雑なテストシナリオに活用できればと思います。

なお、CodeBuildではビルド環境として自分で作成したDockerイメージの利用が可能です。今回は依存関係のライブラリをbuildspec.ymlのコマンドでインストールしていますが、Dockerイメージから起動することでそれらの手間を省いてビルドすることができます。
CodeBuildを利用される方は、ぜひ活用を検討してみてください。

以上、誰かのお役に立てば幸いです。

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7