非公開npmパッケージが使われたFirebase Functionsをデプロイした時の話
株式会社TERASSで主にバックエンド開発・インフラ構築をしている0okmです。
最近、業務において内部実装に社内用の非公開(private)npmパッケージを使ったFirebase Functionsをデプロイするにはどうしたらよいか調べる機会があったのでその結果を記事化してみたいと思います。
前提
- Firebase:12.5.2
- Node.js:18.17.0
- 非公開npmパッケージはGitHub Packagesとして登録されている。
準備
まず、非公開npmパッケージを使ったソースコードを持つFirebaseプロジェクトを用意します。
.
├── dist
├── node_modules
├── package.json
├── firebase.json
├── .firebaserc
├── postbuild.js
├── src
│ ├── helloworld
│ │ └── index.ts
│ └── index.ts
└── tsconfig.json
↓この中にある「@some-org/some-private-package」が対象となる非公開npmパッケージです。
{
"name": "some-project",
"version": "0.0.0",
"private": true,
"main": "index.js",
"scripts": {
"build": "tsc && ./postbuild.js",
"deploy": "firebase deploy --only functions"
},
"dependencies": {
"@some-org/some-private-package": "*", // これが非公開npmパッケージ
"firebase-admin": "^11.9.0",
"firebase-functions": "^4.4.0"
},
"devDependencies": {
"glob": "^10.3.4"
}
}
{
"compilerOptions": {
"outDir": "dist",
・・・
・・・
},
"include": ["src"],
"exclude": ["node_modules"]
}
{
"functions": {
"source": "dist",
"runtime": "nodejs18",
"codebase": "default",
"predeploy": ["yarn build"]
},
}
#!/usr/bin/env node
const OUT_DIR = 'dist'
const fs = require('fs')
const path = require('path')
const glob = require('glob')
const packageFile = glob.sync('package*.json')
packageFile.forEach((file) => {
fs.copyFileSync(file, path.join(OUT_DIR, file))
})
非公開npmパッケージを使っている実装は以下の通りです。
import { setGlobalOptions } from 'firebase-functions/v2'
setGlobalOptions({
region: 'asia-northeast1',
})
export * from './helloworld'
// 非公開npmパッケージをインポートして"hello"という関数を使う。
import { hello } from '@some-org/some-private-package'
import { onRequest } from 'firebase-functions/v2/https'
export const helloworld = onRequest((request, response) => {
response.send(hello())
})
今回の場合、上記hello
は単純に"Hello World!"
という文字列を返すだけの関数です。
ためしに何もせずそのままデプロイしてみる
ローカル環境からデプロイしてみる。
yarn deploy
当然失敗する。
Build failed with status: FAILURE and message: npm ERR! code E404
npm ERR! 404 Not Found - GET https://registry.npmjs.org/@some-org/some-private-packaget - Not found
npm ERR! 404 '@some-org/some-private-package@*' is not in this registry.
「デフォルトのレジストリにはこのパッケージが存在しない」と言われる。加えて仮に存在したとしても非公開パッケージなので取得することはできないはず。ってことはnpmにこのパッケージのレジストリの場所と非公開パッケージにアクセスする方法を教えてあげればいいのではないかと想像がつく。
各種ドキュメントを確認する
.npmrc
を使ってレジストリの指定と非公開npmパッケージへアクセスするためのアクセストークンを設定してあげればよいとの事。今回の非公開npmパッケージはGitHub Packagesとして登録されているのでGitHub Packagesのレジストリを使う。
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
@some-org:registry=https://npm.pkg.github.com/
NPM_TOKEN
が上記アクセストークンに該当する。このトークンは細かな権限管理が可能で一つのトークンに必要以上の権限を与えるような運用は推奨されていない。
GitHub Packagesの場合以下の方法でアクセストークンを発行できます。
通常の運用であればアクセストークンのような機密情報はこのような設定ファイルに直接書かくことはせず、上記NPM_TOKEN
のような変数を用意しビルド実行時に環境変数などを経由して値を設定するといった方法をとるが、今回は検証なので.npmrc
にトークンの値を直接書いて進める。
作成した.npmrcをプロジェクト直下に配置し、再度デプロイする
.
├── dist
├── .npmrc
├── node_modules
おや!? 前回と同じエラーが発生した。なぜだ?
Build failed with status: FAILURE and message: npm ERR! code E404
npm ERR! 404 Not Found - GET https://registry.npmjs.org/@some-org/some-private-packaget - Not found
npm ERR! 404 '@some-org/some-private-package@*' is not in this registry.
Firebase Functionsのデプロイでは何をやっているのかを確かめる
デプロイコマンド実行後、エラーが発生する直前までに以下のようなメッセージが出力されている。
i functions: preparing dist directory for uploading...
i functions: packaged /workspace/dist (3.64 KB) for uploading
i functions: ensuring required API run.googleapis.com is enabled...
i functions: ensuring required API eventarc.googleapis.com is enabled...
i functions: ensuring required API pubsub.googleapis.com is enabled...
i functions: ensuring required API storage.googleapis.com is enabled...
✔ functions: required API eventarc.googleapis.com is enabled
✔ functions: required API run.googleapis.com is enabled
✔ functions: required API pubsub.googleapis.com is enabled
✔ functions: required API storage.googleapis.com is enabled
i functions: generating the service identity for pubsub.googleapis.com...
i functions: generating the service identity for eventarc.googleapis.com...
✔ functions: apps/main/dist folder uploaded successfully
i functions: updating Node.js 18 (2nd Gen) function helloworld(asia-northeast1)...
メッセージ内容を見る限り、やっている事はおおまかに分けて以下の通り。
- ビルド後の出力ディレクトリ(今回の場合dist)の中身を(おそらく)Firebase Functions(Google Cloud Functions)へアップロード。
- 各種GoogleAPIを有効化。
- (必要なら)他のGoogle Cloud サービスの初期化。
- Firebase Functions(Google Cloud Functions)の構築。
さらに「Firebase Functions(Google Cloud Functions)の構築」過程でエラーが発生しているので、もっと詳しく調べるために、該当Cloud Buildのログからエラーが発生した箇所を確認する。
tep #2 - "build": ---------------------------------------------------
Step #2 - "build": Running "npm install --package-lock-only --quiet"
Step #2 - "build": npm ERR! code E404
Step #2 - "build": npm ERR! 404 Not Found - GET https://registry.npmjs.org/@some-org/some-private-packaget - Not found
Step #2 - "build": npm ERR! 404 '@some-org/some-private-package@*' is not in this registry.
ん!? 前工程で.npmrc
を作成したのにその設定が効いてない。なぜだ?
.npmrc
が効いてないという事は設定が間違っているか、そもそも.npmrc
が読み取られていないのかだが・・・。
あ!
Firebase Functionsはアップロードされたビルド後の出力ディレクトリ(dist)の中身(ファイル群)を元にGoogle Cloud Functionsとして構築されるのか!(だからpackage.jsonも必要)
ってことはdistの中に.npmrcを含めてやらないといけない!?
早速やってみる
yarn deploy
の前処理にあたるpostbuild.js
にdistに.npmrc
をコピーする処理を追加する。
const npmrcFile = glob.sync('.npmrc')
npmrcFile.forEach((file) => {
fs.copyFileSync(file, path.join(OUT_DIR, file))
})
もう一度デプロイ
見事、成功!
i functions: updating Node.js 18 (2nd Gen) function helloworld(asia-northeast1)...
✔ functions[helloworld(asia-northeast1)] Successful update operation.
Function URL (helloworld(asia-northeast1)): https://helloworld-●●●●-an.a.run.app
i functions: cleaning up build files...
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/some-project/overview
Done in 96.01s.
まとめ
分かってしまうと結果はいつもシンプル。
- デフォルト以外のレジストリに登録されている非公開npmパッケージを利用するためには
.npmrc
に以下を設定する。- 該当非公開npmパッケージのレジストリ
- 非公開パッケージを取得するためのアクセストークン
- Firebase Finctionsへデプロイするときは上記
.npmrc
をアップロードするソースに含める。
Discussion