-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Create GuSSMParameter using AWS Custom Resources #336
Conversation
src/constructs/core/ssm.ts
Outdated
private readonly customResource: CustomResource; | ||
readonly grantPrincipal: IPrincipal; | ||
|
||
// eslint-disable-next-line custom-rules/valid-constructors -- I think stating an ID would be overkill for this, but happy to discuss |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are plans to change this; see similar a discussion here. However perhaps param
should be part of props, for consistency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good shout, and done. It's actually becoming apparent that passing in an ID might solve a couple of issues down the line but will investigate.
The problem is essentially that it looks like we need to ensure we create new custom resources every time (not the lambda, but possibly everything else) in order to trigger the fetching of an SSM parameter every time. If you have a fixed ID, it might be the case that it won't update past creation unless you somehow inform it that there's been a change.
Will investigate though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#352 revises the linting rules.
src/constructs/core/ssm.ts
Outdated
|
||
const provider = new SingletonFunction(scope, id("Provider"), { | ||
code: Code.fromInline(readFileSync(join(__dirname, "/custom-resources/runtime/lambda.js")).toString()), | ||
// runtime: new Runtime("nodejs14.x", RuntimeFamily.NODEJS, { supportsInlineCode: true }), -- we can use Node14 once we bump the version of @aws-cdk to v1.94 https://github.com/aws/aws-cdk/releases/tag/v1.94.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're in luck!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yay! Updated to use Node 14.
src/constructs/core/ssm.ts
Outdated
} | ||
} | ||
|
||
export function GuSSMDefaultParam(scope: GuStack, param: string): GuSSMParameter { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If GuSSMParameterProps
was:
export interface GuSSMParameterProps {
secure?: boolean;
/*
* If no path is provided we will assume it is`/${STAGE}/${STACK}/${APP}/${param}`
* */
overridePath?: string; // not sure about the name, but hopefully you get the idea
}
Then we could consider dropping GuSSMDefaultParam
entirely?
Although it would still be possible to stray from the well-trodden path (no pun intended), it would be a little harder to do so accidentally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, maybe if we called the prop fullPath
instead, that might be clearer? eg:
const withFullPath = new GuSSMParameter(this, { parameter: "/path/to/param", fullPath: true })
const assumedPath = new GuSSMParameter(this, { parameter: "param" }) // assumes /stage/stack/app/param
package.json
Outdated
"@types/jest": "^26.0.20", | ||
"@types/node": "14.14.32", | ||
"@typescript-eslint/eslint-plugin": "^4.16.1", | ||
"@typescript-eslint/parser": "^4.16.1", | ||
"aws-sdk": "^2.866.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that you could use v3
of this sdk. I had a few problems migrating a project to it recently, but unless you are aware of any known issues with the functionality you're using it could be worth a try?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good shout! I've tried it and looks fine, thanks for the heads up about this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, I've had to revert to v2 of the library since an AWS Node Lambda only has v2 of the library available. If and when this changes we can definitely upgrade though, but not sure we can make this work with v3 at the moment.
Great PR description, thanks for taking the time to write this up! I've left a few comments but it's looking good so far! |
ca1d230
to
f328c36
Compare
I've got a couple of things to work out before this is good to go (getting CI to pass included), so will work those out before requesting another review but all comments so far should be addressed |
6d3f92b
to
007dfb5
Compare
cc9ed72
to
d4ce39c
Compare
This is now ready for review! Good to get eyes on it from @jacobwinch @akash1810 and/or @sihil if possible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Added a few comments.
Could we add some usage guidance to the PR description? The example use case is a database user - when should this be normal application config vs a GuSSMParameter
in the CFN template?
Lastly, should the PR's prefix be "feat" as we're adding a feature and a new minor version should be released?
/* | ||
* You can't specify the file extension (eg .ts) of the file to import, | ||
* and lambda.js exists as a dummy file, so we'd always import that over lambda.ts (the actual code to be tested) if | ||
* we imported `lambda` | ||
* lambda.symlink is a symlink to lambda.ts, so we can test it without importing lambda.js | ||
* */ | ||
import { flatten } from "./lambda.symlink"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow! I'd be curious to know the output of npm pack
now? IIUC npm pack
will create a local bundle that's representative of what gets published to NPM - a sort of publish dry run.
I'm not sure what JS file will be added - the empty lambda.js or the one that tsc will create?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just ran it and it looks like it packs fine! By that I mean the resulting lambda.js
file is the expected compiled TS lambda, not the dummy file. I assume it simply overwrites it no matter what, as the dummy file could equally be seen as a stale JS file in need of updating
src/constructs/core/ssm.test.ts
Outdated
beforeAll(() => { | ||
// TODO: Experiment with compiling `custom-resources/runtime/lambda.ts` to JS so that tests run | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think this is needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good find, thanks!
src/constructs/core/ssm.ts
Outdated
|
||
const provider = new SingletonFunction(scope, id("Provider"), { | ||
code: Code.fromInline(readFileSync(join(__dirname, "/custom-resources/runtime/lambda.js")).toString()), | ||
runtime: Runtime.NODEJS_12_X, // TODO: Ensure that the TS -> JS compile creates a NODE12-compliant file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Node 12 is in maintenance LTS. AWS recently announced support for node 14 in AWS Lambda.
How does the output for a node 14 runtime break? There might be some TS config tweaks we can make? Or raise it with our AWS TAM?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this TODO is outdated, as the TS configuration in the app creates Node 12 compliant JS code. The problem with upgrading to Node 14 is that it for some reason prevents us from using inline code (ie Code.fromInline
), which is necessary here. I can't find anything online yet that specifically explains why, but it means that we're somewhat restricted to 12 until we can inline the code or find a better solution.
src/constructs/core/ssm.ts
Outdated
const id = (id: string) => { | ||
const now = Date.now(); | ||
// We need to create UIDs for the resources in this construct, as otherwise CFN will not trigger the lambda on updates for resources that appear to be the same | ||
const uid = now.toString().substr(now.toString().length - 4); | ||
return parameter.toUpperCase().includes("TOKEN") ? `${id}-token-${uid}` : `${id}-${stripped(parameter)}-${uid}`; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment suggests this is pretty crucial, should we explicitly test it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we should. I've added two tests that hopefully capture this behaviour!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good, but not a blocker, to isolate this function to make it super obvious that given this input, we expect this output. That is, test the id
function outside of the constructor. We can do this later as needed though.
@@ -1,8 +1,19472 @@ | |||
{ | |||
"name": "@guardian/cdk", | |||
"version": "4.0.0", | |||
"lockfileVersion": 1, | |||
"lockfileVersion": 2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're on NPM v7? You'll want to downgrade to v6, I think . See #340 and #335 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
D'oh, thanks! Resolved in a coming push.
Thanks for the comments @akash1810 , I've now addressed them all as well as updated the PR name and added a use case, hope that's what you were after? |
83f64c2
to
6be4b43
Compare
We have a few too many instances of // eslint-disable-next-line custom-rules/valid-constructors 😅 . It looks like this is mainly because the id field is static (for example the Stage parameter) or generated by the props passed in (for example InstanceType). This PR revises the rules to be: 1. Private constructors don't get linted 2. Must be 1, 2 or 3 parameters 3. First parameter must be called scope 4. First parameter must be of type GuStack 5. If 2 parameters: - The second parameter must be called props - The second parameter must be a custom type 6. If 3 parameters: - The second parameter must be called id - The second parameter must be of type string - The third parameter must be called props - The third parameter must be a custom type 7. Only the third parameter can be optional or have a default value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. A couple of minor comments to consider, but feel free to address them in future PRs.
hostname: parsedUrl.hostname, | ||
path: parsedUrl.path, | ||
method: "PUT", | ||
headers: { "content-type": "", "content-length": responseBody.length }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does content-type
of empty string do/mean? Would omitting it yield the same behaviour?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume it's a bad thing, if anything. However, it was copied straight from aws's source code, so I'm tempted to leave it for now and only change if it causes issues?
src/constructs/core/ssm.ts
Outdated
const id = (id: string) => { | ||
const now = Date.now(); | ||
// We need to create UIDs for the resources in this construct, as otherwise CFN will not trigger the lambda on updates for resources that appear to be the same | ||
const uid = now.toString().substr(now.toString().length - 4); | ||
return parameter.toUpperCase().includes("TOKEN") ? `${id}-token-${uid}` : `${id}-${stripped(parameter)}-${uid}`; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good, but not a blocker, to isolate this function to make it super obvious that given this input, we expect this output. That is, test the id
function outside of the constructor. We can do this later as needed though.
readonly grantPrincipal: IPrincipal; | ||
|
||
// eslint-disable-next-line custom-rules/valid-constructors -- TODO: Remove once linting rules have been relaxed for this | ||
constructor(scope: GuStack, props: GuSSMParameterProps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#352 allows for this constructor 🎉 a rebase of main
should allow you to remove this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one, done.
* */ | ||
export class GuSSMIdentityParameter extends GuSSMParameter { | ||
// eslint-disable-next-line custom-rules/valid-constructors -- this may not be necessary going forward | ||
constructor(scope: GuStack, props: GuSSMIdentityParameterProps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#352 allows for this constructor 🎉 a rebase of main
should allow you to remove this line.
I isolated the function and added some tests, hopefully that should do the trick @akash1810 . |
🎉 This PR is included in version 6.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
What does this change?
Problem
At the moment, the general practice for injecting secrets into CloudFormation templates is through CFN parameters. In CDK, this looks like this:
This is a bit gross, as it:
Solution
This PR creates a
GuSSMParameter
construct, making use of the SSM Parameter Store to fetch values dynamically.Using AWS Custom Resources, the
GuSSMParameter
construct can dynamically fetch the latest version of an SSM parameter, something that AWS' own CDK construct cannot do yet.This also creates
GuSSMDefaultParam
(would prefer a new name, suggestions welcome!) that assumes the default path of/$STAGE/$STACK/$APP/$PARAM_NAME
as a prefix to the parameter name, for added simplicity.All three of the below implementations work:
This offers numerous advantages:
/$STAGE/$STACK/$APP/$PARAM_NAME
path prefix with ease and have it dynamically resolve based on the value of those parametersUsage
Abridged from tag-janitor
How does it work?
This uses AWS Custom Resouces, which creates a few things alongside your normal resources. Namely, this creates a lambda which will essentially execute anything you want, but we've restricted ours to just call
SSM getParameter
for our purposes. When you create/update a stack, the lambda will run, fetch the latest version of the SSM parameters you need and populate the relevant resources with them.Does this change require changes to existing projects or CDK CLI?
Not yet, as this would be something to begin replacing traditional
GuParameter
s with.How to test
Tests will be added before moving this from draft to live.
How can we measure success?
$STAGE/$STACK/$APP
pathsHave we considered potential risks?
Not risks as such, but AWS Custom Resources will somewhat unavoidably create additional resources per stack. This isn't necessarily a problem (and should be fine cost-wise due to the low number of executions), but is a bit of a gotcha that's worth knowing about. However, users will not need to touch these resources and can happily ignore them.