Skip to content

Commit

Permalink
feat(push): push only updated parameters
Browse files Browse the repository at this point in the history
Check before pushing parameters which parameters need to be changed. This makes browsing in AWS
Console more clear when checking parameter history. Before this change there could be 2 history
records with the same value.
  • Loading branch information
poikaa committed Oct 11, 2021
1 parent 45f088e commit 00015d7
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 111 deletions.
18 changes: 3 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@aws-sdk/client-ssm": "^3.35.0",
"dotenv": "^10.0.0",
"lodash.sortby": "^4.7.0",
"lodash": "^4.17.21",
"meow": "^10.1.1"
},
"devDependencies": {
Expand Down
41 changes: 41 additions & 0 deletions src/client.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { SSMClient } from '@aws-sdk/client-ssm'

export function createClient({ region, accessKeyId, secretAccessKey }) {
const credentials =
accessKeyId && secretAccessKey
? {
accessKeyId,
secretAccessKey,
}
: undefined

return new SSMClient({ region, credentials })
}

async function delay(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}

export function addThrottleMiddleware(client, { batchSize, wait }) {
let currentBatchSize = 0

// https://aws.amazon.com/blogs/developer/middleware-stack-modular-aws-sdk-js/
client.middlewareStack.add(
(next) => async (args) => {
if (currentBatchSize > batchSize) {
currentBatchSize = 0

await delay(wait)
}

currentBatchSize++

return await next(args)
},
{
step: 'initialize',
}
)

return client
}
3 changes: 2 additions & 1 deletion src/index.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import meow from 'meow'
import { pullParameters, pushParameters } from './ssm.mjs'
import { pullParameters } from './pull.mjs'
import { pushParameters } from './push.mjs'
import { toDotenvString } from './dotenv.mjs'

const COMMANDS = {
Expand Down
29 changes: 29 additions & 0 deletions src/pull.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import _ from 'lodash'
import { paginateGetParametersByPath } from '@aws-sdk/client-ssm'
import { createClient } from './client.mjs'

export async function pullParameters({ client, prefix, ...config }) {
const paginator = paginateGetParametersByPath(
{
client: client ?? createClient(config),
},
{
Path: prefix,
Recursive: true,
WithDecryption: true,
}
)

const parameterList = []
for await (const page of paginator) {
parameterList.push(...page.Parameters)
}

return _.sortBy(parameterList, 'Name').reduce(
(parameters, { Name, Value }) => ({
...parameters,
[Name.substr(prefix.length)]: Value,
}),
{}
)
}
87 changes: 87 additions & 0 deletions src/push.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import _ from 'lodash'
import { PutParameterCommand, ParameterType } from '@aws-sdk/client-ssm'
import { parseDotenv } from './dotenv.mjs'
import { createClient, addThrottleMiddleware } from './client.mjs'
import { pullParameters } from './pull.mjs'

async function analyze({ client, prefix, file }) {
const parsedParameters = parseDotenv(file)

const remoteParameters = await pullParameters({ client, prefix })

let skipped = 0
let updated = 0
let created = 0

const parametersToUpdate = _.reduce(
parsedParameters,
(toUpdate, value, name) => {
if (_.has(remoteParameters, name)) {
if (remoteParameters[name] === value) {
skipped++
} else {
updated++
toUpdate[name] = value
}
} else {
created++
toUpdate[name] = value
}

return toUpdate
},
{}
)

return {
total: _.size(parsedParameters),
skipped,
updated,
created,
parameters: parametersToUpdate,
}
}

function printStat({ total, skipped, updated, created }) {
const padSize = _.max([skipped, updated, created]).toString().length

console.log(
[
`Total ${total} of parameters:`,
` ${_.padStart(skipped, padSize)} up-to-date`,
`~ ${_.padStart(updated, padSize)} updated`,
`+ ${_.padStart(created, padSize)} created`,
].join('\n')
)
}

/**
* Parameters are sent one by one with 1 second delay after each 10 requests to avoid `ThrottlingException: Rate exceeded`.
* See docs:
* https://aws.amazon.com/premiumsupport/knowledge-center/ssm-parameter-store-rate-exceeded/
* https://docs.aws.amazon.com/general/latest/gr/ssm.html#limits_ssm
*/
export async function pushParameters({ prefix, file, ...config }) {
const client = createClient(config)

const { parameters, ...stat } = await analyze({ client, prefix, file })

addThrottleMiddleware(client, { batchSize: 10, wait: 1000 })

const putCommands = _.map(
parameters,
(value, name) =>
new PutParameterCommand({
Type: ParameterType.STRING,
Overwrite: true,
Name: `${prefix}${name}`,
Value: value,
})
)

for (const putCommand of putCommands) {
await client.send(putCommand)
}

printStat(stat)
}
94 changes: 0 additions & 94 deletions src/ssm.mjs

This file was deleted.

0 comments on commit 00015d7

Please sign in to comment.