Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ 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.

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

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.
Expand Down
4 changes: 4 additions & 0 deletions lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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
}
Expand Down
53 changes: 53 additions & 0 deletions lib/ssm-parameters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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 = {}
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(', ')}`)
}
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<object>} 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
73 changes: 73 additions & 0 deletions test/ssm-parameters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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)
})

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)
})
})