Skip to content

Commit

Permalink
feat(aws-ec2): userdata cfn-signal signal resource which is different…
Browse files Browse the repository at this point in the history
… than the attached resource (#16264)

When using `cfn-init` in EC2 UserData with a LaunchTemplate and an AutoScalingGroup the attached resource (used with `cfn-init`) is the LaunchTemplate but the signal (`cfn-signal`) should call the AutoScalingGroup.

`CloudFormationInit::attach` method supports only a single resource for both init and signal - blocking the usage of LaunchTemplate and AutoScalingGroup scenario.
The commit avoids a breaking change in the function signature, by adding an optional `signalResource` to the `AttachInitOptions` parameters argument


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
assafkamil authored Sep 29, 2021
1 parent 6390cb5 commit f24a1ae
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 31 deletions.
46 changes: 32 additions & 14 deletions packages/@aws-cdk/aws-ec2/lib/cfn-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,19 @@ export class CloudFormationInit {
// To identify the resources that have the metadata and where the signal
// needs to be sent, we need { region, stackName, logicalId }
let resourceLocator = `--region ${Aws.REGION} --stack ${Aws.STACK_NAME} --resource ${attachedResource.logicalId}`;
const signalResource = attachOptions.signalResource?.logicalId ?? attachedResource.logicalId;
let notifyResourceLocator = `--region ${Aws.REGION} --stack ${Aws.STACK_NAME} --resource ${signalResource}`;

// If specified in attachOptions, include arguments in cfn-init/cfn-signal commands
if (attachOptions.includeUrl) {
resourceLocator = `${resourceLocator} --url https://cloudformation.${Aws.REGION}.${Aws.URL_SUFFIX}`;
notifyResourceLocator = `${notifyResourceLocator} --url https://cloudformation.${Aws.REGION}.${Aws.URL_SUFFIX}`;
}
if (attachOptions.includeRole) {
resourceLocator = `${resourceLocator} --role ${attachOptions.instanceRole.roleName}`;
notifyResourceLocator = `${notifyResourceLocator} --role ${attachOptions.instanceRole.roleName}`;
}

const configSets = (attachOptions.configSets ?? ['default']).join(',');
const printLog = attachOptions.printLog ?? true;

Expand All @@ -143,22 +148,26 @@ export class CloudFormationInit {

if (attachOptions.platform === OperatingSystemType.WINDOWS) {
const errCode = attachOptions.ignoreFailures ? '0' : '$LASTEXITCODE';
attachOptions.userData.addCommands(...[
`cfn-init.exe -v ${resourceLocator} -c ${configSets}`,
`cfn-signal.exe -e ${errCode} ${resourceLocator}`,
...printLog ? ['type C:\\cfn\\log\\cfn-init.log'] : [],
]);
attachOptions.userData.addCommands(
...[
`cfn-init.exe -v ${resourceLocator} -c ${configSets}`,
`cfn-signal.exe -e ${errCode} ${notifyResourceLocator}`,
...(printLog ? ['type C:\\cfn\\log\\cfn-init.log'] : []),
],
);
} else {
const errCode = attachOptions.ignoreFailures ? '0' : '$?';
attachOptions.userData.addCommands(...[
// Run a subshell without 'errexit', so we can signal using the exit code of cfn-init
'(',
' set +e',
` /opt/aws/bin/cfn-init -v ${resourceLocator} -c ${configSets}`,
` /opt/aws/bin/cfn-signal -e ${errCode} ${resourceLocator}`,
...printLog ? [' cat /var/log/cfn-init.log >&2'] : [],
')',
]);
attachOptions.userData.addCommands(
...[
// Run a subshell without 'errexit', so we can signal using the exit code of cfn-init
'(',
' set +e',
` /opt/aws/bin/cfn-init -v ${resourceLocator} -c ${configSets}`,
` /opt/aws/bin/cfn-signal -e ${errCode} ${notifyResourceLocator}`,
...(printLog ? [' cat /var/log/cfn-init.log >&2'] : []),
')',
],
);
}
}

Expand Down Expand Up @@ -428,4 +437,13 @@ export interface AttachInitOptions {
* @default false
*/
readonly ignoreFailures?: boolean;

/**
* When provided, signals this resource instead of the attached resource
*
* You can use this to support signaling LaunchTemplate while attaching AutoScalingGroup
*
* @default - if this property is undefined cfn-signal signals the attached resource
*/
readonly signalResource?: CfnResource;
}
75 changes: 58 additions & 17 deletions packages/@aws-cdk/aws-ec2/test/cfn-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let stack: Stack;
let instanceRole: iam.Role;
let resource: CfnResource;
let linuxUserData: ec2.UserData;
let signalResource: CfnResource;

function resetState() {
resetStateWithSynthesizer();
Expand All @@ -31,6 +32,9 @@ function resetStateWithSynthesizer(customSynthesizer?: IStackSynthesizer) {
resource = new CfnResource(stack, 'Resource', {
type: 'CDK::Test::Resource',
});
signalResource = new CfnResource(stack, 'SignalResource', {
type: 'CDK::Test::Resource',
});
linuxUserData = ec2.UserData.forLinux();
};

Expand Down Expand Up @@ -135,22 +139,54 @@ describe('userdata', () => {
);
});

test('linux userdata contains right commands', () => {
// WHEN
simpleInit.attach(resource, linuxOptions());

// THEN
function linuxUserDataTest(signalLogicalId: string) {
const lines = linuxUserData.render().split('\n');
expectLine(lines, cmdArg('cfn-init', `--region ${Aws.REGION}`));
expectLine(lines, cmdArg('cfn-init', `--stack ${Aws.STACK_NAME}`));
expectLine(lines, cmdArg('cfn-init', `--resource ${resource.logicalId}`));
expectLine(lines, cmdArg('cfn-init', '-c default'));
expectLine(lines, cmdArg('cfn-signal', `--region ${Aws.REGION}`));
expectLine(lines, cmdArg('cfn-signal', `--stack ${Aws.STACK_NAME}`));
expectLine(lines, cmdArg('cfn-signal', `--resource ${resource.logicalId}`));
expectLine(lines, cmdArg('cfn-signal', `--resource ${signalLogicalId}`));
expectLine(lines, cmdArg('cfn-signal', '-e $?'));
expectLine(lines, cmdArg('cat', 'cfn-init.log'));
expectLine(lines, /fingerprint/);
}

function windowsUserDataTest(
windowsUserData: ec2.UserData,
signalLogicalId: string,
) {
const lines = windowsUserData.render().split('\n');
expectLine(lines, cmdArg('cfn-init', `--region ${Aws.REGION}`));
expectLine(lines, cmdArg('cfn-init', `--stack ${Aws.STACK_NAME}`));
expectLine(lines, cmdArg('cfn-init', `--resource ${resource.logicalId}`));
expectLine(lines, cmdArg('cfn-init', '-c default'));
expectLine(lines, cmdArg('cfn-signal', `--region ${Aws.REGION}`));
expectLine(lines, cmdArg('cfn-signal', `--stack ${Aws.STACK_NAME}`));
expectLine(lines, cmdArg('cfn-signal', `--resource ${signalLogicalId}`));
expectLine(lines, cmdArg('cfn-signal', '-e $LASTEXITCODE'));
expectLine(lines, cmdArg('type', 'cfn-init.log'));
expectLine(lines, /fingerprint/);
}

test('linux userdata contains right commands', () => {
// WHEN
simpleInit.attach(resource, linuxOptions());

// THEN
linuxUserDataTest(resource.logicalId);
});

test('linux userdata contains right commands with different signal resource', () => {
// WHEN
simpleInit.attach(resource, {
...linuxOptions(),
signalResource,
});

// THEN
linuxUserDataTest(signalResource.logicalId);
});

test('linux userdata contains right commands when url and role included', () => {
Expand Down Expand Up @@ -192,17 +228,22 @@ describe('userdata', () => {
});

// THEN
const lines = windowsUserData.render().split('\n');
expectLine(lines, cmdArg('cfn-init', `--region ${Aws.REGION}`));
expectLine(lines, cmdArg('cfn-init', `--stack ${Aws.STACK_NAME}`));
expectLine(lines, cmdArg('cfn-init', `--resource ${resource.logicalId}`));
expectLine(lines, cmdArg('cfn-init', '-c default'));
expectLine(lines, cmdArg('cfn-signal', `--region ${Aws.REGION}`));
expectLine(lines, cmdArg('cfn-signal', `--stack ${Aws.STACK_NAME}`));
expectLine(lines, cmdArg('cfn-signal', `--resource ${resource.logicalId}`));
expectLine(lines, cmdArg('cfn-signal', '-e $LASTEXITCODE'));
expectLine(lines, cmdArg('type', 'cfn-init.log'));
expectLine(lines, /fingerprint/);
windowsUserDataTest(windowsUserData, resource.logicalId);
});

test('Windows userdata contains right commands with different signal resource', () => {
// WHEN
const windowsUserData = ec2.UserData.forWindows();

simpleInit.attach(resource, {
platform: ec2.OperatingSystemType.WINDOWS,
instanceRole,
userData: windowsUserData,
signalResource,
});

// THEN
windowsUserDataTest(windowsUserData, signalResource.logicalId);
});

test('ignoreFailures disables result code reporting', () => {
Expand Down

0 comments on commit f24a1ae

Please sign in to comment.