- 概要
- CodeCommit を使うメリットはあるのか?
- AWS CDK のスタックの分け方について
- S3 と CloudFront の構築
- CodePipeline, CodeBuild の構築
- まとめ
概要
今回紹介する構成は以下の通りです
- AWS CDK 利用
- AWS CodeCommit でソースコード管理
- React.js を使って開発
- CodeBuild + CodePipeline でビルド&デプロイ
- S3 にビルド結果を保存
- CloudFront 経由で配信
基本的には CodeCommit よりも GitHub が使われることが多いと思いますが、今回は CodeCommit を利用します。
React.js を利用して開発したフロントエンドを CodeBuild でビルドして、S3 にアップロード、 CloudFront 経由でホスティングという、よくありそうな構成で解説します。
CodeCommit を使うメリットはあるのか?
CodeCommit は、 AWS 版 GitHub なんですが、基本的には GitHub のほうが使いやすくて高機能です。
CodeCommit を使うと、CodeBuild, CodePipeline といった、 AWS 関連サービスとの相性がいいため、CodeBuild などを使いたい場合は CodeCommit が選択肢に入ってきます。
ただ、後に書きますが、CodePipeline と S3 + CloudFront の相性がそこまで良くないので、個人的には、GitHub + GitHub Actions に軍配があがるかなと思います。
その上で、今回は CodeCommit + CodePipeline を使った場合にどうなるのか説明します。
AWS CDK のスタックの分け方について
AWS CDK は「スタック」の単位でリソースを作成しているので、リソースの依存関係などを考慮しながら、どこまでを一つのスタックにするかが重要です。
今回は、 CodeCommit の部分と、それ以外でスタックを分ける方針にします。S3 は CodePipeline は開発環境と本番環境を分けたいですが、CodeCommit はどちらの環境でも共通になるためです。
スタックが増えると管理が大変になるので、分ける必要がない部分はまとめるのがセオリーかなと思います。
CodeCommit のリポジトリは AWS CDK を使うほどでもないので、手動で作成し、それ以外の部分を AWS CDK で一つのスタックにすることにしています。
S3 と CloudFront の構築
細かいところは省略しますが、以下のように構築します
見やすさ都合上、import 文と、構築のコードをまとめて書きます
import * as s3 from 'aws-cdk-lib/aws-s3'; import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; // S3 バケット const s3Bucket = new s3.Bucket(this, "front-app-s3-bucket", { bucketName: `xxxxx`, }); // ClourFront const distribution = new cloudfront.Distribution(this, "front-app-distribution", { defaultBehavior: { origin: new S3Origin(s3Bucket), }, defaultRootObject: "index.html", // すべてのリクエストを react に飛ばすために、 S3 から 403 が返却された場合は / に飛ばす errorResponses: [ { httpStatus: 403, responseHttpStatus: 200, responsePagePath: "/", }, ], // 実際は domainNames や、certification(SSL証明書)を // 設定すると思うが、今回は省略 });
CodePipeline, CodeBuild の構築
CodePipeline と CodeBuild の関係について
CodeBuild とは、GitHub Actions のようなものです。CodeCommit のリポジトリ内にあるbuildspec.yml
というファイルの通りにビルドを実行してくれます。
CodePipeline は、 CodeBuild や CodeDeploy などの複数のモジュールを組み合わせて、ビルドやデプロイのフローを構築するためのものです。
今回は、 CodePipeline から CodeBuild を使うような方式で、パイプラインを構築します。
CodePipeline のプロジェクト作成
CodePipeline では、「プロジェクト」というものを作成し、その中にいろいろな「ステージ」を追加していくことになります。
ということで、まずは CodePipeline プロジェクトを作成します
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline'; const pipeline = new codepipeline.Pipeline(this, "front-app-codepipeline", { pipelineName: `front-app-deploy`, });
CodePipeline に CodeCommitSourceAction を追加
CodePipeline は、最初にコードを pull するステージを追加する必要があります。AWS CDK には CodeCommitSourceAction
というものがあるため、これを使います。
CodeCommit リポジトリは CDK で作成していないので、リポジトリ名から引っ張ってきます。
CodePipeline のステージには、 input と output が指定でき、基本的には前のステージの output を次のステージの input につなげることになります。
import * as codecommit from 'aws-cdk-lib/aws-codecommit'; import { CodeCommitSourceAction } from 'aws-cdk-lib/aws-codepipeline-actions'; // CodeCommit のリポジトリは CDK で作成していないので、リポジトリ名を指定してリポジトリを取得 const repository = codecommit.Repository.fromRepositoryName(this, "front-app-codecommit", // リソースのID "front-app-repository-name" // CodeCommit のリポジトリ名 ); // Code commit からソースを pull するステージを追加し、 // clone した結果を sourceOutput に入れる const sourceOutput = new codepipeline.Artifact("source_artifact"); pipeline.addStage({ stageName: "Source", actions: [ new CodeCommitSourceAction({ repository: repository, branch: "main", actionName: "source-codecommit", output: sourceOutput, // 今回は trigger に NONE を指定し、手動で pipeline を実行することにする trigger: CodeCommitTrigger.NONE, }) ] });
CodeBuild のステージを追加
CodeBuild のステージでは、 buildspec.yml の通りにビルドを実行します。
buildspec.yml は以下の通りです
version: 0.2 phases: install: commands: - npm install build: commands: - npm run build artifacts: base-directory: build files: - '**/*'
buildspec はシンプルで、npm run build して、ビルド結果が入っている build ディレクトリを artifact(成果物)とします。 artifact を指定するのが重要で、この artifact が次のステップに引き継がれることになります。
AWS CDK はこうなります。
import * as iam from 'aws-cdk-lib/aws-iam'; import { CodeBuildAction } from 'aws-cdk-lib/aws-codepipeline-actions'; // CodeBuild には、CodeCommit から clone する権限を与える const codeBuildRole = new iam.Role(this, "front-app-codebuild-role", { roleName: `FrontAppCodebuildRole`, assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"), }) codeBuildRole.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ["codecommit:Get*"], resources: [repository.repositoryArn] })); // コードをビルドするステージ // ビルドスクリプトは CodeCommit リポジトリにある buildspec.yml const codeBuild = new codebuild.PipelineProject(this, "front-app-codebuild-project", { projectName: `front-app`, environment: { // buildImage は、ビルドを実行する環境をしていするパラメータで、任意のパラメータなのですが、 // デフォルト値が、かなり古い AMZON LINUX になっており、エラーになってしまうので、 // 適切なイメージを指定する必要があります(最新のものを指定しておけばOKだと思います) buildImage: codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0, }, role: codeBuildRole, }); const buildOutput = new codepipeline.Artifact("build_output"); frontCodePipeline.addStage({ stageName: "Build", actions: [ new CodeBuildAction({ actionName: "build-codebuild", project: codeBuild, input: sourceOutput, outputs: [buildOutput], }), ], });
Build が終わったら、S3 へのアップロードと、CloudFront のキャッシュ削除を行う必要があるのですが、buildspec.yml には任意のスクリプトを記載することができるので、buildspec.yml の中で S3 のアップロードとキャッシュ削除を行うこともできます(roleに適切な権限を与える必要があります)。
ただ、今回は別のステージで行うようにしています。どちらが良いかは一長一短だと思います。
S3 へのデプロイステージを追加
最後に S3 のデプロイを行うステージを追加するのですが、S3 へのアップロードの後、 CloudFront のキャッシュ削除を行う必要があります。
しかし、CodePipeline には、「CloudFront のキャッシュを消す」というアクションは用意されていません。
そこで、以下のどちらかの方法で実現する必要があります。
- CodeBuild ステージをもう一つ追加する。CodeBuild ステージでは任意のスクリプトを実行できるので、そこから CloudFront のキャッシュ削除 API を叩く
- CodePipeline から Lambda function を呼び出し、 Lambda function 野中で CloudFront のキャッシュ削除 API を叩く
Lambda function を使う場合、その Lambda 上で実行されるスクリプトを用意したり、スクリプトをデプロイしたりする必要があり、非常に面倒なので、今回は CodeBuild 方式を採用しました。
このように、痒いところに手が届かないのが CodePipeline の微妙なところで、私が GitHub Actions をオススメする理由です。よく使いそうな機能なのに、AWS CodePipeline に機能が用意されていないのです。
話は戻りまして、CodePipeline に「Deploy」というステージを追加して、
- S3 へのアップロード
- CloudFront Distribution のキャッシュ削除
を行います
CKD は以下のとおりです
import { CodeBuildAction, S3DeployAction } from 'aws-cdk-lib/aws-codepipeline-actions'; // S3 デプロイ後に CloudFront のキャッシュをクリアしたいが、 // CodePipeline には CloudFront のキャッシュを削除するアクションは無いため、 // CodeBuild 経由でキャッシュ削除のコマンドを叩く const cacheInvalidation = new codebuild.PipelineProject(this, "codebuild-cloudfront-invalidation", { projectName: `front-app-cloudfront-cache-invalidation`, environment: { buildImage: codebuild.LinuxArmBuildImage.AMAZON_LINUX_2_STANDARD_3_0, }, // buildspec.yml を利用する方法以外に、ここに直接スクリプトを書くこともできる buildSpec: codebuild.BuildSpec.fromObject({ version: 0.2, phases: { build: { commands: [ `aws cloudfront create-invalidation --distribution-id ${distribution.distributionId} --paths "/*"`, ] } } }) }); // CodeBuild に CloudFront の権限を与えておく cacheInvalidation.addToRolePolicy(new iam.PolicyStatement({ resources: [ `arn:aws:cloudfront::${this.account}:distribution/${distribution.distributionId}` ], actions: ['cloudfront:CreateInvalidation'], })) // ビルド結果を S3 に put するアクションと、キャッシュ削除アクションは1つのステージにまとめちゃう frontCodePipeline.addStage({ stageName: "Deploy", actions: [ new S3DeployAction({ actionName: "upload-s3", input: buildOutput, bucket: s3Bucket, runOrder: 1, }), new CodeBuildAction({ actionName: "invalidate-cloudfront-cache", project: cacheInvalidation, // input については特に使いませんが、 // 必須パラメータだった気がするので適当に指定しています(曖昧) input: buildOutput, runOrder: 2 }) ] })
Build の章で書いた通り、 buildspec.yml の中で S3 へのアップロードとキャッシュの削除をすることもできますが、CloudFront や S3 の情報を環境変数で与える必要があったり、若干連携が複雑になるので、どちらが良いかは非常に微妙なところです。
なお、これと全く同じ例は AWS CDK のドキュメントに載っています
まとめ
以上が、React + S3 + CloudFront + CodePipeline の構成を CDK で構築する方法になります。
今回の方法は React に限らず、静的サイトをホスティングするときや、 vue.js など他のフレームワークにも使えるんじゃないかなと思います。
今回使ってみた感想ですが、 最初にも書いた通り、 CodeCommit は、 GitHub より機能が劣化しているにも関わらず、 CodeCommit + CodePipeline を使ったとしても、 GitHub + GitHub Actions に比べて大きなメリットを得ることはできません。
何らかの理由で GitHub が使えなかったり、CodeCommit が社内標準になっている場合は CodePipeline を使えばいいと思いますが、GutHub が使える場合はだいたい GitHub Actions のほうが快適な開発体験になると思います。
おそらくですが、 CodeCommit の利用者が少なく(通常はGitHubを採用するため)、CodePipeline の利用者も少ないため、機能が貧弱な気がしています。
デプロイのロールバック機能があればまだましになりそうですが... CodePipeline にはそういったいい感じの機能がありません。
もし、 CodePipeline を使おうと思っている人がいたら参考にしてください。