Skip to content

Commit edac101

Browse files
authored
feat(s3-deployment): add deployedBucket attribute for sequencing (#15384)
This allows referencing the bucket in a way that ensures the deployment has happened before dependent resources are created. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 9754d44 commit edac101

File tree

5 files changed

+119
-5
lines changed

5 files changed

+119
-5
lines changed

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ This is what happens under the hood:
4242
`websiteBucket`). If there is more than one source, the sources will be
4343
downloaded and merged pre-deployment at this step.
4444

45+
If you are referencing the filled bucket in another construct that depends on
46+
the files already be there, be sure to use `deployment.deployedBucket`. This
47+
will ensure the bucket deployment has finished before the resource that uses
48+
the bucket is created:
49+
50+
```ts
51+
declare const websiteBucket: s3.Bucket;
52+
53+
const deployment = new s3deploy.BucketDeployment(this, 'DeployWebsite', {
54+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
55+
destinationBucket: websiteBucket,
56+
});
57+
58+
new ConstructThatReadsFromTheBucket(this, 'Consumer', {
59+
// Use 'deployment.deployedBucket' instead of 'websiteBucket' here
60+
bucket: deployment.deployedBucket,
61+
});
62+
```
63+
4564
## Supported sources
4665

4766
The following source types are supported for bucket deployments:
@@ -302,7 +321,7 @@ substituting it when its deployed to the destination with the actual value.
302321

303322
## Notes
304323

305-
- This library uses an AWS CloudFormation custom resource which about 10MiB in
324+
- This library uses an AWS CloudFormation custom resource which is about 10MiB in
306325
size. The code of this resource is bundled with this library.
307326
- AWS Lambda execution time is limited to 15min. This limits the amount of data
308327
which can be deployed into the bucket by this timeout.

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { kebab as toKebabCase } from 'case';
1212
import { Construct } from 'constructs';
1313
import { ISource, SourceConfig } from './source';
1414

