Introduction
This is the first exercise in the "AWS CDK 100 Drill Exercises" series.
For more about AWS CDK 100 Drill Exercises, see this introduction article.
Through these 100 exercises, we'll progressively learn practical AWS CDK patterns. For this first exercise, I've chosen S3 buckets because they're an ideal starting point for learning AWS CDK fundamentals.
Why Start with S3?
- Simple Structure: No complex dependencies like networking or security groups
- Rich Configuration Options: Practical features including encryption, lifecycle rules, and versioning
- CloudFormation Comparison: Easy to understand how CDK's defaults translate to CloudFormation
- Cost Effective: Low-cost experimentation for learning purposes
What You'll Learn
- How minimal CDK code translates to CloudFormation templates
- Default behavior and customization of L2 Constructs
- S3 bucket security best practices
- Cost optimization through lifecycle rules
- Versioning and noncurrent version management
📁 Code Repository: All code examples for this exercise are available on GitHub.
Architecture Overview
Here's what we'll build in this exercise:
We'll implement eight different patterns:
- CDKDefault: Completely default settings
- Named: Explicitly specify bucket name
- AutoDeleteObjects: Auto-delete objects on bucket removal
- BlockPublicAccessOff: Partial public access configuration
- EncryptionSSEKMSManaged: AWS-managed KMS encryption
- EncryptionSSEKMSCustomer: Customer-managed KMS encryption
- LifecycleRules: Cost optimization through lifecycle transitions
- VersioningEnabled: Versioning with lifecycle management
Prerequisites
To follow along, you'll need:
- AWS CLI v2 installed and configured
- Node.js 20+
- AWS CDK CLI (
npm install -g aws-cdk) - Basic TypeScript knowledge
- AWS Account (Free Tier works for most exercises)
Project Directory Structure
s3-basics/
├── bin/
│ └── s3-basics.ts # Application entry point
├── lib/
│ ├── stacks/
│ │ └── s3-basics-stack.ts # Stack definition
│ └── stages/
│ └── s3-basics-stage.ts # Stage definition
├── test/
│ ├── compliance
│ │ └── cdk-nag.test.ts # Test code (covered in later exercises)
│ ├── snapshot
│ │ └── snapshot.test.ts # Test code (covered in later exercises)
│ └── unit
│ └── s3-basics.test.ts # Test code (covered in later exercises)
├── cdk.json
├── package.json
└── tsconfig.json
Pattern 1: Understanding CDK Defaults
Let's start with the simplest implementation.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
export class S3BasicStack extends cdk.Stack {
public readonly bucket: s3.IBucket;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// S3 bucket with completely default settings
this.bucket = new s3.Bucket(this, 'CDKDefault', {});
}
}
This minimal code creates an S3 bucket. Let's examine what CloudFormation template this generates.
Generated CloudFormation:
Running cdk synth produces the following CloudFormation template:
{
"Resources": {
"CDKDefaultE8B73DAC": {
"Type": "AWS::S3::Bucket",
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain",
"Metadata": {
"aws:cdk:path": "Dev/DrillexercisesS3Basic/CDKDefault/Resource"
}
}
}
}
Default Configuration Details
Let's examine what CDK automatically configures.
1. Logical ID Generation
The Construct ID CDKDefault gets a hash suffix E8B73DAC, resulting in the logical ID CDKDefaultE8B73DAC. This hash ensures uniqueness within the stack.
2. UpdateReplacePolicy and DeletionPolicy
Both are set to Retain, meaning the S3 bucket persists even after stack deletion - a safe default.
AWS Documentation: UpdateReplacePolicy and DeletionPolicy
⚠️ Note: If you deploy this code and destroy the stack, you'll need to manually delete the CDKDefault bucket.
3. Implicit Settings
While not explicitly shown in the CloudFormation template, these settings apply:
- Encryption: SSE-S3 (Amazon S3-managed encryption keys)
- Public Access: Completely blocked (recommended)
- Versioning: Disabled
- Bucket Name: Auto-generated by AWS
These are AWS default values. CDK prioritizes secure defaults, so even without explicit configuration, you get a safe setup.
Pattern 2: Specifying Bucket Name
This example explicitly specifies a bucket name.
However, for real projects, we recommend NOT specifying bucket names. The AWS documentation also recommends CloudFormation auto-generation.
Reasons for auto-generation:
- New buckets can't be created during resource replacement
- During replacement, new resources are created before old ones are deleted, causing name conflicts
- CDK-generated names are sufficiently unique
- Improves deployment flexibility
Only specify bucket names when absolutely necessary.
const accountId = cdk.Stack.of(this).account;
const region = cdk.Stack.of(this).region;
const regionNoHyphens = region.replace(/-/g, '');
const bucketName = [
props.project, // Project name
props.environment, // Environment identifier
'namedbucket', // Purpose
accountId, // AWS Account ID
regionNoHyphens // Region (hyphens removed)
].join('-').toLowerCase();
new s3.Bucket(this, 'NamedBucket', {
bucketName,
autoDeleteObjects: props.isAutoDeleteObject,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Result example: myproject-dev-logs-123456789012-apnortheast1
Key Bucket Name Rules:
- Must be globally unique
- Only lowercase letters, numbers, periods (
.), and hyphens (-) allowed - 3-63 characters in length
For detailed naming rules, refer to the official documentation.
Pattern 3: Deletion Behavior Control
In development environments, you often want to delete buckets and their objects together when removing the stack.
new s3.Bucket(this, 'AutoDeleteObjects', {
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
Generated CloudFormation:
This configuration adds the following to the CloudFormation template:
- S3 Bucket:
DeletionPolicychanged toDelete - Bucket Policy: Permissions for custom resource
- Custom Resource: Lambda function to delete objects
- IAM Role: Lambda execution role
{
"AutoDeleteObjects9931B84E": {
"Type": "AWS::S3::Bucket",
"Properties": {
"Tags": [
{
"Key": "aws-cdk:auto-delete-objects",
"Value": "true"
}
]
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"AutoDeleteObjectsPolicy6BD2BF78": {
"Type": "AWS::S3::BucketPolicy"
// ... bucket policy details
},
"AutoDeleteObjectsAutoDeleteObjectsCustomResourceF9A68CC5": {
"Type": "Custom::S3AutoDeleteObjects"
// ... custom resource details
},
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": {
"Type": "AWS::IAM::Role"
// ... IAM role details
},
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Runtime": "nodejs22.x",
"Timeout": 900
// ... Lambda function details
}
}
}
Critical Warning
Consider carefully before using autoDeleteObjects: true in production environments.
- Increases risk of accidental data deletion
- Audit logs and retention-required data may be lost
Use only in development/test environments. For production, explicitly set removalPolicy: cdk.RemovalPolicy.RETAIN to prioritize data protection.
Pattern 4: Public Access Control
S3 bucket public access settings are crucial for security. By default, public access blocking is enabled, but this configuration disables it partially.
CDK Code:
// Allow public policy only (block everything else)
new s3.Bucket(this, 'BlockPublicAccessOff', {
blockPublicAccess: new s3.BlockPublicAccess({
blockPublicPolicy: false
}),
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
Generated CloudFormation:
{
"BlockPublicAccessOff9C2A29A0": {
"Type": "AWS::S3::Bucket",
"Properties": {
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": false,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true
}
}
}
}
Four Public Access Block Settings
| Setting | Default | Description |
|---|---|---|
| BlockPublicAcls | true | Deny public ACL settings |
| BlockPublicPolicy | true | Deny public bucket policies |
| IgnorePublicAcls | true | Ignore public ACLs |
| RestrictPublicBuckets | true | Restrict public access |
Best Practices
Normally, keep all settings true (complete blocking). If public access is needed, consider:
- Use CloudFront: Keep S3 private, deliver via CloudFront
- Signed URLs: Grant temporary access permissions
- Strict Bucket Policies: IP address restrictions, etc.
Pattern 5: Encryption Configuration
S3 offers three encryption methods.
1. SSE-S3 (Default)
// Explicit specification
new s3.Bucket(this, 'EncryptionSSES3', {
encryption: s3.BucketEncryption.S3_MANAGED,
});
- AWS-managed encryption keys
- No additional cost
- AWS handles key rotation automatically
2. SSE-KMS (AWS-Managed Key)
new s3.Bucket(this, 'EncryptionSSEKMSManaged', {
encryption: s3.BucketEncryption.KMS_MANAGED,
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
Generated CloudFormation:
{
"EncryptionSSEKMSManagedBEDBF190": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms"
}
}
]
}
}
}
}
- Uses AWS-managed KMS keys
- Auditable via CloudTrail
- Charged for KMS API calls
3. SSE-KMS (Customer-Managed Key)
new s3.Bucket(this, 'EncryptionSSEKMSCustomer', {
encryption: s3.BucketEncryption.KMS,
encryptionKey: new cdk.aws_kms.Key(this, 'CustomKmsKey', {
enableKeyRotation: true,
}),
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
- Full key management control
- Configurable key rotation
- Granular access control
Pattern 6: Lifecycle Rules
Lifecycle rules are essential for cost optimization.
CDK Code:
const lifecycleBucket = new s3.Bucket(this, 'LifecycleRules', {
lifecycleRules: [
{
id: 'MoveToIAAfter30Days',
enabled: true,
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
},
],
},
],
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Adding rules later
lifecycleBucket.addLifecycleRule({
id: 'MoveToGlacierAfter90Days',
enabled: true,
transitions: [
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
});
lifecycleBucket.addLifecycleRule({
id: 'ExpireAfter365Days',
enabled: true,
expiration: cdk.Duration.days(365),
});
Generated CloudFormation:
{
"LifecycleRules2799D541": {
"Type": "AWS::S3::Bucket",
"Properties": {
"LifecycleConfiguration": {
"Rules": [
{
"Id": "MoveToIAAfter30Days",
"Status": "Enabled",
"Transitions": [
{
"StorageClass": "STANDARD_IA",
"TransitionInDays": 30
}
]
},
{
"Id": "MoveToGlacierAfter90Days",
"Status": "Enabled",
"Transitions": [
{
"StorageClass": "GLACIER",
"TransitionInDays": 90
}
]
},
{
"ExpirationInDays": 365,
"Id": "ExpireAfter365Days",
"Status": "Enabled"
}
]
}
}
}
}
Storage Class Selection
| Storage Class | Use Case | Cost |
|---|---|---|
| Standard | Frequent access | High |
| Standard IA | 30 days+ (monthly access) | Medium |
| Glacier Instant Retrieval | 90 days+ (annual access) | Low |
| Glacier Flexible Retrieval | Archive (After 90 days (annual access) ) | Low |
| Glacier Deep Archive | Archive (rare access) | Lowest |
Cost Optimization Tips
- Analyze Access Patterns: Use S3 Storage Lens and access logs
- Gradual Transitions: STANDARD → STANDARD_IA → GLACIER INSTANT RETRIEVAL
- Set Expiration: Auto-delete unnecessary data
Pattern 7: Versioning with Lifecycle Management
Finally, let's examine a configuration with versioning enabled.
CDK Code:
new s3.Bucket(this, 'VersioningEnabled', {
versioned: true,
autoDeleteObjects: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
lifecycleRules: [
{
id: 'ExpireNonCurrentVersionsAfter90Days',
enabled: true,
noncurrentVersionExpiration: cdk.Duration.days(90),
noncurrentVersionsToRetain: 3,
},
{
id: 'NonCurrentVersionTransitionToIAAfter30Days',
enabled: true,
noncurrentVersionTransitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
},
],
},
{
id: 'CurrentVersionTransitionToIAAfter60Days',
enabled: true,
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(60),
},
],
},
{
id: 'CurrentVersionTransitionToGlacierAfter90Days',
enabled: true,
transitions: [
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
},
{
id: 'ExpireCurrentVersionsAfter365Days',
enabled: true,
expiration: cdk.Duration.days(365),
},
{
id: 'AbortIncompleteMultipartUploadsAfter7Days',
enabled: true,
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
}
],
});
What is Versioning?
With versioning enabled, all changes to objects are preserved.
- Current Version: Latest state
- Noncurrent Versions: Previous states
Generated CloudFormation:
{
"VersioningEnabledC271D012": {
"Type": "AWS::S3::Bucket",
"Properties": {
"VersioningConfiguration": {
"Status": "Enabled"
},
"LifecycleConfiguration": {
"Rules": [
{
"Id": "ExpireNonCurrentVersionsAfter90Days",
"NoncurrentVersionExpiration": {
"NewerNoncurrentVersions": 3,
"NoncurrentDays": 90
},
"Status": "Enabled"
}
// ... other rules
]
}
}
}
}
Lifecycle Rule Details
1. Noncurrent Version Expiration
noncurrentVersionExpiration: cdk.Duration.days(90),
noncurrentVersionsToRetain: 3,
- Delete noncurrent versions after 90 days
- But retain the newest 3 versions
2. Noncurrent Version Transitions
noncurrentVersionTransitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(30),
},
]
- Transition noncurrent versions to STANDARD_IA after 30 days
3. Current Version Transitions
transitions: [
{
storageClass: s3.StorageClass.INFREQUENT_ACCESS,
transitionAfter: cdk.Duration.days(60),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
]
- Transition to STANDARD_IA after 60 days
- Transition to GLACIER after 90 days
4. Abort Incomplete Multipart Uploads
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
Auto-delete interrupted multipart uploads after 7 days, reducing unnecessary storage costs.
Deploy and Verify
Deployment
# Check differences
cdk diff --project=sample --env=dev
# Deploy
cdk deploy "**" --project=sample --env=dev
Verification
-
AWS Management Console
- Check created buckets in S3 Console
- Verify encryption settings in Properties tab
- Check lifecycle rules in Management tab
AWS CLI
# List buckets
aws s3 ls
# Check versioning configuration
aws s3api get-bucket-versioning --bucket <bucket-name>
# Check lifecycle configuration
aws s3api get-bucket-lifecycle-configuration --bucket <bucket-name>
# Check encryption configuration
aws s3api get-bucket-encryption --bucket <bucket-name>
Cleanup
# Delete stack
cdk destroy "**" --project=sample --env=dev
# Skip confirmation prompt
cdk destroy "**" --force --project=sample --env=dev
Best Practices
Security
- Block All Public Access: Keep defaults unless specifically needed
- Encryption is Mandatory: Minimum SSE-S3, recommended SSE-KMS
- Enable Versioning: For data protection and compliance
- Enable Access Logging: For security audits and troubleshooting
Cost Optimization
- Configure Lifecycle Rules: Choose storage classes based on access patterns
- Delete Incomplete Uploads: Auto-delete after ~7 days
- Manage Noncurrent Versions: Retain only necessary generations
- Consider S3 Intelligent-Tiering: When access patterns are unclear
Operations
- Naming Conventions: Include project name, environment, and purpose
- Tagging: Essential for cost allocation and resource management
- CloudTrail Integration: Audit API calls
- CloudWatch Metrics: Monitor bucket size and request counts
Summary
In this exercise, we learned AWS CDK fundamentals through S3 buckets.
What We Learned
- CDK Defaults: Secure configurations with minimal code
- CloudFormation Translation: How CDK code converts
- Deletion Control: Using
autoDeleteObjectsandremovalPolicy - Three Encryption Methods: SSE-S3, SSE-KMS (AWS-managed), SSE-KMS (Customer-managed)
- Lifecycle Rules: Gradual transitions for cost optimization
- Versioning: Comprehensive management of current and noncurrent versions
References
Let's continue learning practical AWS CDK patterns through the 100 drill exercises!
If you found this helpful, please ⭐ the repository!

Top comments (0)