Skip to content

Commit 9af36af

Browse files
authored
feat(ssm): allow referencing "latest" version of SSM parameter (#1768)
There are many requests from people to integrate with SSM parameter store in same way, and in particular to get the latest version of a parameter. The mechanisms to get a specific version or the latest version at deployment time are very different, but both are now supported by and hidden in the ssm.ParameterStoreString class. Make the naming around properties that return a (potentially tokenized) value consistent. All properties of objects that return a string value are `stringValue`, all properties of objects that return a list value are `stringListValue`. Fixes #1587. BREAKING CHANGE: Rename `parameter.valueAsString` => `parameter.stringValue`, rename `parameter.valueAsList` => `parameter.stringListValue`, rename `ssmParameter.parameterValue` => `ssmParameter.stringValue` or `ssmParameter.stringListValue` depending on type, rename `secretString.value` => `secretString.stringValue`, rename `secret.toSecretString()` =>`secret.secretString`
1 parent 42876e7 commit 9af36af

23 files changed

+299
-85
lines changed

packages/@aws-cdk/assets-docker/lib/image-asset.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class DockerImageAsset extends cdk.Construct {
6161
this.node.addMetadata(cxapi.ASSET_METADATA, asset);
6262

6363
// parse repository name and tag from the parameter (<REPO_NAME>:<TAG>)
64-
const components = cdk.Fn.split(':', imageNameParameter.valueAsString);
64+
const components = cdk.Fn.split(':', imageNameParameter.stringValue);
6565
const repositoryName = cdk.Fn.select(0, components).toString();
6666
const imageTag = cdk.Fn.select(1, components).toString();
6767

packages/@aws-cdk/assets/lib/asset.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ export class Asset extends cdk.Construct {
109109
description: `S3 key for asset version "${this.node.path}"`
110110
});
111111

112-
this.s3BucketName = bucketParam.value.toString();
113-
this.s3Prefix = cdk.Fn.select(0, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.valueAsString)).toString();
114-
const s3Filename = cdk.Fn.select(1, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.valueAsString)).toString();
112+
this.s3BucketName = bucketParam.stringValue;
113+
this.s3Prefix = cdk.Fn.select(0, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.stringValue)).toString();
114+
const s3Filename = cdk.Fn.select(1, cdk.Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, keyParam.stringValue)).toString();
115115
this.s3ObjectKey = `${this.s3Prefix}${s3Filename}`;
116116

