Guide to Using AWS SSM Parameter Store

Introduction

AWS Systems Manager (SSM) Parameter Store is a centralized, secure storage service for managing configuration data and secrets.

Instead of hard-coding parameters such as application IDs, database connection strings, or API keys directly in your source code, you can store them here to manage them separately and securely.

Key features

  • Hierarchical management: Allows you to organize parameters in a folder-like structure (for example: /prod/db/password or /dev/web/port), making permission control and retrieval more structured.
  • Maximum security: Supports encryption of sensitive data using AWS Key Management Service (KMS). Only users or services with appropriate IAM permissions can decrypt and view values.
  • Version control: Every time you change a parameter’s value, Parameter Store automatically creates a new version. This lets you review history or roll back to previous values if needed.
  • Ecosystem integration: Easily integrates with other AWS services such as Lambda, EC2, ECS, and CloudFormation to automate deployment workflows.


Why Use Parameter Store?

It helps you implement the “Separation of Concerns” principle - keeping your source code clean and flexible while reducing the risk of leaking sensitive information when pushing code to platforms like GitHub.


How to Use

First, create AWS CDK code in lib/ssm-stack.ts:

import * as cdk from "aws-cdk-lib"
import { Construct } from "constructs"
import * as ssm from "aws-cdk-lib/aws-ssm"

export class SsmStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    new ssm.StringParameter(this, "ApiUrlParam", {
      parameterName: "/my-app/api-url",
      stringValue: "https://api.example.com",
      description: "The URL of the external API",
    })

    const paramWithPolicy = new ssm.StringParameter(this, "ApiKeyParamWithPolicy", {
      parameterName: "/my-app/api-key",
      stringValue: "super-secret-key-123",
      tier: ssm.ParameterTier.ADVANCED,
    })

    const cfnParam = paramWithPolicy.node.defaultChild as ssm.CfnParameter
    cfnParam.policies = JSON.stringify([
      {
        Type: "Expiration",
        Version: "1.0",
        Attributes: {
          Timestamp: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
        },
      },
    ])
  }
}

Explanation:

  • parameterName is the name used to access the parameter, while stringValue is its value.
  • tier has two common types: STANDARD (default) and ADVANCED.
  • Using the ADVANCED tier supports additional features such as setting expiration time.
  • In this example, the expiration is set to 15 minutes after deploying the stack; once expired, the parameter will be deleted.


Update bin/aws-cdk.ts:

#!/usr/bin/env node
import * as cdk from "aws-cdk-lib/core"
import { SsmStack } from "../lib/ssm-stack"

const app = new cdk.App()
new SsmStack(app, "SsmStack")


After deploying the stack, the parameters will be created accordingly.


Using These Parameters in NestJS

Note: Every call to AWS to retrieve stored values is counted and converted into cost.

  • Standard tier: free throughput
  • Advanced tier: $0.05 / param / month, $0.05 / 10,000 requests

So the idea here is to fetch all parameters once at startup and load them into ConfigModule, then refresh them every 5 minutes. You can adjust this strategy depending on your needs.

First, create ssm-config.factory.ts:

import {SSMClient, GetParametersByPathCommand} from '@aws-sdk/client-ssm'

export const ssmConfigFactory = async () => {
  const client = new SSMClient()
  const config = {}

  try {
    const command = new GetParametersByPathCommand({
      Path: '/my-app/',
      Recursive: true,
      WithDecryption: true,
    })
    const response = await client.send(command)
    response.Parameters?.forEach(param => {
      if (param?.Name) {
        config[param?.Name] = param.Value
      }
    })

    return config
  } catch (error) {
    console.error('Could not load from SSM:', error)
    return {}
  }
}

  • The Path field is used to retrieve all parameters starting with that path.
  • As you can see here, I’m not passing any parameters into the SSMClient. Because of this, the mechanism will automatically retrieve credentials and configuration in the following order:
    • Environment Variables: The SDK looks for variables such as AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION. This is the standard approach when running applications in Docker or on a local machine.
    • Shared Config/Credentials Files: If you have previously run the aws configure command on your computer, the SDK will retrieve information from the ~/.aws/credentials file.


Next, create dynamic-config.service.ts:

import {Injectable, OnModuleInit} from '@nestjs/common'
import {ConfigService} from '@nestjs/config'
import {Cron, CronExpression} from '@nestjs/schedule'
import {ssmConfigFactory} from 'src/factory/ssm-config.factory'

@Injectable()
export class DynamicConfigService implements OnModuleInit {
  private internalCache: Record<string, any> = {}

  constructor(private configService: ConfigService) {}

  async onModuleInit() {
    await this.refreshConfig()
  }

  @Cron(CronExpression.EVERY_5_MINUTES)
  async refreshConfig() {
    const newConfig = await ssmConfigFactory()
    this.internalCache = newConfig
  }

  get(key: string): string {
    return this.internalCache[key] || this.configService.get(key)
  }
}

  • @Cron(CronExpression.EVERY_5_MINUTES) automatically refreshes configuration every 5 minutes.
  • The get function prioritizes returning the latest cached value; if unavailable, it falls back to ConfigService (initial values).


Create a controller to fetch values in ssm.controller.ts:

import {Controller, Get} from '@nestjs/common'
import {DynamicConfigService} from 'src/service/dynamic-config.service'

@Controller('ssm')
export class SsmController {
  constructor(private readonly dynamicConfig: DynamicConfigService) {}

  @Get('config')
  getConfig() {
    const apiKey = this.dynamicConfig.get('/my-app/api-key')
    const apiUrl = this.dynamicConfig.get('/my-app/api-url')
    return {apiKey, apiUrl}
  }
}

Since DynamicConfigService is configured, values will always reflect the latest updates every 5 minutes.


Add services and controllers to app.module.ts:

import {Module} from '@nestjs/common'
import {ConfigModule} from '@nestjs/config'
import {ScheduleModule} from '@nestjs/schedule'
import {AppController} from './app.controller'
import {AppService} from './app.service'
import {SsmController} from './controller/ssm.controller'
import {ssmConfigFactory} from './factory/ssm-config.factory'
import {DynamicConfigService} from './service/dynamic-config.service'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [ssmConfigFactory],
    }),
    ScheduleModule.forRoot(),
  ],
  controllers: [
    AppController,
    SsmController,
  ],
  providers: [
    AppService,
    DynamicConfigService,
  ],
})
export class AppModule {}


Run the application and you should see the expected results.


Happy coding!

See more articles here.

Comments

Popular posts from this blog

All practice series

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Kubernetes Deployment for Zero Downtime

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

NodeJS Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine