CodeBuildとAmplify CLIでCI/CD

# はじめに
この記事は[AWS Amplify Advent Calendar 2019](https://qiita.com/advent-calendar/2019/aws-amplify)の15日目です。

AWS Amplify、便利ですよね。特にAmplify Consoleのデプロイ機能は超便利ですよね！！
ですが様々な事情によりAmplify Consoleが使えないこともあります。例えばソース管理にGithub Enterpriseを使っている場合とか、Basic認証じゃ事足らなくてWAFがほしい場合とか。

でもAmplifyのCLIでCI/CDするのって結構しんどいんですよ。とはいえいまさら手動デプロイもイヤですよね。
CI/CDができないからってAmplify自体を諦めてしまうのはもっともったいないです。

というわけでこの記事では、ReactとAmplify CLIで構築したWebをCodeBuild（とその他諸々）を使ってCI/CDを構築する方法を紹介します。

## CloudFormationでCode3兄弟を作る
とりあえずCD(Continuous Delivery)から始めていきます。
CodeCommit, CodeBuild, CodePipelineをCloudFormationで構築します。
`CodeBuildServiceRole`と`CodePipelineServiceRole`という名のIAMロールを、以下を参考にして作成しておきます。
(すでにお使いのロールがあれば不要です)

* [CodeBuild サービスロールを作成する](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/setting-up.html#setting-up-service-role)
* [CodeBuild で CodePipeline を使用してコードをテストし、ビルドを実行する](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/how-to-create-pipeline.html#how-to-create-pipeline-prerequisites)



IAMロールができたらCloudFormationのテンプレートを作っていきましょう。

```yml:CodeBuildCodePipeline.yml
Parameters:
  ProjectName:
    Type: String
  AccountID:
    Type: String
AWSTemplateFormatVersion: "2010-09-09"
Resources:
  S3Artifact:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub ${ProductName}-artifact
  CodeCommit:
    Type: "AWS::CodeCommit::Repository"
    Properties:
      RepositoryName: !Sub ${ProductName}
  CodeBuild:
    Type: "AWS::CodeBuild::Project"
    Properties:
      Name: !Sub ${ProductName}
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/standard:2.0"
        Type: "LINUX_CONTAINER"
        EnvironmentVariables:
          - Name: ENV
            Type: PLAINTEXT
            Value: dev
      Source:
        Type: "CODEPIPELINE"
      ServiceRole: !Sub arn:aws:iam::${AccountID}:role/CodeBuildServiceRole
    DependsOn:
      - CodeCommit
  CodePipeLine:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub ${ProductName}
      RoleArn: !Sub arn:aws:iam::${AccountID}:role/CodePipelineServiceRole
      ArtifactStore:
        Location: !Sub ${ProductName}-artifact
        Type: S3
      Stages:
        - Name: Source
          Actions:
            - Name: "CodeCommit"
              ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              OutputArtifacts:
                - Name: SourceOutput
              Configuration:
                RepositoryName: !Sub ${ProductName}
                BranchName: master
              RunOrder: 1
        - Name: Build
          Actions:
            - Name: "CodeBuild"
              InputArtifacts:
                - Name: SourceOutput
              ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: 1
              Configuration:
                ProjectName: !Sub ${ProductName}
              OutputArtifacts:
                - Name: CodeBuildOutput
              RunOrder: 1
    DependsOn:
      - S3Artifact
      - CodeBuild
```

CloudFormationのファイルができたらAWS CLIで流します。

```shell
$ aws cloudformation deploy \
--template-file CodeBuildCodePipeline.yml \
--stack-name {YourStackName} \
--parameter-overrides ProjectName={your-project-name} AccountID={YourAWSAccountID}
```

`{}`は適宜置き換えてください。`{your-project-name}`はS3のURLに使われるので、大文字は使えません。
しばらくしてCodeCommit, CodeBuild, CodePipelineの3点セット（とArtifact用のS3）が作成されれば、
CodeBuildまわりの準備はOKです。masterにpushするとCodeBuildが走るようになります。

## React Webアプリの作成

CodeCommitからcloneしてきた場所で`create-react-app`します。私はTypeScript派です。

```shell
$ git clone ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/{YourProjectName}
$ create-react-app sample_app --typescript
$ cd sample_app
$ npm start
```
ローカルでReactの初期画面が表示されればOKです。

## Amplifyの設定

作成したReact WebアプリにAmplifyの設定をします。詳細な設定は本題ではないので省きます。

```shell
$ amplify init
(中略)
$ amplify add hosting
(中略)
$ amplify publish
```
env名はここでは仮に`dev`とします。
Amplify Consoleを使う場合とは違い、S3とCloudFrontをCLIから立てる`add hosting`が必要になります。
設定はPROD (S3 with CloudFront using HTTPS)がおすすめです。
CloudFrontがセットになることで、HTTPSはもちろん、IP制限などの目的でWAFを使ったりすることもできるようになります。

`amplify publish`が終了して最後に表示されたCloudFrontのURLにReactのページが表示されればOKです。
S3のURLに転送される場合は、S3のStatic website hostingを無効にしてしばらく（20分ぐらい？）経つと正常にアクセスできるようになります。
CloudFront関連は反映に時間がかかるのが難点ですね…


## buildspec.ymlの編集
いよいよ本題です。CodeBuildで使う設定ファイル、buildspec.ymlを作成していきます。
buildspec.ymlはリポジトリのルートディレクトリにおいておきます。
Codebuildの標準ビルドイメージにはAmplify CLIは入っていないのでインストールします。ローカルの環境と揃えるために、バージョン指定しておいたほうがいいと思います。
`amplify_init.sh`はCodeBuild上に開発環境と同様のAmplify CLI環境を作成するためのスクリプトです。このあと作成します。

```yml:buildspec.yml
version: 0.2
phases:
  install:
    runtime-versions:
      nodejs: 10
    commands:
      - npm install -g @aws-amplify/cli@4.5.0
      - cd sample_app
      - ./amplify_init.sh $ENV
      - npm install
  build:
    commands:
      - amplify publish -c --yes

```

## Amplifyのenv設定スクリプトの作成

Amplify CLIの`amplify init`は、パラメータを渡す事で自動化でき、既存のenvをそのままインポートすることができます。なのでシェルスクリプトを作成します。

```bash:amplify_init.sh
#!/bin/bash
set -e
IFS='|'
# Systems Managerのパラメータストアから、Amplify CLIで使用しているaws_access_key_idとaws_secret_access_keyを取得
access_key_id=$(aws ssm get-parameters --names '/AmplifyCICD/AccessKeyID' --query Parameters[].Value --output text)
secret_access_key=$(aws ssm get-parameters --names '/AmplifyCICD/SecretAccessKey' --query Parameters[].Value --output text)

# env名をCodeBuildの環境変数から取得
env=$1

# AWS ProfileをCodeBuild上で設定
aws configure set aws_access_key_id $access_key_id
aws configure set aws_secret_access_key $secret_access_key
aws configure set default.region ap-northeast-1

# Amplifyの設定
AWSCLOUDFORMATIONCONFIG="{\
\"configLevel\":\"project\",\
\"useProfile\":true,\
\"profileName\":\"default\"\
}"
AMPLIFY="{\
\"projectName\":\"sample_app\",\
\"envName\":\"$env\",\
\"defaultEditor\":\"code\"\
}"
PROVIDERS="{\
\"awscloudformation\":$AWSCLOUDFORMATIONCONFIG\
}"

# Amplify initの実行
amplify init \
--amplify $AMPLIFY \
--providers $PROVIDERS \
--yes
```

ポイントは、　

* Amplify CLIはprofileで動くのでAWS Profileを用意する
* AWS Profileのためのaccess key idとsecret accesskeyの取り扱いが悩ましいが、Systems Manergerパラメータストアでとりあえず納得する

というあたりです。もっといい方法があったら教えて下さい。

## Systems Managerパラメータストアの設定

Systems ManagerパラメータストアにAmplify CLIで使用しているAccess Key IDとSecret Access Keyを保存しておきます。

|名前   |値     |
|------|----------|
|/AmplifyCICD/AccessKeyID | aws_access_key_idの値|
|/AmplifyCICD/SecretAccessKey| aws_secret_access_keyの値|

これで準備が整いました。

## 動作確認
いざcommit & push。CodeBuildを開いて祈ります。

```:CodeBuild
（略）
[Container] 2019/12/14 14:36:47 Running command ./amplify_init.sh $ENV 
Scanning for plugins... 
Plugin scan successful 
Note: It is recommended to run this command from the root of your app directory 
 
For more information on AWS Profiles, see: 
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html 
 
- Initializing your environment: dev 
✔ Initialized provider successfully. 
- Updating resources in the cloud. This may take a few minutes... 
✔ All resources are updated in the cloud 
 
 
Initialized your environment successfully. 
 
Your project has been successfully initialized and connected to the cloud! 
 
Some next steps: 
"amplify status" will show you what you've added already and if it's locally configured or deployed 
"amplify <category> add" will allow you to add features like user login or a backend API 
"amplify push" will build all your local backend resources and provision it in the cloud 
“amplify console” to open the Amplify Console and view your project status 
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud 
 
Pro tip: 
Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything 
 
（略）

[Container] 2019/12/14 14:37:35 Entering phase BUILD 
[Container] 2019/12/14 14:37:35 Running command amplify publish -c --yes 
- Fetching updates to backend environment: dev from the cloud. 
✔ Successfully pulled backend environment dev from the cloud. 
 
Current Environment: dev 
 
| Category | Resource name   | Operation | Provider plugin   | 
| -------- | --------------- | --------- | ----------------- | 
| Hosting  | S3AndCloudFront | No Change | awscloudformation | 
 
No changes detected 
 
> sample_app@0.1.0 build /codebuild/output/src196595873/src/sample_app 
> react-scripts build 
 
Creating an optimized production build... 
Compiled successfully. 
 
File sizes after gzip: 
 
  39.93 KB  build/static/js/2.5867ff6c.chunk.js 
  772 B     build/static/js/runtime-main.6b5fbc27.js 
  611 B     build/static/js/main.af5f7261.chunk.js 
  547 B     build/static/css/main.d1b05096.chunk.css 
 
The project was built assuming it is hosted at the server root. 
You can control this with the homepage field in your package.json. 
For example, add this to build it for GitHub Pages: 
 
  "homepage" : "http://myname.github.io/myapp", 
 
The build folder is ready to be deployed. 
You may serve it with a static server: 
 
  yarn global add serve 
  serve -s build 
 
Find out more about deployment here: 
 
  bit.ly/CRA-deploy 
 
frontend build command exited with code 0 
- Uploading files... 
✔ Uploaded files successfully. 
CloudFront invalidation request sent successfuly. 
https://XXXXXXXXXXXXXX.cloudfront.net 
Your app is published successfully. 
https://XXXXXXXXXXXXXX.cloudfront.net 
 
[Container] 2019/12/14 14:37:55 Phase complete: BUILD State: SUCCEEDED 
[Container] 2019/12/14 14:37:55 Phase context status code:  Message:  
[Container] 2019/12/14 14:37:55 Entering phase POST_BUILD 
[Container] 2019/12/14 14:37:55 Phase complete: POST_BUILD State: SUCCEEDED 
[Container] 2019/12/14 14:37:55 Phase context status code:  Message:  
 
```

できたー！

動かないときはだいたいIAMのロールの問題ですので、`CodeBuildServiceRole`と`CodePipelineServiceRole`に適切なロールを割り当ててあげてください。

## Amplifyのリソース追加しても動くの？
もちろん動きます。試しに`amplify add api`して、`amplify push`せずにCommit & Pushしてみましょう。

```console
$ amplify add api
（中略）
$ git commit -am 'amplify add api'
$ git push origin master
```

```:CodeBuild
[Container] 2019/12/14 15:44:43 Running command ./amplify_init.sh $ENV 
Scanning for plugins... 
Plugin scan successful 
Note: It is recommended to run this command from the root of your app directory 
 
For more information on AWS Profiles, see: 
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html 
 
- Initializing your environment: dev 
✔ Initialized provider successfully. 
 
The following types do not have '@auth' enabled. Consider using @auth with @model 
     - Todo 
Learn more about @auth here: https://aws-amplify.github.io/docs/cli-toolchain/graphql#auth  
 
 
GraphQL schema compiled successfully. 
 
Edit your schema at /codebuild/output/src519574347/src/sample_app/amplify/backend/api/sampleApp/schema.graphql or place .graphql files in a directory at /codebuild/output/src519574347/src/sample_app/amplify/backend/api/sampleApp/schema 
- Updating resources in the cloud. This may take a few minutes... 
 
 
UPDATE_IN_PROGRESS amplify-sampleapp-dev-230440 AWS::CloudFormation::Stack Sat Dec 14 2019 15:44:58 GMT+0000 (Coordinated Universal Time) User Initiated 
 
 
CREATE_IN_PROGRESS apisampleApp           AWS::CloudFormation::Stack Sat Dec 14 2019 15:45:03 GMT+0000 (Coordinated Universal Time)                             
CREATE_IN_PROGRESS apisampleApp           AWS::CloudFormation::Stack Sat Dec 14 2019 15:45:04 GMT+0000 (Coordinated Universal Time) Resource creation Initiated 
UPDATE_IN_PROGRESS hostingS3AndCloudFront AWS::CloudFormation::Stack Sat Dec 14 2019 15:45:04 GMT+0000 (Coordinated Universal Time)                             
UPDATE_COMPLETE    hostingS3AndCloudFront AWS::CloudFormation::Stack Sat Dec 14 2019 15:45:05 GMT+0000 (Coordinated Universal Time)                             
 (中略)
[Container] 2019/12/14 15:48:37 Entering phase BUILD 
[Container] 2019/12/14 15:48:37 Running command amplify publish -c --yes 
- Fetching updates to backend environment: dev from the cloud. 
✔ Successfully pulled backend environment dev from the cloud. 
 
Current Environment: dev 
 
| Category | Resource name   | Operation | Provider plugin   | 
| -------- | --------------- | --------- | ----------------- | 
| Hosting  | S3AndCloudFront | No Change | awscloudformation | 
| Api      | sampleApp       | No Change | awscloudformation | 
 
No changes detected 
（以下略）
```
ローカルで`amplify push`してないのに、APIできちゃいましたね…
最初は`amplify publish`のときにリソースが作られるのかなと思ったのですが、init時に変更を検知して作られるようです。
Amplifyのリソースだけでなく、CustomResources.jsonなどに追加した独自のAWSリソースもいけちゃいます。


## ついでにCIしよう
ここまできたらテストも自動化したいですね。
ほぼcreate-react-appの話になってしまいますが、ついでに書いておきます。

`create-react-app`で作成される`package.json`の`scripts`に、CIの定義を追加します。

```json:package.json
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "test:CI": "CI=true react-scripts test",　// 追加
    "eject": "react-scripts eject"
  }
```

さらに、buildspec.ymlにテストを実行する行を追加します。

```yml:buildspec.yml
version: 0.2
phases:
  install:
    runtime-versions:
      nodejs: 10
    commands:
      - npm install -g @aws-amplify/cli@4.5.0
      - cd sample_app
      - ./amplify_init.sh $ENV
      - npm install
  pre_build:
    commands:
      - npm run test:CI  # 追加
  build:
    commands:
      - amplify publish -c --yes
```

Commit & PushしてCodeBuildを確認しましょう。一度出来てしまえばあとは楽ですよね。

```:CodeBuild
[Container] 2019/12/14 15:29:27 Entering phase PRE_BUILD 
[Container] 2019/12/14 15:29:27 Running command npm run test:CI 
 
> sample_app@0.1.0 test:CI /codebuild/output/src453020370/src/sample_app 
> CI=true react-scripts test 
 
PASS src/App.test.tsx 
  ✓ renders learn react link (42ms) 
 
Test Suites: 1 passed, 1 total 
Tests:       1 passed, 1 total 
Snapshots:   0 total 
Time:        1.8s 
Ran all test suites. 
```
これでテストも毎回実行されるようになりました。


## まとめ
ぶっちゃけAmplify Conosle使ったほうが100倍楽です！
でもAmplify使い始めた頃にはConsoleが東京リージョンになかったので仕方なかったんです！！！

お疲れさまでした！！！！