@fjall/components-infrastructure 0.100.0 → 1.1.0

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.
Files changed (74) hide show
  1. package/dist/lib/lambda-assets/cert-generator/asset/index.js +17948 -0
  2. package/dist/lib/lambda-assets/cert-generator/asset/package.json +4 -0
  3. package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +49 -1
  4. package/dist/lib/patterns/aws/clickhouseDatabase.js +137 -20
  5. package/dist/lib/patterns/aws/clickhouseTls/index.d.ts +1 -0
  6. package/dist/lib/patterns/aws/clickhouseTls/index.js +1 -0
  7. package/dist/lib/patterns/aws/clickhouseTls/types.d.ts +48 -0
  8. package/dist/lib/patterns/aws/computeEcs.d.ts +13 -1
  9. package/dist/lib/patterns/aws/computeEcs.js +88 -8
  10. package/dist/lib/patterns/aws/interfaces/database.d.ts +32 -1
  11. package/dist/lib/patterns/aws/interfaces/database.js +1 -1
  12. package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +21 -0
  13. package/dist/lib/resources/aws/database/clickhouseConstants.js +21 -0
  14. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.d.ts +2 -0
  15. package/dist/lib/resources/aws/database/clickhouseSecurityGroup.js +2 -0
  16. package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +21 -0
  17. package/dist/lib/resources/aws/database/clickhouseUserData.js +48 -3
  18. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.d.ts +1 -1
  19. package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +1 -1
  20. package/dist/lib/resources/aws/secrets/index.d.ts +2 -0
  21. package/dist/lib/resources/aws/secrets/index.js +2 -0
  22. package/dist/lib/resources/aws/secrets/tlsCaSecret.d.ts +13 -0
  23. package/dist/lib/resources/aws/secrets/tlsCaSecret.js +15 -0
  24. package/dist/lib/resources/aws/secrets/tlsServerSecret.d.ts +15 -0
  25. package/dist/lib/resources/aws/secrets/tlsServerSecret.js +17 -0
  26. package/dist/lib/resources/aws/utilities/index.d.ts +1 -0
  27. package/dist/lib/resources/aws/utilities/index.js +1 -0
  28. package/dist/lib/resources/aws/utilities/tlsCertGenerator.d.ts +33 -0
  29. package/dist/lib/resources/aws/utilities/tlsCertGenerator.js +67 -0
  30. package/package.json +7 -5
  31. package/dist/lib/config/aws/__t17fixture.js +0 -3
  32. package/dist/lib/config/aws/__t17fixtureType.d.ts +0 -2
  33. package/dist/lib/config/aws/__t17fixtureType.js +0 -1
  34. package/dist/lib/config/aws/eventBus.d.ts +0 -7
  35. package/dist/lib/config/aws/eventBus.js +0 -21
  36. package/dist/lib/config/aws/identityCenterGroupMembership.d.ts +0 -10
  37. package/dist/lib/config/aws/identityCenterGroupMembership.js +0 -102
  38. package/dist/lib/config/aws/securityBaseline.d.ts +0 -15
  39. package/dist/lib/config/aws/securityBaseline.js +0 -27
  40. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.d.ts +0 -1
  41. package/dist/lib/patterns/aws/_eslint_test_tmp/leak.js +0 -4
  42. package/dist/lib/patterns/aws/managedIdentityCenter.d.ts +0 -4
  43. package/dist/lib/patterns/aws/managedIdentityCenter.js +0 -19
  44. package/dist/lib/patterns/aws/subdomainHostedZone.d.ts +0 -9
  45. package/dist/lib/patterns/aws/subdomainHostedZone.js +0 -34
  46. package/dist/lib/resources/aws/analytics/clickhouse.d.ts +0 -15
  47. package/dist/lib/resources/aws/analytics/clickhouse.js +0 -310
  48. package/dist/lib/resources/aws/analytics/clickhouseAlarms.d.ts +0 -49
  49. package/dist/lib/resources/aws/analytics/clickhouseAlarms.js +0 -140
  50. package/dist/lib/resources/aws/analytics/clickhouseConstants.d.ts +0 -73
  51. package/dist/lib/resources/aws/analytics/clickhouseConstants.js +0 -89
  52. package/dist/lib/resources/aws/analytics/clickhouseSecurityGroup.d.ts +0 -13
  53. package/dist/lib/resources/aws/analytics/clickhouseSecurityGroup.js +0 -28
  54. package/dist/lib/resources/aws/analytics/clickhouseTypes.d.ts +0 -59
  55. package/dist/lib/resources/aws/analytics/clickhouseTypes.js +0 -1
  56. package/dist/lib/resources/aws/analytics/clickhouseUserData.d.ts +0 -6
  57. package/dist/lib/resources/aws/analytics/clickhouseUserData.js +0 -299
  58. package/dist/lib/resources/aws/analytics/index.d.ts +0 -4
  59. package/dist/lib/resources/aws/analytics/index.js +0 -2
  60. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.d.ts +0 -2
  61. package/dist/lib/resources/aws/compute/__tmp__/regression-shape.js +0 -11
  62. package/dist/lib/resources/aws/messaging/defaultEventBus.d.ts +0 -7
  63. package/dist/lib/resources/aws/messaging/defaultEventBus.js +0 -21
  64. package/dist/lib/resources/aws/networking/domain.d.ts +0 -13
  65. package/dist/lib/resources/aws/networking/domain.js +0 -100
  66. package/dist/lib/synth_dump.d.ts +0 -1
  67. package/dist/lib/synth_dump.js +0 -42
  68. package/dist/lib/utils/bastionFactory.d.ts +0 -10
  69. package/dist/lib/utils/bastionFactory.js +0 -29
  70. package/dist/lib/utils/constructMap.d.ts +0 -33
  71. package/dist/lib/utils/constructMap.js +0 -154
  72. package/dist/lib/utils/dnsRecords.d.ts +0 -4
  73. package/dist/lib/utils/dnsRecords.js +0 -104
  74. /package/dist/lib/{config/aws/__t17fixture.d.ts → patterns/aws/clickhouseTls/types.js} +0 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "commonjs",
