Amplifyで発生したビルドエラーの原因と解決案(Jamstack&Nuxt.js)
概要
- Nuxt.jsでSSGしたものをAmplify上でhostingするJamstack構成
- GitHubにPushされるとAmplify上でSSGされる流れ
- Amplifyプロジェクトを新規に構築していく中で、3件連続ビルドエラーを経験したのでまとめる
前提
- node.js:v14.17.0
- amplify-cli:4.51.1
- nuxt:2.15.6
結論
aws-exports.js
はフロントエンド側でimportが必要になるので、amplify.yml
でfrontendのビルド前にbackendのビルドでamplify push
を実行し、aws-exports.js
を生成する。(aws-exports.js
のgitignoreはデフォルトのままgit管理しない。他のブログとかを見ているとgitignoreから外してしまっている人もいるが複数人での開発を考慮すると含めないで解決した方が良い認識)amplify-cli
のバージョンがAmplifyビルドで動作しているものと、ローカルで使用しているものが一致しているか確認する。- Amplifyビルド用のロール作成が必要
エラーNo.1
背景
- AmplifyとGitHub連携して、Nuxt.jsでSSGしたものをhostingするところまでは問題なかった
- その後、
amplify add auth
して、プロジェクトにaws-amplify
と@aws-amplify/ui-vue
モジュールをインストールし、aws-amplify
を利用するための、pluginクラスを新規作成。pluginのクラスは下記。
import Vue from 'vue' import Amplify from 'aws-amplify' import '@aws-amplify/ui-vue' import awsExports from '../aws-exports' export default ({ isHMR }) => { if (isHMR) return if (process.client) { Amplify.configure(awsExports) Vue.use(Amplify) } }
- Amplify上のSSGするタイミングでエラーが発生
エラー内容
[fatal] Nuxt build error ERROR in ./plugins/amplify.client.js Module not found: Error: Can't resolve '../aws-exports' in 'plugins'
- ../aws-exports.jsが見つからないとのこと
原因
- amplifyコマンドでプロジェクト構築すると、
aws-exports.js
はgitignoreに含まれている - Amplify上のビルドフローは、ルートディレクトリに
amplify.yml
を配置しておくとで設定可能 - 最初はhostingだけ試していたので、
amplify.yml
を下記のようにしていた
version: 1 frontend: phases: preBuild: commands: - npm ci build: commands: - npm run generate artifacts: # IMPORTANT - Please verify your build output directory baseDirectory: dist files: - '**/*' cache: paths: - node_modules/**/*
解決策
amplify.yml
のfrontendのビルド処理の前に、backendでamplifyPush -simple
を差し込む- frontendの前に
amplify push
が走り、これによりaws-exports.js
がAmplify上で作成され、その後のfrontendのソースからも参照が可能となる
version: 1 backend: phases: build: commands: - '# Execute Amplify CLI with the helper script' - amplifyPush --simple frontend: phases: preBuild: commands: - npm ci build: commands: - npm run generate artifacts: # IMPORTANT - Please verify your build output directory baseDirectory: dist files: - '**/*' cache: paths: - node_modules/**/*
エラーNo.2
背景
- エラーNo.1の解決策をGitHubにPushして再びAmplify上でビルドしたら別のエラーが発生
エラー内容
# Executing command: amplifyPush --simple # Getting Amplify CLI Cloud-Formation stack info from environment cache # Start initializing Amplify environment: main # Initializing new Amplify environment: main (amplify init) File project: data should NOT have additional properties: 'graphqltransformer' JSONValidationError: File project: data should NOT have additional properties: 'graphqltransformer'
原因
- 以下のissueが参考になった
amplify-cli
のバージョンがローカルとAmplify上で違うため、ローカルで生成したファイルが正しく扱えないっぽい
解決策
- ローカルでは、その時点の最新版
4.51.1
を使っていたので、Amplify上も以下のように合わせた。 - 初期段階はPackageには何も指定されていなかった。
- latestを指定しているが、ローカルに明示的に合わせたほうが良いかもしれない。暫く経過観察予定。
エラーNo.3
背景
- エラーNo.2の解決策を保存して、再びAmplify上でビルドしたら別のエラーが発生
エラー内容
- Amplifyコンソール上で以下のエラーが発生
Your app does not have a role and you're attempting to interact with AWS resources In order for you to interact with AWS resources you need to attach a role to your app. This can be done in the General Settings page in the console
原因
- Amplifyビルドするのに必要なロールがあたっていないという感じ
解決案
- https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/how-to-service-role-amplify-console.html
- 上記の公式ドキュメントに沿ってロール作成と割当をすれば解決
eslintとprettier
概要
- eslintとprettierをGitコミット時に実行する
環境
- node:14.14.0
- eslint:7.23.0
- eslint-config-prettier:8.1.0
- husky:6.0.0
- lint-staged:10.5.4
- prettier:2.2.1
設定ファイル
prettierrc.json
{ "singleQuote": true }
eslintrc.json
{ "env": { "es6": true }, "globals": { "alert": false, "document": false, "$": false, "console": false }, "extends": ["eslint:recommended", "prettier"] }
.husky/pre-commit
#!/bin/sh #!/bin/sh . "$(dirname "$0")/_/husky.sh" npm run lint-staged
package.json
"scripts": { "lint-staged": "lint-staged" }, "lint-staged": { "*.{js,ts,jsx,tsx}": [ "npx eslint . --fix", "npx prettier --write ." ] },
Nuxt.jsでPAY.JPの初期化エラー(既にインスタンス化されています)対策
概要
- Nuxt.jsベースでJamstackなECサイトのサンプル実装しているときに、PAY.JPの初期化でエラーが出ることがあったのでメモ
- PAY.JPを実装するにあたり、payjp(2.0.5)のnode moduleを利用
- 画面側では
payjp
のCardElement
を生成し、トークンを作成して決済用のAPI呼び出す流れで実装
エラーとなる実装
- PAY.JPを利用する画面の
mounted
でpayjp
のインスタンス生成していた
<template> <div id="payjp-form"></div> </template> <script> mounted() { const payjp = window.Payjp(process.env.PAY_JP_PK) const elements = this.payjp.elements() const cardElement = elements.create('card') cardElement.mount('#payjp-form') cardElement.on('change', async (event) => { if (event.complete) { const res = await this.$payjp.createToken(cardElement) this.payjpToken = res.id this.isValidCard = true } }) }, </script>
- 画面初期表示時は問題なくカードレイアウトが表示されていたが、画面遷移後、再び画面を開くと「Error: 既にインスタンス化されています」というエラーが発生。
複数回の初期化処理はNGとのこと。
対応策(Pluginを作成し初期化を1度に制御する)
- plugins/payjp.jsを新規作成
import Vue from 'vue'; export default ({store, isHMR}) => { if (isHMR) return if (process.client) { Vue.prototype.$payjp = window.Payjp(process.env.PAY_JP_PK) } }
- nuxt.config.jsにPlugin定義追加
plugins: [ '~/plugins/payjp.js', ],
- エラーとなっていた画面の修正
<template> <div id="payjp-form"></div> </template> <script> mounted() { const elements = this.$payjp.elements() const cardElement = elements.create('card') cardElement.mount('#payjp-form') cardElement.on('change', async (event) => { if (event.complete) { const res = await this.$payjp.createToken(cardElement) this.payjpToken = res.id this.isValidCard = true } }) }, </script>
Firestoreを使用するFirebase Cloud Functionsをファイル分割した時のエラー対応
概要
- Firebase Cloud functionsで、Firestoreを利用している
- 当初は、index.jsに関数定義していた
- 複数関数定義する必要があったので、関数ごとにファイル分割した
- 1回目の関数実行時は特に問題ないが、2回目以降の呼び出し時に以下のエラーが発生 (初期化は1回にしなさいとのこと)
The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.
エラー発生時のコード
- index.js
const foo = require('./foo') const bar = require('./bar') exports.foo = foo.hoge exports.bar = bar.fuga
- foo.js
const functions = require('firebase-functions'); const admin = require('firebase-admin') exports.hoge = functions.region('asia-northeast1').https.onCall(async (data, context) => { admin.initializeApp() const fireStore = admin.firestore() // 以下、firestoreを使った処理 })
- bar.js
const functions = require('firebase-functions'); const admin = require('firebase-admin') exports.fuga = functions.region('asia-northeast1').https.onCall(async (data, context) => { admin.initializeApp() const fireStore = admin.firestore() // 以下、firestoreを使った処理 })
エラー解決用に修正したコード
- index.js(変更なし)
const foo = require('./foo') const bar = require('./bar') exports.foo = foo.hoge exports.bar = bar.fuga
- firestore.js(新規追加)
const admin = require('firebase-admin'); admin.initializeApp(); const fireStore = admin.firestore(); module.exports = { fireStore, };
- foo.js(変更)
const functions = require('firebase-functions'); const { fireStore } = require('./firestore') exports.hoge = functions.region('asia-northeast1').https.onCall(async (data, context) => { // 以下、firestoreを使った処理 })
- bar.js(変更)
const functions = require('firebase-functions'); const { fireStore } = require('./firestore') exports.fuga = functions.region('asia-northeast1').https.onCall(async (data, context) => { // 以下、firestoreを使った処理 })
JamstackなECサイトの考察
概要
- Jamstack Advent Calendar 2020 - Qiita の22日目の記事です。
- 株式会社トライビート| TRIBEAT CO., LTD. という会社でBtoCや業務システムのWebアプリケーション構築や運用・保守をしています。
- これまでJamstackという言葉が出てくる前から、社内独自のSSGの仕組みで試行錯誤して対応することがありました。
- Jamstack関係の環境やサービスが充実してくる中で、ECサイト構築が現実的に良い選択になりそうかを実際に試してみたので、考察やハマったポイントを備忘録としてまとめる。(まだ検証中)
アーキテクチャの構成
- microCMS
- Firebase hosting
- Firestore
- Firebase functions
- Firebase Authentication
- Nuxt.js
- Algolia
- Stripe
- GitHub Actions
※Next.jsとVercelへの移行も試したいが、データ層の設計がまとまったタイミングで検討する。
画面構成
ECサイト
- 商品一覧
- サイトのtopページ
- algoliaから商品一覧を取得して表示
- 検索を可能にするため、商品一覧情報を組み込んだ静的ページ化はせずSPAとする
- 商品をクリックすることで、商品詳細画面へ遷移
- 商品詳細画面
- SSGで静的ファイル化したページ
- 数量変更とカート追加が可能
- カート追加ボタンをクリックすることで、カート画面へ遷移
- カート画面
- アカウント画面(未実装)
- いつでも作成できるので、後回しにして進めていた画面
- 取り急ぎは、Firebase Authentication上でユーザー作成し、簡易画面からログイン処理とログアウト処理を実装
- アカウント情報の作成や更新できるようにする
- ログイン、ログアウトの組み込み
- 認証はあくまでも便利機能として考え、未認証でも購入できる想定
管理者サイト
- プレビュー画面
- 在庫管理画面
- 商品データに在庫数を付与したリスト画面
- 商品データの一覧はSSGビルドで静的ファイルとして作成
- 在庫数のみ、描画時にFirestroeから取得して当て込む
- 在庫数の変更が可能(Firestoreを更新する)
- 販売管理画面
- 販売データ(日付、合計金額)とステータスを表示したリスト画面
- FireStoreから動的にデータを取得するので、静的化せずSPAとする
- 詳細ボタンをクリックすると、購入者情報や購入商品一覧をオーバーレイで表示
Functions(API)
- 決済用関数
- 在庫チェックと金額改ざんチェック後、StripeのAPI呼び出し
- 在庫数変更(Firestore)
- 販売情報の登録(Firestore)
現状の簡易フロー図
実装時の考察
ECサイトの機能について
- 当初、商品一覧画面はページング付きですべてSSGしたもので進めていたが、やはり検索したいということからAlgoliaを利用したSPAとした
- ログイン情報の状態管理はVuexとlocalstorageを同期させた管理方法で実装(詳細は下に別途書く)
- ブラウザから直接Firestoreへwriteすることはやめ、Cloud Functionsを間に挟むように実装(ReadはOK)
- カートの状態管理も認証と同様にVuexで管理
- カート画面から購入時の在庫チェックは、その後のCloud Functions内でもするが一応フロント側でも見るようにした(要検討)
管理者サイトの機能について
- リアルタイム性が必要になるデータを扱うことが多く利用ユーザも限定されるため、SSGに意固地になりすぎずSPAでの設計も積極的に検討する
- Firestoreへのwrite系処理もCloud Functionsが間に挟めなくて良しとする(要検討)
- プレビュー画面ではmicroCMSのAPI_KEYがソースから見えるが良しとする(要検討)
データ層について
- ここが一番迷っていて検討要素が多い(Stripeでどこまでデータ管理すべきか見えていない)
- 在庫データ、商品データ、販売データをFirestoreで管理して作成を進めた(要検討)
- 在庫データと販売データはStripe側で持たせるほうがいいのか?
- 商品データはStripeに連携してそちらでも管理する方がいいのか?
- 商品データはmicroCMSからAPIで取得もでき2重管理になるがFirestoreにも入れた(要検討)
- 決済時のCloud Functionsで金額チェックをしたい 具体的には、商品マスタ情報の金額と画面から渡された数量が、画面から渡された合計金額に一致するかチェックしたい
- 上記チェック時の金額判断するためには、microCMSから更新された商品データの履歴が必要になるため、履歴管理をどこかで持たせたい
- 商品データもStripeでもたせる方がいいのか?
- 顧客データ(要検討)
- 認証周りは簡易実装しかしていないので、現状はFirebase Authenticationに固定ユーザー作って試している ユーザー情報が登録されたときに、StripeとFirestoreどちらで管理すべきか?
SSG(ビルド)について
- 各商品ページをSSG
- 上記で、microCMSのAPIから商品データ一式取得するので、AlgoliaとFirestoreへ必要なデータ更新するように実装
- Firestoreへの更新をトリガーにしたCloud Functionsを用意してAlgoliaのデータと同期するようにした方がいいか迷っている。
- microCMSから商品データ更新(追加・削除)された場合に、Firestoreのデータを全て変更する必要はないので、microCMSで設定するカスタムのWebhookでやってもいいものか迷っている(詳細は下に別途書く)
ハマったポイントと解決案
1. GitHub Actionsでデプロイ時にfirebaseコマンドがないと言われた
背景
GitHub ActionsのFirebaseデプロイでは、よく以下を利用したサンプルを見かけるが、まずは理解のためにも自分でfirebaseコマンドを叩くように書いてみた
デプロイ用のymlでは以下のように、stripeのシークレットKEYを設定し、デプロイコマンドを叩いていた
- name: deploy to Firebase Hosting run: | firebase functions:config:set stripe.secretkey=${{secrets.STRIPE_SECRET_KEY}} firebase deploy env: FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}}
- GitHub Actions内のデプロイ処理で、firebase command not foundエラーが発生していた
解決案
- デプロイの前処理でビルド処理が実行されるので、そこの npm install 時にfirebase-toolsを、-gでインストール
npm install -g firebase-tools
2. ある日突然、firebaseのデプロイがコケるようになった
背景
- 12/16になったらデプロイがコケるようになった。
- GitHub Actionsのエラー内容は以下
Error: An unexpected error has occurred. Error: Process completed with exit code 2.
解決案
- firebase-toolsのバージョンチェック
- プロジェクト生成時は8系で進めていたが、12/16に9系に上がっていた!
- とりあえずバージョンを8系にするように変更して正常動作することを確認できた
npm install -g firebase-tools@^8.0.0
- 9系になったことで設定ファイル系のフォーマットなど変わったのかも、まだ詳細調査はできていない。後日する予定。
3. Firebase Auth使った認証状態の保持
背景
- 認証自体はFirebase Authenticationを利用して簡易実装していたが、どう状態保持するか悩んだ
- 最初、middlewareにauth処理を実装してvuexで状態保持を検討した
- npm run devで確認しているとうまく動くように見えるが、npm run generate & npm run startだとうまくいかない
- middlewareはF5(page refresh)時には呼ばれないため、Jamstack構成では使えないと判断
- 次に、共通のヘッダコンポーネントを用意していたので、そこのbeforeCreateでfirebaseの認証チェックするようにした
- 商品ページの切り替え時に、その都度認証確認リクエストが飛ぶことにモヤモヤ
- 今後ページが増えていく中で、ヘッダが全て共通になるのかモヤモヤ
- カートの状態も何かしらの方法で保持しなくてはいけないことに気づき、使えないと判断
解決案
- 最終的には、以下のpluginを導入しlocalstorage管理することで対応
4. GitHub Actionsのパフォーマンス検討
背景
- GitHub Actionsの処理全体で約4〜5分掛かっている
- 内訳としては、ビルド(npm install含む)とfirebaseのデプロイでそれぞれ2分くらい
- ビルド処理は常に1からインストールしているので、対策ないか調査
解決案
- 探すとすぐに以下が見つかった。
- ただ、使ってみたけど特に大きくパフォーマンスが改善されることはなく、ここについては後日調査
5. nuxt run generate 時にFirestoreへデータ入れようとして発生したワーニング
背景
- build処理時、nuxt.config.jsのgenerateで以下のような感じでFirestoreにデータ更新していた
generate: { async routes() { const { data } = await axios.get( `https://xxx.microcms.io/api/v1/items?limit=100`, { headers: { 'X-API-KEY': API_KEY } } ) const fsStocks = firestore.collection('items') const batch = firestore.batch() const pages = [] data.contents.forEach((content) => { pages.push({ route: `/${content.id}`, payload: content }) const nycRef = fsStocks.doc(content.id) batch.set(nycRef, content) }) await batch.commit() return pages } },
- npm run generate すると一応SSGは正常完了するものの、以下のようなワーニングが表示される
⚠ Nuxt Warning The command 'nuxt generate' finished but did not exit after 5s This is most likely not caused by a bug in Nuxt Make sure to cleanup all timers and listeners you or your plugins/modules start. Nuxt will now force exit DeprecationWarning: Starting with Nuxt version 3 this will be a fatal error
解決案
- generate完了時に明示的にFirestoreを終了させてやる必要がある
- nuxt.config.jsのhooksに以下を追記
hooks: { generate: { done(builder) { firestore.terminate().then() } } },
6. Firestoreへデータ登録かmicroCMSのWebhookで迷った
背景
- ビルド時にmicroCMSから全データ取得しているので、その流れでFirestoreも一律全データ更新するようにしていた
- microCMSで商品データ更新(追加、削除)された場合に、全データ更新ではなく、更新データのみFirestoreに反映した方が効率よいと思った
- microCMSではWebhookにカスタム通知が設定できるので、それを利用しCloud Functionsを呼び出せば、1件ごとのFirestore更新ができるのではないかと思った
解決案
- 以下のようなCloud Functionsを用意し、カスタム通知にURL設定することでWebhook連携の接続までは簡単にできることを確認
exports.updateCms = functions.region('asia-northeast1').https.onCall(async (data, context) => { functions.logger.log("called updateCms.") }) exports.updateCms2 = functions.region('asia-northeast1').https.onRequest((request, response) => { functions.logger.log("called updateCms2.") })
- 関数内でリクエストパラメータを取得しFirestore更新まではいけるものの、セキュリティが心配に。
- microCMSのカスタム通知で渡せるパラメータは決まっているため、認証用トークンなどは含めれないはず?
- カスタム通知に設定するURLにクエリストリングを付与して、それをトークンとして扱うことも検討したが、一旦はそのままビルド時に全データ更新で落ち着いた。
結論
PHPのログライブラリ(monolog)のPull Requestがマージされた
2019年に上記ブログで書いてたとおり、monologを使った日付のログローテート処理でうまくいかないことがありPRを投げていた。
PR作成してから2週間程は気にして見てたけど、特に音沙汰なかったため、自分の拙い英語ではやはり相手にされないのかと勝手に納得していた。自分の関わっているプロジェクトでは必要な修正だったためライブラリのクラスをoverrideして運用していた。
月日は経ち、プロジェクトで使っているフレームワークのバージョンを上げるタイミングがやってきた。それに伴いライブラリ等のバージョンも上げることになった。
バージョンを上げるための調査は僕とは別の人が進めいて、今朝、その人から僕のPRがマージされてると教えてもらった。寝耳に水で、最初何のことかもわからないくらいだった。
確認してみるとPRから3ヶ月後くらいにマージされていて、当たり前だがメール通知もちゃんと届いていた。
PRは普段から業務でPrivateなリポジトリに対して作成しているが、一般的によく使われているオープンソースに対しては初めてだったのでなんだかとても嬉しかった。
今後も続けていきたい。
いつも昼はカップラーメンや柿ピーで済ませてしまうけど、今日は自分へのご褒美にチキンカツのサンドイッチを食べた。