From fb27852dc28088c3680442a51be37c5ba14698f2 Mon Sep 17 00:00:00 2001 From: Srinidhi Veeraraghavan Date: Tue, 16 Jun 2026 16:05:26 +0100 Subject: [PATCH 1/3] feat(aws-lambda): expose created Aliases on the returned Function createLambdaFunction now stashes any aliases it created on the returned function instance under .lambdaAliases, keyed by aliasName. The new FunctionWithAliases type makes this discoverable. Downstream constructs that previously had to import the alias by ARN via Function.fromFunctionArn (which triggers UnclearLambdaEnvironment and silently drops addPermission() invokes) can now consume the real Alias L2 directly, e.g. for LambdaIntegration. --- packages/aws/src/services/lambda/main.ts | 13 ++++++-- packages/aws/src/services/lambda/types.ts | 16 +++++++++- .../aws/test/services/lambda-manager.test.ts | 30 +++++++++++++++++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/packages/aws/src/services/lambda/main.ts b/packages/aws/src/services/lambda/main.ts index ae08d318..127881d8 100644 --- a/packages/aws/src/services/lambda/main.ts +++ b/packages/aws/src/services/lambda/main.ts @@ -22,7 +22,7 @@ import { CommonConstruct, CommonStack } from '../../common/index.js' import { createCfnOutput } from '../../utils/index.js' import { CloudFrontManager } from '../cloudfront/index.js' -import { LambdaAliasProps, LambdaEdgeProps, LambdaProps } from './types.js' +import { FunctionWithAliases, LambdaAliasProps, LambdaEdgeProps, LambdaProps } from './types.js' /** * Provides operations on AWS Lambda @@ -128,7 +128,7 @@ export class LambdaManager { deadLetterQueue = scope.sqsManager.createDeadLetterQueueForLambda(`${id}-dlq`, scope, props, redriveQueue) } - const lambdaFunction = new Function(scope, `${id}`, { + const lambdaFunction: FunctionWithAliases = new Function(scope, `${id}`, { ...props, /* When a VPC is provided, allow placement in public subnets (e.g. for NAT access) */ allowPublicSubnet: !!vpc, @@ -175,11 +175,17 @@ export class LambdaManager { } /* Create aliases and optionally attach provisioned concurrency auto-scaling. - Provisioned concurrency requires an alias — it cannot be set on $LATEST. */ + Provisioned concurrency requires an alias — it cannot be set on $LATEST. + The created Alias instances are also stashed on the returned function under + `.lambdaAliases` (keyed by aliasName) so downstream constructs can use them + directly instead of re-importing by ARN — which carries an UnclearLambdaEnvironment + warning and silently drops permissions when CDK can't statically prove same-env. */ if (props.lambdaAliases && !_.isEmpty(props.lambdaAliases)) { + const aliasMap: Record = {} props.lambdaAliases.forEach(alias => { const aliasId = alias.id ?? `${id}-${alias.aliasName}` const functionAlias = this.createLambdaFunctionAlias(`${aliasId}`, scope, alias, lambdaFunction.currentVersion) + aliasMap[alias.aliasName] = functionAlias createCfnOutput(`${id}-${alias.aliasName}AliasArn`, scope, functionAlias.functionArn) createCfnOutput(`${id}-${alias.aliasName}AliasName`, scope, functionAlias.aliasName) @@ -190,6 +196,7 @@ export class LambdaManager { }) } }) + lambdaFunction.lambdaAliases = aliasMap } if (props.tags && !_.isEmpty(props.tags)) { diff --git a/packages/aws/src/services/lambda/types.ts b/packages/aws/src/services/lambda/types.ts index 0cc0a309..7f8fb232 100644 --- a/packages/aws/src/services/lambda/types.ts +++ b/packages/aws/src/services/lambda/types.ts @@ -1,9 +1,23 @@ -import { AliasProps, FunctionProps } from 'aws-cdk-lib/aws-lambda' +import { Alias, AliasProps, Function, FunctionProps } from 'aws-cdk-lib/aws-lambda' import { SqsEventSourceProps } from 'aws-cdk-lib/aws-lambda-event-sources' import { TagProps } from '../../types/index.js' import { QueueProps } from '../simple-queue-service/index.js' +/** + * A Function returned by {@link LambdaManager.createLambdaFunction} when the + * caller supplied {@link LambdaProps.lambdaAliases}. The aliases that were + * created are exposed under `lambdaAliases`, keyed by aliasName, so downstream + * constructs can pass them directly to integrations (e.g. ApiGateway's + * LambdaIntegration) instead of re-importing by ARN — which carries an + * UnclearLambdaEnvironment warning and silently drops the invoke permission + * when CDK can't statically prove same-env. + */ +/** @category Interface */ +export type FunctionWithAliases = Function & { + lambdaAliases?: Record +} + /** * Props for Lambda@Edge function, matching aws-cdk-lib experimental EdgeFunctionProps. * Inlined because aws-cdk-lib does not export this subpath via its package exports map. diff --git a/packages/aws/test/services/lambda-manager.test.ts b/packages/aws/test/services/lambda-manager.test.ts index 2171b062..a2692c0b 100644 --- a/packages/aws/test/services/lambda-manager.test.ts +++ b/packages/aws/test/services/lambda-manager.test.ts @@ -3,7 +3,8 @@ import { Template } from 'aws-cdk-lib/assertions' import * as iam from 'aws-cdk-lib/aws-iam' import * as lambda from 'aws-cdk-lib/aws-lambda' import { Construct } from 'constructs' -import { CommonConstruct, CommonStack, CommonStackProps } from '../../src/index.js' +import _ from 'lodash' +import { CommonConstruct, CommonStack, CommonStackProps, FunctionWithAliases } from '../../src/index.js' import { STUB_SECRET_ARN } from '../common/stubs.js' interface TestStackProps extends CommonStackProps { @@ -33,6 +34,7 @@ const testStackProps = { class TestCommonStack extends CommonStack { declare props: TestStackProps + declare construct: TestCommonConstruct constructor(parent: cdk.App, name: string, props: cdk.StackProps) { super(parent, name, props) @@ -69,6 +71,7 @@ class TestInvalidCommonStack extends CommonStack { class TestCommonConstruct extends CommonConstruct { declare props: TestStackProps + public testLambdaWithConcurrency!: FunctionWithAliases constructor(parent: Construct, name: string, props: TestStackProps) { super(parent, name, props) @@ -98,7 +101,7 @@ class TestCommonConstruct extends CommonConstruct { [testLayer], new lambda.AssetCode('packages/aws/test/common/nodejs/lib') ) - this.lambdaManager.createLambdaFunction( + this.testLambdaWithConcurrency = this.lambdaManager.createLambdaFunction( 'test-lambda-with-concurrency', this, this.props.testLambdaWithConcurrency, @@ -254,6 +257,29 @@ describe('TestLambdaConstruct', () => { }) }) +describe('TestLambdaConstructAliasesExposed', () => { + test('exposes lambdaAliases keyed by aliasName as expected', () => { + const fn = commonStack.construct.testLambdaWithConcurrency + expect(_.keys(fn.lambdaAliases)).toEqual(['test-concurrent-alias']) + }) + + test('exposes the real Alias L2 attached to the consuming stack as expected', () => { + const alias = commonStack.construct.testLambdaWithConcurrency.lambdaAliases?.['test-concurrent-alias'] + expect(alias?.aliasName).toEqual('test-concurrent-alias') + /* Same stack — what addPermission() needs to skip the UnclearLambdaEnvironment warning */ + expect(cdk.Stack.of(alias!)).toBe(commonStack) + }) + + test('exposed alias count matches the number of AWS::Lambda::Alias CFN resources synthesised', () => { + const aliasResources = _.pickBy( + template.toJSON().Resources as Record, + resource => resource.Type === 'AWS::Lambda::Alias' + ) + const exposedAliasCount = _.size(commonStack.construct.testLambdaWithConcurrency.lambdaAliases) + expect(_.size(aliasResources)).toBeGreaterThanOrEqual(exposedAliasCount) + }) +}) + describe('TestLambdaConstruct', () => { test('provisions new lambda with concurrency settings as expected', () => { template.hasResourceProperties('AWS::Lambda::Function', { From be0ae63dad0905d86ba99c9c79d48feac1dca0b8 Mon Sep 17 00:00:00 2001 From: Srinidhi Veeraraghavan Date: Tue, 16 Jun 2026 16:06:13 +0100 Subject: [PATCH 2/3] feat(aws-s3): accept an IBucket for server access logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S3BucketProps gains an optional logBucket?: IBucket field alongside the existing logBucketName?: string. When the caller already owns the log destination (created in the same stack), passing the L2 lets CDK attach the log-delivery BucketPolicy directly — clearing the accessLogsPolicyNotAdded warning and eliminating an unnecessary Bucket.fromBucketName import. StaticSite is updated to pass its just-created siteLogBucket through so consumers get this for free. logBucketName remains supported for genuine cross-stack imports where only the destination name is known. --- .../aws/src/construct/static-site/main.ts | 8 ++- .../services/simple-storage-service/main.ts | 11 +++- .../services/simple-storage-service/types.ts | 14 ++++- packages/aws/test/services/s3-manager.test.ts | 58 +++++++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/packages/aws/src/construct/static-site/main.ts b/packages/aws/src/construct/static-site/main.ts index 7e54bfe6..ac0be2a6 100644 --- a/packages/aws/src/construct/static-site/main.ts +++ b/packages/aws/src/construct/static-site/main.ts @@ -130,7 +130,13 @@ export class StaticSite extends CommonConstruct { * @summary Method to create a site bucket */ protected createSiteBucket() { - this.siteBucket = this.s3Manager.createS3Bucket(`${this.id}-site`, this, this.props.siteBucket) + /* Pass the just-created siteLogBucket L2 so server-access logging is wired + via Ref instead of an imported bucket name — lets CDK attach the + log-delivery policy and clears the accessLogsPolicyNotAdded warning. */ + this.siteBucket = this.s3Manager.createS3Bucket(`${this.id}-site`, this, { + ...this.props.siteBucket, + logBucket: this.siteLogBucket, + }) } /** diff --git a/packages/aws/src/services/simple-storage-service/main.ts b/packages/aws/src/services/simple-storage-service/main.ts index af477f30..631db35c 100644 --- a/packages/aws/src/services/simple-storage-service/main.ts +++ b/packages/aws/src/services/simple-storage-service/main.ts @@ -105,9 +105,14 @@ export class S3Manager { if (props.existingBucket && props.bucketName) { bucket = Bucket.fromBucketName(scope, `${id}`, S3Manager.determineBucketName(scope, props, props.bucketName)) } else { - /* Optionally look up a separate bucket for server access logging */ - let logBucket - if (props.logBucketName) { + /* Resolve the server-access-logging destination bucket. When the caller + already owns the log bucket (e.g. created earlier in the same stack) + they should pass the IBucket instance via `logBucket` — CDK then attaches + the log-delivery policy directly and no `accessLogsPolicyNotAdded` + warning is raised. The `logBucketName` path remains for true imports + where only the name is known. */ + let logBucket = props.logBucket + if (!logBucket && props.logBucketName) { logBucket = Bucket.fromBucketName( scope, `${id}-logs`, diff --git a/packages/aws/src/services/simple-storage-service/types.ts b/packages/aws/src/services/simple-storage-service/types.ts index 8124cb38..674eb515 100644 --- a/packages/aws/src/services/simple-storage-service/types.ts +++ b/packages/aws/src/services/simple-storage-service/types.ts @@ -41,7 +41,19 @@ export interface S3BucketProps extends s3.BucketProps { existingBucket?: boolean /** Lifecycle rules for objects in the bucket */ lifecycleRules?: LifecycleRule[] - /** Name of an existing bucket to use for server access logging */ + /** + * Bucket instance to use for server access logging. Pass this when the caller + * already owns the log destination bucket (e.g. created earlier in the same + * stack) — CDK can then add the log-delivery policy to it directly. Falls back + * to {@link logBucketName} when not provided. + */ + logBucket?: s3.IBucket + /** + * Name of an existing bucket to use for server access logging. When set, the + * bucket is imported by name — CDK cannot mutate its policy in that case, + * which surfaces an `accessLogsPolicyNotAdded` warning at synth. Prefer + * {@link logBucket} when the destination bucket is owned by the caller. + */ logBucketName?: string /** Tags to apply to the bucket */ tags?: TagProps[] diff --git a/packages/aws/test/services/s3-manager.test.ts b/packages/aws/test/services/s3-manager.test.ts index f88760a3..e9985086 100644 --- a/packages/aws/test/services/s3-manager.test.ts +++ b/packages/aws/test/services/s3-manager.test.ts @@ -1,6 +1,7 @@ import * as cdk from 'aws-cdk-lib' import { Template } from 'aws-cdk-lib/assertions' import { Construct } from 'constructs' +import _ from 'lodash' import { CommonConstruct, CommonStack, CommonStackProps } from '../../src/index.js' interface TestStackProps extends CommonStackProps { @@ -195,6 +196,63 @@ describe('TestS3ManagerLifecycle', () => { }) }) +/* Test S3 access logging wired through the new `logBucket` (L2) prop */ +class TestS3OwnedLogBucketStack extends CommonStack { + declare props: TestStackProps + declare construct: TestS3OwnedLogBucketConstruct + + constructor(parent: cdk.App, name: string, props: cdk.StackProps) { + super(parent, name, props) + this.construct = new TestS3OwnedLogBucketConstruct(this, 'test-s3-owned-log-bucket', this.props) + } + + protected determineConstructProps(props: cdk.StackProps) { + return { + ...super.determineConstructProps(props), + ...{ + testBucket: this.node.tryGetContext('siteBucket'), + testLogBucket: this.node.tryGetContext('siteLogBucket'), + }, + } + } +} + +class TestS3OwnedLogBucketConstruct extends CommonConstruct { + declare props: TestStackProps + + constructor(parent: Construct, name: string, props: TestStackProps) { + super(parent, name, props) + const logBucket = this.s3Manager.createS3Bucket('test-owned-log-bucket', this, this.props.testLogBucket) + this.s3Manager.createS3Bucket('test-owned-bucket', this, { ...this.props.testBucket, logBucket }) + } +} + +const appS3Owned = new cdk.App({ context: testStackProps }) +const stackS3Owned = new TestS3OwnedLogBucketStack(appS3Owned, 'test-s3-owned-log-bucket-stack', testStackProps) +const templateS3Owned = Template.fromStack(stackS3Owned) + +describe('TestS3ManagerOwnedLogBucket', () => { + test('wires LoggingConfiguration via Ref to the owned log bucket as expected', () => { + /* When the caller passes the L2 IBucket, CDK can resolve the log destination + to a same-stack Ref instead of a literal bucket name string — which is what + allows it to attach the log-delivery policy and avoid the + @aws-cdk/aws-s3:accessLogsPolicyNotAdded warning. */ + const buckets = _.pickBy( + templateS3Owned.toJSON().Resources as Record, + resource => resource.Type === 'AWS::S3::Bucket' + ) + const sourceBucket = _.find(buckets, bucket => bucket.Properties.BucketName === 'site-123456789-eu-west-1-test') + const logBucketLogicalId = _.findKey( + buckets, + bucket => bucket.Properties.BucketName === 'site-logs-123456789-eu-west-1-test' + ) + expect(sourceBucket?.Properties.LoggingConfiguration).toEqual({ + DestinationBucketName: { Ref: logBucketLogicalId }, + LogFilePrefix: 'logs/', + }) + }) +}) + describe('TestS3Manager - Error Handling', () => { test('throws error when folders are empty', () => { const testStack = new TestCommonStack(app, 'test-error-folders', testStackProps) From 4cb4eb57f4cda05df4d1400899811ec7c0c7cb11 Mon Sep 17 00:00:00 2001 From: Srinidhi Veeraraghavan Date: Tue, 16 Jun 2026 16:07:25 +0100 Subject: [PATCH 3/3] chore: add changeset for L2-grant-attachment changes --- .changeset/expose-l2-for-grant-attachment.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/expose-l2-for-grant-attachment.md diff --git a/.changeset/expose-l2-for-grant-attachment.md b/.changeset/expose-l2-for-grant-attachment.md new file mode 100644 index 00000000..45000b56 --- /dev/null +++ b/.changeset/expose-l2-for-grant-attachment.md @@ -0,0 +1,8 @@ +--- +'@gradientedge/cdk-utils-aws': minor +'@gradientedge/cdk-utils': minor +--- + +Expose created Lambda Aliases on the returned Function (new `FunctionWithAliases` type), and accept an `IBucket` for S3 server access logging (new optional `logBucket` on `S3BucketProps`). + +Both let downstream constructs work with the real L2 instances instead of re-importing by ARN/name — clearing the `UnclearLambdaEnvironment` warning (and silently-dropped `addPermission()` invokes) when wiring `LambdaIntegration` to an alias, and the `accessLogsPolicyNotAdded` warning when the caller owns the log destination bucket. `StaticSite` is updated to pass its just-created `siteLogBucket` through so consumers benefit without code changes.