3
+ "private": true
4
+ }
@@ -4,7 +4,9 @@ import { type IBucket } from "aws-cdk-lib/aws-s3";
4
4
  import { Construct } from "constructs";
5
5
  import { Secret } from "../../resources/aws/secrets/secret.js";
6
6
  import { type ClickHouseSchemaAdmin, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
7
- import { type IClickHouseDatabase } from "./interfaces/database.js";
7
+ import { type ISecret } from "aws-cdk-lib/aws-secretsmanager";
8
+ import type { ClickHouseTlsOptions } from "./clickhouseTls/index.js";
9
+ import { type ClickHouseMigrationsConfig, type IClickHouseDatabase } from "./interfaces/database.js";
8
10
  import { type ISecurityGroupConnector } from "./interfaces/connector.js";
9
11
  import { type MigrationContributions } from "./interfaces/migrationContributor.js";
10
12
  /**
@@ -95,6 +97,24 @@ export interface ClickHouseDatabaseProps {
95
97
  * non-snake_case names.
96
98
  */
97
99
  profiles?: Record<string, ProfileSpec>;
100
+ /**
101
+ * Schema-version gate. When set, every container of every service whose
102
+ * `connections:` includes this database receives `EXPECTED_CH_SCHEMA_VERSION`
103
+ * baked into its env at synth, unless the service sets `schemaGate: false`.
104
+ * The default resolver picks the lexicographically-latest `*.sql` filename
105
+ * under `dir` (skipping `*.dev.sql`), matching what `runSqlMigrations`
106
+ * records in `_schema_migrations.ch_version`.
107
+ */
108
+ migrations?: ClickHouseMigrationsConfig;
109
+ /**
110
+ * TLS configuration. Default: `{ mode: "self-signed" }` — mints an ECDSA
111
+ * P-256 CA + leaf cert at deploy via a Lambda custom resource, stores both
112
+ * PEMs in Secrets Manager, and the EC2 user-data materialises them at boot
113
+ * for the CH server to read. Set `mode: "imported"` to bring your own CA;
114
+ * `mode: "none"` (acknowledgement literal required) opts out of TLS.
115
+ * See ./clickhouseTls/types.ts.
116
+ */
117
+ tls?: ClickHouseTlsOptions;
98
118
  }
99
119
  /**
100
120
  * ClickHouse analytics database wrapper implementing IClickHouseDatabase.
@@ -112,6 +132,25 @@ export declare class ClickHouseDatabase extends Construct implements IClickHouse
112
132
  readonly additionalTcpPorts: number[];
113
133
  readonly id: string;
114
134
  readonly connections: Connections;
135
+ /**
136
+ * CA cert secret. Self-signed: minted by `TlsCertGenerator`. Imported: the
137
+ * user-supplied ISecret. `undefined` when `tls.mode === "none"`.
138
+ * SecretString shape: raw PEM (single cert, no JSON wrapping).
139
+ */
140
+ readonly tlsCaSecret: ISecret | undefined;
141
+ /**
142
+ * Server cert + key secret. Self-signed: minted by `TlsCertGenerator`.
143
+ * Imported: the user-supplied ISecret. `undefined` when `tls.mode === "none"`.
144
+ * SecretString shape: JSON `{ "cert": "<leaf-pem>", "key": "<key-pem>" }`.
145
+ */
146
+ readonly tlsServerSecret: ISecret | undefined;
147
+ /**
148
+ * SHA-256 of the CA cert PEM. CFN token in self-signed mode (from the cert
149
+ * generator custom resource Data attribute). Plain string in imported mode
150
+ * when supplied. `undefined` when `tls.mode === "none"` or when imported
151
+ * without a digest. Surface on consumers as a task-replacement trigger.
152
+ */
153
+ readonly caCertSha256: string | undefined;
115
154
  constructor(scope: Construct, id: string, props: ClickHouseDatabaseProps);
116
155
  /**
117
156
  * Returns the Fjall `Secret` wrapper for the named user. Throws with a
@@ -129,11 +168,20 @@ export declare class ClickHouseDatabase extends Construct implements IClickHouse
129
168
  getHttpPort(): number;
130
169
  getNativePort(): number;
131
170
  getHostEndpoint(): string;
171
+ /**
172
+ * Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
173
+ * active (default), `http` only when `tls: { mode: "none", … }` is set.
174
+ * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise.
175
+ */
176
+ getUrl(): string;
177
+ /** @deprecated Alias for {@link getUrl}. Removed in a follow-up landing. */
132
178
  getHttpUrl(): string;
133
179
  getNativeUrl(): string;
134
180
  getDatabaseName(): string;
135
181
  getBackupBucket(): IBucket;
136
182
  getColdTierBucket(): IBucket | undefined;
183
+ getMigrationsConfig(): ClickHouseMigrationsConfig | undefined;
184
+ getExpectedSchemaVersion(): string | undefined;
137
185
  grantConnect(grantee: IConnectable): void;
138
186
  /**
139
187
  * Migration contributions for a task connecting to this ClickHouse cluster
@@ -1,4 +1,4 @@
1
- import { CLICKHOUSE_MANAGED_USERS_ENV } from "@fjall/util/migration";
1
+ import { CLICKHOUSE_MANAGED_USERS_ENV, pickLatestClickHouseMigration } from "@fjall/util/migration";
2
2
  import { Annotations, CfnOutput, Duration, Fn, Stack } from "aws-cdk-lib";
3
3
  import { Connections, Port, UserData } from "aws-cdk-lib/aws-ec2";
4
4
  import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
@@ -20,7 +20,8 @@ import { buildClickHouseEntrypointWrapper, buildClickHouseUserData, generateUser
20
20
  import { toPascalCase } from "../../utils/capitaliseString.js";
21
21
  import { ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, ClickHouseDefaultProfiles, PROFILE_NAME_PATTERN } from "../../resources/aws/database/clickhouseSchemas.js";
22
22
  import { inferAmiHardwareType } from "../../resources/aws/compute/ecsConstants.js";
23
- import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, CLICKHOUSE_TASK_MEMORY_MIB, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_SERVER_ROLE_TAG, clickHouseUserSecretName, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_STOP_TIMEOUT_SECONDS, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, userPasswordEnvName, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, CLICKHOUSE_SERVER_CONTAINER_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "../../resources/aws/database/clickhouseConstants.js";
23
+ import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, CLICKHOUSE_TASK_MEMORY_MIB, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_SERVER_ROLE_TAG, clickHouseUserSecretName, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_STOP_TIMEOUT_SECONDS, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, userPasswordEnvName, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, CLICKHOUSE_SERVER_CONTAINER_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "../../resources/aws/database/clickhouseConstants.js";
24
+ import { TlsCertGenerator } from "../../resources/aws/utilities/tlsCertGenerator.js";
24
25
  import { EcsCompute } from "./computeEcs.js";
25
26
  /**
26
27
  * Resolve the ECS desired task count for the ClickHouse service.
@@ -81,14 +82,36 @@ function createClickHouseUserSecret(scope, name) {
81
82
  export class ClickHouseDatabase extends Construct {
82
83
  databaseType = "ClickHouse";
83
84
  connectorType = "relational";
84
- additionalTcpPorts = [CLICKHOUSE_NATIVE_PORT];
85
+ additionalTcpPorts;
85
86
  id;
86
87
  connections;
88
+ /**
89
+ * CA cert secret. Self-signed: minted by `TlsCertGenerator`. Imported: the
90
+ * user-supplied ISecret. `undefined` when `tls.mode === "none"`.
91
+ * SecretString shape: raw PEM (single cert, no JSON wrapping).
92
+ */
93
+ tlsCaSecret;
94
+ /**
95
+ * Server cert + key secret. Self-signed: minted by `TlsCertGenerator`.
96
+ * Imported: the user-supplied ISecret. `undefined` when `tls.mode === "none"`.
97
+ * SecretString shape: JSON `{ "cert": "<leaf-pem>", "key": "<key-pem>" }`.
98
+ */
99
+ tlsServerSecret;
100
+ /**
101
+ * SHA-256 of the CA cert PEM. CFN token in self-signed mode (from the cert
102
+ * generator custom resource Data attribute). Plain string in imported mode
103
+ * when supplied. `undefined` when `tls.mode === "none"` or when imported
104
+ * without a digest. Surface on consumers as a task-replacement trigger.
105
+ */
106
+ caCertSha256;
107
+ #httpPort;
108
+ #nativePort;
87
109
  #users;
88
110
  #schemaAdmin;
89
111
  #managedPasswordNames;
90
112
  #backupBucket;
91
113
  #coldTierBucket;
