canopycms-cdk 0.0.61-int.75 → 0.0.61

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.
@@ -1,5 +1,5 @@
1
1
  import { Construct } from 'constructs';
2
- import { RemovalPolicy, aws_cloudfront as cloudfront, aws_iam as iam, aws_lambda as lambda, aws_s3 as s3 } from 'aws-cdk-lib';
2
+ import { RemovalPolicy, aws_cloudfront as cloudfront, aws_iam as iam, aws_lambda as lambda, aws_logs as logs, aws_s3 as s3 } from 'aws-cdk-lib';
3
3
  export interface AssetSupportProps {
4
4
  /**
5
5
  * Use an existing bucket (BYO mode - e.g. a site's existing content
@@ -52,6 +52,40 @@ export interface AssetSupportProps {
52
52
  * @default false
53
53
  */
54
54
  readonly autoDeleteObjects?: boolean;
55
+ /**
56
+ * Require the transform Lambda's code asset to carry proof that it was
57
+ * built with its native sharp binary (the `.deployable` marker written by
58
+ * `build:lambda`). Leave this ON for anything that can reach a real
59
+ * deploy.
60
+ *
61
+ * Set to `false` ONLY in this package's own tests, which deliberately
62
+ * synth against the cheap `--skip-native` fixture bundle that
63
+ * `build:test-fixtures` produces - the suite never executes the handler,
64
+ * so the binary is irrelevant to what it asserts, and requiring a real
65
+ * build would put a live `npm install sharp` back in front of every test
66
+ * run (which is what kept these tests out of CI in the first place).
67
+ *
68
+ * Adopters never need this: the published package's asset is built by
69
+ * `prepack`'s full `build:lambda`, so the marker is always present.
70
+ *
71
+ * @default true
72
+ */
73
+ readonly requireDeployableBundle?: boolean;
74
+ /**
75
+ * Retention for the transform Lambda's CloudWatch log group (default:
76
+ * three months / 90 days).
77
+ */
78
+ readonly transformLogRetention?: logs.RetentionDays;
79
+ /**
80
+ * Name for the transform Lambda's CloudWatch log group (default:
81
+ * `/canopycms/<stackName>/transform`). Deliberately NOT
82
+ * `/aws/lambda/<function-name>` - see `transformLogGroup`'s comment in the
83
+ * constructor for why a CDK-managed group must avoid that exact name once
84
+ * the function has ever been deployed without one. Override to follow an
85
+ * org naming convention, or when instantiating this construct twice in one
86
+ * stack (the default name would collide).
87
+ */
88
+ readonly transformLogGroupName?: string;
55
89
  }
56
90
  /**
57
91
  * The two CloudFront behavior configs the asset system needs, keyed by the
@@ -107,7 +141,10 @@ export interface AssetCloudFrontBehaviors {
107
141
  * - The bucket's asset-prefix lifecycle rule + CORS (standalone mode only).
108
142
  * - The transform Lambda (`../../lambda/asset-transform/handler.ts`, built
109
143
  * via `pnpm run build:lambda` - see that script's doc comment for the
110
- * no-Docker sharp bundling approach) and its OAC-locked Function URL.
144
+ * no-Docker sharp bundling approach), its dedicated CloudWatch log group
145
+ * (custom name/retention/removal policy instead of the
146
+ * CloudFormation-implicit `/aws/lambda/<function-name>` group), and its
147
+ * OAC-locked Function URL.
111
148
  * - `assetBehaviors()`, the two CloudFront behavior configs a consuming
112
149
  * distribution attaches (see `AssetCloudFrontBehaviors`'s doc comment for
113
150
  * both attachment shapes).
@@ -119,6 +156,8 @@ export declare class AssetSupport extends Construct {
119
156
  readonly bucket: s3.IBucket;
120
157
  /** The transform Lambda function. */
121
158
  readonly transformFunction: lambda.Function;
159
+ /** The transform Lambda's CloudWatch log group (Lambda stdout/stderr). */
160
+ readonly transformLogGroup: logs.LogGroup;
122
161
  /** The transform Lambda's Function URL - use as a CloudFront origin (see `assetBehaviors()`). */
123
162
  readonly transformFunctionUrl: lambda.FunctionUrl;
124
163
  /** Effective advisory upload-size cap (see `AssetSupportProps.maxUploadBytes`). */