15+
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
16+
// eslint-disable-next-line no-duplicate-imports, import/order
17+
import { Token } from '@aws-cdk/core';
18+
1519
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
1620
// eslint-disable-next-line no-duplicate-imports, import/order
1721
import { Construct as CoreConstruct } from '@aws-cdk/core';
@@ -239,6 +243,10 @@ export interface BucketDeploymentProps {
239243
* other S3 buckets or from local disk
240244
*/
241245
export class BucketDeployment extends CoreConstruct {
246+
private readonly cr: cdk.CustomResource;
247+
private _deployedBucket?: s3.IBucket;
248+
private requestDestinationArn: boolean = false;
249+
242250
constructor(scope: Construct, id: string, props: BucketDeploymentProps) {
243251
super(scope, id);
244252

@@ -328,7 +336,7 @@ export class BucketDeployment extends CoreConstruct {
328336
const hasMarkers = sources.some(source => source.markers);
329337

330338
const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.vpc)}`;
331-
const cr = new cdk.CustomResource(this, crUniqueId, {
339+
this.cr = new cdk.CustomResource(this, crUniqueId, {
332340
serviceToken: handler.functionArn,
333341
resourceType: 'Custom::CDKBucketDeployment',
334342
properties: {
@@ -345,13 +353,15 @@ export class BucketDeployment extends CoreConstruct {
345353
SystemMetadata: mapSystemMetadata(props),
346354
DistributionId: props.distribution?.distributionId,
347355
DistributionPaths: props.distributionPaths,
356+
// Passing through the ARN sequences dependencees on the deployment
357+
DestinationBucketArn: cdk.Lazy.string({ produce: () => this.requestDestinationArn ? props.destinationBucket.bucketArn : undefined }),
348358
},
349359
});
350360

351361
let prefix: string = props.destinationKeyPrefix ?
352362
`:${props.destinationKeyPrefix}` :
353363
'';
354-
prefix += `:${cr.node.addr.substr(-8)}`;
364+
prefix += `:${this.cr.node.addr.substr(-8)}`;
355365
const tagKey = CUSTOM_RESOURCE_OWNER_TAG + prefix;
356366

357367
// destinationKeyPrefix can be 104 characters before we hit
@@ -405,6 +415,21 @@ export class BucketDeployment extends CoreConstruct {
405415

406416
}
407417

418+
/**
419+
* The bucket after the deployment
420+
*
421+
* If you want to reference the destination bucket in another construct and make sure the
422+
* bucket deployment has happened before the next operation is started, pass the other construct
423+
* a reference to `deployment.deployedBucket`.
424+
*
425+
* Doing this replaces calling `otherResource.node.addDependency(deployment)`.
426+
*/
427+
public get deployedBucket(): s3.IBucket {
428+
this.requestDestinationArn = true;
429+
this._deployedBucket = this._deployedBucket ?? s3.Bucket.fromBucketArn(this, 'DestinationBucket', Token.asString(this.cr.getAtt('DestinationBucketArn')));
430+
return this._deployedBucket;
431+
}
432+
408433
private renderUniqueId(memoryLimit?: number, vpc?: ec2.IVpc) {
409434
let uuid = '';
410435

packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ def cfn_error(message=None):
118118
if distribution_id:
119119
cloudfront_invalidate(distribution_id, distribution_paths)
120120

121-
cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id)
121+
cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id, responseData={
122+
# Passing through the ARN sequences dependencees on the deployment
123+
'DestinationBucketArn': props.get('DestinationBucketArn')
124+
})
122125
except KeyError as e:
123126
cfn_error("invalid request. Missing key %s" % str(e))
124127
except Exception as e:

packages/@aws-cdk/aws-s3-deployment/rosetta/default.ts-fixture

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ class Fixture extends Stack {
1313
/// here
1414
}
1515
}
16+
17+
class ConstructThatReadsFromTheBucket {
18+
constructor(...args: any[]) {
19+
}
20+
}

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,68 @@ test('deployment allows vpc and subnets to be implicitly supplied to lambda', ()
945945
});
946946
});
947947

948+
test('s3 deployment bucket is identical to destination bucket', () => {
949+
// GIVEN
950+
const stack = new cdk.Stack();
951+
const bucket = new s3.Bucket(stack, 'Dest');
952+
953+
// WHEN
954+
const bd = new s3deploy.BucketDeployment(stack, 'Deployment', {
955+
destinationBucket: bucket,
956+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
957+
});
958+
959+
// Call this function
960+
void(bd.deployedBucket);
961+
962+
// THEN
963+
const template = Template.fromStack(stack);
964+
template.hasResourceProperties('Custom::CDKBucketDeployment', {
965+
// Since this utilizes GetAtt, we know CFN will deploy the bucket first
966+
// before deploying other resources that rely call the destination bucket.
967+
DestinationBucketArn: { 'Fn::GetAtt': ['DestC383B82A', 'Arn'] },
968+
});
969+
});
970+
971+
test('using deployment bucket references the destination bucket by means of the CustomResource', () => {
972+
// GIVEN
973+
const stack = new cdk.Stack();
974+
const bucket = new s3.Bucket(stack, 'Dest');
975+
const deployment = new s3deploy.BucketDeployment(stack, 'Deployment', {
976+
destinationBucket: bucket,
977+
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
978+
});
979+
980+
// WHEN
981+
new cdk.CfnOutput(stack, 'DestinationArn', {
982+
value: deployment.deployedBucket.bucketArn,
983+
});
984+
new cdk.CfnOutput(stack, 'DestinationName', {
985+
value: deployment.deployedBucket.bucketName,
986+
});
987+
988+
// THEN
989+
990+
const template = Template.fromStack(stack);
991+
expect(template.findOutputs('*')).toEqual({
992+
DestinationArn: {
993+
Value: { 'Fn::GetAtt': ['DeploymentCustomResource47E8B2E6', 'DestinationBucketArn'] },
994+
},
995+
DestinationName: {
996+
Value: {
997+
'Fn::Select': [0, {
998+
'Fn::Split': ['/', {
999+
'Fn::Select': [5, {
1000+
'Fn::Split': [':',
1001+
{ 'Fn::GetAtt': ['DeploymentCustomResource47E8B2E6', 'DestinationBucketArn'] }],
1002+
}],
1003+
}],
1004+
}],
1005+
},
1006+
},
1007+
});
1008+
});
1009+
9481010
test('resource id includes memory and vpc', () => {
9491011

9501012
// GIVEN
@@ -1139,4 +1201,4 @@ function readDataFile(casm: cxapi.CloudAssembly, assetId: string, filePath: stri
11391201
const asset = casm.stacks[0].assets.find(a => a.id === assetId);
11401202
if (!asset) { throw new Error('Asset not found'); }
11411203
return readFileSync(path.join(casm.directory, asset.path, filePath), 'utf-8');
1142-
}
1204+
}

0 commit comments

Comments
 (0)