728x90
- 2023년 8월 인턴 업무를 진행하며 프로젝트 키 관리 방식을 바꾸면서
   개발팀 내부에 공유했었던 문서를 각색하여 작성하였습니다.
- Nest js 프로젝트에서 키 파일 관리를 중앙에서 하기 위해 도입한 방식으로
  본 포스팅은 NestJS + AWS 환경에서 배포를 진행하는 프로젝트에 최적화되어 있습니다.

 

 


🧍🏻Intro

  • 도입 배경
    1. Redis를 새롭게 추가하던 중 파이프라인에 변수 설정 누락, Bitbuckets Repository Variables 변수설정 누락 시 빌드는 성공하지만 정작 .env 파일 내에 해당 시크릿 정보가 기입되지 않는 문제 발생
    2. 공동 작업 중 DATABASE_SYNCHRONIZE = true 로 설정하는 실수로 인해 DB의 모든 데이터 유실
    3. 키 파일 변경 시 일일이 모든 개발자에게 키 파일을 보내야하는 번거로움
    4. 등등등… 기존의 👤개발자 한명 → 👥 개발자 여러명으로 늘면서 공동작업시의 별도 환경 변수 관리가 없었음

 

  • 사용 이점 및 특징
    • 연관 AWS 리소스
      • AWS Secrets Manager
      • AWS CLI 접근 & 애플리케이션 키파일 접근용 IAM 액세스키

 

  • Secrets Key의 버전관리 가능 → Cloud Trail, Cloud Watch 연동 시에 (추후 업데이트해야할 사항)
  • 모든 개발자가 동일한 환경변수 설정 공유 가능
  • 런타임 시에 환경 변수 설정
  • 별도로 .dev.env 파일 내부에 모든 설정 정보를 넣지 않아도됨
    • But, AWS Secrets Manager 설정 정보는 추가해야함
  • 파이프라인 구성시에 Make .env file 스테이지 간소화 가능
  • pipelines: default: - step: name: Make .env file

 

 

Before

DATABASE_HOST=
...(약 30개의 key 정보들)...
REDIS_DB=

 

After

SECRETS_REGION=
SECRETS_ACCESS_KEY_ID=
SECRETS_ACCESS_KEY=
SECRETS_ARN=

 

 


📜 Guide

  • 로컬에서 Secrets Manger 값을 쉽게 확인할 수 있는 AWS-CLI 사용 방법에 대해 안내합니다.
  • AWS GUI(Web 환경)에서도 동일한 내용 확인 가능합니다.

 

Step 1. 로컬에서 aws-cli 접근이 가능하도록 설정

AWS CLI 설치, 업데이트 및 제거 - AWS Command Line Interface

  • AWS CLI 설정이 가능하도록 현재 개발중인 OS에 aws-cli 를 설치해주세요.

 

Step 2-1. [aws-cli 접속을 위한 profile 설정하기]

profile 직접 명시

# .aws 디렉토리가 없으면 생성
>> cd ~/.aws

# credentials에 프로필 액세스 키 설정
>> cat > credentials
$ [team-secrets-manage]
$ aws_access_key_id={AWS ACCESS KEY ID}
$ aws_secret_access_key={AWS SECRET ACCESS KEY}
^C

# 메타 정보 설정
>> cat > config
$ [team-secrets-manage]
$ region=ap-northeast-2
$ output=json
^C

 

 

Step 2-2. [aws-cli 접속을 위한 profile 설정하기]

aws configure default profile 설정

>> aws configure
$ AWS Access Key ID [****************HZ3P]: {AWS ACCESS KEY ID}
$ AWS Secret Access Key [****************R9qH]: {AWS SECRET ACCESS KEY}
$ Default region name [ap-northeast-2]: ap-northeast-2
$ Default output format [json]: json

 

 

Step 3. AWS-CLI 에서 Key-Value 확인하기

>> aws secretsmanager get-secret-value --secret-id {secrets-id}
  • secrets-id 선택 옵션
    • mars-dev
    • mars-prod
  • e.g.
>> aws secretsmanager get-secret-value --secret-id mars-dev > .mars-dev.env
>> cat .mars-dev.env
$ {
$     "ARN": "arn:aws:secretsmanager:ap-northeast-2:...",
$     "Name": "mars-dev",
$     "VersionId": "cd9bb8ca-3747-4486-9958-afa8e28c8d94",
$     "SecretString": ...,
$     "VersionStages": [
$         "AWSCURRENT"
$     ],
$     "CreatedDate": "2023-08-30T17:05:40.322000+09:00"
$ }

 


🏃🏻 Try

Try 1

Nest 애플리케이션의 ConfigModule 로 AWS Secrets Manger를 로드해오고 이를 맨 처음 애플리케이션 모듈 로드시 설정 정보를 넣어주도록 할 예정이었다.

  • But, 서비스 로직에서 설정정보 사용시, Config 객체 자체를 넘겨주고 있음
/** src/videos/video.service.ts */

