Repository: awslabs/generative-ai-cdk-constructs
Pull request #193 (2024-01-12 19:31:37)

feat: clip memory for Lambda if it exceeds context
AWS Accounts with AWS Lambda memory size limits below 7076Mb will fail deployments without adjusting.

Override the default configuration if necessary through the CDK context, https://docs.aws.amazon.com/cdk/v2/guide/context.html:

```
cdk deploy --context maximumLambdaMemorySize=3008
```

---
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/awslabs/generative-ai-cdk-constructs/pull/193?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs) Report
Attention: `4 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`11ba8da`)](https://app.codecov.io/gh/awslabs/generative-ai-cdk-constructs/commit/11ba8da6e9b518c4c753c5268ba2e409d0cc3384?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs) 93.83% compared to head [(`9277a89`)](https://app.codecov.io/gh/awslabs/generative-ai-cdk-constructs/pull/193?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs) 93.78%.

| [Files](https://app.codecov.io/gh/awslabs/generative-ai-cdk-constructs/pull/193?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs) | Patch % | Lines |
|---|---|---|
| [src/common/helpers/utils.ts](https://app.codecov.io/gh/awslabs/generative-ai-cdk-constructs/pull/193?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs#diff-c3JjL2NvbW1vbi9oZWxwZXJzL3V0aWxzLnRz) | 77.77% | [4 Missing :warning: ](https://app.codecov.io/gh/awslabs/generative-ai-cdk-constructs/pull/193?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #193      +/-   ##
==========================================
- Coverage   93.83%   93.78%   -0.06%     
==========================================
  Files          18       18              
  Lines        5597     5615      +18     
  Branches      122      126       +4     
==========================================
+ Hits         5252     5266      +14     
- Misses        345      349       +4     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/awslabs/generative-ai-cdk-constructs/pull/193?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=awslabs).

scottschreckengaust: > looks good, just some small queries.

I replied and resolved with no changes.

Patch:
diff --git a/src/common/helpers/utils.ts b/src/common/helpers/utils.ts
index 1508229..9fd06bc 100644
--- a/src/common/helpers/utils.ts
+++ b/src/common/helpers/utils.ts
@@ -84,4 +84,21 @@ export function generatePhysicalName(
   }
 
   return prefix.toLowerCase() + (lower ? allParts.toLowerCase() : allParts) + '-' + uniqueStackIdPart;
+}
+
+export const maximumLambdaMemorySizeContextItem = 'maximumLambdaMemorySize';
+export const recommendedMaximumLambdaMemorySize = 7076;
+export function lambdaMemorySizeLimiter(construct: IConstruct, requestedMemorySizeInMegabytes: number) {
+  const maximumLambaMemorySize = construct.node.tryGetContext(maximumLambdaMemorySizeContextItem) === undefined ?
+    recommendedMaximumLambdaMemorySize :
+    parseInt(construct.node.tryGetContext(maximumLambdaMemorySizeContextItem));
+  if (maximumLambaMemorySize < recommendedMaximumLambdaMemorySize) {
+    console.warn(`Maximum Lambda memorySize, ${maximumLambaMemorySize}, is less than the recommended ${recommendedMaximumLambdaMemorySize}.`);
+  }
+  if (requestedMemorySizeInMegabytes > maximumLambaMemorySize) {
+    console.warn(`Reducing Lambda memorySize, ${requestedMemorySizeInMegabytes} to ${maximumLambaMemorySize} for ${construct.constructor.name}`);
+    return maximumLambaMemorySize;
+  } else {
+    return requestedMemorySizeInMegabytes;
+  }
 }
\ No newline at end of file
diff --git a/src/patterns/gen-ai/aws-rag-appsync-stepfn-opensearch/index.ts b/src/patterns/gen-ai/aws-rag-appsync-stepfn-opensearch/index.ts
index 3413377..f76b384 100644
--- a/src/patterns/gen-ai/aws-rag-appsync-stepfn-opensearch/index.ts
+++ b/src/patterns/gen-ai/aws-rag-appsync-stepfn-opensearch/index.ts
@@ -28,7 +28,7 @@ import * as stepfn_task from 'aws-cdk-lib/aws-stepfunctions-tasks';
 import { NagSuppressions } from 'cdk-nag';
 import { Construct } from 'constructs';
 import * as s3_bucket_helper from '../../../common/helpers/s3-bucket-helper';
-import { generatePhysicalName, version } from '../../../common/helpers/utils';
+import { generatePhysicalName, version, lambdaMemorySizeLimiter } from '../../../common/helpers/utils';
 import * as vpc_helper from '../../../common/helpers/vpc-helper';
 
 /**
@@ -431,7 +431,7 @@ export class RagAppsyncStepfnOpensearch extends Construct {
         tracing: lambda_tracing,
         vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
         securityGroups: [this.securityGroup],
-        memorySize: 1_769 * 4,
+        memorySize: lambdaMemorySizeLimiter(this, 1_769 * 4),
         timeout: Duration.minutes(15),
         environment: {
           GRAPHQL_URL: updateGraphQlApiEndpoint,
@@ -563,7 +563,7 @@ export class RagAppsyncStepfnOpensearch extends Construct {
         tracing: lambda_tracing,
         vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
         securityGroups: [this.securityGroup],
-        memorySize: 1_769 * 4,
+        memorySize: lambdaMemorySizeLimiter(this, 1_769 * 4),
         timeout: Duration.minutes(15),
         role: s3_transformer_job_function_role,
         environment: {
@@ -681,7 +681,7 @@ export class RagAppsyncStepfnOpensearch extends Construct {
         tracing: lambda_tracing,
         vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
         securityGroups: [this.securityGroup],
-        memorySize: 1_769 * 4,
+        memorySize: lambdaMemorySizeLimiter(this, 1_769 * 4),
         timeout: Duration.minutes(15),
         role: embeddings_job_function_role,
         environment: {
diff --git a/src/patterns/gen-ai/aws-summarization-appsync-stepfn/index.ts b/src/patterns/gen-ai/aws-summarization-appsync-stepfn/index.ts
index 08df83a..062c05c 100644
--- a/src/patterns/gen-ai/aws-summarization-appsync-stepfn/index.ts
+++ b/src/patterns/gen-ai/aws-summarization-appsync-stepfn/index.ts
@@ -30,7 +30,7 @@ import { Construct } from 'constructs';
 import * as eventBridge from '../../../common/helpers/eventbridge-helper';
 import * as redisHelper from '../../../common/helpers/redis-helper';
 import * as s3BucketHelper from '../../../common/helpers/s3-bucket-helper';
-import { generatePhysicalName, version } from '../../../common/helpers/utils';
+import { generatePhysicalName, version, lambdaMemorySizeLimiter } from '../../../common/helpers/utils';
 import * as vpcHelper from '../../../common/helpers/vpc-helper';
 
 export interface SummarizationAppsyncStepfnProps {
@@ -511,7 +511,7 @@ export class SummarizationAppsyncStepfn extends Construct {
         tracing: lambda_tracing,
         vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
         securityGroups: [this.securityGroup],
-        memorySize: 1_769 * 1,
+        memorySize: lambdaMemorySizeLimiter(this, 1_769 * 1),
         timeout: Duration.minutes(5),
         role: inputvalidatorLambdaRole,
         environment: {
@@ -593,7 +593,7 @@ export class SummarizationAppsyncStepfn extends Construct {
       vpc: this.vpc,
       vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
       securityGroups: [this.securityGroup],
-      memorySize: 1_769 * 1,
+      memorySize: lambdaMemorySizeLimiter(this, 1_769 * 1),
       tracing: lambda_tracing,
       timeout: Duration.minutes(5),
       role: documentReaderLambdaRole,
@@ -688,7 +688,7 @@ export class SummarizationAppsyncStepfn extends Construct {
       vpc: this.vpc,
       vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
       securityGroups: [this.securityGroup],
-      memorySize: 1_769 * 4,
+      memorySize: lambdaMemorySizeLimiter(this, 1_769 * 4),
       timeout: Duration.minutes(10),
       tracing: lambda_tracing,
       role: summaryGeneratorLambdaRole,


Test patch:
diff --git a/test/common/helpers/utils.test.ts b/test/common/helpers/utils.test.ts
index 52ddacd..8a92183 100644
--- a/test/common/helpers/utils.test.ts
+++ b/test/common/helpers/utils.test.ts
@@ -11,8 +11,133 @@
  *  and limitations under the License.
  */
 import * as cdk from 'aws-cdk-lib';
-import { generatePhysicalName } from '../../../src/common/helpers/utils';
+import { Construct } from 'constructs';
+import { generatePhysicalName, lambdaMemorySizeLimiter, maximumLambdaMemorySizeContextItem, recommendedMaximumLambdaMemorySize } from '../../../src/common/helpers/utils';
 
+describe('lambdaMemorySizeLimiter', () => {
+  let testConstruct: TestConstruct;
+  let spyOnConsoleWarn: any;
+  beforeAll(() => {
+    spyOnConsoleWarn = jest.spyOn(console, 'warn');
+  });
+  afterAll(() => {
+    // console.warn = consoleWarn;
+    jest.restoreAllMocks();
+    console.log('Test "lambdaMemorySizeLimiter" completed');
+  });
+
+  test('above recommended', () => {
+    const app = new cdk.App({});
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = recommendedMaximumLambdaMemorySize + 1;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(recommendedMaximumLambdaMemorySize);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(1); // above recommended
+  });
+  test('equal to recommended', () => {
+    const app = new cdk.App({});
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = recommendedMaximumLambdaMemorySize;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(requestedMemory);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(0);
+  });
+  test('below recommended', () => {
+    const app = new cdk.App({});
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = recommendedMaximumLambdaMemorySize - 1;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(requestedMemory);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(0);
+  });
+  test('above minimum context', () => {
+    const contextLambdaMemorySize = 3008;
+    const app = new cdk.App({
+      context: {
+        [maximumLambdaMemorySizeContextItem]: contextLambdaMemorySize.toString(),
+      },
+    });
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = contextLambdaMemorySize + 1;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(contextLambdaMemorySize);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(2); // warning below recommended and below context
+  });
+  test('at minimum context', () => {
+    const contextLambdaMemorySize = 3008;
+    const app = new cdk.App({
+      context: {
+        [maximumLambdaMemorySizeContextItem]: contextLambdaMemorySize.toString(),
+      },
+    });
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = contextLambdaMemorySize;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(requestedMemory);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(1); // warning below recommended
+  });
+  test('below minimum context', () => {
+    const contextLambdaMemorySize = 3008;
+    const app = new cdk.App({
+      context: {
+        [maximumLambdaMemorySizeContextItem]: contextLambdaMemorySize.toString(),
+      },
+    });
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = contextLambdaMemorySize - 1;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(requestedMemory);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(1); // warning below recommended.
+  });
+  test('above high context', () => {
+    const contextLambdaMemorySize = recommendedMaximumLambdaMemorySize * 2;
+    const app = new cdk.App({
+      context: {
+        [maximumLambdaMemorySizeContextItem]: contextLambdaMemorySize.toString(),
+      },
+    });
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = contextLambdaMemorySize + 1;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(contextLambdaMemorySize);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(1); // below context
+  });
+  test('at high context', () => {
+    const contextLambdaMemorySize = recommendedMaximumLambdaMemorySize * 2;
+    const app = new cdk.App({
+      context: {
+        [maximumLambdaMemorySizeContextItem]: contextLambdaMemorySize.toString(),
+      },
+    });
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = contextLambdaMemorySize;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(requestedMemory);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(0);
+  });
+  test('below high context', () => {
+    const contextLambdaMemorySize = recommendedMaximumLambdaMemorySize * 2;
+    const app = new cdk.App({
+      context: {
+        [maximumLambdaMemorySizeContextItem]: contextLambdaMemorySize.toString(),
+      },
+    });
+    const stack = new cdk.Stack(app, 'TestStack', { env: { account: '012345678912', region: 'bermuda-triangle-1' } });
+    testConstruct = new TestConstruct(stack, 'TestConstruct');
+    const requestedMemory = contextLambdaMemorySize - 1;
+    const actualMemory = lambdaMemorySizeLimiter(testConstruct, requestedMemory);
+    expect(actualMemory).toEqual(requestedMemory);
+    expect(spyOnConsoleWarn).toHaveBeenCalledTimes(0);
+  });
+});
 
 describe('generatePhysicalName', () => {
   let testResourceA: TestResource;
@@ -20,7 +145,7 @@ describe('generatePhysicalName', () => {
   let testStack: cdk.Stack;
 
   afterAll(() => {
-    console.log('Test completed');
+    console.log('Test "generatePhysicalName" completed');
   });
 
   beforeAll(() => {
@@ -105,4 +230,5 @@ describe('generatePhysicalName', () => {
 });
 
 
-class TestResource extends cdk.Resource {}
\ No newline at end of file
+class TestResource extends cdk.Resource {}
+class TestConstruct extends Construct {}
\ No newline at end of file