14 types
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.
website_bucket = AWSCDK::S3::Bucket.new(self, "WebsiteBucket", {
website_index_document: "index.html",
public_read_access: true,
})
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWebsite", {
sources: [AWSCDK::S3Deployment::Source.asset("./website-dist")],
destination_bucket: website_bucket,
destination_key_prefix: "web/static",
})
This is what happens under the hood:
cdk deploy or via CI/CD), the
contents of the local website-dist directory will be archived and uploaded
to an intermediary assets bucket (the StagingBucket of the CDK bootstrap stack).
If there is more than one source, they will be individually uploaded.BucketDeployment construct synthesizes a Lambda-backed custom CloudFormation resource
of type Custom::CDKBucketDeployment into the template. The source bucket/key
is set to point to the assets bucket.aws s3 sync --delete against the destination bucket (in this case
website_bucket). 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:
website_bucket = nil # AWSCDK::S3::Bucket
deployment = AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWebsite", {
sources: [AWSCDK::S3Deployment::Source.asset(path.join(__dirname, "my-website"))],
destination_bucket: website_bucket,
})
ConstructThatReadsFromTheBucket.new(self, "Consumer", {
# Use 'deployment.deployedBucket' instead of 'websiteBucket' here
bucket: deployment.deployed_bucket,
})
It is also possible to add additional sources using the add_source method.
website_bucket = nil # AWSCDK::S3::IBucket
deployment = AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWebsite", {
sources: [AWSCDK::S3Deployment::Source.asset("./website-dist")],
destination_bucket: website_bucket,
destination_key_prefix: "web/static",
})
deployment.add_source(AWSCDK::S3Deployment::Source.asset("./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 handler_role property of BucketDeployment to manually
add the KMS permissions:
destination_bucket = nil # AWSCDK::S3::Bucket
deployment = AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployFiles", {
sources: [AWSCDK::S3Deployment::Source.asset(path.join(__dirname, "source-files"))],
destination_bucket: destination_bucket,
})
deployment.handler_role.add_to_policy(
AWSCDK::IAM::PolicyStatement.new({
actions: ["kms:Decrypt", "kms:DescribeKey"],
effect: AWSCDK::IAM::Effect::ALLOW,
resources: ["<encryption key ARN>"],
}))
The situation above could arise from the following scenarios:
cdk bootstrap command via
the --bootstrap-kms-key-id CLI option.
The default key policy
alone is not sufficient to grant the Lambda KMS permissions.StagingBucket
by 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.BucketDeployment comes from the Source.bucket static 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 Lambda kms:Decrypt and kms:DescribeKey
permissions.The following source types are supported for bucket deployments:
s3deploy.Source.asset('/path/to/local/file.zip')s3deploy.Source.asset('/path/to/local/directory')s3deploy.Source.bucket(bucket, zipObjectKey)s3deploy.Source.data('object-key.txt', 'hello, world!')
(supports deploy-time values)s3deploy.Source.jsonData('object-key.json', { json: 'object' })
(supports deploy-time values)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:
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.
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:
Changing the logical ID of the BucketDeployment construct, 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, and retain_on_delete is false.
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.
If the destination bucket is not dedicated to the specific BucketDeployment construct (i.e shared by other entities),
we recommend to always configure the destination_key_prefix property. This will prevent the deployment from
accidentally deleting data that wasn't uploaded by it.
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
auto_delete_objects
property on the destination bucket. This will avoid the logical ID problem mentioned above.
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.
destination_bucket = nil # AWSCDK::S3::Bucket
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployMeWithoutDeletingFilesOnDestination", {
sources: [AWSCDK::S3Deployment::Source.asset(path.join(__dirname, "my-website"))],
destination_bucket: destination_bucket,
prune: 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:
destination_bucket = nil # AWSCDK::S3::Bucket
AWSCDK::S3Deployment::BucketDeployment.new(self, "BucketDeployment", {
sources: [AWSCDK::S3Deployment::Source.asset("./website", {exclude: ["index.html"]})],
destination_bucket: destination_bucket,
cache_control: [
AWSCDK::S3Deployment::CacheControl.max_age(AWSCDK::Duration.days(365)),
AWSCDK::S3Deployment::CacheControl.immutable,
],
prune: false,
})
AWSCDK::S3Deployment::BucketDeployment.new(self, "HTMLBucketDeployment", {
sources: [AWSCDK::S3Deployment::Source.asset("./website", {exclude: ["*", "!index.html"]})],
destination_bucket: destination_bucket,
cache_control: [
AWSCDK::S3Deployment::CacheControl.max_age(AWSCDK::Duration.seconds(0)),
],
prune: false,
})
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:
destination_bucket = nil # AWSCDK::S3::Bucket
AWSCDK::S3Deployment::BucketDeployment.new(self, "HTMLBucketDeployment", {
sources: [AWSCDK::S3Deployment::Source.asset("./website", {exclude: ["*", "!index.html"]})],
destination_bucket: destination_bucket,
})
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:
destination_bucket = nil # AWSCDK::S3::Bucket
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployButExcludeSpecificFiles", {
sources: [AWSCDK::S3Deployment::Source.asset(path.join(__dirname, "my-website"))],
destination_bucket: destination_bucket,
exclude: ["*.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.
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 in aws s3 sync)--content-disposition in aws s3 sync)--content-encoding in aws s3 sync)--content-language in aws s3 sync)--content-type in aws s3 sync)--expires in aws s3 sync)--storage-class in aws s3 sync)--website-redirect in aws s3 sync)--sse in aws s3 sync)--sse-kms-key-id in aws s3 sync)--sse-c-copy-source in aws s3 sync)--acl in aws s3 sync)You can find more information about system defined metadata keys in
S3 PutObject documentation
and aws s3 sync documentation.
website_bucket = AWSCDK::S3::Bucket.new(self, "WebsiteBucket", {
website_index_document: "index.html",
public_read_access: true,
})
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWebsite", {
sources: [AWSCDK::S3Deployment::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: AWSCDK::S3Deployment::StorageClass::INTELLIGENT_TIERING,
server_side_encryption: AWSCDK::S3Deployment::ServerSideEncryption::AES_256,
cache_control: [
AWSCDK::S3Deployment::CacheControl.set_public,
AWSCDK::S3Deployment::CacheControl.max_age(AWSCDK::Duration.hours(1)),
],
access_control: AWSCDK::S3::BucketAccessControl::BUCKET_OWNER_FULL_CONTROL,
})
You can provide a CloudFront distribution and optional paths to invalidate after the bucket deployment finishes.
require 'aws-cdk-lib'
bucket = AWSCDK::S3::Bucket.new(self, "Destination")
# Handles buckets whether or not they are configured for website hosting.
distribution = AWSCDK::CloudFront::Distribution.new(self, "Distribution", {
default_behavior: {origin: AWSCDK::CloudFrontOrigins::S3Origin.new(bucket)},
})
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWithInvalidation", {
sources: [AWSCDK::S3Deployment::Source.asset("./website-dist")],
destination_bucket: bucket,
distribution: distribution,
distribution_paths: ["/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 wait_for_distribution_invalidation, which can be set to false to skip waiting for the invalidation, but this can be risky as invalidation errors will not be reported.
require 'aws-cdk-lib'
bucket = nil # AWSCDK::S3::IBucket
distribution = nil # AWSCDK::CloudFront::IDistribution
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWithInvalidation", {
sources: [AWSCDK::S3Deployment::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,
})
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.
bucket = nil # AWSCDK::S3::IBucket
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWithSignedPayloads", {
sources: [AWSCDK::S3Deployment::Source.asset("./website-dist")],
destination_bucket: bucket,
sign_content: true,
})
The default memory limit for the deployment resource is 128MiB. If you need to
copy larger files, you can use the memory_limit 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
ephemeral_storage_size 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.
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.).
bucket = nil # AWSCDK::S3::Bucket
param = nil # AWSCDK::SSM::StringParameter
# Example with a secret value that contains double quotes
deployment = AWSCDK::S3Deployment::BucketDeployment.new(self, "JsonDeployment", {
sources: [
AWSCDK::S3Deployment::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,
})
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.
destination_bucket = nil # AWSCDK::S3::Bucket
vpc = nil # AWSCDK::EC2::VPC
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployMeWithEfsStorage", {
sources: [AWSCDK::S3Deployment::Source.asset(path.join(__dirname, "my-website"))],
destination_bucket: destination_bucket,
destination_key_prefix: "efs/",
use_efs: true,
vpc: vpc,
retain_on_delete: false,
})
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:
require 'aws-cdk-lib'
destination_bucket = nil # AWSCDK::S3::Bucket
topic = nil # AWSCDK::SNS::Topic
tg = nil # AWSCDK::ElasticLoadBalancingv2::ApplicationTargetGroup
app_config = {
topic_arn: topic.topic_arn,
base_url: "https://my-endpoint",
lb_name: tg.first_load_balancer_full_name,
}
AWSCDK::S3Deployment::BucketDeployment.new(self, "BucketDeployment", {
sources: [AWSCDK::S3Deployment::Source.json_data("config.json", app_config)],
destination_bucket: destination_bucket,
})
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.
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:
require 'aws-cdk-lib'
my_lambda_function = nil # AWSCDK::Lambda::Function
destination_bucket = nil # AWSCDK::S3::Bucket
# (Optional) if provided, the resulting processed file would be uploaded to the destinationBucket under the destinationKey name.
destination_key = nil
role = nil # AWSCDK::IAM::Role
AWSCDK::S3Deployment::DeployTimeSubstitutedFile.new(self, "MyFile", {
source: "my-file.yaml",
destination_key: destination_key,
destination_bucket: destination_bucket,
substitutions: {
variable_name: my_lambda_function.function_name,
},
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.
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 object_keys getter on the bucket deployment.
require 'aws-cdk-lib'
destination_bucket = nil # AWSCDK::S3::Bucket
my_bucket_deployment = AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployMeWithoutExtractingFilesOnDestination", {
sources: [AWSCDK::S3Deployment::Source.asset(path.join(__dirname, "my-website"))],
destination_bucket: destination_bucket,
extract: false,
})
AWSCDK::CfnOutput.new(self, "ObjectKey", {
value: AWSCDK::Fn.select(0, my_bucket_deployment.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.
require 'aws-cdk-lib'
destination_bucket = nil # AWSCDK::S3::Bucket
my_bucket_deployment = AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployMeWithoutExtractingFilesOnDestination", {
sources: [AWSCDK::S3Deployment::Source.asset(path.join(__dirname, "my-website"))],
destination_bucket: destination_bucket,
output_object_keys: false,
})
AWSCDK::CfnOutput.new(self, "ObjectKey", {
value: AWSCDK::Fn.select(0, my_bucket_deployment.object_keys),
})
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.
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.
vpc = AWSCDK::EC2::VPC.from_lookup(self, "ExistingVPC", {vpc_id: "vpc-12345678"})
bucket = AWSCDK::S3::Bucket.new(self, "MyBucket")
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployToS3", {
destination_bucket: bucket,
vpc: vpc,
sources: [AWSCDK::S3Deployment::Source.asset("./website")],
})
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.
vpc = AWSCDK::EC2::VPC.from_lookup(self, "ExistingVPC", {vpc_id: "vpc-12345678"})
bucket = AWSCDK::S3::Bucket.new(self, "MyBucket")
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployToS3", {
destination_bucket: bucket,
vpc: vpc,
vpc_subnets: {subnet_type: AWSCDK::EC2::SubnetType::PUBLIC},
sources: [AWSCDK::S3Deployment::Source.asset("./website")],
})
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.
vpc = AWSCDK::EC2::VPC.from_lookup(self, "ExistingVPC", {vpc_id: "vpc-12345678"})
bucket = AWSCDK::S3::Bucket.new(self, "MyBucket")
security_group = AWSCDK::EC2::SecurityGroup.new(self, "CustomSG", {
vpc: vpc,
description: "Allow HTTPS outbound access",
allow_all_outbound: false,
})
security_group.add_egress_rule(AWSCDK::EC2::Peer.any_ipv4, AWSCDK::EC2::Port.tcp(443), "Allow HTTPS traffic")
AWSCDK::S3Deployment::BucketDeployment.new(self, "DeployWithSecurityGroup", {
destination_bucket: bucket,
vpc: vpc,
security_groups: [security_group],
sources: [AWSCDK::S3Deployment::Source.asset("./website")],
})
BucketDeployment is 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.
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.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.