AWS S3 Deployment Construct Library
This library allows populating an S3 bucket with the contents of .zip files from other S3 buckets or from local disk.
The following example defines a publicly accessible S3 bucket with web hosting enabled and populates it from a local directory on disk.
const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
websiteIndexDocument: 'index.html',
publicReadAccess: true,
});
new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: websiteBucket,
destinationKeyPrefix: 'web/static', // optional prefix in destination bucket
});
website_bucket = s3.Bucket(self, "WebsiteBucket",
website_index_document="index.html",
public_read_access=True
)
s3deploy.BucketDeployment(self, "DeployWebsite",
sources=[s3deploy.Source.asset("./website-dist")],
destination_bucket=website_bucket,
destination_key_prefix="web/static"
)
Bucket websiteBucket = Bucket.Builder.create(this, "WebsiteBucket")
.websiteIndexDocument("index.html")
.publicReadAccess(true)
.build();
BucketDeployment.Builder.create(this, "DeployWebsite")
.sources(List.of(Source.asset("./website-dist")))
.destinationBucket(websiteBucket)
.destinationKeyPrefix("web/static")
.build();
var websiteBucket = new Bucket(this, "WebsiteBucket", new BucketProps {
WebsiteIndexDocument = "index.html",
PublicReadAccess = true
});
new BucketDeployment(this, "DeployWebsite", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website-dist") },
DestinationBucket = websiteBucket,
DestinationKeyPrefix = "web/static"
});
websiteBucket := s3.NewBucket(this, jsii.String("WebsiteBucket"), &BucketProps{
WebsiteIndexDocument: jsii.String("index.html"),
PublicReadAccess: jsii.Boolean(true),
})
s3deploy.NewBucketDeployment(this, jsii.String("DeployWebsite"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website-dist")),
},
DestinationBucket: websiteBucket,
DestinationKeyPrefix: jsii.String("web/static"),
})
This is what happens under the hood:
- When this stack is deployed (either via
cdk deployor via CI/CD), the contents of the localwebsite-distdirectory will be archived and uploaded to an intermediary assets bucket (theStagingBucketof the CDK bootstrap stack). If there is more than one source, they will be individually uploaded. - The
BucketDeploymentconstruct synthesizes a Lambda-backed custom CloudFormation resource of typeCustom::CDKBucketDeploymentinto the template. The source bucket/key is set to point to the assets bucket. - The custom resource invokes its associated Lambda function, which downloads the .zip archive, extracts it and issues
aws s3 sync --deleteagainst the destination bucket (in this casewebsiteBucket). If there is more than one source, the sources will be downloaded and merged pre-deployment at this step.
If you are referencing the filled bucket in another construct that depends on the files already be there, be sure to use deployment.deployedBucket. This will ensure the bucket deployment has finished before the resource that uses the bucket is created:
declare const websiteBucket: s3.Bucket;
const deployment = new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket: websiteBucket,
});
new ConstructThatReadsFromTheBucket(this, 'Consumer', {
// Use 'deployment.deployedBucket' instead of 'websiteBucket' here
bucket: deployment.deployedBucket,
});
# website_bucket: s3.Bucket
deployment = s3deploy.BucketDeployment(self, "DeployWebsite",
sources=[s3deploy.Source.asset(path.join(__dirname, "my-website"))],
destination_bucket=website_bucket
)
ConstructThatReadsFromTheBucket(self, "Consumer", {
# Use 'deployment.deployedBucket' instead of 'websiteBucket' here
"bucket": deployment.deployed_bucket
})
Bucket websiteBucket;
BucketDeployment deployment = BucketDeployment.Builder.create(this, "DeployWebsite")
.sources(List.of(Source.asset(join(__dirname, "my-website"))))
.destinationBucket(websiteBucket)
.build();
new ConstructThatReadsFromTheBucket(this, "Consumer", Map.of(
// Use 'deployment.deployedBucket' instead of 'websiteBucket' here
"bucket", deployment.getDeployedBucket()));
Bucket websiteBucket;
var deployment = new BucketDeployment(this, "DeployWebsite", new BucketDeploymentProps {
Sources = new [] { Source.Asset(Join(__dirname, "my-website")) },
DestinationBucket = websiteBucket
});
new ConstructThatReadsFromTheBucket(this, "Consumer", new Dictionary<string, IBucket> {
// Use 'deployment.deployedBucket' instead of 'websiteBucket' here
{ "bucket", deployment.DeployedBucket }
});
var websiteBucket Bucket
deployment := s3deploy.NewBucketDeployment(this, jsii.String("DeployWebsite"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(path.join(__dirname, jsii.String("my-website"))),
},
DestinationBucket: websiteBucket,
})
NewConstructThatReadsFromTheBucket(this, jsii.String("Consumer"), map[string]IBucket{
// Use 'deployment.deployedBucket' instead of 'websiteBucket' here
"bucket": deployment.deployedBucket,
})
It is also possible to add additional sources using the addSource method.
declare const websiteBucket: s3.IBucket;
const deployment = new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: websiteBucket,
destinationKeyPrefix: 'web/static', // optional prefix in destination bucket
});
deployment.addSource(s3deploy.Source.asset('./another-asset'));
# website_bucket: s3.IBucket
deployment = s3deploy.BucketDeployment(self, "DeployWebsite",
sources=[s3deploy.Source.asset("./website-dist")],
destination_bucket=website_bucket,
destination_key_prefix="web/static"
)
deployment.add_source(s3deploy.Source.asset("./another-asset"))
IBucket websiteBucket;
BucketDeployment deployment = BucketDeployment.Builder.create(this, "DeployWebsite")
.sources(List.of(Source.asset("./website-dist")))
.destinationBucket(websiteBucket)
.destinationKeyPrefix("web/static")
.build();
deployment.addSource(Source.asset("./another-asset"));
IBucket websiteBucket;
var deployment = new BucketDeployment(this, "DeployWebsite", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website-dist") },
DestinationBucket = websiteBucket,
DestinationKeyPrefix = "web/static"
});
deployment.AddSource(Source.Asset("./another-asset"));
var websiteBucket IBucket
deployment := s3deploy.NewBucketDeployment(this, jsii.String("DeployWebsite"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website-dist")),
},
DestinationBucket: websiteBucket,
DestinationKeyPrefix: jsii.String("web/static"),
})
deployment.AddSource(s3deploy.Source_Asset(jsii.String("./another-asset")))
For the Lambda function to download object(s) from the source bucket, besides the obvious s3:GetObject* permissions, the Lambda's execution role needs the kms:Decrypt and kms:DescribeKey permissions on the KMS key that is used to encrypt the bucket. By default, when the source bucket is encrypted with the S3 managed key of the account, these permissions are granted by the key's resource-based policy, so they do not need to be on the Lambda's execution role policy explicitly. However, if the encryption key is not the s3 managed one, its resource-based policy is quite likely to NOT grant such KMS permissions. In this situation, the Lambda execution will fail with an error message like below:
download failed: ...
An error occurred (AccessDenied) when calling the GetObject operation:
User: *** is not authorized to perform: kms:Decrypt on the resource associated with this ciphertext
because no identity-based policy allows the kms:Decrypt action
When this happens, users can use the public handlerRole property of BucketDeployment to manually add the KMS permissions:
declare const destinationBucket: s3.Bucket;
const deployment = new s3deploy.BucketDeployment(this, 'DeployFiles', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'source-files'))],
destinationBucket,
});
deployment.handlerRole.addToPolicy(
new iam.PolicyStatement({
actions: ['kms:Decrypt', 'kms:DescribeKey'],
effect: iam.Effect.ALLOW,
resources: ['<encryption key ARN>'],
}),
);
# destination_bucket: s3.Bucket
deployment = s3deploy.BucketDeployment(self, "DeployFiles",
sources=[s3deploy.Source.asset(path.join(__dirname, "source-files"))],
destination_bucket=destination_bucket
)
deployment.handler_role.add_to_policy(
iam.PolicyStatement(
actions=["kms:Decrypt", "kms:DescribeKey"],
effect=iam.Effect.ALLOW,
resources=["<encryption key ARN>"]
))
Bucket destinationBucket;
BucketDeployment deployment = BucketDeployment.Builder.create(this, "DeployFiles")
.sources(List.of(Source.asset(join(__dirname, "source-files"))))
.destinationBucket(destinationBucket)
.build();
deployment.handlerRole.addToPolicy(
PolicyStatement.Builder.create()
.actions(List.of("kms:Decrypt", "kms:DescribeKey"))
.effect(Effect.ALLOW)
.resources(List.of("<encryption key ARN>"))
.build());
Bucket destinationBucket;
var deployment = new BucketDeployment(this, "DeployFiles", new BucketDeploymentProps {
Sources = new [] { Source.Asset(Join(__dirname, "source-files")) },
DestinationBucket = destinationBucket
});
deployment.HandlerRole.AddToPolicy(
new PolicyStatement(new PolicyStatementProps {
Actions = new [] { "kms:Decrypt", "kms:DescribeKey" },
Effect = Effect.ALLOW,
Resources = new [] { "<encryption key ARN>" }
}));
var destinationBucket Bucket
deployment := s3deploy.NewBucketDeployment(this, jsii.String("DeployFiles"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(path.join(__dirname, jsii.String("source-files"))),
},
DestinationBucket: DestinationBucket,
})
deployment.HandlerRole.AddToPolicy(
iam.NewPolicyStatement(&PolicyStatementProps{
Actions: []*string{
jsii.String("kms:Decrypt"),
jsii.String("kms:DescribeKey"),
},
Effect: iam.Effect_ALLOW,
Resources: []*string{
jsii.String("<encryption key ARN>"),
},
}))
The situation above could arise from the following scenarios:
User created a customer managed KMS key and passed its ID to the
cdk bootstrapcommand via the--bootstrap-kms-key-idCLI option. The default key policy alone is not sufficient to grant the Lambda KMS permissions.A corporation uses its own custom CDK bootstrap process, which encrypts the CDK
StagingBucketby a KMS key from a management account of the corporation's AWS Organization. In this cross-account access scenario, the KMS permissions must be explicitly present in the Lambda's execution role policy.One of the sources for the
BucketDeploymentcomes from theSource.bucketstatic method, which points to a bucket whose encryption key is not the S3 managed one, and the resource-based policy of the encryption key is not sufficient to grant the Lambdakms:Decryptandkms:DescribeKeypermissions.
Supported sources
The following source types are supported for bucket deployments:
- Local .zip file:
s3deploy.Source.asset('/path/to/local/file.zip') - Local directory:
s3deploy.Source.asset('/path/to/local/directory') - Another bucket:
s3deploy.Source.bucket(bucket, zipObjectKey) - String data:
s3deploy.Source.data('object-key.txt', 'hello, world!')(supports deploy-time values) - JSON data:
s3deploy.Source.jsonData('object-key.json', { json: 'object' })(supports deploy-time values) - YAML data:
s3deploy.Source.yamlData('object-key.yaml', { yaml: 'object' })(supports deploy-time values)
To create a source from a single file, you can pass AssetOptions to exclude all but a single file:
- Single file:
s3deploy.Source.asset('/path/to/local/directory', { exclude: ['**', '!onlyThisFile.txt'] })
IMPORTANT The aws-s3-deployment module is only intended to be used with zip files from trusted sources. Directories bundled by the CDK CLI (by using Source.asset() on a directory) are safe. If you are using Source.asset() or Source.bucket() to reference an existing zip file, make sure you trust the file you are referencing. Zips from untrusted sources might be able to execute arbitrary code in the Lambda Function used by this module, and use its permissions to read or write unexpected files in the S3 bucket.
Referencing deployed object versions
When you deploy a single zip file without extracting it (extract: false) to a versioned destination bucket, you can obtain the S3 VersionId of each deployed object via objectVersionIds. This is useful when a consumer must reference a specific, immutable version of a deployed object — for example a Lambda function that references its code in S3 by version rather than copying it.
import * as cdk from 'aws-cdk-lib';
const bucket = new s3.Bucket(this, 'CodeBucket', { versioned: true });
const deployment = new s3deploy.BucketDeployment(this, 'DeployCode', {
sources: [s3deploy.Source.asset('/path/to/handler.zip')],
destinationBucket: bucket,
extract: false,
});
// `objectVersionIds` positionally matches `objectKeys`
const objectKey = cdk.Fn.select(0, deployment.objectKeys);
const versionId = cdk.Fn.select(0, deployment.objectVersionIds);
import aws_cdk as cdk
bucket = s3.Bucket(self, "CodeBucket", versioned=True)
deployment = s3deploy.BucketDeployment(self, "DeployCode",
sources=[s3deploy.Source.asset("/path/to/handler.zip")],
destination_bucket=bucket,
extract=False
)
# `objectVersionIds` positionally matches `objectKeys`
object_key = cdk.Fn.select(0, deployment.object_keys)
version_id = cdk.Fn.select(0, deployment.object_version_ids)
import software.amazon.awscdk.*;
Bucket bucket = Bucket.Builder.create(this, "CodeBucket").versioned(true).build();
BucketDeployment deployment = BucketDeployment.Builder.create(this, "DeployCode")
.sources(List.of(Source.asset("/path/to/handler.zip")))
.destinationBucket(bucket)
.extract(false)
.build();
// `objectVersionIds` positionally matches `objectKeys`
String objectKey = Fn.select(0, deployment.getObjectKeys());
String versionId = Fn.select(0, deployment.getObjectVersionIds());
using Amazon.CDK;
var bucket = new Bucket(this, "CodeBucket", new BucketProps { Versioned = true });
var deployment = new BucketDeployment(this, "DeployCode", new BucketDeploymentProps {
Sources = new [] { Source.Asset("/path/to/handler.zip") },
DestinationBucket = bucket,
Extract = false
});
// `objectVersionIds` positionally matches `objectKeys`
var objectKey = Fn.Select(0, deployment.ObjectKeys);
var versionId = Fn.Select(0, deployment.ObjectVersionIds);
import "github.com/aws/aws-cdk-go/awscdk"
bucket := s3.NewBucket(this, jsii.String("CodeBucket"), &BucketProps{
Versioned: jsii.Boolean(true),
})
deployment := s3deploy.NewBucketDeployment(this, jsii.String("DeployCode"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("/path/to/handler.zip")),
},
DestinationBucket: bucket,
Extract: jsii.Boolean(false),
})
// `objectVersionIds` positionally matches `objectKeys`
objectKey := cdk.Fn_Select(jsii.Number(0), deployment.objectKeys)
versionId := cdk.Fn_Select(jsii.Number(0), deployment.objectVersionIds)
objectVersionIds returns a list of tokenized version IDs that positionally matches objectKeys. It is only supported with extract: false (reading it with extract: true throws), and requires versioning to be enabled on the destination bucket — otherwise the returned version IDs are empty strings. Because the list is defined to align with objectKeys, it also requires outputObjectKeys to remain enabled (its default); reading it with outputObjectKeys: false throws. A consumer holding a version reference pins that specific object version, so it should not rely on prune semantics for lifecycle of the referenced version.
Retain on Delete
By default, the contents of the destination bucket will not be deleted when the BucketDeployment resource is removed from the stack or when the destination is changed. You can use the option retainOnDelete: false to disable this behavior, in which case the contents will be deleted.
Configuring this has a few implications you should be aware of:
Logical ID Changes
Changing the logical ID of the
BucketDeploymentconstruct, without changing the destination (for example due to refactoring, or intentional ID change) will result in the deletion of the objects. This is because CloudFormation will first create the new resource, which will have no affect, followed by a deletion of the old resource, which will cause a deletion of the objects, since the destination hasn't changed, andretainOnDeleteisfalse.Destination Changes
When the destination bucket or prefix is changed, all files in the previous destination will first be deleted and then uploaded to the new destination location. This could have availability implications on your users.
General Recommendations
Shared Bucket
If the destination bucket is not dedicated to the specific BucketDeployment construct (i.e shared by other entities), we recommend to always configure the destinationKeyPrefix property. This will prevent the deployment from accidentally deleting data that wasn't uploaded by it.
Dedicated Bucket
If the destination bucket is dedicated, it might be reasonable to skip the prefix configuration, in which case, we recommend to remove retainOnDelete: false, and instead, configure the autoDeleteObjects property on the destination bucket. This will avoid the logical ID problem mentioned above.
Prune
By default, files in the destination bucket that don't exist in the source will be deleted when the BucketDeployment resource is created or updated. You can use the option prune: false to disable this behavior, in which case the files will not be deleted.
declare const destinationBucket: s3.Bucket;
new s3deploy.BucketDeployment(this, 'DeployMeWithoutDeletingFilesOnDestination', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket,
prune: false,
});
# destination_bucket: s3.Bucket
s3deploy.BucketDeployment(self, "DeployMeWithoutDeletingFilesOnDestination",
sources=[s3deploy.Source.asset(path.join(__dirname, "my-website"))],
destination_bucket=destination_bucket,
prune=False
)
Bucket destinationBucket;
BucketDeployment.Builder.create(this, "DeployMeWithoutDeletingFilesOnDestination")
.sources(List.of(Source.asset(join(__dirname, "my-website"))))
.destinationBucket(destinationBucket)
.prune(false)
.build();
Bucket destinationBucket;
new BucketDeployment(this, "DeployMeWithoutDeletingFilesOnDestination", new BucketDeploymentProps {
Sources = new [] { Source.Asset(Join(__dirname, "my-website")) },
DestinationBucket = destinationBucket,
Prune = false
});
var destinationBucket Bucket
s3deploy.NewBucketDeployment(this, jsii.String("DeployMeWithoutDeletingFilesOnDestination"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(path.join(__dirname, jsii.String("my-website"))),
},
DestinationBucket: DestinationBucket,
Prune: jsii.Boolean(false),
})
This option also enables you to multiple bucket deployments for the same destination bucket & prefix, each with its own characteristics. For example, you can set different cache-control headers based on file extensions:
declare const destinationBucket: s3.Bucket;
new s3deploy.BucketDeployment(this, 'BucketDeployment', {
sources: [s3deploy.Source.asset('./website', { exclude: ['index.html'] })],
destinationBucket,
cacheControl: [
s3deploy.CacheControl.maxAge(Duration.days(365)),
s3deploy.CacheControl.immutable(),
],
prune: false,
});
new s3deploy.BucketDeployment(this, 'HTMLBucketDeployment', {
sources: [s3deploy.Source.asset('./website', { exclude: ['*', '!index.html'] })],
destinationBucket,
cacheControl: [
s3deploy.CacheControl.maxAge(Duration.seconds(0)),
],
prune: false,
});
# destination_bucket: s3.Bucket
s3deploy.BucketDeployment(self, "BucketDeployment",
sources=[s3deploy.Source.asset("./website", exclude=["index.html"])],
destination_bucket=destination_bucket,
cache_control=[
s3deploy.CacheControl.max_age(Duration.days(365)),
s3deploy.CacheControl.immutable()
],
prune=False
)
s3deploy.BucketDeployment(self, "HTMLBucketDeployment",
sources=[s3deploy.Source.asset("./website", exclude=["*", "!index.html"])],
destination_bucket=destination_bucket,
cache_control=[
s3deploy.CacheControl.max_age(Duration.seconds(0))
],
prune=False
)
Bucket destinationBucket;
BucketDeployment.Builder.create(this, "BucketDeployment")
.sources(List.of(Source.asset("./website", AssetOptions.builder().exclude(List.of("index.html")).build())))
.destinationBucket(destinationBucket)
.cacheControl(List.of(CacheControl.maxAge(Duration.days(365)), CacheControl.immutable()))
.prune(false)
.build();
BucketDeployment.Builder.create(this, "HTMLBucketDeployment")
.sources(List.of(Source.asset("./website", AssetOptions.builder().exclude(List.of("*", "!index.html")).build())))
.destinationBucket(destinationBucket)
.cacheControl(List.of(CacheControl.maxAge(Duration.seconds(0))))
.prune(false)
.build();
Bucket destinationBucket;
new BucketDeployment(this, "BucketDeployment", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website", new AssetOptions { Exclude = new [] { "index.html" } }) },
DestinationBucket = destinationBucket,
CacheControl = new [] { CacheControl.MaxAge(Duration.Days(365)), CacheControl.Immutable() },
Prune = false
});
new BucketDeployment(this, "HTMLBucketDeployment", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website", new AssetOptions { Exclude = new [] { "*", "!index.html" } }) },
DestinationBucket = destinationBucket,
CacheControl = new [] { CacheControl.MaxAge(Duration.Seconds(0)) },
Prune = false
});
var destinationBucket Bucket
s3deploy.NewBucketDeployment(this, jsii.String("BucketDeployment"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website"), &AssetOptions{
Exclude: []*string{
jsii.String("index.html"),
},
}),
},
DestinationBucket: DestinationBucket,
CacheControl: []CacheControl{
s3deploy.CacheControl_MaxAge(awscdk.Duration_Days(jsii.Number(365))),
s3deploy.CacheControl_Immutable(),
},
Prune: jsii.Boolean(false),
})
s3deploy.NewBucketDeployment(this, jsii.String("HTMLBucketDeployment"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_*Asset(jsii.String("./website"), &AssetOptions{
Exclude: []*string{
jsii.String("*"),
jsii.String("!index.html"),
},
}),
},
DestinationBucket: DestinationBucket,
CacheControl: []CacheControl{
s3deploy.CacheControl_*MaxAge(awscdk.Duration_Seconds(jsii.Number(0))),
},
Prune: jsii.Boolean(false),
})
Exclude and Include Filters
There are two points at which filters are evaluated in a deployment: asset bundling and the actual deployment. If you simply want to exclude files in the asset bundling process, you should leverage the exclude property of AssetOptions when defining your source:
declare const destinationBucket: s3.Bucket;
new s3deploy.BucketDeployment(this, 'HTMLBucketDeployment', {
sources: [s3deploy.Source.asset('./website', { exclude: ['*', '!index.html'] })],
destinationBucket,
});
# destination_bucket: s3.Bucket
s3deploy.BucketDeployment(self, "HTMLBucketDeployment",
sources=[s3deploy.Source.asset("./website", exclude=["*", "!index.html"])],
destination_bucket=destination_bucket
)
Bucket destinationBucket;
BucketDeployment.Builder.create(this, "HTMLBucketDeployment")
.sources(List.of(Source.asset("./website", AssetOptions.builder().exclude(List.of("*", "!index.html")).build())))
.destinationBucket(destinationBucket)
.build();
Bucket destinationBucket;
new BucketDeployment(this, "HTMLBucketDeployment", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website", new AssetOptions { Exclude = new [] { "*", "!index.html" } }) },
DestinationBucket = destinationBucket
});
var destinationBucket Bucket
s3deploy.NewBucketDeployment(this, jsii.String("HTMLBucketDeployment"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website"), &AssetOptions{
Exclude: []*string{
jsii.String("*"),
jsii.String("!index.html"),
},
}),
},
DestinationBucket: DestinationBucket,
})
If you want to specify filters to be used in the deployment process, you can use the exclude and include filters on BucketDeployment. If excluded, these files will not be deployed to the destination bucket. In addition, if the file already exists in the destination bucket, it will not be deleted if you are using the prune option:
declare const destinationBucket: s3.Bucket;
new s3deploy.BucketDeployment(this, 'DeployButExcludeSpecificFiles', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket,
exclude: ['*.txt'],
});
# destination_bucket: s3.Bucket
s3deploy.BucketDeployment(self, "DeployButExcludeSpecificFiles",
sources=[s3deploy.Source.asset(path.join(__dirname, "my-website"))],
destination_bucket=destination_bucket,
exclude=["*.txt"]
)
Bucket destinationBucket;
BucketDeployment.Builder.create(this, "DeployButExcludeSpecificFiles")
.sources(List.of(Source.asset(join(__dirname, "my-website"))))
.destinationBucket(destinationBucket)
.exclude(List.of("*.txt"))
.build();
Bucket destinationBucket;
new BucketDeployment(this, "DeployButExcludeSpecificFiles", new BucketDeploymentProps {
Sources = new [] { Source.Asset(Join(__dirname, "my-website")) },
DestinationBucket = destinationBucket,
Exclude = new [] { "*.txt" }
});
var destinationBucket Bucket
s3deploy.NewBucketDeployment(this, jsii.String("DeployButExcludeSpecificFiles"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(path.join(__dirname, jsii.String("my-website"))),
},
DestinationBucket: DestinationBucket,
Exclude: []*string{
jsii.String("*.txt"),
},
})
These filters follow the same format that is used for the AWS CLI. See the CLI documentation for information on Using Include and Exclude Filters.
Objects metadata
You can specify metadata to be set on all the objects in your deployment. There are 2 types of metadata in S3: system-defined metadata and user-defined metadata. System-defined metadata have a special purpose, for example cache-control defines how long to keep an object cached. User-defined metadata are not used by S3 and keys always begin with x-amz-meta- (this prefix is added automatically).
System defined metadata keys include the following:
- cache-control (
--cache-controlinaws s3 sync) - content-disposition (
--content-dispositioninaws s3 sync) - content-encoding (
--content-encodinginaws s3 sync) - content-language (
--content-languageinaws s3 sync) - content-type (
--content-typeinaws s3 sync) - expires (
--expiresinaws s3 sync) - x-amz-storage-class (
--storage-classinaws s3 sync) - x-amz-website-redirect-location (
--website-redirectinaws s3 sync) - x-amz-server-side-encryption (
--sseinaws s3 sync) - x-amz-server-side-encryption-aws-kms-key-id (
--sse-kms-key-idinaws s3 sync) - x-amz-server-side-encryption-customer-algorithm (
--sse-c-copy-sourceinaws s3 sync) - x-amz-acl (
--aclinaws s3 sync)
You can find more information about system defined metadata keys in S3 PutObject documentation and aws s3 sync documentation.
const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', {
websiteIndexDocument: 'index.html',
publicReadAccess: true,
});
new s3deploy.BucketDeployment(this, 'DeployWebsite', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: websiteBucket,
destinationKeyPrefix: 'web/static', // optional prefix in destination bucket
metadata: { A: "1", b: "2" }, // user-defined metadata
// system-defined metadata
contentType: "text/html",
contentLanguage: "en",
storageClass: s3deploy.StorageClass.INTELLIGENT_TIERING,
serverSideEncryption: s3deploy.ServerSideEncryption.AES_256,
cacheControl: [
s3deploy.CacheControl.setPublic(),
s3deploy.CacheControl.maxAge(Duration.hours(1)),
],
accessControl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL,
});
website_bucket = s3.Bucket(self, "WebsiteBucket",
website_index_document="index.html",
public_read_access=True
)
s3deploy.BucketDeployment(self, "DeployWebsite",
sources=[s3deploy.Source.asset("./website-dist")],
destination_bucket=website_bucket,
destination_key_prefix="web/static", # optional prefix in destination bucket
metadata={"A": "1", "b": "2"}, # user-defined metadata
# system-defined metadata
content_type="text/html",
content_language="en",
storage_class=s3deploy.StorageClass.INTELLIGENT_TIERING,
server_side_encryption=s3deploy.ServerSideEncryption.AES_256,
cache_control=[
s3deploy.CacheControl.set_public(),
s3deploy.CacheControl.max_age(Duration.hours(1))
],
access_control=s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL
)
Bucket websiteBucket = Bucket.Builder.create(this, "WebsiteBucket")
.websiteIndexDocument("index.html")
.publicReadAccess(true)
.build();
BucketDeployment.Builder.create(this, "DeployWebsite")
.sources(List.of(Source.asset("./website-dist")))
.destinationBucket(websiteBucket)
.destinationKeyPrefix("web/static") // optional prefix in destination bucket
.metadata(Map.of("A", "1", "b", "2")) // user-defined metadata
// system-defined metadata
.contentType("text/html")
.contentLanguage("en")
.storageClass(StorageClass.INTELLIGENT_TIERING)
.serverSideEncryption(ServerSideEncryption.AES_256)
.cacheControl(List.of(CacheControl.setPublic(), CacheControl.maxAge(Duration.hours(1))))
.accessControl(BucketAccessControl.BUCKET_OWNER_FULL_CONTROL)
.build();
var websiteBucket = new Bucket(this, "WebsiteBucket", new BucketProps {
WebsiteIndexDocument = "index.html",
PublicReadAccess = true
});
new BucketDeployment(this, "DeployWebsite", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website-dist") },
DestinationBucket = websiteBucket,
DestinationKeyPrefix = "web/static", // optional prefix in destination bucket
Metadata = new Dictionary<string, string> { { "A", "1" }, { "b", "2" } }, // user-defined metadata
// system-defined metadata
ContentType = "text/html",
ContentLanguage = "en",
StorageClass = StorageClass.INTELLIGENT_TIERING,
ServerSideEncryption = ServerSideEncryption.AES_256,
CacheControl = new [] { CacheControl.SetPublic(), CacheControl.MaxAge(Duration.Hours(1)) },
AccessControl = BucketAccessControl.BUCKET_OWNER_FULL_CONTROL
});
websiteBucket := s3.NewBucket(this, jsii.String("WebsiteBucket"), &BucketProps{
WebsiteIndexDocument: jsii.String("index.html"),
PublicReadAccess: jsii.Boolean(true),
})
s3deploy.NewBucketDeployment(this, jsii.String("DeployWebsite"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website-dist")),
},
DestinationBucket: websiteBucket,
DestinationKeyPrefix: jsii.String("web/static"),
// optional prefix in destination bucket
Metadata: map[string]*string{
"A": jsii.String("1"),
"b": jsii.String("2"),
},
// user-defined metadata
// system-defined metadata
ContentType: jsii.String("text/html"),
ContentLanguage: jsii.String("en"),
StorageClass: s3deploy.StorageClass_INTELLIGENT_TIERING,
ServerSideEncryption: s3deploy.ServerSideEncryption_AES_256,
CacheControl: []CacheControl{
s3deploy.CacheControl_SetPublic(),
s3deploy.CacheControl_MaxAge(awscdk.Duration_Hours(jsii.Number(1))),
},
AccessControl: s3.BucketAccessControl_BUCKET_OWNER_FULL_CONTROL,
})
CloudFront Invalidation
You can provide a CloudFront distribution and optional paths to invalidate after the bucket deployment finishes.
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
const bucket = new s3.Bucket(this, 'Destination');
// Handles buckets whether or not they are configured for website hosting.
const distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: { origin: new origins.S3Origin(bucket) },
});
new s3deploy.BucketDeployment(this, 'DeployWithInvalidation', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: bucket,
distribution,
distributionPaths: ['/images/*.png'],
});
import aws_cdk.aws_cloudfront as cloudfront
import aws_cdk.aws_cloudfront_origins as origins
bucket = s3.Bucket(self, "Destination")
# Handles buckets whether or not they are configured for website hosting.
distribution = cloudfront.Distribution(self, "Distribution",
default_behavior=cloudfront.BehaviorOptions(origin=origins.S3Origin(bucket))
)
s3deploy.BucketDeployment(self, "DeployWithInvalidation",
sources=[s3deploy.Source.asset("./website-dist")],
destination_bucket=bucket,
distribution=distribution,
distribution_paths=["/images/*.png"]
)
import software.amazon.awscdk.services.cloudfront.*;
import software.amazon.awscdk.services.cloudfront.origins.*;
Bucket bucket = new Bucket(this, "Destination");
// Handles buckets whether or not they are configured for website hosting.
Distribution distribution = Distribution.Builder.create(this, "Distribution")
.defaultBehavior(BehaviorOptions.builder().origin(new S3Origin(bucket)).build())
.build();
BucketDeployment.Builder.create(this, "DeployWithInvalidation")
.sources(List.of(Source.asset("./website-dist")))
.destinationBucket(bucket)
.distribution(distribution)
.distributionPaths(List.of("/images/*.png"))
.build();
using Amazon.CDK.AWS.CloudFront;
using Amazon.CDK.AWS.CloudFront.Origins;
var bucket = new Bucket(this, "Destination");
// Handles buckets whether or not they are configured for website hosting.
var distribution = new Distribution(this, "Distribution", new DistributionProps {
DefaultBehavior = new BehaviorOptions { Origin = new S3Origin(bucket) }
});
new BucketDeployment(this, "DeployWithInvalidation", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website-dist") },
DestinationBucket = bucket,
Distribution = distribution,
DistributionPaths = new [] { "/images/*.png" }
});
import cloudfront "github.com/aws/aws-cdk-go/awscdk"
import origins "github.com/aws/aws-cdk-go/awscdk"
bucket := s3.NewBucket(this, jsii.String("Destination"))
// Handles buckets whether or not they are configured for website hosting.
distribution := cloudfront.NewDistribution(this, jsii.String("Distribution"), &DistributionProps{
DefaultBehavior: &BehaviorOptions{
Origin: origins.NewS3Origin(bucket),
},
})
s3deploy.NewBucketDeployment(this, jsii.String("DeployWithInvalidation"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website-dist")),
},
DestinationBucket: bucket,
Distribution: Distribution,
DistributionPaths: []*string{
jsii.String("/images/*.png"),
},
})
By default, the deployment will wait for invalidation to succeed to complete. This will poll Cloudfront for a maximum of 13 minutes to check for a successful invalidation. The drawback to this is that the deployment will fail if invalidation fails or if it takes longer than 13 minutes. As a workaround, there is the option waitForDistributionInvalidation, which can be set to false to skip waiting for the invalidation, but this can be risky as invalidation errors will not be reported.
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
declare const bucket: s3.IBucket;
declare const distribution: cloudfront.IDistribution;
new s3deploy.BucketDeployment(this, 'DeployWithInvalidation', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: bucket,
distribution,
distributionPaths: ['/images/*.png'],
// Invalidate cache but don't wait or verify that invalidation has completed successfully.
waitForDistributionInvalidation: false
});
import aws_cdk.aws_cloudfront as cloudfront
# bucket: s3.IBucket
# distribution: cloudfront.IDistribution
s3deploy.BucketDeployment(self, "DeployWithInvalidation",
sources=[s3deploy.Source.asset("./website-dist")],
destination_bucket=bucket,
distribution=distribution,
distribution_paths=["/images/*.png"],
# Invalidate cache but don't wait or verify that invalidation has completed successfully.
wait_for_distribution_invalidation=False
)
import software.amazon.awscdk.services.cloudfront.*;
IBucket bucket;
IDistribution distribution;
BucketDeployment.Builder.create(this, "DeployWithInvalidation")
.sources(List.of(Source.asset("./website-dist")))
.destinationBucket(bucket)
.distribution(distribution)
.distributionPaths(List.of("/images/*.png"))
// Invalidate cache but don't wait or verify that invalidation has completed successfully.
.waitForDistributionInvalidation(false)
.build();
using Amazon.CDK.AWS.CloudFront;
IBucket bucket;
IDistribution distribution;
new BucketDeployment(this, "DeployWithInvalidation", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website-dist") },
DestinationBucket = bucket,
Distribution = distribution,
DistributionPaths = new [] { "/images/*.png" },
// Invalidate cache but don't wait or verify that invalidation has completed successfully.
WaitForDistributionInvalidation = false
});
import cloudfront "github.com/aws/aws-cdk-go/awscdk"
var bucket IBucket
var distribution IDistribution
s3deploy.NewBucketDeployment(this, jsii.String("DeployWithInvalidation"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website-dist")),
},
DestinationBucket: bucket,
Distribution: Distribution,
DistributionPaths: []*string{
jsii.String("/images/*.png"),
},
// Invalidate cache but don't wait or verify that invalidation has completed successfully.
WaitForDistributionInvalidation: jsii.Boolean(false),
})
Signed Content Payloads
By default, deployment uses streaming uploads which set the x-amz-content-sha256 request header to UNSIGNED-PAYLOAD (matching default behavior of the AWS CLI tool). In cases where bucket policy restrictions require signed content payloads, you can enable generation of a signed x-amz-content-sha256 request header with signContent: true.
declare const bucket: s3.IBucket;
new s3deploy.BucketDeployment(this, 'DeployWithSignedPayloads', {
sources: [s3deploy.Source.asset('./website-dist')],
destinationBucket: bucket,
signContent: true,
});
# bucket: s3.IBucket
s3deploy.BucketDeployment(self, "DeployWithSignedPayloads",
sources=[s3deploy.Source.asset("./website-dist")],
destination_bucket=bucket,
sign_content=True
)
IBucket bucket;
BucketDeployment.Builder.create(this, "DeployWithSignedPayloads")
.sources(List.of(Source.asset("./website-dist")))
.destinationBucket(bucket)
.signContent(true)
.build();
IBucket bucket;
new BucketDeployment(this, "DeployWithSignedPayloads", new BucketDeploymentProps {
Sources = new [] { Source.Asset("./website-dist") },
DestinationBucket = bucket,
SignContent = true
});
var bucket IBucket
s3deploy.NewBucketDeployment(this, jsii.String("DeployWithSignedPayloads"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website-dist")),
},
DestinationBucket: bucket,
SignContent: jsii.Boolean(true),
})
Size Limits
The default memory limit for the deployment resource is 1024MiB. If you need to copy larger files, you can use the memoryLimit configuration to increase the size of the AWS Lambda resource handler.
The default ephemeral storage size for the deployment resource is 512MiB. If you need to upload larger files, you may hit this limit. You can use the ephemeralStorageSize configuration to increase the storage size of the AWS Lambda resource handler.
NOTE: a new AWS Lambda handler will be created in your stack for each combination of memory and storage size.
Lambda Architecture
The deployment resource handler runs on the arm64 (AWS Graviton) architecture. The handler is CDK-managed internal code rather than your code, so the architecture is fixed and not configurable. Both the handler and the bundled AWS CLI v1 layer are architecture-independent, so the deployment behaves identically on either architecture while arm64 costs less per millisecond of execution.
JSON-Aware Source Processing
When using Source.jsonData with CDK Tokens (references to construct properties), you may need to enable the escaping option. This is particularly important when the referenced properties might contain special characters that require proper JSON escaping (like double quotes, line breaks, etc.).
declare const bucket: s3.Bucket;
declare const param: ssm.StringParameter;
// Example with a secret value that contains double quotes
const deployment = new s3deploy.BucketDeployment(this, 'JsonDeployment', {
sources: [
s3deploy.Source.jsonData('config.json', {
api_endpoint: 'https://api.example.com',
secretValue: param.stringValue, // value with double quotes
config: {
enabled: true,
features: ['feature1', 'feature2']
}
},
// Enable escaping at deployment time
{ escape: true },
)
],
destinationBucket: bucket,
});
# bucket: s3.Bucket
# param: ssm.StringParameter
# Example with a secret value that contains double quotes
deployment = s3deploy.BucketDeployment(self, "JsonDeployment",
sources=[
s3deploy.Source.json_data("config.json", {
"api_endpoint": "https://api.example.com",
"secret_value": param.string_value, # value with double quotes
"config": {
"enabled": True,
"features": ["feature1", "feature2"]
}
}, escape=True)
],
destination_bucket=bucket
)
Bucket bucket;
StringParameter param;
// Example with a secret value that contains double quotes
BucketDeployment deployment = BucketDeployment.Builder.create(this, "JsonDeployment")
.sources(List.of(Source.jsonData("config.json", Map.of(
"api_endpoint", "https://api.example.com",
"secretValue", param.getStringValue(), // value with double quotes
"config", Map.of(
"enabled", true,
"features", List.of("feature1", "feature2"))), JsonProcessingOptions.builder().escape(true).build())))
.destinationBucket(bucket)
.build();
Bucket bucket;
StringParameter param;
// Example with a secret value that contains double quotes
var deployment = new BucketDeployment(this, "JsonDeployment", new BucketDeploymentProps {
Sources = new [] { Source.JsonData("config.json", new Dictionary<string, object> {
{ "api_endpoint", "https://api.example.com" },
{ "secretValue", param.StringValue }, // value with double quotes
{ "config", new Struct {
Enabled = true,
Features = new [] { "feature1", "feature2" }
} }
}, new JsonProcessingOptions { Escape = true }) },
DestinationBucket = bucket
});
var bucket Bucket
var param StringParameter
// Example with a secret value that contains double quotes
deployment := s3deploy.NewBucketDeployment(this, jsii.String("JsonDeployment"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_JsonData(jsii.String("config.json"), map[string]interface{}{
"api_endpoint": jsii.String("https://api.example.com"),
"secretValue": param.stringValue,
// value with double quotes
"config": map[string]interface{}{
"enabled": jsii.Boolean(true),
"features": []*string{
jsii.String("feature1"),
jsii.String("feature2"),
},
},
}, &JsonProcessingOptions{
Escape: jsii.Boolean(true),
}),
},
DestinationBucket: bucket,
})
EFS Support
If your workflow needs more disk space than default (512 MB) disk space, you may attach an EFS storage to underlying lambda function. To Enable EFS support set efs and vpc props for BucketDeployment.
Check sample usage below. Please note that creating VPC inline may cause stack deletion failures. It is shown as below for simplicity. To avoid such condition, keep your network infra (VPC) in a separate stack and pass as props.
declare const destinationBucket: s3.Bucket;
declare const vpc: ec2.Vpc;
new s3deploy.BucketDeployment(this, 'DeployMeWithEfsStorage', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket,
destinationKeyPrefix: 'efs/',
useEfs: true,
vpc,
retainOnDelete: false,
});
# destination_bucket: s3.Bucket
# vpc: ec2.Vpc
s3deploy.BucketDeployment(self, "DeployMeWithEfsStorage",
sources=[s3deploy.Source.asset(path.join(__dirname, "my-website"))],
destination_bucket=destination_bucket,
destination_key_prefix="efs/",
use_efs=True,
vpc=vpc,
retain_on_delete=False
)
Bucket destinationBucket;
Vpc vpc;
BucketDeployment.Builder.create(this, "DeployMeWithEfsStorage")
.sources(List.of(Source.asset(join(__dirname, "my-website"))))
.destinationBucket(destinationBucket)
.destinationKeyPrefix("efs/")
.useEfs(true)
.vpc(vpc)
.retainOnDelete(false)
.build();
Bucket destinationBucket;
Vpc vpc;
new BucketDeployment(this, "DeployMeWithEfsStorage", new BucketDeploymentProps {
Sources = new [] { Source.Asset(Join(__dirname, "my-website")) },
DestinationBucket = destinationBucket,
DestinationKeyPrefix = "efs/",
UseEfs = true,
Vpc = vpc,
RetainOnDelete = false
});
var destinationBucket Bucket
var vpc Vpc
s3deploy.NewBucketDeployment(this, jsii.String("DeployMeWithEfsStorage"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(path.join(__dirname, jsii.String("my-website"))),
},
DestinationBucket: DestinationBucket,
DestinationKeyPrefix: jsii.String("efs/"),
UseEfs: jsii.Boolean(true),
Vpc: Vpc,
RetainOnDelete: jsii.Boolean(false),
})
Data with deploy-time values
The content passed to Source.data(), Source.jsonData(), or Source.yamlData() can include references that will get resolved only during deployment. Only a subset of CloudFormation functions are supported however, namely: Ref, Fn::GetAtt, Fn::Join, and Fn::Select (Fn::Split may be nested under Fn::Select).
For example:
import * as sns from 'aws-cdk-lib/aws-sns';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
declare const destinationBucket: s3.Bucket;
declare const topic: sns.Topic;
declare const tg: elbv2.ApplicationTargetGroup;
const appConfig = {
topic_arn: topic.topicArn,
base_url: 'https://my-endpoint',
lb_name: tg.firstLoadBalancerFullName,
};
new s3deploy.BucketDeployment(this, 'BucketDeployment', {
sources: [s3deploy.Source.jsonData('config.json', appConfig)],
destinationBucket,
});
import aws_cdk.aws_sns as sns
import aws_cdk.aws_elasticloadbalancingv2 as elbv2
# destination_bucket: s3.Bucket
# topic: sns.Topic
# tg: elbv2.ApplicationTargetGroup
app_config = {
"topic_arn": topic.topic_arn,
"base_url": "https://my-endpoint",
"lb_name": tg.first_load_balancer_full_name
}
s3deploy.BucketDeployment(self, "BucketDeployment",
sources=[s3deploy.Source.json_data("config.json", app_config)],
destination_bucket=destination_bucket
)
import software.amazon.awscdk.services.sns.*;
import software.amazon.awscdk.services.elasticloadbalancingv2.*;
Bucket destinationBucket;
Topic topic;
ApplicationTargetGroup tg;
Map<String, Object> appConfig = Map.of(
"topic_arn", topic.getTopicArn(),
"base_url", "https://my-endpoint",
"lb_name", tg.getFirstLoadBalancerFullName());
BucketDeployment.Builder.create(this, "BucketDeployment")
.sources(List.of(Source.jsonData("config.json", appConfig)))
.destinationBucket(destinationBucket)
.build();
using Amazon.CDK.AWS.SNS;
using Amazon.CDK.AWS.ElasticLoadBalancingV2;
Bucket destinationBucket;
Topic topic;
ApplicationTargetGroup tg;
IDictionary<string, object> appConfig = new Dictionary<string, object> {
{ "topic_arn", topic.TopicArn },
{ "base_url", "https://my-endpoint" },
{ "lb_name", tg.FirstLoadBalancerFullName }
};
new BucketDeployment(this, "BucketDeployment", new BucketDeploymentProps {
Sources = new [] { Source.JsonData("config.json", appConfig) },
DestinationBucket = destinationBucket
});
import sns "github.com/aws/aws-cdk-go/awscdk"
import elbv2 "github.com/aws/aws-cdk-go/awscdk"
var destinationBucket Bucket
var topic Topic
var tg ApplicationTargetGroup
appConfig := map[string]interface{}{
"topic_arn": topic.topicArn,
"base_url": jsii.String("https://my-endpoint"),
"lb_name": tg.firstLoadBalancerFullName,
}
s3deploy.NewBucketDeployment(this, jsii.String("BucketDeployment"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_JsonData(jsii.String("config.json"), appConfig),
},
DestinationBucket: DestinationBucket,
})
The value in topic.topicArn is a deploy-time value. It only gets resolved during deployment by placing a marker in the generated source file and substituting it when its deployed to the destination with the actual value.
Substitutions from Templated Files
The DeployTimeSubstitutedFile construct allows you to specify substitutions to make from placeholders in a local file which will be resolved during deployment. This is especially useful in situations like creating an API from a spec file, where users might want to reference other CDK resources they have created.
The syntax for template variables is {{ variableName }} in your local file. Then, you would specify the substitutions in CDK like this:
import * as lambda from 'aws-cdk-lib/aws-lambda';
declare const myLambdaFunction: lambda.Function;
declare const destinationBucket: s3.Bucket;
//(Optional) if provided, the resulting processed file would be uploaded to the destinationBucket under the destinationKey name.
declare const destinationKey: string;
declare const role: iam.Role;
new s3deploy.DeployTimeSubstitutedFile(this, 'MyFile', {
source: 'my-file.yaml',
destinationKey: destinationKey,
destinationBucket: destinationBucket,
substitutions: {
variableName: myLambdaFunction.functionName,
},
role: role,
});
import aws_cdk.aws_lambda as lambda_
# my_lambda_function: lambda.Function
# destination_bucket: s3.Bucket
# (Optional) if provided, the resulting processed file would be uploaded to the destinationBucket under the destinationKey name.
# destination_key: str
# role: iam.Role
s3deploy.DeployTimeSubstitutedFile(self, "MyFile",
source="my-file.yaml",
destination_key=destination_key,
destination_bucket=destination_bucket,
substitutions={
"variable_name": my_lambda_function.function_name
},
role=role
)
import software.amazon.awscdk.services.lambda.*;
Function myLambdaFunction;
Bucket destinationBucket;
//(Optional) if provided, the resulting processed file would be uploaded to the destinationBucket under the destinationKey name.
String destinationKey;
Role role;
DeployTimeSubstitutedFile.Builder.create(this, "MyFile")
.source("my-file.yaml")
.destinationKey(destinationKey)
.destinationBucket(destinationBucket)
.substitutions(Map.of(
"variableName", myLambdaFunction.getFunctionName()))
.role(role)
.build();
using Amazon.CDK.AWS.Lambda;
Function myLambdaFunction;
Bucket destinationBucket;
//(Optional) if provided, the resulting processed file would be uploaded to the destinationBucket under the destinationKey name.
string destinationKey;
Role role;
new DeployTimeSubstitutedFile(this, "MyFile", new DeployTimeSubstitutedFileProps {
Source = "my-file.yaml",
DestinationKey = destinationKey,
DestinationBucket = destinationBucket,
Substitutions = new Dictionary<string, string> {
{ "variableName", myLambdaFunction.FunctionName }
},
Role = role
});
import lambda "github.com/aws/aws-cdk-go/awscdk"
var myLambdaFunction Function
var destinationBucket Bucket
//(Optional) if provided, the resulting processed file would be uploaded to the destinationBucket under the destinationKey name.
var destinationKey string
var role Role
s3deploy.NewDeployTimeSubstitutedFile(this, jsii.String("MyFile"), &DeployTimeSubstitutedFileProps{
Source: jsii.String("my-file.yaml"),
DestinationKey: destinationKey,
DestinationBucket: destinationBucket,
Substitutions: map[string]*string{
"variableName": myLambdaFunction.functionName,
},
Role: role,
})
Nested variables, like {{ {{ foo }} }} or {{ foo {{ bar }} }}, are not supported by this construct. In the first case of a single variable being is double nested {{ {{ foo }} }}, only the {{ foo }} would be replaced by the substitution, and the extra brackets would remain in the file. In the second case of two nexted variables {{ foo {{ bar }} }}, only the {{ bar }} would be replaced in the file.
Keep Files Zipped
By default, files are zipped, then extracted into the destination bucket.
You can use the option extract: false to disable this behavior, in which case, files will remain in a zip file when deployed to S3. To reference the object keys, or filenames, which will be deployed to the bucket, you can use the objectKeys getter on the bucket deployment.
import * as cdk from 'aws-cdk-lib';
declare const destinationBucket: s3.Bucket;
const myBucketDeployment = new s3deploy.BucketDeployment(this, 'DeployMeWithoutExtractingFilesOnDestination', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket,
extract: false,
});
new cdk.CfnOutput(this, 'ObjectKey', {
value: cdk.Fn.select(0, myBucketDeployment.objectKeys),
});
import aws_cdk as cdk
# destination_bucket: s3.Bucket
my_bucket_deployment = s3deploy.BucketDeployment(self, "DeployMeWithoutExtractingFilesOnDestination",
sources=[s3deploy.Source.asset(path.join(__dirname, "my-website"))],
destination_bucket=destination_bucket,
extract=False
)
cdk.CfnOutput(self, "ObjectKey",
value=cdk.Fn.select(0, my_bucket_deployment.object_keys)
)
import software.amazon.awscdk.*;
Bucket destinationBucket;
BucketDeployment myBucketDeployment = BucketDeployment.Builder.create(this, "DeployMeWithoutExtractingFilesOnDestination")
.sources(List.of(Source.asset(join(__dirname, "my-website"))))
.destinationBucket(destinationBucket)
.extract(false)
.build();
CfnOutput.Builder.create(this, "ObjectKey")
.value(Fn.select(0, myBucketDeployment.getObjectKeys()))
.build();
using Amazon.CDK;
Bucket destinationBucket;
var myBucketDeployment = new BucketDeployment(this, "DeployMeWithoutExtractingFilesOnDestination", new BucketDeploymentProps {
Sources = new [] { Source.Asset(Join(__dirname, "my-website")) },
DestinationBucket = destinationBucket,
Extract = false
});
new CfnOutput(this, "ObjectKey", new CfnOutputProps {
Value = Fn.Select(0, myBucketDeployment.ObjectKeys)
});
import "github.com/aws/aws-cdk-go/awscdk"
var destinationBucket Bucket
myBucketDeployment := s3deploy.NewBucketDeployment(this, jsii.String("DeployMeWithoutExtractingFilesOnDestination"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(path.join(__dirname, jsii.String("my-website"))),
},
DestinationBucket: DestinationBucket,
Extract: jsii.Boolean(false),
})
cdk.NewCfnOutput(this, jsii.String("ObjectKey"), &CfnOutputProps{
Value: cdk.Fn_Select(jsii.Number(0), myBucketDeployment.objectKeys),
})
Controlling the Output of Source Object Keys
By default, the keys of the source objects copied to the destination bucket are returned in the Data property of the custom resource. However, you can disable this behavior by setting the outputObjectKeys property to false. This is particularly useful when the number of objects is too large and might exceed the size limit of the responseData property.
import * as cdk from 'aws-cdk-lib';
declare const destinationBucket: s3.Bucket;
const myBucketDeployment = new s3deploy.BucketDeployment(this, 'DeployMeWithoutExtractingFilesOnDestination', {
sources: [s3deploy.Source.asset(path.join(__dirname, 'my-website'))],
destinationBucket,
outputObjectKeys: false,
});
new cdk.CfnOutput(this, 'ObjectKey', {
value: cdk.Fn.select(0, myBucketDeployment.objectKeys),
});
import aws_cdk as cdk
# destination_bucket: s3.Bucket
my_bucket_deployment = s3deploy.BucketDeployment(self, "DeployMeWithoutExtractingFilesOnDestination",
sources=[s3deploy.Source.asset(path.join(__dirname, "my-website"))],
destination_bucket=destination_bucket,
output_object_keys=False
)
cdk.CfnOutput(self, "ObjectKey",
value=cdk.Fn.select(0, my_bucket_deployment.object_keys)
)
import software.amazon.awscdk.*;
Bucket destinationBucket;
BucketDeployment myBucketDeployment = BucketDeployment.Builder.create(this, "DeployMeWithoutExtractingFilesOnDestination")
.sources(List.of(Source.asset(join(__dirname, "my-website"))))
.destinationBucket(destinationBucket)
.outputObjectKeys(false)
.build();
CfnOutput.Builder.create(this, "ObjectKey")
.value(Fn.select(0, myBucketDeployment.getObjectKeys()))
.build();
using Amazon.CDK;
Bucket destinationBucket;
var myBucketDeployment = new BucketDeployment(this, "DeployMeWithoutExtractingFilesOnDestination", new BucketDeploymentProps {
Sources = new [] { Source.Asset(Join(__dirname, "my-website")) },
DestinationBucket = destinationBucket,
OutputObjectKeys = false
});
new CfnOutput(this, "ObjectKey", new CfnOutputProps {
Value = Fn.Select(0, myBucketDeployment.ObjectKeys)
});
import "github.com/aws/aws-cdk-go/awscdk"
var destinationBucket Bucket
myBucketDeployment := s3deploy.NewBucketDeployment(this, jsii.String("DeployMeWithoutExtractingFilesOnDestination"), &BucketDeploymentProps{
Sources: []ISource{
s3deploy.Source_Asset(path.join(__dirname, jsii.String("my-website"))),
},
DestinationBucket: DestinationBucket,
OutputObjectKeys: jsii.Boolean(false),
})
cdk.NewCfnOutput(this, jsii.String("ObjectKey"), &CfnOutputProps{
Value: cdk.Fn_Select(jsii.Number(0), myBucketDeployment.objectKeys),
})
Specifying a Custom VPC, Subnets, and Security Groups in BucketDeployment
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.
Using a Custom VPC
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.
const vpc = ec2.Vpc.fromLookup(this, 'ExistingVPC', { vpcId: 'vpc-12345678' });
const bucket = new s3.Bucket(this, 'MyBucket');
new s3deploy.BucketDeployment(this, 'DeployToS3', {
destinationBucket: bucket,
vpc: vpc,
sources: [s3deploy.Source.asset('./website')],
});
vpc = ec2.Vpc.from_lookup(self, "ExistingVPC", vpc_id="vpc-12345678")
bucket = s3.Bucket(self, "MyBucket")
s3deploy.BucketDeployment(self, "DeployToS3",
destination_bucket=bucket,
vpc=vpc,
sources=[s3deploy.Source.asset("./website")]
)
IVpc vpc = Vpc.fromLookup(this, "ExistingVPC", VpcLookupOptions.builder().vpcId("vpc-12345678").build());
Bucket bucket = new Bucket(this, "MyBucket");
BucketDeployment.Builder.create(this, "DeployToS3")
.destinationBucket(bucket)
.vpc(vpc)
.sources(List.of(Source.asset("./website")))
.build();
var vpc = Vpc.FromLookup(this, "ExistingVPC", new VpcLookupOptions { VpcId = "vpc-12345678" });
var bucket = new Bucket(this, "MyBucket");
new BucketDeployment(this, "DeployToS3", new BucketDeploymentProps {
DestinationBucket = bucket,
Vpc = vpc,
Sources = new [] { Source.Asset("./website") }
});
vpc := ec2.Vpc_FromLookup(this, jsii.String("ExistingVPC"), &VpcLookupOptions{
VpcId: jsii.String("vpc-12345678"),
})
bucket := s3.NewBucket(this, jsii.String("MyBucket"))
s3deploy.NewBucketDeployment(this, jsii.String("DeployToS3"), &BucketDeploymentProps{
DestinationBucket: bucket,
Vpc: vpc,
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website")),
},
})
Specifying Subnets for Deployment
By default, when you specify a VPC, the BucketDeployment function is deployed in the private subnets of that VPC. However, you can customize the subnet selection using the vpcSubnets property.
const vpc = ec2.Vpc.fromLookup(this, 'ExistingVPC', { vpcId: 'vpc-12345678' });
const bucket = new s3.Bucket(this, 'MyBucket');
new s3deploy.BucketDeployment(this, 'DeployToS3', {
destinationBucket: bucket,
vpc: vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
sources: [s3deploy.Source.asset('./website')],
});
vpc = ec2.Vpc.from_lookup(self, "ExistingVPC", vpc_id="vpc-12345678")
bucket = s3.Bucket(self, "MyBucket")
s3deploy.BucketDeployment(self, "DeployToS3",
destination_bucket=bucket,
vpc=vpc,
vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
sources=[s3deploy.Source.asset("./website")]
)
IVpc vpc = Vpc.fromLookup(this, "ExistingVPC", VpcLookupOptions.builder().vpcId("vpc-12345678").build());
Bucket bucket = new Bucket(this, "MyBucket");
BucketDeployment.Builder.create(this, "DeployToS3")
.destinationBucket(bucket)
.vpc(vpc)
.vpcSubnets(SubnetSelection.builder().subnetType(SubnetType.PUBLIC).build())
.sources(List.of(Source.asset("./website")))
.build();
var vpc = Vpc.FromLookup(this, "ExistingVPC", new VpcLookupOptions { VpcId = "vpc-12345678" });
var bucket = new Bucket(this, "MyBucket");
new BucketDeployment(this, "DeployToS3", new BucketDeploymentProps {
DestinationBucket = bucket,
Vpc = vpc,
VpcSubnets = new SubnetSelection { SubnetType = SubnetType.PUBLIC },
Sources = new [] { Source.Asset("./website") }
});
vpc := ec2.Vpc_FromLookup(this, jsii.String("ExistingVPC"), &VpcLookupOptions{
VpcId: jsii.String("vpc-12345678"),
})
bucket := s3.NewBucket(this, jsii.String("MyBucket"))
s3deploy.NewBucketDeployment(this, jsii.String("DeployToS3"), &BucketDeploymentProps{
DestinationBucket: bucket,
Vpc: vpc,
VpcSubnets: &SubnetSelection{
SubnetType: ec2.SubnetType_PUBLIC,
},
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website")),
},
})
Defining Custom Security Groups
For enhanced network security, you can now specify custom security groups in BucketDeploymentProps. This allows fine-grained control over ingress and egress rules for the deployment Lambda function.
const vpc = ec2.Vpc.fromLookup(this, 'ExistingVPC', { vpcId: 'vpc-12345678' });
const bucket = new s3.Bucket(this, 'MyBucket');
const securityGroup = new ec2.SecurityGroup(this, 'CustomSG', {
vpc: vpc,
description: 'Allow HTTPS outbound access',
allowAllOutbound: false,
});
securityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS traffic');
new s3deploy.BucketDeployment(this, 'DeployWithSecurityGroup', {
destinationBucket: bucket,
vpc: vpc,
securityGroups: [securityGroup],
sources: [s3deploy.Source.asset('./website')],
});
vpc = ec2.Vpc.from_lookup(self, "ExistingVPC", vpc_id="vpc-12345678")
bucket = s3.Bucket(self, "MyBucket")
security_group = ec2.SecurityGroup(self, "CustomSG",
vpc=vpc,
description="Allow HTTPS outbound access",
allow_all_outbound=False
)
security_group.add_egress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(443), "Allow HTTPS traffic")
s3deploy.BucketDeployment(self, "DeployWithSecurityGroup",
destination_bucket=bucket,
vpc=vpc,
security_groups=[security_group],
sources=[s3deploy.Source.asset("./website")]
)
IVpc vpc = Vpc.fromLookup(this, "ExistingVPC", VpcLookupOptions.builder().vpcId("vpc-12345678").build());
Bucket bucket = new Bucket(this, "MyBucket");
SecurityGroup securityGroup = SecurityGroup.Builder.create(this, "CustomSG")
.vpc(vpc)
.description("Allow HTTPS outbound access")
.allowAllOutbound(false)
.build();
securityGroup.addEgressRule(Peer.anyIpv4(), Port.tcp(443), "Allow HTTPS traffic");
BucketDeployment.Builder.create(this, "DeployWithSecurityGroup")
.destinationBucket(bucket)
.vpc(vpc)
.securityGroups(List.of(securityGroup))
.sources(List.of(Source.asset("./website")))
.build();
var vpc = Vpc.FromLookup(this, "ExistingVPC", new VpcLookupOptions { VpcId = "vpc-12345678" });
var bucket = new Bucket(this, "MyBucket");
var securityGroup = new SecurityGroup(this, "CustomSG", new SecurityGroupProps {
Vpc = vpc,
Description = "Allow HTTPS outbound access",
AllowAllOutbound = false
});
securityGroup.AddEgressRule(Peer.AnyIpv4(), Port.Tcp(443), "Allow HTTPS traffic");
new BucketDeployment(this, "DeployWithSecurityGroup", new BucketDeploymentProps {
DestinationBucket = bucket,
Vpc = vpc,
SecurityGroups = new [] { securityGroup },
Sources = new [] { Source.Asset("./website") }
});
vpc := ec2.Vpc_FromLookup(this, jsii.String("ExistingVPC"), &VpcLookupOptions{
VpcId: jsii.String("vpc-12345678"),
})
bucket := s3.NewBucket(this, jsii.String("MyBucket"))
securityGroup := ec2.NewSecurityGroup(this, jsii.String("CustomSG"), &SecurityGroupProps{
Vpc: vpc,
Description: jsii.String("Allow HTTPS outbound access"),
AllowAllOutbound: jsii.Boolean(false),
})
securityGroup.AddEgressRule(ec2.Peer_AnyIpv4(), ec2.Port_Tcp(jsii.Number(443)), jsii.String("Allow HTTPS traffic"))
s3deploy.NewBucketDeployment(this, jsii.String("DeployWithSecurityGroup"), &BucketDeploymentProps{
DestinationBucket: bucket,
Vpc: vpc,
SecurityGroups: []ISecurityGroup{
securityGroup,
},
Sources: []ISource{
s3deploy.Source_Asset(jsii.String("./website")),
},
})
Notes
- This library uses an AWS CloudFormation custom resource which is about 10MiB in size. The code of this resource is bundled with this library.
- AWS Lambda execution time is limited to 15min. This limits the amount of data which can be deployed into the bucket by this timeout.
- When the
BucketDeploymentis removed from the stack, the contents are retained in the destination bucket (#952). - If you are using
s3deploy.Source.bucket()to take the file source from another bucket: the deployed files will only be updated if the key (file name) of the file in the source bucket changes. Mutating the file in place will not be good enough: the custom resource will simply not run if the properties don't change.- If you use assets (
s3deploy.Source.asset()) you don't need to worry about this: the asset system will make sure that if the files have changed, the file name is unique and the deployment will run.
- If you use assets (
Development
The custom resource is implemented in Python 3.9 in order to be able to leverage the AWS CLI for "aws s3 sync". The code is now in the @aws-cdk/custom-resource-handlers package under lib/aws-s3-deployment/bucket-deployment-handler and unit tests are under test/aws-s3-deployment/bucket-deployment-handler.
This package requires Python 3.9 during build time in order to create the custom resource Lambda bundle and test it. It also relies on a few bash scripts, so might be tricky to build on Windows.
Roadmap
- [ ] Support "blue/green" deployments (#954)
Types
| Name | Kind | Summary |
|---|---|---|
BucketDeployment | Construct | BucketDeployment populates an S3 bucket with the contents of .zip files from other S3 buckets or from local disk. |
BucketDeploymentProps | Struct | Properties for BucketDeployment. |
CacheControl | Class | Used for HTTP cache-control header, which influences downstream caches. |
DeploymentSourceContext | Struct | Bind context for ISources. |
DeployTimeSubstitutedFile | Construct | DeployTimeSubstitutedFile is an extension of BucketDeployment that allows users to upload individual files and specify to make substitutions in the file. |
DeployTimeSubstitutedFileProps | Struct | |
ISource | Interface | Represents a source for bucket deployments. |
JsonProcessingOptions | Struct | Define options which can be passed using the method Source.jsonData(). |
MarkersConfig | Struct | A configuration for markers substitution strategy. |
ServerSideEncryption | Enum | Indicates whether server-side encryption is enabled for the object, and whether that encryption is from the AWS Key Management Service (AWS KMS) or from Amazon S3 managed encryption (SSE-S3). |
Source | Class | Specifies bucket deployment source. |
SourceConfig | Struct | Source information. |
StorageClass | Enum | Storage class used for storing the object. |
UserDefinedObjectMetadata | Struct | Custom user defined metadata. |