AWS CodeBuildを用いてVue CLI製ウェブアプリケーションをビルドからテスト、静的解析、デプロイ、通知まで全部自動にする
Automate the Boring Stuff with… Cloud?
ある種のジャンルのDJ⁰は一曲一曲が短いため、オーディエンスからのフィードバックを受けるとすぐさま新しい曲を試すことができる — —
[0]: つまり私のことです
はじめに
みなさま、はじめまして。今年度から新卒でウイングアークに入社したnakamura.kaと申します。
今回は、新人研修で取り組んだVue CLI製ウェブアプリケーションの開発をベースに、プロジェクトが配置されているリモートリポジトリへの変更(つまりpush
)をトリガーとした
- 自動スタイルチェック(静的解析)
- 自動テスト
- 自動ビルド
- 自動デプロイ
- 自動通知
まで一貫する自動システムの構築を行った記録を、備忘録もかねてご紹介したいと思います。
今回用いたサービス
- Atlassian Bitbucket
- AWS CodeBuild
- Amazon S3
- Amazon CloudWatch
- AWS Lambda
- Slack
自動化したシステムの全体像
- Bitbucket上のリポジトリに
push
された際、CodeBuildに対しWebHookが飛ぶ - CodeBuild上で記述された方法によりビルドが行われる
- ビルド結果が正常であればS3上に成果物を配置する
- ビルド結果をCloud Watchで受け取り、Lambdaを叩いてSlack上でビルド結果の通知を行う
[1]: お互いまったく別の業務に関わっていて、顔も合わせないのにどういうタスクを抱え込んでいるのかなんとなくわかるのは面白い こういうカンバンの使い方もデジタルならではでアリなのかなと思う
1. CodeBuildでビルドプロジェクトを作成する
前提条件
今回は
- デフォルトでBitbucketのリポジトリを用いていた
- IAM²の関係上CodeCommit³を扱えない
という上記の2点からCodeBuildのみを用いたビルドプロジェクトを作成します。
なお、2019/09現在CodePipeline⁴が対応しているリポジトリプロバイダにBitbucketが含まれていないため、今回はCodeBuild上でビルドからデプロイまで(無理矢理)一貫しておこなっていますが、本来業務として一連のシステムを構築するのであれば、パイプラインシステムの構築できるCodePipelineの利用をおすすめします。⁵
[2]: AWS Identity and Access Management、AWS上のサービスにおけるアクセス権の制御を行うサービス 今回は後述するCodeCommit上でのアクセス権を与えられなかった
[3]: AWS上でGitリポジトリを利用できるサービス
[4]: CodeBuild、CodeDeploy等のAWS開発者ツールサービスを統合し、AWS上でワークフローを構築できるサービス
[5]: それを言い出すと業務としてはもう一段上の自動化および業務システムの構築としてTerraform⁶等を用いたほうがよいと思います
[6]: Kubernetesなどのようにクラウド上のインフラストラクチャを宣言的に定義できるツール
ビルドプロジェクトの作成
基本的には特にカスタムしなくても動くはずです。
ソースプロバイダにBitbucketを設定する
今回はそのままpush
時に自動ビルドを行う設定にしましたが、この際にプルリクエストの状態やBranch
名等の正規表現を用いたフィルターを設定することで、柔軟に自動ビルドのタイミングを設定することができます。
ビルド環境を設定する
OSイメージやタイムアウト、使用する計算機資源、環境変数等を設定することができます。
今回はすべてデフォルトですが、実業務ではDockerを用いたカスタムイメージ⁷やタイムアウトの指定、計算機資源を選定⁸する必要があると思います。
[7]: どのような環境でもビルドできるほうがよいと考えたため今回はデフォルトのイメージを使用していますが、外部サービスに依存するプロジェクトなどの場合Docker等コンテナ化技術も利用していくことも視野に入れるべきだと思います
[8]: ただし完全マネージド型であるため、Auto Scalingは可能
buildspecの作成および設定
CodeBuildでのビルドには
- BuildコマンドをまとめたbuildspecというYAML形式のファイルを用いる方法
- ビルドコマンドを指定する方法
の二つの方法があります。
たとえばmavenでは、maven内にビルドフェーズという概念をすでに持っているためフェーズ内にテストやスタイルチェックを組み込むことが可能であり、今回のプロジェクト規模の場合後者のビルドコマンドで十分だと思います。⁹
しかし今回のプロジェクトで用いているVue-CLIのビルドにはそのような概念は存在せず
- ビルド時にテストを組み込みたい
- ビルド成功時にS3の既存ファイルを削除したい
等の理由から、前者のbuildspecファイルを用いたビルドを行っています。
[9]: ただし、前者のbuildspecファイルだとgitの管理対象に含まれるため、実業務では後者のほうがよいと思います¹⁰
[10]: が、それだとbuildspecファイルを作る作業やbuildspecファイルを維持管理する作業自体が存在してしまうため、Terraform等を用いて完全に構築してしまうのであれば後者のほうがよいのでしょうか……?(要検証)
version: 0.2phases:
install:
runtime-versions:
nodejs: 10
commands:
- echo update npm...
- npm install -g n
- n latest
- npm update -g npm
- echo node -v
- node -v
- echo npm -v
- npm -v
- echo Installing source NPM dependencies...
- npm install
pre_build:
commands:
- echo code check start
- npm run lint
- echo test start
- npm run test
build:
commands:
- if [ $CODEBUILD_BUILD_SUCCEEDING = 1 ]; then echo build start; npm run build; echo build completed; else echo test failed; false; fi
post_build:
commands:
- echo Delete S3 Bucket object...
- aws s3 rm s3://cloud-kanban --recursive
artifacts:
files:
- '**/*'
base-directory: 'dist'
ここがCodeBuildのコアとなる部分なので詳しく見ていきます。
phases
ここではビルドの際の処理手順を書いていきます。
install
Node.jsのアップデート、テストコードフレームワークや依存する外部ライブラリなどをインストールするビルド環境構築時の処理を書きます。
今回はすべて最新バージョンへのアップデートをしていますが、場合によっては指定バージョンへのダウングレード等もここで行います。
pre_build
build
の前に行う処理があればここに書きます。
今回のプロジェクトでは前述したとおりCodePipelineを用いていないため、このタイミングで静的解析(npm run lint
)とテスト(npm run test
)を行うように書いています。
build
ビルド時の処理をここで書きます。
if [ $CODEBUILD_BUILD_SUCCEEDING = 1 ];
then echo build start;
npm run build;
echo build completed;
else echo test failed;
false;
fi
ここは本来であればnpm run build
のみでよいのですが、CodeBuildは仕様上どのフェーズがfailed
しても後続のフェーズが走ってしまいます。¹¹
そこでCodePipelineを用いない今回は、ビルド環境の環境変数であるCODEBUILD_BUILD_SUCCEEDING
を見て、これまでのビルドが成功しているかを確認しています。pre_build
でもしビルドが失敗していた場合(つまりスタイルチェックやテストに失敗していた場合)、テストは行われず失敗の終了コードを返すようにしています。
[11]: と思ったのですがAWSの公式ドキュメント¹²だとPRE_BUILDが失敗した場合BUILDはされないような書かれ方になっています 開発当時は
pre_build
失敗時もbuild
に移行していたように思ったのでこのような書き方をしていますが、もしかしたらnpm run build
だけでよいのかもしれません
[12]: https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/view-build-details.html#view-build-details-phases
post_build
新しい成果物を配置するため、ビルド成功時にデプロイおよびホスティング先となるS3バケット内の既存のプロジェクトを削除しています。
artifacts
成果物をどのように扱うか書いていきます。
files
どのようなファイルを配置するか正規表現で指定します。
ここでは指定したディレクトリ以下を再帰的に配置するよう、 **/*
という指定を行っています。
base-directory
どこのディレクトリ以下を配置するか指定します。
npm run build
でのビルドでは通常dist
以下に成果物が生成されるためこのように書いていますが、たとえば mvn package
でのビルドの場合 target
以下に生成されるため、 target
となります。
以上を書いた buildspec.yml
ファイルを(デフォルトでは)リポジトリのルートディレクトリに配置します。
アーティファクトの設定
成果物をどこにアップロードするのか設定します。
今回の場合は生成先とデプロイ先とホスティング先がすべて同じなのでそれ用に用意してあるS3バケットに対して上記 dist
以下をそのまま配置します。
ログの設定
ログをどこに吐くか指定します。
まったく吐かなくても後述する通知機能の作成は可能ですが、今回はおためしで上述のS3バケットとは別のバケットに吐くように設定しています。
2. ビルド通知の設定を行う
一応以上の設定ですでに push
した際に自動的にビルドからデプロイまでされるようになっていますが、このままではいつどのようなビルドがあったのか、どのようなフェイズでビルドが失敗したのか、コンソールを開かなければすぐにはわかりません。
なのでビルド後に結果を通知するシステムを作ってみます。
Slackの設定
まずはカスタムインテグレーションとして Incoming Webhook
を追加します。
通知を行いたいチャンネルを選択し追加を行うと、Webhook URLが表示されます。基本的にはそこに対し POST
してあげるだけでよいです。
AWS Lambdaの設定
後述するCloudWatchでトリガーが引かれた(つまりビルドが終了した)際に叩くLambdaを設定しておきます。
const https = require('https');module.exports.notify = (event, context, callback) => {
const detail = event.detail;
const buildStatus = detail['build-status'];
const projectName = detail['project-name'];
const additionalInfo = detail['additional-information'];
const phases = additionalInfo['phases'];
var attachments = [];
for (const phase of phases) {
if (phase['phase-status'] && phase['phase-status'] != "SUCCEEDED") {
var phaseContext = "";
for (const index in phase['phase-context']) {
phaseContext += "`" + phase['phase-context'][index] + "`";
}
attachments.push({
"title": phase['phase-type'],
"text" : phaseContext,
"color": "danger"
});
}
}
var data = {
"username": "linus torvalds",
"icon_url": "https://pbs.twimg.com/profile_images/2828597835/0f1840e9c2fbafa93fe6f0d7ccf64a3e_400x400.jpeg",
"text" : (buildStatus != "SUCCEEDED" ? "<!here> :x: " : ":heavy_check_mark: ") + projectName + "'s build was " + buildStatus,
"attachments": attachments
};data = JSON.stringify(data);
var options = {
hostname: 'hooks.slack.com',
port: 443,
path: hogehoge,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
};
var req = https.request(options, (res) =>{
if(res.statusCode===200){
console.log("OK:"+res.statusCode);
}else{
console.log("Status Error:"+res.statusCode);
}
});
req.on('error',(e)=>{
console.error(e);
});
req.write(data);
req.end();
};
基本的にはWebhook URLを叩いてるだけです。
ただし、後述のCloudWatchから来るデータがどうなっているのかわかりづらく膨大であるため、ここである程度情報を絞る必要があります。¹⁹
詳しい解説は省きますが、今回は、 event
で受け取るビルド結果のjsonをパースし、ビルド結果が FAILED
だった場合にはメンションをつけてFAILED
フェーズのログを載せて POST
しています。なお、ビルド結果が SUCCEEDED
だった場合にはただ結果をPOST
しているだけです。¹³
[13]: これは、常にビルド結果が通知されると誰も見なくなると考えたからです¹⁹
CloudWatchの設定
CloudWatchではたとえば期間内にどれだけビルドが走ってどれだけfailしたかというのを確認、通知¹⁴する機能もありますが¹⁵、今回はビルドステータスが変化した時、つまりビルド結果がSUCCEEDED
、FAILED
およびSTOPPED
のいずれかになったとき(つまりビルド終了時)に結果を通知する仕組みにします。
これは一例なので例えばビルドに数時間かかるような実製品の場合は柔軟に、ビルド開始時やビルドフェーズの変更時(つまりビルドフェーズ移行時)などを通知することも視野に入ってくると思います。
しかし、あまりにも頻繁に通知が飛んできて誰も見なくなってしまうということも十分考えられるため、監視システムの構築には十分に検討を重ねる必要があるでしょう。¹⁹
[14]: 正しくはAWS Lambdaを叩く機能
[15]: また、AWS内のコンポーネントを定期実行するための機能も存在します これはたとえば実業務においてビルド時間が長くなりすぎるため平日深夜2時にビルドを走らせたいというときに有効です¹⁶
[16]: ただしビルド自動化においてはアンチパターン²⁰
ルールを作成する
ルールタブからルールの作成を行います。
- イベントソースにイベントパターンを指定し、サービス名にCodeBuild、イベントタイプにCodeBuild Build State Changeを選択します
- イベントパターンのプレビューとしてだいたい以下のように表示されると思いますので、編集を行います(具体的に編集する部分としては監視したい
build-status
、project-name
を追加します) - 以下から以下のようになると思います
- トリガーされたときに叩くAWS Lambdaの関数として上で作った関数を選択します
{
"source": [
"aws.codebuild"
],
"detail-type": [
"CodeBuild Build State Change"
]
}{
"source": [
"aws.codebuild"
],
"detail-type": [
"CodeBuild Build State Change"
],
"detail": {
"build-status": [
"SUCCEEDED",
"FAILED",
"STOPPED"
],
"project-name": [
"cloud-kanban"
]
}
}
おめでとうございます!
以上でビルドから通知まですべて自動で行われるようになりました。
ちなみにS3上に配置されたログを確認してみるとテストが失敗したというログが吐かれているのがわかると思います。
まとめ
DevOps、継続的インテグレーション・デリバリーとしてのメリット
- 開発からリリースまで高速
- 変更に対しすぐさま反応があるので変更者の修正が容易
- ビルドという行為それ自体がテスト(ひいては開発者の安心)となる²⁰
クラウド・サーバレスとしてのメリット
- ビルドサーバーの維持管理が不要
- 抽象的でステートレスな仮想化環境でビルドを行える
- Auto Scalingによりビルドの待ちが発生しない
デメリット…?
EC2上に常時ビルドサーバーを立てるというやり方に比べると料金は使ったときに使った分だけではあるが、このような自動ビルドの仕組みは頻繁に行ってこそ価値がある²⁰ため、時間ではなくビルドに対してお金が発生するのであればそれはビルドのインセンティブを減少させる……のかもしれない。
将来像
- ビルドから通知までの流れがバラバラなのでTerraform等で自動化
- また、CodePipelineでステップや依存関係などを設定、可視化できるとよい
- テストケースを別で管理し、どのようなテストを行っているのか、カバレッジはどれくらいか、つまり、いま自分たちがどこにいるのか可視化できるとなおよい¹⁷
- ビルド失敗時は失敗時でもう少し詳細なログ解析を行いたい¹⁸
- ビルドの失敗、成功だけでなく実行回数、実行時間等のメトリクスもロギングすることでより開発および品質保証に活かしていきたい¹⁸
今回は新人研修という狭いスコープの中ですが、開発という立場から取り組めそうな品質保証への取り組みの一例として自動ビルドから通知までの一連のプロセスを自動化してみました。
この記事が誰かのための巨人の肩となれば幸いです。
[17]:たとえばmavenだとbuild時にテストレポートを出力する機能があるため、CloudWatchでのログ生成時にLambdaでパースするということが考えられると思います
[18]:たとえばCloudWatchにログを溜め、CloudWatch上で精査、必要な情報だけLambdaで通知するということが考えられると思います
参考文献
[19]: Mike Julian 著、松浦 隼人 訳「入門 監視 ――モダンなモニタリングのためのデザインパターン」オライリージャパン、2019年
[20]: David Scott Bernstein 著、吉羽 龍太郎、永瀬 美穂、原田 騎郎、有野 雅士 訳「レガシーコードからの脱却 ――ソフトウェアの寿命を延ばし価値を高める9つのプラクティス」オライリージャパン、2019年