Skip to content

Commit

Permalink
feat(ssm): allow referencing "latest" version of SSM parameter (#1768)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
rix0rrr authored Feb 16, 2019
1 parent 42876e7 commit 9af36af
Show file tree
Hide file tree
Showing 23 changed files with 299 additions and 85 deletions.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assets-docker/lib/image-asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class DockerImageAsset extends cdk.Construct {
this.node.addMetadata(cxapi.ASSET_METADATA, asset);

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

Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/assets/lib/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ export class Asset extends cdk.Construct {
description: `S3 key for asset version "${this.node.path}"`
});

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

this.bucket = s3.Bucket.import(this, 'AssetBucket', {
Expand Down
19 changes: 13 additions & 6 deletions packages/@aws-cdk/aws-secretsmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ const secretsmanager = require('@aws-cdk/aws-secretsmanager');
```

### Create a new Secret in a Stack

In order to have SecretsManager generate a new secret value automatically, you can get started with the following:
In order to have SecretsManager generate a new secret value automatically,
you can get started with the following:

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

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

If you need to use a pre-existing secret, the recommended way is to manually
provision the secret in *AWS SecretsManager* and use the `Secret.import`
method to make it available in your CDK Application:

```ts
const secret = Secret.import(scope, 'ImportedSecret', {
Expand All @@ -22,3 +26,6 @@ const secret = Secret.import(scope, 'ImportedSecret', {
encryptionKey,
});
```

SecretsManager secret values can only be used in select set of properties. For the
list of properties, see [the CloudFormation Dynamic References documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.htm).
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-secretsmanager/lib/secret-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class SecretString extends cdk.DynamicReference {
/**
* Return the full value of the secret
*/
public get value(): string {
public get stringValue(): string {
return this.resolveStringForJsonKey('');
}

Expand Down
33 changes: 26 additions & 7 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,21 @@ export interface ISecret extends cdk.IConstruct {
readonly secretArn: string;

/**
* Returns a SecretString corresponding to this secret, so that the secret value can be referred to from other parts
* of the application (such as an RDS instance's master user password property).
* Returns a SecretString corresponding to this secret.
*
* SecretString represents the value of the Secret.
*/
readonly secretString: SecretString;

/**
* Retrieve the value of the Secret, as a string.
*/
toSecretString(): SecretString;
readonly stringValue: string;

/**
* Interpret the secret as a JSON object and return a field's value from it
*/
jsonFieldValue(key: string): string;

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

private secretString?: SecretString;
private _secretString?: SecretString;

public abstract export(): SecretImportProps;

Expand Down Expand Up @@ -127,9 +138,17 @@ export abstract class SecretBase extends cdk.Construct implements ISecret {
}
}

public toSecretString() {
this.secretString = this.secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn });
return this.secretString;
public get secretString() {
this._secretString = this._secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn });
return this._secretString;
}

public get stringValue() {
return this.secretString.stringValue;
}

public jsonFieldValue(key: string): string {
return this.secretString.jsonFieldValue(key);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import secretsmanager = require('../lib');

const app = new cdk.App();
const stack = new cdk.Stack(app, 'aws-cdk-rds-integ');
class ExampleStack extends cdk.Stack {
constructor(scope: cdk.App, id: string) {
super(scope, id);

/// !show
const loginSecret = new secretsmanager.SecretString(this, 'Secret', {
secretId: 'SomeLogin'
});

/// !show
const loginSecret = new secretsmanager.SecretString(stack, 'Secret', { secretId: 'SomeLogin', });
new iam.User(this, 'User', {
// Get the 'password' field from the secret that looks like
// { "username": "XXXX", "password": "YYYY" }
password: loginSecret.jsonFieldValue('password')
});
/// !hide

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

const app = new cdk.App();
new ExampleStack(app, 'aws-cdk-secret-integ');
app.run();
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@
"Properties": {
"GenerateSecretString": {}
}
},
"User00B015A1": {
"Type": "AWS::IAM::User",
"Properties": {
"LoginProfile": {
"Password": {
"Fn::Join": [
"",
[
"{{resolve:secretsmanager:",
{
"Ref": "SecretA720EF05"
},
":SecretString:::}}"
]
]
}
}
}
}
}
}
24 changes: 17 additions & 7 deletions packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import secretsManager = require('../lib');

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

const role = new iam.Role(this, 'TestRole', { assumedBy: new iam.AccountRootPrincipal() });

/// !show
const secret = new secretsManager.Secret(stack, 'Secret');
secret.grantRead(role);
/// !hide
/// !show
const secret = new secretsManager.Secret(this, 'Secret');
secret.grantRead(role);

new iam.User(this, 'User', {
password: secret.stringValue
});
/// !hide
}
}

const app = new cdk.App();
new SecretsManagerStack(app, 'Integ-SecretsManager-Secret');
app.run();
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export = {
});

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

test.done();
},
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export = {
new cdk.Resource(stack, 'FakeResource', {
type: 'CDK::Phony::Resource',
properties: {
value: secret.toSecretString().value
value: secret.stringValue
}
});

Expand Down
23 changes: 17 additions & 6 deletions packages/@aws-cdk/aws-ssm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,23 @@ Import it into your code:
import ssm = require('@aws-cdk/aws-ssm');
```

### Creating SSM Parameters
You can use either the `ssm.StringParameter` or `ssm.StringListParameter` (AWS CloudFormation does not support creating
*Secret-String* SSM parameters, as those would require the secret value to be inlined in the template document) classes
to register new SSM Parameters into your application:
### Using existing SSM Parameters in your CDK app

You can reference existing SSM Parameter Store values that you want to use in
your CDK app by using `ssm.ParameterStoreString`:

[using SSM parameter](test/integ.parameter-store-string.lit.ts)

### Creating new SSM Parameters in your CDK app

You can create either `ssm.StringParameter` or `ssm.StringListParameter`s in
a CDK app. These are public (not secret) values. Parameters of type
*SecretString* cannot be created directly from a CDK application; if you want
to provision secrets automatically, use Secrets Manager Secrets (see the
`@aws-cdk/aws-secretsmanager` package).

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

When specifying an `allowedPattern`, the values provided as string literals are validated against the pattern and an
exception is raised if a value provided does not comply.
When specifying an `allowedPattern`, the values provided as string literals
are validated against the pattern and an exception is raised if a value
provided does not comply.
39 changes: 32 additions & 7 deletions packages/@aws-cdk/aws-ssm/lib/parameter-store-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,43 @@ export interface ParameterStoreStringProps {

/**
* The version number of the value you wish to retrieve.
*
* @default The latest version will be retrieved.
*/
version: number;
version?: number;
}

/**
* References a secret value in AWS Systems Manager Parameter Store
* References a public value in AWS Systems Manager Parameter Store
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html
*/
export class ParameterStoreString extends cdk.DynamicReference {
export class ParameterStoreString extends cdk.Construct {
public readonly stringValue: string;

constructor(scope: cdk.Construct, id: string, props: ParameterStoreStringProps) {
super(scope, id, {
service: cdk.DynamicReferenceService.Ssm,
referenceKey: `${props.parameterName}:${props.version}`,
});
super(scope, id);

// We use a different inner construct depend on whether we want the latest
// or a specific version.
//
// * Latest - generate a Parameter and reference that.
// * Specific - use a Dynamic Reference.
if (props.version === undefined) {
// Construct/get a singleton parameter under the stack
const param = new cdk.Parameter(this, 'Parameter', {
type: 'AWS::SSM::Parameter::Value<String>',
default: props.parameterName
});
this.stringValue = param.stringValue;
} else {
// Use a dynamic reference
const dynRef = new cdk.DynamicReference(this, 'Reference', {
service: cdk.DynamicReferenceService.Ssm,
referenceKey: `${props.parameterName}:${props.version}`,
});
this.stringValue = dynRef.stringValue;
}
}
}

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

0 comments on commit 9af36af

Please sign in to comment.