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

この記事は [AWS Advent Calendar 2021](https://qiita.com/advent-calendar/2021/aws) 14日目の記事です。

## 概要

AWSでCI/CDパイプラインを構築する場合、マネージドサービスであるCodeシリーズを利用することが多々あります。
その中でも、ビルドを実行する**CodeBuild**では自動テストのツールを組み込むことが可能です。

この記事では、CodeBuildでREST APIの自動テストを実行する方法を紹介します。記事の開発言語はPythonですが、REST APIのテストは言語に依存しないメリットがあるので、他の言語やフレームワークを使っている方にも参考になればと思います。
REST APIの自動テストツールとしては**Karate**を利用します。

前提知識のある方は「[CodeBuildの設定](https://qiita.com/ny7760/private/1b47c62df9a18d4160e6#codebuildの設定)」だけ読んでいただければ、大まかな内容は掴めると思います。

2021/12/15 追記
利用したソースコードはgithubに上げています。

https://github.com/ny7760/restapi-test-in-codebuild

## 環境

 - 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/a986ff1e-a131-0ce5-338f-56fa4cb2d376.png)

### CodeDeploy

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/bc87384c-0044-8e20-a464-5ad79db6fe08.png)

### Karate
 - REST APIの自動テストツール。
 - 指定したAPIに対して、任意のリクエストを送ったり想定されるレスポンス内容を検証することができます。
 - テストシナリオをDSL（Domain Specific Language）という独自のスクリプトで記載します。言語自体はわかりやすく、複雑なシナリオはJavaScriptの関数を定義して実行することも可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/690518d4-c6c4-61d0-b4e4-f5c329e067b1.png)
※画像は[Github](https://github.com/karatelabs/karate)から引用

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月号
](https://aws.amazon.com/jp/blogs/news/aws-hands-on-for-beginners-10/)
 - [APIテスト自動化ツール「Karate」のまとめ](https://qiita.com/takanorig/items/46098b066f1216e3ca89)
 - [KarateによるAPIのシナリオテスト自動化 #01 Quick Start](https://qiita.com/takanorig/items/11fd65a4359280cb82e5)


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

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

### Pythonアプリケーション
CI/CDのベースとしては、PythonのFlask, gunicornを使ったシンプルなAPIアプリケーションです。
※Webサーバ（Nginx）は参考に置いてるだけで、今回のCI/CDとは直接関係しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/842cf24d-e3a3-5a76-d999-d7c017c2e801.png)

```python: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()
```

```bash:ローカルでのリクエスト結果
$  curl 127.0.0.1:5000/students
{"name": "Alice"}               
```

### Karateの構成

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

```java: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の実行環境が必要です。

```zsh
$ 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/be1208d1-ef97-0dc6-3a2a-fe3292b98f22.png)


## パイプラインの構築

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

### CodeCommitの設定

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

 - [AWS CodeCommit の使用を開始する](https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/getting-started-cc.html)

ポイントとして、**アプリ用のリポジトリ**と**Karate用のリポジトリ**の2つを作成します。今回は前者を`python-sample`, 後者を`python-karate-sample`という名前にしています。
この理由として、**KarateもDSLなどのコードを含む以上はGit管理されるべき**という思想からです。これによりテストコードに変更があった場合、チーム間での作業が可能になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/3acfa45f-dac6-0911-d73b-788cb7f1f58a.png)

アプリ用のリポジトリのディレクトリ構成は以下の通りです。Karateのリポジトリは先程と同様です。

