GitHub ActionsでReactアプリをS3にデプロイ(+ Cloud Frontのキャッシュクリア)して、Slackに通知するまで

以前から業務でReactを使用していましたが、デプロイは手動でやっていたため、自動化できたらいいなという話をチームでしていました。
そこで、正式リリースもされたGitHub Actionsをせっかくなので使ってみたいなと思い、自動化に挑戦してみました。

※この記事はQiitaからの転載です。

GitHub Actionsとは? #

簡単に言うとGitHubが公式で公開している機能で、トリガーをきっかけにあらかじめ定義しておいた処理を実行するというものです。はじめはベータ版で公開されていましたが、2019年11月に正式公開されました。
いわゆるCI/CDが実現できます。

これまではCircle CIなど、外部サービスと連携させて行うことが多かったですが、GitHub Actionsを使用することで、GitHubだけで実現させることができます。

こちらの記事で概要から機能まで幅広く解説されています。


※以下、AWSアカウント、Slackワークスペースがある前提で進めていきます。


S3の準備 #

GitHub Actionsでデプロイする先を用意します。
マネジメントコンソールでS3の画面へ。

バケットの作成 #

  1. 「バケットを作成する」を選択
  2. 名前とリージョン
    • バケット名を入力(全世界で一意の名前の必要があります)
    • リージョンを選択
    • 「次へ」
  3. オプションの設定
    • 任意で設定(そのまま「次へ」でもOK)
  4. アクセス許可の設定
    • 4つともオフにする(本来は適度にオンがいいと思いますが、とりあえず動作させたいのでオフにしました) 4つともオンのままでOK(※2020/3/14修正)
    • 「次へ」
  5. 確認
    • 確認して問題なければ「バケットを作成」

静的サイトとして公開 #

※2020/3/14追記
今回のCloud FrontのOAI認証を用いるやり方では、この手順は不要なので静的サイトとして非公開のままで大丈夫です。
ちなみにCloud Frontと組み合わせるやり方は以下の記事にある通り、2パターンあるそうです。今回は構成1の方になります。

  1. 作成したバケットを選択
  2. プロパティタブ → Static website hosting を選択
  3. 「このバケットを使用してウェブサイトをホストする」を選択し、インデックスドキュメントに「index.html」を入力
  4. 「保存」を選択

その後、バケットホスティングと表示されていれば公開されています。
なお、バケット名はポリシー作成で使用するので控えておきます。

