@friggframework/devtools 2.0.0--canary.463.62579dd.0 → 2.0.0--canary.461.ec909cf.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/frigg-cli/__tests__/unit/commands/db-setup.test.js +1 -1
- package/frigg-cli/db-setup-command/index.js +1 -1
- package/infrastructure/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/aws-discovery.js +549 -21
- package/infrastructure/aws-discovery.test.js +447 -1
- package/infrastructure/domains/database/aurora-builder.js +307 -0
- package/infrastructure/domains/database/aurora-builder.test.js +482 -0
- package/infrastructure/domains/networking/vpc-builder.js +718 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +772 -0
- package/infrastructure/domains/networking/vpc-discovery.js +159 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +445 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +385 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +129 -0
- package/infrastructure/infrastructure-composer.test.js +1895 -0
- package/infrastructure/scripts/build-prisma-layer.js +534 -0
- package/infrastructure/serverless-template.js +790 -84
- package/infrastructure/serverless-template.test.js +94 -1
- package/package.json +8 -6
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/utils/prisma-runner.js +0 -280
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const { AWSDiscovery } = require('./aws-discovery');
|
|
4
|
+
const { buildPrismaLayer } = require('./scripts/build-prisma-layer');
|
|
4
5
|
|
|
5
6
|
const shouldRunDiscovery = (AppDefinition) => {
|
|
6
7
|
console.log(
|
|
@@ -17,7 +18,8 @@ const shouldRunDiscovery = (AppDefinition) => {
|
|
|
17
18
|
return (
|
|
18
19
|
AppDefinition.vpc?.enable === true ||
|
|
19
20
|
AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
|
|
20
|
-
AppDefinition.ssm?.enable === true
|
|
21
|
+
AppDefinition.ssm?.enable === true ||
|
|
22
|
+
AppDefinition.database?.postgres?.enable === true
|
|
21
23
|
);
|
|
22
24
|
};
|
|
23
25
|
|
|
@@ -68,8 +70,7 @@ const getAppEnvironmentVars = (AppDefinition) => {
|
|
|
68
70
|
}
|
|
69
71
|
if (skippedKeys.length > 0) {
|
|
70
72
|
console.log(
|
|
71
|
-
` ⚠️ Skipped ${
|
|
72
|
-
skippedKeys.length
|
|
73
|
+
` ⚠️ Skipped ${skippedKeys.length
|
|
73
74
|
} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
|
|
74
75
|
);
|
|
75
76
|
}
|
|
@@ -230,7 +231,26 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
230
231
|
Tags: [
|
|
231
232
|
{
|
|
232
233
|
Key: 'Name',
|
|
233
|
-
Value: '${self:service}-${self:provider.stage}-public-subnet',
|
|
234
|
+
Value: '${self:service}-${self:provider.stage}-public-subnet-1',
|
|
235
|
+
},
|
|
236
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
237
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
238
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
239
|
+
{ Key: 'Type', Value: 'Public' },
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
FriggPublicSubnet2: {
|
|
244
|
+
Type: 'AWS::EC2::Subnet',
|
|
245
|
+
Properties: {
|
|
246
|
+
VpcId: { Ref: 'FriggVPC' },
|
|
247
|
+
CidrBlock: '10.0.4.0/24',
|
|
248
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
249
|
+
MapPublicIpOnLaunch: true,
|
|
250
|
+
Tags: [
|
|
251
|
+
{
|
|
252
|
+
Key: 'Name',
|
|
253
|
+
Value: '${self:service}-${self:provider.stage}-public-subnet-2',
|
|
234
254
|
},
|
|
235
255
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
236
256
|
{ Key: 'Service', Value: '${self:service}' },
|
|
@@ -341,6 +361,13 @@ const createVPCInfrastructure = (AppDefinition) => {
|
|
|
341
361
|
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
342
362
|
},
|
|
343
363
|
},
|
|
364
|
+
FriggPublicSubnet2RouteTableAssociation: {
|
|
365
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
366
|
+
Properties: {
|
|
367
|
+
SubnetId: { Ref: 'FriggPublicSubnet2' },
|
|
368
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
369
|
+
},
|
|
370
|
+
},
|
|
344
371
|
FriggPrivateRouteTable: {
|
|
345
372
|
Type: 'AWS::EC2::RouteTable',
|
|
346
373
|
Properties: {
|
|
@@ -544,10 +571,18 @@ const gatherDiscoveredResources = async (AppDefinition) => {
|
|
|
544
571
|
try {
|
|
545
572
|
const region = process.env.AWS_REGION || 'us-east-1';
|
|
546
573
|
const discovery = new AWSDiscovery(region);
|
|
574
|
+
// Use Serverless Framework's stage resolution (opt:stage with 'dev' as default)
|
|
575
|
+
// This matches how serverless.yml resolves ${opt:stage, "dev"}
|
|
576
|
+
// IMPORTANT: Use SLS_STAGE (not STAGE) to match actual deployment stage
|
|
577
|
+
const stage = process.env.SLS_STAGE || 'dev';
|
|
578
|
+
|
|
547
579
|
const config = {
|
|
548
580
|
vpc: AppDefinition.vpc || {},
|
|
549
581
|
encryption: AppDefinition.encryption || {},
|
|
550
582
|
ssm: AppDefinition.ssm || {},
|
|
583
|
+
database: AppDefinition.database || {},
|
|
584
|
+
serviceName: AppDefinition.name || 'create-frigg-app',
|
|
585
|
+
stage: stage,
|
|
551
586
|
};
|
|
552
587
|
|
|
553
588
|
const discoveredResources = await discovery.discoverResources(config);
|
|
@@ -592,7 +627,9 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
|
|
|
592
627
|
defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
|
|
593
628
|
privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
|
|
594
629
|
privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
|
|
595
|
-
publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID',
|
|
630
|
+
publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID', // Keep for backward compat
|
|
631
|
+
publicSubnetId1: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_1',
|
|
632
|
+
publicSubnetId2: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_2',
|
|
596
633
|
defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
|
|
597
634
|
defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
|
|
598
635
|
};
|
|
@@ -603,6 +640,22 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
|
|
|
603
640
|
}
|
|
604
641
|
}
|
|
605
642
|
|
|
643
|
+
// Add Aurora discovery mappings
|
|
644
|
+
if (discoveredResources.aurora) {
|
|
645
|
+
if (discoveredResources.aurora.clusterIdentifier) {
|
|
646
|
+
environment.AWS_DISCOVERY_AURORA_CLUSTER_ID = discoveredResources.aurora.clusterIdentifier;
|
|
647
|
+
}
|
|
648
|
+
if (discoveredResources.aurora.endpoint) {
|
|
649
|
+
environment.AWS_DISCOVERY_AURORA_ENDPOINT = discoveredResources.aurora.endpoint;
|
|
650
|
+
}
|
|
651
|
+
if (discoveredResources.aurora.port) {
|
|
652
|
+
environment.AWS_DISCOVERY_AURORA_PORT = discoveredResources.aurora.port.toString();
|
|
653
|
+
}
|
|
654
|
+
if (discoveredResources.aurora.secretArn) {
|
|
655
|
+
environment.AWS_DISCOVERY_AURORA_SECRET_ARN = discoveredResources.aurora.secretArn;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
606
659
|
return environment;
|
|
607
660
|
};
|
|
608
661
|
|
|
@@ -613,20 +666,76 @@ const createBaseDefinition = (
|
|
|
613
666
|
) => {
|
|
614
667
|
const region = process.env.AWS_REGION || 'us-east-1';
|
|
615
668
|
|
|
669
|
+
// Function-level package config to exclude Prisma and AWS SDK
|
|
670
|
+
// Uses native Serverless package.exclude since jetpack function-level config isn't supported in v3
|
|
671
|
+
const functionPackageConfig = {
|
|
672
|
+
exclude: [
|
|
673
|
+
// Exclude AWS SDK (already in Lambda runtime)
|
|
674
|
+
'node_modules/aws-sdk/**',
|
|
675
|
+
'node_modules/@aws-sdk/**',
|
|
676
|
+
|
|
677
|
+
// Exclude Prisma (provided via Lambda Layer)
|
|
678
|
+
'node_modules/@prisma/**',
|
|
679
|
+
'node_modules/.prisma/**',
|
|
680
|
+
'node_modules/prisma/**',
|
|
681
|
+
'node_modules/@friggframework/core/generated/**',
|
|
682
|
+
|
|
683
|
+
// Exclude nested node_modules from symlinked frigg packages (for npm link development)
|
|
684
|
+
'node_modules/@friggframework/core/node_modules/**',
|
|
685
|
+
'node_modules/@friggframework/devtools/node_modules/**',
|
|
686
|
+
],
|
|
687
|
+
};
|
|
688
|
+
|
|
616
689
|
return {
|
|
617
690
|
frameworkVersion: '>=3.17.0',
|
|
618
691
|
service: AppDefinition.name || 'create-frigg-app',
|
|
619
692
|
package: {
|
|
620
693
|
individually: true,
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
694
|
+
// NOTE: These patterns are NOT used when serverless-jetpack is enabled with trace mode
|
|
695
|
+
// Jetpack's trace mode completely overrides package.patterns during dependency resolution
|
|
696
|
+
// These are kept commented out as a fallback if Jetpack needs to be disabled
|
|
697
|
+
patterns: [
|
|
698
|
+
// AWS SDK exclusions (already in Lambda runtime)
|
|
699
|
+
// '!**/node_modules/aws-sdk/**',
|
|
700
|
+
// '!**/node_modules/@aws-sdk/**',
|
|
701
|
+
|
|
702
|
+
// Prisma exclusions (provided via Lambda Layer)
|
|
703
|
+
// '!**/node_modules/@prisma/**',
|
|
704
|
+
// '!**/node_modules/.prisma/**',
|
|
705
|
+
// '!**/node_modules/@prisma-mongodb/**',
|
|
706
|
+
// '!**/node_modules/@prisma-postgresql/**',
|
|
707
|
+
// '!**/node_modules/prisma/**',
|
|
708
|
+
|
|
709
|
+
// Exclude Prisma generated clients from @friggframework/core
|
|
710
|
+
// '!**/node_modules/@friggframework/core/generated/**',
|
|
711
|
+
|
|
712
|
+
// Exclude development and test files
|
|
713
|
+
// '!**/test/**',
|
|
714
|
+
// '!**/tests/**',
|
|
715
|
+
// '!**/*.test.js',
|
|
716
|
+
// '!**/*.spec.js',
|
|
717
|
+
// '!**/*.map',
|
|
718
|
+
// '!**/jest.config.js',
|
|
719
|
+
// '!**/jest.unit.config.js',
|
|
720
|
+
// '!**/.eslintrc.json',
|
|
721
|
+
// '!**/.prettierrc',
|
|
722
|
+
// '!**/.prettierignore',
|
|
723
|
+
// '!**/.markdownlintignore',
|
|
724
|
+
// '!**/docker-compose.yml',
|
|
725
|
+
// '!**/package.json',
|
|
726
|
+
// '!**/README.md',
|
|
727
|
+
// '!**/*.md',
|
|
728
|
+
|
|
729
|
+
// Exclude .DS_Store and other OS files
|
|
730
|
+
// '!**/.DS_Store',
|
|
731
|
+
// '!**/.git/**',
|
|
732
|
+
// '!**/.claude-flow/**',
|
|
625
733
|
],
|
|
626
734
|
},
|
|
627
735
|
useDotenv: true,
|
|
628
736
|
provider: {
|
|
629
737
|
name: AppDefinition.provider || 'aws',
|
|
738
|
+
...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
|
|
630
739
|
runtime: 'nodejs20.x',
|
|
631
740
|
timeout: 30,
|
|
632
741
|
region,
|
|
@@ -697,13 +806,17 @@ const createBaseDefinition = (
|
|
|
697
806
|
skipCacheInvalidation: false,
|
|
698
807
|
},
|
|
699
808
|
jetpack: {
|
|
700
|
-
base: '..',
|
|
809
|
+
base: '..', // Essential for reaching handlers in node_modules/@friggframework
|
|
810
|
+
// NOTE: Service-level preInclude applies to EVERYTHING (functions + layers)
|
|
811
|
+
// We need to ONLY exclude from functions, not from the Prisma layer
|
|
812
|
+
// Solution: Apply exclusions at function level instead
|
|
701
813
|
},
|
|
702
814
|
},
|
|
703
815
|
functions: {
|
|
704
816
|
auth: {
|
|
705
|
-
handler:
|
|
706
|
-
|
|
817
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
|
|
818
|
+
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
819
|
+
package: functionPackageConfig,
|
|
707
820
|
events: [
|
|
708
821
|
{ httpApi: { path: '/api/integrations', method: 'ANY' } },
|
|
709
822
|
{
|
|
@@ -716,20 +829,52 @@ const createBaseDefinition = (
|
|
|
716
829
|
],
|
|
717
830
|
},
|
|
718
831
|
user: {
|
|
719
|
-
handler:
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
],
|
|
832
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
|
|
833
|
+
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
834
|
+
package: functionPackageConfig,
|
|
835
|
+
events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
|
|
724
836
|
},
|
|
725
837
|
health: {
|
|
726
|
-
handler:
|
|
727
|
-
|
|
838
|
+
handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
|
|
839
|
+
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
840
|
+
package: functionPackageConfig,
|
|
728
841
|
events: [
|
|
729
842
|
{ httpApi: { path: '/health', method: 'GET' } },
|
|
730
843
|
{ httpApi: { path: '/health/{proxy+}', method: 'GET' } },
|
|
731
844
|
],
|
|
732
845
|
},
|
|
846
|
+
dbMigrate: {
|
|
847
|
+
handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
|
|
848
|
+
// Uses Prisma Layer (includes CLI) - simpler than standalone packaging
|
|
849
|
+
layers: [{ Ref: 'PrismaLambdaLayer' }],
|
|
850
|
+
timeout: 300, // 5 minutes for long-running migrations
|
|
851
|
+
memorySize: 512, // Extra memory for Prisma CLI operations
|
|
852
|
+
reservedConcurrency: 1, // Prevent concurrent migrations
|
|
853
|
+
description: 'Runs database migrations via Prisma (invoke manually from CI/CD). Uses Prisma layer with CLI.',
|
|
854
|
+
package: functionPackageConfig, // Use same exclusions as other functions
|
|
855
|
+
// No events - this function is invoked manually via AWS CLI
|
|
856
|
+
maximumEventAge: 60, // Don't retry old migration requests (60 seconds)
|
|
857
|
+
maximumRetryAttempts: 0, // Don't auto-retry failed migrations
|
|
858
|
+
tags: {
|
|
859
|
+
Purpose: 'DatabaseMigration',
|
|
860
|
+
ManagedBy: 'Frigg',
|
|
861
|
+
},
|
|
862
|
+
// Environment variables for non-interactive Prisma CLI operation
|
|
863
|
+
environment: {
|
|
864
|
+
CI: '1', // Forces Prisma to non-interactive mode
|
|
865
|
+
PRISMA_HIDE_UPDATE_MESSAGE: '1', // Suppress update messages
|
|
866
|
+
PRISMA_MIGRATE_SKIP_SEED: '1', // Skip seeding during migrations
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
layers: {
|
|
871
|
+
prisma: {
|
|
872
|
+
path: 'layers/prisma',
|
|
873
|
+
name: '${self:service}-prisma-${sls:stage}',
|
|
874
|
+
description: 'Prisma ORM client with CLI and rhel-openssl-3.0.x binaries. Configured based on AppDefinition database settings. Used by all functions.',
|
|
875
|
+
compatibleRuntimes: ['nodejs18.x', 'nodejs20.x'],
|
|
876
|
+
retain: false, // Don't retain old layer versions
|
|
877
|
+
},
|
|
733
878
|
},
|
|
734
879
|
resources: {
|
|
735
880
|
Resources: {
|
|
@@ -828,18 +973,22 @@ const applyKmsConfiguration = (
|
|
|
828
973
|
}
|
|
829
974
|
|
|
830
975
|
if (discoveredResources.defaultKmsKeyId) {
|
|
831
|
-
console.log(
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
976
|
+
console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
|
|
977
|
+
|
|
978
|
+
// Only create alias if it doesn't already exist
|
|
979
|
+
if (!discoveredResources.kmsAliasExists) {
|
|
980
|
+
console.log('Creating KMS alias for discovered key...');
|
|
981
|
+
definition.resources.Resources.FriggKMSKeyAlias = {
|
|
982
|
+
Type: 'AWS::KMS::Alias',
|
|
983
|
+
DeletionPolicy: 'Retain',
|
|
984
|
+
Properties: {
|
|
985
|
+
AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
|
|
986
|
+
TargetKeyId: discoveredResources.defaultKmsKeyId,
|
|
987
|
+
},
|
|
988
|
+
};
|
|
989
|
+
} else {
|
|
990
|
+
console.log('KMS alias already exists, skipping alias creation');
|
|
991
|
+
}
|
|
843
992
|
|
|
844
993
|
definition.provider.iamRoleStatements.push({
|
|
845
994
|
Effect: 'Allow',
|
|
@@ -850,7 +999,7 @@ const applyKmsConfiguration = (
|
|
|
850
999
|
if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
|
|
851
1000
|
throw new Error(
|
|
852
1001
|
'KMS field-level encryption is enabled but no KMS key was found. ' +
|
|
853
|
-
|
|
1002
|
+
'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
|
|
854
1003
|
);
|
|
855
1004
|
}
|
|
856
1005
|
|
|
@@ -889,9 +1038,8 @@ const applyKmsConfiguration = (
|
|
|
889
1038
|
Resource: '*',
|
|
890
1039
|
Condition: {
|
|
891
1040
|
StringEquals: {
|
|
892
|
-
'kms:ViaService': `lambda.${
|
|
893
|
-
|
|
894
|
-
}.amazonaws.com`,
|
|
1041
|
+
'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'
|
|
1042
|
+
}.amazonaws.com`,
|
|
895
1043
|
},
|
|
896
1044
|
},
|
|
897
1045
|
},
|
|
@@ -1218,7 +1366,34 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1218
1366
|
Tags: [
|
|
1219
1367
|
{
|
|
1220
1368
|
Key: 'Name',
|
|
1221
|
-
Value: '${self:service}-${self:provider.stage}-public',
|
|
1369
|
+
Value: '${self:service}-${self:provider.stage}-public-1',
|
|
1370
|
+
},
|
|
1371
|
+
{ Key: 'Type', Value: 'Public' },
|
|
1372
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
1373
|
+
],
|
|
1374
|
+
},
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
// Create second public subnet in different AZ for Aurora
|
|
1378
|
+
let publicSubnet2Cidr;
|
|
1379
|
+
if (vpcManagement === 'create-new') {
|
|
1380
|
+
const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] };
|
|
1381
|
+
publicSubnet2Cidr = { 'Fn::Select': [3, generatedCidrs] };
|
|
1382
|
+
} else {
|
|
1383
|
+
publicSubnet2Cidr = '172.31.251.0/24';
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
definition.resources.Resources.FriggPublicSubnet2 = {
|
|
1387
|
+
Type: 'AWS::EC2::Subnet',
|
|
1388
|
+
Properties: {
|
|
1389
|
+
VpcId: subnetVpcId,
|
|
1390
|
+
CidrBlock: publicSubnet2Cidr,
|
|
1391
|
+
MapPublicIpOnLaunch: true,
|
|
1392
|
+
AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
|
|
1393
|
+
Tags: [
|
|
1394
|
+
{
|
|
1395
|
+
Key: 'Name',
|
|
1396
|
+
Value: '${self:service}-${self:provider.stage}-public-2',
|
|
1222
1397
|
},
|
|
1223
1398
|
{ Key: 'Type', Value: 'Public' },
|
|
1224
1399
|
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
@@ -1231,6 +1406,12 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1231
1406
|
{ Ref: 'FriggPrivateSubnet2' },
|
|
1232
1407
|
];
|
|
1233
1408
|
|
|
1409
|
+
// Map created subnets to discoveredResources for Aurora to use
|
|
1410
|
+
discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
|
|
1411
|
+
discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
|
|
1412
|
+
discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
|
|
1413
|
+
discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
|
|
1414
|
+
|
|
1234
1415
|
if (
|
|
1235
1416
|
!AppDefinition.vpc.natGateway ||
|
|
1236
1417
|
AppDefinition.vpc.natGateway.management === 'discover'
|
|
@@ -1293,13 +1474,22 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1293
1474
|
};
|
|
1294
1475
|
|
|
1295
1476
|
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1477
|
+
{
|
|
1478
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1479
|
+
Properties: {
|
|
1480
|
+
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
1481
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1482
|
+
},
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
|
|
1486
|
+
{
|
|
1487
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1488
|
+
Properties: {
|
|
1489
|
+
SubnetId: { Ref: 'FriggPublicSubnet2' },
|
|
1490
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1491
|
+
},
|
|
1492
|
+
};
|
|
1303
1493
|
|
|
1304
1494
|
definition.resources.Resources.FriggLambdaRouteTable = {
|
|
1305
1495
|
Type: 'AWS::EC2::RouteTable',
|
|
@@ -1316,22 +1506,22 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1316
1506
|
};
|
|
1317
1507
|
|
|
1318
1508
|
definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation =
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1509
|
+
{
|
|
1510
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1511
|
+
Properties: {
|
|
1512
|
+
SubnetId: { Ref: 'FriggPrivateSubnet1' },
|
|
1513
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1514
|
+
},
|
|
1515
|
+
};
|
|
1326
1516
|
|
|
1327
1517
|
definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation =
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1518
|
+
{
|
|
1519
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1520
|
+
Properties: {
|
|
1521
|
+
SubnetId: { Ref: 'FriggPrivateSubnet2' },
|
|
1522
|
+
RouteTableId: { Ref: 'FriggLambdaRouteTable' },
|
|
1523
|
+
},
|
|
1524
|
+
};
|
|
1335
1525
|
}
|
|
1336
1526
|
} else if (subnetManagement === 'use-existing') {
|
|
1337
1527
|
if (
|
|
@@ -1348,12 +1538,12 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1348
1538
|
AppDefinition.vpc.subnets?.ids?.length > 0
|
|
1349
1539
|
? AppDefinition.vpc.subnets.ids
|
|
1350
1540
|
: discoveredResources.privateSubnetId1 &&
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1541
|
+
discoveredResources.privateSubnetId2
|
|
1542
|
+
? [
|
|
1543
|
+
discoveredResources.privateSubnetId1,
|
|
1544
|
+
discoveredResources.privateSubnetId2,
|
|
1545
|
+
]
|
|
1546
|
+
: [];
|
|
1357
1547
|
|
|
1358
1548
|
if (vpcConfig.subnetIds.length < 2) {
|
|
1359
1549
|
if (AppDefinition.vpc.selfHeal) {
|
|
@@ -1524,7 +1714,28 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1524
1714
|
Tags: [
|
|
1525
1715
|
{
|
|
1526
1716
|
Key: 'Name',
|
|
1527
|
-
Value: '${self:service}-${self:provider.stage}-public-subnet',
|
|
1717
|
+
Value: '${self:service}-${self:provider.stage}-public-subnet-1',
|
|
1718
|
+
},
|
|
1719
|
+
{ Key: 'Type', Value: 'Public' },
|
|
1720
|
+
],
|
|
1721
|
+
},
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
definition.resources.Resources.FriggPublicSubnet2 = {
|
|
1725
|
+
Type: 'AWS::EC2::Subnet',
|
|
1726
|
+
Properties: {
|
|
1727
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
1728
|
+
CidrBlock:
|
|
1729
|
+
AppDefinition.vpc.natGateway
|
|
1730
|
+
?.publicSubnetCidr2 || '172.31.251.0/24',
|
|
1731
|
+
AvailabilityZone: {
|
|
1732
|
+
'Fn::Select': [1, { 'Fn::GetAZs': '' }],
|
|
1733
|
+
},
|
|
1734
|
+
MapPublicIpOnLaunch: true,
|
|
1735
|
+
Tags: [
|
|
1736
|
+
{
|
|
1737
|
+
Key: 'Name',
|
|
1738
|
+
Value: '${self:service}-${self:provider.stage}-public-subnet-2',
|
|
1528
1739
|
},
|
|
1529
1740
|
{ Key: 'Type', Value: 'Public' },
|
|
1530
1741
|
],
|
|
@@ -1560,13 +1771,26 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1560
1771
|
};
|
|
1561
1772
|
|
|
1562
1773
|
definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1774
|
+
{
|
|
1775
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1776
|
+
Properties: {
|
|
1777
|
+
SubnetId: { Ref: 'FriggPublicSubnet' },
|
|
1778
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1779
|
+
},
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
|
|
1783
|
+
{
|
|
1784
|
+
Type: 'AWS::EC2::SubnetRouteTableAssociation',
|
|
1785
|
+
Properties: {
|
|
1786
|
+
SubnetId: { Ref: 'FriggPublicSubnet2' },
|
|
1787
|
+
RouteTableId: { Ref: 'FriggPublicRouteTable' },
|
|
1788
|
+
},
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
// Map created public subnets to discoveredResources for Aurora
|
|
1792
|
+
discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
|
|
1793
|
+
discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
|
|
1570
1794
|
}
|
|
1571
1795
|
|
|
1572
1796
|
definition.resources.Resources.FriggNATGateway = {
|
|
@@ -1577,11 +1801,11 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1577
1801
|
AllocationId: useExistingEip
|
|
1578
1802
|
? discoveredResources.existingElasticIpAllocationId
|
|
1579
1803
|
: {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1804
|
+
'Fn::GetAtt': [
|
|
1805
|
+
'FriggNATGatewayEIP',
|
|
1806
|
+
'AllocationId',
|
|
1807
|
+
],
|
|
1808
|
+
},
|
|
1585
1809
|
SubnetId: discoveredResources.publicSubnetId || {
|
|
1586
1810
|
Ref: 'FriggPublicSubnet',
|
|
1587
1811
|
},
|
|
@@ -1894,7 +2118,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1894
2118
|
},
|
|
1895
2119
|
};
|
|
1896
2120
|
|
|
1897
|
-
if
|
|
2121
|
+
// Create Secrets Manager VPC Endpoint if explicitly enabled OR if Aurora is enabled
|
|
2122
|
+
// (Aurora requires Secrets Manager access for credential retrieval)
|
|
2123
|
+
if (AppDefinition.secretsManager?.enable === true || AppDefinition.database?.postgres?.enable === true) {
|
|
1898
2124
|
definition.resources.Resources.VPCEndpointSecretsManager = {
|
|
1899
2125
|
Type: 'AWS::EC2::VPCEndpoint',
|
|
1900
2126
|
Properties: {
|
|
@@ -1912,6 +2138,440 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
|
|
|
1912
2138
|
}
|
|
1913
2139
|
};
|
|
1914
2140
|
|
|
2141
|
+
const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
|
|
2142
|
+
const dbConfig = AppDefinition.database.postgres;
|
|
2143
|
+
const publiclyAccessible = dbConfig.publiclyAccessible === true;
|
|
2144
|
+
|
|
2145
|
+
console.log('🔧 Creating Aurora Serverless v2 infrastructure...');
|
|
2146
|
+
console.log(` Publicly Accessible: ${publiclyAccessible}`);
|
|
2147
|
+
|
|
2148
|
+
// 1. DB Subnet Group
|
|
2149
|
+
// Use public subnets if publicly accessible, private subnets otherwise
|
|
2150
|
+
let subnetIds;
|
|
2151
|
+
if (publiclyAccessible) {
|
|
2152
|
+
subnetIds = [discoveredResources.publicSubnetId1, discoveredResources.publicSubnetId2];
|
|
2153
|
+
console.log(` Using public subnets: ${subnetIds.join(', ')}`);
|
|
2154
|
+
|
|
2155
|
+
// Safety check - this should have been caught earlier, but double-check
|
|
2156
|
+
if (!subnetIds[0] || !subnetIds[1]) {
|
|
2157
|
+
throw new Error(
|
|
2158
|
+
'Public subnets are required for publicly accessible Aurora deployment but were not found. ' +
|
|
2159
|
+
'This should have been caught earlier in validation.'
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
} else {
|
|
2163
|
+
subnetIds = [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2];
|
|
2164
|
+
console.log(` Using private subnets: ${subnetIds.join(', ')}`);
|
|
2165
|
+
|
|
2166
|
+
// Safety check - this should have been caught earlier, but double-check
|
|
2167
|
+
if (!subnetIds[0] || !subnetIds[1]) {
|
|
2168
|
+
throw new Error(
|
|
2169
|
+
'Private subnets are required for private Aurora deployment but were not found. ' +
|
|
2170
|
+
'This should have been caught earlier in validation.'
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
definition.resources.Resources.FriggDBSubnetGroup = {
|
|
2176
|
+
Type: 'AWS::RDS::DBSubnetGroup',
|
|
2177
|
+
Properties: {
|
|
2178
|
+
DBSubnetGroupDescription: `Subnet group for Frigg Aurora cluster (${publiclyAccessible ? 'public' : 'private'})`,
|
|
2179
|
+
SubnetIds: subnetIds,
|
|
2180
|
+
Tags: [
|
|
2181
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
|
|
2182
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
2183
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
2184
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
2185
|
+
]
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
// 2. Security Group
|
|
2190
|
+
// Build security group ingress rules based on configuration
|
|
2191
|
+
const securityGroupIngress = [];
|
|
2192
|
+
|
|
2193
|
+
// Always allow Lambda functions to access the database (if VPC is configured)
|
|
2194
|
+
if (AppDefinition.vpc?.enable) {
|
|
2195
|
+
const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
|
|
2196
|
+
? { Ref: 'FriggLambdaSecurityGroup' }
|
|
2197
|
+
: discoveredResources.defaultSecurityGroupId;
|
|
2198
|
+
|
|
2199
|
+
securityGroupIngress.push({
|
|
2200
|
+
IpProtocol: 'tcp',
|
|
2201
|
+
FromPort: 5432,
|
|
2202
|
+
ToPort: 5432,
|
|
2203
|
+
SourceSecurityGroupId: lambdaSecurityGroupId,
|
|
2204
|
+
Description: 'PostgreSQL access from Lambda functions'
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// Add IP whitelist rules for public access
|
|
2209
|
+
if (publiclyAccessible && dbConfig.allowedIpAddresses) {
|
|
2210
|
+
const allowedIps = Array.isArray(dbConfig.allowedIpAddresses)
|
|
2211
|
+
? dbConfig.allowedIpAddresses
|
|
2212
|
+
: [dbConfig.allowedIpAddresses];
|
|
2213
|
+
|
|
2214
|
+
console.log(` Adding ${allowedIps.length} whitelisted IP address(es)`);
|
|
2215
|
+
|
|
2216
|
+
allowedIps.forEach((ip, index) => {
|
|
2217
|
+
// Ensure IP has CIDR notation
|
|
2218
|
+
const cidrIp = ip.includes('/') ? ip : `${ip}/32`;
|
|
2219
|
+
securityGroupIngress.push({
|
|
2220
|
+
IpProtocol: 'tcp',
|
|
2221
|
+
FromPort: 5432,
|
|
2222
|
+
ToPort: 5432,
|
|
2223
|
+
CidrIp: cidrIp,
|
|
2224
|
+
Description: `PostgreSQL access from whitelisted IP ${index + 1}`
|
|
2225
|
+
});
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// If publicly accessible but no IPs specified, warn the user
|
|
2230
|
+
if (publiclyAccessible && !dbConfig.allowedIpAddresses) {
|
|
2231
|
+
console.log(' ⚠️ WARNING: Database is publicly accessible but no IP whitelist configured!');
|
|
2232
|
+
console.log(' ⚠️ Add allowedIpAddresses to your database.postgres config for security.');
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
definition.resources.Resources.FriggAuroraSecurityGroup = {
|
|
2236
|
+
Type: 'AWS::EC2::SecurityGroup',
|
|
2237
|
+
Properties: {
|
|
2238
|
+
GroupDescription: `Security group for Frigg Aurora PostgreSQL (${publiclyAccessible ? 'public' : 'private'})`,
|
|
2239
|
+
VpcId: discoveredResources.defaultVpcId,
|
|
2240
|
+
SecurityGroupIngress: securityGroupIngress,
|
|
2241
|
+
Tags: [
|
|
2242
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
|
|
2243
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
2244
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
2245
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
2246
|
+
]
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
// 3. Secrets Manager Secret (database credentials)
|
|
2251
|
+
definition.resources.Resources.FriggDatabaseSecret = {
|
|
2252
|
+
Type: 'AWS::SecretsManager::Secret',
|
|
2253
|
+
Properties: {
|
|
2254
|
+
Name: '${self:service}-${self:provider.stage}-aurora-credentials',
|
|
2255
|
+
Description: 'Aurora PostgreSQL credentials for Frigg application',
|
|
2256
|
+
GenerateSecretString: {
|
|
2257
|
+
SecretStringTemplate: JSON.stringify({
|
|
2258
|
+
username: dbConfig.masterUsername || 'frigg_admin'
|
|
2259
|
+
}),
|
|
2260
|
+
GenerateStringKey: 'password',
|
|
2261
|
+
PasswordLength: 32,
|
|
2262
|
+
ExcludeCharacters: '"@/\\'
|
|
2263
|
+
},
|
|
2264
|
+
Tags: [
|
|
2265
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
2266
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
2267
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
2268
|
+
]
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
|
|
2272
|
+
// 4. Aurora Serverless v2 Cluster
|
|
2273
|
+
definition.resources.Resources.FriggAuroraCluster = {
|
|
2274
|
+
Type: 'AWS::RDS::DBCluster',
|
|
2275
|
+
DeletionPolicy: 'Snapshot',
|
|
2276
|
+
UpdateReplacePolicy: 'Snapshot',
|
|
2277
|
+
Properties: {
|
|
2278
|
+
Engine: 'aurora-postgresql',
|
|
2279
|
+
EngineVersion: dbConfig.engineVersion || '15.3',
|
|
2280
|
+
EngineMode: 'provisioned', // Required for Serverless v2
|
|
2281
|
+
DatabaseName: dbConfig.databaseName || 'frigg_db',
|
|
2282
|
+
MasterUsername: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
|
|
2283
|
+
MasterUserPassword: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
|
|
2284
|
+
DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
|
|
2285
|
+
VpcSecurityGroupIds: [{ Ref: 'FriggAuroraSecurityGroup' }],
|
|
2286
|
+
ServerlessV2ScalingConfiguration: {
|
|
2287
|
+
MinCapacity: dbConfig.scaling?.minCapacity || 0.5,
|
|
2288
|
+
MaxCapacity: dbConfig.scaling?.maxCapacity || 1.0
|
|
2289
|
+
},
|
|
2290
|
+
BackupRetentionPeriod: dbConfig.backupRetentionDays || 7,
|
|
2291
|
+
PreferredBackupWindow: dbConfig.preferredBackupWindow || '03:00-04:00',
|
|
2292
|
+
DeletionProtection: dbConfig.deletionProtection !== false,
|
|
2293
|
+
EnableCloudwatchLogsExports: ['postgresql'],
|
|
2294
|
+
Tags: [
|
|
2295
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-cluster' },
|
|
2296
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
2297
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
2298
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
2299
|
+
]
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
|
|
2303
|
+
// 5. Aurora Serverless v2 Instance
|
|
2304
|
+
definition.resources.Resources.FriggAuroraInstance = {
|
|
2305
|
+
Type: 'AWS::RDS::DBInstance',
|
|
2306
|
+
Properties: {
|
|
2307
|
+
Engine: 'aurora-postgresql',
|
|
2308
|
+
DBInstanceClass: 'db.serverless',
|
|
2309
|
+
DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
|
|
2310
|
+
PubliclyAccessible: publiclyAccessible,
|
|
2311
|
+
EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
|
|
2312
|
+
Tags: [
|
|
2313
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
|
|
2314
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
2315
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
2316
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
2317
|
+
]
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
// 6. Secret Attachment (links cluster to secret)
|
|
2322
|
+
definition.resources.Resources.FriggSecretAttachment = {
|
|
2323
|
+
Type: 'AWS::SecretsManager::SecretTargetAttachment',
|
|
2324
|
+
Properties: {
|
|
2325
|
+
SecretId: { Ref: 'FriggDatabaseSecret' },
|
|
2326
|
+
TargetId: { Ref: 'FriggAuroraCluster' },
|
|
2327
|
+
TargetType: 'AWS::RDS::DBCluster'
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
// 7. Add IAM permissions for Secrets Manager
|
|
2332
|
+
definition.provider.iamRoleStatements.push({
|
|
2333
|
+
Effect: 'Allow',
|
|
2334
|
+
Action: [
|
|
2335
|
+
'secretsmanager:GetSecretValue',
|
|
2336
|
+
'secretsmanager:DescribeSecret'
|
|
2337
|
+
],
|
|
2338
|
+
Resource: { Ref: 'FriggDatabaseSecret' }
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
// 8. Set DATABASE_URL environment variable
|
|
2342
|
+
definition.provider.environment.DATABASE_URL = {
|
|
2343
|
+
'Fn::Sub': [
|
|
2344
|
+
'postgresql://${Username}:${Password}@${Endpoint}:5432/${DatabaseName}',
|
|
2345
|
+
{
|
|
2346
|
+
Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
|
|
2347
|
+
Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
|
|
2348
|
+
Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
|
|
2349
|
+
DatabaseName: dbConfig.databaseName || 'frigg_db'
|
|
2350
|
+
}
|
|
2351
|
+
]
|
|
2352
|
+
};
|
|
2353
|
+
|
|
2354
|
+
// 9. Set DB_TYPE for Prisma client selection
|
|
2355
|
+
definition.provider.environment.DB_TYPE = 'postgresql';
|
|
2356
|
+
|
|
2357
|
+
console.log('✅ Aurora infrastructure resources created');
|
|
2358
|
+
};
|
|
2359
|
+
|
|
2360
|
+
const useExistingAurora = (definition, AppDefinition, discoveredResources) => {
|
|
2361
|
+
const dbConfig = AppDefinition.database.postgres;
|
|
2362
|
+
const selfHeal = AppDefinition.database?.postgres?.selfHeal !== false; // Default to true
|
|
2363
|
+
|
|
2364
|
+
console.log(`🔗 Using existing Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
|
|
2365
|
+
console.log(`[DEBUG] discoveredResources.aurora.isFriggManaged: ${discoveredResources.aurora.isFriggManaged}`);
|
|
2366
|
+
console.log(`[DEBUG] selfHeal: ${selfHeal}`);
|
|
2367
|
+
console.log(`[DEBUG] discoveredResources.aurora.secretArn: ${discoveredResources.aurora.secretArn}`);
|
|
2368
|
+
console.log(`[DEBUG] dbConfig.secretArn: ${dbConfig.secretArn}`);
|
|
2369
|
+
|
|
2370
|
+
// Add IAM permissions for Secrets Manager if secret exists
|
|
2371
|
+
if (discoveredResources.aurora.secretArn) {
|
|
2372
|
+
definition.provider.iamRoleStatements.push({
|
|
2373
|
+
Effect: 'Allow',
|
|
2374
|
+
Action: [
|
|
2375
|
+
'secretsmanager:GetSecretValue',
|
|
2376
|
+
'secretsmanager:DescribeSecret'
|
|
2377
|
+
],
|
|
2378
|
+
Resource: discoveredResources.aurora.secretArn
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
// Set DATABASE_URL from discovered secret
|
|
2382
|
+
definition.provider.environment.DATABASE_URL = {
|
|
2383
|
+
'Fn::Sub': [
|
|
2384
|
+
'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
|
|
2385
|
+
{
|
|
2386
|
+
Username: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:username}}` },
|
|
2387
|
+
Password: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:password}}` },
|
|
2388
|
+
Endpoint: discoveredResources.aurora.endpoint,
|
|
2389
|
+
Port: discoveredResources.aurora.port,
|
|
2390
|
+
DatabaseName: dbConfig.databaseName || 'frigg_db'
|
|
2391
|
+
}
|
|
2392
|
+
]
|
|
2393
|
+
};
|
|
2394
|
+
} else if (dbConfig.secretArn) {
|
|
2395
|
+
// Use user-provided secret ARN
|
|
2396
|
+
definition.provider.iamRoleStatements.push({
|
|
2397
|
+
Effect: 'Allow',
|
|
2398
|
+
Action: [
|
|
2399
|
+
'secretsmanager:GetSecretValue',
|
|
2400
|
+
'secretsmanager:DescribeSecret'
|
|
2401
|
+
],
|
|
2402
|
+
Resource: dbConfig.secretArn
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
definition.provider.environment.DATABASE_URL = {
|
|
2406
|
+
'Fn::Sub': [
|
|
2407
|
+
'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
|
|
2408
|
+
{
|
|
2409
|
+
Username: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:username}}` },
|
|
2410
|
+
Password: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:password}}` },
|
|
2411
|
+
Endpoint: discoveredResources.aurora.endpoint,
|
|
2412
|
+
Port: discoveredResources.aurora.port,
|
|
2413
|
+
DatabaseName: dbConfig.databaseName || 'frigg_db'
|
|
2414
|
+
}
|
|
2415
|
+
]
|
|
2416
|
+
};
|
|
2417
|
+
} else if (selfHeal && discoveredResources.aurora?.isFriggManaged) {
|
|
2418
|
+
// Self-healing mode: recreate missing secret for Frigg-managed cluster
|
|
2419
|
+
console.log('⚠️ No database secret found for Frigg-managed cluster');
|
|
2420
|
+
console.log('🔧 Self-healing enabled: Creating new database secret with automatic password rotation');
|
|
2421
|
+
|
|
2422
|
+
// Get the current master username from the cluster
|
|
2423
|
+
const currentUsername = discoveredResources.aurora.masterUsername || dbConfig.masterUsername || 'frigg_admin';
|
|
2424
|
+
|
|
2425
|
+
// Create Secrets Manager Secret (database credentials)
|
|
2426
|
+
// Note: We generate a NEW password, which will be synced to the cluster via SecretTargetAttachment
|
|
2427
|
+
definition.resources.Resources.FriggDatabaseSecret = {
|
|
2428
|
+
Type: 'AWS::SecretsManager::Secret',
|
|
2429
|
+
Properties: {
|
|
2430
|
+
Name: '${self:service}-${self:provider.stage}-aurora-credentials',
|
|
2431
|
+
Description: 'Aurora PostgreSQL credentials for Frigg application (auto-healed)',
|
|
2432
|
+
GenerateSecretString: {
|
|
2433
|
+
SecretStringTemplate: JSON.stringify({
|
|
2434
|
+
username: currentUsername
|
|
2435
|
+
}),
|
|
2436
|
+
GenerateStringKey: 'password',
|
|
2437
|
+
PasswordLength: 32,
|
|
2438
|
+
ExcludeCharacters: '"@/\\`\''
|
|
2439
|
+
},
|
|
2440
|
+
Tags: [
|
|
2441
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
2442
|
+
{ Key: 'Service', Value: '${self:service}' },
|
|
2443
|
+
{ Key: 'Stage', Value: '${self:provider.stage}' },
|
|
2444
|
+
{ Key: 'AutoHealed', Value: 'true' }
|
|
2445
|
+
]
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
// Create SecretTargetAttachment to link secret to existing cluster
|
|
2450
|
+
// This will automatically rotate the cluster password to match the secret!
|
|
2451
|
+
definition.resources.Resources.FriggSecretAttachment = {
|
|
2452
|
+
Type: 'AWS::SecretsManager::SecretTargetAttachment',
|
|
2453
|
+
Properties: {
|
|
2454
|
+
SecretId: { Ref: 'FriggDatabaseSecret' },
|
|
2455
|
+
TargetId: discoveredResources.aurora.clusterIdentifier,
|
|
2456
|
+
TargetType: 'AWS::RDS::DBCluster'
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
|
|
2460
|
+
// Add IAM permissions for the new secret
|
|
2461
|
+
definition.provider.iamRoleStatements.push({
|
|
2462
|
+
Effect: 'Allow',
|
|
2463
|
+
Action: [
|
|
2464
|
+
'secretsmanager:GetSecretValue',
|
|
2465
|
+
'secretsmanager:DescribeSecret'
|
|
2466
|
+
],
|
|
2467
|
+
Resource: { Ref: 'FriggDatabaseSecret' }
|
|
2468
|
+
});
|
|
2469
|
+
|
|
2470
|
+
// Set DATABASE_URL from new secret
|
|
2471
|
+
definition.provider.environment.DATABASE_URL = {
|
|
2472
|
+
'Fn::Sub': [
|
|
2473
|
+
'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
|
|
2474
|
+
{
|
|
2475
|
+
Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
|
|
2476
|
+
Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
|
|
2477
|
+
Endpoint: discoveredResources.aurora.endpoint,
|
|
2478
|
+
Port: discoveredResources.aurora.port,
|
|
2479
|
+
DatabaseName: dbConfig.databaseName || 'frigg_db'
|
|
2480
|
+
}
|
|
2481
|
+
]
|
|
2482
|
+
};
|
|
2483
|
+
|
|
2484
|
+
console.log('✅ Self-healing configuration complete:');
|
|
2485
|
+
console.log(' - New secret will be created with auto-generated password');
|
|
2486
|
+
console.log(' - SecretTargetAttachment will automatically update cluster password');
|
|
2487
|
+
console.log(' - No manual password sync required!');
|
|
2488
|
+
} else {
|
|
2489
|
+
throw new Error(
|
|
2490
|
+
'No database secret found. Options:\n' +
|
|
2491
|
+
' 1. Provide secretArn in database.postgres configuration\n' +
|
|
2492
|
+
' 2. Ensure Secrets Manager secret exists\n' +
|
|
2493
|
+
' 3. Enable self-healing: set database.postgres.selfHeal to true (for Frigg-managed clusters only)'
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// Set DB_TYPE for Prisma client selection
|
|
2498
|
+
definition.provider.environment.DB_TYPE = 'postgresql';
|
|
2499
|
+
|
|
2500
|
+
console.log('✅ Existing Aurora cluster configured');
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
const useDiscoveredAurora = (definition, AppDefinition, discoveredResources) => {
|
|
2504
|
+
console.log(`🔍 Using discovered Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
|
|
2505
|
+
useExistingAurora(definition, AppDefinition, discoveredResources);
|
|
2506
|
+
};
|
|
2507
|
+
|
|
2508
|
+
const configurePostgres = (definition, AppDefinition, discoveredResources) => {
|
|
2509
|
+
if (!AppDefinition.database?.postgres?.enable) {
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
const dbConfig = AppDefinition.database.postgres;
|
|
2514
|
+
const publiclyAccessible = dbConfig.publiclyAccessible === true;
|
|
2515
|
+
|
|
2516
|
+
// Validate VPC is enabled for private deployments
|
|
2517
|
+
// Public deployments can work without VPC if using default VPC
|
|
2518
|
+
if (!publiclyAccessible && !AppDefinition.vpc?.enable) {
|
|
2519
|
+
throw new Error(
|
|
2520
|
+
'Aurora PostgreSQL requires VPC deployment for private access. ' +
|
|
2521
|
+
'Either set vpc.enable to true, or set database.postgres.publiclyAccessible to true for public access.'
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// Validate subnets based on deployment type
|
|
2526
|
+
const vpcManagement = AppDefinition.vpc?.management || 'discover';
|
|
2527
|
+
|
|
2528
|
+
if (publiclyAccessible) {
|
|
2529
|
+
// For public deployments, validate public subnets exist
|
|
2530
|
+
if (vpcManagement !== 'create-new' && (!discoveredResources.publicSubnetId1 || !discoveredResources.publicSubnetId2)) {
|
|
2531
|
+
throw new Error(
|
|
2532
|
+
'Aurora PostgreSQL with publiclyAccessible requires at least 2 public subnets in different availability zones. ' +
|
|
2533
|
+
'No public subnets were discovered in your VPC. ' +
|
|
2534
|
+
'Options:\n' +
|
|
2535
|
+
' 1. Create public subnets in your VPC with Internet Gateway attached\n' +
|
|
2536
|
+
' 2. Use VPC management mode "create-new" (will create public subnets automatically)\n' +
|
|
2537
|
+
' 3. Set publiclyAccessible to false and use private subnets instead'
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
} else {
|
|
2541
|
+
// For private deployments, validate private subnets exist
|
|
2542
|
+
if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
'Aurora PostgreSQL requires at least 2 private subnets in different availability zones for private deployment. ' +
|
|
2545
|
+
'No private subnets were discovered in your VPC. ' +
|
|
2546
|
+
'Options:\n' +
|
|
2547
|
+
' 1. Create private subnets in your VPC\n' +
|
|
2548
|
+
' 2. Use VPC management mode "create-new" (will create private subnets automatically)\n' +
|
|
2549
|
+
' 3. Set publiclyAccessible to true and use public subnets instead'
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
const management = dbConfig.management || 'discover';
|
|
2555
|
+
|
|
2556
|
+
console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
|
|
2557
|
+
|
|
2558
|
+
if (management === 'create-new' || discoveredResources.aurora?.needsCreation) {
|
|
2559
|
+
createAuroraInfrastructure(definition, AppDefinition, discoveredResources);
|
|
2560
|
+
} else if (management === 'use-existing') {
|
|
2561
|
+
if (!discoveredResources.aurora?.clusterIdentifier && !dbConfig.clusterIdentifier) {
|
|
2562
|
+
throw new Error('PostgreSQL management is set to "use-existing" but no clusterIdentifier was found or provided');
|
|
2563
|
+
}
|
|
2564
|
+
useExistingAurora(definition, AppDefinition, discoveredResources);
|
|
2565
|
+
} else {
|
|
2566
|
+
// discover mode
|
|
2567
|
+
if (discoveredResources.aurora?.clusterIdentifier) {
|
|
2568
|
+
useDiscoveredAurora(definition, AppDefinition, discoveredResources);
|
|
2569
|
+
} else {
|
|
2570
|
+
throw new Error('No Aurora cluster found in discovery mode. Set management to "create-new" or provide clusterIdentifier with "use-existing".');
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
|
|
1915
2575
|
const configureSsm = (definition, AppDefinition) => {
|
|
1916
2576
|
if (AppDefinition.ssm?.enable !== true) {
|
|
1917
2577
|
return;
|
|
@@ -1949,19 +2609,31 @@ const attachIntegrations = (definition, AppDefinition) => {
|
|
|
1949
2609
|
`Processing ${AppDefinition.integrations.length} integrations...`
|
|
1950
2610
|
);
|
|
1951
2611
|
|
|
2612
|
+
// Get the functionPackageConfig from the definition (defined in createBaseDefinition)
|
|
2613
|
+
const functionPackageConfig = {
|
|
2614
|
+
exclude: [
|
|
2615
|
+
'node_modules/aws-sdk/**',
|
|
2616
|
+
'node_modules/@aws-sdk/**',
|
|
2617
|
+
'node_modules/@prisma/**',
|
|
2618
|
+
'node_modules/.prisma/**',
|
|
2619
|
+
'node_modules/prisma/**',
|
|
2620
|
+
'node_modules/@friggframework/core/generated/**',
|
|
2621
|
+
],
|
|
2622
|
+
};
|
|
2623
|
+
|
|
1952
2624
|
for (const integration of AppDefinition.integrations) {
|
|
1953
2625
|
if (!integration?.Definition?.name) {
|
|
1954
2626
|
throw new Error('Invalid integration: missing Definition or name');
|
|
1955
2627
|
}
|
|
1956
2628
|
|
|
1957
2629
|
const integrationName = integration.Definition.name;
|
|
1958
|
-
const queueReference = `${
|
|
1959
|
-
|
|
1960
|
-
}Queue`;
|
|
2630
|
+
const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
|
|
2631
|
+
}Queue`;
|
|
1961
2632
|
const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
|
|
1962
2633
|
|
|
1963
2634
|
definition.functions[integrationName] = {
|
|
1964
2635
|
handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
|
|
2636
|
+
package: functionPackageConfig,
|
|
1965
2637
|
events: [
|
|
1966
2638
|
{
|
|
1967
2639
|
httpApi: {
|
|
@@ -1990,6 +2662,7 @@ const attachIntegrations = (definition, AppDefinition) => {
|
|
|
1990
2662
|
const queueWorkerName = `${integrationName}QueueWorker`;
|
|
1991
2663
|
definition.functions[queueWorkerName] = {
|
|
1992
2664
|
handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
|
|
2665
|
+
package: functionPackageConfig,
|
|
1993
2666
|
reservedConcurrency: 5,
|
|
1994
2667
|
events: [
|
|
1995
2668
|
{
|
|
@@ -2013,10 +2686,7 @@ const attachIntegrations = (definition, AppDefinition) => {
|
|
|
2013
2686
|
|
|
2014
2687
|
// Add webhook handler if enabled
|
|
2015
2688
|
const webhookConfig = integration.Definition.webhooks;
|
|
2016
|
-
if (
|
|
2017
|
-
webhookConfig &&
|
|
2018
|
-
(webhookConfig === true || webhookConfig.enabled === true)
|
|
2019
|
-
) {
|
|
2689
|
+
if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
|
|
2020
2690
|
const webhookFunctionName = `${integrationName}Webhook`;
|
|
2021
2691
|
|
|
2022
2692
|
definition.functions[webhookFunctionName] = {
|
|
@@ -2056,9 +2726,44 @@ const configureWebsockets = (definition, AppDefinition) => {
|
|
|
2056
2726
|
};
|
|
2057
2727
|
};
|
|
2058
2728
|
|
|
2729
|
+
/**
|
|
2730
|
+
* Ensure Prisma Lambda Layer exists
|
|
2731
|
+
* Automatically builds the layer if it doesn't exist in the project root
|
|
2732
|
+
* @param {Object} databaseConfig - Database configuration from AppDefinition.database
|
|
2733
|
+
*/
|
|
2734
|
+
async function ensurePrismaLayerExists(databaseConfig = {}) {
|
|
2735
|
+
const projectRoot = process.cwd();
|
|
2736
|
+
const layerPath = path.join(projectRoot, 'layers/prisma');
|
|
2737
|
+
|
|
2738
|
+
// Check if layer already exists
|
|
2739
|
+
if (fs.existsSync(layerPath)) {
|
|
2740
|
+
console.log('✓ Prisma Lambda Layer already exists at', layerPath);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// Layer doesn't exist - build it automatically
|
|
2745
|
+
console.log('📦 Prisma Lambda Layer not found - building automatically...');
|
|
2746
|
+
console.log(' Building layer with CLI (used by all functions including dbMigrate)');
|
|
2747
|
+
console.log(' This may take a minute on first deployment.\n');
|
|
2748
|
+
|
|
2749
|
+
try {
|
|
2750
|
+
// Build layer WITH CLI (includeCLI = true) - all functions use same layer
|
|
2751
|
+
await buildPrismaLayer(databaseConfig, true);
|
|
2752
|
+
console.log('✓ Prisma Lambda Layer built successfully\n');
|
|
2753
|
+
} catch (error) {
|
|
2754
|
+
console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
|
|
2755
|
+
console.error(' You may need to run: npm install @friggframework/core\n');
|
|
2756
|
+
throw error;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2059
2760
|
const composeServerlessDefinition = async (AppDefinition) => {
|
|
2060
2761
|
console.log('composeServerlessDefinition', AppDefinition);
|
|
2061
2762
|
|
|
2763
|
+
// Ensure Prisma layer exists before generating serverless config
|
|
2764
|
+
// Pass database config so layer only includes needed database clients
|
|
2765
|
+
await ensurePrismaLayerExists(AppDefinition.database || {});
|
|
2766
|
+
|
|
2062
2767
|
const discoveredResources = await gatherDiscoveredResources(AppDefinition);
|
|
2063
2768
|
const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
|
|
2064
2769
|
const definition = createBaseDefinition(
|
|
@@ -2080,6 +2785,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
2080
2785
|
if (!isLocalBuild) {
|
|
2081
2786
|
applyKmsConfiguration(definition, AppDefinition, discoveredResources);
|
|
2082
2787
|
configureVpc(definition, AppDefinition, discoveredResources);
|
|
2788
|
+
configurePostgres(definition, AppDefinition, discoveredResources);
|
|
2083
2789
|
configureSsm(definition, AppDefinition);
|
|
2084
2790
|
} else {
|
|
2085
2791
|
console.log(
|