114
+ #migrationsConfig;
92
115
  constructor(scope, id, props) {
93
116
  super(scope, id);
94
117
  this.id = id;
@@ -156,7 +179,46 @@ export class ClickHouseDatabase extends Construct {
156
179
  (props.backupRetentionDays < 1 || props.backupRetentionDays > 3650)) {
157
180
  throw new Error(`ClickHouseDatabase: backupRetentionDays must be between 1 and 3650; got ${props.backupRetentionDays}.`);
158
181
  }
159
- const securityGroup = createClickHouseSecurityGroup(this, vpc, CLICKHOUSE_NATIVE_PORT);
182
+ const tlsOptions = props.tls ?? {
183
+ mode: "self-signed"
184
+ };
185
+ const tlsActive = tlsOptions.mode !== "none";
186
+ const httpPort = tlsActive ? CLICKHOUSE_HTTPS_PORT : CLICKHOUSE_HTTP_PORT;
187
+ const nativePort = tlsActive
188
+ ? CLICKHOUSE_TCP_SECURE_PORT
189
+ : CLICKHOUSE_NATIVE_PORT;
190
+ let tlsCaSecret;
191
+ let tlsServerSecret;
192
+ let caCertSha256;
193
+ if (tlsOptions.mode === "self-signed") {
194
+ const namespaceName = App.getInstance().getNamespace().namespaceName;
195
+ const hostname = `${CLICKHOUSE_CLOUDMAP_SERVICE_NAME}.${namespaceName}`;
196
+ const certGen = new TlsCertGenerator(this, "TlsCertGenerator", {
197
+ appName: App.getInstance().getName(),
198
+ hostname,
199
+ ...(tlsOptions.caValidityYears !== undefined && {
200
+ caValidityYears: tlsOptions.caValidityYears
201
+ }),
202
+ ...(tlsOptions.leafValidityYears !== undefined && {
203
+ leafValidityYears: tlsOptions.leafValidityYears
204
+ })
205
+ });
206
+ tlsCaSecret = certGen.caSecret.secret;
207
+ tlsServerSecret = certGen.serverSecret.secret;
208
+ caCertSha256 = certGen.caCertSha256;
209
+ }
210
+ else if (tlsOptions.mode === "imported") {
211
+ tlsCaSecret = tlsOptions.caCertSecret;
212
+ tlsServerSecret = tlsOptions.serverCertSecret;
213
+ caCertSha256 = tlsOptions.caCertSha256;
214
+ }
215
+ this.tlsCaSecret = tlsCaSecret;
216
+ this.tlsServerSecret = tlsServerSecret;
217
+ this.caCertSha256 = caCertSha256;
218
+ this.additionalTcpPorts = [nativePort];
219
+ this.#httpPort = httpPort;
220
+ this.#nativePort = nativePort;
221
+ const securityGroup = createClickHouseSecurityGroup(this, vpc, nativePort);
160
222
  const userSecrets = new Map();
161
223
  for (const name of allUserNames) {
162
224
  userSecrets.set(name, createClickHouseUserSecret(this, name));
@@ -185,6 +247,7 @@ export class ClickHouseDatabase extends Construct {
185
247
  : undefined;
186
248
  this.#backupBucket = backupBucket;
187
249
  this.#coldTierBucket = coldTierBucket;
250
+ this.#migrationsConfig = props.migrations;
188
251
  const backupTaskLogGroup = backupEnabled
189
252
  ? new LogGroup(this, "ClickHouseBackupTaskLogGroup", {
190
253
  retention: RetentionDays.TWO_WEEKS
@@ -198,6 +261,13 @@ export class ClickHouseDatabase extends Construct {
198
261
  bucketName: coldTierBucket.bucketName,
199
262
  region: Stack.of(this).region
200
263
  }
264
+ }),
265
+ tlsActive,
266
+ ...(tlsActive &&
267
+ tlsCaSecret !== undefined &&
268
+ tlsServerSecret !== undefined && {
269
+ caSecretArn: tlsCaSecret.secretArn,
270
+ serverSecretArn: tlsServerSecret.secretArn
201
271
  })
202
272
  }));
203
273
  // Source.data wraps the content in a zip; the default `extract: true`
@@ -237,9 +307,10 @@ export class ClickHouseDatabase extends Construct {
237
307
  "--host",
238
308
  clickHouseHost,
239
309
  "--port",
240
- String(CLICKHOUSE_NATIVE_PORT),
310
+ String(nativePort),
241
311
  "--user",
242
312
  schemaAdmin.name,
313
+ ...(tlsActive ? ["--secure", "--accept-invalid-certificate"] : []),
243
314
  "--query",
244
315
  `${optimiseQuery};`
245
316
  ],
@@ -261,7 +332,7 @@ export class ClickHouseDatabase extends Construct {
261
332
  "sh",
262
333
  "-c",
263
334
  // Password via CLICKHOUSE_PASSWORD env, not --password on argv (argv → /proc/<pid>/cmdline).
264
- `STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${CLICKHOUSE_NATIVE_PORT} --user ${schemaAdmin.name} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
335
+ `STAMP=$(date +%Y%m%d-%H%M%S) && clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${tlsActive ? " --secure --accept-invalid-certificate" : ""} --query "BACKUP DATABASE ${CLICKHOUSE_DATABASE_NAME} TO S3('${backupDestUrl}weekly-$STAMP/')"`
265
336
  ],
266
337
  secrets: {
267
338
  CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password")
@@ -329,12 +400,12 @@ export class ClickHouseDatabase extends Construct {
329
400
  secretsImport: clickHouseContainerSecrets,
330
401
  portMappings: [
331
402
  {
332
- containerPort: CLICKHOUSE_HTTP_PORT,
333
- hostPort: CLICKHOUSE_HTTP_PORT
403
+ containerPort: httpPort,
404
+ hostPort: httpPort
334
405
  },
335
406
  {
336
- containerPort: CLICKHOUSE_NATIVE_PORT,
337
- hostPort: CLICKHOUSE_NATIVE_PORT
407
+ containerPort: nativePort,
408
+ hostPort: nativePort
338
409
  },
339
410
  {
340
411
  containerPort: CLICKHOUSE_PROMETHEUS_PORT,
@@ -358,12 +429,24 @@ export class ClickHouseDatabase extends Construct {
358
429
  hostSourcePath: `${CLICKHOUSE_DATA_MOUNT_PATH}/${CLICKHOUSE_USERS_SUBDIR}`,
359
430
  mountPath: "/etc/clickhouse-server/users.d",
360
431
  readOnly: true
361
- }
432
+ },
433
+ ...(tlsActive
434
+ ? [
435
+ {
436
+ name: "clickhouse-certs",
437
+ hostSourcePath: `${CLICKHOUSE_DATA_MOUNT_PATH}/server-certs`,
438
+ mountPath: CLICKHOUSE_TLS_CERT_MOUNT_PATH,
439
+ readOnly: true
440
+ }
441
+ ]
442
+ : [])
362
443
  ],
363
444
  healthCheck: {
364
445
  command: [
365
446
  "CMD-SHELL",
366
- `wget -q -O /dev/null http://127.0.0.1:${CLICKHOUSE_HTTP_PORT}/ping || exit 1`
447
+ tlsActive
448
+ ? `wget -q --no-check-certificate -O /dev/null https://127.0.0.1:${httpPort}/ping || exit 1`
449
+ : `wget -q -O /dev/null http://127.0.0.1:${httpPort}/ping || exit 1`
367
450
  ],
