マイクロフロントエンド② GitHub ActionsでCI/CDの構築

前回の記事でマイクロフロントエンドの基礎を学んだので、今回はReactでマイクロフロントエンドを構築し、GitHub ActionsでCI/CDを組んでみることにしました。

https://qiita.com/suzuki0430/items/b1b7c2d63e1eaf4d2c66

3つのプロジェクト(Marketing, Authentification, Dashboard)で計6つの画面を実装し、Containerでまとめて表示するような構成にします(記事内で作成するのは、Marketingのみになります)。
![スクリーンショット 2021-03-17 21.15.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/55dbe06f-38cb-bc8b-ec6a-d5e2656f7e79.png)

#マイクロフロントエンド構築における鉄則

##1. 子プロジェクト同士は疎結合させる
プロジェクト間での関数/オブジェクト/クラスのimportやstateの共有をさせないようにする必要があります。
もし2つのプロジェクト(MarketingとAuthentificationなど)でstateの共有などを行ってしまうと、数年後に片方のプロジェクトを使わなくなってしまったときに、もう片方も大きく修正を加えなければならない状態になってしまうからですね。

##2. Containerと子プロジェクトはできるだけ疎結合させる
親プロジェクトであるContainerと子プロジェクト同士も疎結合が基本です。Containerは子プロジェクトのフレームワークの種類に依存しないような設計になっていなければなりません。ただ、ルーティングを実装するときにコールバックやイベントを渡すこともあり、完全な疎結合化は難しいです。

##3. CSSが他のプロジェクトの表示に影響を与えない
クラス名が被ってしまうことによる表示崩れに気をつける必要があります。

##4. Containerは子プロジェクトのバージョンを選択できる
常に最新のバージョンを選択する場合はContainerの再デプロイが必要なく、特定のバージョンを選択する場合は再デプロイが必要になります。

#プロジェクト構築の注意点

##Webpackの設定ファイルは3つ必要
開発環境(`webpack.config.dev.js`)、本番環境(`webpack.config.prod.js`)、共通設定(`webpack.common.js`)の3つのファイルを作成します。

##疎結合な設計
鉄則2で書いたように、Containerと子プロジェクトは疎結合である必要があります。そのため、Containerで子プロジェクトを読み込む際には、間にもう一つコンポーネントをはさんであげる必要があります。
![スクリーンショット 2021-03-18 7.35.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/48316013-084d-256b-8f50-c12bdd0efdaf.png)

Marketing側では`ReactDOM`を`mount`という関数に置き換えて、 Reactが使われていることを外側のプロジェクト(Container)からわからないようにします。

```bootstrap.js
const mount = (el) => {
  ReactDOM.render(<App />, el);
};

export { mount };
```
Container側では新しく`MarketingApp.js`というファイルをつくり、Marketing側で作成した`mount`関数を読み込みます。そして、Container側のDOMにアクセスするためにuseRef Hooksを利用します。

```MarketingApp.js
import { mount } from 'marketing/MarketingApp';
import React, { useRef, useEffect } from 'react';

export default () => {
  const ref = useRef(null);

  useEffect(() => {
    mount(ref.current);
  });

  return <div ref={ref} />;
};
```
最後にContainer側の`App.js`で`MarketingApp`を読み込みます。

```App.js
import React from 'react';
import MarketingApp from './components/MarketingApp';

export default () => {
  return (
    <div>
      <MarketingApp />
    </div>
  );
};
```
このような設計で実装すれば、Marketing側をReactからVueに変えなければいけなくなったとしても、同様に`mount`関数としてエクスポートしてあげればContainer側でそのまま読み込むことができます。


#本番環境の構築

##`webpack.prod.js`の作成

`webpack.dev.js`と共有する設定については、`webpack.common.js`にまとめます。

```webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
            plugins: ['@babel/plugin-transform-runtime'],
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ]
};
```
`webpack.prod.js`は以下のように記述し、ファイルの最後で`webpack.common.js`とマージしてエクスポートします。`PRODUCTION_DOMAIN`という環境変数を使っていますが、これはMarketingの`remoteEntry.js`が配置されるS3のドメインを指しており、後ほどGitHubで設定します。