117117
this.bucket = s3.Bucket.import(this, 'AssetBucket', {

packages/@aws-cdk/aws-secretsmanager/README.md

+13-6
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ const secretsmanager = require('@aws-cdk/aws-secretsmanager');
55
```
66

77
### Create a new Secret in a Stack
8-
9-
In order to have SecretsManager generate a new secret value automatically, you can get started with the following:
8+
In order to have SecretsManager generate a new secret value automatically,
9+
you can get started with the following:
1010

1111
[example of creating a secret](test/integ.secret.lit.ts)
1212

13-
The `Secret` construct does not allow specifying the `SecretString` property of the `AWS::SecretsManager::Secret`
14-
resource as this will almost always lead to the secret being surfaced in plain text and possibly committed to your
15-
source control. If you need to use a pre-existing secret, the recommended way is to manually provision
16-
the secret in *AWS SecretsManager* and use the `Secret.import` method to make it available in your CDK Application:
13+
The `Secret` construct does not allow specifying the `SecretString` property
14+
of the `AWS::SecretsManager::Secret` resource (as this will almost always
15+
lead to the secret being surfaced in plain text and possibly committed to
16+
your source control).
17+
18+
If you need to use a pre-existing secret, the recommended way is to manually
19+
provision the secret in *AWS SecretsManager* and use the `Secret.import`
20+
method to make it available in your CDK Application:
1721

1822
```ts
1923
const secret = Secret.import(scope, 'ImportedSecret', {
@@ -22,3 +26,6 @@ const secret = Secret.import(scope, 'ImportedSecret', {
2226
encryptionKey,
2327
});
2428
```
29+
30+
SecretsManager secret values can only be used in select set of properties. For the
31+
list of properties, see [the CloudFormation Dynamic References documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.htm).

packages/@aws-cdk/aws-secretsmanager/lib/secret-string.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class SecretString extends cdk.DynamicReference {
4444
/**
4545
* Return the full value of the secret
4646
*/
47-
public get value(): string {
47+
public get stringValue(): string {
4848
return this.resolveStringForJsonKey('');
4949
}
5050

packages/@aws-cdk/aws-secretsmanager/lib/secret.ts

+26-7
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,21 @@ export interface ISecret extends cdk.IConstruct {
2020
readonly secretArn: string;
2121

2222
/**
23-
* Returns a SecretString corresponding to this secret, so that the secret value can be referred to from other parts
24-
* of the application (such as an RDS instance's master user password property).
23+
* Returns a SecretString corresponding to this secret.
24+
*
25+
* SecretString represents the value of the Secret.
26+
*/
27+
readonly secretString: SecretString;
28+
29+
/**
30+
* Retrieve the value of the Secret, as a string.
2531
*/
26-
toSecretString(): SecretString;
32+
readonly stringValue: string;
33+
34+
/**
35+
* Interpret the secret as a JSON object and return a field's value from it
36+
*/
37+
jsonFieldValue(key: string): string;
2738

2839
/**
2940
* Exports this secret.
@@ -97,7 +108,7 @@ export abstract class SecretBase extends cdk.Construct implements ISecret {
97108
public abstract readonly encryptionKey?: kms.IEncryptionKey;
98109
public abstract readonly secretArn: string;
99110

100-
private secretString?: SecretString;
111+
private _secretString?: SecretString;
101112

102113
public abstract export(): SecretImportProps;
103114

@@ -127,9 +138,17 @@ export abstract class SecretBase extends cdk.Construct implements ISecret {
127138
}
128139
}
129140

130-
public toSecretString() {
131-
this.secretString = this.secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn });
132-
return this.secretString;
141+
public get secretString() {
142+
this._secretString = this._secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn });
143+
return this._secretString;
144+
}
145+
146+
public get stringValue() {
147+
return this.secretString.stringValue;
148+
}
149+
150+
public jsonFieldValue(key: string): string {
151+
return this.secretString.jsonFieldValue(key);
133152
}
134153
}
135154

Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1+
import iam = require('@aws-cdk/aws-iam');
12
import cdk = require('@aws-cdk/cdk');
23
import secretsmanager = require('../lib');
34

4-
const app = new cdk.App();
5-
const stack = new cdk.Stack(app, 'aws-cdk-rds-integ');
5+
class ExampleStack extends cdk.Stack {
6+
constructor(scope: cdk.App, id: string) {
7+
super(scope, id);
8+
9+
/// !show
10+
const loginSecret = new secretsmanager.SecretString(this, 'Secret', {
11+
secretId: 'SomeLogin'
12+
});
613

7-
/// !show
8-
const loginSecret = new secretsmanager.SecretString(stack, 'Secret', { secretId: 'SomeLogin', });
14+
new iam.User(this, 'User', {
15+
// Get the 'password' field from the secret that looks like
16+
// { "username": "XXXX", "password": "YYYY" }
17+
password: loginSecret.jsonFieldValue('password')
18+
});
19+
/// !hide
920

10-
// DO NOT ACTUALLY DO THIS, as this will expose your secret.
11-
// This code only exists to show how the secret would be used.
12-
new cdk.Output(stack, 'SecretUsername', { value: loginSecret.jsonFieldValue('username') });
13-
new cdk.Output(stack, 'SecretPassword', { value: loginSecret.jsonFieldValue('password') });
14-
/// !hide
21+
}
22+
}
1523

24+
const app = new cdk.App();
25+
new ExampleStack(app, 'aws-cdk-secret-integ');
1626
app.run();

packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json

+19
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@
6060
"Properties": {
6161
"GenerateSecretString": {}
6262
}
63+
},
64+
"User00B015A1": {
65+
"Type": "AWS::IAM::User",
66+
"Properties": {
67+
"LoginProfile": {
68+
"Password": {
69+
"Fn::Join": [
70+
"",
71+
[
72+
"{{resolve:secretsmanager:",
73+
{
74+
"Ref": "SecretA720EF05"
75+
},
76+
":SecretString:::}}"
77+
]
78+
]
79+
}
80+
}
81+
}
6382
}
6483
}
6584
}

packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@ import iam = require('@aws-cdk/aws-iam');
22
import cdk = require('@aws-cdk/cdk');
33
import secretsManager = require('../lib');
44

5-
const app = new cdk.App();
6-
const stack = new cdk.Stack(app, 'Integ-SecretsManager-Secret');
7-
const role = new iam.Role(stack, 'TestRole', { assumedBy: new iam.AccountRootPrincipal() });
5+
class SecretsManagerStack extends cdk.Stack {
6+
constructor(scope: cdk.App, id: string) {
7+
super(scope, id);
8+
9+
const role = new iam.Role(this, 'TestRole', { assumedBy: new iam.AccountRootPrincipal() });
810

9-
/// !show
10-
const secret = new secretsManager.Secret(stack, 'Secret');
11-
secret.grantRead(role);
12-
/// !hide
11+
/// !show
12+
const secret = new secretsManager.Secret(this, 'Secret');
13+
secret.grantRead(role);
1314

15+
new iam.User(this, 'User', {
16+
password: secret.stringValue
17+
});
18+
/// !hide
19+
}
20+
}
21+
22+
const app = new cdk.App();
23+
new SecretsManagerStack(app, 'Integ-SecretsManager-Secret');
1424
app.run();

packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export = {
1313
});
1414

1515
// THEN
16-
test.equal(ref.node.resolve(ref.value), '{{resolve:secretsmanager:SomeSecret:SecretString:::}}');
16+
test.equal(ref.node.resolve(ref.stringValue), '{{resolve:secretsmanager:SomeSecret:SecretString:::}}');
1717

1818
test.done();
1919
},

packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export = {
224224
new cdk.Resource(stack, 'FakeResource', {
225225
type: 'CDK::Phony::Resource',
226226
properties: {
227-
value: secret.toSecretString().value
227+
value: secret.stringValue
228228
}
229229
});
230230

packages/@aws-cdk/aws-ssm/README.md

+17-6
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@ Import it into your code:
1414
import ssm = require('@aws-cdk/aws-ssm');
1515
```
1616

17-
### Creating SSM Parameters
18-
You can use either the `ssm.StringParameter` or `ssm.StringListParameter` (AWS CloudFormation does not support creating
19-
*Secret-String* SSM parameters, as those would require the secret value to be inlined in the template document) classes
20-
to register new SSM Parameters into your application:
17+
### Using existing SSM Parameters in your CDK app
18+
19+
You can reference existing SSM Parameter Store values that you want to use in
20+
your CDK app by using `ssm.ParameterStoreString`:
21+
22+
[using SSM parameter](test/integ.parameter-store-string.lit.ts)
23+
24+
### Creating new SSM Parameters in your CDK app
25+
26+
You can create either `ssm.StringParameter` or `ssm.StringListParameter`s in
27+
a CDK app. These are public (not secret) values. Parameters of type
28+
*SecretString* cannot be created directly from a CDK application; if you want
29+
to provision secrets automatically, use Secrets Manager Secrets (see the
30+
`@aws-cdk/aws-secretsmanager` package).
2131

2232
[creating SSM parameters](test/integ.parameter.lit.ts)
2333

24-
When specifying an `allowedPattern`, the values provided as string literals are validated against the pattern and an
25-
exception is raised if a value provided does not comply.
34+
When specifying an `allowedPattern`, the values provided as string literals
35+
are validated against the pattern and an exception is raised if a value
36+
provided does not comply.

packages/@aws-cdk/aws-ssm/lib/parameter-store-string.ts

+32-7
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,43 @@ export interface ParameterStoreStringProps {
1111

1212
/**
1313
* The version number of the value you wish to retrieve.
14+
*
15+
* @default The latest version will be retrieved.
1416
*/
15-
version: number;
17+
version?: number;
1618
}
1719

1820
/**
19-
* References a secret value in AWS Systems Manager Parameter Store
21+
* References a public value in AWS Systems Manager Parameter Store
2022
*
2123
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html
2224
*/
23-
export class ParameterStoreString extends cdk.DynamicReference {
25+
export class ParameterStoreString extends cdk.Construct {
26+
public readonly stringValue: string;
27+
2428
constructor(scope: cdk.Construct, id: string, props: ParameterStoreStringProps) {
25-
super(scope, id, {
26-
service: cdk.DynamicReferenceService.Ssm,
27-
referenceKey: `${props.parameterName}:${props.version}`,
28-
});
29+
super(scope, id);
30+
31+
// We use a different inner construct depend on whether we want the latest
32+
// or a specific version.
33+
//
34+
// * Latest - generate a Parameter and reference that.
35+
// * Specific - use a Dynamic Reference.
36+
if (props.version === undefined) {
37+
// Construct/get a singleton parameter under the stack
38+
const param = new cdk.Parameter(this, 'Parameter', {
39+
type: 'AWS::SSM::Parameter::Value<String>',
40+
default: props.parameterName
41+
});
42+
this.stringValue = param.stringValue;
43+
} else {
44+
// Use a dynamic reference
45+
const dynRef = new cdk.DynamicReference(this, 'Reference', {
46+
service: cdk.DynamicReferenceService.Ssm,
47+
referenceKey: `${props.parameterName}:${props.version}`,
48+
});
49+
this.stringValue = dynRef.stringValue;
50+
}
2951
}
3052
}
3153

@@ -47,6 +69,9 @@ export interface ParameterStoreSecureStringProps {
4769
/**
4870
* References a secret value in AWS Systems Manager Parameter Store
4971
*
72+
* It is not possible to retrieve the "latest" value of a secret.
73+
* Use Secrets Manager if you need that ability.
74+
*
5075
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html
5176
*/
5277
export class ParameterStoreSecureString extends cdk.DynamicReference {

0 commit comments

Comments
 (0)