368
451
  interval: CLICKHOUSE_HEALTH_CHECK.INTERVAL_SECONDS,
369
452
  timeout: CLICKHOUSE_HEALTH_CHECK.TIMEOUT_SECONDS,
@@ -388,6 +471,10 @@ export class ClickHouseDatabase extends Construct {
388
471
  instanceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));
389
472
  backupBucket.grantRead(instanceRole, "config/*");
390
473
  adminSecret.secret.grantRead(instanceRole);
474
+ if (tlsCaSecret !== undefined)
475
+ tlsCaSecret.grantRead(instanceRole);
476
+ if (tlsServerSecret !== undefined)
477
+ tlsServerSecret.grantRead(instanceRole);
391
478
  const adminSecretName = clickHouseUserSecretName(schemaAdmin.name);
392
479
  // Password via CLICKHOUSE_CLIENT_PASSWORD env, not --password on argv
393
480
  // (argv → /proc/<pid>/cmdline). `jq -r .password` on an empty pipeline
@@ -402,7 +489,7 @@ export class ClickHouseDatabase extends Construct {
402
489
  `ADMIN_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "${adminSecretName}" --query SecretString --output text | jq -r .password)`,
403
490
  `if [ -z "$ADMIN_PASSWORD" ] || [ "$ADMIN_PASSWORD" = "null" ]; then echo "fjall:reload:status=failed reason=password-fetch" >&2; exit 1; fi`,
404
491
  `mv /var/lib/clickhouse/users.d/fjall.xml.new /var/lib/clickhouse/users.d/fjall.xml`,
405
- `docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client --port 9000 --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
492
+ `docker exec -i -e CLICKHOUSE_CLIENT_PASSWORD="$ADMIN_PASSWORD" "$CONTAINER" clickhouse-client${tlsActive ? " --secure --accept-invalid-certificate" : ""} --port ${nativePort} --user ${schemaAdmin.name} -q "SYSTEM RELOAD USERS"`
406
493
  ].join("\n");
407
494
  const region = Stack.of(this).region;
408
495
  const account = Stack.of(this).account;
@@ -454,15 +541,21 @@ export class ClickHouseDatabase extends Construct {
454
541
  reloadCustomResource.node.addDependency(usersConfigDeployment);
455
542
  this.connections = new Connections({
456
543
  securityGroups: [securityGroup],
457
- defaultPort: Port.tcp(CLICKHOUSE_HTTP_PORT)
544
+ defaultPort: Port.tcp(httpPort)
458
545
  });
459
546
  const declareOutput = (name, value) => {
460
547
  const output = new CfnOutput(this, name, { value });
461
548
  output.overrideLogicalId(name);
462
549
  };
463
550
  declareOutput("Endpoint", clickHouseHost);
464
- declareOutput("HttpPort", String(CLICKHOUSE_HTTP_PORT));
465
- declareOutput("NativePort", String(CLICKHOUSE_NATIVE_PORT));
551
+ declareOutput("HttpPort", String(httpPort));
552
+ declareOutput("NativePort", String(nativePort));
553
+ if (tlsActive) {
554
+ declareOutput("TlsActive", "true");
555
+ }
556
+ if (caCertSha256 !== undefined) {
557
+ declareOutput("CaCertSha256", caCertSha256);
558
+ }
466
559
  declareOutput("DatabaseName", CLICKHOUSE_DATABASE_NAME);
467
560
  for (const [name, secret] of userSecrets) {
468
561
  declareOutput(`${toPascalCase(name)}SecretArn`, secret.secret.secretArn);
@@ -495,19 +588,29 @@ export class ClickHouseDatabase extends Construct {
495
588
  return this.#users.get(name);
496
589
  }
497
590
  getHttpPort() {
498
- return CLICKHOUSE_HTTP_PORT;
591
+ return this.#httpPort;
499
592
  }
500
593
  getNativePort() {
501
- return CLICKHOUSE_NATIVE_PORT;
594
+ return this.#nativePort;
502
595
  }
503
596
  getHostEndpoint() {
504
597
  return `${CLICKHOUSE_CLOUDMAP_SERVICE_NAME}.${App.getInstance().getNamespace().namespaceName}`;
505
598
  }
599
+ /**
600
+ * Canonical URL for the HTTP(S) interface. Scheme is `https` when TLS is
601
+ * active (default), `http` only when `tls: { mode: "none", … }` is set.
602
+ * Port matches: HTTPS (8443) under TLS, HTTP (8123) otherwise.
603
+ */
604
+ getUrl() {
605
+ const scheme = this.tlsCaSecret !== undefined ? "https" : "http";
606
+ return `${scheme}://${this.getHostEndpoint()}:${this.#httpPort}`;
607
+ }
608
+ /** @deprecated Alias for {@link getUrl}. Removed in a follow-up landing. */
506
609
  getHttpUrl() {
507
- return `http://${this.getHostEndpoint()}:${this.getHttpPort()}`;
610
+ return this.getUrl();
508
611
  }
