@appliance.sh/infra 1.12.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -36,7 +36,7 @@ export async function applianceInfra() {
|
|
|
36
36
|
const applianceBase = new baseController(
|
|
37
37
|
`${base}`,
|
|
38
38
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
-
{ config: baseConfig.data
|
|
39
|
+
{ config: baseConfig.data },
|
|
40
40
|
{
|
|
41
41
|
globalProvider: baseGlobalProvider,
|
|
42
42
|
provider: baseRegionalProvider,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import * as pulumi from '@pulumi/pulumi';
|
|
2
2
|
import * as aws from '@pulumi/aws';
|
|
3
|
-
import * as awsNative from '@pulumi/aws-native';
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { ApplianceBaseConfigInput, ApplianceBaseType } from '@appliance.sh/sdk';
|
|
6
5
|
|
|
7
6
|
export type ApplianceBaseAwsPublicArgs = {
|
|
8
|
-
config:
|
|
7
|
+
config: ApplianceBaseConfigInput;
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
export interface ApplianceBaseAwsPublicOpts extends pulumi.ComponentResourceOptions {
|
|
@@ -21,9 +20,15 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
|
|
|
21
20
|
public readonly certificateArn?: pulumi.Output<string>;
|
|
22
21
|
public readonly cloudfrontDistribution?: aws.cloudfront.Distribution;
|
|
23
22
|
|
|
23
|
+
public readonly config;
|
|
24
|
+
|
|
24
25
|
constructor(name: string, args: ApplianceBaseAwsPublicArgs, opts?: ApplianceBaseAwsPublicOpts) {
|
|
25
26
|
super('appliance-infra:appliance-base-aws-public', name, args, opts);
|
|
26
27
|
|
|
28
|
+
if (args.config.type !== ApplianceBaseType.ApplianceAwsPublic) {
|
|
29
|
+
throw new Error('Invalid config');
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
if (args.config.dns.createZone) {
|
|
28
33
|
this.zone = new aws.route53.Zone(
|
|
29
34
|
`${name}-zone`,
|
|
@@ -91,6 +96,33 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
|
|
|
91
96
|
).arn;
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
const state = new aws.s3.Bucket(
|
|
100
|
+
`${name}-state`,
|
|
101
|
+
{
|
|
102
|
+
acl: 'private',
|
|
103
|
+
forceDestroy: true,
|
|
104
|
+
},
|
|
105
|
+
{ parent: this, provider: opts?.provider }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
new aws.s3.BucketVersioning(
|
|
109
|
+
`${name}-state-versioning`,
|
|
110
|
+
{
|
|
111
|
+
bucket: state.bucket,
|
|
112
|
+
versioningConfiguration: { status: 'Enabled' },
|
|
113
|
+
},
|
|
114
|
+
{ parent: this, provider: opts?.provider }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
new aws.s3.BucketServerSideEncryptionConfiguration(
|
|
118
|
+
`${name}-state-sse`,
|
|
119
|
+
{
|
|
120
|
+
bucket: state.bucket,
|
|
121
|
+
rules: [{ applyServerSideEncryptionByDefault: { sseAlgorithm: 'AES256' } }],
|
|
122
|
+
},
|
|
123
|
+
{ parent: this, provider: opts?.provider }
|
|
124
|
+
);
|
|
125
|
+
|
|
94
126
|
const lambdaOrigin = new aws.lambda.CallbackFunction(
|
|
95
127
|
`${name}-origin`,
|
|
96
128
|
{
|
|
@@ -137,6 +169,193 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
|
|
|
137
169
|
{ parent: this, provider: opts?.globalProvider }
|
|
138
170
|
);
|
|
139
171
|
|
|
172
|
+
const edgeRouterRole = new aws.iam.Role(
|
|
173
|
+
`${name}-edge-router-role`,
|
|
174
|
+
{
|
|
175
|
+
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
|
|
176
|
+
Service: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'],
|
|
177
|
+
}),
|
|
178
|
+
},
|
|
179
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
new aws.iam.RolePolicyAttachment(
|
|
183
|
+
`${name}-edge-router-role-logging`,
|
|
184
|
+
{
|
|
185
|
+
role: edgeRouterRole.name,
|
|
186
|
+
policyArn: aws.iam.ManagedPolicy.AWSLambdaBasicExecutionRole,
|
|
187
|
+
},
|
|
188
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const edgeFunction = new aws.lambda.CallbackFunction(
|
|
192
|
+
`${name.replaceAll('.', '-')}-edge-router`,
|
|
193
|
+
{
|
|
194
|
+
role: edgeRouterRole,
|
|
195
|
+
runtime: 'nodejs22.x',
|
|
196
|
+
timeout: 5,
|
|
197
|
+
publish: true,
|
|
198
|
+
loggingConfig: {
|
|
199
|
+
logGroup: `/appliance/base/${name}/edge-router-logs`,
|
|
200
|
+
logFormat: 'Text',
|
|
201
|
+
},
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
|
+
callback: async (event: any) => {
|
|
204
|
+
const request = event.Records[0].cf.request;
|
|
205
|
+
const headers = request.headers;
|
|
206
|
+
const host = headers.host[0].value;
|
|
207
|
+
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
209
|
+
const dns = require('dns').promises;
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
211
|
+
const crypto = require('crypto');
|
|
212
|
+
|
|
213
|
+
let originUrl: URL;
|
|
214
|
+
let signatureRequired = false;
|
|
215
|
+
try {
|
|
216
|
+
const txtRecords = await dns.resolveTxt(`origin.${host}`);
|
|
217
|
+
const functionUrl = txtRecords[0][0];
|
|
218
|
+
originUrl = new URL(functionUrl);
|
|
219
|
+
signatureRequired = true;
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error(`Failed to resolve TXT record for origin.${host}`, e);
|
|
222
|
+
originUrl = new URL(lambdaOriginFunctionUrl.functionUrl.get());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (signatureRequired) {
|
|
226
|
+
// Extract the region from the Lambda Function URL hostname
|
|
227
|
+
// Format: <function-id>.lambda-url.<region>.on.aws
|
|
228
|
+
const hostnameParts = originUrl.hostname.split('.');
|
|
229
|
+
const regionIndex = hostnameParts.indexOf('lambda-url');
|
|
230
|
+
const targetRegion =
|
|
231
|
+
regionIndex >= 0 && hostnameParts[regionIndex + 1] ? hostnameParts[regionIndex + 1] : 'us-east-1';
|
|
232
|
+
|
|
233
|
+
// Helper functions for SigV4 signing
|
|
234
|
+
const sha256 = (data: string | Buffer) => crypto.createHash('sha256').update(data).digest();
|
|
235
|
+
const hmacSha256 = (key: Buffer | string, data: string) =>
|
|
236
|
+
crypto.createHmac('sha256', key).update(data).digest();
|
|
237
|
+
const toHex = (buffer: Buffer) => buffer.toString('hex');
|
|
238
|
+
|
|
239
|
+
const getSignatureKey = (secretKey: string, dateStamp: string, regionName: string, serviceName: string) => {
|
|
240
|
+
const kDate = hmacSha256(`AWS4${secretKey}`, dateStamp);
|
|
241
|
+
const kRegion = hmacSha256(kDate, regionName);
|
|
242
|
+
const kService = hmacSha256(kRegion, serviceName);
|
|
243
|
+
const kSigning = hmacSha256(kService, 'aws4_request');
|
|
244
|
+
return kSigning;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Sign the request using SigV4
|
|
248
|
+
// Get credentials from environment (Lambda@Edge has access to execution role credentials)
|
|
249
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
250
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
251
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
252
|
+
|
|
253
|
+
if (accessKeyId && secretAccessKey) {
|
|
254
|
+
const method = request.method;
|
|
255
|
+
const service = 'lambda';
|
|
256
|
+
const canonicalUri = request.uri || '/';
|
|
257
|
+
const canonicalQuerystring = request.querystring || '';
|
|
258
|
+
|
|
259
|
+
const now = new Date();
|
|
260
|
+
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
|
|
261
|
+
const dateStamp = amzDate.substring(0, 8);
|
|
262
|
+
|
|
263
|
+
// Create canonical headers
|
|
264
|
+
const payloadHash = toHex(
|
|
265
|
+
sha256(request.body?.data ? Buffer.from(request.body.data, request.body.encoding || 'base64') : '')
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const canonicalHeaders =
|
|
269
|
+
`host:${originUrl.hostname}\n` +
|
|
270
|
+
`x-amz-content-sha256:${payloadHash}\n` +
|
|
271
|
+
`x-amz-date:${amzDate}\n` +
|
|
272
|
+
(sessionToken ? `x-amz-security-token:${sessionToken}\n` : '');
|
|
273
|
+
|
|
274
|
+
const signedHeaders = sessionToken
|
|
275
|
+
? 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token'
|
|
276
|
+
: 'host;x-amz-content-sha256;x-amz-date';
|
|
277
|
+
|
|
278
|
+
const canonicalRequest = [
|
|
279
|
+
method,
|
|
280
|
+
canonicalUri,
|
|
281
|
+
canonicalQuerystring,
|
|
282
|
+
canonicalHeaders,
|
|
283
|
+
signedHeaders,
|
|
284
|
+
payloadHash,
|
|
285
|
+
].join('\n');
|
|
286
|
+
|
|
287
|
+
const algorithm = 'AWS4-HMAC-SHA256';
|
|
288
|
+
const credentialScope = `${dateStamp}/${targetRegion}/${service}/aws4_request`;
|
|
289
|
+
const stringToSign = [algorithm, amzDate, credentialScope, toHex(sha256(canonicalRequest))].join('\n');
|
|
290
|
+
|
|
291
|
+
const signingKey = getSignatureKey(secretAccessKey, dateStamp, targetRegion, service);
|
|
292
|
+
const signature = toHex(hmacSha256(signingKey, stringToSign));
|
|
293
|
+
|
|
294
|
+
const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
295
|
+
|
|
296
|
+
// Add SigV4 headers to the request
|
|
297
|
+
request.headers['authorization'] = [{ key: 'Authorization', value: authorizationHeader }];
|
|
298
|
+
request.headers['x-amz-date'] = [{ key: 'X-Amz-Date', value: amzDate }];
|
|
299
|
+
request.headers['x-amz-content-sha256'] = [{ key: 'X-Amz-Content-Sha256', value: payloadHash }];
|
|
300
|
+
if (sessionToken) {
|
|
301
|
+
request.headers['x-amz-security-token'] = [{ key: 'X-Amz-Security-Token', value: sessionToken }];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
request.origin = {
|
|
307
|
+
custom: {
|
|
308
|
+
domainName: originUrl.hostname,
|
|
309
|
+
port: 443,
|
|
310
|
+
protocol: 'https',
|
|
311
|
+
path: request.path,
|
|
312
|
+
sslProtocols: ['TLSv1', 'TLSv1.1', 'TLSv1.2'],
|
|
313
|
+
readTimeout: 30,
|
|
314
|
+
keepaliveTimeout: 5,
|
|
315
|
+
customHeaders: {},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
request.headers['host'] = [{ key: 'host', value: originUrl.hostname }];
|
|
319
|
+
request.headers['X-Forwarded-Host'] = [{ key: 'X-Forwarded-Host', value: host }];
|
|
320
|
+
|
|
321
|
+
return request;
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
new aws.lambda.Permission(
|
|
328
|
+
`${name}-edge-router-invoke-url-permission`,
|
|
329
|
+
{
|
|
330
|
+
function: edgeFunction.name,
|
|
331
|
+
action: 'lambda:InvokeFunction',
|
|
332
|
+
principal: 'edgelambda.amazonaws.com',
|
|
333
|
+
statementId: 'AllowExecutionFromCloudFront',
|
|
334
|
+
},
|
|
335
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const distributionLogs = new aws.cloudwatch.LogGroup(
|
|
339
|
+
`${name}-distribution-logs`,
|
|
340
|
+
{
|
|
341
|
+
name: pulumi.interpolate`/appliance/base/${name}/distribution-logs`,
|
|
342
|
+
retentionInDays: 7,
|
|
343
|
+
},
|
|
344
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const distributionDeliveryDestination = new aws.cloudwatch.LogDeliveryDestination(
|
|
348
|
+
`${name.replace('.', '-')}-distribution-delivery-destination`,
|
|
349
|
+
{
|
|
350
|
+
outputFormat: 'json',
|
|
351
|
+
deliveryDestinationType: 'CWL',
|
|
352
|
+
deliveryDestinationConfiguration: {
|
|
353
|
+
destinationResourceArn: distributionLogs.arn,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
357
|
+
);
|
|
358
|
+
|
|
140
359
|
this.cloudfrontDistribution = new aws.cloudfront.Distribution(
|
|
141
360
|
`${name}-distribution`,
|
|
142
361
|
{
|
|
@@ -152,17 +371,24 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
|
|
|
152
371
|
.apply((res) => res.id ?? ''),
|
|
153
372
|
originRequestPolicyId: aws.cloudfront
|
|
154
373
|
.getOriginRequestPolicyOutput(
|
|
155
|
-
{ name: 'Managed-
|
|
374
|
+
{ name: 'Managed-AllViewer' },
|
|
156
375
|
{
|
|
157
376
|
parent: this,
|
|
158
377
|
provider: opts?.globalProvider,
|
|
159
378
|
}
|
|
160
379
|
)
|
|
161
380
|
.apply((res) => res.id ?? ''),
|
|
162
|
-
allowedMethods: ['
|
|
163
|
-
cachedMethods: ['
|
|
381
|
+
allowedMethods: ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'],
|
|
382
|
+
cachedMethods: ['GET', 'HEAD'],
|
|
164
383
|
targetOriginId: 'LambdaOrigin',
|
|
165
384
|
viewerProtocolPolicy: 'redirect-to-https',
|
|
385
|
+
lambdaFunctionAssociations: [
|
|
386
|
+
{
|
|
387
|
+
eventType: 'origin-request',
|
|
388
|
+
lambdaArn: edgeFunction.qualifiedArn,
|
|
389
|
+
includeBody: true,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
166
392
|
},
|
|
167
393
|
origins: [
|
|
168
394
|
{
|
|
@@ -189,28 +415,24 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
|
|
|
189
415
|
{ parent: this, provider: opts?.globalProvider }
|
|
190
416
|
);
|
|
191
417
|
|
|
192
|
-
new aws.
|
|
193
|
-
`${name.
|
|
418
|
+
const distributionDeliverySource = new aws.cloudwatch.LogDeliverySource(
|
|
419
|
+
`${name.replace('.', '-')}-distribution-delivery-source`,
|
|
194
420
|
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
functionUrlAuthType: 'AWS_IAM',
|
|
199
|
-
sourceArn: this.cloudfrontDistribution.arn,
|
|
421
|
+
region: 'us-east-1',
|
|
422
|
+
logType: 'ACCESS_LOGS',
|
|
423
|
+
resourceArn: this.cloudfrontDistribution.arn,
|
|
200
424
|
},
|
|
201
|
-
{ parent: this, provider: opts?.
|
|
425
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
202
426
|
);
|
|
203
427
|
|
|
204
|
-
new
|
|
205
|
-
`${name.
|
|
428
|
+
new aws.cloudwatch.LogDelivery(
|
|
429
|
+
`${name.replace('.', '-')}-distribution-logging`,
|
|
206
430
|
{
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
functionName: lambdaOrigin.name,
|
|
211
|
-
invokedViaFunctionUrl: true,
|
|
431
|
+
region: 'us-east-1',
|
|
432
|
+
deliverySourceName: distributionDeliverySource.name,
|
|
433
|
+
deliveryDestinationArn: distributionDeliveryDestination.arn,
|
|
212
434
|
},
|
|
213
|
-
{ parent: this, provider: opts?.
|
|
435
|
+
{ parent: this, provider: opts?.globalProvider }
|
|
214
436
|
);
|
|
215
437
|
|
|
216
438
|
new aws.route53.Record(
|
|
@@ -224,5 +446,31 @@ export class ApplianceBaseAwsPublic extends pulumi.ComponentResource {
|
|
|
224
446
|
},
|
|
225
447
|
{ parent: this, provider: opts?.globalProvider }
|
|
226
448
|
);
|
|
449
|
+
|
|
450
|
+
this.config = {
|
|
451
|
+
name: name,
|
|
452
|
+
stateBackendUrl: pulumi.interpolate`s3://${state.bucket}`,
|
|
453
|
+
domainName: args.config.dns.domainName,
|
|
454
|
+
type: ApplianceBaseType.ApplianceAwsPublic,
|
|
455
|
+
aws: {
|
|
456
|
+
region: args.config.region,
|
|
457
|
+
zoneId: this.zoneId,
|
|
458
|
+
cloudfrontDistributionId: this.cloudfrontDistribution.id,
|
|
459
|
+
cloudfrontDistributionDomainName: this.cloudfrontDistribution.domainName,
|
|
460
|
+
edgeRouterRoleArn: edgeRouterRole.arn,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
new aws.ssm.Parameter(
|
|
465
|
+
`${name}-base-config`,
|
|
466
|
+
{
|
|
467
|
+
name: `/appliance/base/${name}/config`,
|
|
468
|
+
type: 'SecureString',
|
|
469
|
+
value: pulumi.jsonStringify(this.config),
|
|
470
|
+
},
|
|
471
|
+
{ parent: this, provider: opts?.provider }
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
this.registerOutputs(this.config);
|
|
227
475
|
}
|
|
228
476
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as pulumi from '@pulumi/pulumi';
|
|
2
|
-
import {
|
|
2
|
+
import { ApplianceBaseConfigInput, ApplianceBaseType } from '@appliance.sh/sdk';
|
|
3
3
|
|
|
4
4
|
export type ApplianceBaseAwsVpcArgs = {
|
|
5
|
-
config:
|
|
5
|
+
config: ApplianceBaseConfigInput;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export interface ApplianceBaseAwsVpcOpts extends pulumi.ComponentResourceOptions {
|
|
@@ -14,5 +14,9 @@ export interface ApplianceBaseAwsVpcOpts extends pulumi.ComponentResourceOptions
|
|
|
14
14
|
export class ApplianceBaseAwsVpc extends pulumi.ComponentResource {
|
|
15
15
|
constructor(name: string, args: ApplianceBaseAwsVpcArgs, opts?: ApplianceBaseAwsVpcOpts) {
|
|
16
16
|
super('appliance-infra:appliance-base-aws-vpc', name, args, opts);
|
|
17
|
+
|
|
18
|
+
if (args.config.type !== ApplianceBaseType.ApplianceAwsVpc) {
|
|
19
|
+
throw new Error('Invalid config');
|
|
20
|
+
}
|
|
17
21
|
}
|
|
18
22
|
}
|