本来はここで、サイトへのアクセスを受け入れるために以下のようなバケットポリシーを設定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{※バケット名}/*"
        }
    ]
}

ですが、今回はCloud Frontを経由してのみアクセスできるようにするために、あとで設定を上書きするので、この場では何も設定しません。

ちなみにバケット作成時にブロックパブリックアクセスを4つすべてオフにした場合、オフにしたからといって、誰でも自由にアクセスできるようになるということではありません。
バケットポリシーで許可されたアクセスであったり、付属ポリシーで許可されたIAMユーザのアクセスであれば可能です。

Cloud Frontの準備 #

ホスティング自体はS3だけで可能ですが、CDNを前に置きたいので設定します。
マネジメントコンソールでCloud Frontの画面へ。

ディストリビューションの作成へ #

  1. 「Create Distribution」を選択
  2. Webの「Get Started」を選択

Create Distribution #

  • Origin Settings
    • Origin Domain Name:先ほど作成したバケットを選択(選択するとRestrict Bucket Accessが出現します)
    • Origin ID:Origin Domain Nameを選択すると自動的に入力される
    • Restrict Bucket Access:Yes(Yesにすると以下の項目が出現します)
    • Origin Access Identity:Create a New Identity
    • Comment:任意の名称(デフォルトのままでもOK)
    • Grant Read Permissions on Bucket:Yes, Update Bucket Policy
    • ※これ以外は任意(そのままでもOK)
  • Default Cache Behavior Settings
    • ※任意(そのままでもOK)
  • Distribution Settings
    • Default Root Object:「index.html」と入力
    • ※これ以外は任意(そのままでもOK)
  • 「Create Distribution」を選択

これでディストリビューションが作成されますが、動作するまで15分ほどかかります。
statusがIn ProgressからDeployedに変わればOKです。

ディストリビューションのARNをポリシー作成で使用するので控えておきます。
arn:aws:cloudfront::XXXXXXXXXXXX:distribution/XXXXXXXXXXXXXX のような形式です。
(ディストリビューションを選択して、Generalタブで確認できます)

補足 #

Create Distributionでの設定について。

Restrict Bucket AccessとOrigin Access Identity #

Restrict Bucket Accessについては、S3バケットへのアクセスをClout Front経由からのみにする設定です。
この設定をするにあたって、Cloud Frontのどのディストリビューションからのみアクセスを受け入れればいいのか、S3が判断するために必要なのがOrigin Access Identity(通称:OAI)です。
OAI自体はただの文字列にすぎませんが、ディストリビューションと紐づけることで、アクセス制限をかけられます。

ちなみにRestrict Bucket AccessOrigin Domain Nameでバケットを選択することで出現しますが、うまく動作しなかったのか出現しなかったことがありました。(何度かやりなおしたら出現しました)

Grant Read Permissions on Bucket #

ディストリビューションに紐づいているS3バケットのバケットポリシーを、OAIを使用したアクセス制限をかけるための設定へ上書きするかどうかです。
上書きすることで以下のようなバケットポリシーに変わります。

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {※OAI ID}"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{※バケット名}/*"
        }
    ]
}

Default Root Object #

こちらについては、S3バケットで静的サイトとして公開する際に設定しているのでは?となるかもしれませんが、Cloud Front側でも設定しておく必要があります(設定しないとブラウザからアクセスしたときにAccessDeniedになりました)

IAMの準備 #

GitHub Actionsでデプロイする際に使用する、デプロイ用のIAMユーザを作成します。
マネジメントコンソールでIAMの画面へ。

ポリシーの作成 #

  1. ポリシー一覧から「ポリシーを作成」を選択
  2. JSONに以下を記述して「ポリシーの確認」を選択
    {}の部分はこれまで控えたものに適宜置き換えてください。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::{※S3バケット名}",
                "arn:aws:s3:::{※S3バケット名}/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:GetDistribution",
                "cloudfront:GetDistributionConfig",
                "cloudfront:CreateInvalidation",
                "cloudfront:ListInvalidations",
                "cloudfront:GetInvalidation"
            ],
            "Resource": "{※Cloud FrontディストリビューションのARN}"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:ListDistributions",
                "cloudfront:ListStreamingDistributions"
            ],
            "Resource": "*"
        }
    ]
}

3.ポリシーの確認
 - 任意の名前と説明を入力して「ポリシーの作成」を選択

ユーザの作成 #

  1. ユーザ一覧から「ユーザを追加」を選択
  2. ユーザ名は任意、アクセスの種類は「プログラムによるアクセス」のみ選択して「次のステップ」
  3. アクセス権限
    • 「既存のポリシーを直接アタッチ」を選択
    • 先ほど作成したポリシーを選択して「次のステップ」
  4. タグ
    • 任意(そのまま次のステップでもOK)
  5. 確認して「ユーザの作成」を選択
  6. アクセスキーとシークレットキーを控えておく

Slackの準備 #

GitHub Actionsの結果を通知するためのWebhook URLを用意します。

Slack Appの作成 #

  1. Slackワークスペースのワークスペース名のところからメニューを開く
  2. ビルド画面へ
    • ※ワークスペースのオーナーの場合
      その他管理項目 → Appを管理する → 右上の「ビルド」
    • ※オーナーでない場合
      Slackをカスタマイズ → 左上のメニューを開く → App管理 → 右上の「ビルド」
  3. Start Buildingを選択
    • App Name:任意の名前
    • Development Slack Workspace:任意のワークスペース
    • 「Create App」を選択

以下、作成したAppの画面で進めます。

Botの作成 #

Incomming Webhookを使用するために必要なので用意します。

  1. Building Apps for SlackのAdd features and functionalityを開いて、Botsを選択
  2. How Your App Displaysで「Edit」を選択
    • Display Name (Bot Name):任意の名前を入力
    • Default username:任意の名前を入力
    • 「Add」を選択

Incomming Webhook URLの作成 #

  1. Building Apps for SlackのAdd features and functionalityを開いて、Incomming Webhooksを選択
  2. Activate Incoming WebhooksをONにして、「Add New Webhook to Workspace」
  3. 投稿するチャンネルを選択して、「許可する」を選択

これでWebhook URLが生成されるので控えておきます。

GitHubの準備 #

リポジトリ #

適当なリポジトリを用意します。

Reactアプリ #

Reactアプリをリポジトリにpushして用意しておきます。

ちなみに自分はcreate-react-appで作れる雛形でやりました。
yarnの場合だと、yarnが使える環境で以下のコマンドでサクッと作れます。
create-react-appはグローバルインストールサポートしなくなったらしいです)

$ yarn create react-app (作成する場所のパス)

それとGitHub Actionsの中でESLintを使うので、package.jsonのscriptsに"lint": "./node_modules/.bin/eslint \"src/**/*.js\""を追加しておきます。

