先日CodePipelineやCodeBuildを初めて触ってみました。初心者がAWSでCIを行う上で考慮するべき点が多くありましたので、実装の道筋を解説した上で、嵌った所や理解に時間がかかった部分などのノウハウを共有させて頂ければと思います。
今回やったこと
Vue
で開発したSPA
アプリをS3
にホストするCodeBuild
プロジェクトをTerraform
で作成しました。
そもそも何を使用すればいいか?
AWSでCIを行うことを検討すると、下記の4つのツールの名前が浮かぶと思います。
- CodeCommit
- CodeBuild
- CodeDeploy
- CodePipeline
初心者としてはまずどのツールを選定していけばいいのかから結構はまってしまう所なので、まずそれぞれのツールのポイントをさっくり説明します。
CodeCommit
Githubの様なものです。CodeBuild, CodePipeline共に, GithubやBitBucketをサポートしているので、無理に使用する必要はありません。
CodeBuild
いわゆる一般的なCIツールに近い部分で、設定した任意のshコマンドを順番に実行してくれます。また、CodeCommitやGithubと簡単に連携でき、ソースコードを先に読み込んでおくことができます。設定はbuildspec.ymlに任意のコマンドを列挙していく様な形になります。(他のCIを使った人には分かりやすいと思います)。実行のタイミングもGithubのイベントなどと連携できるので、「masterにマージされた時でデプロイ」や「とにかくpushされた時にlint」など様々な状況に対応できます。当然、Github側に打ち返すことも可能で、Buildが成功するまでマージができない様にするなどの設定ができます。ソースコードのビルドに使用する場合、生成物のアップロードはビルド作業の中に実装してあげる必要があります( ex) aws cliでs3へアップロードする )。
CodeDeploy
CodeBuildなどで生成したコードをサーバ等に配置します。現在ではS3へのデプロイも対応している見たいですが、CodeBuildの中でも実行可能です。
Code Pipeline
ここが混乱ポイントだと思っています。CodePipelineは上記のいわゆるコード3兄弟をまとめ上げる存在です。じゃあとりあえずCodePipelineを使用すればいいじゃん!と思ってましたが、CodeBuild視点で違いがあります。
まず1点目ですが、前提としてCodePipelineは3つのステージを定義します。それぞれ、source, build, deploy(staging)のステージです。そしてそれぞれのステージにサービスを当てはめていきます。例えば、
- source: CodeCommit, Github
- build: CodeBuild, Jenkins
- deploy: CodeDeploy, Elastic Beanstalk
などです。よって、CodeBuild単体で使用する場合はCodeBuildに対して入力ソースやイベントの設定を行うのに対して、CodePipelineを使用する際はCodePipelineのSourceステージで入力ソースを指定し、CodeBuildの入力ソースはCodePipelineになります。CodeBuildと違い、Githubのどのイベントで実行するのか(いわゆるWebhookFilter)を細かく設定することはできません。
2点目ですが、CodePipelineの中でのデータの受け渡し方が独特です。AWS公式に乗っている以下の図を見ると分かりますが、
Soruce -> Build, Build -> Deploy(Staging)間のデータの受け渡しはS3を介して行われます。一方、CodeBuildの場合はGithubから直接ソースコードをクローンしてくる様な形になります。ここで重要な考慮点として、CodePipelineを使用する場合はビルドステージでgitのメタデータ(.git)に触ることができません。よって、例えばですが「特定のファイルが変更された時だけにビルドを実行する」などgitのhistoryを使用した処理がやりづらいという特徴があります。無理やりやるなら下記参考の通り、git cloneをビルドステージでやり直すくらいだと思います。
3点目として、CodeBuildにはLocalCacheが使用できます。これは指定したディレクトリやソースコードその物を一定期間AWSがキャッシュしてくれる機能です。たとえはnode_modules
などはいちいちnpm i
していると時間とコストがかかってしまうので、便利な機能です。一方で現在のところCodePipelineではBuildステージでLocalCacheは使用することができません。
選定したツール
私のケースですと以下の要件がありました。
- 特定のファイルの変更時だけ実行したい。(gitのヒストリーを使用した作業を含めたい。)
- GithubのPUSHイベントで発火させたい作業や、masterブランチへのマージイベントで発火させたい作業など、タイミングを細かく設定したかった。(つまり、WebHookフィルターを詳細に設定したい)
そこで、選定したツールはCodeBuildになります。ちなみに、CodeBuildの中では任意のコマンドを実行することができるので、S3へのアップロードはCodeDeployを使用せず、CodeBuildだけで完結させることにしました。(一旦生成物(artifacts)保管用のS3にアップロードしてから、ホスティングするS3にアップロードするのでは二度手間なので)
コードビルドをTerraformで実装する
Githubに事前に連携しておく
GithubのPrivateRepositoryを使用する場合Terraformで作成する前にまずはコンソールから手動でビルドプロジェクトを作成を開始します。理由としてはGithubへの認証、接続部分に関してはTerraformで記述できないからです。
コンソールからビルドプロジェクトの新規作成をします。出てきた画面に以下の入力項目があるので、Githubで作成したpersonal access tokenを入力して連携をしておきます。
この認証を一回通しておけばコンソールを閉じて大丈夫です。
Terraformのコード
// まずはAssume Roleの作成
data "aws_iam_policy_document" "assume_role_policy" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
identifiers = ["codebuild.amazonaws.com"]
type = "Service"
}
}
}
resource "aws_iam_role" "code_build_role" {
name = "code-build-role"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}
// CodeBuildのIamPolicyを作成
data "aws_iam_policy_document" "code_build_policy" {
statement {
effect = "Allow"
// CloudWatchを設定するための権限
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = ["*"]
}
// S3にアップロードするための権限
statement {
effect = "Allow"
actions = ["s3:*"]
resources = [
"*"
]
}
// ビルドを途中で終了させるための権限など
statement {
effect = "Allow"
actions = [
"codebuild:StopBuild",
"ec2:*"
]
resources = ["*"]
}
}
// ロールにポリシーを付与
resource "aws_iam_role_policy" "code_build_policy" {
role = aws_iam_role.code_build_role.name
policy = data.aws_iam_policy_document.code_build_policy.json
}
// CodeBuildプロジェクトの定義
resource "aws_codebuild_project" "build" {
name = "your_build_project_name"
description = "..."
build_timeout = "30"
service_role = aws_iam_role.code_build_role.arn
artifacts {
type = "NO_ARTIFACTS"
}
cache {
type = "LOCAL"
modes = [
"LOCAL_SOURCE_CACHE",
"LOCAL_CUSTOM_CACHE"
]
}
logs_config {
cloudwatch_logs {
group_name = "build_group"
stream_name = "build"
}
}
environment {
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/standard:2.0"
type = "LINUX_CONTAINER"
image_pull_credentials_type = "CODEBUILD"
}
source {
type = "GITHUB"
location = "https://github.com/{owner}/{repo}.git"
git_clone_depth = 5
report_build_status = true
buildspec = "{ビルドスペックを配置のパス}"
}
tags = {
Environment = var.environment
}
}
// WebHookの定義
resource "aws_codebuild_webhook" "build" {
project_name = aws_codebuild_project.build.name
filter_group {
filter {
type = "EVENT"
pattern = "PULL_REQUEST_MERGED"
}
filter {
type = "BASE_REF"
pattern = "^refs/heads/master$"
}
}
}
何点かポイントになります。
aws_codebuild_project
のcache部分ですが、ソースコードをキャッシュしてくれるLOCAL_SOURCE_CACHE
と指定したディレクトリをキャッシュしてくれるLOCAL_CUSTOM_CACHE
の2機能を有効にしています。
logs_config
の部分はCloudWatchのログ設定を記述していますが、この記述は2019年7月11日リリースのprovider(2.19.0)で対応した機能です。
release note
このため、Providerのversion設定には気をつけてください。
Providerのversion設定方法に関してはTerraformのコード上に以下の様に指定します。
provider "aws" {
version = ">= 2.19.0"
region = "ap-northeast-1"
}
source
の部分ですが、CodeBuildは(CircleCIの様に)ymlでbuildspecを作成してビルドの内容を指定するので、その設定ファイルのパスを記載します。
aws_codebuild_webhook
の部分ですが、Githubからのwebhookの設定を記載していますが、aws_codebuild_project
とは独立して定義しているのがポイントです。今回の例ですと、Pull Requestがマージされた時のイベントで発火します。また、マージ先のブランチがmasterの時にだけ実行する様にフィルターをかけています。
ちなみにですが、GithubからのWebhookの内容はSettings > Webhooks
からCodeBuild用のWebhookを選択すると、表示される画面の一番下で確認ができます。
buildspec
今回はmasterブランチにコードがマージされた時に、特定のファイルの変更があるかを判定し、SPAアプリをビルドしてS3アップロードするビルドスペックを作成します。
version: 0.2
phases:
install:
runtime-versions:
nodejs: 8
commands:
# CODEBUILD_WEBHOOK_BASE_REFはCodeBuildによってセットされる環境変数です。
# マージリクエストの場合はマージ先のブランチ名が
# refs/heads/master の形式で格納されます。
- git checkout ${CODEBUILD_WEBHOOK_BASE_REF#refs/heads/}
# ローカルキャッシュの有無を確認して、あった場合はリストアします。
- |
if [ -e cache_node_modules ]; then
echo ">>> npm キャッシュをリストア";
cp -r ./cache_node_modules/. ./node_modules/
else
echo ">>> npm キャッシュなし"
fi
# src/componentsディレクトリ配の.ts$ファイルに変更があったか判定します。
# .shの内容は後述
# 変更がなかった場合はビルドを停止します。
- |
if file.change.of.pattern.in.directory.sh "src/components" ".ts$"; then
echo "アプリに変更がありました。ビルドを実行します"
else
echo "アプリに変更はなかったので、ビルドを停止します"
aws codebuild stop-build --id ${CODEBUILD_BUILD_ID}
fi
- node -v
- npm -v
pre_build:
commands:
- echo Installing source NPM dependencies...
# キャッシュがリストアされていればこの処理は軽くなります
- npm install
build:
commands:
- echo Build started
- npm run build:demo
post_build:
commands:
- echo Distributing to S3...
- aws s3 sync dist/. s3://your-bucket-name/ --delete --acl public-read
# symlinkはキャッシュできないので、一旦コピーしておきます
- cp -r ./node_modules/. ./cache_node_modules/
- echo notice to slack channel
# キャッシュするディレクトリの指定
paths:
- './cache_node_modules/**/*'
Symlinkがキャッシュされないというのは地味にハマりポイントですね。
以下はファイルの変更を判定するshellです。
#!/bin/bash
DIR=$1
PATTERN=$2;
echo '>>> 探索するディレクトリ: '${DIR}
echo '>>> 検索パターン: '${PATTERN}
TARGETS_COUNT=(`git diff HEAD^ HEAD --name-only --relative=${DIR} | grep -c ${PATTERN}`)
echo '>>> 対象行数: '${TARGETS_COUNT}
if [ ${TARGETS_COUNT} = 0 ]; then
echo '>>> 実行しない'
exit 1
else
echo '>>> 実行する'
exit 0
fi
まとめ
CodePipelineだと意外とできないことが多かったので、CodeBuildを単体で使用するのが個人的には無難かと思いました。CodeBuildはyamlに記載したshellをそのまま実行してくれるので非常に簡潔ですね。
いずれのツールもまだ発展途上な部分がある様に感じましたので、今後のアップデートも楽しみにしていきたいです。
では、少しでも皆さんの参考になれば幸いです。ご指摘などあればよろしくお願いいたします。