in packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts [303:514]
constructor(scope: Construct, id: string, props: BucketDeploymentProps) {
super(scope, id);
if (props.distributionPaths) {
if (!props.distribution) {
throw new ValidationError('Distribution must be specified if distribution paths are specified', this);
}
if (!cdk.Token.isUnresolved(props.distributionPaths)) {
if (!props.distributionPaths.every(distributionPath => cdk.Token.isUnresolved(distributionPath) || distributionPath.startsWith('/'))) {
throw new ValidationError('Distribution paths must start with "/"', this);
}
}
}
if (props.useEfs && !props.vpc) {
throw new ValidationError('Vpc must be specified if useEfs is set', this);
}
this.destinationBucket = props.destinationBucket;
const accessPointPath = '/lambda';
let accessPoint;
if (props.useEfs && props.vpc) {
const accessMode = '0777';
const fileSystem = this.getOrCreateEfsFileSystem(scope, {
vpc: props.vpc,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
accessPoint = fileSystem.addAccessPoint('AccessPoint', {
path: accessPointPath,
createAcl: {
ownerUid: '1001',
ownerGid: '1001',
permissions: accessMode,
},
posixUser: {
uid: '1001',
gid: '1001',
},
});
accessPoint.node.addDependency(fileSystem.mountTargetsAvailable);
}
// Making VPC dependent on BucketDeployment so that CFN stack deletion is smooth.
// Refer comments on https://github.com/aws/aws-cdk/pull/15220 for more details.
if (props.vpc) {
this.node.addDependency(props.vpc);
}
const mountPath = `/mnt${accessPointPath}`;
const handler = new BucketDeploymentSingletonFunction(this, 'CustomResourceHandler', {
uuid: this.renderSingletonUuid(props.memoryLimit, props.ephemeralStorageSize, props.vpc),
layers: [new AwsCliLayer(this, 'AwsCliLayer')],
environment: {
...props.useEfs ? { MOUNT_PATH: mountPath } : undefined,
// Override the built-in CA bundle from the AWS CLI with the Lambda-curated one
// This is necessary to make the CLI work in ADC regions.
AWS_CA_BUNDLE: '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem',
},
lambdaPurpose: 'Custom::CDKBucketDeployment',
timeout: cdk.Duration.minutes(15),
role: props.role,
memorySize: props.memoryLimit,
ephemeralStorageSize: props.ephemeralStorageSize,
vpc: props.vpc,
vpcSubnets: props.vpcSubnets,
filesystem: accessPoint ? lambda.FileSystem.fromEfsAccessPoint(
accessPoint,
mountPath,
) : undefined,
// props.logRetention is deprecated, make sure we only set it if it is actually provided
// otherwise jsii will print warnings even for users that don't use this directly
...(props.logRetention ? { logRetention: props.logRetention } : {}),
logGroup: props.logGroup,
});
const handlerRole = handler.role;
if (!handlerRole) { throw new ValidationError('lambda.SingletonFunction should have created a Role', this); }
this.handlerRole = handlerRole;
this.sources = props.sources.map((source: ISource) => source.bind(this, { handlerRole: this.handlerRole }));
this.destinationBucket.grantReadWrite(handler);
if (props.accessControl) {
this.destinationBucket.grantPutAcl(handler);
}
if (props.distribution) {
handler.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['cloudfront:GetInvalidation', 'cloudfront:CreateInvalidation'],
resources: ['*'],
}));
}
// Markers are not replaced if zip sources are not extracted, so throw an error
// if extraction is not wanted and sources have markers.
const _this = this;
this.node.addValidation({
validate(): string[] {
if (_this.sources.some(source => source.markers) && props.extract == false) {
return ['Some sources are incompatible with extract=false; sources with deploy-time values (such as \'snsTopic.topicArn\') must be extracted.'];
}
return [];
},
});
const crUniqueId = `CustomResource${this.renderUniqueId(props.memoryLimit, props.ephemeralStorageSize, props.vpc)}`;
this.cr = new cdk.CustomResource(this, crUniqueId, {
serviceToken: handler.functionArn,
resourceType: 'Custom::CDKBucketDeployment',
properties: {
SourceBucketNames: cdk.Lazy.uncachedList({ produce: () => this.sources.map(source => source.bucket.bucketName) }),
SourceObjectKeys: cdk.Lazy.uncachedList({ produce: () => this.sources.map(source => source.zipObjectKey) }),
SourceMarkers: cdk.Lazy.uncachedAny({
produce: () => {
return this.sources.reduce((acc, source) => {
if (source.markers) {
acc.push(source.markers);
// if there are more than 1 source, then all sources
// require markers (custom resource will throw an error otherwise)
} else if (this.sources.length > 1) {
acc.push({});
}
return acc;
}, [] as Array<Record<string, any>>);
},
}, { omitEmptyArray: true }),
SourceMarkersConfig: cdk.Lazy.uncachedAny({
produce: () => {
return this.sources.reduce((acc, source) => {
if (source.markersConfig) {
acc.push(source.markersConfig);
} else if (this.sources.length > 1) {
acc.push({});
}
return acc;
}, [] as Array<MarkersConfig>);
},
}, { omitEmptyArray: true }),
DestinationBucketName: this.destinationBucket.bucketName,
DestinationBucketKeyPrefix: props.destinationKeyPrefix,
RetainOnDelete: props.retainOnDelete,
Extract: props.extract,
Prune: props.prune ?? true,
Exclude: props.exclude,
Include: props.include,
UserMetadata: props.metadata ? mapUserMetadata(props.metadata) : undefined,
SystemMetadata: mapSystemMetadata(props),
DistributionId: props.distribution?.distributionId,
DistributionPaths: props.distributionPaths,
SignContent: props.signContent,
OutputObjectKeys: props.outputObjectKeys ?? true,
// Passing through the ARN sequences dependency on the deployment
DestinationBucketArn: cdk.Lazy.string({ produce: () => this.requestDestinationArn ? this.destinationBucket.bucketArn : undefined }),
},
});
let prefix: string = props.destinationKeyPrefix ?
`:${props.destinationKeyPrefix}` :
'';
prefix += `:${this.cr.node.addr.slice(-8)}`;
const tagKey = CUSTOM_RESOURCE_OWNER_TAG + prefix;
// destinationKeyPrefix can be 104 characters before we hit
// the tag key limit of 128
// '/this/is/a/random/key/prefix/that/is/a/lot/of/characters/do/we/think/that/it/will/ever/be/this/long?????'
// better to throw an error here than wait for CloudFormation to fail
if (!cdk.Token.isUnresolved(tagKey) && tagKey.length > 128) {
throw new ValidationError('The BucketDeployment construct requires that the "destinationKeyPrefix" be <=104 characters.', this);
}
/*
* This will add a tag to the deployment bucket in the format of
* `aws-cdk:cr-owned:{keyPrefix}:{uniqueHash}`
*
* For example:
* {
* Key: 'aws-cdk:cr-owned:deploy/here/:240D17B3',
* Value: 'true',
* }
*
* This will allow for scenarios where there is a single S3 Bucket that has multiple
* BucketDeployment resources deploying to it. Each bucket + keyPrefix can be "owned" by
* 1 or more BucketDeployment resources. Since there are some scenarios where multiple BucketDeployment
* resources can deploy to the same bucket and key prefix (e.g. using include/exclude) we
* also append part of the id to make the key unique.
*
* As long as a bucket + keyPrefix is "owned" by a BucketDeployment resource, another CR
* cannot delete data. There are a couple of scenarios where this comes into play.
*
* 1. If the LogicalResourceId of the CustomResource changes (e.g. the crUniqueId changes)
* CloudFormation will first issue a 'Create' to create the new CustomResource and will
* update the Tag on the bucket. CloudFormation will then issue a 'Delete' on the old CustomResource
* and since the new CR "owns" the Bucket+keyPrefix it will not delete the contents of the bucket
*
* 2. If the BucketDeployment resource is deleted _and_ it is the only CR for that bucket+keyPrefix
* then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the
* CR. Since there are no tags indicating that this bucket+keyPrefix is "owned" then it will delete
* the contents.
*
* 3. If the BucketDeployment resource is deleted _and_ it is *not* the only CR for that bucket:keyPrefix
* then CloudFormation will first remove the tag from the bucket and then issue a "Delete" to the CR.
* Since there are other CRs that also "own" that bucket+keyPrefix there will still be a tag on the bucket
* and the contents will not be removed.
*
* 4. If the BucketDeployment resource _and_ the S3 Bucket are both removed, then CloudFormation will first
* issue a "Delete" to the CR and since there is a tag on the bucket the contents will not be removed. If you
* want the contents of the bucket to be removed on bucket deletion, then `autoDeleteObjects` property should
* be set to true on the Bucket.
*/
cdk.Tags.of(this.destinationBucket).add(tagKey, 'true');
}