猫でもわかるWebプログラミングと副業

本業エンジニアリングマネージャー。副業Webエンジニア。Web開発のヒントや、副業、日常生活のことを書きます。

Code Pipeline で CloudFront のキャッシュを飛ばす【React, S3, CloudFront】

概要

今回紹介する構成は以下の通りです

  • 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 のドキュメントに載っています

docs.aws.amazon.com

まとめ

以上が、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 を使おうと思っている人がいたら参考にしてください。