509
612
  getNativeUrl() {
510
- return `tcp://${this.getHostEndpoint()}:${this.getNativePort()}`;
613
+ return `tcp://${this.getHostEndpoint()}:${this.#nativePort}`;
511
614
  }
512
615
  getDatabaseName() {
513
616
  return CLICKHOUSE_DATABASE_NAME;
@@ -518,6 +621,20 @@ export class ClickHouseDatabase extends Construct {
518
621
  getColdTierBucket() {
519
622
  return this.#coldTierBucket;
520
623
  }
624
+ getMigrationsConfig() {
625
+ return this.#migrationsConfig;
626
+ }
627
+ getExpectedSchemaVersion() {
628
+ const config = this.#migrationsConfig;
629
+ if (config === undefined)
630
+ return undefined;
631
+ const resolver = config.versionResolver ?? pickLatestClickHouseMigration;
632
+ const version = resolver(config.dir);
633
+ if (version.trim() === "") {
634
+ throw new Error(`ClickHouseDatabase '${this.id}': migrations.versionResolver returned an empty string for dir '${config.dir}' — refusing to inject an empty EXPECTED_CH_SCHEMA_VERSION.`);
635
+ }
636
+ return version;
637
+ }
521
638
  grantConnect(grantee) {
522
639
  this.connections.allowDefaultPortFrom(grantee);
523
640
  this.connections.allowFrom(grantee, Port.tcp(CLICKHOUSE_NATIVE_PORT));
@@ -0,0 +1 @@
1
+ export * from "./types.js";
@@ -0,0 +1 @@
1
+ export * from "./types.js";
@@ -0,0 +1,48 @@
1
+ import type { ISecret } from "aws-cdk-lib/aws-secretsmanager";
2
+ /**
3
+ * Self-signed mode (default). The construct mints an ECDSA P-256 CA + leaf
4
+ * cert at stack deploy via a custom-resource Lambda. Bumping
5
+ * `caValidityYears` / `leafValidityYears` triggers re-issuance on the next
6
+ * deploy.
7
+ */
8
+ export interface ClickHouseSelfSignedTls {
9
+ readonly mode: "self-signed";
10
+ readonly caValidityYears?: number;
11
+ readonly leafValidityYears?: number;
12
+ }
13
+ /**
14
+ * Imported mode. Caller owns the CA + server-cert secrets in Secrets
15
+ * Manager. `serverCertSecret`'s SecretString MUST be JSON of the shape
16
+ * `{ "cert": "<pem>", "key": "<pem>" }`. `caCertSha256` is optional — when
17
+ * supplied the construct surfaces it as a CFN env-var token so consumers
18
+ * can wire it into containers for rotation-triggers-task-replacement
19
+ * parity with self-signed mode. Omit it and rotation requires manual task
20
+ * restart (documented in `aiDocs/patterns/clickhouse-tls-pattern.md § "Imported mode"`).
21
+ */
22
+ export interface ClickHouseImportedTls {
23
+ readonly mode: "imported";
24
+ readonly caCertSecret: ISecret;
25
+ readonly serverCertSecret: ISecret;
26
+ readonly caCertSha256?: string;
27
+ }
28
+ /**
29
+ * Plaintext escape hatch. Disables TLS entirely. The acknowledgement is a
30
+ * literal-string type so misuse fails at compile time — passing any other
31
+ * string fails `tsc --noEmit`.
32
+ */
33
+ export interface ClickHouseNoTls {
34
+ readonly mode: "none";
35
+ readonly acknowledgement: "I understand plaintext-only ClickHouse fails SOC2 DP5";
36
+ }
37
+ export type ClickHouseTlsOptions = ClickHouseSelfSignedTls | ClickHouseImportedTls | ClickHouseNoTls;
38
+ /**
39
+ * The resolved TLS surface a `ClickHouseDatabase` carries after dispatch.
40
+ * Every field is `undefined` in `mode: "none"`. `certGeneratorFunction`
41
+ * is `undefined` in `mode: "imported"`. `caCertSha256` is `undefined` in
42
+ * `mode: "none"` AND in `mode: "imported"` unless the caller supplied it.
43
+ */
44
+ export interface ClickHouseTlsResolved {
45
+ readonly caSecret: ISecret | undefined;
46
+ readonly serverSecret: ISecret | undefined;
47
+ readonly caCertSha256: string | undefined;
48
+ }
@@ -48,7 +48,7 @@ export declare function expandMigrationsSugar(service: EcsServiceConfig, userCon
48
48
  * themselves and the resolved value differs.
49
49
  * @internal Exported for testing only
50
50
  */
51
- export declare function buildContainerConfigs(service: EcsServiceConfig, schemaVersionEnv?: Record<string, string>, annotationsScope?: Construct): EcsClusterProps["services"][number]["containers"];
51
+ export declare function buildContainerConfigs(service: EcsServiceConfig, schemaVersionEnv?: Record<string, string>, annotationsScope?: Construct, chSchemaVersionEnv?: Record<string, string>): EcsClusterProps["services"][number]["containers"];
52
52
  /**
53
53
  * Resolved scaling configuration for an ECS service.
54
54
  * @internal Exported for testing only
@@ -92,6 +92,18 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
92
92
  * hardcoding one.
93
93
  */
94
94
  private resolveSchemaVersionEnv;
95
+ /**
96
+ * ClickHouse mirror of `resolveSchemaVersionEnv`. Returns the
97
+ * `EXPECTED_CH_SCHEMA_VERSION` env entry, or `undefined` when the service is
98
+ * not gated.
99
+ *
100
+ * - `schemaGate: false` → returns `undefined` (auditable opt-out — same flag
101
+ * covers BOTH PG and CH gates)
102
+ * - No migrated CH in connections → returns `undefined`
103
+ * - Exactly one migrated CH → returns `{ EXPECTED_CH_SCHEMA_VERSION }`
104
+ * - Two or more migrated CHs → throws via `resolveClickHouseDatabaseForService`
105
+ */
106
+ private resolveClickHouseSchemaVersionEnv;
95
107
  private materialiseScheduledTasks;
96
108
  private buildScheduledTaskDefinition;
97
109
  /**
@@ -7,7 +7,7 @@ import { Secret } from "aws-cdk-lib/aws-secretsmanager";
7
7
  import { StringParameter } from "aws-cdk-lib/aws-ssm";
8
8
  import { Construct } from "constructs";
9
9
  import { resolveAlertsTopic } from "../../utils/resolveAlertsTopic.js";
10
- import { EXPECTED_SCHEMA_VERSION_ENV, EXPECTED_SCHEMA_VERSION_TOOL_ENV, isDatabase, isRelationalDatabase } from "./interfaces/database.js";
10
+ import { EXPECTED_SCHEMA_VERSION_ENV, EXPECTED_SCHEMA_VERSION_TOOL_ENV, EXPECTED_CH_SCHEMA_VERSION_ENV, isClickHouseDatabase, isDatabase, isRelationalDatabase } from "./interfaces/database.js";
11
11
  import { isConnectionConfig } from "../../utils/connector.js";
12
12
  import { isMigrationContributor } from "./interfaces/migrationContributor.js";
13
13
  import App from "../../app.js";
@@ -274,6 +274,37 @@ function resolveMigrationDatabaseForService(service) {
274
274
  }
275
275
  return matches[0];
276
276
  }
277
+ /**
278
+ * ClickHouse mirror of `resolveMigrationDatabaseForService`. Two-CH-per-service
279
+ * is rejected for the same reason: the gate can only inject one
280
+ * `EXPECTED_CH_SCHEMA_VERSION`.
281
+ */
282
+ function resolveClickHouseDatabaseForService(service) {
283
+ const connections = service.connections;
284
+ if (connections === undefined || connections.length === 0)
285
+ return undefined;
286
+ const matches = [];
287
+ for (const spec of connections) {
288
+ const resource = isConnectionConfig(spec) ? spec.resource : spec;
289
+ if (!isDatabase(resource))
290
+ continue;
291
+ if (!isClickHouseDatabase(resource))
292
+ continue;
293
+ if (resource.getMigrationsConfig() !== undefined)
294
+ matches.push(resource);
295
+ }
296
+ if (matches.length === 0)
297
+ return undefined;
298
+ if (matches.length > 1) {
299
+ const names = matches.map((d) => d.node.id).join(", ");
300
+ throw new Error(`Service '${service.name}': connections include two or more ClickHouse ` +
301
+ `databases declaring \`migrations:\` (${names}). The schema-version ` +
302
+ `gate cannot inject ${EXPECTED_CH_SCHEMA_VERSION_ENV} unambiguously. ` +
303
+ `Split the service so each consumes a single migrated database, or ` +
304
+ `set \`schemaGate: false\` and inject the env manually.`);
305
+ }
306
+ return matches[0];
307
+ }
277
308
  function getResourceLabel(resource) {
278
309
  if (resource !== null &&
279
310
  typeof resource === "object" &&
@@ -436,7 +467,7 @@ function mergeContributionsIntoSeparateTaskDef(separateTaskDef, contributions) {
436
467
  * themselves and the resolved value differs.
437
468
  * @internal Exported for testing only
438
469
  */
439
- export function buildContainerConfigs(service, schemaVersionEnv, annotationsScope) {
470
+ export function buildContainerConfigs(service, schemaVersionEnv, annotationsScope, chSchemaVersionEnv) {
440
471
  const userContainers = service.containers && service.containers.length > 0
441
472
  ? service.containers
442
473
  : undefined;
@@ -464,19 +495,43 @@ export function buildContainerConfigs(service, schemaVersionEnv, annotationsScop
464
495
  };
465
496
  }
466
497
  const merged = { ...(authored ?? {}) };
467
- for (const [key, value] of Object.entries(schemaVersionEnv)) {
468
- if (merged[key] === undefined)
469
- merged[key] = value;
498
+ if (schemaVersionEnv[EXPECTED_SCHEMA_VERSION_ENV] !== undefined) {
499
+ merged[EXPECTED_SCHEMA_VERSION_ENV] =
500
+ schemaVersionEnv[EXPECTED_SCHEMA_VERSION_ENV];
501
+ }
502
+ if (schemaVersionEnv[EXPECTED_SCHEMA_VERSION_TOOL_ENV] !== undefined) {
503
+ merged[EXPECTED_SCHEMA_VERSION_TOOL_ENV] =
504
+ schemaVersionEnv[EXPECTED_SCHEMA_VERSION_TOOL_ENV];
470
505
  }
471
506
  return merged;
472
507
  };
508
+ const mergeChSchemaEnv = (authored) => {
509
+ if (chSchemaVersionEnv === undefined)
510
+ return authored;
511
+ const authoredValue = authored?.[EXPECTED_CH_SCHEMA_VERSION_ENV];
512
+ const resolvedValue = chSchemaVersionEnv[EXPECTED_CH_SCHEMA_VERSION_ENV];
513
+ if (authoredValue !== undefined) {
514
+ if (authoredValue !== resolvedValue && annotationsScope !== undefined) {
515
+ Annotations.of(annotationsScope).addWarning(`Service '${service.name}': author-set ${EXPECTED_CH_SCHEMA_VERSION_ENV}` +
516
+ `='${authoredValue}' overrides the value resolved from the connected ` +
517
+ `ClickHouse database's \`migrations:\` config ('${resolvedValue}'). The ` +
518
+ `author value wins; set \`schemaGate: false\` to silence this warning.`);
519
+ }
520
+ return authored;
521
+ }
522
+ return {
523
+ ...(authored ?? {}),
524
+ [EXPECTED_CH_SCHEMA_VERSION_ENV]: resolvedValue
525
+ };
526
+ };
527
+ const mergeAllSchemaEnv = (authored) => mergeChSchemaEnv(mergeSchemaEnv(authored));
473
528
  if (expanded) {
474
529
  return expanded.map((c, index) => {
475
530
  return {
476
531
  name: c.name || `${service.name}Container${index > 0 ? index : ""}`,
477
532
  image: c.image,
478
533
  port: c.port,
479
- environment: mergeSchemaEnv(c.environment),
534
+ environment: mergeAllSchemaEnv(c.environment),
480
535
  secrets: c.secrets,
481
536
  secretsImport: c.secretsImport,
482
537
  command: c.command,
@@ -490,7 +545,7 @@ export function buildContainerConfigs(service, schemaVersionEnv, annotationsScop
490
545
  };
491
546
  });
492
547
  }
493
- const fallbackEnv = mergeSchemaEnv(undefined);
548
+ const fallbackEnv = mergeAllSchemaEnv(undefined);
494
549
  return [
495
550
  {
496
551
  name: `${service.name}Container`,
@@ -562,7 +617,8 @@ export class EcsCompute extends Construct {
562
617
  this.appName = props.appName;
563
618
  const services = props.services.map((service) => {
564
619
  const schemaVersionEnv = this.resolveSchemaVersionEnv(service);
565
- const containers = buildContainerConfigs(service, schemaVersionEnv, this);
620
+ const chSchemaVersionEnv = this.resolveClickHouseSchemaVersionEnv(service);
621
+ const containers = buildContainerConfigs(service, schemaVersionEnv, this, chSchemaVersionEnv);
566
622
  const { scalingType, minCapacity, maxCapacity } = resolveScalingConfig(service.scaling);
567
623
  const cloudMapService = service.serviceDiscovery !== undefined
568
624
  ? App.getInstance().registerService({
@@ -670,6 +726,30 @@ export class EcsCompute extends Construct {
670
726
  [EXPECTED_SCHEMA_VERSION_TOOL_ENV]: config.tool
671
727
  };
672
728
  }
729
+ /**
730
+ * ClickHouse mirror of `resolveSchemaVersionEnv`. Returns the
731
+ * `EXPECTED_CH_SCHEMA_VERSION` env entry, or `undefined` when the service is
732
+ * not gated.
733
+ *
734
+ * - `schemaGate: false` → returns `undefined` (auditable opt-out — same flag
735
+ * covers BOTH PG and CH gates)
736
+ * - No migrated CH in connections → returns `undefined`
737
+ * - Exactly one migrated CH → returns `{ EXPECTED_CH_SCHEMA_VERSION }`
738
+ * - Two or more migrated CHs → throws via `resolveClickHouseDatabaseForService`
739
+ */
740
+ resolveClickHouseSchemaVersionEnv(service) {
741
+ if (service.schemaGate === false)
742
+ return undefined;
743
+ const db = resolveClickHouseDatabaseForService(service);
744
+ if (db === undefined)
745
+ return undefined;
746
+ const version = db.getExpectedSchemaVersion();
747
+ if (version === undefined)
748
+ return undefined;
749
+ return {
750
+ [EXPECTED_CH_SCHEMA_VERSION_ENV]: version
751
+ };
752
+ }
673
753
  materialiseScheduledTasks(id, props) {
674
754
  const scheduledTasks = props.cluster?.scheduledTasks;
675
755
  if (!scheduledTasks || scheduledTasks.length === 0)