フロントエンドチームのSET(Software Engineer in Test)の @urahiroshi です。
メルカリのフロントエンドチームは、JavaScriptを中心とした技術を用いてメルカリのWebサイトやアプリ内WebViewの開発を行っています。
私はチーム内のSETとして、開発環境の構築やトラブルシューティング、CI・運用ツールの導入やビルド・デプロイ処理の修正などを主に行っているのですが、今回は私たちが利用しているCircleCIの設定についてご紹介したいと思います。
CircleCIでは以下のようなタスクを実行しています。
- Lint
- ユニットテスト
- ビルド・デプロイ
- Storybookのデプロイ
- npmパッケージのpublish
- 脆弱性検知
各タスクの詳細について、順に記述していきます。
1. Lint
コードに対してLintツールを走らせます。
最近のプロジェクトではTypeScriptを使っているものが多く、tslint を利用してチーム内で共通の設定を用いてチェックを行っています。
また、CSSに対しては stylelint を用いて、こちらもチーム内で共通の設定を用いてチェックを行っています。
ただ、最近のプロジェクトでは husky と lint-staged を用いてコミット前に prettier
, tslint --fix
, stylelint --fix
を自動的に実行させており、CIツール上のLintに引っかかるケースは少なくなっています。
2. ユニットテスト
ユニットテストを実行します。
また、ユニットテストのカバレッジを取得し、 Codecov に送信しています。
(CircleCIの設定例)
- run: name: test command: | npm test -- --coverage npx codecov@3.0.2
設定例の npx codecov@3.0.2
は codecov@3.0.2
をダウンロードして実行するという処理で、CI上で npm install -g
の代わりに用いることができます。バージョン指定は予期せぬ変更が入り込まないように入れています。
Codecovは、GitHubと連携してカバレッジレポートの表示やPull Requestのステータス更新などを行うことができ、カバレッジが下がった場合などにGitHubのステータスをNGとするなどの設定を行えます。
<図. Codecovのカバレッジレポート>
Codecovでは、プロジェクト全体のカバレッジと、Pull Requestで差分が出たファイルのカバレッジのそれぞれに対し、どのような条件を満たせばステータスがOKとするか設定できるため、プロジェクト全体では一定値のカバレッジを満たせば良しとしつつ、差分についてはあらかじめ指定したディレクトリ・ファイル内でカバレッジが下がっていなければ良しとするなど、プロジェクトの状況に合わせた設定を行うことができます。
(codecov.ymlの例)
coverage: status: project: default: target: 70% patch: default: paths: - server - components
上記の例は、Pull Requestに対して以下の両方の条件を満たしているかチェックする設定になります。
- プロジェクト全体で70%以上のカバレッジがある
- server, componentsディレクトリ内のファイルのカバレッジが下がっていない
3. ビルド・デプロイ
ここでのビルドとは、 Gulp, webpackなどのCLIや専用のビルドスクリプトを用いて、テスト環境や本番環境で用いるファイルを生成する処理を指します。
CIツール上で行っているビルドやデプロイの処理はプロジェクトによって異なり、以下のようなパターンがあります。
- ビルドがエラーにならないか確認する (git pushごと)
- ビルドし、その成果物を外部ストレージに保存する (Pull Requestがマージされたタイミング)
- Dockerfileの処理でビルドを行い、Docker imageをDockerレジストリにpushする (Pull Requestがマージされたタイミング)
これらの処理はリリースフローと大きく関連するため、ここでは詳細は記載せず、別の機会に紹介できればと思います。
4. Storybookのデプロイ
Storybookは、ReactなどのUIコンポーネントにテスト用のパラメータを割り当てて、ブラウザ上でUIコンポーネントごとの表示を確認できるライブラリです。build-storybook
コマンドにより、ブラウザで表示するための静的ファイル一式を書き出すことができます。
<図. Storybookの例>
これを利用して、CircleCI上で書き出したStorybookの静的ファイルをホスティング環境にアップロードし、Pull Requestをレビューする際に参照できるようにしています。
アップロード先のホスティング環境は、GitHub上に公開している(もしくはGitHub Enterpriseを利用している)ソフトウェアであればGitHub Pagesが使いやすいのですが、GitHubのPrivateリポジトリを利用しているリポジトリの場合は、社外からアクセスできない環境を別途用意する必要があります。
メルカリのフロントエンドチームでは、AWSのCloudFrontとS3を利用し、CloudFrontにアクセス制限をかけた上で、S3上にアップロードしたファイルをCloudFront経由で参照できる環境を用いています。S3上にファイルをアップロードするNode.js CLIも内製で作成しました。
また、CI上でStorybookのデプロイを実行した後、デプロイ先のURLがPull Request上に通知されると便利です。
CircleCIでは、デフォルトでpushされたコミットごとにCIが実行されてしまうため、Pull Requestを作る以前に git push
されたコミットについては、CI上でPull Requestの情報が取得できません。
しかし、CircleCIのWeb UI上での設定変更により、Pull Requestが生成された時点でCIを実行するようにできるため、これを利用してCI上でPull RequestのURLを取得し、StorybookのURLをコメントとして通知しています。
<図. GitHubのコメント>
5. npmパッケージのpublish
弊社の npm private registry にpublishするnpmパッケージの場合、Pull Requestがmasterブランチにマージされたタイミングで npm publish
を行っています。
Pull Requestによってはバージョン更新が不要なものもあるため、毎回 npm publish
を実行しているとエラーになってしまいます。そのため、現在リリースされている最新バージョンと比較し、更新があった場合のみpublishするようにしています。
バージョンの比較にはsemverのCLIを使うと便利で、semver -r ">${CURRENT_VERSION}" ${LOCAL_VERSION}
のようにすれば $LOCAL_VERSION
が $CURRENT_VERSION
より高いかどうかを比較できます。
また、publishを実行した後に git tag
を実行することで、publishしたバージョンとgit上のタグが一致するようにしています。
全体の処理としては、以下のようなshell scriptをCircleCIから実行しています。
#!/bin/bash -ex PACKAGE_NAME=node -p </span><span class="synStatement">"</span><span class="synConstant">require('./package.json').name</span><span class="synStatement">"</span><span class="synSpecial">
# IF npm view fails the result will be '0.0.0', ELSE it will be latest published version REPO_VERSION=npm view </span><span class="synPreProc">${PACKAGE_NAME}</span><span class="synSpecial"> version </span><span class="synConstant">2</span><span class="synSpecial">>/dev/null || </span><span class="synStatement">echo</span><span class="synConstant"> 0.0.0</span><span class="synSpecial">
LOCAL_VERSION=node -p </span><span class="synStatement">"</span><span class="synConstant">require('./package.json').version</span><span class="synStatement">"</span><span class="synSpecial">
if (npx semver@5.5.0 -r ">${REPO_VERSION}" ${LOCAL_VERSION}); then echo "$LOCAL_VERSION exists. skip publish." else npm publish --unsafe-perm git tag ${LOCAL_VERSION} git push --tags origin fi
上の処理で npm publish
に --unsafe-perm
オプションをつけているのは、Node.jsのDockerイメージ上で処理を実行する場合はrootユーザーになり、npmのデフォルトの挙動ではrootユーザーの処理はuidを付け替えられてしまう(デフォルトでnobody)ため、これを防ぐためのものです。
6. 脆弱性検知
CircleCIでは、Pull Requestなどのトリガーからではなく、指定したJobを定期的に実行するための設定も行うことができます。
(参照:
https://circleci.com/blog/manual-job-approval-and-scheduled-workflow-runs )
これを利用して、プロジェクト内で利用しているNode.jsモジュールの脆弱性をチェックしてSlackに投稿するようなジョブを定期的に実行しています。
脆弱性の確認には、npm@5.10以上であれば npm audit
が利用できます。その前のバージョンの場合は nspが利用できますが、こちらはGitHub上でもarchiveされているため、 npm audit
を使えればそちらを使うほうが望ましいです。
悩ましいのが脆弱性の通知方法です。脆弱性によっては緊急度が低く対応を見送っているものもあり、そういった脆弱性が毎回通知されると煩わしくなります。
一方で、新たに見つかった脆弱性は早めに検知して影響を評価したいと考えます。そこで、メルカリでは以下のような2つのジョブを実行しています。
- 毎日実行するジョブ: 前回の実行時に検知した脆弱性と比較して、差分があればSlack通知する
- 毎週実行するジョブ: 検知した脆弱性をすべてSlack通知する
前回の結果との比較は、前回の結果をテキストファイルとしてCircleCI上のキャッシュに保存し、diffを取ることで実行しています。
また、デフォルトの出力形式だと冗長なため、jsonの出力形式にした上で、jqで整形して出力しています。
Slackに通知する際は最小限の出力(nodesecurity.ioのURLとseverity)として、パッケージがどこで使用されているかなどの詳細はCircleCIのログ上に出すようにしています。
(出力例: 短縮版)
https://nodesecurity.io/advisories/157 (low) https://nodesecurity.io/advisories/525 (high)
(出力例: 詳細版)
https://nodesecurity.io/advisories/157 (low) "ava>chokidar>anymatch>micromatch>braces>expand-range>fill-range>randomatic" "ava>jest-snapshot>jest-util>jest-message-util>micromatch>braces>expand-range>fill-range>randomatic" https://nodesecurity.io/advisories/525 (high) "ava>chokidar>fsevents>node-pre-gyp>request>tough-cookie"
なお、パッケージの更新は手動で行っていますが、 Renovate が便利そうなので現在試用中です。
また、 npm audit fix
コマンドでで脆弱性の修正を行えるため、こちらをCircleCIに組み込んでも良いかと思います。
まとめ
これらのCI設定は適宜改善していっており、プロジェクトによっては上記のような設定が適切ではない場合もあるかと思いますが、CI設定を考える上での参考になれば幸いです。
メルカリのSETでは、このようなCIの改善をはじめとして、心地よく開発を行えるための取り組みをメンバー自身が考えて提案し、実装していくことができます。
もちろん、CIの設定をSETだけが管理するのではなく、チームメンバーが積極的にCI設定を更新できる土台を整えていくことも役割の一つであり、必ずしもSETがCI設定を行う必要があるわけではありません。
開発チームの生産性向上、品質向上のために何が必要なのかを考え、開発チームと一体となって取り組みを進められることがこの役割の醍醐味であると感じています。
SET, Frontendチームともにエンジニアを絶賛募集中なので、興味がある方はぜひご応募ください!