@fjall/components-infrastructure 1.1.0 → 2.1.1
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 +9 -3
- package/dist/lib/patterns/aws/clickhouseDatabase.js +43 -20
- package/dist/lib/patterns/aws/interfaces/database.d.ts +7 -5
- package/dist/lib/resources/aws/database/clickhouseUserData.js +20 -0
- package/dist/lib/utils/manifestWriter.js +6 -13
- package/package.json +5 -5
|
@@ -2,7 +2,7 @@ import { Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
|
|
|
2
2
|
import { type TaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
3
3
|
import { type IBucket } from "aws-cdk-lib/aws-s3";
|
|
4
4
|
import { Construct } from "constructs";
|
|
5
|
-
import { Secret } from "../../resources/aws/secrets/secret.js";
|
|
5
|
+
import { Secret, type SecretImport } from "../../resources/aws/secrets/secret.js";
|
|
6
6
|
import { type ClickHouseSchemaAdmin, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
7
7
|
import { type ISecret } from "aws-cdk-lib/aws-secretsmanager";
|
|
8
8
|
import type { ClickHouseTlsOptions } from "./clickhouseTls/index.js";
|
|
@@ -174,8 +174,14 @@ export declare class ClickHouseDatabase extends Construct implements IClickHouse
|
|
|
174
174
|
* Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise.
|
|
175
175
|
*/
|
|
176
176
|
getUrl(): string;
|
|
177
|
-
/**
|
|
178
|
-
|
|
177
|
+
/**
|
|
178
|
+
* SecretImport for the CA certificate secret, or `undefined` when TLS is
|
|
179
|
+
* disabled (`tls: { mode: "none", … }`). Consumers wire this into their
|
|
180
|
+
* `secretsImport:` block under `CLICKHOUSE_CA_CERT` so `@fjall/clickhouse`
|
|
181
|
+
* `createFjallClickHouseClient()` picks it up from `process.env`. The secret
|
|
182
|
+
* holds raw PEM (no JSON field) so `field` is `undefined`.
|
|
183
|
+
*/
|
|
184
|
+
getTlsCaCertImport(): SecretImport | undefined;
|
|
179
185
|
getNativeUrl(): string;
|
|
180
186
|
getDatabaseName(): string;
|
|
181
187
|
getBackupBucket(): IBucket;
|
|
@@ -294,6 +294,15 @@ export class ClickHouseDatabase extends Construct {
|
|
|
294
294
|
...OPTIMISE_MV_TABLES.map((table) => `OPTIMIZE TABLE ${CLICKHOUSE_DATABASE_NAME}.${table}`)
|
|
295
295
|
].join("; ");
|
|
296
296
|
const adminSecret = expectDefined(userSecrets.get(schemaAdmin.name), `schemaAdmin '${schemaAdmin.name}' secret not minted.`);
|
|
297
|
+
const sidecarTlsPreamble = tlsActive
|
|
298
|
+
? `set -eu && printf '%s\\n' "$CLICKHOUSE_CA_CERT" > /tmp/ca.crt && printf '%s' '<?xml version="1.0"?><config><openSSL><client><caConfig>/tmp/ca.crt</caConfig><verificationMode>strict</verificationMode><loadDefaultCAFile>false</loadDefaultCAFile><invalidCertificateHandler><name>RejectCertificateHandler</name></invalidCertificateHandler></client></openSSL></config>' > /tmp/clickhouse-client.xml && `
|
|
299
|
+
: "";
|
|
300
|
+
const sidecarTlsClientArgs = tlsActive
|
|
301
|
+
? " --config-file=/tmp/clickhouse-client.xml --secure"
|
|
302
|
+
: "";
|
|
303
|
+
const sidecarTlsSecrets = tlsActive && tlsCaSecret !== undefined
|
|
304
|
+
? { CLICKHOUSE_CA_CERT: EcsSecret.fromSecretsManager(tlsCaSecret) }
|
|
305
|
+
: {};
|
|
297
306
|
const scheduledTasks = [];
|
|
298
307
|
if (optimiseEnabled) {
|
|
299
308
|
scheduledTasks.push({
|
|
@@ -303,19 +312,13 @@ export class ClickHouseDatabase extends Construct {
|
|
|
303
312
|
cpu: OPTIMISE_TASK_CPU_UNITS,
|
|
304
313
|
memoryLimitMiB: OPTIMISE_TASK_MEMORY_MIB,
|
|
305
314
|
command: [
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
clickHouseHost
|
|
309
|
-
"--port",
|
|
310
|
-
String(nativePort),
|
|
311
|
-
"--user",
|
|
312
|
-
schemaAdmin.name,
|
|
313
|
-
...(tlsActive ? ["--secure", "--accept-invalid-certificate"] : []),
|
|
314
|
-
"--query",
|
|
315
|
-
`${optimiseQuery};`
|
|
315
|
+
"sh",
|
|
316
|
+
"-c",
|
|
317
|
+
`${sidecarTlsPreamble}clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${sidecarTlsClientArgs} --query "${optimiseQuery};"`
|
|
316
318
|
],
|
|
317
319
|
secrets: {
|
|
318
|
-
CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
|
|
320
|
+
CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password"),
|
|
321
|
+
...sidecarTlsSecrets
|
|
319
322
|
},
|
|
320
323
|
logRetention: RetentionDays.ONE_WEEK,
|
|
321
324
|
securityGroups: [securityGroup]
|
|
@@ -332,10 +335,11 @@ export class ClickHouseDatabase extends Construct {
|
|
|
332
335
|
"sh",
|
|
333
336
|
"-c",
|
|
334
337
|
// Password via CLICKHOUSE_PASSWORD env, not --password on argv (argv → /proc/<pid>/cmdline).
|
|
335
|
-
|
|
338
|
+
`${sidecarTlsPreamble}STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${sidecarTlsClientArgs} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
|
|
336
339
|
],
|
|
337
340
|
secrets: {
|
|
338
|
-
CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
|
|
341
|
+
CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password"),
|
|
342
|
+
...sidecarTlsSecrets
|
|
339
343
|
},
|
|
340
344
|
logGroup: backupTaskLogGroup,
|
|
341
345
|
securityGroups: [securityGroup]
|
|
@@ -445,7 +449,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
445
449
|
command: [
|
|
446
450
|
"CMD-SHELL",
|
|
447
451
|
tlsActive
|
|
448
|
-
? `wget -q --
|
|
452
|
+
? `wget -q --ca-certificate=${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/ca.crt -O /dev/null https://127.0.0.1:${httpPort}/ping || exit 1`
|
|
449
453
|
: `wget -q -O /dev/null http://127.0.0.1:${httpPort}/ping || exit 1`
|
|
450
454
|
],
|
|
451
455
|
interval: CLICKHOUSE_HEALTH_CHECK.INTERVAL_SECONDS,
|
|
@@ -489,7 +493,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
489
493
|
`ADMIN_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "${adminSecretName}" --query SecretString --output text | jq -r .password)`,
|
|
490
494
|
`if [ -z "$ADMIN_PASSWORD" ] || [ "$ADMIN_PASSWORD" = "null" ]; then echo "fjall:reload:status=failed reason=password-fetch" >&2; exit 1; fi`,
|
|
491
495
|
`mv /var/lib/clickhouse/users.d/fjall.xml.new /var/lib/clickhouse/users.d/fjall.xml`,
|
|
492
|
-
`docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client${tlsActive ?
|
|
496
|
+
`docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client${tlsActive ? ` --config-file=${CLICKHOUSE_TLS_CERT_MOUNT_PATH}/client.xml --secure --host localhost` : ""} --port ${nativePort} --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
|
|
493
497
|
].join("\n");
|
|
494
498
|
const region = Stack.of(this).region;
|
|
495
499
|
const account = Stack.of(this).account;
|
|
@@ -605,9 +609,21 @@ export class ClickHouseDatabase extends Construct {
|
|
|
605
609
|
const scheme = this.tlsCaSecret !== undefined ? "https" : "http";
|
|
606
610
|
return `${scheme}://${this.getHostEndpoint()}:${this.#httpPort}`;
|
|
607
611
|
}
|
|
608
|
-
/**
|
|
609
|
-
|
|
610
|
-
|
|
612
|
+
/**
|
|
613
|
+
* SecretImport for the CA certificate secret, or `undefined` when TLS is
|
|
614
|
+
* disabled (`tls: { mode: "none", … }`). Consumers wire this into their
|
|
615
|
+
* `secretsImport:` block under `CLICKHOUSE_CA_CERT` so `@fjall/clickhouse`
|
|
616
|
+
* `createFjallClickHouseClient()` picks it up from `process.env`. The secret
|
|
617
|
+
* holds raw PEM (no JSON field) so `field` is `undefined`.
|
|
618
|
+
*/
|
|
619
|
+
getTlsCaCertImport() {
|
|
620
|
+
if (this.tlsCaSecret === undefined)
|
|
621
|
+
return undefined;
|
|
622
|
+
return {
|
|
623
|
+
id: `${this.node.id}TlsCaCertImport`,
|
|
624
|
+
name: this.tlsCaSecret.secretName,
|
|
625
|
+
field: undefined
|
|
626
|
+
};
|
|
611
627
|
}
|
|
612
628
|
getNativeUrl() {
|
|
613
629
|
return `tcp://${this.getHostEndpoint()}:${this.#nativePort}`;
|
|
@@ -662,12 +678,19 @@ export class ClickHouseDatabase extends Construct {
|
|
|
662
678
|
*/
|
|
663
679
|
getMigrationContributions() {
|
|
664
680
|
const environment = {
|
|
665
|
-
CLICKHOUSE_URL: this.
|
|
681
|
+
CLICKHOUSE_URL: this.getUrl(),
|
|
666
682
|
CLICKHOUSE_DATABASE: this.getDatabaseName()
|
|
667
683
|
};
|
|
668
684
|
const secretsImport = {};
|
|
669
685
|
const adminSecret = this.getUser(this.#schemaAdmin.name);
|
|
670
686
|
secretsImport.SCHEMA_ADMIN_PASSWORD = adminSecret.getImport("password");
|
|
687
|
+
const caCertImport = this.getTlsCaCertImport();
|
|
688
|
+
if (caCertImport !== undefined) {
|
|
689
|
+
secretsImport.CLICKHOUSE_CA_CERT = caCertImport;
|
|
690
|
+
}
|
|
691
|
+
if (this.caCertSha256 !== undefined) {
|
|
692
|
+
environment.CLICKHOUSE_CA_CERT_SHA256 = this.caCertSha256;
|
|
693
|
+
}
|
|
671
694
|
for (const name of this.#managedPasswordNames) {
|
|
672
695
|
const userSecret = this.getUser(name);
|
|
673
696
|
secretsImport[userPasswordEnvName(name)] =
|
|
@@ -707,7 +730,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
707
730
|
if (container === undefined) {
|
|
708
731
|
throw new Error("ClickHouseDatabase.connectFromTask: task has no default container");
|
|
709
732
|
}
|
|
710
|
-
container.addEnvironment("CLICKHOUSE_URL", this.
|
|
733
|
+
container.addEnvironment("CLICKHOUSE_URL", this.getUrl());
|
|
711
734
|
container.addEnvironment("CLICKHOUSE_DATABASE", CLICKHOUSE_DATABASE_NAME);
|
|
712
735
|
if (opts?.user !== undefined) {
|
|
713
736
|
const userSecret = this.getUser(opts.user);
|
|
@@ -263,12 +263,14 @@ export interface IClickHouseDatabase extends IDatabase, IConnectable, IMigration
|
|
|
263
263
|
/** Native TCP port (typically 9000). */
|
|
264
264
|
getNativePort(): number;
|
|
265
265
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
266
|
+
* Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
|
|
267
|
+
* active (default), `http` only when `tls: { mode: "none", … }` is set.
|
|
268
|
+
* Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise. No
|
|
269
|
+
* credentials in the URL — ClickHouse HTTP uses Basic Auth headers or
|
|
270
|
+
* `X-ClickHouse-User`/`X-ClickHouse-Key` headers. Compose creds at
|
|
271
|
+
* runtime via `getUser(name).getImport(...)`.
|
|
270
272
|
*/
|
|
271
|
-
|
|
273
|
+
getUrl(): string;
|
|
272
274
|
/**
|
|
273
275
|
* Native TCP URL (`tcp://<host>:<nativePort>`).
|
|
274
276
|
* No credentials in the URL — the native protocol handshake carries them.
|
|
@@ -288,8 +288,28 @@ SERVER_JSON=$(aws secretsmanager get-secret-value --secret-id "${options.serverS
|
|
|
288
288
|
printf '%s' "$SERVER_JSON" | jq -r .cert > "$MOUNT_POINT/server-certs/server.crt"
|
|
289
289
|
printf '%s' "$SERVER_JSON" | jq -r .key > "$MOUNT_POINT/server-certs/server.key"
|
|
290
290
|
|
|
291
|
+
# Strict-verify client config consumed by docker-exec reload SSM script and any
|
|
292
|
+
# in-container clickhouse-client invocation. RejectCertificateHandler + strict
|
|
293
|
+
# verificationMode together close the trust-anchor-skipped class.
|
|
294
|
+
cat > "$MOUNT_POINT/server-certs/client.xml" <<'CLIENTXML'
|
|
295
|
+
<?xml version="1.0"?>
|
|
296
|
+
<config>
|
|
297
|
+
<openSSL>
|
|
298
|
+
<client>
|
|
299
|
+
<caConfig>/etc/clickhouse-server/certs/ca.crt</caConfig>
|
|
300
|
+
<verificationMode>strict</verificationMode>
|
|
301
|
+
<loadDefaultCAFile>false</loadDefaultCAFile>
|
|
302
|
+
<invalidCertificateHandler>
|
|
303
|
+
<name>RejectCertificateHandler</name>
|
|
304
|
+
</invalidCertificateHandler>
|
|
305
|
+
</client>
|
|
306
|
+
</openSSL>
|
|
307
|
+
</config>
|
|
308
|
+
CLIENTXML
|
|
309
|
+
|
|
291
310
|
chown -R 101:101 "$MOUNT_POINT/server-certs"
|
|
292
311
|
chmod 600 "$MOUNT_POINT/server-certs/server.key"
|
|
312
|
+
chmod 644 "$MOUNT_POINT/server-certs/client.xml"
|
|
293
313
|
`
|
|
294
314
|
: "";
|
|
295
315
|
const script = `#!/bin/bash
|
|
@@ -17,7 +17,7 @@ import { writeFileSync, readFileSync, existsSync, readdirSync, renameSync, unlin
|
|
|
17
17
|
import { join, basename } from "path";
|
|
18
18
|
import { createHash } from "crypto";
|
|
19
19
|
import { buildConstructMap, constructMapToRecord, ACCOUNT_CONSTRUCT_GROUPS } from "@fjall/util/constructMap";
|
|
20
|
-
import { maskSensitiveOutput } from "@fjall/util";
|
|
20
|
+
import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
|
|
21
21
|
import { FJALL_MANIFEST_FILENAME, MANIFEST_SCHEMA_VERSION, FjallManifestSchema } from "@fjall/util/manifest/schemas";
|
|
22
22
|
import { FjallLogger } from "./validationLogger.js";
|
|
23
23
|
/**
|
|
@@ -117,7 +117,7 @@ function computeTemplateHash(templatePath) {
|
|
|
117
117
|
return createHash("sha256").update(normalised).digest("hex");
|
|
118
118
|
}
|
|
119
119
|
catch (error) {
|
|
120
|
-
const maskedMessage = maskSensitiveOutput(
|
|
120
|
+
const maskedMessage = maskSensitiveOutput(getErrorMessage(error));
|
|
121
121
|
FjallLogger.warn(`Failed to compute hash for ${templatePath}: ${maskedMessage}`);
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
@@ -144,7 +144,7 @@ function computeStackHashes(cdkOutPath) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
catch (error) {
|
|
147
|
-
const maskedMessage = maskSensitiveOutput(
|
|
147
|
+
const maskedMessage = maskSensitiveOutput(getErrorMessage(error));
|
|
148
148
|
FjallLogger.warn(`Failed to read cdk.out for hash computation: ${maskedMessage}`);
|
|
149
149
|
}
|
|
150
150
|
return stacks;
|
|
@@ -194,16 +194,10 @@ export function writeManifest(assembly, collector) {
|
|
|
194
194
|
unlinkSync(tmpPath);
|
|
195
195
|
}
|
|
196
196
|
catch (cleanupError) {
|
|
197
|
-
|
|
198
|
-
? cleanupError.message
|
|
199
|
-
: String(cleanupError);
|
|
200
|
-
FjallLogger.warn(`Failed to clean up tmp manifest at ${tmpPath}: ${maskSensitiveOutput(cleanupMessage)}`);
|
|
197
|
+
FjallLogger.warn(`Failed to clean up tmp manifest at ${tmpPath}: ${maskSensitiveOutput(getErrorMessage(cleanupError))}`);
|
|
201
198
|
}
|
|
202
199
|
}
|
|
203
|
-
|
|
204
|
-
throw new Error(`Failed to write Fjall manifest: ${errorMessage}`, {
|
|
205
|
-
cause: error
|
|
206
|
-
});
|
|
200
|
+
throw new Error(`Failed to write Fjall manifest: ${getErrorMessage(error)}`, { cause: error });
|
|
207
201
|
}
|
|
208
202
|
}
|
|
209
203
|
/**
|
|
@@ -224,8 +218,7 @@ export function readExistingManifest(cdkOutPath) {
|
|
|
224
218
|
return result.data;
|
|
225
219
|
}
|
|
226
220
|
catch (error) {
|
|
227
|
-
|
|
228
|
-
FjallLogger.warn(`Failed to parse existing manifest: ${maskSensitiveOutput(message)}`);
|
|
221
|
+
FjallLogger.warn(`Failed to parse existing manifest: ${maskSensitiveOutput(getErrorMessage(error))}`);
|
|
229
222
|
return null;
|
|
230
223
|
}
|
|
231
224
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"eslint": "^10.2.1",
|
|
60
60
|
"prettier": "^3.8.3",
|
|
61
61
|
"typescript": "^6.0.3",
|
|
62
|
-
"vitest": "^4.1.
|
|
62
|
+
"vitest": "^4.1.7"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@aws-sdk/client-organizations": "^3.1038.0",
|
|
66
|
-
"@fjall/generator": "^1.1
|
|
67
|
-
"@fjall/util": "^1.1
|
|
66
|
+
"@fjall/generator": "^2.1.1",
|
|
67
|
+
"@fjall/util": "^2.1.1",
|
|
68
68
|
"constructs": "^10.0.0",
|
|
69
69
|
"uuid": "^14.0.0"
|
|
70
70
|
},
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=18.0.0"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "971d9ed50c6a21d24e67c9c83b9c471a632bf284"
|
|
83
83
|
}
|