TerraformでCodeBuildプロジェクトを作成し、SPAをS3でホストした時の手順と色々検討したことまとめ

先日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公式に乗っている以下の図を見ると分かりますが、

![https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/welcome.html](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/images/Hi-Level-PipelineFlow.png)

Soruce -> Build, Build -> Deploy(Staging)間のデータの受け渡しはS3を介して行われます。一方、CodeBuildの場合はGithubから直接ソースコードをクローンしてくる様な形になります。ここで重要な考慮点として、CodePipelineを使用する場合はビルドステージでgitのメタデータ（.git）に触ることができません。よって、例えばですが「特定のファイルが変更された時だけにビルドを実行する」などgitのhistoryを使用した処理がやりづらいという特徴があります。無理やりやるなら下記参考の通り、git cloneをビルドステージでやり直すくらいだと思います。

[参考](http://kaz29.hatenablog.com/entry/2018/11/24/112638)

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を入力して連携をしておきます。
![スクリーンショット 2019-07-20 19.00.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/194978/2c6d7e31-8c24-bc7f-7089-b89800b5570c.png)

この認証を一回通しておけばコンソールを閉じて大丈夫です。

### Terraformのコード

```ts
// まずは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](https://github.com/terraform-providers/terraform-provider-aws/blob/master/CHANGELOG.md#2190-july-11-2019)
このため、Providerのversion設定には気をつけてください。
Providerのversion設定方法に関してはTerraformのコード上に以下の様に指定します。

```ts
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アップロードするビルドスペックを作成します。

```yaml:buildspec.yml
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です。

```sh:file.change.of.pattern.in.directory.sh
#!/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をそのまま実行してくれるので非常に簡潔ですね。
いずれのツールもまだ発展途上な部分がある様に感じましたので、今後のアップデートも楽しみにしていきたいです。

では、少しでも皆さんの参考になれば幸いです。ご指摘などあればよろしくお願いいたします。