```webpack.prod.js
const { merge } = require('webpack-merge'); // webpack.common.jsとマージするためのモジュール
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); //外部プロジェクトを読み込むためのモジュール
const commonConfig = require('./webpack.common');
const packageJson = require('../package.json');

const domain = process.env.PRODUCTION_DOMAIN; //S3ドメイン(GitHubで環境変数を設定する)

const prodConfig = {
  mode: 'production',
  output: {
    filename: '[name].[contenthash].js', //アセットの中身が変更されるごとにハッシュを付与する
    publicPath: '/container/latest/', // ビルドファイルのアウトプット先
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
        marketing: `marketing@${domain}/marketing/latest/remoteEntry.js`, // remoteEntry.jsがあるS3バケットのディレクトリ
      },
      shared: packageJson.dependencies,
    }),
  ],
};

module.exports = merge(commonConfig, prodConfig); // webpack.common.jsとwebpack.prod.jsをマージしてエクスポート
```

##構成
ビルドしたファイルをS3にデプロイして、ブラウザからはCloudFront経由でアクセスできるようにします。
![スクリーンショット 2021-03-18 21.10.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/de7fe881-000c-a879-3990-6d0a91c696b2.png)

###GitHub Actions
今回はGitHub ActionsでCI/CDを構築します。

- CI: ソフトウェアのビルドやテストを自動化することでソフトウェアの品質向上や開発効率化を目指す手法
- CD: リリースやデプロイを自動化する手法

GitHubリポジトリのmasterブランチにプッシュしてContainerフォルダに変更があった場合のみ、以下のワークフローが実行されるような設定を行います。

1. dependencyのインストール
2. 本番用ファイルのビルド
3. S3へのファイルのデプロイ

##AWSの設定
###S3の設定
新しくバケットを作成します。バケット名はグローバルで一意なので、他ユーザがつくったバケットの名前は使えません。
![スクリーンショット 2021-03-15 21.13.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/09627c81-8045-afdb-d45f-78db9647e61d.png)

ホスティングの設定を行います。
![スクリーンショット 2021-03-15 21.16.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/eef340e0-c398-0698-193f-18735aa294ee.png)

パブリックアクセスをブロックしないようにチェックを外します。
![スクリーンショット 2021-03-15 21.19.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/3402f25a-0e35-bf3e-38b2-4c043c58ec34.png)

ブラウザからCloudFront経由でS3にアクセスするために、バケットポリシーの設定が必要となります。JSONをベタ書きして作成することもできるのですが、今回はAWS Policy Generatorを使います。
![スクリーンショット 2021-03-15 21.24.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/1a3664bf-a02d-06db-d04d-7cb9ccbe4541.png)

ARNはバケットのプロパティに書いてあるものを使い、`/*`を最後につけます。
![スクリーンショット 2021-03-18 21.55.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/5b61dc0d-d05a-7641-9dcd-c80b215319d5.png)

作成したバケットポリシーをアクセス許可タブのバケットポリシーにコピペします。
これでS3の設定は完了です。
![スクリーンショット 2021-03-18 22.01.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/19b3f71e-138f-9ec0-5647-6027ea490aa6.png)

###CloudFrontの設定
Distributionを新しく作成します。
![スクリーンショット 2021-03-15 21.30.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c12a9064-e8b8-2cf9-0311-4aafc9e8eb80.png)

Edit DistributionのDefault Root Objectで読み込むhtmlファイルのS3のディレクトリを指定します。
![スクリーンショット 2021-03-15 21.33.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/eb924e99-bd19-9dc7-4cbc-d26a03fc890b.png)

エラーレスポンスの設定を行います。
![スクリーンショット 2021-03-15 21.34.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/4708e949-f171-ff1a-ac02-13ab25240289.png)

###IAMの設定
今回、GitHub ActionsのワークフローからAWS CLI経由でS3にビルドしたファイルをデプロイします。
そのため、S3オブジェクトにアクセス可能なIAMユーザーを準備し、アクセスキーIDとシークレットアクセスキーを作成する必要があります。
![スクリーンショット 2021-03-15 21.40.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/2a943e30-2af4-97d0-1aa4-0dd1a2b0928e.png)

S3とCloudFrontのフルアクセスポリシーをIAMユーザにアタッチします。
![スクリーンショット 2021-03-15 21.43.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ca367507-978f-b05c-ef88-75e0bb96be6d.png)