GitHub Secrets #

GitHub Actionsで使用する環境変数を設定します。
公開したくない秘匿情報などを定義するのに向いており、GitHub Secretsに登録した変数の値は後から確認できないようになっています。

リポジトリのSettings → Secretsで以下の情報を登録しておきます。値はこれまで控えてきたものです。
(AWS_S3_BUCKET_PATHはs3://{s3のバケット名}形式です)

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_S3_BUCKET_PATH
  • SLACK_WEBHOOK_URL

なお、GITHUB_TOKENについては自動で値がセットされるので、設定しなくて大丈夫です。

GitHub Actionsのワークフロー定義ファイル作成 #

ワークフロー定義ファイルはymlで記述して、リポジトリルート直下に.github/workflows/ワークフロー定義ファイル名.ymlで配置します。
配置する場所さえ合っていれば、ファイル名は何でもOKです。

自分の場合は以下のようになりました(Docker使ってやってました)

(プロジェクトフォルダ)
├─ .github
|  ├─ workflows
|  |   ├─ ※ワークフロー定義ファイル
├─ node_modules
|  ├─ (各種パッケージ)
├─ public
|  ├─ favicon.ico
|  ├─ index.html
|  ├─ logo192.png
|  ├─ logo512.png
|  ├─ manifest.json
|  ├─ rebots.txt
├─ src
|  ├─ App.css
|  ├─ App.js
|  ├─ App.test.js
|  ├─ index.css
|  ├─ index.js
|  ├─ logo.svg
|  ├─ serviceWorker.js
|  ├─ setupTest.js
├─ docker-compose.yml
├─ Dockerfile
├─ .gitignore
├─ package.json
├─ README.md
├─ yarn.lock

以下の内容でymlファイルを作成します。
自分の場合、ステージング環境にデプロイするイメージで作りました。

name: Staging Deploy
on:
  push:
    branches:
      - release/**
env:
  project-name: ga-test-project

jobs:
  check:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x

      - name: Get Yarn Cache Directory Path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - name: Cache Node Modules
        uses: actions/cache@v1
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-${{ env.project-name }}-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
          restore-keys: |
            ${{ runner.os }}-${{ env.project-name }}-

      - name: Package Install
        run: yarn install

      - name: Lint
        run: yarn lint

      - name: Slack Notification by NonSuccess
        uses: 8398a7/action-slack@v2
        if: success() != true
        with:
          status: ${{ job.status }}
          author_name: 'check'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

  deploy:
    name: Build & Deploy
    needs: check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x

      - name: Get Yarn Cache Directory Path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - name: Cache Node Modules
        uses: actions/cache@v1
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-${{ env.project-name }}-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
          restore-keys: |
            ${{ runner.os }}-${{ env.project-name }}-

      - name: Package Install
        run: yarn install

      - name: Build
        run: yarn build

      - name: Publish to AWS S3 & CloudFront Cache Clear
        uses: opspresso/action-s3-sync@v0.2.3
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: 'ap-northeast-1'
          FROM_PATH: './build'
          DEST_PATH: ${{ secrets.AWS_S3_BUCKET_PATH }}

      - name: Slack Notification
        uses: 8398a7/action-slack@v2
        if: always()
        with:
          status: ${{ job.status }}
          author_name: 'deploy'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

いざデプロイ #

今回はrelease/が接頭辞のブランチをpushすることがトリガーになっているため、これに該当する名称のブランチを作成してpushしてみましょう。
ちゃんと動作していれば、リポジトリのActionsからワークフローの状況を確認できるようになっていると思います。
それぞれのステップのログも確認できるので、もしどこかでエラーになって失敗しても原因調査に役立ちます。

無事最後まで成功していれば、Cloud Frontからデプロイしたサイトにアクセスしてみましょう。
(ディストリビューションのGeneralのDomain NameでURLが確認できます)
また、S3に直接アクセスできないことも確認してみましょう。403 Forbiddenになるでしょうか。 (プロパティのStatic website hostingからURLが確認できます)
静的サイトとして非公開なので確認は不要です(※2020/3/14修正)

そして、Slackにもちゃんと通知できているでしょうか?
通知の内容は以下のような感じになります。

成功
ワークフロー成功時のSlack通知

キャンセル
ワークフローキャンセル時のSlack通知

失敗
ワークフロー失敗時のSlack通知

ワークフロー定義ファイルの簡単な解説 #

ワークフロー名 #

GitHubのActions上では、この名前が表示されます。

name: Staging Deploy

トリガー #

onでトリガーになるGitHubイベントを指定。
指定できるGitHubイベントについては以下を参照ください。

イベントをpushpull_requestにした場合は、さらにブランチ(branches)やタグ(tags)の指定ができます。

今回の場合は、release/が接頭辞のブランチをpushすることをトリガーにしています。

on:
  push:
    branches:
      - release/**

逆にこのブランチやタグはトリガーから除外したいという場合は、それぞれbranches-ignoretags-ignoreで指定ができます。

また、onにはGitHubイベントだけでなく、scheduleが指定でき、クーロン構文を指定してスケジュール実行させることもできます。

ワークフローの環境変数 #

ワークフロー全体で使用できる環境変数の指定。
ここで設定したものはenv.〇〇で使用することができます。

env:
  project-name: ga-test-project

ジョブ #

ワークフローで実行するジョブ定義を記述。複数指定可能です。
GitHubのActions上ではジョブ名が表示されます。

マシンの種類については以下を参照。

jobs:
  deploy: # ジョブID
    name: Build & Deploy # ジョブ名
    needs: check # このジョブ実行前に完了する必要があるジョブのID
    runs-on: ubuntu-latest # ジョブを実行するマシンの種類
    steps:
      # 各種ステップの定義

jobs.<job_id>.stepsで、そのジョブで実行するステップ処理を記述。
usesで指定しているactionsは、あらかじめ処理を定義しているもので、これを使用することでステップを組み立てやすくなります。

ステップ・リポジトリのチェックアウト #

リポジトリのコードを落としてきます。

- name: Checkout
  uses: actions/checkout@v2

ステップ・Node.jsのセットアップ #

ちなみにNode.jsだけでなく、いろんな言語のセットアップactionが提供されています。

- name: Setup Node
  uses: actions/setup-node@v1
  with:
    node-version: 12.x

ステップ・yarnのキャッシュを格納するディレクトリのパスを取得 #

- name: Get Yarn Cache Directory Path
  id: yarn-cache-dir-path
  run: echo "::set-output name=dir::$(yarn cache dir)"

ステップ・キャッシュの復元もしくは作成 #

keyの完全一致で既存のキャッシュを探し、マッチすればキャッシュをpathに復元。
なければrestore-keyの完全一致で既存のキャッシュを探し、マッチすればキャッシュをpathに復元。
さらになければrestore-keyの部分一致で既存のキャッシュを探し、見つければ最新のキャッシュをpathに復元。

keyの完全一致がなかった場合に、このワークフローが成功して完了した際にキャッシュを作成します。

詳細は以下を参照。

ちなみに今回の場合だと、yarn.lockの内容に変更があった時にkeyが変わります。

- name: Cache Node Modules
  uses: actions/cache@v1
  with:
    path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
    key: ${{ runner.os }}-${{ env.project-name }}-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
    restore-keys: |
            ${{ runner.os }}-${{ env.project-name }}-

キャッシュのあるなしで実行時間を比較してみたところ、こんな感じになりました。

キャッシュなし・46s
キャッシュなしの場合のワークフロー

キャッシュあり・26s
キャッシュありの場合のワークフロー

ステップ・yarnでパッケージのインストール #

- name: Package Install
  run: yarn install

ステップ・ESLintで静的解析チェック #

あらかじめpackage.jsonに定義しておいた、ESLint実行コマンドを実行。

テストコードを書いている場合は、これに加えてテスト実行もあると一緒に自動化できます。

- name: Lint
  run: yarn lint

ステップ・yarnでビルド #

ビルドの成果物は./buildに格納されます。

- name: Build
  run: yarn build

ステップ・S3にデプロイ & Cloud Frontのキャッシュクリア #

必要な情報を指定することで、S3にデプロイを実行します。また、S3から辿って、紐づいているCloud Frontのディストリビューションのキャッシュクリアも一緒に行ってくれます。

- name: Publish to AWS S3 & CloudFront Cache Clear
  uses: opspresso/action-s3-sync@v0.2.3
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} # デプロイ用IAMユーザのアクセスキー
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # デプロイ用IAMユーザのシークレットキー
    AWS_REGION: 'ap-northeast-1' # デプロイ先のリージョン
    FROM_PATH: './build' # デプロイするものがあるパス
    DEST_PATH: ${{ secrets.AWS_S3_BUCKET_PATH }} # デプロイ先のS3バケットパス

ステップ・Slackに通知 #

ifで通知する条件を指定。

  • success():ジョブの前のステップが成功した場合
  • always():ジョブのこのステップが実行された場合
  • cancelled():ワークフローがキャンセルされた場合
  • failure():ジョブの前のステップが失敗した場合、もしくはジョブが失敗した場合

job.statusにはそのジョブの成功、失敗、キャンセルに応じた値が入ります。

- name: Slack Notification
  uses: 8398a7/action-slack@v2
  if: always()
  with:
    status: ${{ job.status }}
    author_name: 'deploy'
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 自動で設定される
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # Slack投稿用のWebhook URL

開発用ブランチでも静的解析等を自動化したい #

上記のワークフローのうち、checkジョブは開発用ブランチでも使えると思います。

自分の場合はトリガーだけ変えて別ファイルにして、開発ブランチ用のワークフローを作りました。

on:
  push:
    branches-ignore:
      - master
      - release/**

ちなみに開発用ブランチでワークフローを実行して、そのブランチでプルリクを出した場合、プルリクの画面で以下のようにワークフローが表示されるようになります。
ワークフロー実行ブランチでのプルリクエスト


とりあえずデプロイまで無事到達できてよかったのですが、色々検証しながらやっていたら、かなり時間を使ってました(苦笑)
まぁ、はじめてやることに時間かかるのはつきものでしょうか。
GitHub Actionsを使おうとしている方の何かの参考になれば幸いです。

参考リンクまとめ #