Open3

NestJSでSecret Manager(Google Cloud)を使う

hosaka313hosaka313

What

Nest.jsでSecret Manager(Google Cloud)の値を扱うためのメモ。

Scope

  • Nest.jsでSecret Managerの値を読む際に上手くいった2通りの手法のメモ。
  • Google Cloudの認証はapplication default credentialsを用いる前提。
  • Secret Managerの設定などSecret Manager自体の説明は扱わない。

Related

https://zenn.dev/hosaka313/scraps/657d0a21269bc8

hosaka313hosaka313

方法1. Secretを使うProviderをAsynchronous providersとして定義

Secret Mangerは非同期で値を読み出すため、単にInjectするだけではServiceのconstructorでアクセスできない。

そこで、useFactory内の非同期関数でSecret Mangerからの読み出しを行う。

Asynchronous providers

https://docs.nestjs.com/fundamentals/async-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とは別に管理しなければならない。
hosaka313hosaka313

方法2. ConfigModule.forRootのload内で読み込み

ConfigServiceにInjectして、this.configService.get("<SecretName>")で使う場合は、ConfigModule.forRootloadオプションが使える。ここに非同期の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している様子。(大雑把な理解)

https://github.com/nestjs/config/blob/8fe704ec32ff945b9b8a2971d9ee26b05e5272c9/lib/config.module.ts#L98-L108

Injectした中身は、internalConfigとして、ConfigServiceで参照される。
https://github.com/nestjs/config/blob/8fe704ec32ff945b9b8a2971d9ee26b05e5272c9/lib/config.service.ts#L62-L66

メリット

  • Secretとその他の環境変数を一括管理できる。

デメリット

  • Secretのスコープが広くなってしまう。(グローバル)

参考

https://docs.nestjs.com/techniques/configuration#custom-configuration-files

https://medium.com/simform-engineering/securing-secrets-in-nest-js-a-guide-to-using-aws-secret-manager-for-application-credentials-882952359a54

https://zenn.dev/dove/articles/2990f0e1eba07e#独自のconfigmoduleを作る