@fjall/components-infrastructure 39.0.1 → 39.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +20 -8
- package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.d.ts +11 -2
- package/dist/lib/resources/aws/compute/asgInlineLifecycleHook.js +12 -2
- package/dist/lib/resources/aws/compute/ec2.d.ts +10 -3
- package/dist/lib/resources/aws/compute/ec2.js +8 -4
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.d.ts +17 -5
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +55 -46
- package/dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs +422 -147
- package/dist/lib/resources/aws/compute/ecsCapacityProviderGate.d.ts +41 -0
- package/dist/lib/resources/aws/compute/ecsCapacityProviderGate.js +116 -0
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +5 -0
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +6 -5
- package/dist/lib/resources/aws/compute/lambda.d.ts +3 -0
- package/dist/lib/resources/aws/compute/lambda.js +10 -6
- package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +17 -15
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +27 -21
- package/dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs +235 -104
- package/dist/lib/resources/aws/database/clickhouseUserData.js +49 -6
- package/dist/lib/resources/aws/monitoring/metricNamespaces.d.ts +4 -1
- package/dist/lib/resources/aws/monitoring/metricNamespaces.js +4 -1
- package/package.json +3 -3
|
@@ -26,16 +26,25 @@
|
|
|
26
26
|
* Lambda-Errors alarm (a tag-uniqueness invariant violation; silently
|
|
27
27
|
* picking one risks attaching the wrong data and requires operator
|
|
28
28
|
* investigation).
|
|
29
|
-
* 3.
|
|
30
|
-
*
|
|
29
|
+
* 3. Wait for the volume to be released: poll DescribeVolumes until its
|
|
30
|
+
* State is "available", deadline RELEASE_DEADLINE_MS. Timeout →
|
|
31
|
+
* ABANDON; the ASG launches another instance and the wait starts over.
|
|
32
|
+
* 4. AttachVolume(Device: DEVICE_NAME, InstanceId, VolumeId). If EC2
|
|
33
|
+
* refuses because a concurrent delivery of the same event is already
|
|
34
|
+
* attaching the volume here, carry on to the attach wait.
|
|
35
|
+
* 5. Poll DescribeVolumes until State === "in-use" AND the matching
|
|
31
36
|
* Attachments[].State === "attached", deadline 90s. Timeout → ABANDON.
|
|
32
|
-
*
|
|
37
|
+
* 6. CompleteLifecycleAction(CONTINUE) on success. A token whose action
|
|
38
|
+
* has already ended is logged as
|
|
39
|
+
* `event: "lifecycle_action_already_ended"`, not retried.
|
|
33
40
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
41
|
+
* Nothing detaches the volume. The instance that holds it releases it when
|
|
42
|
+
* it terminates, after the TERMINATING handler
|
|
43
|
+
* (ec2GracefulTerminationLambda.source.cjs) has drained its tasks, so a
|
|
44
|
+
* detach can never pull the filesystem out from under a running database.
|
|
45
|
+
* This handler therefore waits for the release instead of forcing it.
|
|
46
|
+
* RELEASE_DEADLINE_MS covers the TERMINATING handler's drain and StopTask
|
|
47
|
+
* budgets plus the instance's shutdown; a test pins it to them.
|
|
39
48
|
*
|
|
40
49
|
* CommonJS rather than ESM because Code.fromInline lands the source as
|
|
41
50
|
* `index.js`, which Lambda treats as CommonJS by default.
|
|
@@ -51,18 +60,41 @@ const {
|
|
|
51
60
|
} = require("@aws-sdk/client-auto-scaling");
|
|
52
61
|
|
|
53
62
|
const ATTACH_POLL_INTERVAL_MS = 5_000;
|
|
54
|
-
//
|
|
55
|
-
// script waits 60s for /dev/xvdf so the hook MUST complete inside that.
|
|
63
|
+
// AWS volume attach is usually < 30s.
|
|
56
64
|
const ATTACH_DEADLINE_MS = 90_000;
|
|
65
|
+
const RELEASE_POLL_INTERVAL_MS = 10_000;
|
|
66
|
+
// The TERMINATING handler's drain (240 s) and StopTask (150 s) budgets, plus
|
|
67
|
+
// its network cleanup and the instance's OS shutdown.
|
|
68
|
+
const RELEASE_DEADLINE_MS = 480_000;
|
|
57
69
|
const TAG_OWNER_LOGICAL_ID = "fjall:OwnerLogicalId";
|
|
58
70
|
const TAG_LIFECYCLE = "fjall:Lifecycle";
|
|
59
71
|
const TAG_LIFECYCLE_VALUE = "data-volume";
|
|
60
72
|
const TAG_STACK_ID = "fjall:StackId";
|
|
73
|
+
// Bounds every AWS call, SDK retries included, so a hung request cannot carry
|
|
74
|
+
// an attempt past the Lambda's timeout.
|
|
75
|
+
const SDK_CALL_TIMEOUT_MS = 20_000;
|
|
76
|
+
const LIFECYCLE_EVENT = {
|
|
77
|
+
ALREADY_ENDED: "lifecycle_action_already_ended"
|
|
78
|
+
};
|
|
79
|
+
const LIFECYCLE_OUTCOME = {
|
|
80
|
+
COMPLETED: "completed",
|
|
81
|
+
ALREADY_ENDED: "already_ended"
|
|
82
|
+
};
|
|
61
83
|
|
|
62
84
|
function errMessage(err) {
|
|
63
85
|
return err && err.message ? err.message : String(err);
|
|
64
86
|
}
|
|
65
87
|
|
|
88
|
+
function send(client, command) {
|
|
89
|
+
return client.send(command, {
|
|
90
|
+
abortSignal: AbortSignal.timeout(SDK_CALL_TIMEOUT_MS)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function consoleLog(msg, fields) {
|
|
95
|
+
console.log(JSON.stringify({ message: msg, ...(fields || {}) }));
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
let _ec2;
|
|
67
99
|
let _asg;
|
|
68
100
|
function getEc2() {
|
|
@@ -106,7 +138,8 @@ function readEnv(env, name) {
|
|
|
106
138
|
}
|
|
107
139
|
|
|
108
140
|
async function findOwnedVolume(ec2Client, ownerLogicalId, stackId) {
|
|
109
|
-
const result = await
|
|
141
|
+
const result = await send(
|
|
142
|
+
ec2Client,
|
|
110
143
|
new DescribeVolumesCommand({
|
|
111
144
|
Filters: [
|
|
112
145
|
{ Name: `tag:${TAG_OWNER_LOGICAL_ID}`, Values: [ownerLogicalId] },
|
|
@@ -119,6 +152,57 @@ async function findOwnedVolume(ec2Client, ownerLogicalId, stackId) {
|
|
|
119
152
|
return volumes;
|
|
120
153
|
}
|
|
121
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Poll until the volume can be attached to `instanceId`. Resolves
|
|
157
|
+
* `{ released: true, attachedHere: false }` once it is "available",
|
|
158
|
+
* `{ released: true, attachedHere: true }` if an earlier delivery of this
|
|
159
|
+
* lifecycle action already attached it here, and `{ released: false, ... }`
|
|
160
|
+
* at the deadline. A failed read is retried until the deadline and named in
|
|
161
|
+
* the result, so a timeout caused by an unreadable volume says so.
|
|
162
|
+
*/
|
|
163
|
+
async function pollUntilReleased(
|
|
164
|
+
ec2Client,
|
|
165
|
+
volumeId,
|
|
166
|
+
instanceId,
|
|
167
|
+
sleepFn,
|
|
168
|
+
nowFn
|
|
169
|
+
) {
|
|
170
|
+
const deadline = nowFn() + RELEASE_DEADLINE_MS;
|
|
171
|
+
let lastState;
|
|
172
|
+
let lastError;
|
|
173
|
+
for (;;) {
|
|
174
|
+
try {
|
|
175
|
+
const desc = await send(
|
|
176
|
+
ec2Client,
|
|
177
|
+
new DescribeVolumesCommand({ VolumeIds: [volumeId] })
|
|
178
|
+
);
|
|
179
|
+
const volume =
|
|
180
|
+
desc.Volumes && desc.Volumes[0] ? desc.Volumes[0] : undefined;
|
|
181
|
+
lastError = undefined;
|
|
182
|
+
lastState = volume ? volume.State : undefined;
|
|
183
|
+
if (volume && volume.State === "available") {
|
|
184
|
+
return { released: true, attachedHere: false };
|
|
185
|
+
}
|
|
186
|
+
const attachedHere = (volume?.Attachments || []).some(
|
|
187
|
+
(a) => a.InstanceId === instanceId && a.State === "attached"
|
|
188
|
+
);
|
|
189
|
+
if (attachedHere) {
|
|
190
|
+
return { released: true, attachedHere: true };
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
lastError = errMessage(err);
|
|
194
|
+
}
|
|
195
|
+
if (nowFn() >= deadline) {
|
|
196
|
+
return {
|
|
197
|
+
released: false,
|
|
198
|
+
lastState,
|
|
199
|
+
...(lastError !== undefined && { error: lastError })
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
await sleepFn(Math.min(RELEASE_POLL_INTERVAL_MS, deadline - nowFn()));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
122
206
|
async function pollUntilAttached(
|
|
123
207
|
ec2Client,
|
|
124
208
|
volumeId,
|
|
@@ -127,8 +211,9 @@ async function pollUntilAttached(
|
|
|
127
211
|
nowFn
|
|
128
212
|
) {
|
|
129
213
|
const deadline = nowFn() + ATTACH_DEADLINE_MS;
|
|
130
|
-
|
|
131
|
-
const desc = await
|
|
214
|
+
for (;;) {
|
|
215
|
+
const desc = await send(
|
|
216
|
+
ec2Client,
|
|
132
217
|
new DescribeVolumesCommand({ VolumeIds: [volumeId] })
|
|
133
218
|
);
|
|
134
219
|
const volume =
|
|
@@ -141,9 +226,45 @@ async function pollUntilAttached(
|
|
|
141
226
|
return true;
|
|
142
227
|
}
|
|
143
228
|
}
|
|
144
|
-
|
|
229
|
+
if (nowFn() >= deadline) return false;
|
|
230
|
+
await sleepFn(Math.min(ATTACH_POLL_INTERVAL_MS, deadline - nowFn()));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Whether a refused AttachVolume lost a race with a concurrent delivery of
|
|
235
|
+
// the same event, which is attaching the volume to this instance.
|
|
236
|
+
async function attachingHere(ec2Client, volumeId, instanceId, log) {
|
|
237
|
+
try {
|
|
238
|
+
const desc = await send(
|
|
239
|
+
ec2Client,
|
|
240
|
+
new DescribeVolumesCommand({ VolumeIds: [volumeId] })
|
|
241
|
+
);
|
|
242
|
+
const volume =
|
|
243
|
+
desc.Volumes && desc.Volumes[0] ? desc.Volumes[0] : undefined;
|
|
244
|
+
return (volume?.Attachments || []).some(
|
|
245
|
+
(a) =>
|
|
246
|
+
a.InstanceId === instanceId &&
|
|
247
|
+
(a.State === "attaching" || a.State === "attached")
|
|
248
|
+
);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
log("Volume read after a refused attach failed", {
|
|
251
|
+
volumeId,
|
|
252
|
+
instanceId,
|
|
253
|
+
error: errMessage(err)
|
|
254
|
+
});
|
|
255
|
+
return false;
|
|
145
256
|
}
|
|
146
|
-
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Auto Scaling refuses a token whose action has already ended, because
|
|
260
|
+
// another delivery of the event completed it or the heartbeat expired, with
|
|
261
|
+
// a ValidationError naming no active lifecycle action.
|
|
262
|
+
function isNoActiveLifecycleAction(err) {
|
|
263
|
+
return (
|
|
264
|
+
Boolean(err) &&
|
|
265
|
+
err.name === "ValidationError" &&
|
|
266
|
+
/No active Lifecycle Action/i.test(errMessage(err))
|
|
267
|
+
);
|
|
147
268
|
}
|
|
148
269
|
|
|
149
270
|
async function completeLifecycle(
|
|
@@ -153,13 +274,28 @@ async function completeLifecycle(
|
|
|
153
274
|
actionToken,
|
|
154
275
|
result
|
|
155
276
|
) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
277
|
+
try {
|
|
278
|
+
await send(
|
|
279
|
+
asgClient,
|
|
280
|
+
new CompleteLifecycleActionCommand({
|
|
281
|
+
AutoScalingGroupName: asgName,
|
|
282
|
+
LifecycleHookName: hookName,
|
|
283
|
+
LifecycleActionToken: actionToken,
|
|
284
|
+
LifecycleActionResult: result
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
return LIFECYCLE_OUTCOME.COMPLETED;
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (isNoActiveLifecycleAction(err)) return LIFECYCLE_OUTCOME.ALREADY_ENDED;
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function logLifecycleOutcome(log, outcome, instanceId) {
|
|
295
|
+
if (outcome !== LIFECYCLE_OUTCOME.ALREADY_ENDED) return;
|
|
296
|
+
log(
|
|
297
|
+
"The lifecycle action had already ended: another delivery completed it, or its heartbeat expired",
|
|
298
|
+
{ event: LIFECYCLE_EVENT.ALREADY_ENDED, instanceId }
|
|
163
299
|
);
|
|
164
300
|
}
|
|
165
301
|
|
|
@@ -171,11 +307,7 @@ async function processRecord(record, deps) {
|
|
|
171
307
|
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
172
308
|
const nowFn = (deps && deps.now) || (() => Date.now());
|
|
173
309
|
const env = (deps && deps.env) || process.env;
|
|
174
|
-
const log =
|
|
175
|
-
(deps && deps.log) ||
|
|
176
|
-
((msg, fields) => {
|
|
177
|
-
console.log(JSON.stringify({ message: msg, ...(fields || {}) }));
|
|
178
|
-
});
|
|
310
|
+
const log = (deps && deps.log) || consoleLog;
|
|
179
311
|
|
|
180
312
|
const parsed = parseLifecycleMessage(record);
|
|
181
313
|
if (!parsed.valid) {
|
|
@@ -185,6 +317,18 @@ async function processRecord(record, deps) {
|
|
|
185
317
|
return;
|
|
186
318
|
}
|
|
187
319
|
const { instanceId, asgName, actionToken, hookName } = parsed;
|
|
320
|
+
const finish = async (result) =>
|
|
321
|
+
logLifecycleOutcome(
|
|
322
|
+
log,
|
|
323
|
+
await completeLifecycle(
|
|
324
|
+
asgClient,
|
|
325
|
+
asgName,
|
|
326
|
+
hookName,
|
|
327
|
+
actionToken,
|
|
328
|
+
result
|
|
329
|
+
),
|
|
330
|
+
instanceId
|
|
331
|
+
);
|
|
188
332
|
|
|
189
333
|
const ownerLogicalId = readEnv(env, "OWNER_LOGICAL_ID");
|
|
190
334
|
const stackId = readEnv(env, "STACK_ID");
|
|
@@ -198,13 +342,7 @@ async function processRecord(record, deps) {
|
|
|
198
342
|
asgName,
|
|
199
343
|
instanceId
|
|
200
344
|
});
|
|
201
|
-
await
|
|
202
|
-
asgClient,
|
|
203
|
-
asgName,
|
|
204
|
-
hookName,
|
|
205
|
-
actionToken,
|
|
206
|
-
"ABANDON"
|
|
207
|
-
);
|
|
345
|
+
await finish("ABANDON");
|
|
208
346
|
return;
|
|
209
347
|
}
|
|
210
348
|
|
|
@@ -217,13 +355,7 @@ async function processRecord(record, deps) {
|
|
|
217
355
|
instanceId,
|
|
218
356
|
error: errMessage(err)
|
|
219
357
|
});
|
|
220
|
-
await
|
|
221
|
-
asgClient,
|
|
222
|
-
asgName,
|
|
223
|
-
hookName,
|
|
224
|
-
actionToken,
|
|
225
|
-
"ABANDON"
|
|
226
|
-
);
|
|
358
|
+
await finish("ABANDON");
|
|
227
359
|
return;
|
|
228
360
|
}
|
|
229
361
|
|
|
@@ -232,13 +364,7 @@ async function processRecord(record, deps) {
|
|
|
232
364
|
ownerLogicalId,
|
|
233
365
|
instanceId
|
|
234
366
|
});
|
|
235
|
-
await
|
|
236
|
-
asgClient,
|
|
237
|
-
asgName,
|
|
238
|
-
hookName,
|
|
239
|
-
actionToken,
|
|
240
|
-
"ABANDON"
|
|
241
|
-
);
|
|
367
|
+
await finish("ABANDON");
|
|
242
368
|
return;
|
|
243
369
|
}
|
|
244
370
|
|
|
@@ -250,41 +376,44 @@ async function processRecord(record, deps) {
|
|
|
250
376
|
volumeIds: volumes.map((v) => v.VolumeId)
|
|
251
377
|
}
|
|
252
378
|
);
|
|
253
|
-
await
|
|
254
|
-
asgClient,
|
|
255
|
-
asgName,
|
|
256
|
-
hookName,
|
|
257
|
-
actionToken,
|
|
258
|
-
"ABANDON"
|
|
259
|
-
);
|
|
379
|
+
await finish("ABANDON");
|
|
260
380
|
return;
|
|
261
381
|
}
|
|
262
382
|
|
|
263
|
-
const
|
|
264
|
-
const volumeId = volume.VolumeId;
|
|
383
|
+
const volumeId = volumes[0].VolumeId;
|
|
265
384
|
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
const
|
|
269
|
-
|
|
385
|
+
// An earlier delivery of this lifecycle action may already have attached
|
|
386
|
+
// the volume here; the release wait reports that as `attachedHere`.
|
|
387
|
+
const release = await pollUntilReleased(
|
|
388
|
+
ec2Client,
|
|
389
|
+
volumeId,
|
|
390
|
+
instanceId,
|
|
391
|
+
sleepFn,
|
|
392
|
+
nowFn
|
|
270
393
|
);
|
|
271
|
-
if (
|
|
394
|
+
if (!release.released) {
|
|
395
|
+
log("Volume was not released within the deadline; abandoning launch", {
|
|
396
|
+
volumeId,
|
|
397
|
+
instanceId,
|
|
398
|
+
deadlineMs: RELEASE_DEADLINE_MS,
|
|
399
|
+
lastState: release.lastState,
|
|
400
|
+
...(release.error !== undefined && { error: release.error })
|
|
401
|
+
});
|
|
402
|
+
await finish("ABANDON");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (release.attachedHere) {
|
|
272
406
|
log("Volume already attached to launching instance; completing CONTINUE", {
|
|
273
407
|
volumeId,
|
|
274
408
|
instanceId
|
|
275
409
|
});
|
|
276
|
-
await
|
|
277
|
-
asgClient,
|
|
278
|
-
asgName,
|
|
279
|
-
hookName,
|
|
280
|
-
actionToken,
|
|
281
|
-
"CONTINUE"
|
|
282
|
-
);
|
|
410
|
+
await finish("CONTINUE");
|
|
283
411
|
return;
|
|
284
412
|
}
|
|
285
413
|
|
|
286
414
|
try {
|
|
287
|
-
await
|
|
415
|
+
await send(
|
|
416
|
+
ec2Client,
|
|
288
417
|
new AttachVolumeCommand({
|
|
289
418
|
VolumeId: volumeId,
|
|
290
419
|
InstanceId: instanceId,
|
|
@@ -292,19 +421,21 @@ async function processRecord(record, deps) {
|
|
|
292
421
|
})
|
|
293
422
|
);
|
|
294
423
|
} catch (err) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
424
|
+
if (await attachingHere(ec2Client, volumeId, instanceId, log)) {
|
|
425
|
+
log("AttachVolume was refused, but the volume is attaching here", {
|
|
426
|
+
volumeId,
|
|
427
|
+
instanceId,
|
|
428
|
+
error: errMessage(err)
|
|
429
|
+
});
|
|
430
|
+
} else {
|
|
431
|
+
log("AttachVolume failed; abandoning", {
|
|
432
|
+
volumeId,
|
|
433
|
+
instanceId,
|
|
434
|
+
error: errMessage(err)
|
|
435
|
+
});
|
|
436
|
+
await finish("ABANDON");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
308
439
|
}
|
|
309
440
|
|
|
310
441
|
const attached = await pollUntilAttached(
|
|
@@ -323,13 +454,7 @@ async function processRecord(record, deps) {
|
|
|
323
454
|
deadlineMs: ATTACH_DEADLINE_MS
|
|
324
455
|
}
|
|
325
456
|
);
|
|
326
|
-
await
|
|
327
|
-
asgClient,
|
|
328
|
-
asgName,
|
|
329
|
-
hookName,
|
|
330
|
-
actionToken,
|
|
331
|
-
"ABANDON"
|
|
332
|
-
);
|
|
457
|
+
await finish("ABANDON");
|
|
333
458
|
return;
|
|
334
459
|
}
|
|
335
460
|
|
|
@@ -337,13 +462,7 @@ async function processRecord(record, deps) {
|
|
|
337
462
|
volumeId,
|
|
338
463
|
instanceId
|
|
339
464
|
});
|
|
340
|
-
await
|
|
341
|
-
asgClient,
|
|
342
|
-
asgName,
|
|
343
|
-
hookName,
|
|
344
|
-
actionToken,
|
|
345
|
-
"CONTINUE"
|
|
346
|
-
);
|
|
465
|
+
await finish("CONTINUE");
|
|
347
466
|
}
|
|
348
467
|
|
|
349
468
|
exports.handler = async (event) => {
|
|
@@ -362,12 +481,16 @@ exports.handler = async (event) => {
|
|
|
362
481
|
try {
|
|
363
482
|
const parsed = parseLifecycleMessage(record);
|
|
364
483
|
if (!parsed.valid) continue;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
484
|
+
logLifecycleOutcome(
|
|
485
|
+
consoleLog,
|
|
486
|
+
await completeLifecycle(
|
|
487
|
+
getAsg(),
|
|
488
|
+
parsed.asgName,
|
|
489
|
+
parsed.hookName,
|
|
490
|
+
parsed.actionToken,
|
|
491
|
+
"ABANDON"
|
|
492
|
+
),
|
|
493
|
+
parsed.instanceId
|
|
371
494
|
);
|
|
372
495
|
} catch (innerErr) {
|
|
373
496
|
console.error(
|
|
@@ -386,13 +509,21 @@ exports._internals = {
|
|
|
386
509
|
parseLifecycleMessage,
|
|
387
510
|
readEnv,
|
|
388
511
|
findOwnedVolume,
|
|
512
|
+
pollUntilReleased,
|
|
389
513
|
pollUntilAttached,
|
|
514
|
+
attachingHere,
|
|
515
|
+
isNoActiveLifecycleAction,
|
|
390
516
|
completeLifecycle,
|
|
391
517
|
processRecord,
|
|
392
518
|
ATTACH_POLL_INTERVAL_MS,
|
|
393
519
|
ATTACH_DEADLINE_MS,
|
|
520
|
+
RELEASE_POLL_INTERVAL_MS,
|
|
521
|
+
RELEASE_DEADLINE_MS,
|
|
394
522
|
TAG_OWNER_LOGICAL_ID,
|
|
395
523
|
TAG_LIFECYCLE,
|
|
396
524
|
TAG_LIFECYCLE_VALUE,
|
|
397
|
-
TAG_STACK_ID
|
|
525
|
+
TAG_STACK_ID,
|
|
526
|
+
SDK_CALL_TIMEOUT_MS,
|
|
527
|
+
LIFECYCLE_EVENT,
|
|
528
|
+
LIFECYCLE_OUTCOME
|
|
398
529
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PERSISTENT_DATA_VOLUME_LAUNCHING_HEARTBEAT_SECONDS } from "../compute/persistentDataVolume.js";
|
|
1
2
|
import { CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, CLICKHOUSE_ECS_RESERVED_MEMORY_MIB, CLICKHOUSE_HOST_METRICS, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_UID, clickHousePasswordSha256Snippet } from "./clickhouseConstants.js";
|
|
2
3
|
import { deriveClickHouseServerTuning, MAX_SERVER_MEMORY_RAM_RATIO, SYSTEM_LOG_TTL } from "./clickhouseTuning.js";
|
|
3
4
|
import { renderUsersXml } from "./clickhouseXmlRenderer.js";
|
|
@@ -462,7 +463,7 @@ mkdir -p /etc/systemd/system/docker.service.d
|
|
|
462
463
|
cat > /etc/systemd/system/docker.service.d/override.conf << 'DOCKEROVERRIDE'
|
|
463
464
|
[Service]
|
|
464
465
|
LimitNOFILE=1048576
|
|
465
|
-
LimitNPROC=
|
|
466
|
+
LimitNPROC=infinity
|
|
466
467
|
DOCKEROVERRIDE
|
|
467
468
|
systemctl daemon-reload
|
|
468
469
|
systemctl restart docker || true
|
|
@@ -475,8 +476,10 @@ systemctl restart docker || true
|
|
|
475
476
|
mkdir -p /etc/ecs
|
|
476
477
|
echo "ECS_RESERVED_MEMORY=${CLICKHOUSE_ECS_RESERVED_MEMORY_MIB}" >> /etc/ecs/ecs.config
|
|
477
478
|
|
|
478
|
-
# Wait for the EBS volume
|
|
479
|
-
|
|
479
|
+
# Wait for the EBS volume. The launching lifecycle hook attaches it only once
|
|
480
|
+
# the previous instance has released it, and ABANDONs this instance when its
|
|
481
|
+
# heartbeat expires, so the wait matches the heartbeat.
|
|
482
|
+
for _ in $(seq 1 ${PERSISTENT_DATA_VOLUME_LAUNCHING_HEARTBEAT_SECONDS}); do
|
|
480
483
|
if [ -b "$DEVICE" ]; then
|
|
481
484
|
break
|
|
482
485
|
fi
|
|
@@ -484,13 +487,53 @@ for i in $(seq 1 60); do
|
|
|
484
487
|
done
|
|
485
488
|
|
|
486
489
|
if [ ! -b "$DEVICE" ]; then
|
|
487
|
-
echo "ERROR: EBS volume $DEVICE not found after
|
|
490
|
+
echo "ERROR: EBS volume $DEVICE not found after ${PERSISTENT_DATA_VOLUME_LAUNCHING_HEARTBEAT_SECONDS} seconds" >&2
|
|
488
491
|
exit 1
|
|
489
492
|
fi
|
|
490
493
|
|
|
491
|
-
# Format only
|
|
492
|
-
|
|
494
|
+
# Format only a device that holds no filesystem this construct could have
|
|
495
|
+
# made. blkid -p exits 2 both for a blank device and for one it could not
|
|
496
|
+
# read, so the device must also read back, and carry the ext4 magic neither
|
|
497
|
+
# in the primary superblock nor in the first backup (group 1 of a 4 KiB-block
|
|
498
|
+
# filesystem, the one e2fsck -b 32768 reads). Content is not a test: a new EBS
|
|
499
|
+
# volume need not read back as zeros. Random bytes carry the magic at a given
|
|
500
|
+
# offset once in 65,536 reads, and such a new volume is refused too, which is
|
|
501
|
+
# the side to err on. Anything else is refused: formatting is the one step
|
|
502
|
+
# here that can destroy the database.
|
|
503
|
+
# Prints, as hex, the two bytes at offset $2 of the 4 KiB block $1; fails
|
|
504
|
+
# unless the whole block was read and parsed.
|
|
505
|
+
read_magic() {
|
|
506
|
+
local block
|
|
507
|
+
block=$(mktemp) || return 1
|
|
508
|
+
if ! dd if="$DEVICE" of="$block" bs=4096 skip="$1" count=1 iflag=direct status=none || [ "$(stat -c %s "$block")" != 4096 ]; then
|
|
509
|
+
rm -f "$block"
|
|
510
|
+
return 1
|
|
511
|
+
fi
|
|
512
|
+
od -An -tx1 -j "$2" -N 2 "$block" | tr -d ' \\n'
|
|
513
|
+
local rc=$?
|
|
514
|
+
rm -f "$block"
|
|
515
|
+
return "$rc"
|
|
516
|
+
}
|
|
517
|
+
BLKID_RC=0
|
|
518
|
+
DATA_VOLUME_FS_TYPE=$(blkid -p -s TYPE -o value "$DEVICE") || BLKID_RC=$?
|
|
519
|
+
if [ "$BLKID_RC" -eq 0 ]; then
|
|
520
|
+
if [ "$DATA_VOLUME_FS_TYPE" != "ext4" ]; then
|
|
521
|
+
echo "ERROR: $DEVICE holds '$DATA_VOLUME_FS_TYPE', not an ext4 filesystem; refusing to mount or format it" >&2
|
|
522
|
+
exit 1
|
|
523
|
+
fi
|
|
524
|
+
elif [ "$BLKID_RC" -eq 2 ]; then
|
|
525
|
+
if ! PRIMARY_MAGIC=$(read_magic 0 1080) || ! BACKUP_MAGIC=$(read_magic 32768 56); then
|
|
526
|
+
echo "ERROR: blkid found no filesystem on $DEVICE, but its superblocks could not be read; refusing to format it" >&2
|
|
527
|
+
exit 1
|
|
528
|
+
fi
|
|
529
|
+
if [ "$PRIMARY_MAGIC" = 53ef ] || [ "$BACKUP_MAGIC" = 53ef ]; then
|
|
530
|
+
echo "ERROR: blkid found no filesystem on $DEVICE, but it carries an ext4 superblock (primary '$PRIMARY_MAGIC', backup '$BACKUP_MAGIC'); refusing to format it (e2fsck -b 32768 $DEVICE repairs one whose primary superblock is damaged)" >&2
|
|
531
|
+
exit 1
|
|
532
|
+
fi
|
|
493
533
|
mkfs.ext4 -m 0 "$DEVICE"
|
|
534
|
+
else
|
|
535
|
+
echo "ERROR: blkid could not probe $DEVICE (exit $BLKID_RC); refusing to mount or format it" >&2
|
|
536
|
+
exit 1
|
|
494
537
|
fi
|
|
495
538
|
|
|
496
539
|
# Mount
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* Log-pattern metric filters and their alarms read from these namespaces. The
|
|
5
5
|
* webapp app service declares RLS alarms under `WEBAPP` and relocates the
|
|
6
6
|
* ClickHouse stuck-merge alarm under `CLICKHOUSE` (the log line is app-emitted,
|
|
7
|
-
* so it is an app-log alarm carrying a database-domain namespace).
|
|
7
|
+
* so it is an app-log alarm carrying a database-domain namespace). The EC2
|
|
8
|
+
* graceful-termination handler alarms under `EC2` when it cannot prove a
|
|
9
|
+
* terminating instance's ECS tasks stopped.
|
|
8
10
|
*/
|
|
9
11
|
export declare const METRIC_NAMESPACE: {
|
|
10
12
|
readonly CLICKHOUSE: "Fjall/ClickHouse";
|
|
13
|
+
readonly EC2: "Fjall/EC2";
|
|
11
14
|
readonly ECS: "Fjall/ECS";
|
|
12
15
|
readonly WEBAPP: "Fjall/WebApp";
|
|
13
16
|
};
|
|
@@ -5,10 +5,13 @@ import { Aws } from "aws-cdk-lib";
|
|
|
5
5
|
* Log-pattern metric filters and their alarms read from these namespaces. The
|
|
6
6
|
* webapp app service declares RLS alarms under `WEBAPP` and relocates the
|
|
7
7
|
* ClickHouse stuck-merge alarm under `CLICKHOUSE` (the log line is app-emitted,
|
|
8
|
-
* so it is an app-log alarm carrying a database-domain namespace).
|
|
8
|
+
* so it is an app-log alarm carrying a database-domain namespace). The EC2
|
|
9
|
+
* graceful-termination handler alarms under `EC2` when it cannot prove a
|
|
10
|
+
* terminating instance's ECS tasks stopped.
|
|
9
11
|
*/
|
|
10
12
|
export const METRIC_NAMESPACE = {
|
|
11
13
|
CLICKHOUSE: "Fjall/ClickHouse",
|
|
14
|
+
EC2: "Fjall/EC2",
|
|
12
15
|
ECS: "Fjall/ECS",
|
|
13
16
|
WEBAPP: "Fjall/WebApp"
|
|
14
17
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "39.0.
|
|
3
|
+
"version": "39.0.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
83
|
-
"@fjall/generator": "^39.0.
|
|
84
|
-
"@fjall/util": "^39.0.
|
|
83
|
+
"@fjall/generator": "^39.0.2",
|
|
84
|
+
"@fjall/util": "^39.0.2",
|
|
85
85
|
"constructs": "^10.7.2",
|
|
86
86
|
"zod": "^4.4.3"
|
|
87
87
|
},
|