じゃんけんアドベントカレンダー の 3 日目です。
昨日 は自動テストを作成しましたが、自動テストは作っただけだと実行されなかったりします。
そこで今日は自動テストが間違いなく実行されるよう、CI を構築したいと思います。
アジャイル開発などで教科書的には最初に用意しろと言われているにも関わらず、時間がないからと後回しになりがちな CI ですが、今回はちゃんとプロジェクト初期に構築します。
今回構築する CI では、テストとビルドを実行するに止まらず、依存関係の脆弱性診断も組み込もうと思います。
いわゆる「DevSecOps」を意識しています。
CI ツールはどうするか
CI を構築するにあたり、ツールをどうするかという話があります。
今回はどのプラットフォームにデプロイするのか未定なので、AWS CodeBuild などのデプロイ先の環境が提供する CI サービスではなく、CI 専用の SaaS を使うことを考えてみます。
その場合、現状では CircleCI か GitHub Actions が第一候補です。
CircleCI と GitHub Actions のどちらを使うかは好みで決めて大丈夫ですが、個人的には GitHub Actions の方が好きです。
というのも、GitHub Actions は CircleCI と異なり、GUI 上の操作不要で、GitHub に設定ファイルを push するだけで設定できます。1
そこで今回は GitHub Actions を使おうと思います。
ちなみに、CI サービスの選定の考え方については自分の個人ブログの この記事 にまとめていたりするので、興味がある方は是非見てみてください。
GitHub Actions のセットアップ
さて、それでは GitHub Actions をセットアップします。
といってもセットアップは非常に簡単で、.github/workflows ディレクトリに YAML ファイルをコミットして push するだけです。
YAML ファイルの中身
今回は以下の YAML ファイルを用意しました。
(GitHub の こちら からも確認可能です)
name: workflow
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
# 日本時間で毎晩 21 時
- cron: '0 12 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- uses: actions/setup-java@v1
with:
java-version: '11.0.9'
java-package: jdk
architecture: x64
- run: ./bin/build.sh
- name: Archive reports
uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: reports
path: ./app/build/reports/**
- name: Archive JAR
uses: actions/upload-artifact@v2
with:
name: jar
path: ./app/build/libs/app.jar
if-no-files-found: error
簡単に言えば、
- main ブランチへの push やプルリクエスト、または定期実行をトリガーとして
- ビルド用のシェルスクリプトを実行し
- 成果物を保存する
だけです。
では、いくつか特筆すべき点だけ説明していきます。
キャッシュ
SaaS の CI サービスでは実行環境が毎回リセットされるため、キャッシュを特に設定していないとビルドが毎回依存関係のダウンロードから始まり、実行時間が長くなってしまう場合があります。
Java (Gradle) であれば ~/.gradle、Node.js であれば node_modules といったディレクトリをキャッシュに保存しておくことで、依存関係のダウンロード時間を削減できることが多いです。
GitHub Actions の場合は cache@v2 というアクションを使うことで実現可能です。
- uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
CircleCI や AWS CodeBuild など、どんな CI サービスにも同様の機能があります。
成果物の保存
以下の記述により、CI で作成された成果物を保存しています。
- name: Archive reports
uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: reports
path: ./app/build/reports/**
- name: Archive JAR
uses: actions/upload-artifact@v2
with:
name: jar
path: ./app/build/libs/app.jar
if-no-files-found: error
./app/build/reports/** で指定しているテスト結果などのレポートについては、if: ${{ always() }}
という記述により、ビルドエラーが発生した際でも保存されるようにしています。
JAR ファイルについては、if-no-files-found: error
という設定により、存在しなければジョブをエラーにするよう設定しています。
ちなみにですが、成果物のデフォルトの保存期間は 90 日です。
その他の設定など、詳しくは Upload-Artifact v2 の GitHub リポジトリ を参照ください。
このように、他の CI サービスでは YAML の記法として組み込まれている cache や artifact といった設定がプラグイン的になっているのは、GitHub Actions の面白いところだと思います。
pre-commit の設定
さて、CI の基本的なセットアップはもう完了してしまいましたが、CI で実行している処理はローカルでもコミット前に確認されると嬉しかったりしないでしょうか。
よくある手法として、Git の pre-commit のフックを利用して、コミット時にビルドが通るかチェックするというものがあります。
実際に、以下のコマンドをプロジェクトホームで実行することで ./bin/build.sh を実行する pre-commit のフックを作成できます。2
printf '#!/bin/bash\n'"$(pwd)"'/bin/build.sh\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
上記のコマンドの通り、pre-commit の設定は .git 以下に保存されるため、git clone のたびに各自が毎回設定する必要があります。
pre-commit を設定すれば CI は不要ではないかと思われるかもしれませんが、以下の 3 つの理由から CI はやはり必要です。
- pre-commit は強制できないこと
- git add していないファイルがある場合、pre-commit ではビルドが通っても、コミット済みのファイルだけでビルドが通るとは限らないこと
- ローカルでビルドできるからといって他の環境でもビルドできる保証はないこと
CI の環境を作るのが何らかの理由で難しいなど、特殊な事情がある場合はまずは pre-commit だけでも設定しておくといいかもしれません。
OWASP Dependency Check
続いて、Java の依存関係の脆弱性診断を入れてしまおうと思います。
DevSecOps 的には、依存関係の脆弱性診断だけでなく、アプリケーションレベルでの脆弱性診断やインフラの脆弱性診断なども CI 組み込むべきかもしれません。
とはいえそこまで一気に対応するのは大変なので、今回まずは依存関係の脆弱性診断だけを組み込みます。
Java の依存関係の脆弱性診断のツールとしては、OWASP Dependency Check を使います。
導入は簡単で、build.gradle にプラグインを追加するだけです。
plugins {
:
id 'org.owasp.dependencycheck' version '6.0.3'
}
この設定だけで、
./gradlew dependencyCheckAnalyze
のコマンドで脆弱性診断が実行され、結果がレポートとして保存されるようになります。
さらに、例えば、build.gradle に以下のような記述を入れれば、CVSS 7.0 以上の脆弱性が発見された場合にビルドエラーになります。3
dependencyCheck {
failBuildOnCVSS = 7.0F
}
このように依存関係の脆弱性診断を組み込み場合は、時間の経過によって新たな脆弱性が生じていないか確認するため、CI は push やプルリクエストをトリガーに実行するだけでなく、日時などで定期実行するようにしておくと良いでしょう。
なお、OWASP Dependency Check は実行時間が結構長くなりやすいので、pre-commit で実行するかは要検討です。
バッジの追加
CI を一通り構築できたので、最後に README に GitHub Actions のステータスのバッジをつけようと思います。
README に以下のように書くだけです。
![badge.svg](https://github.com/os1ma/JankenEnterpriseEdition/workflows/workflow/badge.svg)
※ URL は適宜変更が必要です。
バッジは https://github.com 以下で提供されているため、GitHub と同じ認証情報でアクセスできるようで、private リポジトリであってもトークンなどの設定は不要です。
CircleCI のバッジを private リポジトリにつけるときはトークンの設定が必要だったりするので、ここも GitHub Actions ならではのメリットかもしれません。
次回のテーマ
前回・今回で自動テストと CI をセットアップし、ついにリファクタリングの準備が整いました。
次回からついにコードのリファクタリングを進めていきます。
まずは典型的な Web フレームワークを使った際の MVC のような構成を CLI アプリケーションのまま目指していきます。
それでは、今回の記事はここまでにします。最後まで読んでくださりありがとうございました。
じゃんけんアドベントカレンダー に興味を持ってくださった方は、是非購読お願いします。
次回の記事
【Day 4】DTO と Enum を使ってみる【じゃんけんアドカレ】
現時点のコード
現時点のコードは GitHub の この時点のコミット を参照ください。