@@ -1 +1 @@
1
- {"version":3,"file":"asset-support.d.ts","sourceRoot":"","sources":["../../src/constructs/asset-support.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAEL,aAAa,EACb,cAAc,IAAI,UAAU,EAE5B,OAAO,IAAI,GAAG,EACd,UAAU,IAAI,MAAM,EACpB,MAAM,IAAI,EAAE,EACb,MAAM,aAAa,CAAA;AAwDpB,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,OAAO,CAAA;IAE5B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAA;IAEhC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAEhC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAE5B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAA;IAEtC;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,eAAe,CAAA;IAE3C;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,eAAe,CAAA;CACrD;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,YAAa,SAAQ,SAAS;IACzC,0EAA0E;IAC1E,SAAgB,MAAM,EAAE,EAAE,CAAC,OAAO,CAAA;IAElC,qCAAqC;IACrC,SAAgB,iBAAiB,EAAE,MAAM,CAAC,QAAQ,CAAA;IAElD,iGAAiG;IACjG,SAAgB,oBAAoB,EAAE,MAAM,CAAC,WAAW,CAAA;IAExD,mFAAmF;IACnF,SAAgB,cAAc,EAAE,MAAM,CAAA;IAEtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAExC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB;IAgFlE,OAAO,CAAC,cAAc;IA+CtB,uIAAuI;IAChI,cAAc,IAAI,wBAAwB;IAIjD;;;;;;;;;OASG;IACI,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,UAAU,GAAG,IAAI;CAexD"}
1
+ {"version":3,"file":"asset-support.d.ts","sourceRoot":"","sources":["../../src/constructs/asset-support.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAEL,aAAa,EAEb,cAAc,IAAI,UAAU,EAE5B,OAAO,IAAI,GAAG,EACd,UAAU,IAAI,MAAM,EACpB,QAAQ,IAAI,IAAI,EAChB,MAAM,IAAI,EAAE,EACb,MAAM,aAAa,CAAA;AAiFpB,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,OAAO,CAAA;IAE5B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAA;IAEhC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAEhC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAE5B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAA;IAEtC;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAEpC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAA;IAE1C;;;OAGG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,IAAI,CAAC,aAAa,CAAA;IAEnD;;;;;;;;OAQG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;CACxC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,eAAe,CAAA;IAE3C;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,EAAE,UAAU,CAAC,eAAe,CAAA;CACrD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,YAAa,SAAQ,SAAS;IACzC,0EAA0E;IAC1E,SAAgB,MAAM,EAAE,EAAE,CAAC,OAAO,CAAA;IAElC,qCAAqC;IACrC,SAAgB,iBAAiB,EAAE,MAAM,CAAC,QAAQ,CAAA;IAElD,0EAA0E;IAC1E,SAAgB,iBAAiB,EAAE,IAAI,CAAC,QAAQ,CAAA;IAEhD,iGAAiG;IACjG,SAAgB,oBAAoB,EAAE,MAAM,CAAC,WAAW,CAAA;IAExD,mFAAmF;IACnF,SAAgB,cAAc,EAAE,MAAM,CAAA;IAEtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAExC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB;IA2IlE,OAAO,CAAC,cAAc;IA+CtB,uIAAuI;IAChI,cAAc,IAAI,wBAAwB;IAIjD;;;;;;;;;OASG;IACI,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,UAAU,GAAG,IAAI;CAexD"}
@@ -1,11 +1,35 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import * as path from 'node:path';
2
3
  import { fileURLToPath } from 'node:url';
3
4
  import { Construct } from 'constructs';
4
- import { Duration, RemovalPolicy, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_lambda as lambda, aws_s3 as s3, } from 'aws-cdk-lib';
5
+ import { Duration, RemovalPolicy, Stack, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_lambda as lambda, aws_logs as logs, aws_s3 as s3, } from 'aws-cdk-lib';
5
6
  // This package (`canopycms-cdk`) is `"type": "module"`, so its compiled
6
7
  // output is real ESM - `__dirname` is not a global there. Derive it from
7
8
  // `import.meta.url` instead (mirrors ../../lambda/asset-transform/build.mjs).
8
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ /**
11
+ * The transform Lambda's built code asset. Computed ONCE and shared by the
12
+ * deployability guard and `Code.fromAsset()` below - if the check and the
13
+ * bundle ever read two independently-computed paths, the guard can silently
14
+ * start statting a directory that is never the one being deployed, and
15
+ * nothing about either call site would look wrong.
16
+ *
17
+ * The `'..', '..'` walk is correct from both `<pkg>/src/constructs/` (local
18
+ * source) and `<pkg>/dist/constructs/` (published package) only because those
19
+ * sit at the same depth - see the `__dirname` note above.
20
+ */
21
+ const transformAssetDir = path.join(__dirname, '..', '..', 'lambda', 'asset-transform', 'dist');
22
+ /**
23
+ * Written by `build:lambda` as the last act of a successful FULL build, once
24
+ * the linux/arm64 sharp binary is verified present. See that script's header
25
+ * for why the marker is positive rather than negative: a partial build (a
26
+ * thrown `npm install sharp`, a failed platform check) leaves a sharp-less
27
+ * `dist/` on disk WITHOUT reaching the `--skip-native` branch, so a
28
+ * "is it marked bad?" test would wave exactly that bundle through to a
29
+ * deploy. Requiring proof-of-good instead fails closed on every unexpected
30
+ * path, including the producer's and consumer's paths drifting apart.
31
+ */
32
+ const DEPLOYABLE_MARKER = '.deployable';
9
33
  /**
10
34
  * The four S3 key prefixes the asset system uses, mirrored from
11
35
  * `packages/canopycms/src/assets/asset-prefixes.ts` (the source of truth).
@@ -58,7 +82,10 @@ const TRANSFORM_CACHE_MAX_TTL = Duration.days(365);
58
82
  * - The bucket's asset-prefix lifecycle rule + CORS (standalone mode only).
59
83
  * - The transform Lambda (`../../lambda/asset-transform/handler.ts`, built
60
84
  * via `pnpm run build:lambda` - see that script's doc comment for the
61
- * no-Docker sharp bundling approach) and its OAC-locked Function URL.
85
+ * no-Docker sharp bundling approach), its dedicated CloudWatch log group
86
+ * (custom name/retention/removal policy instead of the
87
+ * CloudFormation-implicit `/aws/lambda/<function-name>` group), and its
88
+ * OAC-locked Function URL.
62
89
  * - `assetBehaviors()`, the two CloudFront behavior configs a consuming
63
90
  * distribution attaches (see `AssetCloudFrontBehaviors`'s doc comment for
64
91
  * both attachment shapes).
@@ -68,6 +95,25 @@ const TRANSFORM_CACHE_MAX_TTL = Duration.days(365);
68
95
  export class AssetSupport extends Construct {
69
96
  constructor(scope, id, props) {
70
97
  super(scope, id);
98
+ // Fail closed before anything else: refuse to build a stack around a
99
+ // transform Lambda whose code asset was never verified to contain the
100
+ // linux/arm64 sharp binary. Without this, `pnpm test` (whose
101
+ // canopycms-cdk suite rebuilds that directory as a --skip-native
102
+ // fixture) or any partially-failed build leaves a sharp-less bundle on
103
+ // disk, and a later in-repo `cdk deploy` ships it - producing a Lambda
104
+ // that throws at cold start on the first image request, a long way from
105
+ // the cause. Guarding in the construct rather than at one deploy
106
+ // entrypoint means future entrypoints inherit the protection instead of
107
+ // having to remember it.
108
+ if ((props.requireDeployableBundle ?? true) &&
109
+ !existsSync(path.join(transformAssetDir, DEPLOYABLE_MARKER))) {
110
+ throw new Error(`The transform Lambda code asset at ${transformAssetDir} carries no ${DEPLOYABLE_MARKER} ` +
111
+ 'marker, so it was not built with its native sharp binary and must not be deployed. ' +
112
+ 'This is usually a --skip-native fixture bundle left behind by `pnpm test`, or a ' +
113
+ '`build:lambda` run that failed partway through its sharp install.\n' +
114
+ 'Build it for real:\n' +
115
+ ' pnpm --filter canopycms-cdk run build:lambda');
116
+ }
71
117
  this.maxUploadBytes = props.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
72
118
  if (props.bucket) {
73
119
  this.bucket = props.bucket;
@@ -101,13 +147,30 @@ export class AssetSupport extends Construct {
101
147
  ],
102
148
  });
103
149
  }
150
+ // Dedicated CloudWatch log group for the transform Lambda's stdout/
151
+ // stderr. Custom name (NOT the CloudFormation-implicit
152
+ // `/aws/lambda/<function name>`), for two reasons: (1) CDK does not
153
+ // manage that implicit group at all - infinite retention, and
154
+ // `cdk destroy` leaves it behind; (2) Lambda auto-creates
155
+ // `/aws/lambda/<function name>` on first invoke, OUTSIDE CloudFormation -
156
+ // once that has happened (e.g. this construct was already deployed
157
+ // before this log group existed), a CDK `LogGroup` construct using that
158
+ // exact name would fail its `CreateLogGroup` call with "already exists"
159
+ // and block every future `cdk deploy`. Mirrors `CanopyCmsService`'s
160
+ // `workerLogGroup`/`cmsLogGroup` (cms-service.ts), which established this
161
+ // convention.
162
+ this.transformLogGroup = new logs.LogGroup(this, 'TransformFunctionLogs', {
163
+ logGroupName: props.transformLogGroupName ?? `/canopycms/${Stack.of(this).stackName}/transform`,
164
+ retention: props.transformLogRetention ?? logs.RetentionDays.THREE_MONTHS,
165
+ removalPolicy: RemovalPolicy.DESTROY,
166
+ });
104
167
  this.transformFunction = new lambda.Function(this, 'TransformFunction', {
105
168
  // Built by `pnpm run build:lambda` (lambda/asset-transform/build.mjs) -
106
169
  // esbuild bundle + a real linux/arm64 `npm install sharp` alongside it,
107
170
  // no Docker. `cdk synth`/`deploy` need that script run first; it is
108
171
  // NOT run automatically here (kept explicit rather than magic - see
109
172
  // this construct's class doc comment).
110
- code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', 'lambda', 'asset-transform', 'dist')),
173
+ code: lambda.Code.fromAsset(transformAssetDir),
111
174
  handler: 'handler.handler',
112
175
  // nodejs20.x was deprecated 2026-04-30; CDK's CloudFormation validation
113
176
  // now fails synth on it. The esbuild bundle targets node20 and runs
@@ -116,10 +179,27 @@ export class AssetSupport extends Construct {
116
179
  architecture: lambda.Architecture.ARM_64,
117
180
  memorySize: TRANSFORM_LAMBDA_MEMORY_MB,
118
181
  timeout: TRANSFORM_LAMBDA_TIMEOUT,
182
+ // Pass the pre-created group via `logGroup`, NOT `logRetention` (CDK
183
+ // throws LogRetentionLogGroupConflict/ConflictingLogPolicyOptions if
184
+ // both are set on the same function) - the removal policy lives on the
185
+ // LogGroup construct above instead.
186
+ logGroup: this.transformLogGroup,
119
187
  environment: {
120
188
  ASSET_BUCKET: this.bucket.bucketName,
121
189
  },
122
190
  });
191
+ // Explicit, scoped grant - NOT a reliance on the auto-created execution
192
+ // role's AWSLambdaBasicExecutionRole managed policy (attached by CDK's
193
+ // lambda.Function regardless of `logGroup`, and never adjusted for it -
194
+ // passing `logGroup` only points the function's LoggingConfig at this
195
+ // group, it grants no IAM permissions). That managed policy's
196
+ // logs:CreateLogStream/logs:PutLogEvents statement is scoped to
197
+ // `arn:aws:logs:*:*:log-group:/aws/lambda/*:*` only (its
198
+ // logs:CreateLogGroup statement is the sole one that's unrestricted) -
199
+ // it grants nothing for a custom-named group like this one. Without this
200
+ // grantWrite, log delivery to this group would fail permission checks
201
+ // with no error surfaced anywhere - logs would simply vanish.
202
+ this.transformLogGroup.grantWrite(this.transformFunction);
123
203
  // Read access to originals (what it transforms) - deviation from the PR
124
204
  // spec's literal "read asset-originals/, write assets/" grant list: the
125
205
  // handler must also read `asset-meta/{hash32}.json` to look up kind/ext
@@ -1 +1 @@
1
- {"version":3,"file":"asset-support.js","sourceRoot":"","sources":["../../src/constructs/asset-support.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"asset-support.js","sourceRoot":"","sources":["../../src/constructs/asset-support.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EACL,QAAQ,EACR,aAAa,EACb,KAAK,EACL,cAAc,IAAI,UAAU,EAC5B,sBAAsB,IAAI,OAAO,EAEjC,UAAU,IAAI,MAAM,EACpB,QAAQ,IAAI,IAAI,EAChB,MAAM,IAAI,EAAE,GACb,MAAM,aAAa,CAAA;AAEpB,wEAAwE;AACxE,yEAAyE;AACzE,8EAA8E;AAC9E,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AAE9D;;;;;;;;;;GAUG;AACH,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAAA;AAE/F;;;;;;;;;GASG;AACH,MAAM,iBAAiB,GAAG,aAAa,CAAA;AAEvC;;;;;;;;;;;;GAYG;AACH,MAAM,QAAQ,GAAG;IACf,SAAS,EAAE,iBAAiB;IAC5B,OAAO,EAAE,eAAe;IACxB,IAAI,EAAE,YAAY;IAClB,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,UAAU;CACb,CAAA;AAEV,mFAAmF;AACnF,MAAM,oBAAoB,GAAG,IAAI,CAAA;AAEjC,sFAAsF;AACtF,MAAM,wBAAwB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;AAEjD,MAAM,0BAA0B,GAAG,IAAI,CAAA;AACvC,MAAM,wBAAwB,GAAG,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;AAErD;;;;;;;;;;;;;;GAcG;AACH,MAAM,uBAAuB,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AACnD,MAAM,2BAA2B,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACpD,MAAM,uBAAuB,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAkJlD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,YAAa,SAAQ,SAAS;IAkBzC,YAAY,KAAgB,EAAE,EAAU,EAAE,KAAwB;QAChE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;QAEhB,qEAAqE;QACrE,sEAAsE;QACtE,6DAA6D;QAC7D,iEAAiE;QACjE,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,iEAAiE;QACjE,wEAAwE;QACxE,yBAAyB;QACzB,IACE,CAAC,KAAK,CAAC,uBAAuB,IAAI,IAAI,CAAC;YACvC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,EAC5D,CAAC;YACD,MAAM,IAAI,KAAK,CACb,sCAAsC,iBAAiB,eAAe,iBAAiB,GAAG;gBACxF,qFAAqF;gBACrF,kFAAkF;gBAClF,qEAAqE;gBACrE,sBAAsB;gBACtB,gDAAgD,CACnD,CAAA;QACH,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,cAAc,IAAI,wBAAwB,CAAA;QAEtE,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;QAC5B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE;gBAC1C,iBAAiB,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS;gBACjD,UAAU,EAAE,EAAE,CAAC,gBAAgB,CAAC,UAAU;gBAC1C,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,KAAK;gBACnC,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM;gBAC1D,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,IAAI,KAAK;gBACnD,kEAAkE;gBAClE,6DAA6D;gBAC7D,0DAA0D;gBAC1D,cAAc,EAAE;oBACd;wBACE,EAAE,EAAE,sBAAsB;wBAC1B,OAAO,EAAE,IAAI;wBACb,MAAM,EAAE,GAAG,QAAQ,CAAC,OAAO,GAAG;wBAC9B,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;qBAC7B;iBACF;gBACD,IAAI,EAAE;oBACJ;wBACE,EAAE,EAAE,yBAAyB;wBAC7B,cAAc,EAAE,KAAK,CAAC,aAAa;wBACnC,cAAc,EAAE,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC;wBAC7E,cAAc,EAAE,CAAC,GAAG,CAAC;wBACrB,MAAM,EAAE,oBAAoB;qBAC7B;iBACF;aACF,CAAC,CAAA;QACJ,CAAC;QAED,oEAAoE;QACpE,uDAAuD;QACvD,oEAAoE;QACpE,8DAA8D;QAC9D,0DAA0D;QAC1D,0EAA0E;QAC1E,mEAAmE;QACnE,wEAAwE;QACxE,wEAAwE;QACxE,oEAAoE;QACpE,0EAA0E;QAC1E,cAAc;QACd,IAAI,CAAC,iBAAiB,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,EAAE;YACxE,YAAY,EACV,KAAK,CAAC,qBAAqB,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,SAAS,YAAY;YACnF,SAAS,EAAE,KAAK,CAAC,qBAAqB,IAAI,IAAI,CAAC,aAAa,CAAC,YAAY;YACzE,aAAa,EAAE,aAAa,CAAC,OAAO;SACrC,CAAC,CAAA;QAEF,IAAI,CAAC,iBAAiB,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,EAAE;YACtE,wEAAwE;YACxE,wEAAwE;YACxE,oEAAoE;YACpE,oEAAoE;YACpE,uCAAuC;YACvC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;YAC9C,OAAO,EAAE,iBAAiB;YAC1B,wEAAwE;YACxE,oEAAoE;YACpE,mCAAmC;YACnC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW;YACnC,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,MAAM;YACxC,UAAU,EAAE,0BAA0B;YACtC,OAAO,EAAE,wBAAwB;YACjC,qEAAqE;YACrE,qEAAqE;YACrE,uEAAuE;YACvE,oCAAoC;YACpC,QAAQ,EAAE,IAAI,CAAC,iBAAiB;YAChC,WAAW,EAAE;gBACX,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU;aACrC;SACF,CAAC,CAAA;QAEF,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,sEAAsE;QACtE,8DAA8D;QAC9D,gEAAgE;QAChE,yDAAyD;QACzD,uEAAuE;QACvE,yEAAyE;QACzE,sEAAsE;QACtE,8DAA8D;QAC9D,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAA;QAEzD,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,wEAAwE;QACxE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAA;QACxE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAA;QACnE,+DAA+D;QAC/D,+DAA+D;QAC/D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,CAAA;QAEpE,wEAAwE;QACxE,sEAAsE;QACtE,oEAAoE;QACpE,gDAAgD;QAChD,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC;YAChE,QAAQ,EAAE,MAAM,CAAC,mBAAmB,CAAC,OAAO;SAC7C,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,cAAc,EAAE,CAAA;IACxC,CAAC;IAEO,cAAc;QACpB,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,uBAAuB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC5E,MAAM,qBAAqB,GAAG,OAAO,CAAC,iBAAiB,CAAC,uBAAuB,CAC7E,IAAI,CAAC,oBAAoB,CAC1B,CAAA;QAED,MAAM,MAAM,GAA+B;YACzC,MAAM,EAAE,QAAQ;YAChB,oBAAoB,EAAE,UAAU,CAAC,oBAAoB,CAAC,iBAAiB;YACvE,WAAW,EAAE,UAAU,CAAC,WAAW,CAAC,iBAAiB;YACrD,QAAQ,EAAE,IAAI;SACf,CAAA;QAED,yEAAyE;QACzE,kEAAkE;QAClE,kEAAkE;QAClE,gEAAgE;QAChE,gCAAgC;QAChC,MAAM,oBAAoB,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,IAAI,EAAE,4BAA4B,EAAE;YAC1F,MAAM,EAAE,uBAAuB;YAC/B,UAAU,EAAE,2BAA2B;YACvC,MAAM,EAAE,uBAAuB;YAC/B,wBAAwB,EAAE,IAAI;YAC9B,0BAA0B,EAAE,IAAI;YAChC,mBAAmB,EAAE,UAAU,CAAC,wBAAwB,CAAC,IAAI,EAAE;YAC/D,cAAc,EAAE,UAAU,CAAC,mBAAmB,CAAC,IAAI,EAAE;YACrD,cAAc,EAAE,UAAU,CAAC,mBAAmB,CAAC,IAAI,EAAE;SACtD,CAAC,CAAA;QAEF,MAAM,eAAe,GAA+B;YAClD,MAAM,EAAE,IAAI,OAAO,CAAC,WAAW,CAAC;gBAC9B,aAAa,EAAE,QAAQ;gBACvB,cAAc,EAAE,qBAAqB;gBACrC,+DAA+D;gBAC/D,+DAA+D;gBAC/D,4DAA4D;gBAC5D,gEAAgE;gBAChE,gEAAgE;gBAChE,mBAAmB,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC;aAChC,CAAC;YACF,oBAAoB,EAAE,UAAU,CAAC,oBAAoB,CAAC,iBAAiB;YACvE,WAAW,EAAE,oBAAoB;SAClC,CAAA;QAED,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,CAAA;IACpC,CAAC;IAED,uIAAuI;IAChI,cAAc;QACnB,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IAED;;;;;;;;;OASG;IACI,iBAAiB,CAAC,OAAuB;QAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAA;QAEtD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAA;QACvD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAA;QACzD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAA;QACpD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,CAAA;QAEtD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,SAAS,IAAI,CAAC,CAAA;QACxD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAA;QACnD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,CAAA;QAErD,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAA;QACzD,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAA;IACxD,CAAC;CACF"}
@@ -38,6 +38,25 @@ export interface CanopyCmsServiceProps {
38
38
  clerkSecretKeySecretArn?: string;
39
39
  /** Base branch name (default: 'main') */
40
40
  baseBranch?: string;
41
+ /**
42
+ * Deployment name (default: 'prod'). Namespaces the settings branch
43
+ * (`canopycms-settings-{deploymentName}`) so this stack's CMS Lambda/worker
44
+ * don't fight another CanopyCMS deployment over the same orphan settings
45
+ * branch. Stamped into the Lambda's `CANOPYCMS_DEPLOYMENT_NAME` environment
46
+ * variable and the worker's `.env`; both resolve it through
47
+ * `resolveDeploymentName` (packages/canopycms/src/operating-mode/deployment-name.ts),
48
+ * which lets this env value win over any `deploymentName` baked into the
49
+ * shared repo's `canopycms.config.ts` — the point of this prop.
50
+ *
51
+ * Two `CanopyCmsService` stacks pointed at the SAME GitHub repo MUST set
52
+ * distinct values here, or both resolve to `canopycms-settings-prod` and
53
+ * fight over the same branch (permissions/groups changes reviewed on one PR
54
+ * clobber the other). Changing this value later, on a stack that already
55
+ * has a populated settings workspace, is refused at boot (see
56
+ * settings-workspace.ts's rename guard) rather than silently reset — plan
57
+ * the value up front for any stack that shares a repo.
58
+ */
59
+ deploymentName?: string;
41
60
  /**
42
61
  * The asset bucket (from `AssetSupport`, or any bucket following its
43
62
  * prefix layout) the CMS Lambda's role should be granted access to. When
@@ -49,7 +68,7 @@ export interface CanopyCmsServiceProps {
49
68
  assetBucket?: IBucket;
50
69
  /**
51
70
  * Retention for the EC2 worker's CloudWatch log group (default: three
52
- * months). Worker-only: the Lambdas keep their auto-created log groups.
71
+ * months).
53
72
  */
54
73
  workerLogRetention?: logs.RetentionDays;
55
74
  /**
@@ -59,6 +78,23 @@ export interface CanopyCmsServiceProps {
59
78
  * default name would collide).
60
79
  */
61
80
  workerLogGroupName?: string;
81
+ /**
82
+ * Retention for the CMS Lambda's CloudWatch log group (default: three
83
+ * months / 90 days).
84
+ */
85
+ cmsLogRetention?: logs.RetentionDays;
86
+ /**
87
+ * Name for the CMS Lambda's CloudWatch log group (default:
88
+ * `/canopycms/<stackName>/cms`). Deliberately NOT
89
+ * `/aws/lambda/<function-name>` - see the constructor's `cmsLogGroup`
90
+ * comment for why a CDK-managed group must avoid that exact name once the
91
+ * function has ever been deployed without one (CloudFormation's
92
+ * `CreateLogGroup` call fails "already exists" against a group Lambda
93
+ * itself auto-created outside CloudFormation). Override to follow an org
94
+ * naming convention, or when instantiating this construct twice in one
95
+ * stack (the default name would collide).
96
+ */
97
+ cmsLogGroupName?: string;
62
98
  }
63
99
  /**
64
100
  * Core CDK construct for CanopyCMS deployment.
@@ -68,12 +104,21 @@ export interface CanopyCmsServiceProps {
68
104
  * - EFS filesystem with access point at /workspace
69
105
  * - Lambda function (Docker image, EFS mount, private subnet, no internet)
70
106
  * - Lambda Function URL (for CloudFront origin)
71
- * - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount, systemd)
72
- * - Dedicated CloudWatch log group for the worker's stdout/stderr, shipped
73
- * via the amazon-cloudwatch-agent (journald is not agent-readable)
107
+ * - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount, systemd) -
108
+ * rolled on every deploy via the ASG's UpdatePolicy, so a changed worker
109
+ * bundle actually reaches the instance instead of sitting unused in a
110
+ * launch template until the next spot interruption (see the UpdatePolicy
111
+ * below)
112
+ * - Dedicated CloudWatch log groups for the CMS Lambda and the worker's
113
+ * stdout/stderr (the worker's is shipped via the amazon-cloudwatch-agent -
114
+ * journald is not agent-readable), each with a custom name/retention/
115
+ * removal policy instead of the CloudFormation-implicit
116
+ * `/aws/lambda/<function-name>` group (infinite retention, survives
117
+ * `cdk destroy`)
74
118
  * - Security groups (least-privilege)
75
- * - IAM roles (Lambda: EFS only; EC2: EFS + Secrets Manager + CloudWatch
76
- * Logs write, scoped to its own log group)
119
+ * - IAM roles (Lambda: EFS + CloudWatch Logs write scoped to its own log
120
+ * group; EC2: EFS + Secrets Manager + CloudWatch Logs write, scoped to its
121
+ * own log group)
77
122
  */
78
123
  export declare class CanopyCmsService extends Construct {
79
124
  /** Lambda Function URL — use as CloudFront origin */
@@ -84,6 +129,8 @@ export declare class CanopyCmsService extends Construct {
84
129
  readonly vpc: ec2.IVpc;
85
130
  /** The Lambda function */
86
131
  readonly lambdaFunction: lambda.Function;
132
+ /** The CMS Lambda's CloudWatch log group (Lambda stdout/stderr) */
133
+ readonly cmsLogGroup: logs.LogGroup;
87
134
  /** The EC2 worker Auto Scaling Group */
88
135
  readonly workerAsg: autoscaling.AutoScalingGroup;
89
136
  /** The EC2 worker's CloudWatch log group (worker stdout/stderr) */
@@ -1 +1 @@
1
- {"version":3,"file":"cms-service.d.ts","sourceRoot":"","sources":["../../src/constructs/cms-service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EACL,QAAQ,EACR,aAAa,EAEb,OAAO,IAAI,GAAG,EACd,OAAO,IAAI,GAAG,EAEd,UAAU,IAAI,MAAM,EACpB,eAAe,IAAI,WAAW,EAE9B,QAAQ,IAAI,IAAI,EACjB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAWjD,MAAM,WAAW,qBAAqB;IACpC,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAC,eAAe,CAAA;IAEtC,4DAA4D;IAC5D,GAAG,CAAC,EAAE,GAAG,CAAC,IAAI,CAAA;IAEd,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,QAAQ,CAAA;IAElB,oDAAoD;IACpD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,YAAY,CAAA;IAElC,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IAEtB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEpC,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,aAAa,CAAA;IAEhC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAA;IAEnB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAElB,mDAAmD;IACnD,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAE7B,mDAAmD;IACnD,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAEhC,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,IAAI,CAAC,aAAa,CAAA;IAEvC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;IAC7C,qDAAqD;IACrD,SAAgB,WAAW,EAAE,MAAM,CAAC,WAAW,CAAA;IAE/C,yBAAyB;IACzB,SAAgB,UAAU,EAAE,GAAG,CAAC,UAAU,CAAA;IAE1C,cAAc;IACd,SAAgB,GAAG,EAAE,GAAG,CAAC,IAAI,CAAA;IAE7B,0BAA0B;IAC1B,SAAgB,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAA;IAE/C,wCAAwC;IACxC,SAAgB,SAAS,EAAE,WAAW,CAAC,gBAAgB,CAAA;IAEvD,mEAAmE;IACnE,SAAgB,cAAc,EAAE,IAAI,CAAC,QAAQ,CAAA;gBAEjC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB;CAgbvE"}
1
+ {"version":3,"file":"cms-service.d.ts","sourceRoot":"","sources":["../../src/constructs/cms-service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EACL,QAAQ,EACR,aAAa,EAEb,OAAO,IAAI,GAAG,EACd,OAAO,IAAI,GAAG,EAEd,UAAU,IAAI,MAAM,EACpB,eAAe,IAAI,WAAW,EAE9B,QAAQ,IAAI,IAAI,EACjB,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAwBjD,MAAM,WAAW,qBAAqB;IACpC,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAC,eAAe,CAAA;IAEtC,4DAA4D;IAC5D,GAAG,CAAC,EAAE,GAAG,CAAC,IAAI,CAAA;IAEd,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,QAAQ,CAAA;IAElB,oDAAoD;IACpD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,YAAY,CAAA;IAElC,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;IAEtB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEpC,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,aAAa,CAAA;IAEhC,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAA;IAEnB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAElB,mDAAmD;IACnD,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAE7B,mDAAmD;IACnD,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAEhC,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IAEvB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,IAAI,CAAC,aAAa,CAAA;IAEvC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAE3B;;;OAGG;IACH,eAAe,CAAC,EAAE,IAAI,CAAC,aAAa,CAAA;IAEpC;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,gBAAiB,SAAQ,SAAS;IAC7C,qDAAqD;IACrD,SAAgB,WAAW,EAAE,MAAM,CAAC,WAAW,CAAA;IAE/C,yBAAyB;IACzB,SAAgB,UAAU,EAAE,GAAG,CAAC,UAAU,CAAA;IAE1C,cAAc;IACd,SAAgB,GAAG,EAAE,GAAG,CAAC,IAAI,CAAA;IAE7B,0BAA0B;IAC1B,SAAgB,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAA;IAE/C,mEAAmE;IACnE,SAAgB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAA;IAE1C,wCAAwC;IACxC,SAAgB,SAAS,EAAE,WAAW,CAAC,gBAAgB,CAAA;IAEvD,mEAAmE;IACnE,SAAgB,cAAc,EAAE,IAAI,CAAC,QAAQ,CAAA;gBAEjC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB;CAgjBvE"}
@@ -10,6 +10,17 @@ import { Duration, RemovalPolicy, Stack, aws_ec2 as ec2, aws_efs as efs, aws_iam
10
10
  // SSR/CJS-interop transform shims `__dirname` automatically. Same fix as
11
11
  // ../../lambda/asset-transform/build.mjs and ./asset-support.ts.
12
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ /**
14
+ * Synth-time mirror of `resolveDeploymentName`'s rule in the `canopycms`
15
+ * package (packages/canopycms/src/operating-mode/deployment-name.ts).
16
+ * Duplicated rather than imported: `canopycms-cdk` deliberately does not
17
+ * depend on the runtime package. Keep the two in step — see the constructor
18
+ * guard for why this is checked here as well as at runtime.
19
+ */
20
+ const isValidDeploymentName = (name) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) &&
21
+ !name.includes('..') &&
22
+ !name.endsWith('.') &&
23
+ !name.endsWith('.lock');
13
24
  /**
14
25
  * Core CDK construct for CanopyCMS deployment.
15
26
  *
@@ -18,16 +29,37 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
29
  * - EFS filesystem with access point at /workspace
19
30
  * - Lambda function (Docker image, EFS mount, private subnet, no internet)
20
31
  * - Lambda Function URL (for CloudFront origin)
21
- * - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount, systemd)
22
- * - Dedicated CloudWatch log group for the worker's stdout/stderr, shipped
23
- * via the amazon-cloudwatch-agent (journald is not agent-readable)
32
+ * - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount, systemd) -
33
+ * rolled on every deploy via the ASG's UpdatePolicy, so a changed worker
34
+ * bundle actually reaches the instance instead of sitting unused in a
35
+ * launch template until the next spot interruption (see the UpdatePolicy
36
+ * below)
37
+ * - Dedicated CloudWatch log groups for the CMS Lambda and the worker's
38
+ * stdout/stderr (the worker's is shipped via the amazon-cloudwatch-agent -
39
+ * journald is not agent-readable), each with a custom name/retention/
40
+ * removal policy instead of the CloudFormation-implicit
41
+ * `/aws/lambda/<function-name>` group (infinite retention, survives
42
+ * `cdk destroy`)
24
43
  * - Security groups (least-privilege)
25
- * - IAM roles (Lambda: EFS only; EC2: EFS + Secrets Manager + CloudWatch
26
- * Logs write, scoped to its own log group)
44
+ * - IAM roles (Lambda: EFS + CloudWatch Logs write scoped to its own log
45
+ * group; EC2: EFS + Secrets Manager + CloudWatch Logs write, scoped to its
46
+ * own log group)
27
47
  */
28
48
  export class CanopyCmsService extends Construct {
29
49
  constructor(scope, id, props) {
30
50
  super(scope, id);
51
+ // Fail at synth, not at boot. deploymentName is interpolated BOTH into a
52
+ // git ref (`canopycms-settings-{deploymentName}`) and into a line of the
53
+ // worker's `.env`, which user-data writes with a shell heredoc — a value
54
+ // carrying a newline or quote would corrupt the worker's environment file
55
+ // before any runtime validation could run. Mirrors the package-side rule
56
+ // in operating-mode/deployment-name.ts (resolveDeploymentName); keep the
57
+ // two in step.
58
+ if (props.deploymentName !== undefined && !isValidDeploymentName(props.deploymentName)) {
59
+ throw new Error(`CanopyCmsService: invalid deploymentName ${JSON.stringify(props.deploymentName)}. ` +
60
+ `It must start with a letter or digit, contain only letters, digits, '.', '_' or '-', ` +
61
+ `and must not contain '..' or end with '.' or '.lock'.`);
62
+ }
31
63
  // ========================================================================
32
64
  // VPC — 2 AZs, public + private subnets, NO NAT
33
65
  // ========================================================================
@@ -117,6 +149,22 @@ export class CanopyCmsService extends Construct {
117
149
  // region-agnostic way to reference the S3 managed prefix list lands in
118
150
  // CDK, or if this VPC ever gains a NAT/IGW route.
119
151
  lambdaSg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'HTTPS to S3 (via gateway endpoint)');
152
+ // Dedicated CloudWatch log group for the CMS Lambda's stdout/stderr.
153
+ // Custom name (NOT the CloudFormation-implicit `/aws/lambda/<function
154
+ // name>`), for two reasons: (1) CDK does not manage that implicit group
155
+ // at all - infinite retention, and `cdk destroy` leaves it behind (the
156
+ // deploy-test teardown had to sweep it manually); (2) Lambda auto-creates
157
+ // `/aws/lambda/<function name>` on first invoke, OUTSIDE CloudFormation -
158
+ // once that has happened (e.g. this stack was already deployed before
159
+ // this log group existed), a CDK `LogGroup` construct using that exact
160
+ // name would fail its `CreateLogGroup` call with "already exists" and
161
+ // block every future `cdk deploy`. Mirrors `workerLogGroup` below, which
162
+ // predates this and already follows the same convention.
163
+ this.cmsLogGroup = new logs.LogGroup(this, 'CmsFunctionLogs', {
164
+ logGroupName: props.cmsLogGroupName ?? `/canopycms/${Stack.of(this).stackName}/cms`,
165
+ retention: props.cmsLogRetention ?? logs.RetentionDays.THREE_MONTHS,
166
+ removalPolicy: RemovalPolicy.DESTROY,
167
+ });
120
168
  this.lambdaFunction = new lambda.DockerImageFunction(this, 'CmsFunction', {
121
169
  code: props.cmsDockerImage,
122
170
  memorySize: props.memorySize ?? 2048,
@@ -127,6 +175,11 @@ export class CanopyCmsService extends Construct {
127
175
  vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
128
176
  securityGroups: [lambdaSg],
129
177
  filesystem: lambda.FileSystem.fromEfsAccessPoint(accessPoint, '/mnt/efs'),
178
+ // Pass the pre-created group via `logGroup`, NOT `logRetention` (CDK
179
+ // throws LogRetentionLogGroupConflict/ConflictingLogPolicyOptions if
180
+ // both are set on the same function) - the removal policy lives on the
181
+ // LogGroup construct above instead.
182
+ logGroup: this.cmsLogGroup,
130
183
  environment: {
131
184
  // INVARIANT (B1): the Lambda mounts EFS through the WorkspaceAP access
132
185
  // point above, which is already rooted at EFS:/workspace - so /mnt/efs
@@ -136,6 +189,9 @@ export class CanopyCmsService extends Construct {
136
189
  // or the Lambda and worker silently operate on different directories.
137
190
  CANOPYCMS_WORKSPACE_ROOT: '/mnt/efs',
138
191
  CANOPY_AUTH_CACHE_PATH: '/mnt/efs/.cache',
192
+ // Placed before ...props.environment below so an adopter can still
193
+ // override it explicitly via that escape hatch.
194
+ CANOPYCMS_DEPLOYMENT_NAME: props.deploymentName ?? 'prod',
139
195
  // B7 note: git >= 2.35.2 refuses repos owned by another uid (the
140
196
  // access point forces uid 1000; Lambda containers run as a different
141
197
  // user). Env-based GIT_CONFIG_* CANNOT fix this - simple-git
@@ -145,6 +201,19 @@ export class CanopyCmsService extends Construct {
145
201
  ...props.environment,
146
202
  },
147
203
  });
204
+ // Explicit, scoped grant - NOT a reliance on the auto-created execution
205
+ // role's AWSLambdaBasicExecutionRole managed policy (attached by CDK's
206
+ // lambda.Function/DockerImageFunction regardless of `logGroup`, and
207
+ // never adjusted for it - passing `logGroup` only points the function's
208
+ // LoggingConfig at this group, it grants no IAM permissions). That
209
+ // managed policy's logs:CreateLogStream/logs:PutLogEvents statement is
210
+ // scoped to `arn:aws:logs:*:*:log-group:/aws/lambda/*:*` only (its
211
+ // logs:CreateLogGroup statement is the sole one that's unrestricted) -
212
+ // it grants nothing for a custom-named group like this one. Without this
213
+ // grantWrite, the function would still create log streams to the void:
214
+ // CloudWatch Logs delivery failures are never surfaced to the function's
215
+ // own invocation, so logs would simply vanish with no error anywhere.
216
+ this.cmsLogGroup.grantWrite(this.lambdaFunction);
148
217
  // Function URL for CloudFront origin.
149
218
  // AWS_IAM (not NONE): the URL must only be reachable through CloudFront,
150
219
  // which signs origin requests via Origin Access Control (see
@@ -235,6 +304,7 @@ export class CanopyCmsService extends Construct {
235
304
  `CANOPYCMS_GITHUB_OWNER=${props.githubOwner}`,
236
305
  `CANOPYCMS_GITHUB_REPO=${props.githubRepo}`,
237
306
  `CANOPYCMS_BASE_BRANCH=${props.baseBranch ?? 'main'}`,
307
+ `CANOPYCMS_DEPLOYMENT_NAME=${props.deploymentName ?? 'prod'}`,
238
308
  // B8: the AWS SDK JS v3 cannot resolve a region from IMDS on its own -
239
309
  // without this the worker's bare `SecretsManagerClient({})` crash-loops
240
310
  // with "Region is missing".
@@ -249,7 +319,22 @@ export class CanopyCmsService extends Construct {
249
319
  const envFileContent = envLines.join('\n');
250
320
  // UserData script
251
321
  const userData = ec2.UserData.forLinux();
252
- userData.addCommands('#!/bin/bash', 'set -euo pipefail', '', '# Install dependencies (unzip is not guaranteed in the AL2023 AMI)', 'yum install -y git unzip', 'curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -', 'yum install -y nodejs', '', '# Mount EFS', 'yum install -y amazon-efs-utils', 'mkdir -p /mnt/efs', `mount -t efs ${this.fileSystem.fileSystemId}:/ /mnt/efs`, '# Persist the mount across instance reboots: user-data runs once per', '# instance, so without an fstab entry a plain reboot leaves /mnt/efs an', '# empty local dir and the worker would clone a divergent remote.git', '# onto the instance disk, invisible to the Lambda.', `echo '${this.fileSystem.fileSystemId}:/ /mnt/efs efs _netdev 0 0' >> /etc/fstab`, '', '# Download worker from CDK S3 Asset', `aws s3 cp s3://${workerAsset.s3BucketName}/${workerAsset.s3ObjectKey} /tmp/canopy-worker.zip`, 'mkdir -p /opt/canopy-worker', 'cd /opt/canopy-worker', 'unzip -o /tmp/canopy-worker.zip', '# The worker bundle is ESM (esbuild --format=esm). Node 20 treats .js as', '# CommonJS without this marker and crash-loops on the import statement', '# (Node >=22.7 auto-detects and masks the bug locally).', `echo '{"type":"module"}' > /opt/canopy-worker/package.json`, '', '# Write environment file for systemd service', `cat > /opt/canopy-worker/.env << 'ENVEOF'`, envFileContent, 'ENVEOF', '', '# Create systemd service', `cat > /etc/systemd/system/canopy-worker.service << 'SVCEOF'`, '[Unit]', 'Description=CanopyCMS Worker Daemon', 'After=network.target', '# Never run against an unmounted /mnt/efs (see the fstab note above).', 'RequiresMountsFor=/mnt/efs', '', '[Service]', 'Type=simple', 'User=ec2-user', 'WorkingDirectory=/opt/canopy-worker', 'ExecStart=/usr/bin/node index.js', 'Restart=always', 'RestartSec=5', 'TimeoutStartSec=300', '# File output, not journal: the CloudWatch agent cannot read journald,', '# so it tails this file instead (see the agent config below).', '# CAUTION: /var/log/canopy-worker must exist BEFORE first start. systemd', '# opens append: targets before it creates LogsDirectory= dirs', '# (systemd#27591), so without the pre-created dir the exec fails with', '# 209/STDOUT and Restart=always crash-loops forever. User-data runs', '# mkdir before systemctl start; LogsDirectory= is kept for ownership.', 'LogsDirectory=canopy-worker', 'StandardOutput=append:/var/log/canopy-worker/worker.log', 'StandardError=append:/var/log/canopy-worker/worker.log', 'EnvironmentFile=/opt/canopy-worker/.env', '', '[Install]', 'WantedBy=multi-user.target', 'SVCEOF', '', '# Set ownership for ec2-user', 'chown -R ec2-user:ec2-user /opt/canopy-worker', '# Non-recursive: EFS access point enforces UID 1000 for Lambda.', '# Only set ownership on mount point and workspace dir to avoid', '# slow recursive chown on large filesystems during ASG replacements.', 'chown ec2-user:ec2-user /mnt/efs', 'mkdir -p /mnt/efs/workspace', 'chown ec2-user:ec2-user /mnt/efs/workspace', '', '# Pre-create the worker log dir (crash-loop guard, MUST precede the', '# first systemctl start): systemd opens StandardOutput=append: files', '# BEFORE it creates LogsDirectory= dirs (systemd#27591), so on a fresh', '# instance the unit would fail exec with 209/STDOUT and crash-loop', '# forever without this. The unit keeps LogsDirectory= for ownership', '# management on subsequent starts.', 'mkdir -p /var/log/canopy-worker', 'chown ec2-user:ec2-user /var/log/canopy-worker', '', '# Start worker', 'systemctl daemon-reload', 'systemctl enable canopy-worker', 'systemctl start canopy-worker', '', '# ---- CloudWatch log shipping ----', '# Placed AFTER worker start: with set -euo pipefail, a yum/agent failure', '# here must not prevent the worker from running (shipping is best-effort).', 'yum install -y amazon-cloudwatch-agent logrotate', '', '# Bound on-disk growth; copytruncate keeps the fd the CW agent tails valid', '# (tiny copy->truncate loss window is acceptable for diagnostic logs).', `cat > /etc/logrotate.d/canopy-worker << 'ROTEOF'`, '/var/log/canopy-worker/worker.log {', ' size 10M', ' rotate 5', ' compress', ' copytruncate', ' missingok', ' notifempty', '}', 'ROTEOF', '# The config above only fires when logrotate actually runs; AL2023', '# presets may leave logrotate.timer disabled, and without it the size', '# cap never triggers and worker.log grows until the nano disk fills.', '# --now is idempotent if the timer is already enabled/running.', 'systemctl enable --now logrotate.timer', '', '# No retention_in_days here: CDK owns retention on the pre-created group.', `cat > /opt/aws/amazon-cloudwatch-agent/etc/canopy-worker-logs.json << 'CWEOF'`, '{', ' "logs": {', ' "logs_collected": {', ' "files": {', ' "collect_list": [', ' {', ' "file_path": "/var/log/canopy-worker/worker.log",', ` "log_group_name": "${this.workerLogGroup.logGroupName}",`, ' "log_stream_name": "{instance_id}"', ' }', ' ]', ' }', ' }', ' }', '}', 'CWEOF', '# fetch-config -s starts AND systemctl-enables the agent (reboot-persistent).', '/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/etc/canopy-worker-logs.json');
322
+ userData.addCommands('#!/bin/bash', 'set -euo pipefail', '', '# Install dependencies (unzip is not guaranteed in the AL2023 AMI)', 'yum install -y git unzip', 'curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -', 'yum install -y nodejs', '', '# Mount EFS', 'yum install -y amazon-efs-utils', 'mkdir -p /mnt/efs', `mount -t efs ${this.fileSystem.fileSystemId}:/ /mnt/efs`, '# Persist the mount across instance reboots: user-data runs once per', '# instance, so without an fstab entry a plain reboot leaves /mnt/efs an', '# empty local dir and the worker would clone a divergent remote.git', '# onto the instance disk, invisible to the Lambda.', `echo '${this.fileSystem.fileSystemId}:/ /mnt/efs efs _netdev 0 0' >> /etc/fstab`, '', '# Download worker from CDK S3 Asset', `aws s3 cp s3://${workerAsset.s3BucketName}/${workerAsset.s3ObjectKey} /tmp/canopy-worker.zip`, 'mkdir -p /opt/canopy-worker', 'cd /opt/canopy-worker', 'unzip -o /tmp/canopy-worker.zip', '# The worker bundle is ESM (esbuild --format=esm). Node 20 treats .js as', '# CommonJS without this marker and crash-loops on the import statement', '# (Node >=22.7 auto-detects and masks the bug locally).', `echo '{"type":"module"}' > /opt/canopy-worker/package.json`, '', '# Write environment file for systemd service', `cat > /opt/canopy-worker/.env << 'ENVEOF'`, envFileContent, 'ENVEOF', '', '# Create systemd service', `cat > /etc/systemd/system/canopy-worker.service << 'SVCEOF'`, '[Unit]', 'Description=CanopyCMS Worker Daemon', 'After=network.target', '# Never run against an unmounted /mnt/efs (see the fstab note above).', 'RequiresMountsFor=/mnt/efs', '', '[Service]', 'Type=simple', 'User=ec2-user', 'WorkingDirectory=/opt/canopy-worker', 'ExecStart=/usr/bin/node index.js', 'Restart=always', 'RestartSec=5', 'TimeoutStartSec=300', '# File output, not journal: the CloudWatch agent cannot read journald,', '# so it tails this file instead (see the agent config below).', '# CAUTION: /var/log/canopy-worker must exist BEFORE first start. systemd', '# opens append: targets before it creates LogsDirectory= dirs', '# (systemd#27591), so without the pre-created dir the exec fails with', '# 209/STDOUT and Restart=always crash-loops forever. User-data runs', '# mkdir before systemctl start; LogsDirectory= is kept for ownership.', 'LogsDirectory=canopy-worker', 'StandardOutput=append:/var/log/canopy-worker/worker.log', 'StandardError=append:/var/log/canopy-worker/worker.log', 'EnvironmentFile=/opt/canopy-worker/.env', '', '[Install]', 'WantedBy=multi-user.target', 'SVCEOF', '', '# Set ownership for ec2-user', 'chown -R ec2-user:ec2-user /opt/canopy-worker', '# Non-recursive: EFS access point enforces UID 1000 for Lambda.', '# Only set ownership on mount point and workspace dir to avoid', '# slow recursive chown on large filesystems during ASG replacements.', 'chown ec2-user:ec2-user /mnt/efs', 'mkdir -p /mnt/efs/workspace', 'chown ec2-user:ec2-user /mnt/efs/workspace', '', '# Pre-create the worker log dir (crash-loop guard, MUST precede the', '# first systemctl start): systemd opens StandardOutput=append: files', '# BEFORE it creates LogsDirectory= dirs (systemd#27591), so on a fresh', '# instance the unit would fail exec with 209/STDOUT and crash-loop', '# forever without this. The unit keeps LogsDirectory= for ownership', '# management on subsequent starts.', 'mkdir -p /var/log/canopy-worker', 'chown ec2-user:ec2-user /var/log/canopy-worker', '', '# Start worker', 'systemctl daemon-reload', 'systemctl enable canopy-worker', 'systemctl start canopy-worker', '', '# ---- CloudWatch log shipping ----', '# Placed AFTER worker start: with set -euo pipefail, a yum/agent failure', '# here must not prevent the worker from running (shipping is best-effort).', 'yum install -y amazon-cloudwatch-agent logrotate', '', '# Bound on-disk growth; copytruncate keeps the fd the CW agent tails valid', '# (tiny copy->truncate loss window is acceptable for diagnostic logs).', `cat > /etc/logrotate.d/canopy-worker << 'ROTEOF'`, '/var/log/canopy-worker/worker.log {', ' size 10M', ' rotate 5', ' compress', ' copytruncate', ' missingok', ' notifempty', '}', 'ROTEOF', '# The config above only fires when logrotate actually runs; AL2023', '# presets may leave logrotate.timer disabled, and without it the size', '# cap never triggers and worker.log grows until the nano disk fills.', '# --now is idempotent if the timer is already enabled/running.', 'systemctl enable --now logrotate.timer', '', '# No retention_in_days here: CDK owns retention on the pre-created group.', `cat > /opt/aws/amazon-cloudwatch-agent/etc/canopy-worker-logs.json << 'CWEOF'`, '{', ' "logs": {', ' "logs_collected": {', ' "files": {', ' "collect_list": [', ' {', ' "file_path": "/var/log/canopy-worker/worker.log",', ` "log_group_name": "${this.workerLogGroup.logGroupName}",`, ' "log_stream_name": "{instance_id}",',
323
+ // The worker prefixes every line with an ISO-8601 timestamp
324
+ // (packages/canopycms/src/worker/log.ts). Parsing it here is what makes
325
+ // CloudWatch show the time the WORKER emitted a line rather than the
326
+ // time the agent shipped it - those diverge exactly when it matters
327
+ // (agent hiccup, buffered burst, post-restart backlog).
328
+ ' "timestamp_format": "%Y-%m-%dT%H:%M:%S.%f",',
329
+ // The prefix ends in `Z`, so the parsed time is UTC. Without this the
330
+ // agent would interpret it in the instance's local zone.
331
+ ' "timezone": "UTC",',
332
+ // "{timestamp_format}" reuses the pattern above as the multi-line start
333
+ // marker: a line WITHOUT the timestamp prefix continues the previous
334
+ // event instead of becoming its own. That is what keeps a stack trace as
335
+ // one CloudWatch event, and why every writer to this file must go
336
+ // through the worker log helpers.
337
+ ' "multi_line_start_pattern": "{timestamp_format}"', ' }', ' ]', ' }', ' }', ' }', '}', 'CWEOF', '# fetch-config -s starts AND systemctl-enables the agent (reboot-persistent).', '/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/etc/canopy-worker-logs.json');
253
338
  // Launch template (not the deprecated AutoScalingGroup instanceType/
254
339
  // machineImage/... shorthand): that shorthand synthesizes an
255
340
  // AWS::AutoScaling::LaunchConfiguration, which AWS accounts created after
@@ -279,7 +364,64 @@ export class CanopyCmsService extends Construct {
279
364
  healthCheck: autoscaling.HealthCheck.ec2({
280
365
  grace: Duration.minutes(5),
281
366
  }),
367
+ // Without an updatePolicy, CloudFormation's default behavior for an ASG
368
+ // behind a changed launch template is to update the template resource
369
+ // and do NOTHING else - the running instance keeps its old user-data
370
+ // (and therefore the old worker code: the worker bundle is a CDK S3
371
+ // asset whose hash is interpolated into user-data's `aws s3 cp
372
+ // s3://...`) until a spot interruption or a manual terminate happens
373
+ // to replace it. `cdk deploy` would then silently deploy everything
374
+ // EXCEPT the worker. `rollingUpdate` makes CloudFormation actually
375
+ // terminate-and-relaunch the instance on every deploy that changes the
376
+ // launch template, so a worker code change actually reaches it.
377
+ //
378
+ // minInstancesInService: 0 is REQUIRED, not just accepted, because
379
+ // minCapacity/maxCapacity are both 1: there is no way to keep an
380
+ // instance "in service" out of a max of 1 while its replacement is
381
+ // being created. The update is therefore terminate-then-relaunch, with
382
+ // a short worker outage while the replacement boots (yum install
383
+ // git/unzip/nodejs/efs-utils, mount EFS - realistically 2-4 minutes).
384
+ // That outage is acceptable: the task queue and branch workspaces live
385
+ // on EFS, not on the instance, so the new instance picks up exactly
386
+ // where the old one left off; the Lambda's Save/Publish paths only
387
+ // enqueue task files onto EFS and never talk to the worker directly,
388
+ // so they are unaffected by the worker being briefly down. A task that
389
+ // was actually mid-flight when the old instance was terminated is
390
+ // handled by orphan recovery now running on every task-queue cycle,
391
+ // not only at worker boot (see recoverOrphanedTasks's call site in
392
+ // CmsWorker.processTaskQueue(), packages/canopycms/src/worker/cms-worker.ts).
393
+ //
394
+ // Deliberately NOT paired with a `signals`/cfn-signal setup (and
395
+ // `waitOnResourceSignals` therefore defaults to false here, so this
396
+ // rolling update does not wait on one): see the comment below.
397
+ updatePolicy: autoscaling.UpdatePolicy.rollingUpdate({ minInstancesInService: 0 }),
282
398
  });
399
+ // Why this PR does NOT add cfn-signal, despite the rolling update above
400
+ // now making instance replacement routine instead of rare:
401
+ //
402
+ // 1. User-data runs under `set -euo pipefail`, and the CloudWatch-agent
403
+ // block is placed at the very end ON PURPOSE (see that block's own
404
+ // comment) so an agent failure there cannot kill the boot. A
405
+ // cfn-signal placed after it would then never run when that block
406
+ // fails, and CloudFormation would wait out its own timeout and
407
+ // fail/roll back the ENTIRE deploy - the opposite of the intent
408
+ // (agent shipping is best-effort; the worker itself must not be
409
+ // blocked by it).
410
+ // 2. Even placed earlier (right after `systemctl start canopy-worker`),
411
+ // a signal there would prove almost nothing: the systemd unit is
412
+ // `Type=simple` with `Restart=always`, so `systemctl start` returns 0
413
+ // the instant the process execs, regardless of what happens next. A
414
+ // worker that immediately crash-loops (bad env, bad bundle) would
415
+ // still signal SUCCESS. A real readiness gate would have to poll
416
+ // `worker-status.json` or `systemctl is-active` in a loop before
417
+ // signaling - a bigger change than this PR should carry.
418
+ //
419
+ // recoverOrphanedTasks()'s per-cycle recovery (see above) is the
420
+ // intentionally simpler fix for the actual problem (a stranded task
421
+ // surviving an instance replacement) - it works regardless of WHY the
422
+ // instance was replaced (rolling update, spot interruption, manual
423
+ // terminate) and does not depend on the new instance ever proving
424
+ // "ready" in the first place.
283
425
  // Boot ordering: the ASG can launch before EFS mount targets are
284
426
  // available; user-data runs with `set -euo pipefail`, so an early
285
427
  // `mount -t efs` failure kills the whole bootstrap and the