Skip to content

Commit

Permalink
feat(ssm): reference existing SSM list parameters (aws#21880)
Browse files Browse the repository at this point in the history
This PR fixes some issues with how SSM parameter types are implemented.
Currently this module models a single type of parameter (`ParameterType`
enum) and that type is used to represent _both_ [CloudFormation SSM
Parameter types](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types)
For example,

```ts
new cdk.CfnParameter(this, 'Param', {
  type: 'AWS::SSM::Parameter::Value<String>', // type
});
```

_and_ the [AWS::SSM::Parameter.type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#cfn-ssm-parameter-type)
For example,

```ts
new ssm.CfnParameter(this, 'Param', {
  type: 'String',
});
```

This overloading caused the issue in the referenced issue as well as
making it more confusing for the user. For example, You can specify a type when
creating a `StringParameter`, but you shouldn't need to since the only
valid values are `String | StringList` and these are modeled as two
separate classes `StringParameter & StringListParameter`.

To address this, the PR introduces a new enum `ParameterValueType` to
model the CloudFormation SSM Parameter Types. This enum is only used in
the `valueForXXX` and `fromXXX` methods since those return a CFN
parameter.

- Deprecated `ssm.StringParameter.valueForTypedStringParameter` since it
  uses the old overloaded `ParameterType`.
  - Introduce a new `ssm.StringParameter.valueForTypedStringParameterV2`
    that uses the new `ParameterValueType` enum
- Add `ssm.StringListParameter.valueForTypedListParameter`
- Add `ssm.StringListParameter.fromListParameterAttributes`
- Deprecated `StringParameterProps.type` since the value should only be
  `String`.

fix aws#12477, aws#14364


----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
corymhall authored and madeline-k committed Oct 10, 2022
1 parent 950ccd5 commit 9979d90
Show file tree
Hide file tree
Showing 18 changed files with 1,748 additions and 22 deletions.
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-ec2/lib/machine-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export class GenericSSMParameterImage implements IMachineImage {
* Return the image to use in the given context
*/
public getImage(scope: Construct): MachineImageConfig {
const ami = ssm.StringParameter.valueForTypedStringParameter(scope, this.parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID);
const ami = ssm.StringParameter.valueForTypedStringParameterV2(scope, this.parameterName, ssm.ParameterValueType.AWS_EC2_IMAGE_ID);
return {
imageId: ami,
osType: this.os,
Expand Down Expand Up @@ -732,5 +732,5 @@ export interface LookupMachineImageProps {
function lookupImage(scope: Construct, cachedInContext: boolean | undefined, parameterName: string) {
return cachedInContext
? ssm.StringParameter.valueFromLookup(scope, parameterName)
: ssm.StringParameter.valueForTypedStringParameter(scope, parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID);
: ssm.StringParameter.valueForTypedStringParameterV2(scope, parameterName, ssm.ParameterValueType.AWS_EC2_IMAGE_ID);
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ecs/lib/amis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,5 +369,5 @@ export class BottleRocketImage implements ec2.IMachineImage {
function lookupImage(scope: Construct, cachedInContext: boolean | undefined, parameterName: string) {
return cachedInContext
? ssm.StringParameter.valueFromLookup(scope, parameterName)
: ssm.StringParameter.valueForTypedStringParameter(scope, parameterName, ssm.ParameterType.AWS_EC2_IMAGE_ID);
: ssm.StringParameter.valueForTypedStringParameterV2(scope, parameterName, ssm.ParameterValueType.AWS_EC2_IMAGE_ID);
}
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-ssm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ your CDK app by using `ssm.StringParameter.fromStringParameterAttributes`:

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

You can also reference an existing SSM Parameter Store value that matches an
[AWS specific parameter type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-specific-parameter-types):

```ts
ssm.StringParameter.valueForTypedStringParameterV2(stack, '/My/Public/Parameter', ssm.ParameterValueType.AWS_EC2_IMAGE_ID);
```

To do the same for a SSM Parameter Store value that is stored as a list:

```ts
ssm.StringListParameter.valueForTypedListParameter(stack, '/My/Public/Parameter', ssm.ParameterValueType.AWS_EC2_IMAGE_ID);
```

### Lookup existing parameters

You can also use an existing parameter by looking up the parameter from the AWS environment.
Expand Down
204 changes: 186 additions & 18 deletions packages/@aws-cdk/aws-ssm/lib/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export interface StringParameterProps extends ParameterOptions {
* The type of the string parameter
*
* @default ParameterType.STRING
* @deprecated - type will always be 'String'
*/
readonly type?: ParameterType;

Expand Down Expand Up @@ -199,8 +200,75 @@ abstract class ParameterBase extends Resource implements IParameter {
}
}

/**
* The type of CFN SSM Parameter
*
* Using specific types can be helpful in catching invalid values
* at the start of creating or updating a stack. CloudFormation validates
* the values against existing values in the account.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types
*/
export enum ParameterValueType {
/**
* String
*/
STRING = 'String',

/**
* An Availability Zone, such as us-west-2a.
*/
AWS_EC2_AVAILABILITYZONE_NAME = 'AWS::EC2::AvailabilityZone::Name',

/**
* An Amazon EC2 image ID, such as ami-0ff8a91507f77f867.
*/
AWS_EC2_IMAGE_ID = 'AWS::EC2::Image::Id',

/**
* An Amazon EC2 instance ID, such as i-1e731a32.
*/
AWS_EC2_INSTANCE_ID = 'AWS::EC2::Instance::Id',

/**
* An Amazon EC2 key pair name.
*/
AWS_EC2_KEYPAIR_KEYNAME = 'AWS::EC2::KeyPair::KeyName',

/**
* An EC2-Classic or default VPC security group name, such as my-sg-abc.
*/
AWS_EC2_SECURITYGROUP_GROUPNAME = 'AWS::EC2::SecurityGroup::GroupName',

/**
* A security group ID, such as sg-a123fd85.
*/
AWS_EC2_SECURITYGROUP_ID = 'AWS::EC2::SecurityGroup::Id',

/**
* A subnet ID, such as subnet-123a351e.
*/
AWS_EC2_SUBNET_ID = 'AWS::EC2::Subnet::Id',

/**
* An Amazon EBS volume ID, such as vol-3cdd3f56.
*/
AWS_EC2_VOLUME_ID = 'AWS::EC2::Volume::Id',

/**
* A VPC ID, such as vpc-a123baa3.
*/
AWS_EC2_VPC_ID = 'AWS::EC2::VPC::Id',

/**
* An Amazon Route 53 hosted zone ID, such as Z23YXV4OVPL04A.
*/
AWS_ROUTE53_HOSTEDZONE_ID = 'AWS::Route53::HostedZone::Id',
}

/**
* SSM parameter type
* @deprecated these types are no longer used
*/
export enum ParameterType {
/**
Expand Down Expand Up @@ -302,8 +370,55 @@ export interface StringParameterAttributes extends CommonStringParameterAttribut
* The type of the string parameter
*
* @default ParameterType.STRING
* @deprecated - use valueType instead
*/
readonly type?: ParameterType;

/**
* The type of the string parameter value
*
* Using specific types can be helpful in catching invalid values
* at the start of creating or updating a stack. CloudFormation validates
* the values against existing values in the account.
*
* Note - if you want to allow values from different AWS accounts, use
* ParameterValueType.STRING
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types
*
* @default ParameterValueType.STRING
*/
readonly valueType?: ParameterValueType;
}

/**
* Attributes for parameters of string list type.
*
* @see ParameterType
*/
export interface ListParameterAttributes extends CommonStringParameterAttributes {
/**
* The version number of the value you wish to retrieve.
*
* @default The latest version will be retrieved.
*/
readonly version?: number;

/**
* The type of the string list parameter value.
*
* Using specific types can be helpful in catching invalid values
* at the start of creating or updating a stack. CloudFormation validates
* the values against existing values in the account.
*
* Note - if you want to allow values from different AWS accounts, use
* ParameterValueType.STRING
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html#aws-ssm-parameter-types
*
* @default ParameterValueType.STRING
*/
readonly elementType?: ParameterValueType;
}

/**
Expand Down Expand Up @@ -331,25 +446,10 @@ export interface SecureStringParameterAttributes extends CommonStringParameterAt
* @resource AWS::SSM::Parameter
*
* @example
*
* const ssmParameter = new ssm.StringParameter(this, 'mySsmParameter', {
* parameterName: 'mySsmParameter',
* stringValue: 'mySsmParameterValue',
* type: ssm.ParameterType.STRING,
* });
*
* const secureParameter = new ssm.StringParameter(this, 'mySecretParameter', {
* parameterName: 'mySecretParameter',
* stringValue: 'mySecretParameterValue',
* type: ssm.ParameterType.SECURE_STRING,
* });
*
* const listParameter = new ssm.StringParameter(this, 'myListParameter', {
* parameterName: 'myListParameter',
* stringValue: ["myListParameterValue1", "myListParameterValue2"],
* type: ssm.ParameterType.STRING_LIST,
* });
*
*/
export class StringParameter extends ParameterBase implements IStringParameter {

Expand All @@ -367,8 +467,11 @@ export class StringParameter extends ParameterBase implements IStringParameter {
if (!attrs.parameterName) {
throw new Error('parameterName cannot be an empty string');
}
if (attrs.type && ![ParameterType.STRING, ParameterType.AWS_EC2_IMAGE_ID].includes(attrs.type)) {
throw new Error(`fromStringParameterAttributes does not support ${attrs.type}. Please use ParameterType.STRING or ParameterType.AWS_EC2_IMAGE_ID`);
}

const type = attrs.type || ParameterType.STRING;
const type = attrs.type ?? attrs.valueType ?? ParameterValueType.STRING;

const stringValue = attrs.version
? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toString()
Expand All @@ -377,7 +480,7 @@ export class StringParameter extends ParameterBase implements IStringParameter {
class Import extends ParameterBase {
public readonly parameterName = attrs.parameterName;
public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName });
public readonly parameterType = type;
public readonly parameterType = ParameterType.STRING; // this is the type returned by CFN @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#aws-resource-ssm-parameter-return-values
public readonly stringValue = stringValue;
}

Expand Down Expand Up @@ -426,7 +529,24 @@ export class StringParameter extends ParameterBase implements IStringParameter {
* @param version The parameter version (recommended in order to ensure that the value won't change during deployment)
*/
public static valueForStringParameter(scope: Construct, parameterName: string, version?: number): string {
return StringParameter.valueForTypedStringParameter(scope, parameterName, ParameterType.STRING, version);
return StringParameter.valueForTypedStringParameterV2(scope, parameterName, ParameterValueType.STRING, version);
}

/**
* Returns a token that will resolve (during deployment) to the string value of an SSM string parameter.
* @param scope Some scope within a stack
* @param parameterName The name of the SSM parameter.
* @param type The type of the SSM parameter.
* @param version The parameter version (recommended in order to ensure that the value won't change during deployment)
*/
public static valueForTypedStringParameterV2(scope: Construct, parameterName: string, type = ParameterValueType.STRING, version?: number): string {
const stack = Stack.of(scope);
const id = makeIdentityForImportedValue(parameterName);
const exists = stack.node.tryFindChild(id) as IStringParameter;

if (exists) { return exists.stringValue; }

return this.fromStringParameterAttributes(stack, id, { parameterName, version, valueType: type }).stringValue;
}

/**
Expand All @@ -435,8 +555,13 @@ export class StringParameter extends ParameterBase implements IStringParameter {
* @param parameterName The name of the SSM parameter.
* @param type The type of the SSM parameter.
* @param version The parameter version (recommended in order to ensure that the value won't change during deployment)
* @deprecated - use valueForTypedStringParameterV2 instead
*/
public static valueForTypedStringParameter(scope: Construct, parameterName: string, type = ParameterType.STRING, version?: number): string {
if (type === ParameterType.STRING_LIST) {
throw new Error('valueForTypedStringParameter does not support STRING_LIST, '
+'use valueForTypedListParameter instead');
}
const stack = Stack.of(scope);
const id = makeIdentityForImportedValue(parameterName);
const exists = stack.node.tryFindChild(id) as IStringParameter;
Expand Down Expand Up @@ -528,6 +653,49 @@ export class StringListParameter extends ParameterBase implements IStringListPar
return new Import(scope, id);
}

/**
* Imports an external string list parameter with name and optional version.
*/
public static fromListParameterAttributes(scope: Construct, id: string, attrs: ListParameterAttributes): IStringListParameter {
if (!attrs.parameterName) {
throw new Error('parameterName cannot be an empty string');
}

const type = attrs.elementType ?? ParameterValueType.STRING;
const valueType = `List<${type}>`;

const stringValue = attrs.version
? new CfnDynamicReference(CfnDynamicReferenceService.SSM, `${attrs.parameterName}:${Tokenization.stringifyNumber(attrs.version)}`).toStringList()
: new CfnParameter(scope, `${id}.Parameter`, { type: `AWS::SSM::Parameter::Value<${valueType}>`, default: attrs.parameterName }).valueAsList;

class Import extends ParameterBase {
public readonly parameterName = attrs.parameterName;
public readonly parameterArn = arnForParameterName(this, attrs.parameterName, { simpleName: attrs.simpleName });
public readonly parameterType = valueType; // it doesn't really matter what this is since a CfnParameter can only be `String | StringList`
public readonly stringListValue = stringValue;
}

return new Import(scope, id);
}

/**
* Returns a token that will resolve (during deployment) to the list value of an SSM StringList parameter.
* @param scope Some scope within a stack
* @param parameterName The name of the SSM parameter.
* @param type the type of the SSM list parameter
* @param version The parameter version (recommended in order to ensure that the value won't change during deployment)
*/
public static valueForTypedListParameter(scope: Construct, parameterName: string, type = ParameterValueType.STRING, version?: number): string[] {
const stack = Stack.of(scope);
const id = makeIdentityForImportedValue(parameterName);
const exists = stack.node.tryFindChild(id) as IStringListParameter;

if (exists) { return exists.stringListValue; }

return this.fromListParameterAttributes(stack, id, { parameterName, elementType: type, version }).stringListValue;
}


public readonly parameterArn: string;
public readonly parameterName: string;
public readonly parameterType: string;
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ssm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@aws-cdk/assertions": "0.0.0",
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/integ-tests": "0.0.0",
"@aws-cdk/cfn2ts": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@types/jest": "^27.5.2",
Expand Down
Loading

0 comments on commit 9979d90

Please sign in to comment.