DEV Community

Cover image for AWS CDK 100 Drill Exercises #001: S3 Bucket Fundamentals —— From Default Settings to Practical Customization

AWS CDK 100 Drill Exercises #001: S3 Bucket Fundamentals —— From Default Settings to Practical Customization

Level 100

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?

  1. Simple Structure: No complex dependencies like networking or security groups
  2. Rich Configuration Options: Practical features including encryption, lifecycle rules, and versioning
  3. CloudFormation Comparison: Easy to understand how CDK's defaults translate to CloudFormation
  4. 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:

Architecture Overview

We'll implement eight different patterns:

  1. CDKDefault: Completely default settings
  2. Named: Explicitly specify bucket name
  3. AutoDeleteObjects: Auto-delete objects on bucket removal
  4. BlockPublicAccessOff: Partial public access configuration
  5. EncryptionSSEKMSManaged: AWS-managed KMS encryption
  6. EncryptionSSEKMSCustomer: Customer-managed KMS encryption
  7. LifecycleRules: Cost optimization through lifecycle transitions
  8. 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
Enter fullscreen mode Exit fullscreen mode

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', {});
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

Generated CloudFormation:

This configuration adds the following to the CloudFormation template:

  1. S3 Bucket: DeletionPolicy changed to Delete
  2. Bucket Policy: Permissions for custom resource
  3. Custom Resource: Lambda function to delete objects
  4. 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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

Generated CloudFormation:

{
  "BlockPublicAccessOff9C2A29A0": {
    "Type": "AWS::S3::Bucket",
    "Properties": {
      "PublicAccessBlockConfiguration": {
        "BlockPublicAcls": true,
        "BlockPublicPolicy": false,
        "IgnorePublicAcls": true,
        "RestrictPublicBuckets": true
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Use CloudFront: Keep S3 private, deliver via CloudFront
  2. Signed URLs: Grant temporary access permissions
  3. 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,
});
Enter fullscreen mode Exit fullscreen mode
  • 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,
});
Enter fullscreen mode Exit fullscreen mode

Generated CloudFormation:

{
  "EncryptionSSEKMSManagedBEDBF190": {
    "Type": "AWS::S3::Bucket",
    "Properties": {
      "BucketEncryption": {
        "ServerSideEncryptionConfiguration": [
          {
            "ServerSideEncryptionByDefault": {
              "SSEAlgorithm": "aws:kms"
            }
          }
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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,
});
Enter fullscreen mode Exit fullscreen mode
  • 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),
});
Enter fullscreen mode Exit fullscreen mode

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"
          }
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Analyze Access Patterns: Use S3 Storage Lens and access logs
  2. Gradual Transitions: STANDARD → STANDARD_IA → GLACIER INSTANT RETRIEVAL
  3. 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),
    }
  ],
});
Enter fullscreen mode Exit fullscreen mode

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
        ]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Lifecycle Rule Details

1. Noncurrent Version Expiration

noncurrentVersionExpiration: cdk.Duration.days(90),
noncurrentVersionsToRetain: 3,
Enter fullscreen mode Exit fullscreen mode
  • 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),
  },
]
Enter fullscreen mode Exit fullscreen mode
  • 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),
  },
]
Enter fullscreen mode Exit fullscreen mode
  • Transition to STANDARD_IA after 60 days
  • Transition to GLACIER after 90 days

4. Abort Incomplete Multipart Uploads

abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Verification

  1. AWS Management Console

    • Check created buckets in S3 Console
    • Verify encryption settings in Properties tab
    • Check lifecycle rules in Management tab
  2. 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>
Enter fullscreen mode Exit fullscreen mode

Cleanup

# Delete stack
cdk destroy "**" --project=sample --env=dev

# Skip confirmation prompt
cdk destroy "**" --force --project=sample --env=dev
Enter fullscreen mode Exit fullscreen mode

Best Practices

Security

  1. Block All Public Access: Keep defaults unless specifically needed
  2. Encryption is Mandatory: Minimum SSE-S3, recommended SSE-KMS
  3. Enable Versioning: For data protection and compliance
  4. Enable Access Logging: For security audits and troubleshooting

Cost Optimization

  1. Configure Lifecycle Rules: Choose storage classes based on access patterns
  2. Delete Incomplete Uploads: Auto-delete after ~7 days
  3. Manage Noncurrent Versions: Retain only necessary generations
  4. Consider S3 Intelligent-Tiering: When access patterns are unclear

Operations

  1. Naming Conventions: Include project name, environment, and purpose
  2. Tagging: Essential for cost allocation and resource management
  3. CloudTrail Integration: Audit API calls
  4. CloudWatch Metrics: Monitor bucket size and request counts

Summary

In this exercise, we learned AWS CDK fundamentals through S3 buckets.

What We Learned

  1. CDK Defaults: Secure configurations with minimal code
  2. CloudFormation Translation: How CDK code converts
  3. Deletion Control: Using autoDeleteObjects and removalPolicy
  4. Three Encryption Methods: SSE-S3, SSE-KMS (AWS-managed), SSE-KMS (Customer-managed)
  5. Lifecycle Rules: Gradual transitions for cost optimization
  6. 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)