@3billion_dev/secrets-manager-module
v1.2.0
Published
A NestJS module for using AWS Secrets Manager
Readme
@3billion_dev/secrets-manager-module
AWS Secrets Manager 값을 가져오고 검증하여 애플리케이션에서 쉽게 사용할 수 있게 해 주는 가벼운 NestJS 모듈입니다.
기능:
forRoot/forRootAsync모듈 등록 방식 지원forFeature({ name, ... })다중 클라이언트 구성 지원(각기 다른 Secret/Region 동시 사용)- AWS SDK
SecretsManagerClient를 DI로 주입(테스트 시 클라이언트 모킹 용이) - 커스텀 Zod 스키마 검증(검증 실패 시 항상 예외 발생, 원인 포함)
- 메모리 스냅샷 보관 및 타입 안전한
get/getAll, dot-path 접근(lodash.get),getOrThrow지원 - 모듈 시작 시 자동 초기화(
onModuleInit)
설치
pnpm add @3billion_dev/secrets-manager-module @aws-sdk/client-secrets-manager zod lodash필수 피어 의존성:
@nestjs/common(v11)zod(v3)
사내 프라이빗 GitHub Packages 레지스트리를 사용한다면, 프로젝트의 .npmrc에 유효한 토큰이 설정되어 있어야 합니다.
빠른 시작
import { Module } from "@nestjs/common";
import {
SecretsManagerModule,
getServiceToken,
} from "@3billion_dev/secrets-manager-module";
import { z } from "zod";
// Zod 스키마 예시
const schema = z.object({
DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),
});
@Module({
imports: [
SecretsManagerModule.forRoot({
isGlobal: true, // 선택 사항: 전역 모듈로 등록
secretName: "prod/app/secrets",
clientConfig: { region: "ap-northeast-2" }, // 선택 사항; 생략 시 런타임 자격 증명 사용
schema,
}),
],
})
export class AppModule {}DI를 통해 시크릿 사용하기(기본 인스턴스):
import { Inject, Injectable } from "@nestjs/common";
import {
SecretsManagerService,
InjectService,
} from "@3billion_dev/secrets-manager-module";
type SecretsShape = { DATABASE_URL: string; http: { timeout: number } };
@Injectable()
export class AnyService {
constructor(
@InjectService()
private readonly secrets: SecretsManagerService<SecretsShape>
) {}
async use() {
const url = await this.secrets.get("DATABASE_URL");
const timeout = await this.secrets.get<number>("http.timeout", 5000);
const required = await this.secrets.getOrThrow("DATABASE_URL");
}
}여러 클라이언트 구성(forFeature)
서로 다른 Secret/Region을 동시에 사용해야 할 때는 forFeature를 사용합니다.
import { Module, Inject, Injectable } from "@nestjs/common";
import {
SecretsManagerModule,
SecretsManagerService,
getServiceToken,
} from "@3billion_dev/secrets-manager-module";
@Module({
imports: [
SecretsManagerModule.forFeature({
name: "payments",
secretName: "payments/secrets",
clientConfig: { region: "us-east-1" },
}),
SecretsManagerModule.forFeature({
name: "analytics",
secretName: "analytics/secrets",
clientConfig: { region: "eu-central-1" },
}),
],
})
export class FeatureModule {}
@Injectable()
export class UsesMultiple {
constructor(
@InjectService("payments")
private readonly paymentsSecrets: SecretsManagerService<any>,
@InjectService("analytics")
private readonly analyticsSecrets: SecretsManagerService<any>
) {}
}비동기 설정
import { ConfigModule, ConfigService } from "@nestjs/config";
SecretsManagerModule.forRootAsync({
isGlobal: true, // 선택 사항: 전역 모듈로 등록 (비동기 등록 시 이 레벨에서 설정)
imports: [ConfigModule],
useFactory: async (cfg: ConfigService) => ({
secretName: cfg.get<string>("SECRET_NAME")!,
clientConfig: { region: cfg.get<string>("AWS_REGION") },
schema, // 선택 사항이지만 권장
}),
inject: [ConfigService],
});DI 토큰
여러 인스턴스/명명된 인스턴스를 지원하기 위해 다음 토큰 헬퍼를 제공합니다.
getOptionsToken(name?)getClientToken(name?)– AWSSecretsManagerClientgetServiceToken(name?)–SecretsManagerService
기본 인스턴스는 name을 생략하고, 특징별 인스턴스는 name을 지정합니다.
DI 데코레이터
InjectService(name?: string)InjectClient(name?: string)InjectOptions(name?: string)
API
모듈 옵션 (forRoot/forRootAsync):
isGlobal?: boolean– 모듈을 전역으로 등록할지 여부 (기본값:false)secretName: string(필수) – AWS Secrets Manager Secret IDclientConfig?: SecretsManagerClientConfig–SecretsManagerClient에 전달되는 옵션schema?: z.ZodSchema<unknown>– Zod 스키마(유효하지 않으면 예외 발생)
스키마 타입: z.ZodSchema<YourSecretsType>
forFeature 옵션:
name: string(필수)- 나머지 필드 동일(
secretName,clientConfig,schema)
참고:
forFeature는 특정 명명 인스턴스를 추가로 등록하기 위한 서브 모듈입니다.isGlobal은 지원하지 않으며 필요하지도 않습니다. 전역 등록이 필요한 경우forRoot또는forRootAsync에서isGlobal: true를 설정하세요.- 현재
forFeatureAsync는 제공되지 않습니다. 비동기 구성이 필요한 경우forRootAsync를 사용하고, 필요 시 추가 인스턴스는 동기forFeature로 등록하세요. forRootAsync의isGlobal은 최상위 옵션(팩토리 반환값이 아닌forRootAsync({...})의 옵션)에 설정해야 적용됩니다.- service 내의 secret 값들은
get/getAll/getOrThrow매서드 호출 시에 값이 없는 경우에만 secrets manager에서 값을 가져옵니다.
서비스 메서드:
initialize(): Promise<void>– 시크릿을 가져와 검증 (모듈 초기화 시 자동 실행)getAll(): TSecrets– 검증된 전체 맵 반환get<TKey extends keyof TSecrets>(key: TKey): TSecrets[TKey]– 최상위 키 접근get<TValue = unknown>(path: string, defaultValue?: TValue): TValue– dot-path로 중첩 값 접근(lodash.get사용)getOrThrow<TKey extends keyof TSecrets>(key: TKey): TSecrets[TKey]– 필수 최상위 키, 없으면 예외getOrThrow<TValue = unknown>(path: string): TValue– 필수 dot-path, 없으면 예외
타이핑 팁:
- 모듈 등록은 제네릭을 요구하지 않습니다. 대신 DI 시점에
SecretsManagerService<YourSecretsType>으로 타입을 지정해 안전하게 사용하세요.
참고:
- 검증 실패 시 항상 예외가 발생합니다.
- 최초 로드 후 시크릿은 메모리에 보관됩니다. 로테이션이 필요하면 서비스 확장으로 갱신 로직을 추가하세요.
- AWS 자격 증명은
clientConfig.credentials를 명시하지 않으면 AWS SDK v3 기본 프로바이더 체인을 따릅니다. - 중첩 경로 접근은
lodash.get으로 처리합니다.
테스트
클라이언트를 DI로 주입하므로, 테스트에서 네트워크 호출 없이 간단히 모킹할 수 있습니다.
import { Test } from "@nestjs/testing";
import {
getOptionsToken,
getClientToken,
SecretsManagerModuleOptions,
SecretsManagerService,
} from "@3billion_dev/secrets-manager-module";
type SecretsShape = { TOKEN: string; nested: { url: string } };
const secrets: SecretsShape = {
TOKEN: "abc",
nested: { url: "postgres://localhost" },
};
const mockClient = {
send: jest.fn().mockResolvedValue({
SecretString: JSON.stringify(secrets),
}),
};
const options: SecretsManagerModuleOptions = { secretName: "dummy" };
const moduleRef = await Test.createTestingModule({
providers: [
{ provide: getOptionsToken(), useValue: options },
{ provide: getClientToken(), useValue: mockClient },
SecretsManagerService<SecretsShape>,
],
}).compile();
await moduleRef.init();
const svc = moduleRef.get(SecretsManagerService<SecretsShape>);
expect(svc.get("TOKEN")).toBe("abc");
expect(svc.get<string>("nested.url")).toBe("postgres://localhost");라이선스
MIT
