Search code examples
nestjstypeormaws-ssm

Using AWS SSM Parameter Store for TypeORM config in NestJS


I have a simple NestJS application running on AWS Lambda. I am using the AWS SSM Parameter Store to keep database connection information and credentials. When I import TypeORM I use the parameters already retrieved from the store.

Right now I am just retrieving the params right in my AppModule where TypeORM is imported. I'm sure there is a better way to do this, but I'm not sure what it would be. A custom Provider? Some kind of settings Service? I don't think my current solution is very robust and there isn't great error handling.

The only requirement is that I am retrieving the SSM parameters at runtime and not at build or deploy time. Any advice

This is what I am currently doing:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThingyModule } from 'thingy/thingy.module';
import * as awsParamStore from 'aws-param-store';

const ssmParams = awsParamStore.getParametersByPathSync('/myapp/prod', {region: (process.env['AWS_DEFAULT_REGION'] ? process.env['AWS_DEFAULT_REGION'] : 'us-east-2')}); 
const ssmMap = ssmParams.reduce(function(map, obj) {
  map[obj.Name] = obj.Value;
  return map;
}, {});

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: ssmMap['/myapp/prod/db/host'],
      port: 5432,
      username: ssmMap['/myapp/prod/db/username'],
      password: ssmMap['/myapp/prod/db/password'],
      database: 'postgres',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
    ThingyModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

The parameters have been created previously with the AWS CLI:

// aws ssm put-parameter --type String --name /myapp/prod/db/username --value postgres --region us-east-2
// aws ssm put-parameter --type String --name /myapp/prod/db/password --value supRCkrit --region us-east-2
// aws ssm put-parameter --type String --name /myapp/prod/db/host --value localhost --region us-east-2

Solution

  • We can do this using async providers.

    Sample from a working project:

    Create async provider:

    import * as AWS from 'aws-sdk';
    import { Parameter } from 'aws-sdk/clients/ssm';
    
    export const ssmProvider = {
      provide: 'AWS_SSM',
      useFactory: async (): Promise<Parameter[]> => {
        const ssmClient = new AWS.SSM({
          endpoint: 'endpoint',
          region: 'us-west-2',
        });
        const result = await ssmClient
          .getParametersByPath({
            Path: '/ssm/path',
            Recursive: true,
          })
          .promise();
        return result?.Parameters;
      },
    };
    

    We can then use this provider for injecting values.

    For example injecting AWS SSM Parameters in config service:

    export class ConfigModule {
      static register(options: ConfigModuleOptions): DynamicModule {
        return {
          global: true,
          module: ConfigModule,
          providers: [
            ssmProvider,
            {
              provide: CONFIG_OPTIONS,
              useValue: options,
            },
            ConfigService,
          ],
          exports: [ConfigService],
        };
      }
    }
    

    And in config service constructor:

    constructor(
        @Inject(CONFIG_OPTIONS) options: ConfigOptions,
        @Inject('AWS_SSM') awsParameters: Parameter[],
      ) {}
    

    Edit: NPM Package which provide a service which loads aws param store parameters at the application start up:

    param-store-service