0Pricing
NestJS Enterprise Backend APIs · 강의

HashiCorp Vault와 AWS SSM에서 비밀 값 불러오기

비동기 구성 팩토리와 사용자 지정 로더를 사용해 외부 저장소에서 실행 중 비밀 값을 가져옵니다

HashiCorp Vault와 AWS SSM에서 비밀 값 불러오기은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 NestJS Enterprise Backend APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Pull Secrets at Runtime?

Hardcoding API keys and DB passwords in .env files works for a single laptop, but it falls apart at enterprise scale.

  • Rotation: security teams rotate credentials frequently; baking them into images means a redeploy on every rotation.
  • Auditing: central stores like HashiCorp Vault and AWS SSM Parameter Store log who read which secret and when.
  • Least privilege: each service authenticates with its own identity and only sees the secrets it needs.

In this lesson we wire NestJS's ConfigModule to fetch secrets asynchronously at boot from these external stores using custom loaders.

The Async Config Factory Pattern

NestJS's ConfigModule normally loads from static files. To reach a network store we need code that runs before the rest of the app, returning a plain object of resolved values.

That code is a custom loader — an async function passed to ConfigModule.forRoot({ load: [...] }). Nest awaits each loader and merges the results into the config namespace.

Key rule: a loader must fully resolve its promise before the module finishes initializing, so every downstream provider sees fully-populated config.

// config/config.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { vaultLoader } from './loaders/vault.loader';
import { ssmLoader } from './loaders/ssm.loader';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      // each loader is an async () => Record<string, unknown>
      load: [vaultLoader, ssmLoader],
    }),
  ],
})
export class AppConfigModule {}

Anatomy of a Custom Loader

A loader is just an async function returning an object. The keys become config paths you read later with configService.get('database.password').

Best practices:

  • Group secrets under a namespace key (database, jwt) so reads are predictable.
  • Convert and validate types here — external stores return everything as strings.
  • Throw on missing critical secrets so the app fails fast at boot, not mid-request.
// loaders/example.loader.ts
export const exampleLoader = async () => {
  const raw = await fetchFromStore(); // returns string map
  if (!raw.DB_PASSWORD) {
    throw new Error('Missing DB_PASSWORD from secret store');
  }
  return {
    database: {
      host: raw.DB_HOST ?? 'localhost',
      port: Number(raw.DB_PORT ?? 5432),
      password: raw.DB_PASSWORD,
    },
  };
};

async function fetchFromStore() {
  // placeholder for the real Vault/SSM call
  return {} as Record<string, string>;
}

How Vault Stores Secrets

HashiCorp Vault exposes secrets over an HTTP API. The common KV v2 engine stores versioned key-value pairs at a path like secret/data/myapp/database.

To read, a client sends a token in the X-Vault-Token header and receives JSON shaped like { data: { data: { ... } } } — note the double data nesting unique to KV v2.

In production the token usually comes from an AppRole login (a role id + secret id exchanged for a short-lived token), not a static root token.

A Vault Loader with AppRole Login

This loader first exchanges an AppRole roleId/secretId for a client token, then reads a KV v2 path. We use the built-in fetch available in Node 18+.

Notice how we unwrap the double data nesting and namespace the result under vault.

