はじめに
こちらの記事では以下のことについて説明しています。
- Cloud Run functionsからSecret Managerを使うための環境構築
- ローカル環境からSecret Managerのデータにアクセスする方法
.
├── dist
├── node_modules
├── my-secret.json # シークレット用のjson
├── main.tf # terraformファイル
├── jest.config.js
├── package-lock.json
├── package.json
├── package.prod.json # デプロイ時に使う簡素なpackage.json。前記事参考
├── src
│ ├── hoge
│ └── index.ts
├── test
└── tsconfig.json
前準備
以下のコマンドを実行して、Googleのライブラリが自分のプロジェクトにアクセスできるよう認証を済ませておきましょう。
gcloud auth application-default login
Node.js用の Secret Manager ライブラリをインスコする
Cloud Run functionsからSecret Managerを使えるようにするために、Node.js用のSecret Managerクライアントライブラリをインスコします。
以下から探します。
npm install --save-prod @google-cloud/secret-manager
ついでに本番用の package.prod.json
も更新します。ここの手順が抜けて事故にならないように自動化したいですね。
{
"name": "advent-calendar-2024",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/functions-framework": "^3.4.2",
"@google-cloud/secret-manager": "^5.6.0" // 追加
}
}
Secret Manager呼び出しコードを書く
シークレットバージョンにアクセスする関数を書きます。一部ハードコーディングになっていますがお許しを。
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();
const accessSecretVersion = async (secretName: string): Promise<string | null> => {
const [version] = await client.accessSecretVersion({
name: `projects/プロジェクト番号/secrets/${secretName}/versions/latest`,
});
if (version && version.payload && version.payload.data) {
return version.payload.data.toString();
}
return null;
}
そしてSecretのバージョンを取得してその内容をレスポンスに表示します(御法度ですが今回はサンプルなので...)
export const helloGET: HttpFunction = (req: ff.Request, res: ff.Response) => {
const mySecret = accessSecretVersion("my-secret");
res.send(`Hello, World! Secret is ${mySecret}.`);
};
全体像はこうなります。
import * as ff from '@google-cloud/functions-framework'
import type { HttpFunction } from "@google-cloud/functions-framework";
/**
* Secret Managerクライアントの処理追加
*/
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();
const accessSecretVersion = async (secretName: string): Promise<string | null> => {
const [version] = await client.accessSecretVersion({
name: `projects/advent-calendar-2024-w/secrets/${secretName}/versions/latest`,
});
if (version && version.payload && version.payload.data) {
return version.payload.data.toString();
}
return null;
}
// エントリーポイント関数
export const helloGET: HttpFunction = async (req: ff.Request, res: ff.Response) => {
const mySecret
= await accessSecretVersion("my-secret"); // 非同期通信なことを忘れずに
res.send(`Hello, World! Secret is ${JSON.stringify(mySecret)}.`);
};
そしてこれを npm run build
して terraform plan
→ terraform apply
してみましょう。
╷
│ Error: Error waiting for Updating function: Error code 3, message: Could not create or update Cloud Run service 関数名, Container Healthcheck failed. Revision '関数名-00003-nor' is not ready and cannot serve traffic. The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable within the allocated timeout. This can happen when the container port is misconfigured or if the timeout is too short. The health check timeout can be extended. Logs for this revision might contain more information.
│
│ Logs URL: https://console.cloud.google.com/logs/viewer?project=プロジェクト名resource=cloud_run_revision/service_name/sample-crf/revision_name/関数名-00003-nor&advancedFilter=resource.type%3D%22cloud_run_revision%22%0Aresource.labels.service_name%3D%22sample-crf%22%0Aresource.labels.revision_name%3D%22関数名-00003-nor%22
│ For more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-start
│
│ with google_cloudfunctions2_function.default,
│ on main.tf line 40, in resource "google_cloudfunctions2_function" "default":
│ 40: resource "google_cloudfunctions2_function" "default" {
│
╵
あれ?失敗した?
ログを見ると以下のようなエラーが出ています。
Provided module can't be loaded.
Is there a syntax error in your code?
Detailed stack trace: Error: Dynamic require of "process" is not supported
at file:///workspace/index.js:1:382
at file:///workspace/index.js:1:76808
at file:///workspace/index.js:1:494
at file:///workspace/index.js:1:78508
at file:///workspace/index.js:1:494
at file:///workspace/index.js:1:80850
at file:///workspace/index.js:1:494
at file:///workspace/index.js:21:90034
at file:///workspace/index.js:1:494
at file:///workspace/index.js:64:670591
Could not load the function, shutting down.
なんか構文エラーが発生していますね。
これ、実はNode.js用の Secret Manager クライアントライブラリは commonjs 形式で書かれており、esbuild がESMでは使えない commonjs 特有の機能を特に変換せずそのままビルド結果に含めてしまったが故にこのようなエラーが発生しております。 なのでそのままビルドしただけでは ESM形式で動く形になりません。
(ちなみにParcelでビルドしたら問題なく動きます。なんやねん)
ということで既存のビルドスクリプトを改良した、esbuild の実行ファイル build.ts
を作りましょう。banner
のプロパティを追加し、ESM形式で動く形に変換する処理を書きます。おまじないなのでそのままコピーして使用してください。
// package.json の "build" スクリプト
// esbuild ./src/index.ts \
// --bundle \
// --minify \
// --sourcemap \
// --platform=node \
// --format=esm \
// --outfile=./dist/index.js"
// に banner のプロパティを追加して切り出したもの。
import esbuild from 'esbuild';
await esbuild.build({
logLevel: 'info',
entryPoints: ['./src/index.ts'],
outdir: './dist',
minify: true,
bundle: true,
sourcemap: true,
platform: 'node',
format: 'esm',
banner: { // ここで commonjs 特有の機能をESMで動くように変換する
js: 'import { createRequire as topLevelCreateRequire } from "module"; import url from "url"; const require = topLevelCreateRequire(import.meta.url); const __filename = url.fileURLToPath(import.meta.url); const __dirname = url.fileURLToPath(new URL(".", import.meta.url));',
},
})
参考にした神記事
そして package.json
のビルドコマンドも変えておきましょう。
{
"scripts": {
"build": "tsx build.ts",
}
npm run build
を実行して terraform plan
→ terraform apply
をしましょう。
Apply complete! Resources: 1 added, 1 changed, 1 destroyed.
Outputs:
function_uri = "https://xxx.a.run.app"
表示されたURLを curl で呼んでみます。
$ curl https://xxx.a.run.app
Hello, World! Secret is "{\n \"hoge\": \"hoge\"\n}".
無事呼び出されましたね。
ちなみにローカル環境でも同様にSecret Managerの呼び出しが可能です。gcloud auth application-default login
の実行を忘れずにね。
$ npm run start
> advent-calendar-2024@1.0.0 start
> tsx --inspect --watch node_modules/.bin/functions-framework --target=helloGET --source=./src/index.ts
Debugger listening on ws://127.0.0.1:9229/13fad6ee-e1f4-49e2-8dcd-a11df4aab6d4
For help, see: https://nodejs.org/en/docs/inspector
Serving function...
Function: helloGET
Signature type: http
URL: http://localhost:8080/
---
$ curl http://localhost:8080
Hello, World! Secret is "{\n \"hoge\": \"hoge\"\n}".
おわりに
Secret Manager for Node.js の環境構築とコード、ESM用の動かし方、そしてローカル環境で動かす方法について解説しました。次もセキュリティ系でなんか書きます。
Appendix
便利リンク
シークレットを作成する / アクセスする
Cloud Run functionsでシークレットを構成する方法
Secret Managerクライアントライブラリについて
Secret Manager コードサンプル for Node.js
Secret Manager for Node.js 公式ページ
トラブルシューティング
Error: 7 PERMISSION_DENIED: Permission 'secretmanager.versions.access' denied for resource 'projects/xxx/secrets/シークレット名/versions/latest' (or it may not exist).
原因
プログラム上のGoogle Cloud SDKがプロジェクトにアクセスする権限がないため。
解決策
以下を実行してアプリケーション認証を行う。
gcloud auth application-default login