Skip to content

Commit 33c979d

Browse files
committed
feat(s3-deployment): support vsecurityGroups in BucketDeploymentProps
1 parent 71c492a commit 33c979d

File tree

3 files changed

+150
-4
lines changed

3 files changed

+150
-4
lines changed

packages/aws-cdk-lib/aws-s3-deployment/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,65 @@ new cdk.CfnOutput(this, 'ObjectKey', {
489489
});
490490
```
491491

492+
## Specifying a Custom VPC, Subnets, and Security Groups in BucketDeployment
493+
494+
By default, the AWS CDK BucketDeployment construct runs in a publicly accessible environment. However, for enhanced security and compliance, you may need to deploy your assets from within a VPC while restricting network access through custom subnets and security groups.
495+
496+
### Using a Custom VPC
497+
498+
To deploy assets within a private network, specify the vpc property in BucketDeploymentProps. This ensures that the deployment Lambda function executes within your specified VPC.
499+
500+
```ts
501+
const app = new cdk.App();
502+
const stack = new cdk.Stack(app, 'BucketDeploymentExample');
503+
504+
const vpc = ec2.Vpc.fromLookup(stack, 'ExistingVPC', { vpcId: 'vpc-12345678' });
505+
506+
const bucket = new s3.Bucket(stack, 'MyBucket');
507+
508+
new s3deployment.BucketDeployment(stack, 'DeployToS3', {
509+
destinationBucket: bucket,
510+
vpc: vpc,
511+
sources: [s3deployment.Source.asset('./website')],
512+
});
513+
```
514+
515+
### Specifying Subnets for Deployment
516+
517+
By default, when you specify a VPC, the BucketDeployment function is deployed in the private subnets of that VPC.
518+
However, you can customize the subnet selection using the vpcSubnets property.
519+
520+
```ts
521+
new s3deployment.BucketDeployment(stack, 'DeployToS3', {
522+
destinationBucket: bucket,
523+
vpc: vpc,
524+
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
525+
sources: [s3deployment.Source.asset('./website')],
526+
});
527+
```
528+
529+
### Defining Custom Security Groups
530+
531+
For enhanced network security, you can now specify custom security groups in BucketDeploymentProps.
532+
This allows fine-grained control over ingress and egress rules for the deployment Lambda function.
533+
534+
```ts
535+
const securityGroup = new ec2.SecurityGroup(stack, 'CustomSG', {
536+
vpc,
537+
description: 'Allow HTTPS outbound access',
538+
allowAllOutbound: false,
539+
});
540+
541+
securityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS traffic');
542+
543+
new s3deployment.BucketDeployment(stack, 'DeployWithSecurityGroup', {
544+
destinationBucket: bucket,
545+
vpc: vpc,
546+
securityGroups: [securityGroup],
547+
sources: [s3deployment.Source.asset('./website')],
548+
});
549+
```
550+
492551
## Notes
493552

494553
- This library uses an AWS CloudFormation custom resource which is about 10MiB in

packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,17 @@ export interface BucketDeploymentProps {
282282
* @default true
283283
*/
284284
readonly outputObjectKeys?: boolean;
285+
286+
/**
287+
* The list of security groups to associate with the lambda handlers network interfaces.
288+
*
289+
* Only used if 'vpc' is supplied.
290+
*
291+
* @default undefined - If the function is placed within a VPC and a security group is
292+
* not specified, either by this or securityGroup prop, a dedicated security
293+
* group will be created for this function.
294+
*/
295+
readonly securityGroups?: ec2.ISecurityGroup[];
285296
}
286297

287298
/**
@@ -351,7 +362,7 @@ export class BucketDeployment extends Construct {
351362

352363
const mountPath = `/mnt${accessPointPath}`;
353364
const handler = new BucketDeploymentSingletonFunction(this, 'CustomResourceHandler', {
354-
uuid: this.renderSingletonUuid(props.memoryLimit, props.ephemeralStorageSize, props.vpc),
365+
uuid: this.renderSingletonUuid(props.memoryLimit, props.ephemeralStorageSize, props.vpc, props.securityGroups),
355366
layers: [new AwsCliLayer(this, 'AwsCliLayer')],
356367
environment: {
357368
...props.useEfs ? { MOUNT_PATH: mountPath } : undefined,
@@ -366,6 +377,7 @@ export class BucketDeployment extends Construct {
366377
ephemeralStorageSize: props.ephemeralStorageSize,
367378
vpc: props.vpc,
368379
vpcSubnets: props.vpcSubnets,
380+
securityGroups: props.securityGroups ? props.securityGroups : undefined,
369381
filesystem: accessPoint ? lambda.FileSystem.fromEfsAccessPoint(
370382
accessPoint,
371383
mountPath,
@@ -559,7 +571,7 @@ export class BucketDeployment extends Construct {
559571
}
560572
}
561573

562-
private renderUniqueId(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc) {
574+
private renderUniqueId(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc, securityGroups?: ec2.ISecurityGroup[]) {
563575
let uuid = '';
564576

565577
// if the user specifes a custom memory limit, we define another singleton handler
@@ -592,13 +604,24 @@ export class BucketDeployment extends Construct {
592604
uuid += `-${vpc.node.addr}`;
593605
}
594606

607+
// if the user specifies security groups, we define another singleton handler
608+
// with this configuration. otherwise, it won't be possible to use multiple
609+
// configurations since we have a singleton.
610+
if (securityGroups && securityGroups.length > 0) {
611+
const sortedSecurityGroupIds = securityGroups
612+
.map(sg => sg.securityGroupId)
613+
.sort() // Ensure a consistent order
614+
.join('-'); // Join into a single string
615+
uuid += `-${sortedSecurityGroupIds}`;
616+
}
617+
595618
return uuid;
596619
}
597620

598-
private renderSingletonUuid(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc) {
621+
private renderSingletonUuid(memoryLimit?: number, ephemeralStorageSize?: cdk.Size, vpc?: ec2.IVpc, securityGroups?: ec2.ISecurityGroup[]) {
599622
let uuid = '8693BB64-9689-44B6-9AAF-B0CC9EB8756C';
600623

601-
uuid += this.renderUniqueId(memoryLimit, ephemeralStorageSize, vpc);
624+
uuid += this.renderUniqueId(memoryLimit, ephemeralStorageSize, vpc, securityGroups);
602625

603626
return uuid;
604627
}

packages/aws-cdk-lib/aws-s3-deployment/test/bucket-deployment.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readdirSync, readFileSync, existsSync } from 'fs';
22
import * as path from 'path';
33
import { testDeprecated } from '@aws-cdk/cdk-build-tools';
4+
import { Construct } from 'constructs';
45
import { Match, Template } from '../../assertions';
56
import * as cloudfront from '../../aws-cloudfront';
67
import * as ec2 from '../../aws-ec2';
@@ -1123,6 +1124,69 @@ test('deployment allows vpc and subnets to be implicitly supplied to lambda', ()
11231124
});
11241125
});
11251126

1127+
test('deployment allows security groups to be explicitly supplied to lambda', () => {
1128+
// GIVEN
1129+
const stack = new cdk.Stack();
1130+
const bucket = new s3.Bucket(stack, 'Dest');
1131+
const vpc = new ec2.Vpc(stack, 'SomeVpc', {});
1132+
const sg = new ec2.SecurityGroup(stack, 'SomeSecurityGroup', { vpc });
1133+
1134+
// WHEN
1135+
new s3deploy.BucketDeployment(stack, 'DeployWithVpc1', {
1136+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
1137+
destinationBucket: bucket,
1138+
vpc: vpc,
1139+
securityGroups: [sg],
1140+
});
1141+
1142+
// THEN
1143+
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', {
1144+
VpcConfig: Match.objectLike({
1145+
SecurityGroupIds: Match.arrayWith([
1146+
{
1147+
'Fn::GetAtt': Match.arrayWith([
1148+
Match.stringLikeRegexp('SomeSecurityGroup'), // Matches dynamically generated SG name
1149+
'GroupId',
1150+
]),
1151+
},
1152+
]),
1153+
}),
1154+
});
1155+
});
1156+
1157+
test('different security groups create different Lambdas and single CLI', () => {
1158+
// GIVEN
1159+
const stack = new cdk.Stack();
1160+
const bucket = new s3.Bucket(stack, 'Dest');
1161+
const c1 = new Construct(stack, 'Construct1');
1162+
const c2 = new Construct(stack, 'Construct2');
1163+
const vpc = new ec2.Vpc(stack, 'SomeVpc', {});
1164+
const sg1 = new ec2.SecurityGroup(stack, 'SomeSecurityGroup1', { vpc });
1165+
const sg2 = new ec2.SecurityGroup(stack, 'SomeSecurityGroup2', { vpc });
1166+
1167+
// WHEN
1168+
new s3deploy.BucketDeployment(c1, 'Deploy1', {
1169+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
1170+
destinationBucket: bucket,
1171+
destinationKeyPrefix: 'foo',
1172+
vpc: vpc,
1173+
securityGroups: [sg1],
1174+
});
1175+
new s3deploy.BucketDeployment(c2, 'Deploy2', {
1176+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website-2'))],
1177+
destinationBucket: bucket,
1178+
destinationKeyPrefix: 'bar',
1179+
vpc: vpc,
1180+
securityGroups: [sg2],
1181+
});
1182+
1183+
// THEN
1184+
const template = Template.fromStack(stack);
1185+
template.resourceCountIs('AWS::Lambda::LayerVersion', 1);
1186+
template.resourceCountIs('AWS::Lambda::Function', 2);
1187+
template.resourceCountIs('Custom::CDKBucketDeployment', 2);
1188+
});
1189+
11261190
test('s3 deployment bucket is identical to destination bucket', () => {
11271191
// GIVEN
11281192
const stack = new cdk.Stack();

0 commit comments

Comments
 (0)