Skip to content

Commit f24a1ae

Browse files
authoredSep 29, 2021
feat(aws-ec2): userdata cfn-signal signal resource which is different than the attached resource (aws#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*
1 parent 6390cb5 commit f24a1ae

File tree

2 files changed

+90
-31
lines changed

2 files changed

+90
-31
lines changed
 

‎packages/@aws-cdk/aws-ec2/lib/cfn-init.ts

+32-14
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,19 @@ export class CloudFormationInit {
125125
// To identify the resources that have the metadata and where the signal
126126
// needs to be sent, we need { region, stackName, logicalId }
127127
let resourceLocator = `--region ${Aws.REGION} --stack ${Aws.STACK_NAME} --resource ${attachedResource.logicalId}`;
128+
const signalResource = attachOptions.signalResource?.logicalId ?? attachedResource.logicalId;
129+
let notifyResourceLocator = `--region ${Aws.REGION} --stack ${Aws.STACK_NAME} --resource ${signalResource}`;
128130

129131
// If specified in attachOptions, include arguments in cfn-init/cfn-signal commands
130132
if (attachOptions.includeUrl) {
131133
resourceLocator = `${resourceLocator} --url https://cloudformation.${Aws.REGION}.${Aws.URL_SUFFIX}`;
134+
notifyResourceLocator = `${notifyResourceLocator} --url https://cloudformation.${Aws.REGION}.${Aws.URL_SUFFIX}`;
132135
}
133136
if (attachOptions.includeRole) {
134137
resourceLocator = `${resourceLocator} --role ${attachOptions.instanceRole.roleName}`;
138+
notifyResourceLocator = `${notifyResourceLocator} --role ${attachOptions.instanceRole.roleName}`;
135139
}
140+
136141
const configSets = (attachOptions.configSets ?? ['default']).join(',');
137142
const printLog = attachOptions.printLog ?? true;
138143

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

144149
if (attachOptions.platform === OperatingSystemType.WINDOWS) {
145150
const errCode = attachOptions.ignoreFailures ? '0' : '$LASTEXITCODE';
146-
attachOptions.userData.addCommands(...[
147-
`cfn-init.exe -v ${resourceLocator} -c ${configSets}`,
148-
`cfn-signal.exe -e ${errCode} ${resourceLocator}`,
149-
...printLog ? ['type C:\\cfn\\log\\cfn-init.log'] : [],
150-
]);
151+
attachOptions.userData.addCommands(
152+
...[
153+
`cfn-init.exe -v ${resourceLocator} -c ${configSets}`,
154+
`cfn-signal.exe -e ${errCode} ${notifyResourceLocator}`,
155+
...(printLog ? ['type C:\\cfn\\log\\cfn-init.log'] : []),
156+
],
157+
);
151158
} else {
152159
const errCode = attachOptions.ignoreFailures ? '0' : '$?';
153-
attachOptions.userData.addCommands(...[
154-
// Run a subshell without 'errexit', so we can signal using the exit code of cfn-init
155-
'(',
156-
' set +e',
157-
` /opt/aws/bin/cfn-init -v ${resourceLocator} -c ${configSets}`,
158-
` /opt/aws/bin/cfn-signal -e ${errCode} ${resourceLocator}`,
159-
...printLog ? [' cat /var/log/cfn-init.log >&2'] : [],
160-
')',
161-
]);
160+
attachOptions.userData.addCommands(
161+
...[
162+
// Run a subshell without 'errexit', so we can signal using the exit code of cfn-init
163+
'(',
164+
' set +e',
165+
` /opt/aws/bin/cfn-init -v ${resourceLocator} -c ${configSets}`,
166+
` /opt/aws/bin/cfn-signal -e ${errCode} ${notifyResourceLocator}`,
167+
...(printLog ? [' cat /var/log/cfn-init.log >&2'] : []),
168+
')',
169+
],
170+
);
162171
}
163172
}
164173

@@ -428,4 +437,13 @@ export interface AttachInitOptions {
428437
* @default false
429438
*/
430439
readonly ignoreFailures?: boolean;
440+
441+
/**
442+
* When provided, signals this resource instead of the attached resource
443+
*
444+
* You can use this to support signaling LaunchTemplate while attaching AutoScalingGroup
445+
*
446+
* @default - if this property is undefined cfn-signal signals the attached resource
447+
*/
448+
readonly signalResource?: CfnResource;
431449
}

‎packages/@aws-cdk/aws-ec2/test/cfn-init.test.ts

+58-17
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let stack: Stack;
1414
let instanceRole: iam.Role;
1515
let resource: CfnResource;
1616
let linuxUserData: ec2.UserData;
17+
let signalResource: CfnResource;
1718

1819
function resetState() {
1920
resetStateWithSynthesizer();
@@ -31,6 +32,9 @@ function resetStateWithSynthesizer(customSynthesizer?: IStackSynthesizer) {
3132
resource = new CfnResource(stack, 'Resource', {
3233
type: 'CDK::Test::Resource',
3334
});
35+
signalResource = new CfnResource(stack, 'SignalResource', {
36+
type: 'CDK::Test::Resource',
37+
});
3438
linuxUserData = ec2.UserData.forLinux();
3539
};
3640

@@ -135,22 +139,54 @@ describe('userdata', () => {
135139
);
136140
});
137141

138-
test('linux userdata contains right commands', () => {
139-
// WHEN
140-
simpleInit.attach(resource, linuxOptions());
141-
142-
// THEN
142+
function linuxUserDataTest(signalLogicalId: string) {
143143
const lines = linuxUserData.render().split('\n');
144144
expectLine(lines, cmdArg('cfn-init', `--region ${Aws.REGION}`));
145145
expectLine(lines, cmdArg('cfn-init', `--stack ${Aws.STACK_NAME}`));
146146
expectLine(lines, cmdArg('cfn-init', `--resource ${resource.logicalId}`));
147147
expectLine(lines, cmdArg('cfn-init', '-c default'));
148148
expectLine(lines, cmdArg('cfn-signal', `--region ${Aws.REGION}`));
149149
expectLine(lines, cmdArg('cfn-signal', `--stack ${Aws.STACK_NAME}`));
150-
expectLine(lines, cmdArg('cfn-signal', `--resource ${resource.logicalId}`));
150+
expectLine(lines, cmdArg('cfn-signal', `--resource ${signalLogicalId}`));
151151
expectLine(lines, cmdArg('cfn-signal', '-e $?'));
152152
expectLine(lines, cmdArg('cat', 'cfn-init.log'));
153153
expectLine(lines, /fingerprint/);
154+
}
155+
156+
function windowsUserDataTest(
157+
windowsUserData: ec2.UserData,
158+
signalLogicalId: string,
159+
) {
160+
const lines = windowsUserData.render().split('\n');
161+
expectLine(lines, cmdArg('cfn-init', `--region ${Aws.REGION}`));
162+
expectLine(lines, cmdArg('cfn-init', `--stack ${Aws.STACK_NAME}`));
163+
expectLine(lines, cmdArg('cfn-init', `--resource ${resource.logicalId}`));
164+
expectLine(lines, cmdArg('cfn-init', '-c default'));
165+
expectLine(lines, cmdArg('cfn-signal', `--region ${Aws.REGION}`));
166+
expectLine(lines, cmdArg('cfn-signal', `--stack ${Aws.STACK_NAME}`));
167+
expectLine(lines, cmdArg('cfn-signal', `--resource ${signalLogicalId}`));
168+
expectLine(lines, cmdArg('cfn-signal', '-e $LASTEXITCODE'));
169+
expectLine(lines, cmdArg('type', 'cfn-init.log'));
170+
expectLine(lines, /fingerprint/);
171+
}
172+
173+
test('linux userdata contains right commands', () => {
174+
// WHEN
175+
simpleInit.attach(resource, linuxOptions());
176+
177+
// THEN
178+
linuxUserDataTest(resource.logicalId);
179+
});
180+
181+
test('linux userdata contains right commands with different signal resource', () => {
182+
// WHEN
183+
simpleInit.attach(resource, {
184+
...linuxOptions(),
185+
signalResource,
186+
});
187+
188+
// THEN
189+
linuxUserDataTest(signalResource.logicalId);
154190
});
155191

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

194230
// THEN
195-
const lines = windowsUserData.render().split('\n');
196-
expectLine(lines, cmdArg('cfn-init', `--region ${Aws.REGION}`));
197-
expectLine(lines, cmdArg('cfn-init', `--stack ${Aws.STACK_NAME}`));
198-
expectLine(lines, cmdArg('cfn-init', `--resource ${resource.logicalId}`));
199-
expectLine(lines, cmdArg('cfn-init', '-c default'));
200-
expectLine(lines, cmdArg('cfn-signal', `--region ${Aws.REGION}`));
201-
expectLine(lines, cmdArg('cfn-signal', `--stack ${Aws.STACK_NAME}`));
202-
expectLine(lines, cmdArg('cfn-signal', `--resource ${resource.logicalId}`));
203-
expectLine(lines, cmdArg('cfn-signal', '-e $LASTEXITCODE'));
204-
expectLine(lines, cmdArg('type', 'cfn-init.log'));
205-
expectLine(lines, /fingerprint/);
231+
windowsUserDataTest(windowsUserData, resource.logicalId);
232+
});
233+
234+
test('Windows userdata contains right commands with different signal resource', () => {
235+
// WHEN
236+
const windowsUserData = ec2.UserData.forWindows();
237+
238+
simpleInit.attach(resource, {
239+
platform: ec2.OperatingSystemType.WINDOWS,
240+
instanceRole,
241+
userData: windowsUserData,
242+
signalResource,
243+
});
244+
245+
// THEN
246+
windowsUserDataTest(windowsUserData, signalResource.logicalId);
206247
});
207248

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

0 commit comments

Comments
 (0)
Please sign in to comment.