はじめに
この記事はAWS Amplify Advent Calendar 2019の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ロールを、以下を参考にして作成しておきます。
(すでにお使いのロールがあれば不要です)
IAMロールができたらCloudFormationのテンプレートを作っていきましょう。
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で流します。
$ 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派です。
$ 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の設定をします。詳細な設定は本題ではないので省きます。
$ 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環境を作成するためのスクリプトです。このあと作成します。
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をそのままインポートすることができます。なのでシェルスクリプトを作成します。
#!/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を開いて祈ります。
(略)
[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してみましょう。
$ amplify add api
(中略)
$ git commit -am 'amplify add api'
$ git push origin master
[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の定義を追加します。
"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にテストを実行する行を追加します。
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を確認しましょう。一度出来てしまえばあとは楽ですよね。
[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が東京リージョンになかったので仕方なかったんです!!!
お疲れさまでした!!!!