Skip to content

Commit

Permalink
Use "/" instead of "@" in SSM parameter key (#208)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Elad Ben-Israel committed Jul 2, 2018
1 parent e15a75c commit eafc9fe
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 12 deletions.
54 changes: 54 additions & 0 deletions packages/@aws-cdk/rtv/README.md
Original file line number Diff line number Diff line change
@@ -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/<stack-name>/<package>/<name>
```

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/<stack-name>/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.
63 changes: 55 additions & 8 deletions packages/@aws-cdk/rtv/lib/rtv.ts
Original file line number Diff line number Diff line change
@@ -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 {

}
12 changes: 9 additions & 3 deletions packages/@aws-cdk/rtv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
164 changes: 164 additions & 0 deletions packages/@aws-cdk/rtv/test/integ.rtv.lambda.expected.json
Original file line number Diff line number Diff line change
@@ -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"
]
]
}
}
}
}
}
40 changes: 40 additions & 0 deletions packages/@aws-cdk/rtv/test/integ.rtv.lambda.ts
Original file line number Diff line number Diff line change
@@ -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/<stack-name>/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());
2 changes: 1 addition & 1 deletion packages/@aws-cdk/rtv/test/test.rtv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

0 comments on commit eafc9fe

Please sign in to comment.