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.
- package/dist/constructs/asset-support.d.ts +41 -2
- package/dist/constructs/asset-support.d.ts.map +1 -1
- package/dist/constructs/asset-support.js +83 -3
- package/dist/constructs/asset-support.js.map +1 -1
- package/dist/constructs/cms-service.d.ts +53 -6
- package/dist/constructs/cms-service.d.ts.map +1 -1
- package/dist/constructs/cms-service.js +148 -6
- package/dist/constructs/cms-service.js.map +1 -1
- package/lambda/asset-transform/dist/.deployable +5 -0
- package/lambda/asset-transform/dist/handler.js +49 -3
- package/package.json +7 -6
- package/worker/dist/index.js +31728 -30775
|
@@ -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)
|
|
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":"
|
|
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)
|
|
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(
|
|
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":"AAAA,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,cAAc,IAAI,UAAU,EAC5B,sBAAsB,IAAI,OAAO,EAEjC,UAAU,IAAI,MAAM,EACpB,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;;;;;;;;;;;;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;
|
|
1
|
+
{"version":3,"file":"asset-support.js","sourceRoot":"","sources":["../../src/constructs/asset-support.ts"],"names":[],"mappings":"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"}
|
|
@@ -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).
|
|
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
|
-
*
|
|
73
|
-
*
|
|
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
|
|
76
|
-
* Logs write, scoped to its
|
|
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;
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
26
|
-
* Logs write, scoped to its
|
|
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}"
|
|
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
|