// loaders/vault.loader.ts
export const vaultLoader = async () => {
  const addr = process.env.VAULT_ADDR!;

  // 1) AppRole login -> short-lived client token
  const login = await fetch(`${addr}/v1/auth/approle/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      role_id: process.env.VAULT_ROLE_ID,
      secret_id: process.env.VAULT_SECRET_ID,
    }),
  });
  const { auth } = await login.json();

  // 2) Read KV v2 secret using the issued token
  const res = await fetch(`${addr}/v1/secret/data/myapp/database`, {
    headers: { 'X-Vault-Token': auth.client_token },
  });
  const body = await res.json();
  const kv = body.data.data; // KV v2 double nesting

  return {
    database: {
      host: kv.host,
      port: Number(kv.port),
      password: kv.password,
    },
  };
};

How AWS SSM Parameter Store Works

AWS SSM Parameter Store holds parameters by name, e.g. /myapp/prod/db-password. Secrets use type SecureString, encrypted with a KMS key.

  • GetParametersByPath fetches every parameter under a prefix in one call — ideal for loading a whole namespace.
  • Pass WithDecryption: true to receive plaintext values (your IAM role needs kms:Decrypt).
  • Authentication is automatic via the instance/task IAM role — no token to manage.

An SSM Loader by Path

This loader pulls every parameter under /myapp/prod and strips the prefix to build clean keys. We page through results because GetParametersByPath caps each response and returns a NextToken.

// loaders/ssm.loader.ts
import { SSMClient, GetParametersByPathCommand } from '@aws-sdk/client-ssm';

export const ssmLoader = async () => {
  const client = new SSMClient({ region: process.env.AWS_REGION });
  const prefix = '/myapp/prod/';
  const out: Record<string, string> = {};
  let NextToken: string | undefined;

  do {
    const res = await client.send(
      new GetParametersByPathCommand({
        Path: prefix,
        Recursive: true,
        WithDecryption: true,
        NextToken,
      }),
    );
    for (const p of res.Parameters ?? []) {
      const key = p.Name!.replace(prefix, '');
      out[key] = p.Value!;
    }
    NextToken = res.NextToken;
  } while (NextToken);

  return {
    jwt: { secret: out['jwt-secret'] },
    database: { password: out['db-password'] },
  };
};

Merging Loaders & Precedence

When you list multiple loaders in load: [vaultLoader, ssmLoader], Nest deep-merges their returned objects. Loaders later in the array win on key conflicts.

This lets you layer sources: a base loader for defaults, then an override loader for environment-specific secrets. Plan the order intentionally — accidental overrides are a common bug.

The merge is a simple example of how last-write-wins resolves overlapping keys:

function deepMerge(target: any, source: any): any {
  for (const key of Object.keys(source)) {
    if (
      source[key] && typeof source[key] === 'object' &&
      !Array.isArray(source[key])
    ) {
      target[key] = deepMerge(target[key] ?? {}, source[key]);
    } else {
      target[key] = source[key]; // later source wins
    }
  }
  return target;
}

const base = { db: { host: 'localhost', port: 5432 } };
const override = { db: { host: 'prod-db', password: 's3cr3t' } };
console.log(deepMerge(base, override));
// { db: { host: 'prod-db', port: 5432, password: 's3cr3t' } }

Validating Loaded Secrets

External stores fail in surprising ways — a typo'd path returns nothing, a rotated secret may be empty. Validate the merged config with a schema so the app refuses to start when something critical is missing.

NestJS supports a validate function in forRoot. Here we use a lightweight check, but Joi or class-validator are common in production.

// config/validate.ts
export function validateConfig(config: Record<string, any>) {
  const required = [
    ['database', 'password'],
    ['jwt', 'secret'],
  ];
  for (const [ns, key] of required) {
    if (!config[ns]?.[key]) {
      throw new Error(`Missing required secret: ${ns}.${key}`);
    }
  }
  return config; // must return the validated config
}

Consuming Secrets in Providers

Once loaders have run, any provider injects ConfigService and reads namespaced values. Because loading was async at boot, reads are synchronous and cheap — no network call per request.

For modules that themselves need config (like TypeORM), use the forRootAsync + useFactory pattern so they receive resolved secrets.

// database/database.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get<string>('database.host'),
        port: config.get<number>('database.port'),
        password: config.get<string>('database.password'),
        autoLoadEntities: true,
      }),
    }),
  ],
})
export class DatabaseModule {}

Caching, TTL, and Rotation

Loaders run once at boot, so a value rotated in Vault won't reach a long-running pod until restart. Strategies to handle rotation:

  • Lease-aware refresh: Vault dynamic secrets carry a TTL; a background job renews or re-fetches before expiry.
  • Sidecar injection: tools like Vault Agent or AWS Secrets Manager rotation write fresh values to a file/env the app watches.
  • Rolling restart: simplest for static KV secrets — redeploy on rotation.

Avoid fetching secrets on every request — it adds latency and can hit store rate limits. Cache in memory with an explicit, bounded TTL instead.

Quick Check: Loader Behavior

Test your understanding of async config loaders in NestJS.

Recap & Takeaways

You learned how to load runtime secrets from external stores into NestJS:

  • Custom loaders are async functions passed to ConfigModule.forRoot({ load }); they fully resolve before providers initialize.
  • Vault (KV v2) is read over HTTP with an AppRole-issued token; remember the double data nesting.
  • AWS SSM uses GetParametersByPath with WithDecryption and IAM-role auth; page through NextToken.
  • Loaders are deep-merged with last-write-wins, so order matters.
  • Validate merged config to fail fast, and plan a rotation strategy since loaders run only at boot.

With these patterns your services authenticate with their own identity, never ship plaintext secrets in images, and stay auditable at scale.

자주 묻는 질문

“HashiCorp Vault와 AWS SSM에서 비밀 값 불러오기” 강의는 무료인가요?

네 — “HashiCorp Vault와 AWS SSM에서 비밀 값 불러오기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.

“HashiCorp Vault와 AWS SSM에서 비밀 값 불러오기”에서 뭘 배우나요?

비동기 구성 팩토리와 사용자 지정 로더를 사용해 외부 저장소에서 실행 중 비밀 값을 가져옵니다 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.

“HashiCorp Vault와 AWS SSM에서 비밀 값 불러오기” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Joi와 forRoot를 사용한 스키마 검증 환경 변수
  2. registerAs를 사용한 네임스페이스 구성
  3. HashiCorp Vault와 AWS SSM에서 비밀 값 불러오기
  4. 환경별 구성과 안전한 기본값
← NestJS Enterprise Backend APIs(으)로 돌아가기