Skip to content
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(ssm): allow referencing "latest" version of SSM parameter #1768

Merged
merged 4 commits into from
Feb 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer this to be called “toSecretString” because it’s not a property of the secret, but rather a conversion method.

(Like toPipelineAction)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer this to be called “toSecretString” because it’s not a property of the secret

I disagree. It do see it as a property access (like .secretArn, but instead we access the VALUE of the secret). It just so happens that the value is of a complex type instead of a primitive (like Date).

It does not create a new resource, nor convert to anything that's used anywhere else in the construct tree. From this complex value object, you're still supposed to take stringValue or jsonFieldValue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, makes sense. Thanks for the explanation. What threw me off was the sugar methods stringValue and jsonFieldValue.

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