canopycms-cdk 0.0.0 → 0.0.2
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/package.json +3 -4
- package/src/constructs/cms-distribution.ts +0 -151
- package/src/constructs/cms-service.ts +0 -325
- package/src/index.ts +0 -9
- package/src/worker.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canopycms-cdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "AWS CDK constructs and EC2 worker for CanopyCMS deployment",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
-
"src",
|
|
21
20
|
"worker/dist",
|
|
22
21
|
"worker/canopy-worker.service"
|
|
23
22
|
],
|
|
@@ -53,8 +52,8 @@
|
|
|
53
52
|
"devDependencies": {
|
|
54
53
|
"@types/node": "^22.9.0",
|
|
55
54
|
"aws-cdk-lib": "^2.150.0",
|
|
56
|
-
"canopycms": "
|
|
57
|
-
"canopycms-auth-clerk": "
|
|
55
|
+
"canopycms": "^0.0.2",
|
|
56
|
+
"canopycms-auth-clerk": "^0.0.2",
|
|
58
57
|
"@octokit/rest": "^20.0.2",
|
|
59
58
|
"simple-git": "^3.22.0",
|
|
60
59
|
"@aws-sdk/client-secrets-manager": "^3.600.0",
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { Construct } from 'constructs'
|
|
2
|
-
import {
|
|
3
|
-
Duration,
|
|
4
|
-
Fn,
|
|
5
|
-
aws_cloudfront as cloudfront,
|
|
6
|
-
aws_cloudfront_origins as origins,
|
|
7
|
-
aws_certificatemanager as acm,
|
|
8
|
-
aws_route53 as route53,
|
|
9
|
-
aws_route53_targets as targets,
|
|
10
|
-
aws_lambda as lambda,
|
|
11
|
-
} from 'aws-cdk-lib'
|
|
12
|
-
|
|
13
|
-
export interface CanopyCmsDistributionProps {
|
|
14
|
-
/** Lambda Function URL from CanopyCmsService */
|
|
15
|
-
functionUrl: lambda.FunctionUrl
|
|
16
|
-
|
|
17
|
-
/** Domain name for the CMS (e.g., 'cms.docs.example.org') */
|
|
18
|
-
domainName: string
|
|
19
|
-
|
|
20
|
-
/** Route53 hosted zone domain (e.g., 'example.org') */
|
|
21
|
-
hostedZoneDomain: string
|
|
22
|
-
|
|
23
|
-
/** Optional: provide an existing hosted zone instead of looking up by domain */
|
|
24
|
-
hostedZone?: route53.IHostedZone
|
|
25
|
-
|
|
26
|
-
/** Optional: provide an existing ACM certificate instead of creating one */
|
|
27
|
-
certificate?: acm.ICertificate
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Optional CDK construct for CanopyCMS CloudFront distribution.
|
|
32
|
-
*
|
|
33
|
-
* Use this if you don't have existing CloudFront infrastructure.
|
|
34
|
-
* If you do, use the functionUrl output from CanopyCmsService
|
|
35
|
-
* and wire it into your own CloudFront setup.
|
|
36
|
-
*
|
|
37
|
-
* Creates:
|
|
38
|
-
* - ACM certificate (DNS validated) — unless provided
|
|
39
|
-
* - CloudFront distribution with Function URL origin
|
|
40
|
-
* - Route53 A/AAAA alias records
|
|
41
|
-
* - Cache policies: no-cache for /api/* and /edit*, cache /_next/static/*
|
|
42
|
-
*/
|
|
43
|
-
export class CanopyCmsDistribution extends Construct {
|
|
44
|
-
/** The CloudFront distribution */
|
|
45
|
-
public readonly distribution: cloudfront.Distribution
|
|
46
|
-
|
|
47
|
-
constructor(scope: Construct, id: string, props: CanopyCmsDistributionProps) {
|
|
48
|
-
super(scope, id)
|
|
49
|
-
|
|
50
|
-
// ========================================================================
|
|
51
|
-
// DNS — Hosted Zone lookup
|
|
52
|
-
// ========================================================================
|
|
53
|
-
|
|
54
|
-
const hostedZone =
|
|
55
|
-
props.hostedZone ??
|
|
56
|
-
route53.HostedZone.fromLookup(this, 'Zone', {
|
|
57
|
-
domainName: props.hostedZoneDomain,
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
// ========================================================================
|
|
61
|
-
// ACM Certificate
|
|
62
|
-
// ========================================================================
|
|
63
|
-
|
|
64
|
-
const certificate =
|
|
65
|
-
props.certificate ??
|
|
66
|
-
new acm.Certificate(this, 'Cert', {
|
|
67
|
-
domainName: props.domainName,
|
|
68
|
-
validation: acm.CertificateValidation.fromDns(hostedZone),
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// ========================================================================
|
|
72
|
-
// CloudFront Distribution
|
|
73
|
-
// ========================================================================
|
|
74
|
-
|
|
75
|
-
// Extract the domain from the Function URL (https://xxx.lambda-url.region.on.aws)
|
|
76
|
-
const functionUrlDomain = extractFunctionUrlDomain(props.functionUrl)
|
|
77
|
-
|
|
78
|
-
// Origin: Lambda Function URL
|
|
79
|
-
const origin = new origins.HttpOrigin(functionUrlDomain, {
|
|
80
|
-
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// Cache policy for API/editor routes: no caching, forward all headers
|
|
84
|
-
const noCachePolicy = new cloudfront.CachePolicy(this, 'NoCachePolicy', {
|
|
85
|
-
cachePolicyName: `${id}-no-cache`,
|
|
86
|
-
defaultTtl: Duration.seconds(0),
|
|
87
|
-
maxTtl: Duration.seconds(0),
|
|
88
|
-
minTtl: Duration.seconds(0),
|
|
89
|
-
headerBehavior: cloudfront.CacheHeaderBehavior.allowList('Authorization', 'Cookie', 'Host'),
|
|
90
|
-
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
|
|
91
|
-
cookieBehavior: cloudfront.CacheCookieBehavior.all(),
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// Cache policy for static assets
|
|
95
|
-
const staticCachePolicy = new cloudfront.CachePolicy(this, 'StaticCachePolicy', {
|
|
96
|
-
cachePolicyName: `${id}-static`,
|
|
97
|
-
defaultTtl: Duration.days(365),
|
|
98
|
-
maxTtl: Duration.days(365),
|
|
99
|
-
minTtl: Duration.days(365),
|
|
100
|
-
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
|
|
101
|
-
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
|
|
102
|
-
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
this.distribution = new cloudfront.Distribution(this, 'Distribution', {
|
|
106
|
-
domainNames: [props.domainName],
|
|
107
|
-
certificate,
|
|
108
|
-
defaultBehavior: {
|
|
109
|
-
origin,
|
|
110
|
-
cachePolicy: noCachePolicy,
|
|
111
|
-
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
112
|
-
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
|
113
|
-
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
|
|
114
|
-
},
|
|
115
|
-
additionalBehaviors: {
|
|
116
|
-
'/_next/static/*': {
|
|
117
|
-
origin,
|
|
118
|
-
cachePolicy: staticCachePolicy,
|
|
119
|
-
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
// ========================================================================
|
|
125
|
-
// DNS Records
|
|
126
|
-
// ========================================================================
|
|
127
|
-
|
|
128
|
-
new route53.ARecord(this, 'ARecord', {
|
|
129
|
-
zone: hostedZone,
|
|
130
|
-
recordName: props.domainName,
|
|
131
|
-
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
new route53.AaaaRecord(this, 'AaaaRecord', {
|
|
135
|
-
zone: hostedZone,
|
|
136
|
-
recordName: props.domainName,
|
|
137
|
-
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(this.distribution)),
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Extract the domain from a Lambda Function URL.
|
|
144
|
-
* The URL is like https://xxx.lambda-url.region.on.aws/
|
|
145
|
-
* We need just the domain part for CloudFront origin.
|
|
146
|
-
*/
|
|
147
|
-
function extractFunctionUrlDomain(functionUrl: lambda.FunctionUrl): string {
|
|
148
|
-
// Function URL is a Token at synth time, so we use Fn.select + Fn.split
|
|
149
|
-
// to extract the domain from the URL: https://xxx.lambda-url.region.on.aws/
|
|
150
|
-
return Fn.select(2, Fn.split('/', functionUrl.url))
|
|
151
|
-
}
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
import { Construct } from 'constructs'
|
|
3
|
-
import {
|
|
4
|
-
Duration,
|
|
5
|
-
RemovalPolicy,
|
|
6
|
-
aws_ec2 as ec2,
|
|
7
|
-
aws_efs as efs,
|
|
8
|
-
aws_iam as iam,
|
|
9
|
-
aws_lambda as lambda,
|
|
10
|
-
aws_autoscaling as autoscaling,
|
|
11
|
-
aws_s3_assets as s3assets,
|
|
12
|
-
} from 'aws-cdk-lib'
|
|
13
|
-
|
|
14
|
-
export interface CanopyCmsServiceProps {
|
|
15
|
-
/** Docker image for the CMS Lambda function */
|
|
16
|
-
cmsDockerImage: lambda.DockerImageCode
|
|
17
|
-
|
|
18
|
-
/** Optional: use an existing VPC instead of creating one */
|
|
19
|
-
vpc?: ec2.IVpc
|
|
20
|
-
|
|
21
|
-
/** Lambda memory in MB (default: 2048) */
|
|
22
|
-
memorySize?: number
|
|
23
|
-
|
|
24
|
-
/** Lambda timeout (default: 60 seconds) */
|
|
25
|
-
timeout?: Duration
|
|
26
|
-
|
|
27
|
-
/** Lambda reserved concurrency cap (default: 10) */
|
|
28
|
-
reservedConcurrency?: number
|
|
29
|
-
|
|
30
|
-
/** EC2 spot max price (default: on-demand rate for t4g.nano) */
|
|
31
|
-
spotMaxPrice?: string
|
|
32
|
-
|
|
33
|
-
/** Secrets Manager ARNs the worker needs to read (GitHub token, Clerk key) */
|
|
34
|
-
secretsArns?: string[]
|
|
35
|
-
|
|
36
|
-
/** Environment variables for the Lambda function */
|
|
37
|
-
environment?: Record<string, string>
|
|
38
|
-
|
|
39
|
-
/** EFS removal policy (default: RETAIN) */
|
|
40
|
-
efsRemovalPolicy?: RemovalPolicy
|
|
41
|
-
|
|
42
|
-
/** GitHub owner for worker git operations (e.g., 'safeinsights') */
|
|
43
|
-
githubOwner: string
|
|
44
|
-
|
|
45
|
-
/** GitHub repo name for worker git operations (e.g., 'docs-site') */
|
|
46
|
-
githubRepo: string
|
|
47
|
-
|
|
48
|
-
/** Secrets Manager ARN for the GitHub bot token */
|
|
49
|
-
githubTokenSecretArn?: string
|
|
50
|
-
|
|
51
|
-
/** Secrets Manager ARN for the Clerk secret key */
|
|
52
|
-
clerkSecretKeySecretArn?: string
|
|
53
|
-
|
|
54
|
-
/** Base branch name (default: 'main') */
|
|
55
|
-
baseBranch?: string
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Core CDK construct for CanopyCMS deployment.
|
|
60
|
-
*
|
|
61
|
-
* Creates:
|
|
62
|
-
* - VPC (2 AZs, public + private subnets, NO NAT)
|
|
63
|
-
* - EFS filesystem with access point at /workspace
|
|
64
|
-
* - Lambda function (Docker image, EFS mount, private subnet, no internet)
|
|
65
|
-
* - Lambda Function URL (for CloudFront origin)
|
|
66
|
-
* - EC2 Worker (t4g.nano spot in ASG, public subnet, EFS mount, systemd)
|
|
67
|
-
* - Security groups (least-privilege)
|
|
68
|
-
* - IAM roles (Lambda: EFS only; EC2: EFS + Secrets Manager)
|
|
69
|
-
*/
|
|
70
|
-
export class CanopyCmsService extends Construct {
|
|
71
|
-
/** Lambda Function URL — use as CloudFront origin */
|
|
72
|
-
public readonly functionUrl: lambda.FunctionUrl
|
|
73
|
-
|
|
74
|
-
/** The EFS filesystem */
|
|
75
|
-
public readonly fileSystem: efs.FileSystem
|
|
76
|
-
|
|
77
|
-
/** The VPC */
|
|
78
|
-
public readonly vpc: ec2.IVpc
|
|
79
|
-
|
|
80
|
-
/** The Lambda function */
|
|
81
|
-
public readonly lambdaFunction: lambda.Function
|
|
82
|
-
|
|
83
|
-
/** The EC2 worker Auto Scaling Group */
|
|
84
|
-
public readonly workerAsg: autoscaling.AutoScalingGroup
|
|
85
|
-
|
|
86
|
-
constructor(scope: Construct, id: string, props: CanopyCmsServiceProps) {
|
|
87
|
-
super(scope, id)
|
|
88
|
-
|
|
89
|
-
// ========================================================================
|
|
90
|
-
// VPC — 2 AZs, public + private subnets, NO NAT
|
|
91
|
-
// ========================================================================
|
|
92
|
-
|
|
93
|
-
this.vpc =
|
|
94
|
-
props.vpc ??
|
|
95
|
-
new ec2.Vpc(this, 'Vpc', {
|
|
96
|
-
maxAzs: 2,
|
|
97
|
-
natGateways: 0, // No NAT — Lambda has no internet access
|
|
98
|
-
subnetConfiguration: [
|
|
99
|
-
{
|
|
100
|
-
name: 'Public',
|
|
101
|
-
subnetType: ec2.SubnetType.PUBLIC,
|
|
102
|
-
cidrMask: 24,
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: 'Private',
|
|
106
|
-
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
|
|
107
|
-
cidrMask: 24,
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
// ========================================================================
|
|
113
|
-
// EFS — persistent filesystem for content, git repos, cache
|
|
114
|
-
// ========================================================================
|
|
115
|
-
|
|
116
|
-
const efsSg = new ec2.SecurityGroup(this, 'EfsSg', {
|
|
117
|
-
vpc: this.vpc,
|
|
118
|
-
description: 'CanopyCMS EFS',
|
|
119
|
-
allowAllOutbound: false,
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
this.fileSystem = new efs.FileSystem(this, 'FileSystem', {
|
|
123
|
-
vpc: this.vpc,
|
|
124
|
-
encrypted: true,
|
|
125
|
-
performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
|
|
126
|
-
removalPolicy: props.efsRemovalPolicy ?? RemovalPolicy.RETAIN,
|
|
127
|
-
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
|
|
128
|
-
securityGroup: efsSg,
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const accessPoint = this.fileSystem.addAccessPoint('WorkspaceAP', {
|
|
132
|
-
path: '/workspace',
|
|
133
|
-
createAcl: {
|
|
134
|
-
ownerGid: '1000',
|
|
135
|
-
ownerUid: '1000',
|
|
136
|
-
permissions: '755',
|
|
137
|
-
},
|
|
138
|
-
posixUser: {
|
|
139
|
-
gid: '1000',
|
|
140
|
-
uid: '1000',
|
|
141
|
-
},
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
// ========================================================================
|
|
145
|
-
// Lambda — CMS app, private subnet, no internet, EFS mount
|
|
146
|
-
// ========================================================================
|
|
147
|
-
|
|
148
|
-
const lambdaSg = new ec2.SecurityGroup(this, 'LambdaSg', {
|
|
149
|
-
vpc: this.vpc,
|
|
150
|
-
description: 'CanopyCMS Lambda',
|
|
151
|
-
allowAllOutbound: false, // No internet access
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
// Lambda → EFS
|
|
155
|
-
efsSg.addIngressRule(lambdaSg, ec2.Port.tcp(2049), 'Lambda NFS access')
|
|
156
|
-
|
|
157
|
-
this.lambdaFunction = new lambda.DockerImageFunction(this, 'CmsFunction', {
|
|
158
|
-
code: props.cmsDockerImage,
|
|
159
|
-
memorySize: props.memorySize ?? 2048,
|
|
160
|
-
timeout: props.timeout ?? Duration.seconds(60),
|
|
161
|
-
reservedConcurrentExecutions: props.reservedConcurrency ?? 10,
|
|
162
|
-
vpc: this.vpc,
|
|
163
|
-
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
|
|
164
|
-
securityGroups: [lambdaSg],
|
|
165
|
-
filesystem: lambda.FileSystem.fromEfsAccessPoint(accessPoint, '/mnt/efs'),
|
|
166
|
-
environment: {
|
|
167
|
-
CANOPYCMS_WORKSPACE_ROOT: '/mnt/efs/workspace',
|
|
168
|
-
CANOPY_AUTH_CACHE_PATH: '/mnt/efs/workspace/.cache',
|
|
169
|
-
...props.environment,
|
|
170
|
-
},
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
// Function URL for CloudFront origin
|
|
174
|
-
this.functionUrl = this.lambdaFunction.addFunctionUrl({
|
|
175
|
-
authType: lambda.FunctionUrlAuthType.NONE,
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// ========================================================================
|
|
179
|
-
// EC2 Worker — t4g.nano spot, public subnet, internet, EFS mount
|
|
180
|
-
// ========================================================================
|
|
181
|
-
|
|
182
|
-
const workerSg = new ec2.SecurityGroup(this, 'WorkerSg', {
|
|
183
|
-
vpc: this.vpc,
|
|
184
|
-
description: 'CanopyCMS EC2 Worker',
|
|
185
|
-
allowAllOutbound: false,
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
// Worker ↔ EFS (ingress on EFS SG + egress on Worker SG)
|
|
189
|
-
efsSg.addIngressRule(workerSg, ec2.Port.tcp(2049), 'Worker NFS access')
|
|
190
|
-
workerSg.addEgressRule(efsSg, ec2.Port.tcp(2049), 'NFS to EFS')
|
|
191
|
-
|
|
192
|
-
// Worker → internet (HTTPS only)
|
|
193
|
-
workerSg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'HTTPS outbound')
|
|
194
|
-
|
|
195
|
-
// Worker → DNS (needed for EFS DNS-based mount targets)
|
|
196
|
-
workerSg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(53), 'DNS TCP')
|
|
197
|
-
workerSg.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.udp(53), 'DNS UDP')
|
|
198
|
-
|
|
199
|
-
// Worker IAM role
|
|
200
|
-
const workerRole = new iam.Role(this, 'WorkerRole', {
|
|
201
|
-
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
|
|
202
|
-
description: 'CanopyCMS EC2 Worker role',
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// Worker needs to read secrets
|
|
206
|
-
if (props.secretsArns && props.secretsArns.length > 0) {
|
|
207
|
-
workerRole.addToPolicy(
|
|
208
|
-
new iam.PolicyStatement({
|
|
209
|
-
actions: ['secretsmanager:GetSecretValue'],
|
|
210
|
-
resources: props.secretsArns,
|
|
211
|
-
}),
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Worker needs EFS access (handled via security group, but mount needs ec2:DescribeAvailabilityZones)
|
|
216
|
-
workerRole.addManagedPolicy(
|
|
217
|
-
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonElasticFileSystemClientReadWriteAccess'),
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
// Worker S3 Asset — upload bundled worker code to CDK assets bucket
|
|
221
|
-
// The worker is bundled with esbuild into a single JS file (npm run build:worker)
|
|
222
|
-
const workerAsset = new s3assets.Asset(this, 'WorkerCode', {
|
|
223
|
-
path: path.join(__dirname, '../../worker/dist'),
|
|
224
|
-
})
|
|
225
|
-
workerAsset.grantRead(workerRole)
|
|
226
|
-
|
|
227
|
-
// Build worker .env file content from CDK props
|
|
228
|
-
const envLines = [
|
|
229
|
-
`CANOPYCMS_WORKSPACE_ROOT=/mnt/efs/workspace`,
|
|
230
|
-
`CANOPYCMS_GITHUB_OWNER=${props.githubOwner}`,
|
|
231
|
-
`CANOPYCMS_GITHUB_REPO=${props.githubRepo}`,
|
|
232
|
-
`CANOPYCMS_BASE_BRANCH=${props.baseBranch ?? 'main'}`,
|
|
233
|
-
]
|
|
234
|
-
if (props.githubTokenSecretArn) {
|
|
235
|
-
envLines.push(`CANOPYCMS_GITHUB_TOKEN_SECRET_ARN=${props.githubTokenSecretArn}`)
|
|
236
|
-
}
|
|
237
|
-
if (props.clerkSecretKeySecretArn) {
|
|
238
|
-
envLines.push(`CLERK_SECRET_KEY_SECRET_ARN=${props.clerkSecretKeySecretArn}`)
|
|
239
|
-
}
|
|
240
|
-
const envFileContent = envLines.join('\n')
|
|
241
|
-
|
|
242
|
-
// UserData script
|
|
243
|
-
const userData = ec2.UserData.forLinux()
|
|
244
|
-
userData.addCommands(
|
|
245
|
-
'#!/bin/bash',
|
|
246
|
-
'set -euo pipefail',
|
|
247
|
-
'',
|
|
248
|
-
'# Install dependencies',
|
|
249
|
-
'yum install -y git',
|
|
250
|
-
'curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -',
|
|
251
|
-
'yum install -y nodejs',
|
|
252
|
-
'',
|
|
253
|
-
'# Mount EFS',
|
|
254
|
-
'yum install -y amazon-efs-utils',
|
|
255
|
-
'mkdir -p /mnt/efs',
|
|
256
|
-
`mount -t efs ${this.fileSystem.fileSystemId}:/ /mnt/efs`,
|
|
257
|
-
'',
|
|
258
|
-
'# Download worker from CDK S3 Asset',
|
|
259
|
-
`aws s3 cp s3://${workerAsset.s3BucketName}/${workerAsset.s3ObjectKey} /tmp/canopy-worker.zip`,
|
|
260
|
-
'mkdir -p /opt/canopy-worker',
|
|
261
|
-
'cd /opt/canopy-worker',
|
|
262
|
-
'unzip -o /tmp/canopy-worker.zip',
|
|
263
|
-
'',
|
|
264
|
-
'# Write environment file for systemd service',
|
|
265
|
-
`cat > /opt/canopy-worker/.env << 'ENVEOF'`,
|
|
266
|
-
envFileContent,
|
|
267
|
-
'ENVEOF',
|
|
268
|
-
'',
|
|
269
|
-
'# Create systemd service',
|
|
270
|
-
`cat > /etc/systemd/system/canopy-worker.service << 'SVCEOF'`,
|
|
271
|
-
'[Unit]',
|
|
272
|
-
'Description=CanopyCMS Worker Daemon',
|
|
273
|
-
'After=network.target',
|
|
274
|
-
'',
|
|
275
|
-
'[Service]',
|
|
276
|
-
'Type=simple',
|
|
277
|
-
'User=ec2-user',
|
|
278
|
-
'WorkingDirectory=/opt/canopy-worker',
|
|
279
|
-
'ExecStart=/usr/bin/node index.js',
|
|
280
|
-
'Restart=always',
|
|
281
|
-
'RestartSec=5',
|
|
282
|
-
'TimeoutStartSec=300',
|
|
283
|
-
'StandardOutput=journal',
|
|
284
|
-
'StandardError=journal',
|
|
285
|
-
'EnvironmentFile=/opt/canopy-worker/.env',
|
|
286
|
-
'',
|
|
287
|
-
'[Install]',
|
|
288
|
-
'WantedBy=multi-user.target',
|
|
289
|
-
'SVCEOF',
|
|
290
|
-
'',
|
|
291
|
-
'# Set ownership for ec2-user',
|
|
292
|
-
'chown -R ec2-user:ec2-user /opt/canopy-worker',
|
|
293
|
-
'# Non-recursive: EFS access point enforces UID 1000 for Lambda.',
|
|
294
|
-
'# Only set ownership on mount point and workspace dir to avoid',
|
|
295
|
-
'# slow recursive chown on large filesystems during ASG replacements.',
|
|
296
|
-
'chown ec2-user:ec2-user /mnt/efs',
|
|
297
|
-
'mkdir -p /mnt/efs/workspace',
|
|
298
|
-
'chown ec2-user:ec2-user /mnt/efs/workspace',
|
|
299
|
-
'',
|
|
300
|
-
'# Start worker',
|
|
301
|
-
'systemctl daemon-reload',
|
|
302
|
-
'systemctl enable canopy-worker',
|
|
303
|
-
'systemctl start canopy-worker',
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
// Auto Scaling Group
|
|
307
|
-
this.workerAsg = new autoscaling.AutoScalingGroup(this, 'WorkerAsg', {
|
|
308
|
-
vpc: this.vpc,
|
|
309
|
-
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
|
|
310
|
-
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO),
|
|
311
|
-
machineImage: ec2.MachineImage.latestAmazonLinux2023({
|
|
312
|
-
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
|
|
313
|
-
}),
|
|
314
|
-
role: workerRole,
|
|
315
|
-
securityGroup: workerSg,
|
|
316
|
-
minCapacity: 1,
|
|
317
|
-
maxCapacity: 1,
|
|
318
|
-
userData,
|
|
319
|
-
spotPrice: props.spotMaxPrice ?? '0.0042', // On-demand rate for t4g.nano
|
|
320
|
-
healthCheck: autoscaling.HealthCheck.ec2({
|
|
321
|
-
grace: Duration.minutes(5),
|
|
322
|
-
}),
|
|
323
|
-
})
|
|
324
|
-
}
|
|
325
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Worker daemon
|
|
2
|
-
export { CmsWorker } from './worker'
|
|
3
|
-
export type { CmsWorkerConfig } from './worker'
|
|
4
|
-
|
|
5
|
-
// CDK Constructs
|
|
6
|
-
export { CanopyCmsService } from './constructs/cms-service'
|
|
7
|
-
export type { CanopyCmsServiceProps } from './constructs/cms-service'
|
|
8
|
-
export { CanopyCmsDistribution } from './constructs/cms-distribution'
|
|
9
|
-
export type { CanopyCmsDistributionProps } from './constructs/cms-distribution'
|
package/src/worker.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Re-export CmsWorker from the core canopycms package.
|
|
3
|
-
* The worker is cloud-agnostic and auth-agnostic — it lives in canopycms core.
|
|
4
|
-
* This file re-exports it for convenience from the CDK package.
|
|
5
|
-
*/
|
|
6
|
-
export { CmsWorker } from 'canopycms/worker/cms-worker'
|
|
7
|
-
export type { CmsWorkerConfig, AuthCacheRefresher } from 'canopycms/worker/cms-worker'
|