🙌

非公開npmパッケージが使われたFirebase Functionsをデプロイした時の話

2023/09/01に公開

株式会社TERASSで主にバックエンド開発・インフラ構築をしている0okmです。
最近、業務において内部実装に社内用の非公開(private)npmパッケージを使ったFirebase Functionsをデプロイするにはどうしたらよいか調べる機会があったのでその結果を記事化してみたいと思います。

前提

  • Firebase:12.5.2
  • Node.js:18.17.0
  • 非公開npmパッケージはGitHub Packagesとして登録されている。

https://docs.github.com/en/packages/learn-github-packages/introduction-to-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パッケージです。

package.json
{
  "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"
  }
}
tsconfig.json
{
  "compilerOptions": {
    "outDir": "dist",
    ・・・
    ・・・
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}
firebase.json
{
  "functions": {
    "source": "dist",
    "runtime": "nodejs18",
    "codebase": "default",
    "predeploy": ["yarn build"]
  },
}
postbuild.js
#!/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パッケージを使っている実装は以下の通りです。

src/index.ts
import { setGlobalOptions } from 'firebase-functions/v2'

setGlobalOptions({
  region: 'asia-northeast1',
})
export * from './helloworld'
src/helloworld/index.ts
// 非公開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にこのパッケージのレジストリの場所と非公開パッケージにアクセスする方法を教えてあげればいいのではないかと想像がつく。

各種ドキュメントを確認する

https://firebase.google.com/docs/functions/handle-dependencies?hl=ja#using_private_modules
https://docs.github.com/ja/packages/working-with-a-github-packages-registry/working-with-the-npm-registry
.npmrcを使ってレジストリの指定と非公開npmパッケージへアクセスするためのアクセストークンを設定してあげればよいとの事。今回の非公開npmパッケージはGitHub Packagesとして登録されているのでGitHub Packagesのレジストリを使う。

.npmrc
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
@some-org:registry=https://npm.pkg.github.com/

NPM_TOKENが上記アクセストークンに該当する。このトークンは細かな権限管理が可能で一つのトークンに必要以上の権限を与えるような運用は推奨されていない。
GitHub Packagesの場合以下の方法でアクセストークンを発行できます。
https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic
通常の運用であればアクセストークンのような機密情報はこのような設定ファイルに直接書かくことはせず、上記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をコピーする処理を追加する。

postbuild.js
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をアップロードするソースに含める。
Terass Tech Blog

Discussion