概要
create-react-app から作成したReactのWebアプリケーション(SPA)のビルドとデプロイを自動化するためのCodePipelineを構築する自分用CloudFormationテンプレートの紹介記事です。
Gitリポジトリ(CodeCommit)への特定ブランチへのpushをトリガーに起動してデプロイ先のS3バケットにデプロイするCodePipelineのAWSリソースが構築されます。
デプロイ時にS3オブジェクトにキャッシュコントロール用のメタデータを付与することにより"cache-control"ヘッダがHTTPレスポンスに付与されるようになります。
buildspec.ymlについて
当記事で紹介しているテンプレートのパイプラインでのビルドには、React側のリソースの最上位階層のフォルダに「buildspec.yml」という名前のファイルを下記の内容で配置しておく必要があります。
version: 0.2
phases:
pre_build:
commands:
- npm ci
build:
on-failure: ABORT
commands:
- npm run build
post_build:
commands:
- cd build
- aws s3 sync --exact-timestamps --cache-control 'max-age=2592000' --exclude '*' --include 'static/**/*' . s3://${DEPLOY_BUKET}
- aws s3 sync --exact-timestamps --cache-control 'no-cache' --delete . s3://${DEPLOY_BUKET}
配置する場所 ※package.jsonと同じ階層に置きます
「buildspec.yml」ファイルはCodeBuildで使われるファイルで、"build"フェーズに定義されたコマンドでビルドされたリソースを"post_build"フェーズのAWS CLIのコマンドでS3バケットにデプロイしています。
"post_build"フェーズで、ビルド成果物が出力される"build"フォルダに移動し、2回に分けた"aws s3 sync"コマンド実行でS3バケットにデプロイしています。
最初のs3 syncでは、"static"フォルダ配下のjsやcssなどのファイル群(サイズが大きくビルド毎にファイル名が変わる)に限定して'max-age=2592000'のキャッシュコントロール用メタデータの設定をしてデプロイを行っています(2592000は1ヶ月の秒です)。
2番目のs3 syncでは、"index.html"を含むブラウザキャッシュを効かせたくないファイル群(最初のs3 syncでデプロイされたもの以外のファイル)に対して"no-cache"のキャッシュコントロール用メタデータの設定をしてデプロイを行っています。
こうすることでブラウザとCloudFrontのキャッシュを有効活用しつつもデプロイによるアプリケーションの更新時には即座に反映されるようにすることができます。
aws s3 syncコマンドの"--exact-timestamps"オプションは、更新があってもファイルサイズが変わらなった場合に同期の対象外になってしまう事故を防ぐために指定しています。
参考: S3 sync で s3からファイルを同期させる時の注意点
2番目のsyncで指定している"--delete"オプションは、古いリソースを削除して最新のビルドリソースのみがS3バケットに存在するようにしています。
S3バケットを指定する部分が"${DEPLOY_BUKET}"となっていますが、この部分はCodeBuildで設定する環境変数から設定されますのでこのままで構いません。
CloudFormationテンプレート
こちらのGitHubからダウンロードもできます。
下記のリソースがこのテンプレートで作成されます。
リソース | 説明 |
---|---|
CodePipeline | "Source"と"Build"の2ステージで構成されるPipelineが作成されます |
CodeBuild | Pipelineから呼び出されます |
IAMロール | CodePipeline用、CodeBuild用、EventBridge用の3つが作成されます |
S3バケット | Pipelineがアーティファクトストアとして一時的に使用するバケットです。※SPAのデプロイ先ではありません |
EventBridge Rule | CodeCommitのブランチへのpushを検知してPipelineを開始するためのリソースです |
パラメータとして下記3つを受け取ります。
- デプロイ先のS3バケット名
- PipelineのトリガーとなるCodeCommitのリポジトリ名
- PipelineのトリガーとなるGitブランチ名(オプション。デフォルトで"main")
CodeCommitのリポジトリやデプロイ先のS3バケットは当テンプレートでは作成しないので、別途用意する必要があります。
AWSTemplateFormatVersion: '2010-09-09'
Description: Sample CodePipeline template for SPA(React) with S3+CloudFront.
Parameters:
DeployBucketName:
Type: String
SourceRepositoryName:
Type: String
SourceBranch:
Type: String
Default: main
Resources:
### CodeBuild and CodePipeline ###
CodeBuild:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub ${AWS::StackName}-CodeBuild
Description: !Sub 'Created by ${AWS::StackName}'
Source:
BuildSpec: buildspec.yml
Type: CODEPIPELINE
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: 'BUILD_GENERAL1_SMALL'
Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
LogsConfig:
CloudWatchLogs:
Status: ENABLED
CodePipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Sub ${AWS::StackName}-CodePipeline
RoleArn: !GetAtt CodepipelineServiceRole.Arn
ArtifactStore:
Location: !Ref ArtifactStoreBucket
Type: S3
RestartExecutionOnUpdate: false
Stages:
- Name: Source
Actions:
- Name: Source
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: '1'
Configuration:
RepositoryName: !Ref SourceRepositoryName
BranchName: !Ref SourceBranch
PollForSourceChanges: false
OutputArtifacts:
- Name: SourceArtifact
- Name: Build
Actions:
- Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: !Ref CodeBuild
EnvironmentVariables: !Sub |
[
{
"name":"DEPLOY_BUKET",
"type":"PLAINTEXT",
"value": "${DeployBucketName}"
}
]
InputArtifacts:
- Name: SourceArtifact
Namespace: BuildVariables
ArtifactStoreBucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Sub ${AWS::StackName}-artifactstore-${AWS::AccountId}
LifecycleConfiguration:
Rules:
- Id: clear-old-objects-rule
Status: Enabled
ExpirationInDays: 7
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
### ServiceRoles for CodeService ###
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
Path: /service-role/
RoleName: !Sub ${AWS::StackName}-codebuild-ServiceRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
CodepipelineServiceRole:
Type: AWS::IAM::Role
Properties:
Path: /service-role/
RoleName: !Sub ${AWS::StackName}-codepipeline-ServiceRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
### Resorces for EventBridge Rule ###
EventBridgeIAMPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'codepipeline:StartPipelineExecution'
Resource:
- !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}
ManagedPolicyName: !Sub '${CodePipeline}-policy'
EventBridgeIAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}-eventbridge-Role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- !Ref EventBridgeIAMPolicy
EventBridge:
Type: AWS::Events::Rule
Properties:
Name: !Sub 'changeEvent-rule-${CodePipeline}'
Description: !Sub 'for ${CodePipeline}. Created by ${AWS::StackName}'
EventPattern:
source:
- aws.codecommit
detail-type:
- 'CodeCommit Repository State Change'
resources:
- !Sub arn:${AWS::Partition}:codecommit:${AWS::Region}:${AWS::AccountId}:${SourceRepositoryName}
detail:
event: ['referenceCreated', 'referenceUpdated']
referenceType:
- branch
referenceName:
- !Ref SourceBranch
State: ENABLED
Targets:
- Arn: !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}
Id: CodePipeline
RoleArn: !GetAtt EventBridgeIAMRole.Arn
テンプレートからコマンドでリソースを構築する場合は、下記のようなコマンド実行になります。
ここではスタック名を「sample-ppl」としています。構築される各リソース名のプリフィックスとして使われます。
※ "tmpl_codepipeline.yaml"は↑の内容をファイルに保存したものです
aws cloudformation deploy \
--stack-name sample-ppl \
--template-file ./tmpl_codepipeline.yaml \
--parameter-overrides DeployBucketName=S3バケット名 SourceRepositoryName=CodeCommitリポジトリ名 \
--capabilities CAPABILITY_NAMED_IAM
補足説明
SourceがCodeCommitではない場合
EventBridge Ruleのリソースは、CodeCommitの特定のブランチ(テンプレートのパラメータで指定したもの。省略した場合は"main")の更新を検知した場合にCodePipelineを起動します。
ソースとしてAWS外のサービス(GitHubなど)を使用する場合には別の方法で変更を検知することになるので、必要ないリソースとなります。
また、SourceのConfigurationの設定は種類によって設定項目が異なります。種類別の設定項目については下記UserGuideを参照下さい。
AWS CodePipeline User Guide #reference-pipeline-structure
Configuration:
RepositoryName: !Ref SourceRepositoryName
BranchName: !Ref SourceBranch
PollForSourceChanges: false
(SourceがGitHubなどの場合にどういう変更が必要かは、別記事にしたいかもしれません)
CodeBuildの環境変数を設定している箇所
下記の部分で、テンプレートのパラメータとして指定されたデプロイ先バケット名を"DEPLOY_BUKET"という環境変数に設定しているので前述の「buildspec.yml」ファイルで変数として使えています。
Configuration:
ProjectName: !Ref CodeBuild
EnvironmentVariables: !Sub |
[
{
"name":"DEPLOY_BUKET",
"type":"PLAINTEXT",
"value": "${DeployBucketName}"
}
]
作業用バケットにはライフサイクルルールを設定している
CodePipelineがアーティファクトストアとして使用するS3バケットには、7日経過で自動でファイルが削除されるライフサイクルルールを設定しています。
ArtifactStoreBucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Sub ${AWS::StackName}-artifactstore-${AWS::AccountId}
LifecycleConfiguration:
Rules:
- Id: clear-old-objects-rule
Status: Enabled
ExpirationInDays: 7
CodeBuildとCodePipeline用のサービスロール
当記事では手順の簡略化のために、CodePipelineと一緒のテンプレートでCodeBuildとCodePipeline用のIAMロールが作成されるようになってます。
しかし、これらのサービスロールは内容的に使い回しができるものなので、別のスタックで作成した上でクロススタック参照あるいはパラメータ指定などで使い回すようにした方が同じようなものを何個も作らなくてよいので良いかと思います。
こちらのGitHubに、サービスロールを別のテンプレートに外だしして、CodePipelineのテンプレートではクロススタック参照でロールのArnを設定するバージョンのテンプレートも置いているので、よろしければご参考にしてください。
作成されたCodePipelineの実行例
パラメータで指定したCodeCommitリポジトリのブランチに"buildspec.yml"ファイルを含めたReactのソースをpushすることで、CodePipelineが起動してビルドが行われます。
ビルドが成功すると、パラメータで指定したデプロイ先のS3バケットに"npm run build"コマンド("buildspec.yml"のbuildフェーズで指定したコマンド)で出力されたビルド成果物が配置されます。
S3バケットにデプロイされた各オブジェクトのメタデータにはキャッシュコントロール用の値が設定されています。
(下の画像は"index.html"ファイルの例です。staticフォルダ配下のファイルには"no-cache"ではなく"max-age=2592000"が設定されているはずです。)
最後に
テンプレート化することで、同様の作業をなんども管理コンソール画面上でポチポチする手間が省け、作業ミスも予防できるので便利かと思います。
当記事がなにかのご参考になれば幸いです。
デプロイ先のS3バケットでは当記事のテンプレートでは作成しませんが、デプロイ先として使えるWebリソース公開用のS3バケットとCloudFrontをサクッと構築するためのテンプレートは「こちら」で記事にしているので、よろしければ合わせてご参照いただければと思います。