```bash:アプリ用のリポジトリ
├── 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公式のハンズオンの方がわかりやすいかと思います。

 - [コンソールを使用した AWS CodeBuild の開始方法](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/getting-started.html)

設定内容のサマリとしては以下の通りですが、簡素化のためあまり複雑な設定はしていません。

 - 追加のソース2を設定
 - イメージにはデフォルトの`aws/codebuild/amazonlinux2-x86_64-standard:3.0`を利用
 - VPC、ファイルシステム、バッチ設定は無し

特殊な設定をしている部分としては、ソース2の設定とbuildspec.ymlになります。

#### ソース2の設定

CodeBuildの作成時に「ソースの追加」を押すと複数のリポジトリを指定することが可能になります。
ソース1 - プライマリにはPythonアプリのリポジトリを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/cf0d831b-5b0e-848d-9055-e4ad1a643276.png)
ソース2には、Karateのリポジトリを設定します。
ソース識別子は任意の文字列で問題ありません。今回は`KARATE_CONFIG`とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/d0b3a495-5d1f-b250-32ee-b78e3fbe3f8f.png)

#### buildspec.ymlの設定

buildspec.ymlを作成するにあたって、まずはビルドプロセスを設計します。
Pythonにコンパイルはありませんので、ビルドで実行するのはライブラリのインストールと静的解析、自動テスト程度です。

CodeBuildでのKarateの実行に関しては、**CodeBuildのビルドインスタンス上でアプリサーバーを起動して、localhostに対してKarateからリクエストを送る**、という方法で実現します。（力技感があるので、もっといいやり方ありましたらご指摘ください...）

CodeBuildではフェーズという概念でビルドプロセスを詳細化します。フェーズごとの実行内容と実際のbuildspec.ymlは以下の通りです。

|フェーズ|実行内容|
|---|---|
|install|・各種依存関係をインストール|
|pre_build|・flake8およびbanditによる静的解析<br>・**アプリサーバーの起動とKarateの実行**|
|build|・Artifactのzipファイルを生成|
|post_build|・ArtifactをS3にアップロード|


```yaml: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`という環境変数名です。このあたりの仕様はドキュメントに記載されています。

 - [複数の入力ソースと出力アーティファクトのサンプル](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/sample-multi-in-out.html)

### CodeDeployの設定

アプリケーションの作成>デプロイグループの作成という順で設定を行います。
CodeBuildと同様に簡素化のため以下のような設定としています。

 - デプロイタイプはインプレース、デプロイ設定にAllAtOnce
 - Load balancerは無効

わかりにくいところは、CodeDeployエージェントのインストールとIAMロール関連かと思います。
appspec.ymlと合わせて補足します。

#### appspec.ymlの設定内容

```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へインストールしておきます。
公式ドキュメントは翻訳の都合もあってインストール方法がわかりづらいため、以下のようなクラスメソッドさんの記事が参考になります。

 - [CodeDeployエージェントをインストールする](https://dev.classmethod.jp/articles/codedeploy-agent-install-for-amazon-linux/)

#### IAMロールの設定

前提知識で紹介したCodeDeployの仕組み上、以下のIAMロール設定が必要になります。

 - CodeDeployからEC2へののアクセス権限
    - CodeDeployからEC2にインストールされたエージェントに指示出しを行うため
 - S3への読み込み権限のあるIAMロールをEC2に付与
    - EC2にインストールされたエージェントがS3のArtifactを取得するため

特にEC2の方はEC2のコンソール or CLIからの作業が必要になるため忘れがちです。

### CodePipelineの設定

こちらもコンソールから作成していきます。
ソースステージでは、Pythonアプリのリポジトリを指定します。これによりアプリのリポジトリにpushがあった場合に、パイプラインが発火します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/a5ec166d-1839-6e32-6fae-53e048ceb28a.png)

順にさきほど作成したCodeBuild, CodeDeployのプロジェクトも指定すると、以下のようなパイプラインが構築されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/f098ce79-e91a-e59e-7630-eb915ddd9b2a.png)

## パイプラインの実行

ようやく実行まで来ました。
アプリのリポジトリへのPushすると以下のようにパイプラインが起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/6311ab9e-85bd-4eab-2e3c-b2c44eb0b5e1.png)

Karateによるテストをパスすると以下のようにPOST_BUILDフェーズが完了します。
依存関係のインストールもありCodeBuildのログがかなり長くなりますが、最後の方に出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/b96bdc53-b93d-a07a-e438-fd737975e02c.png)
CodeDeployまで完了すると、無事アプリがEC2にデプロイされてAPIとして利用可能になります。

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

また、試しにKarateのテストを失敗させてみると、想定通りCodeBuildで失敗してパイプラインが停止されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/1f893074-c8c6-bb80-2053-6263613e8513.png)
この例では、Response Bodyを`{"name": "Bob"}`に変えてテストを失敗させています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/2cae3247-c57b-a80a-ee04-65a16cbf9eef.png)

以上で、REST APIの自動テストをパスしたアプリのみがデプロイされる、というパイプラインを構築することができました。

### パス関係で上手くいかない場合

ここからはハマったポイントを紹介します。１つ目はパス関連です。

今回の方法では、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`

詳細はこちらの記事で解説されています。

 - [CodeDeployエージェントが出力するログをCloudWatch Agentを利用してCloudWatch Logsに転送してみた](https://dev.classmethod.jp/articles/codedeploy-cloudwatchagent-cloudwatchlogs/)

また、上手くログ出力されていない場合はエージェントを再起動したら上手くいくことがあります。

 - [CodeDeployでEC2へデプロイした結果、処理が1つも進まずに失敗してしまうときの対処方法](https://techblog.forgevision.com/entry/2020/07/16/210647)

これらの記事が助けになりましたので、同じようにハマった方は参考にしてみてください。

## 補足1：Karateのテスト結果をブラウザで見れるようにする

Karateの特徴として、テスト結果がHTMLファイルで生成されます。
CodeBuildの実行結果はCLIでしか確認できませんが、せっかくのHTMLファイルなのでブラウザから確認したいところです。

というわけで、Karateから生成されたHTMLファイルをブラウザから確認できるようにします。
やり方は単純で、生成されたHTMLファイルをあらかじめ用意したS3バケットにアップロードするだけです。

 - 参考：[ウェブサイトのホスティングの有効化](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/EnableWebsiteHosting.html)

buildspec.ymlの修正箇所は以下のとおりです。

```yaml: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/e47bbcf7-4f2a-e4ee-200d-644126496834.png)

 - 参考：[特定の VPC エンドポイントまたは IP アドレスを使用して Amazon S3 バケットへのアクセスを制限するにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/block-s3-traffic-vpc-ip/)

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

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

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

1. CodeBuildの設定から、VPCでビルドのインスタンスが起動するように変更
   - デプロイ先のインスタンスと同じVPC/サブネットで起動するように設定を変更します。また必要に応じてセキュリティグループも設定します。これにより、ビルドの自動テスト時にデータベースにアクセスできるようになります。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/e7a14e28-ed94-336b-a8da-1597f7eb5818.png)

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

## さいごに

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

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

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

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