Skip to content

Commit d6e00c3

Browse files
committed
feat(infra): #353 wire SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID to enable edge invalidation
Activates the durable CloudFront-invalidation path shipped dormant in #823/#826. Wires envConfig.cloudFrontDistributionId into the app web task and the cdn-reconcile task environment, so the synchronous post-commit purge (lib/edit/revalidation.ts) and the outbox reconciler both run. Distribution id was deliberately omitted before to keep the path dormant. Per-env: staging -> E17NRWINXLP3B3, prod -> E28NKDFXC7K2ZL (config.ts). The web + cdn-reconcile task roles already carry the matching cloudfront:CreateInvalidation grant (distribution-scoped). The synth-time guard test flips from asserting absence to asserting the prod distribution id. DRAFT / gated: only merge + deploy at the #502 prod edge cutover (CloudFront stays as CDN). The env var needs an explicit cdk deploy Sps-App-<env> + Sps-Etl-<env> (the GitHub workflow only re-rolls the image). Deploy staging first as a canary, then prod. Refs #353
1 parent 59f8831 commit d6e00c3

5 files changed

Lines changed: 48 additions & 18 deletions

File tree

cdk/lib/app-stack.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,12 @@ export class AppStack extends Stack {
884884
OPENSEARCH_NODE: `https://${Fn.importValue(
885885
`Sps-Data-${env}-OpenSearchDomainEndpoint`,
886886
)}`,
887+
// #353 -- CloudFront distribution id for the synchronous post-commit
888+
// edge purge (lib/edit/revalidation.ts invalidateCloudFront). Populated
889+
// in both envs; before this the var was unset so the purge no-opped.
890+
// The web task role carries the matching cloudfront:CreateInvalidation
891+
// grant (TaskRoleCloudFrontPolicy). Enable rides the #502 edge cutover.
892+
SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID: envConfig.cloudFrontDistributionId,
887893
// SAML SP non-secret config (#466). Without these, getSamlEnv()'s
888894
// requireEnv throws on the first missing var and every SAML route
889895
// 503s ("SAML SP is not configured"); SP-initiated sign-in is dead.

cdk/lib/etl-stack.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,11 +1295,12 @@ export class EtlStack extends Stack {
12951295
// outage / SDK error; the ~5 min SLA is that recovery floor, not everyday
12961296
// edge-cache purge latency.
12971297
//
1298-
// Dormant-safe: the task injects NO SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID, so
1299-
// the worker no-ops without touching the DB (empty-queue-safe) until the
1300-
// operator supplies the distribution id at enable time -- exactly as the
1301-
// synchronous invalidation path is dormant pre-launch. This keeps the
1302-
// reconciler decoupled from the #502-frozen EdgeStack distribution.
1298+
// #353 enabled: the task injects SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID from
1299+
// envConfig.cloudFrontDistributionId so the reconciler drains the
1300+
// cdn_invalidation outbox against the EdgeStack distribution. Empty-queue-
1301+
// safe, so it stays a no-op whenever the outbox is empty. Enable lands with
1302+
// the #502 prod edge cutover (CloudFront stays as CDN); deploy via
1303+
// `cdk deploy Sps-Etl-<env>` (the env var needs CDK, not just an image roll).
13031304
// ------------------------------------------------------------------
13041305
const cdnReconcileLogGroup = new logs.LogGroup(
13051306
this,
@@ -1402,9 +1403,11 @@ export class EtlStack extends Stack {
14021403
}),
14031404
environment: {
14041405
NODE_ENV: "production",
1405-
// NO SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID: dormant-safe -- the worker
1406-
// no-ops without touching the DB until the operator supplies it at
1407-
// enable time. No OPENSEARCH_* either (this worker never reads it).
1406+
// #353 enable -- wire the EdgeStack distribution id so the reconciler
1407+
// drains the cdn_invalidation outbox. envConfig.cloudFrontDistributionId
1408+
// is populated in both envs; this was omitted while the path was
1409+
// dormant. No OPENSEARCH_* (this worker never reads it).
1410+
SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID: envConfig.cloudFrontDistributionId,
14081411
},
14091412
secrets: {
14101413
// db.read + db.write collapse onto this single DSN (no

cdk/test/__snapshots__/app-stack.test.ts.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ exports[`AppStack prod matches the snapshot 1`] = `
291291
],
292292
},
293293
},
294+
{
295+
"Name": "SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID",
296+
"Value": "E28NKDFXC7K2ZL",
297+
},
294298
{
295299
"Name": "SAML_IDP_ENTITY_ID",
296300
"Value": "https://login-proxy.weill.cornell.edu/idp",
@@ -3295,6 +3299,10 @@ exports[`AppStack staging matches the snapshot 1`] = `
32953299
],
32963300
},
32973301
},
3302+
{
3303+
"Name": "SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID",
3304+
"Value": "E17NRWINXLP3B3",
3305+
},
32983306
{
32993307
"Name": "SAML_IDP_ENTITY_ID",
33003308
"Value": "https://login-proxy.weill.cornell.edu/idp",

cdk/test/__snapshots__/etl-stack.test.ts.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,10 @@ exports[`EtlStack prod matches the snapshot 1`] = `
925925
"Name": "NODE_ENV",
926926
"Value": "production",
927927
},
928+
{
929+
"Name": "SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID",
930+
"Value": "E28NKDFXC7K2ZL",
931+
},
928932
],
929933
"Essential": true,
930934
"Image": {
@@ -5914,6 +5918,10 @@ exports[`EtlStack staging matches the snapshot 1`] = `
59145918
"Name": "NODE_ENV",
59155919
"Value": "production",
59165920
},
5921+
{
5922+
"Name": "SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID",
5923+
"Value": "E17NRWINXLP3B3",
5924+
},
59175925
],
59185926
"Essential": true,
59195927
"Image": {

cdk/test/etl-stack.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ describe("EtlStack", () => {
682682
expect(td.Properties?.Memory).toBe("512");
683683
});
684684

685-
it("injects ONLY the DATABASE_URL secret (no OPENSEARCH_*, no SCHOLARS_*) and omits SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID (dormant-safe)", () => {
685+
it("injects ONLY the DATABASE_URL secret (no OPENSEARCH_*/SCHOLARS_* secrets) and wires the SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID env to the distribution id (#353 enabled)", () => {
686686
const td = cdnReconcileTaskDef();
687687
const container = (
688688
td.Properties?.ContainerDefinitions as
@@ -703,16 +703,21 @@ describe("EtlStack", () => {
703703
/^OPENSEARCH_/.test(n ?? ""),
704704
);
705705
expect(leaked).toEqual([]);
706-
// Dormant-safe: no distribution id is hardcoded onto the task; the
707-
// worker no-ops until the operator supplies it at enable time.
708-
const envNames = (
709-
container?.Environment as Array<{ Name?: string }> | undefined
710-
)?.map((e) => e.Name);
711-
expect(envNames ?? []).not.toContain(
712-
"SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID",
706+
// #353 enabled: the distribution id is wired (plaintext env, not a
707+
// secret) so the reconciler drains the outbox; the prod template gets
708+
// the prod EdgeStack distribution. Was omitted while the path was dormant.
709+
const envEntries =
710+
(container?.Environment as
711+
| Array<{ Name?: string; Value?: string }>
712+
| undefined) ?? [];
713+
const distEntry = envEntries.find(
714+
(e) => e.Name === "SCHOLARS_CLOUDFRONT_DISTRIBUTION_ID",
713715
);
714-
// No OpenSearch endpoint either.
715-
expect(envNames ?? []).not.toContain("OPENSEARCH_NODE");
716+
// Prod EdgeStack distribution (config.ts prod.cloudFrontDistributionId);
717+
// staging's is E17NRWINXLP3B3.
718+
expect(distEntry?.Value).toBe("E28NKDFXC7K2ZL");
719+
// No OpenSearch endpoint either -- this worker never reads it.
720+
expect(envEntries.map((e) => e.Name)).not.toContain("OPENSEARCH_NODE");
716721
});
717722

718723
it("the cdn reconcile exec role lists EXACTLY ONE secret ARN (db/etl; no opensearch, no *)", () => {

0 commit comments

Comments
 (0)