From eafc9fe6697e636cec4fd1cb6aadb5ce9ce43a88 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Mon, 2 Jul 2018 16:34:46 +0300 Subject: [PATCH] Use "/" instead of "@" in SSM parameter key (#208) Use "/" instead of "@" to delimit package and name in SSM parameter since "@" is not allowed in SSM parameter keys. Obvsiouly this has never been tested properly, so add an integration test. Fixes #151 --- packages/@aws-cdk/rtv/README.md | 54 ++++++ packages/@aws-cdk/rtv/lib/rtv.ts | 63 ++++++- packages/@aws-cdk/rtv/package.json | 12 +- .../rtv/test/integ.rtv.lambda.expected.json | 164 ++++++++++++++++++ .../@aws-cdk/rtv/test/integ.rtv.lambda.ts | 40 +++++ packages/@aws-cdk/rtv/test/test.rtv.ts | 2 +- 6 files changed, 323 insertions(+), 12 deletions(-) create mode 100644 packages/@aws-cdk/rtv/README.md create mode 100644 packages/@aws-cdk/rtv/test/integ.rtv.lambda.expected.json create mode 100644 packages/@aws-cdk/rtv/test/integ.rtv.lambda.ts diff --git a/packages/@aws-cdk/rtv/README.md b/packages/@aws-cdk/rtv/README.md new file mode 100644 index 0000000000000..fd6de3b8a2eb9 --- /dev/null +++ b/packages/@aws-cdk/rtv/README.md @@ -0,0 +1,54 @@ +# Runtime Values + +The CDK allows apps to advertise values from __construction time__ to __runtime +code__. For example, consider code in a Lambda function which needs to know the +URL of the SQS queue created as part of your CDK app. + +Runtime values are advertised as textual SSM parameters with the following key: + +``` +/rtv/// +``` + +Therefore, in order to advertise a value you will need to: + +1. Make the current stack name available as an environment variable to your + runtime code. The convention is to use `RTV_STACK_NAME`. +2. Use the `RuntimeValue` construct in order to create the SSM parameter and + specify least-privilege permissions. + +For example, say we want to publish a queue's URL to a lambda function. + +### Construction Code + +```ts +import { RuntimeValue } from '@aws-cdk/rtv' + +const queue = new Queue(this, 'MyQueue', { /* props.... */ }); +const fn = new Lambda(this, 'MyFunction', { /* props... */ }); +const fleet = new Fleet(this, 'MyFleet', { /* props... */ }); + +// this line defines an AWS::SSM::Parameter resource with the +// key "/rtv//com.myorg/MyQueueURL" and the actual queue URL as value +const queueUrlRtv = new RuntimeValue(this, 'QueueRTV', { + package: 'com.myorg', + name: 'MyQueueURL', + value: queue.queueUrl +}); + +// this line adds read permissions for this SSM parameter to the policies associated with +// the IAM roles of the Lambda function and the EC2 fleet +queueUrlRtv.grantRead(fn.role); +queueUrlRtv.grantRead(fleet.role); + +// adds the `RTV_STACK_NAME` to the environment of the lambda function +// and the fleet (via user-data) +fn.env(RuntimeValue.ENV_NAME, RuntimeValue.ENV_VALUE); +fleet.env(RuntimeValue.ENV_NAME, RuntimeValue.ENV_VALUE); +``` + +### Runtime Code + +Then, your runtime code will need to use the SSM Parameter Store AWS SDK in +order to format the SSM parameter key and read the value. In future releases, we +will provide runtime libraries to make this easy. diff --git a/packages/@aws-cdk/rtv/lib/rtv.ts b/packages/@aws-cdk/rtv/lib/rtv.ts index 1ff35230259ce..c720400277773 100644 --- a/packages/@aws-cdk/rtv/lib/rtv.ts +++ b/packages/@aws-cdk/rtv/lib/rtv.ts @@ -1,47 +1,94 @@ -import { Arn, AwsStackName, Construct, FnConcat, PolicyStatement } from '@aws-cdk/core'; +import { Arn, AwsStackName, Construct, FnConcat, PolicyStatement, Token } from '@aws-cdk/core'; import { IIdentityResource } from '@aws-cdk/iam'; import { ssm } from '@aws-cdk/resources'; export interface RuntimeValueProps { + /** + * A namespace for the runtime value. + * It is recommended to use the name of the library/package that advertises this value. + */ package: string; + + /** + * The value to advertise. Can be either a primitive value or a token. + */ value: any; } +/** + * Defines a value published from construction code which needs to be accessible + * by runtime code. + */ export class RuntimeValue extends Construct { + + /** + * The recommended name of the environment variable to use to set the stack name + * from which the runtime value is published. + */ public static readonly ENV_NAME = 'RTV_STACK_NAME'; + + /** + * The value to assign to the `RTV_STACK_NAME` environment variable. + */ public static readonly ENV_VALUE = new AwsStackName(); + /** + * IAM actions needed to read a value from an SSM parameter. + */ private static readonly SSM_READ_ACTIONS = [ 'ssm:DescribeParameters', 'ssm:GetParameters', 'ssm:GetParameter' ]; - public readonly parameterName: any; + /** + * The name of the runtime parameter. + */ + public readonly parameterName: ParameterName; + + /** + * The ARN fo the SSM parameter used for this runtime value. + */ + public readonly parameterArn: Arn; constructor(parent: Construct, name: string, props: RuntimeValueProps) { super(parent, name); - this.parameterName = new FnConcat('/rtv/', new AwsStackName(), '/', props.package, '@', name); + this.parameterName = new FnConcat('/rtv/', new AwsStackName(), '/', props.package, '/', name); new ssm.ParameterResource(this, 'Parameter', { parameterName: this.parameterName, type: 'String', value: props.value, }); - } - get arn() { - return Arn.fromComponents({ + this.parameterArn = Arn.fromComponents({ service: 'ssm', resource: 'parameter', resourceName: this.parameterName }); } - public grantReadPermissions(principal: IIdentityResource) { + /** + * Grants a principal read permissions on this runtime value. + * @param principal The principal (e.g. Role, User, Group) + */ + public grantRead(principal?: IIdentityResource) { + + // sometimes "role" is optional, so we want `rtv.grantRead(role)` to be a no-op + if (!principal) { + return; + } + principal.addToPolicy(new PolicyStatement() - .addResource(this.arn) + .addResource(this.parameterArn) .addActions(...RuntimeValue.SSM_READ_ACTIONS)); } } + +/** + * The full name of the runtime value's SSM parameter. + */ +export class ParameterName extends Token { + +} diff --git a/packages/@aws-cdk/rtv/package.json b/packages/@aws-cdk/rtv/package.json index d8a673fb5915a..6269d000e679b 100644 --- a/packages/@aws-cdk/rtv/package.json +++ b/packages/@aws-cdk/rtv/package.json @@ -19,8 +19,9 @@ "prepare": "jsii && tslint -p . && pkglint", "watch": "jsii -w", "lint": "tsc && tslint -p . --force", - "test": "nyc nodeunit test/test.*.js", - "pkglint": "pkglint -f" + "test": "nyc nodeunit test/test.*.js && cdk-integ-assert", + "pkglint": "pkglint -f", + "integ": "cdk-integ" }, "keywords": [ "aws", @@ -33,7 +34,12 @@ }, "license": "LicenseRef-LICENSE", "devDependencies": { - "pkglint": "^0.7.1" + "pkglint": "^0.7.1", + "aws-cdk": "^0.7.2-beta", + "@aws-cdk/assert": "^0.7.2-beta", + "@aws-cdk/ec2": "^0.7.2-beta", + "@aws-cdk/sqs": "^0.7.2-beta", + "@aws-cdk/lambda": "^0.7.2-beta" }, "dependencies": { "@aws-cdk/core": "^0.7.2-beta", diff --git a/packages/@aws-cdk/rtv/test/integ.rtv.lambda.expected.json b/packages/@aws-cdk/rtv/test/integ.rtv.lambda.expected.json new file mode 100644 index 0000000000000..a9b3a0ac18a24 --- /dev/null +++ b/packages/@aws-cdk/rtv/test/integ.rtv.lambda.expected.json @@ -0,0 +1,164 @@ +{ + "Resources": { + "MyQueueE6CA6235": { + "Type": "AWS::SQS::Queue" + }, + "MyFunctionServiceRole3C357FF2": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyFunctionServiceRoleDefaultPolicyB705ABD4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "ssm", + ":", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + "parameter", + "/", + { + "Fn::Join": [ + "", + [ + "/rtv/", + { + "Ref": "AWS::StackName" + }, + "/", + "com.myorg", + "/", + "MyQueueURL" + ] + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyFunctionServiceRoleDefaultPolicyB705ABD4", + "Roles": [ + { + "Ref": "MyFunctionServiceRole3C357FF2" + } + ] + } + }, + "MyFunction3BAA72D1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function runtimeCode(_event, _context, callback) {\n return callback();\n}" + }, + "Environment": { + "Variables": { + "RTV_STACK_NAME": { + "Ref": "AWS::StackName" + } + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionServiceRole3C357FF2", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "Timeout": 30 + }, + "DependsOn": [ + "MyFunctionServiceRole3C357FF2", + "MyFunctionServiceRoleDefaultPolicyB705ABD4" + ] + }, + "MyQueueURLParameterA4918D6E": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": { + "Ref": "MyQueueE6CA6235" + }, + "Name": { + "Fn::Join": [ + "", + [ + "/rtv/", + { + "Ref": "AWS::StackName" + }, + "/", + "com.myorg", + "/", + "MyQueueURL" + ] + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/rtv/test/integ.rtv.lambda.ts b/packages/@aws-cdk/rtv/test/integ.rtv.lambda.ts new file mode 100644 index 0000000000000..bf3bd30149828 --- /dev/null +++ b/packages/@aws-cdk/rtv/test/integ.rtv.lambda.ts @@ -0,0 +1,40 @@ +import { App, Stack } from '@aws-cdk/core'; +import { InlineJavaScriptLambda } from '@aws-cdk/lambda'; +import { Queue } from '@aws-cdk/sqs'; +import { RuntimeValue } from '../lib'; + +function runtimeCode(_event: any, _context: any, callback: any) { + return callback(); +} + +class TestStack extends Stack { + constructor(parent: App, name: string) { + super(parent, name); + + const queue = new Queue(this, 'MyQueue'); + const fn = new InlineJavaScriptLambda(this, 'MyFunction', { + handler: { fn: runtimeCode }, + }); + + // this line defines an AWS::SSM::Parameter resource with the + // key "/rtv//com.myorg/MyQueueURL" and the actual queue URL as value + const queueUrlRtv = new RuntimeValue(this, 'MyQueueURL', { + package: 'com.myorg', + value: queue.queueUrl + }); + + // this line adds read permissions for this SSM parameter to the policies associated with + // the IAM roles of the Lambda function and the EC2 fleet + queueUrlRtv.grantRead(fn.role); + + // adds the `RTV_STACK_NAME` to the environment of the lambda function + // and the fleet (via user-data) + fn.addEnvironment(RuntimeValue.ENV_NAME, RuntimeValue.ENV_VALUE); + } +} + +const app = new App(process.argv); + +new TestStack(app, 'aws-cdk-rtv-lambda'); + +process.stdout.write(app.run()); \ No newline at end of file diff --git a/packages/@aws-cdk/rtv/test/test.rtv.ts b/packages/@aws-cdk/rtv/test/test.rtv.ts index 7c62fc1202948..7ef90e52f9789 100644 --- a/packages/@aws-cdk/rtv/test/test.rtv.ts +++ b/packages/@aws-cdk/rtv/test/test.rtv.ts @@ -54,6 +54,6 @@ class RuntimeValueTest extends Construct { new RuntimeValue(this, 'MyQueueName', { package: RTV_PACKAGE, value: queue.queueName }) ]; - runtimeValues.forEach(rtv => rtv.grantReadPermissions(role)); + runtimeValues.forEach(rtv => rtv.grantRead(role)); } }