@Injectable()
export class VideoService implements OnModuleInit {
    constructor(

        ...

        @Inject(aiConfig.KEY)
        private readonly aiConfigSet: ConfigType<typeof aiConfig>,
        @Inject(s3Config.KEY)
        private readonly s3ConfigSet: ConfigType<typeof s3Config>,
        @Inject(esConfig.KEY)
        private readonly esConfigSet: ConfigType<typeof esConfig>,
        @Inject(videoConfig.KEY)
        private readonly videoConfigSet: ConfigType<typeof videoConfig>,

            ...

      ) {

            ...

 

→ 일종의 DTO 개념처럼 설정 정보를 직접적으로 서비스 로직에서 기입해주기보다 기존 로직 처럼 맨처음 app.module.ts 에서 넣어주는게 더 직관적이고 객체 단위로 주고 받기에 편하다고 판단함

 

 

Try 2

그렇다면 Secrets Manager 또한 ConfigService로 등록을 해서 각각의 Config 설정에 등록을 해주자!

[MARS] Info     2023.08.30 15:59:41:5941 [NestFactory] Starting Nest application... - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] TypeOrmModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] DiscoveryModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] ConfigHostModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] SseModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] AppModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] ScheduleModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] ConfigModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] ConfigModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] JwtModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] CacheModule dependencies initialized - {}
[MARS] Info     2023.08.30 15:59:41:5941 [InstanceLoader] AuthModule dependencies initialized - {}

...
  • 위와 같이 애플리케이션 구동 시 찍히는 로그를 확인해보면 app.module.ts 에 명시한 모듈들이 순서에 상관없이 비동기적으로 로드되는 걸 볼 수 있다.
  • ∴ Secrets Manager를 로드해오는데에 시간이 걸림 (비동기)
    → 동시에 TypeOrmModule 로드 시작
    → Secrets Manager를 아직 로드해오지 못했으므로 DB 설정 정보가 주입이 안된 상태
    → 에러 발생

 


🎉 Solve

위와 같은 이유들로, Nest 애플리케이션 인스턴스가 생성되기 전에 process.env 에 직접 설정 정보를 주입해주는 방식을 채택했다.

⭐ 로컬에서 간단하게 설정 정보를 바꾸면서 테스트하고 싶은데, 중간에 Secrets Manager가 끼어있어서 불편하셨다고요?
아래와 같이 loadSecrets();를 주석처리해주시면 Secrets Manager 없이도 애플리케이션 개발이 가능합니다.

/** src/main.ts */

async function bootstrap() {
  **await loadSecrets();
  // 해당 코드 주석 처리 후, 동일하게 .env 파일 구성 후 애플리케이션 구동하면 됩니다!
  // 또는 새로 추가하는 변수의 경우 .env 에 해당하는 변수 값만 넣어주면 됩니다.**

  const app = await NestFactory.create(AppModule, {
    logger: piaLogger,
  });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  app.enableCors();
  setUpSwagger(app);

  await app.listen(3000);
}
  • NestFactory.create로 NestApplication 인스턴스 생성 이전에 SecretsManager의 값을 불러오는 로직을 추가한다.

 

  • 📄 src/utils/loadSecrets.ts
import { GetSecretValueCommand, GetSecretValueResponse, SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import { Logger, HttpException, HttpStatus } from '@nestjs/common';

interface SecretsStringList {
  key: string;
  value: string;
}

const logger = new Logger('Load Secrets Variable');

export const loadSecrets = async () => {

    // secretsManager 객체를 가져올 수 있도록 객체 생성
  const secretsManager = new SecretsManagerClient({
    region: `${process.env.SECRETS_REGION}`,
    credentials: {
      accessKeyId: `${process.env.SECRETS_ACCESS_KEY_ID}`,
      secretAccessKey: `${process.env.SECRETS_ACCESS_KEY}`,
    },
  });

  const command = new GetSecretValueCommand({
    SecretId: `${process.env.SECRETS_ARN}`,
  });

  try {
    const secretResult: GetSecretValueResponse = await secretsManager.send(command);
    const secretList: SecretsStringList = JSON.parse(secretResult.SecretString);

        // Secrets Manager SecretString
        // key-value 값을 읽어와서 'process.env'에 넣어주는 작업 수행
    for (const [key, value] of Object.entries(secretList)) {
      process.env[key] = value;
    }
  } catch (e) {
    logger.error(e.message);

    throw new HttpException(
      'AWS Secrets Manager 환경변수를 읽어오는데 문제가 발생했습니다.',
      HttpStatus.INTERNAL_SERVER_ERROR,
    );
  }
};
  • ‘aws sdk’ 사용 시, 구버전의 호환성 문제로 인해 ‘@aws-sdk/client-secrets-manager’ 를 이용하여 Secrets Manager 로드 환경을 구성하였습니다.

 

 


🗓️ TBD

  • Cloud Trail, Cloud Watch 연동을 통한 환경 변수 설정 버전 관리
    • 이전 버전의 환경 변수 혹은 변경자 확인이 필요할 경우 사용하면 좋을 것 같다!

 

 


🔗 Reference

728x90