Open3
NestJSでSecret Manager(Google Cloud)を使う
What
Nest.jsでSecret Manager(Google Cloud)の値を扱うためのメモ。
Scope
- Nest.jsでSecret Managerの値を読む際に上手くいった2通りの手法のメモ。
- Google Cloudの認証は
application default credentials
を用いる前提。 - Secret Managerの設定などSecret Manager自体の説明は扱わない。
Related
方法1. Secretを使うProviderをAsynchronous providersとして定義
Secret Mangerは非同期で値を読み出すため、単にInjectするだけではServiceのconstructorでアクセスできない。
そこで、useFactory
内の非同期関数でSecret Mangerからの読み出しを行う。
Asynchronous providers
以下は、仮にCloud Storageをサービスアカウント認証で扱うStorageService
でSecretを使う場合。
secret-manager.service.ts
import { Injectable } from '@nestjs/common';
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class SecretsManagerService {
private readonly client: SecretManagerServiceClient;
private readonly projectId: string;
constructor(private configService: ConfigService) {
this.client = new SecretManagerServiceClient();
this.projectId = this.configService.get('GCLOUD_PROJECT_ID');
}
async readSecret(secretName: string): Promise<string> {
const [secretVersion] = await this.client.accessSecretVersion({
name: `projects/${this.projectId}/secrets/${secretName}/versions/latest`,
});
console.log(`Reading secret: ${secretName}`);
return secretVersion.payload.data.toString();
}
}
storage.service.ts
import { Injectable } from '@nestjs/common';
import { Bucket, GetSignedUrlConfig, Storage } from '@google-cloud/storage';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class StorageService {
private storage: Storage;
private bucket: Bucket;
constructor(
private readonly configService: ConfigService,
private readonly secretManagerService: SecretsManagerService,
) {}
async init() {
if (!this.storage) {
const serviceAccount = JSON.parse(
await this.secretManagerService.readSecret('GCLOUD_GCS_PRIVATE_KEY'),
);
this.storage = new Storage({
projectId: this.configService.get('GCLOUD_PROJECT_ID'),
credentials: {
client_email: serviceAccount.client_email,
private_key: serviceAccount.private_key,
},
});
this.bucket = this.storage.bucket(
this.configService.get('GCLOUD_STORAGE_BUCKET_NAME'),
);
}
}
}
app.module.ts
providers: [
{
provide: StorageService,
useFactory: async (
configService: ConfigService,
secretManagerService: SecretsManagerService,
) => {
const storageService = new StorageService(
configService,
secretManagerService,
);
await storageService.init();
return storageService;
},
inject: [ConfigService, SecretsManagerService],
}
]
メリット
- 明示的にInjectするので、どこでSecretを使っているかが追いやすい。
- Secretを必要なスコープできめ細やかに使える。
デメリット
- confgServiceとは別に管理しなければならない。
方法2. ConfigModule.forRootのload内で読み込み
ConfigServiceにInjectして、this.configService.get("<SecretName>")
で使う場合は、ConfigModule.forRoot
のload
オプションが使える。ここに非同期のFactory関数を与える。
configuration.ts
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
export default async () => {
const secretManager = new SecretManagerServiceClient();
const projectId = process.env.GCLOUD_PROJECT_ID;
const [secretVersion] = await secretManager.accessSecretVersion({
name: `projects/${projectId}/secrets/GCLOUD_GCS_PRIVATE_KEY/versions/latest`,
});
return {
GCLOUD_GCS_PRIVATE_KEY: secretVersion.payload.data.toString(),
};
};
app.module.ts
...
import configuration from './configuration';
@Module({
imports: [
...
ConfigModule.forRoot({
load: [configuration],
}),
...
],
@nestjs/config
のソースを除いてみると、load内の関数をuseFactory
のfactoryとするproviderを作成し、ConfigServiceにInjectしている様子。(大雑把な理解)
Injectした中身は、internalConfig
として、ConfigServiceで参照される。
メリット
- Secretとその他の環境変数を一括管理できる。
デメリット
- Secretのスコープが広くなってしまう。(グローバル)
参考