Lambda config and secrets (Part 2 of 2)

Effectively using the SSM Parameter Store - How do I use SSM Parameter Store in my code?

Gary Holland
a year ago | 6 min read

Woman putting finger to lips to keep a secret

In part 1 we covered what the SSM Parameter Store is, along with why it's ace and some simple examples of how to pull configuration into your code.

So you've got an array of parameters from SSM Parameter Store. Now what? How do I get them into my code and how do I use them?

With a simple config

If your Parameter Store is so well organised that some algorithm could take the parameter’s full name as an input and be able to output a unique environment variable name, then that’s exactly what you should do: write a function that takes the output of getParametersByPaths, figures out what to call each parameter, and adds them to process.env (or wherever you’re storing them).

const getEnvVariableName = name =>
  name
    .replace(/dev|prod/, '')
    .replace('/', '_')
    .toUpperCase()

const environmentVariables = Parameters.reduce((accumulator, currentValue) => {
  accumulator[getEnvironmentVariableName(currentValue.Name)] =
    currentValue.Value

  return accumulator
}, {})

Object.assign(process.env, environmentVariables)

Here we simply remove the name of the stage and replace slashes with underscores. This might work fine for you, but depending on how deeply nested your paths are, you might end up with some really unwieldy environment variable names, like process.env.common_authProvider_site1_issuer. Gross.

With a more complicated config

Here’s an example of a situation where the above won’t work so well:

  • you’re pulling in parameters from multiple paths
  • the ends of some of their names are the same (you have multiple connectionStrings, for example)
  • they can’t all be differentiated from each other with the same number of parts of their full path.

Our preferred solution is to start with a map of each path to a prefix for all the environment variables we set with that path’s parameters and then use a function as described above to turn the parameter name into a name for the environment variable, but with the supplied prefix at the beginning to ensure uniqueness.

const getEnvVariableName = (path, name, prefix) => {
  const nameWithoutPrefix = name.replace(`${path}/`, ``).replace(`/`, ``)

  const fullLocalName = prefix ? `${prefix}_${nameWithoutPrefix}` : localName

  return fullLocalName.toUpperCase()
}

const Parameters = [
  {
            Version: 1,
            Type: "String",
            Name: "/dev/common/connString",
            Path: "/dev/common/"
            Value: "https://some.string",
            Prefix: "common"
  },
  {
            Version: 1,
            Type: "String",
            Name: "/dev/myApi/auth_token",
            Path: "/dev/myApi/",
            Value: "s0m3t4k3n",
            Prefix: "api"
  }
]

const environmentVariables = Parameters.reduce((accumulator, currentValue) => {
  accumulator[getEnvironmentVariableName(currentValue.path, currentValue.Name, currentValue.prefix)] =
    currentValue.Value

    return accumulator
}, {}))

Object.assign(process.env, environmentVariables)

You'll notice that our Parameters have two new properties: path and prefix. path is the path we used to fetch this particular parameter. Removing that from the parameter's name leaves only what is needed to distinguish it in the context of that path. But, as we discussed above, that might not be enough to distinguish it from parameters fetched with other paths, so we also add our prefix.

The question that remains is how you create the objects containing both the parameter data returned from SSM and your path and prefix. And there are two possible answers:

  1. Write the code yourself. You start with a map of path -> prefix, make each call to SSM in turn and combine each output array with the path and prefix to create objects similar to the ones in the above snippet
  2. You find a library that's already implemented this for you. Speaking of which ...

Don’t reinvent the wheel

This kind of code is ripe for abstraction into a reusable tool: it's got nothing to do with your business logic and if you need it once then you probably need it way more than once.

We’re huge fans of Middy, “🛵 the stylish Node.js middleware engine for AWS Lambda” and use it to implement lots of functionality that we need across multiple Lambdas and isn’t specific to those functions themselves. As it wraps the whole handler, it also allows our code to be agnostic of the cloud provider, and as an added bonus it’s open source. Given all that, we were thrilled to find that it features an SSM middleware for fetching values from the Parameter Store.

It previously only supported the getParameters method, so we forked the project and updated the SSM middleware to also support getParametersByPath for use in our projects, and we’re very happy to say that our fork has now been pulled into Middy itself, so you can add it to your project today and use our solution rather than replicating code that’s already been written.

You can and should read the Middy docs to see how the library works, and the section on the SSM middleware specifically to see all of its configuration options, but here's a quick example of some code that uses it from the docs itself:

const middy = require('middy')
const { ssm } = require('middy/middlewares')

// to build a handler that can use middleware, you simply call middy() and pass in your function logic.
const handler = middy((event, context, cb) => {
  cb(null, {})
})

// to use a middleware, call handler.use() passing in the middleware in question
// in this case, ssm() takes a configuration object in which we specify the paths for our SSM parameters
handler.use(
  ssm({
    cache: true,
    paths: {
      DB: '/dev/db'
    }
  })
)

// assuming we have /dev/db/access_token and /dev/db/secret in the Parameter Store ...
handler(event, context, (_, response) => {
  expect(process.env.DB.ACCESS_TOKEN).toEqual('some-access-token')
  expect(process.env.DB_SECRET).toEqual('some-secret')
})

And there you have it! We're pretty sure this is the simplest and best way to access the SSM Parameter Store from a Lambda function, and the best way to manage your secrets and other config.

Techniques like this allow us to rapidly build new serverless microservices. By abstracting configuration out into a reusable tool, we free ourselves up to focus on the problems that our clients want us to solve rather than reinventing the wheel at the start of every new project. If that sounds like the way you'd like to work too, then we have good news: we're hiring!