作成されたアクセスキーIDとシークレットアクセスキーをGitHubのsecretsに保存し、以後は環境変数として使用します。
![スクリーンショット 2021-03-15 21.45.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/19bb0008-9022-c55d-c039-1d54e783ef8c.png)

##GitHub Actionsの設定
GitHubリポジトリで管理したいディレクトリの直下に`.github/workflows`というフォルダを作成し、ワークフローを`~.yml`ファイルに記述します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/ba9db524-29de-2ad8-16d9-62850ad9ab9f.png" width="400">


###`container.yml`の作成
Containerのワークフローを以下のように記述します。

```container.yml
name: deploy-container #ワークフローの名前

on: #イベント
  push: #プッシュ
    branches: #プッシュ先ブランチ
      - master
    paths: #変更箇所
      - 'packages/container/**'

defaults: #すべてのジョブに適用されるデフォルト設定
  run:
    working-directory: packages/container #runを実行するディレクトリ

jobs: #ジョブ
  build: #ビルド
    runs-on: ubuntu-latest #ジョブを実行するための仮想マシン

    steps:
      - uses: actions/checkout@v2 #仮想マシンにロードするためにチェックアウトするブランチ
      - run: npm install #dependencyのインストール
      - run: npm run build #Webpackでビルド
        env:
          PRODUCTION_DOMAIN: ${{ secrets.PRODUCTION_DOMAIN }} #CloudFrontのドメイン名(GitHubのsecretsに保存)

      - name: ACTIONS_ALLOW_UNSECURE_COMMANDS
        run: echo 'ACTIONS_ALLOW_UNSECURE_COMMANDS=true' >> $GITHUB_ENV

      - uses: chrislennon/action-aws-cli@v1.1
      - run: aws s3 sync dist s3://${{ secrets.AWS_S3_BUCKET_NAME }}/container/latest #S3の/container/latestにビルドしたファイルを配置
        env: #AWS-CLIの実行に必要なアクセスキーIDとシークレットアクセスキーの指定
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - run: aws cloudfront create-invalidation --distribution-id ${{secrets.AWS_DISTRIBUTION_ID}} --paths "/container/latest/index.html" #CloudFrontのキャッシュの無効化
        env: #AWS-CLIの実行に必要なアクセスキーIDとシークレットアクセスキーの指定
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
```

再デプロイしてもCloudFrontのキャッシュによって表示が変わらないことがあるので、以下の部分でinvalidation(無効化)を設定しています。

```yml
      - run: aws cloudfront create-invalidation --distribution-id ${{secrets.AWS_DISTRIBUTION_ID}} --paths "/container/latest/index.html"
          env:
            AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
            AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
```

###`marketing.yml`の作成
Marketingのワークフローは以下のようになります。

```marketing.yml
name: deploy-marketing

on: #イベント
  push:
    branches:
      - master
    paths:
      - 'packages/marketing/**'

defaults:
  run:
    working-directory: packages/marketing

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - run: npm install
      - run: npm run build
      - name: ACTIONS_ALLOW_UNSECURE_COMMANDS
        run: echo 'ACTIONS_ALLOW_UNSECURE_COMMANDS=true' >> $GITHUB_ENV

      - uses: chrislennon/action-aws-cli@v1.1
      - run: aws s3 sync dist s3://${{ secrets.AWS_S3_BUCKET_NAME }}/marketing/latest
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - run: aws cloudfront create-invalidation --distribution-id ${{secrets.AWS_DISTRIBUTION_ID}} --paths "/marketing/latest/remoteEntry.js"
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

```

##GitHub Actionsワークフローの実行
Containerフォルダのファイルを変更した上で、`git push origin master`でリモートリポジトリにプッシュするとワークフローが実行されます。
リポジトリのActionsタブを開くとジョブの進捗がわかり、無事に実行を終えると以下の画面になります。
![スクリーンショット 2021-03-18 23.13.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/9c69a269-6cbd-4e93-be65-d6b6cf44a28e.png)

CloudFrontのドメインにアクセスしたらちゃんと開けました！
![スクリーンショット 2021-03-18 23.20.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c696dab0-4336-e2bf-483d-36aefcd868a1.png)

#おわりに
次の記事ではマイクロフロントエンドにおけるCSSとReact Routerの設定についてまとめます。

#参考資料

https://www.udemy.com/share/103KXSCEEZd11VRno=/

https://docs.github.com/ja/actions