Chalice x Circle CI 環境における CI/CD について

# 序文
https://chalice.readthedocs.io/en/latest/

AWS Chalice は、AWS Lambda と API Gateway などを組み合わせて、Heroku のような手軽さで Serverless 環境を構築することができるフレームワークです。
現在のところ開発言語は Python しか対応していない（今後も対応する？かは不明）ですが、その制約を受け入れれば他の Serverless フレームワークよりも手軽で開発しやすい環境が提供されているように思います。
私の環境でも、簡易的な API の作成や、バッチ、cron 処理など、かなり広範囲の処理について Chalice を用いて開発をすることが多くなりました。

当記事では Chalice でできることの説明は割愛します。

そして、テスト周りについてはすたじおさんが記事を書いている(https://qiita.com/studio3104/items/8a6b7e5f696e8453d97a) ので、私の方では Chalice で開発する際の CI/CD 環境をどのようにするか、自分の実例を基に記事を書きたいと思います。
なお、ここでは、使用する CI 環境は `Circle CI` とします。

いくつかのテーマがありますが、ここでは以下のテーマについて記述します。

- monorepo 対応
- unit test / code coverage
- lint / code smells
- chalice 設定ファイルの validation 
- 脆弱性診断
- auto deploy

# monorepo 対応 (プロジェクトごとの差分チェック)
chalice は、作成したい機能ごとに ```chalice new-project sada``` のような形でプロジェクトの scaffold を作成し、開発をしていくスタイルになります。
プロジェクトごとに GitHub などのリポジトリを作成していくと細分化が激しくなります。なので monorepo 構成にしたいのですが、かといって単純にひとつのリポジトリに複数のプロジェクトを入れると、CI/CD の際に一部のプロジェクトの修正の際に全部のプロジェクトのテストや検証、デプロイが走ったりしがちで、CI/CDの速度が低下します。

なので、基本的な戦略としては、`GitHub リポジトリの中で変更があったプロジェクトに対してのみ CI/CDを実施する`という形を取りたいです。

こちらの実現の際に、以下のブログの内容を参考にしました。GitHub の Head の内容と比較して、差分があるかどうかを判別するスクリプトです。
https://blog.hatappi.me/entry/2018/10/08/232625

```
$ cat tools/is_changed 
#!/bin/bash

if [ "${CIRCLE_BRANCH}" = "master" ]; then
  DIFF_TARGET="HEAD^ HEAD"
else
  DIFF_TARGET="origin/master"
fi

DIFF_FILES=(`git diff ${DIFF_TARGET} --name-only --relative=${1}`)

if [ ${#DIFF_FILES[@]} -eq 0 ]; then
  exit 1
else
  exit 0
fi
```
このような形でスクリプトを用意し、CI/CD の際にチェックしたい Chalice プロジェクトを当該スクリプトの引数にわたすことでプロジェクトごとに差分チェックが行え、差分が無いプロジェクトはスキップし差分があるものだけ後続の CI 処理を実施する、ということが実現できます。

```
$ cat tools/echo_changed 

for dir in ${PROJECTS}; do \
  if ${WORKDIR}/is_changed "${dir}"; then
    echo "changed."
  else
    echo "nothing change."
  fi
done
```

# unit test / code coverage
テストの書き方などについては、すたじおさんが詳細に記載をされてると思うのでここでは詳細は割愛します。

私の環境では、 `coverage` と `pytest` を組み合わせて実施をしています。unittestの実施と、その結果としてどの程度 Test Coverage が達成されているかの測定を実施しています。

```
$ coverage run -m pytest
$ coverage report
```

```
Name                Stmts   Miss  Cover
---------------------------------------
app.py                 35     22    37%
tests/__init__.py       0      0   100%
tests/conftest.py       4      0   100%
tests/test_app.py       5      0   100%
---------------------------------------
TOTAL                  44     22    50%
```

unit テストがある環境で coverage を動作させると、上記のようなレポートを出力することができます。
レポートの内容については、HTML リポートを Circle CI の Artifacts として保存して CI 結果画面から確認することができます。

```
    - run:
        name: Run Tests
        command: |
          $HOME/.local/bin/coverage run -m pytest
          $HOME/.local/bin/coverage report
          $HOME/.local/bin/coverage html  # open htmlcov/index.html in a browser
    - store_artifacts:
        path: htmlcov
```

詳しい設定等については以下の公式ドキュメントを参照ください。

https://circleci.com/docs/2.0/code-coverage/#python

また、特定のカバレッジ率を下回った際にテストを止めたい場合は、`coverage report --fail-number [num]` とすると、カバレッジが数値以下の場合に `return code = 2` が返却されます。
つまり、しきい値を満たさない時に CI を止めることができます。

この辺の処理を shell として書くと以下のようになります。一通り monorepo 中のすべてのプロジェクトのうち、何かしら差分があるものついて unitetest / coverage 測定を実施し、テストが fail になるかひとつでも coverage がしきい値以下のものがあると、CI が止まります。

```
$ cat tools/coverage 

ret=0
for dir in ${PROJECTS}; do 
  if ${WORKDIR}/is_changed "${dir}"; then
    cd ${WORKDIR}/../${dir}/ 
    sudo pip install -r requirements.txt
    coverage run -m pytest
    coverage report --fail-under=${THRESHOLD}
    r=$?
    coverage html -d ../htmlcov/${dir}
    if [ $r != 0 ]; then ret=$r; fi
  else
    echo "nothing change."
  fi
done

exit $ret
```
# lint / code smells
テストやカバレッジ測定だけでなく、Lint ツールや Code Smells ツールを使って、ヒューリスティックに **「あまり良くない書き方」** の箇所を特定することも CI/CD のフローではよく実施されています。

こちらについても詳細は割愛しますが、私の環境では `pep8 (pycodestyle)` と `pyflakes` を組み合わせて実施します。
いずれのツールも、指摘事項がある際は `return code = 1` になるので、必要に応じて CI のハンドリングを行います。

以下は pyflakes の結果サンプルですが、以下のケースでは `sada` という変数が宣言されているが使われていない、といったことが指摘されています。

```
$ pyflakes app.py 
app.py:32: local variable 'sada' is assigned to but never used
```

ただ、いずれも、初期設定のまま実施するとかなり細かく指摘事項を挙げてくるので、環境に応じて設定ファイルを記述して ignore する、もしくは環境に応じてルールを記述するのが現実的な運用だと思います。

この辺の処理を shell として書くと以下のようになります。

```
$ cat tools/lint 

ret=0
for dir in ${PROJECTS}; do 
  if ${WORKDIR}/is_changed "${dir}"; then
    cd ${WORKDIR}/../${dir}/ 
    sudo pip install -r requirements.txt
    pep8 ${WORKDIR}/../${dir}/*.py --config ${WORKDIR}/../.config/pep8 
    pyflakes ${WORKDIR}/../${dir}/*.py 
    r=$?
    if [ $r != 0 ]; then ret=$r; fi
  else
    echo "nothing change."
  fi
done

exit $ret

```
# chalice 設定ファイルの validation 
上述の内容で、一般的な CI は実施できていると思いますが、chalice の場合それぞれの stage ごとに設定ファイル **(config.json, deployed/)** を用意する必要があり、その設定ファイルの妥当性も検証をしたいところです。

一番手軽な方法として、CI/CD の際に `chalice package` コマンドを実行し、ただしく chalice を Packaging できるかどうかを確認することで設定ファルの妥当性を簡易的に確認することができます。

```
$ cat tools/package 

for dir in ${PROJECTS}; do 
  if ${WORKDIR}/is_changed "${dir}"; then
    cd ${WORKDIR}/../${dir}/ 
    sudo pip install -r requirements.txt
    chalice package /tmp/${CIRCLE_SHA1}
  else
    echo "nothing change."
  fi
done
```

# 脆弱性診断
昨今はアプリケーションのセキュリティについてセンシティブな事案も多いため、できる限り不安のあるコードをアプリケーションに紛れ込ませたくありません。
そのため使用しているライブラリなどに脆弱性が含まれているものがないか、CI のタイミングで都度確認したいところです。

いくつかソリューションがあるとは思いますが、ここでは **snyk** (https://snyk.io/) というセキュリティベンチャー企業のサービスを使用します。

snyk はソースコードの vulunability chek (脆弱性診断) を行ってくれる機能を提供する SaaS です。様々な言語に対応していますが、もちろん Chalice で使用する Python も対応しています。
基本的に `requirements.txt` の中身をみて使用ライブラリを把握し、脆弱性データベースに含まれるバージョンのライブラリが含まれていないかどうかをチェックします。

GitHub との連携が用意な事もあり、 CI のフローに組み込みやすいのが特徴です。
public な OSS であれば無制限、Private リポジトリでも 200 テストまでは無料で使用することもできます。
https://snyk.io/plans/

snyk は circle.yml に定義を書くというより、GitHub と連携設定を行う形になります。細かい設定方法などは公式ドキュメントに譲ります。
https://snyk.io/docs/github/

# auto deploy
最後に、CI/CD の CD の部分について。
chalice は `chalice deploy` といったコマンドで簡単にデプロイ作業が行えるため、CI の結果問題がなかったソースを自動デプロイすることは容易です。

以下は、差分があったプロジェクトのみ最新の状態に `chalice deploy` する処理を shell で書いたものです。

```
$ cat tools/deploy 

for dir in ${PROJECTS}; do \
  echo "${WORKDIR}/../${dir}/"
  if ${WORKDIR}/is_changed "${dir}"; then
    cd ${WORKDIR}/../${dir}/
    sudo pip install -r requirements.txt
    AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} chalice deploy --stage ${STAGE}
  else 
    echo "nothing change."
  fi
done
```

CI ツールで本番環境への自動デプロイを行うかは、議論が分かれるところかなとは思います。
（私の環境では本番環境への自動デプロイは実施せず、検証環境のみにすることが多いです）

# まとめにかえて
AWS Chalice は、Python 限定ではありますが Heroku のような形で手軽に Serverless な処理を書けるフレームワークで、普通のアプリケーションのように Lambda の処理を書けるのが利点だと思います。
そして、その利点があるため、Lambda のような Serverless 環境であっても上述のような CI/CD のフローに組み込む事も容易です。

ということで、みなさんも Chalice を使いましょう。
Happy Chalice Life and Serverless Life!!