前回の記事でマイクロフロントエンドの基礎を学んだので、今回はReactでマイクロフロントエンドを構築し、GitHub ActionsでCI/CDを組んでみることにしました。
3つのプロジェクト(Marketing, Authentification, Dashboard)で計6つの画面を実装し、Containerでまとめて表示するような構成にします(記事内で作成するのは、Marketingのみになります)。
マイクロフロントエンド構築における鉄則
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で子プロジェクトを読み込む際には、間にもう一つコンポーネントをはさんであげる必要があります。
Marketing側ではReactDOM
をmount
という関数に置き換えて、 Reactが使われていることを外側のプロジェクト(Container)からわからないようにします。
const mount = (el) => {
ReactDOM.render(<App />, el);
};
export { mount };
Container側では新しくMarketingApp.js
というファイルをつくり、Marketing側で作成したmount
関数を読み込みます。そして、Container側のDOMにアクセスするためにuseRef Hooksを利用します。
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
を読み込みます。
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
にまとめます。
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で設定します。
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経由でアクセスできるようにします。
GitHub Actions
今回はGitHub ActionsでCI/CDを構築します。
- CI: ソフトウェアのビルドやテストを自動化することでソフトウェアの品質向上や開発効率化を目指す手法
- CD: リリースやデプロイを自動化する手法
GitHubリポジトリのmasterブランチにプッシュしてContainerフォルダに変更があった場合のみ、以下のワークフローが実行されるような設定を行います。
- dependencyのインストール
- 本番用ファイルのビルド
- S3へのファイルのデプロイ
AWSの設定
S3の設定
新しくバケットを作成します。バケット名はグローバルで一意なので、他ユーザがつくったバケットの名前は使えません。
パブリックアクセスをブロックしないようにチェックを外します。
ブラウザからCloudFront経由でS3にアクセスするために、バケットポリシーの設定が必要となります。JSONをベタ書きして作成することもできるのですが、今回はAWS Policy Generatorを使います。
ARNはバケットのプロパティに書いてあるものを使い、/*
を最後につけます。
作成したバケットポリシーをアクセス許可タブのバケットポリシーにコピペします。
これでS3の設定は完了です。
CloudFrontの設定
Edit DistributionのDefault Root Objectで読み込むhtmlファイルのS3のディレクトリを指定します。
IAMの設定
今回、GitHub ActionsのワークフローからAWS CLI経由でS3にビルドしたファイルをデプロイします。
そのため、S3オブジェクトにアクセス可能なIAMユーザーを準備し、アクセスキーIDとシークレットアクセスキーを作成する必要があります。
S3とCloudFrontのフルアクセスポリシーをIAMユーザにアタッチします。
作成されたアクセスキーIDとシークレットアクセスキーをGitHubのsecretsに保存し、以後は環境変数として使用します。
GitHub Actionsの設定
GitHubリポジトリで管理したいディレクトリの直下に.github/workflows
というフォルダを作成し、ワークフローを~.yml
ファイルに記述します。
container.yml
の作成
Containerのワークフローを以下のように記述します。
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(無効化)を設定しています。
- 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のワークフローは以下のようになります。
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タブを開くとジョブの進捗がわかり、無事に実行を終えると以下の画面になります。
CloudFrontのドメインにアクセスしたらちゃんと開けました!
おわりに
次の記事ではマイクロフロントエンドにおけるCSSとReact Routerの設定についてまとめます。
参考資料