GitHub ActionsでReactアプリをS3 + Cloud Frontにデプロイして、Slackに通知するまで

公開:2020/10/05
更新:2020/10/17
20 min読了の目安(約18100字TECH技術記事

以前から業務で 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つともオンのままでOK
    • 「次へ」
  5. 確認
    • 確認して問題なければ「バケットを作成」

静的サイトとして公開に関して

今回の 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 を経由してのみアクセスできるようにするため、あとで設定を上書きします。この場では何も設定しなくて OK です。

ちなみにバケット作成時にブロックパブリックアクセスを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

ルート階層のオブジェクトとしたいものを指定します。
今回の場合は「index.html」なので、index.html でアクセスを受け取る感じです。

ちなみに S3 バケットを静的サイトとして公開する設定にしている場合、その設定の中にインデックスドキュメントを指定するところがあります。
これと同様の設定のように思えますが、Cloud Front 側でも設定しておく必要がありました(設定しないとブラウザからアクセスしたとき AccessDenied になりました)

IAM の準備

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

ポリシーの作成

  1. ポリシー一覧から「ポリシーを作成」を選択
  2. JSON に以下を記述して「ポリシーの確認」を選択
    {}の部分はこれまで控えたものに適宜置き換えてください。
  3. ポリシーの確認
    • 任意の名前と説明を入力して「ポリシーの作成」を選択

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": "*"
        }
    ]
}

ユーザの作成

  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にて作れる雛形でやりました。
npx や yarn コマンドでサクッと作れますが、自分は yarn を使っているので、以降は yarn コマンドで書いていきます。
create-react-appはグローバルインストールサポートしなくなったらしいです)

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

それと GitHub Actions の中で ESLint を使うので、package.json の scripts に以下を追加しておきます。

"scripts": {
  .
  .
  .
  "lint":  "yarn -s run 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 が確認できます)

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

成功
github-actions-slack-success.png

キャンセル
github-actions-slack-canceled.png

失敗
github-actions-slack-failed.png

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

ワークフロー名

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
github-actions-no-cache.png

キャッシュあり・26s
github-actions-cache.png

ステップ・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/**

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


とりあえずデプロイまで無事到達できてよかったのですが、色々検証しながらやっていたら、かなり時間を使ってました(苦笑)

まぁ、はじめてやることに時間かかるのはつきものでしょうか。
GitHub Actions を使おうとしている方の何かの参考になれば幸いです。

参考リンクまとめ