From e409ad90906d2974b708c2258cdcb8f8a185d19d Mon Sep 17 00:00:00 2001 From: nbp Date: Thu, 17 Apr 2025 16:29:24 -0700 Subject: [PATCH 1/6] Add Amazon Systems Manager Parameter Store support library --- lib/ssm-parameters.js | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/ssm-parameters.js diff --git a/lib/ssm-parameters.js b/lib/ssm-parameters.js new file mode 100644 index 0000000..914819c --- /dev/null +++ b/lib/ssm-parameters.js @@ -0,0 +1,46 @@ +const AWS = require('aws-sdk') + +async function getSsmParameters(names, options = {}) { + const ssm = new AWS.SSM() + const params = { + Names: names, + WithDecryption: options.withDecryption !== false + } + const result = await ssm.getParameters(params).promise() + const values = {} + for (const param of result.Parameters) { + // Use only the last segment after the last slash as the config key + const key = param.Name.substring(param.Name.lastIndexOf('/') + 1); + values[key] = param.Value; + } + if (result.InvalidParameters && result.InvalidParameters.length > 0) { + throw new Error(`Invalid SSM Parameters: ${result.InvalidParameters.join(', ')}`) + } + return values +} + +/** + * Decorates config with values from AWS SSM Parameter Store. + * If SSM_PARAMETER_NAMES is present and valid, fetches and merges those values. + * SSM values are overridden by direct event fields. + * @param {object} baseConfig + * @returns {Promise} decorated config + */ +async function decorateWithSsmParameters(baseConfig) { + if ( + baseConfig.SSM_PARAMETER_NAMES && + Array.isArray(baseConfig.SSM_PARAMETER_NAMES) && + baseConfig.SSM_PARAMETER_NAMES.length > 0 + ) { + try { + const ssmValues = await getSsmParameters(baseConfig.SSM_PARAMETER_NAMES) + return { ...ssmValues, ...baseConfig } + } catch (error) { + console.log('Error fetching SSM parameters:', error) + return baseConfig + } + } + return baseConfig +} + +module.exports = decorateWithSsmParameters From 20e3c6c06dc7207ed0a951346e08091d16adcdaa Mon Sep 17 00:00:00 2001 From: nbp Date: Thu, 17 Apr 2025 16:30:24 -0700 Subject: [PATCH 2/6] Integrate ssm-parameters library --- lib/handler.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/handler.js b/lib/handler.js index bef2e53..9070c2c 100644 --- a/lib/handler.js +++ b/lib/handler.js @@ -5,6 +5,7 @@ const decorateWithIamToken = require('./iam') const decorateWithSecretsManagerCredentials = require('./secrets-manager') const parseDatabaseNames = require('./parseDatabaseNames') const encryption = require('./encryption') +const decorateWithSsmParameters = require('./ssm-parameters') const DEFAULT_CONFIG = require('./config') @@ -46,6 +47,9 @@ async function handler(event) { else if (event.SECRETS_MANAGER_SECRET_ID) { decoratedConfig = await decorateWithSecretsManagerCredentials(baseConfig) } + else if (event.SSM_PARAMETER_NAMES) { + decoratedConfig = await decorateWithSsmParameters(baseConfig) + } else { decoratedConfig = baseConfig } From ddcf2911229949f47272161c80fd48ddf80fa317 Mon Sep 17 00:00:00 2001 From: nbp Date: Thu, 17 Apr 2025 16:30:49 -0700 Subject: [PATCH 3/6] Add SSM Parameter details --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index d611538..0b89f97 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,24 @@ If you supply `SECRETS_MANAGER_SECRET_ID`, you can ommit the 'PG\*' keys, and th You can provide overrides in your event to any PG\* keys as event parameters will take precedence over secret values. +#### SSM Parameter Store-based configuration + +If you want to load configuration or secrets from AWS Systems Manager Parameter Store, you can provide a list of parameter names in the event via the `SSM_PARAMETER_NAMES` field. Only one external configuration source (IAM, Secrets Manager, or SSM) is used per invocation: if IAM or Secrets Manager fields are present, those take precedence and SSM is ignored. + +**Example:** + +```json +{ + "SSM_PARAMETER_NAMES": ["/my/app/PGUSER", "/my/app/PGPASSWORD", "/my/app/PGHOST", "/my/app/PGDATABASE"], + "S3_BUCKET": "db-backups", + "ROOT": "hourly-backups" +} +``` + +The fetched parameter values will be merged into the event config, using only the last segment of each parameter name as the config key (e.g., '/my/app/PGUSER' becomes 'PGUSER'). If there are conflicts, event fields override SSM values. + +The Lambda execution role must have permission to call `ssm:GetParameters` for the specified parameter names (e.g., by attaching the AWS managed policy `AmazonSSMReadOnlyAccess`). + #### Multiple databases If you'd like to export multiple databases in a single event, you can add a comma-separated list of database names to the PGDATABASE setting. The results will return in a list. From 852dfd3f62186ae74b417ae7febcf7f6758e04e1 Mon Sep 17 00:00:00 2001 From: nbp Date: Thu, 17 Apr 2025 16:31:10 -0700 Subject: [PATCH 4/6] Add SSM parameters tests --- test/ssm-parameters.js | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/ssm-parameters.js diff --git a/test/ssm-parameters.js b/test/ssm-parameters.js new file mode 100644 index 0000000..ff46d0c --- /dev/null +++ b/test/ssm-parameters.js @@ -0,0 +1,58 @@ +const { expect } = require('chai') +const AWSMOCK = require('aws-sdk-mock') +const AWS = require('aws-sdk') +const decorateWithSsmParameters = require('../lib/ssm-parameters') + +describe('decorateWithSsmParameters', () => { + before(() => { + AWSMOCK.setSDKInstance(AWS) + }) + + afterEach(() => { + AWSMOCK.restore('SSM') + }) + + it('should merge SSM parameter values into config', async () => { + AWSMOCK.mock('SSM', 'getParameters', (params, cb) => { + cb(null, { + Parameters: [ + { Name: '/my/app/PGUSER', Value: 'bar' }, + { Name: '/my/app/PGPASSWORD', Value: 'qux' } + ], + InvalidParameters: [] + }) + }) + const baseConfig = { SSM_PARAMETER_NAMES: ['/my/app/PGUSER', '/my/app/PGPASSWORD'], someOther: 'value' } + const result = await decorateWithSsmParameters(baseConfig) + expect(result).to.deep.equal({ PGUSER: 'bar', PGPASSWORD: 'qux', SSM_PARAMETER_NAMES: ['/my/app/PGUSER', '/my/app/PGPASSWORD'], someOther: 'value' }) + }) + + it('should allow event fields to override SSM values', async () => { + AWSMOCK.mock('SSM', 'getParameters', (params, cb) => { + cb(null, { + Parameters: [ + { Name: '/my/app/PGUSER', Value: 'bar' } + ], + InvalidParameters: [] + }) + }) + const baseConfig = { SSM_PARAMETER_NAMES: ['/my/app/PGUSER'], PGUSER: 'override' } + const result = await decorateWithSsmParameters(baseConfig) + expect(result.PGUSER).to.equal('override') + }) + + it('should return baseConfig if no SSM_PARAMETER_NAMES', async () => { + const baseConfig = { PGUSER: 'user' } + const result = await decorateWithSsmParameters(baseConfig) + expect(result).to.deep.equal(baseConfig) + }) + + it('should return baseConfig on SSM error', async () => { + AWSMOCK.mock('SSM', 'getParameters', (params, cb) => { + cb(new Error('fail')) + }) + const baseConfig = { SSM_PARAMETER_NAMES: ['foo'] } + const result = await decorateWithSsmParameters(baseConfig) + expect(result).to.deep.equal(baseConfig) + }) +}) From 529bc157ff424a10b8361597249e0e922e0d8723 Mon Sep 17 00:00:00 2001 From: nbp Date: Thu, 17 Apr 2025 17:02:06 -0700 Subject: [PATCH 5/6] Handle missing parameters --- lib/ssm-parameters.js | 7 +++++++ test/ssm-parameters.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/ssm-parameters.js b/lib/ssm-parameters.js index 914819c..340cf73 100644 --- a/lib/ssm-parameters.js +++ b/lib/ssm-parameters.js @@ -8,11 +8,18 @@ async function getSsmParameters(names, options = {}) { } const result = await ssm.getParameters(params).promise() const values = {} + const requestedKeys = names.map(name => name.substring(name.lastIndexOf('/') + 1)) + const returnedKeys = result.Parameters.map(param => param.Name.substring(param.Name.lastIndexOf('/') + 1)) for (const param of result.Parameters) { // Use only the last segment after the last slash as the config key const key = param.Name.substring(param.Name.lastIndexOf('/') + 1); values[key] = param.Value; } + // If any requested keys are missing, throw a clear error + const missingKeys = requestedKeys.filter(key => !returnedKeys.includes(key)) + if (missingKeys.length > 0) { + throw new Error(`Missing SSM Parameters (by config key): ${missingKeys.join(', ')}`) + } if (result.InvalidParameters && result.InvalidParameters.length > 0) { throw new Error(`Invalid SSM Parameters: ${result.InvalidParameters.join(', ')}`) } diff --git a/test/ssm-parameters.js b/test/ssm-parameters.js index ff46d0c..d773f1f 100644 --- a/test/ssm-parameters.js +++ b/test/ssm-parameters.js @@ -55,4 +55,19 @@ describe('decorateWithSsmParameters', () => { const result = await decorateWithSsmParameters(baseConfig) expect(result).to.deep.equal(baseConfig) }) + + it('should return baseConfig if any requested SSM parameter is missing', async () => { + AWSMOCK.mock('SSM', 'getParameters', (params, cb) => { + cb(null, { + Parameters: [ + { Name: '/my/app/PGUSER', Value: 'bar' } + ], + InvalidParameters: [] + }) + }) + // PGUSER will be found, PGPASSWORD will be missing + const baseConfig = { SSM_PARAMETER_NAMES: ['/my/app/PGUSER', '/my/app/PGPASSWORD'] } + const result = await decorateWithSsmParameters(baseConfig) + expect(result).to.deep.equal(baseConfig) + }) }) From ec55de7a5b40056716983b155f79b68351537dd3 Mon Sep 17 00:00:00 2001 From: nbp Date: Thu, 17 Apr 2025 17:03:04 -0700 Subject: [PATCH 6/6] Add error flow info --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0b89f97..a260dfe 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,8 @@ If you want to load configuration or secrets from AWS Systems Manager Parameter The fetched parameter values will be merged into the event config, using only the last segment of each parameter name as the config key (e.g., '/my/app/PGUSER' becomes 'PGUSER'). If there are conflicts, event fields override SSM values. +If any requested SSM parameter is missing or an error occurs during fetching, the function will log an error and fall back to using only the provided event configuration (i.e., SSM values will not be merged). + The Lambda execution role must have permission to call `ssm:GetParameters` for the specified parameter names (e.g., by attaching the AWS managed policy `AmazonSSMReadOnlyAccess`). #### Multiple databases