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

SSM - parameter path and value changes are not updated in the stack. #7722

Open
brettswift opened this issue Apr 30, 2020 · 11 comments
Open
Labels
@aws-cdk/aws-ssm Related to AWS Systems Manager bug This issue is a bug. effort/small Small work item – less than a day of effort p2

Comments

@brettswift
Copy link

brettswift commented Apr 30, 2020

There are two ways of resolving ssm parameters (as long as it isn't a secure parameter):

  1. ssm.StringParameter.valueForStringParameter
  2. ssm.StringParameter.fromStringParameterAttributes

They behave differently, but according to the documentation I would expect them to behave the same.

  1. Test by changing paths
    ie: change the SSM path from one deployment to the next.
    Retrieving by value does what I expect - it updates values when the path changes.
    Retrieving by attributes does not. The value in the stack will not change.

  2. Test by changing values
    it doesn't matter what we change, the values in the stack do not change.

Reproduction Steps

https://github.com/brettswift/cdk-ssm-test

follow the README.md

shell scripts are there to demonstrate everything.

Environment

  • **CLI Version :**1.36.1
  • Framework Version:
  • **OS :**OSX
  • **Language :**typescript

This is 🐛 Bug Report

@brettswift brettswift added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Apr 30, 2020
@SomayaB SomayaB added the @aws-cdk/aws-ssm Related to AWS Systems Manager label Apr 30, 2020
@MrArnoldPalmer MrArnoldPalmer added p1 and removed needs-triage This issue or PR still needs to be triaged. labels May 4, 2020
@brettswift
Copy link
Author

I'm seeing differing behaviour with the valueForStringParameter function, that is working in the sample provided vs my actual stack.

I've changed the value in SSM, but do not see any change when I deploy.

@joraycorn
Copy link

The same happened to me.
I'm trying to save a new version of my layerArn into SSM. While my layer updates, SSM valueForStringParameter doesn't appear to change at all for my other stacks.

CDK diff is telling me that my layer gets replaced, but is telling me that none of my other stacks is being updated.

Any idea of what's going on here ?

@MrArnoldPalmer MrArnoldPalmer added the effort/medium Medium work item – several days of effort label Aug 17, 2020
@cynicaljoy
Copy link

This is because the values are being stored in your cdk.context.json -- if you clear the context, which you can either clear the entire context cdk context --clear or you can reset by key cdk context --reset <key> then the latest value should be loaded the next time you cdk synth

@Seamus1989
Copy link

doesnt work for me

@corymhall
Copy link
Contributor

After testing this the only scenario that I could reproduce was the first one - changing the parameterName. It looks like this is due to the way CloudFormation (and CDK) handles parameters, i.e. by default cdk deploy uses --previous-parameters which tells CloudFormation to use the previous parameter value (the parameter name in this case).

When you use valueForStringParameter CDK generates a logicalId that includes the parameterName. So when you change the parameter name the logicalId changes which causes CloudFormation to see it is a different (new) resource and it fetches the new value.
https://github.com/aws/aws-cdk/blob/main/packages/@aws-cdk/aws-ssm/lib/parameter.ts#L441-L441

When you use fromStringParameterAttributes the logicalId is set to whatever you provide as the construct id. When the Default for a parameter of type AWS::SSM::Parameter::Value<String> changes, it does not cause CloudFormation to fetch the new value, because of the --previous-parameters options. If you instead run cdk deploy --no-previous-parameters you should see the new value being used.

There are a couple of workarounds that you can use.

  1. Run cdk deploy with --no-previous-parameters when you update the parameter name.
  2. When you change the parameter name, also change the construct id to have CloudFormation treat it as a different resource.

A permanent solution to this may be to update the fromStringParameterAttributes to generate a logicalId the say way that valueForStringParameter does.

@corymhall corymhall added effort/small Small work item – less than a day of effort and removed effort/medium Medium work item – several days of effort labels Aug 8, 2022
@corymhall corymhall self-assigned this Aug 8, 2022
@ayozemr
Copy link

ayozemr commented Sep 2, 2022

Run cdk deploy with --no-previous-parameters when you update the parameter name.

OMG didnt know about this and saved my day... Thanks!

@corymhall corymhall removed their assignment Oct 18, 2022
@0xdevalias
Copy link
Contributor

0xdevalias commented Jul 17, 2023

A permanent solution to this may be to update the fromStringParameterAttributes to generate a logicalId the say way that valueForStringParameter does.

Would be awesome if CDK could be updated with better default values here.. as currently this is an issue I have run into multiple times, and it's always one that I forget about until I have to deep dive into why things aren't working as expected. It's definitely not obvious/expected behaviour IMO.


Edit: Also.. where are those values that using --no-previous-parameters resets cached; as they don't appear to be in my cdk.context.json file at all?


Edit 2: Haven't checked this out fully yet, but sounds promising:

  • https://docs.aws.amazon.com/cdk/v2/guide/context.html#context_construct
    • Context values can be provided to your AWS CDK app in six different ways:

      • Automatically from the current AWS account.
      • Through the --context option to the cdk command. (These values are always strings.)
      • In the project's cdk.context.json file.
      • In the context key of the project's cdk.json file.
      • In the context key of your ~/.cdk.json file.
      • In your AWS CDK app using the construct.node.setContext() method.
  • https://docs.aws.amazon.com/cdk/v2/guide/context.html#context_methods
    • The following are the context methods:

      • ..snip..
      • StringParameter.valueFromLookup: Gets a value from the current Region's Amazon EC2 Systems Manager Parameter Store.
  • https://docs.aws.amazon.com/cdk/v2/guide/context.html#context_viewing
    • Viewing and managing context
      Use the cdk context command to view and manage the information in your cdk.context.json file. To see this information, use the cdk context command without any options.

    • To remove a context value, run cdk context --reset, specifying the value's corresponding key or number.

    • To clear all of the stored context values for your app, run cdk context --clear

    • Only context values stored in cdk.context.json can be reset or cleared. The AWS CDK does not touch other context values. Therefore, to protect a context value from being reset using these commands, you might copy the value to cdk.json

  • https://docs.aws.amazon.com/cdk/v2/guide/context.html#context_example
    • You can use cdk diff to see the effects of passing in a context value on the command line: eg. cdk diff -c vpcid=vpc-0cb9c31031d0d3e22

Looking at cdk context --help, I have the values I expect from cdk.context.json, and the explicit values I set in cdk.json, but none of them seem to correlate to the paths/values I'm using with StringParameter.fromStringParameterAttributes.. so still not sure where those values are being cached/read from/etc.


Edit 3: Looking closer at the CDK docs for StringParameter.fromStringParameterAttributes(scope, id, attrs), and particularly StringParameterAttributes, there appears to be a forceDynamicReference option:

Looking for more information about dynamic references:

@0xdevalias
Copy link
Contributor

0xdevalias commented Jul 18, 2023

Edit 3: Looking closer at the CDK docs for StringParameter.fromStringParameterAttributes(scope, id, attrs), and particularly StringParameterAttributes, there appears to be a forceDynamicReference option:

Ha.. so apparently forceDynamicReference and the underlying functionality related to it is brand new as of CDK 2.87.0 (released ~2 weeks ago):

Previously, when we import a SSM parameter by ssm.StringParameter.fromStringParameterAttributes, we use CfnParameter to get the value.

  "Parameters": {
    "importsqsstringparamParameter": {
      "Type": "AWS::SSM::Parameter::Value<String>",
      "Default": {
        "Fn::ImportValue": "some-exported-value-holding-the-param-name"
      }
    },

However, Parameters.<Name>.Default only allows a concrete string value. If it contains e.g. intrinsic functions, we get an error like this from CFn: Template format error: Every Default member must be a string.

This PR changes the behavior of fromStringParameterAttributes method. Now it uses CfnDynamicReference instead of CfnParameter if a parameter name contains unresolved tokens.

Originally posted by @tmokmss in #25749

Another thing we can say about ssm parameters is that it doesn't differ much between CfnParameters and dynamic references. Reading through the document, it seems that most of the characteristics are the same, such as when it's resolved and updated, where it can be used in a template, etc. There are of course some differences e.g. max num of references (200 vs 60), but they seems trivial.

Originally posted by @tmokmss in #25749 (comment)

I'm now wondering whether switching a parameter to a dynamic reference should really be considered as a breaking change. As far as I read the docs, there seems to be no remarkable difference between them. Given it's also very rare to use a lazy token for parameter names, we can tolerate the change, maybe under a feature flag.

Originally posted by @tmokmss in #25749 (comment)

If only we could stop using CfnParameter and use CfnDynamicReference instead for all cases... I see no point to use CfnParameter here. (Actually, isn't that the purpose of feature flags?)

Originally posted by @tmokmss in #25749 (comment)

There are some limitations #22239 (comment)

Originally posted by @corymhall in #25749 (comment)

So how about letting users choose which they use, parameter or dynamic reference? We'll add a property like forceDynamicReference?: boolean (default to false) to CommonStringParameterAttributes. This is kind of a leaky abstraction, but it should at least solve all the problem above. Plus we can easily ensure there is no breaking change, without adding any feature flag.

Originally posted by @tmokmss in #25749 (comment)


Playing with this new forceDynamicReference option, making this change:

  const stripeSecretApiKey = StringParameter.fromStringParameterAttributes(
    this,
    'StripeSecretApiKey',
    {
      parameterName: `${parameterStoreNamespace}/stripe/secret_api_key`,
+     forceDynamicReference: true,
    }
  ).stringValue

Resulted in this cdk diff output:

  Stack REDACTED

  Parameters
- [-] Parameter StripeSecretApiKeyParameter: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/REDACTED/development/stripe/secret_api_key"}

  Resources
  [~] AWS::Lambda::Function FnStripeCustomerSubscriptionEventsHandler/Handler FnStripeCustomerSubscriptionEventsHandlerB91CA9D9
   └─ [~] Environment
       └─ [~] .Variables:
           └─ [~] .stripeApiKey:
               └─ @@ -1,3 +1,1 @@
-                 [-] {
-                 [-]   "Ref": "StripeSecretApiKeyParameter"
-                 [-] }
+                 [+] "{{resolve:ssm:/REDACTED/development/stripe/secret_api_key}}"

For reference/comparison, changing the above StringParameter.fromStringParameterAttributes to use StringParameter.valueForStringParameter as follows:

const stripeSecretApiKey = StringParameter.valueForStringParameter(
  this,
  `${parameterStoreNamespace}/stripe/secret_api_key`
)

Resulted in this cdk diff output:

  Stack REDACTED

  Parameters
- [-] Parameter StripeSecretApiKeyParameter: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/REDACTED/development/stripe/secret_api_key"}
+ [+] Parameter SsmParameterValue:--REDACTED--development--stripe--secret_api_key:REDACTED.Parameter SsmParameterValueREDACTEDdevelopmentstripesecretapikeyREDACTEDParameter: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/REDACTED/development/stripe/secret_api_key"}

  Resources
  [~] AWS::Lambda::Function FnStripeCustomerSubscriptionEventsHandler/Handler FnStripeCustomerSubscriptionEventsHandlerB91CA9D9
   └─ [~] Environment
       └─ [~] .Variables:
           └─ [~] .stripeApiKey:
               └─ [~] .Ref:
-                  ├─ [-] StripeSecretApiKeyParameter
+                  └─ [+] SsmParameterValueREDACTEDdevelopmentstripesecretapikeyREDACTEDParameter

Exploring the CDK code to see exactly how --no-previous-parameters works:

We can see the option being parsed from the CLI as usePreviousParameters here:

https://github.com/aws/aws-cdk/blob/6c75581ae2b9537fa9d1d724b837fe81ae22d345/packages/aws-cdk/lib/cli.ts#L510C11-L510C32

We can see lib/api/deploy-stack.ts using the same usePreviousParameters option:

/**
* Use previous values for unspecified parameters
*
* If not set, all parameters must be specified for every deployment.
*
* @default false
*/
readonly usePreviousParameters?: boolean;

Which is used later on in that same file. When usePreviousParameters is true then templateParams.updateExisting is called, otherwise templateParams.supplyAll is called.

const assetParams = await addMetadataAssetsToManifest(stackArtifact, legacyAssets, options.toolkitInfo, options.reuseAssets);
const finalParameterValues = { ...options.parameters, ...assetParams };
const templateParams = TemplateParameters.fromTemplate(stackArtifact.template);
const stackParams = options.usePreviousParameters
? templateParams.updateExisting(finalParameterValues, cloudFormationStack.parameters)
: templateParams.supplyAll(finalParameterValues);

We can see the definitions of both of these functions here:

/**
* Calculate stack parameters to pass from the given desired parameter values
*
* Will throw if parameters without a Default value or a Previous value are not
* supplied.
*/
public supplyAll(updates: Record<string, string | undefined>): ParameterValues {
return new ParameterValues(this.params, updates);
}
/**
* From the template, the given desired values and the current values, calculate the changes to the stack parameters
*
* Will take into account parameters already set on the template (will emit
* 'UsePreviousValue: true' for those unless the value is changed), and will
* throw if parameters without a Default value or a Previous value are not
* supplied.
*/
public updateExisting(updates: Record<string, string | undefined>, previousValues: Record<string, string>): ParameterValues {
return new ParameterValues(this.params, updates, previousValues);
}

Which seems to describe how it tells CloudFormation to use the old values as:

Will take into account parameters already set on the template (will emit UsePreviousValue: true for those unless the value is changed), and will throw if parameters without a Default value or a Previous value are not supplied.

We can see the definition of the ParameterValues class here:

/**
* The set of parameters we're going to pass to a Stack
*/
export class ParameterValues {
public readonly values: Record<string, string> = {};
public readonly apiParameters: CloudFormation.Parameter[] = [];
constructor(
private readonly formalParams: Record<string, TemplateParameter>,
updates: Record<string, string | undefined>,
previousValues: Record<string, string> = {}) {
const missingRequired = new Array<string>();
for (const [key, formalParam] of Object.entries(this.formalParams)) {
// Check updates first, then use the previous value (if available), then use
// the default (if available).
//
// If we don't find a parameter value using any of these methods, then that's an error.
const updatedValue = updates[key];
if (updatedValue !== undefined) {
this.values[key] = updatedValue;
this.apiParameters.push({ ParameterKey: key, ParameterValue: updates[key] });
continue;
}
if (key in previousValues) {
this.values[key] = previousValues[key];
this.apiParameters.push({ ParameterKey: key, UsePreviousValue: true });
continue;
}
if (formalParam.Default !== undefined) {
this.values[key] = formalParam.Default;
continue;
}
// Oh no
missingRequired.push(key);
}
if (missingRequired.length > 0) {
throw new Error(`The following CloudFormation Parameters are missing a value: ${missingRequired.join(', ')}`);
}
// Just append all supplied overrides that aren't really expected (this
// will fail CFN but maybe people made typos that they want to be notified
// of)
const unknownParam = ([key, _]: [string, any]) => this.formalParams[key] === undefined;
const hasValue = ([_, value]: [string, any]) => !!value;
for (const [key, value] of Object.entries(updates).filter(unknownParam).filter(hasValue)) {
this.values[key] = value!;
this.apiParameters.push({ ParameterKey: key, ParameterValue: value });
}
}
/**
* Whether this set of parameter updates will change the actual stack values
*/
public hasChanges(currentValues: Record<string, string>): ParameterChanges {
// If any of the parameters are SSM parameters, deploying must always happen
// because we can't predict what the values will be. We will allow some
// parameters to opt out of this check by having a magic string in their description.
if (Object.values(this.formalParams).some(p => p.Type.startsWith('AWS::SSM::Parameter::') && !p.Description?.includes(SSMPARAM_NO_INVALIDATE))) {
return 'ssm';
}
// Otherwise we're dirty if:
// - any of the existing values are removed, or changed
if (Object.entries(currentValues).some(([key, value]) => !(key in this.values) || value !== this.values[key])) {
return true;
}
// - any of the values we're setting are new
if (Object.keys(this.values).some(key => !(key in currentValues))) {
return true;
}
return false;
}
}
export type ParameterChanges = boolean | 'ssm';

Of particular note/interest is the hasChanges function in that class:

/**
* Whether this set of parameter updates will change the actual stack values
*/
public hasChanges(currentValues: Record<string, string>): ParameterChanges {
// If any of the parameters are SSM parameters, deploying must always happen
// because we can't predict what the values will be. We will allow some
// parameters to opt out of this check by having a magic string in their description.
if (Object.values(this.formalParams).some(p => p.Type.startsWith('AWS::SSM::Parameter::') && !p.Description?.includes(SSMPARAM_NO_INVALIDATE))) {
return 'ssm';
}
// Otherwise we're dirty if:
// - any of the existing values are removed, or changed
if (Object.entries(currentValues).some(([key, value]) => !(key in this.values) || value !== this.values[key])) {
return true;
}
// - any of the values we're setting are new
if (Object.keys(this.values).some(key => !(key in currentValues))) {
return true;
}
return false;
}

Which suggests that:

If any of the parameters are SSM parameters, deploying must always happen because we can't predict what the values will be. We will allow some parameters to opt out of this check by having a magic string in their description.

The magic string is denoted by SSMPARAM_NO_INVALIDATE, which is defined in aws-cdk-lib/cx-api:

/**
* This SSM parameter does not invalidate the template
*
* If this string occurs in the description of an SSM parameter, the CLI
* will not assume that the stack must always be redeployed.
*/
export const SSMPARAM_NO_INVALIDATE = '[cdk:skip]';

The hasChanges function is called in lib/api/deploy-stack.ts:

if (await canSkipDeploy(options, cloudFormationStack, stackParams.hasChanges(cloudFormationStack.parameters))) {

And is passed to canSkipDeploy as the parameterChanges param:

/**
* Checks whether we can skip deployment
*
* We do this in a complicated way by preprocessing (instead of just
* looking at the changeset), because if there are nested stacks involved
* the changeset will always show the nested stacks as needing to be
* updated, and the deployment will take a long time to in effect not
* do anything.
*/
async function canSkipDeploy(
deployStackOptions: DeployStackOptions,
cloudFormationStack: CloudFormationStack,
parameterChanges: ParameterChanges): Promise<boolean> {

Which will force a deploy when ssm parameters are used:

https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/deploy-stack.ts#L735-L743

Which unfortunately seems to suggest that the behaviour for how this is actually resolved at deploy time looks like it is defined somewhere else.. either in CloudFormation itself, or perhaps somewhere in the CDK bootstrapper/related code/similar; not too sure.


Looking deeper into how CDK template diffing works:

The main diffTemplate function is defined here:

/**
* Compare two CloudFormation templates and return semantic differences between them.
*
* @param currentTemplate the current state of the stack.
* @param newTemplate the target state of the stack.
*
* @returns a +types.TemplateDiff+ object that represents the changes that will happen if
* a stack which current state is described by +currentTemplate+ is updated with
* the template +newTemplate+.
*/
export function diffTemplate(currentTemplate: { [key: string]: any }, newTemplate: { [key: string]: any }): types.TemplateDiff {
// Base diff
const theDiff = calculateTemplateDiff(currentTemplate, newTemplate);
// We're going to modify this in-place
const newTemplateCopy = deepCopy(newTemplate);
let didPropagateReferenceChanges;
let diffWithReplacements;
do {
diffWithReplacements = calculateTemplateDiff(currentTemplate, newTemplateCopy);
// Propagate replacements for replaced resources
didPropagateReferenceChanges = false;
if (diffWithReplacements.resources) {
diffWithReplacements.resources.forEachDifference((logicalId, change) => {
if (change.changeImpact === types.ResourceImpact.WILL_REPLACE) {
if (propagateReplacedReferences(newTemplateCopy, logicalId)) {
didPropagateReferenceChanges = true;
}
}
});
}
} while (didPropagateReferenceChanges);
// Copy "replaced" states from `diffWithReplacements` to `theDiff`.
diffWithReplacements.resources
.filter(r => isReplacement(r!.changeImpact))
.forEachDifference((logicalId, downstreamReplacement) => {
const resource = theDiff.resources.get(logicalId);
if (resource.changeImpact !== downstreamReplacement.changeImpact) {
propagatePropertyReplacement(downstreamReplacement, resource);
}
});
return theDiff;
}

Which then seems to call calculateTemplateDiff(currentTemplate, newTemplate), which is defined here:

function calculateTemplateDiff(currentTemplate: { [key: string]: any }, newTemplate: { [key: string]: any }): types.TemplateDiff {
const differences: types.ITemplateDiff = {};
const unknown: { [key: string]: types.Difference<any> } = {};
for (const key of unionOf(Object.keys(currentTemplate), Object.keys(newTemplate)).sort()) {
const oldValue = currentTemplate[key];
const newValue = newTemplate[key];
if (deepEqual(oldValue, newValue)) {
continue;
}
const handler: DiffHandler = DIFF_HANDLERS[key]
|| ((_diff, oldV, newV) => unknown[key] = impl.diffUnknown(oldV, newV));
handler(differences, oldValue, newValue);
}
if (Object.keys(unknown).length > 0) {
differences.unknown = new types.DifferenceCollection(unknown);
}
return new types.TemplateDiff(differences);
}

That seems to use one of various different DIFF_HANDLERS depending on the key being compared, falling back to a default diffUnknown if there is no more specific handler.

DIFF_HANDLERS is defined here:

const DIFF_HANDLERS: HandlerRegistry = {
AWSTemplateFormatVersion: (diff, oldValue, newValue) =>
diff.awsTemplateFormatVersion = impl.diffAttribute(oldValue, newValue),
Description: (diff, oldValue, newValue) =>
diff.description = impl.diffAttribute(oldValue, newValue),
Metadata: (diff, oldValue, newValue) =>
diff.metadata = new types.DifferenceCollection(diffKeyedEntities(oldValue, newValue, impl.diffMetadata)),
Parameters: (diff, oldValue, newValue) =>
diff.parameters = new types.DifferenceCollection(diffKeyedEntities(oldValue, newValue, impl.diffParameter)),
Mappings: (diff, oldValue, newValue) =>
diff.mappings = new types.DifferenceCollection(diffKeyedEntities(oldValue, newValue, impl.diffMapping)),
Conditions: (diff, oldValue, newValue) =>
diff.conditions = new types.DifferenceCollection(diffKeyedEntities(oldValue, newValue, impl.diffCondition)),
Transform: (diff, oldValue, newValue) =>
diff.transform = impl.diffAttribute(oldValue, newValue),
Resources: (diff, oldValue, newValue) =>
diff.resources = new types.DifferenceCollection(diffKeyedEntities(oldValue, newValue, impl.diffResource)),
Outputs: (diff, oldValue, newValue) =>
diff.outputs = new types.DifferenceCollection(diffKeyedEntities(oldValue, newValue, impl.diffOutput)),
};

It seems to use impl.diffParameter for the parameters, which is defined here:

export function diffParameter(oldValue: types.Parameter, newValue: types.Parameter): types.ParameterDifference {
return new types.ParameterDifference(oldValue, newValue);
}

And then calls types.ParameterDifference(oldValue, newValue), which is defined here:

export class ParameterDifference extends Difference<Parameter> {
// TODO: define specific difference attributes
}

And seems to just extend from Difference<Parameter>, which is defined here:

/**
* Models an entity that changed between two versions of a CloudFormation template.
*/
export class Difference<ValueType> implements IDifference<ValueType> {
/**
* Whether this is an actual different or the values are actually the same
*
* isDifferent => (isUpdate | isRemoved | isUpdate)
*/
public readonly isDifferent: boolean;
/**
* @param oldValue the old value, cannot be equal (to the sense of +deepEqual+) to +newValue+.
* @param newValue the new value, cannot be equal (to the sense of +deepEqual+) to +oldValue+.
*/
constructor(public readonly oldValue: ValueType | undefined, public readonly newValue: ValueType | undefined) {
if (oldValue === undefined && newValue === undefined) {
throw new AssertionError({ message: 'oldValue and newValue are both undefined!' });
}
this.isDifferent = !deepEqual(oldValue, newValue);
}
/** @returns +true+ if the element is new to the template. */
public get isAddition(): boolean {
return this.oldValue === undefined;
}
/** @returns +true+ if the element was removed from the template. */
public get isRemoval(): boolean {
return this.newValue === undefined;
}
/** @returns +true+ if the element was already in the template and is updated. */
public get isUpdate(): boolean {
return this.oldValue !== undefined
&& this.newValue !== undefined;
}
}

Which then uses deepEqual, which is defined here:

/**
* Compares two objects for equality, deeply. The function handles arguments that are
* +null+, +undefined+, arrays and objects. For objects, the function will not take the
* object prototype into account for the purpose of the comparison, only the values of
* properties reported by +Object.keys+.
*
* If both operands can be parsed to equivalent numbers, will return true.
* This makes diff consistent with CloudFormation, where a numeric 10 and a literal "10"
* are considered equivalent.
*
* @param lvalue the left operand of the equality comparison.
* @param rvalue the right operand of the equality comparison.
*
* @returns +true+ if both +lvalue+ and +rvalue+ are equivalent to each other.
*/
export function deepEqual(lvalue: any, rvalue: any): boolean {
if (lvalue === rvalue) { return true; }
// CloudFormation allows passing strings into boolean-typed fields
if (((typeof lvalue === 'string' && typeof rvalue === 'boolean') ||
(typeof lvalue === 'boolean' && typeof rvalue === 'string')) &&
lvalue.toString() === rvalue.toString()) {
return true;
}
// allows a numeric 10 and a literal "10" to be equivalent;
// this is consistent with CloudFormation.
if ((typeof lvalue === 'string' || typeof rvalue === 'string') &&
safeParseFloat(lvalue) === safeParseFloat(rvalue)) {
return true;
}
if (typeof lvalue !== typeof rvalue) { return false; }
if (Array.isArray(lvalue) !== Array.isArray(rvalue)) { return false; }
if (Array.isArray(lvalue) /* && Array.isArray(rvalue) */) {
if (lvalue.length !== rvalue.length) { return false; }
for (let i = 0 ; i < lvalue.length ; i++) {
if (!deepEqual(lvalue[i], rvalue[i])) { return false; }
}
return true;
}
if (typeof lvalue === 'object' /* && typeof rvalue === 'object' */) {
if (lvalue === null || rvalue === null) {
// If both were null, they'd have been ===
return false;
}
const keys = Object.keys(lvalue);
if (keys.length !== Object.keys(rvalue).length) { return false; }
for (const key of keys) {
if (!rvalue.hasOwnProperty(key)) { return false; }
if (key === 'DependsOn') {
if (!dependsOnEqual(lvalue[key], rvalue[key])) { return false; };
// check differences other than `DependsOn`
continue;
}
if (!deepEqual(lvalue[key], rvalue[key])) { return false; }
}
return true;
}
// Neither object, nor array: I deduce this is primitive type
// Primitive type and not ===, so I deduce not deepEqual
return false;
}

Going back to calculateTemplateDiff, once the raw differences are identified, it then passes them into types.TemplateDiff(differences), which is defined here:

/** Semantic differences between two CloudFormation templates. */
export class TemplateDiff implements ITemplateDiff {
public awsTemplateFormatVersion?: Difference<string>;
public description?: Difference<string>;
public transform?: Difference<string>;
public conditions: DifferenceCollection<Condition, ConditionDifference>;
public mappings: DifferenceCollection<Mapping, MappingDifference>;
public metadata: DifferenceCollection<Metadata, MetadataDifference>;
public outputs: DifferenceCollection<Output, OutputDifference>;
public parameters: DifferenceCollection<Parameter, ParameterDifference>;
public resources: DifferenceCollection<Resource, ResourceDifference>;
/** The differences in unknown/unexpected parts of the template */
public unknown: DifferenceCollection<any, Difference<any>>;

We can see that parameters ends up as DifferenceCollection<Parameter, ParameterDifference>, and is initialised in the constructor here:

this.parameters = args.parameters || new DifferenceCollection({});

@0xdevalias
Copy link
Contributor

Opened a tangentially related issue to allow --no-previous-parameters to be configured in cdk.json:

@WtfJoke
Copy link
Contributor

WtfJoke commented Nov 10, 2023

forceDynamicReference didnt helped in my case.

I used following workaround:

 const fetchAlwaysNewVersionId = `ImportedVersion-${Date.now()}}`;
    const codeObjectVersion = StringParameter.fromStringParameterAttributes(
      this,
      fetchAlwaysNewVersionId,
      {
        parameterName,
      }
    );

Since each synth the id is changed (due to usage of Date.now())), it will be refetched.

@pahud pahud added p2 and removed p1 labels Jun 11, 2024
@anentropic
Copy link

This is a terrible default behaviour!

If I'm making a deployment I never want to risk deploying with stale parameter values! I nearly messed up my production environment with this unexpected stupidity.

Is doing cdk deploy --no-previous-parameters still the correct answer? If I do cdk deploy --help that arg is not listed.

There is this one instead:

      --previous-parameters  Use previous values for existing parameters (you
                             must specify all parameters on every deployment if
                             this is disabled)         [boolean] [default: true]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-ssm Related to AWS Systems Manager bug This issue is a bug. effort/small Small work item – less than a day of effort p2
Projects
None yet
Development

No branches or pull requests