Chapter 2: CDK Core Concepts

Haiyue
17min
Learning Objectives
  • Master the three core concepts: App, Stack, and Construct
  • Understand CDK’s hierarchical structure and organization
  • Learn to use basic CDK CLI commands
  • Understand CDK lifecycle and deployment process

Detailed Explanation of Core Concepts

The Three Core Concepts of CDK

🔄 正在渲染 Mermaid 图表...

1. App (Application)

App is the root container of a CDK application, representing the entire CDK application.

#!/usr/bin/env python3
import aws_cdk as cdk
from my_app.vpc_stack import VpcStack
from my_app.database_stack import DatabaseStack
from my_app.web_stack import WebStack

# Create CDK application
app = cdk.App()

# Define multiple Stacks in the application
vpc_stack = VpcStack(app, "VpcStack")
db_stack = DatabaseStack(app, "DatabaseStack", vpc=vpc_stack.vpc)
web_stack = WebStack(app, "WebStack", vpc=vpc_stack.vpc, database=db_stack.database)

# Synthesize the application
app.synth()

2. Stack

Stack is the basic unit of deployment, corresponding to a CloudFormation stack.

from aws_cdk import Stack, Environment
from constructs import Construct
import aws_cdk as cdk

class MyStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Stack properties
        self.stack_name = "MyApplicationStack"
        self.description = "This stack contains my web application resources"

        # Define your resource constructs here
        # self.create_networking()
        # self.create_compute()
        # self.create_storage()

    @property
    def availability_zones(self):
        """Get availability zones"""
        return self.get_azs()

    @property
    def region(self):
        """Get current region"""
        return self.region

# Stack with environment configuration
production_env = Environment(
    account="123456789012",
    region="us-east-1"
)

stack = MyStack(app, "ProductionStack", env=production_env)

3. Construct

Construct is the basic building block of CDK, representing cloud components.

from constructs import Construct
from aws_cdk import (
    aws_s3 as s3,
    aws_iam as iam,
    RemovalPolicy
)

class S3BucketWithPolicy(Construct):
    """Custom construct: S3 bucket with policy"""

    def __init__(self, scope: Construct, construct_id: str,
                 bucket_name: str = None, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Create S3 bucket
        self.bucket = s3.Bucket(
            self,
            "Bucket",
            bucket_name=bucket_name,
            versioned=True,
            encryption=s3.BucketEncryption.S3_MANAGED,
            removal_policy=RemovalPolicy.DESTROY,
            auto_delete_objects=True
        )

        # Create IAM policy
        self.read_policy = iam.PolicyStatement(
            effect=iam.Effect.ALLOW,
            actions=["s3:GetObject", "s3:GetObjectVersion"],
            resources=[f"{self.bucket.bucket_arn}/*"],
            principals=[iam.ServicePrincipal("lambda.amazonaws.com")]
        )

        # Apply policy to bucket
        self.bucket.add_to_resource_policy(self.read_policy)

    @property
    def bucket_name(self) -> str:
        """Return bucket name"""
        return self.bucket.bucket_name

    @property
    def bucket_arn(self) -> str:
        """Return bucket ARN"""
        return self.bucket.bucket_arn

# Using custom construct in Stack
class MyApplicationStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Use custom construct
        data_bucket = S3BucketWithPolicy(
            self,
            "DataBucket",
            bucket_name="my-app-data-bucket"
        )

        logs_bucket = S3BucketWithPolicy(
            self,
            "LogsBucket",
            bucket_name="my-app-logs-bucket"
        )

CDK Hierarchical Structure

Construct Hierarchy

🔄 正在渲染 Mermaid 图表...

Hierarchy Details

LevelNameCharacteristicsUse Cases
L1CFN Resources1
mapping to CloudFormation
Need precise control over resource attributes
L2Intent-based APIEncapsulates best practicesRecommended for daily development
L3PatternsMulti-resource compositionQuickly implement common architectures

Example: Creating S3 Buckets at Different Levels

from aws_cdk import (
    Stack,
    aws_s3 as s3,
    aws_s3_deployment as s3deploy,
    RemovalPolicy
)
from constructs import Construct

class S3ExamplesStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # L1 Construct - Direct CloudFormation resource
        l1_bucket = s3.CfnBucket(
            self,
            "L1Bucket",
            bucket_name="my-l1-bucket",
            versioning_configuration={
                "status": "Enabled"
            },
            public_access_block_configuration={
                "blockPublicAcls": True,
                "blockPublicPolicy": True,
                "ignorePublicAcls": True,
                "restrictPublicBuckets": True
            }
        )

        # L2 Construct - High-level API with best practices
        l2_bucket = s3.Bucket(
            self,
            "L2Bucket",
            bucket_name="my-l2-bucket",
            versioned=True,  # Simplified versioning setting
            block_public_access=s3.BlockPublicAccess.BLOCK_ALL,  # Simplified access control
            encryption=s3.BucketEncryption.S3_MANAGED,
            removal_policy=RemovalPolicy.DESTROY,
            auto_delete_objects=True
        )

        # L3 Construct - Pattern/composed construct (requires additional packages)
        # Here's a custom L3 construct example
        website_bucket = StaticWebsite(
            self,
            "StaticWebsite",
            domain_name="example.com"
        )

class StaticWebsite(Construct):
    """L3 construct example: Static website hosting"""

    def __init__(self, scope: Construct, construct_id: str,
                 domain_name: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Create S3 bucket for hosting
        self.bucket = s3.Bucket(
            self,
            "WebsiteBucket",
            website_index_document="index.html",
            website_error_document="error.html",
            public_read_access=True,
            removal_policy=RemovalPolicy.DESTROY,
            auto_delete_objects=True
        )

        # Deploy website content
        s3deploy.BucketDeployment(
            self,
            "DeployWebsite",
            sources=[s3deploy.Source.asset("./website-dist")],
            destination_bucket=self.bucket
        )

    @property
    def url(self) -> str:
        return self.bucket.bucket_website_url

CDK Project Structure

Typical Project Structure

my-cdk-app/
├── app.py                  # Application entry point
├── cdk.json               # CDK configuration
├── requirements.txt       # Python dependencies
├── requirements-dev.txt   # Development dependencies
├── README.md
├── .gitignore
├── tests/                 # Test directory
│   ├── unit/
│   └── integration/
└── my_cdk_app/           # Application package
    ├── __init__.py
    ├── stacks/           # Stack definitions
    │   ├── __init__.py
    │   ├── vpc_stack.py
    │   ├── database_stack.py
    │   └── web_stack.py
    ├── constructs/       # Custom constructs
    │   ├── __init__.py
    │   └── custom_constructs.py
    └── config/           # Configuration files
        ├── __init__.py
        └── environments.py

Modular Application Organization

# app.py
#!/usr/bin/env python3
import aws_cdk as cdk
from my_cdk_app.config.environments import get_environment_config
from my_cdk_app.stacks.vpc_stack import VpcStack
from my_cdk_app.stacks.database_stack import DatabaseStack
from my_cdk_app.stacks.web_stack import WebStack

app = cdk.App()

# Get environment configuration from context
env_name = app.node.try_get_context("env") or "dev"
env_config = get_environment_config(env_name)

# Create VPC Stack
vpc_stack = VpcStack(
    app,
    f"VpcStack-{env_name}",
    env_config=env_config,
    env=env_config.cdk_environment
)

# Create Database Stack, depends on VPC
database_stack = DatabaseStack(
    app,
    f"DatabaseStack-{env_name}",
    vpc=vpc_stack.vpc,
    env_config=env_config,
    env=env_config.cdk_environment
)

# Create Web Stack, depends on VPC and Database
web_stack = WebStack(
    app,
    f"WebStack-{env_name}",
    vpc=vpc_stack.vpc,
    database=database_stack.database,
    env_config=env_config,
    env=env_config.cdk_environment
)

app.synth()
# my_cdk_app/config/environments.py
from dataclasses import dataclass
from aws_cdk import Environment

@dataclass
class EnvironmentConfig:
    environment_name: str
    account_id: str
    region: str
    vpc_cidr: str
    database_instance_type: str
    web_instance_count: int

    @property
    def cdk_environment(self) -> Environment:
        return Environment(account=self.account_id, region=self.region)

def get_environment_config(env_name: str) -> EnvironmentConfig:
    """Get configuration based on environment name"""
    configs = {
        "dev": EnvironmentConfig(
            environment_name="development",
            account_id="111111111111",
            region="us-west-2",
            vpc_cidr="10.0.0.0/16",
            database_instance_type="db.t3.micro",
            web_instance_count=1
        ),
        "staging": EnvironmentConfig(
            environment_name="staging",
            account_id="222222222222",
            region="us-west-2",
            vpc_cidr="10.1.0.0/16",
            database_instance_type="db.t3.small",
            web_instance_count=2
        ),
        "prod": EnvironmentConfig(
            environment_name="production",
            account_id="333333333333",
            region="us-east-1",
            vpc_cidr="10.2.0.0/16",
            database_instance_type="db.r5.large",
            web_instance_count=3
        )
    }

    if env_name not in configs:
        raise ValueError(f"Unknown environment: {env_name}")

    return configs[env_name]

CDK Lifecycle

Development to Deployment Workflow

🔄 正在渲染 Mermaid 图表...

CDK Command Details

# Project initialization
# cdk init app --language python

# View all available stacks
# cdk list

# Synthesize template for specific stack
# cdk synth MyStack --output ./cdk.out

# View changes to be deployed
# cdk diff MyStack

# Deploy single stack
# cdk deploy MyStack

# Deploy all stacks
# cdk deploy --all

# Deploy with parameters
# cdk deploy --context env=prod

# Destroy stack
# cdk destroy MyStack

# View stack metadata
# cdk metadata MyStack

# List construct tree
# cdk tree

# Initialize bootstrap environment
# cdk bootstrap aws://ACCOUNT-NUMBER/REGION

Context and Configuration

# cdk.json - CDK project configuration file
{
  "app": "python app.py",
  "watch": {
    "include": ["**"],
    "exclude": [
      "README.md",
      "cdk*.json",
      "requirements*.txt",
      "source.bat",
      "**/__pycache__",
      "**/*.pyc"
    ]
  },
  "context": {
    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
    "@aws-cdk/core:checkSecretUsage": true,
    "@aws-cdk/core:target-partitions": ["aws", "aws-cn"],

    // Custom context values
    "vpc-cidr": "10.0.0.0/16",
    "enable-dns-hostnames": true,
    "database-backup-retention": 7
  },
  "feature_flags": {
    "@aws-cdk/core:enableStackNameDuplicates": true,
    "@aws-cdk/core:enableStringConsolidation": true
  }
}
# Using context in code
from aws_cdk import Stack
from constructs import Construct

class MyStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Get context values
        vpc_cidr = self.node.try_get_context("vpc-cidr")
        enable_dns = self.node.try_get_context("enable-dns-hostnames")
        backup_retention = self.node.try_get_context("database-backup-retention")

        # Override context with environment variables (passed via command line)
        environment = self.node.try_get_context("environment") or "dev"

        print(f"Deploying to environment: {environment}")
        print(f"Using VPC CIDR: {vpc_cidr}")

Best Practices

1. Stack Design Principles

# ✅ Good practice: Separate stacks by function and lifecycle
class NetworkStack(Stack):
    """Network infrastructure - long lifecycle, infrequent changes"""
    pass

class DatabaseStack(Stack):
    """Database layer - long lifecycle, but configuration may change"""
    pass

class ApplicationStack(Stack):
    """Application layer - frequent deployments and updates"""
    pass

# ❌ Avoid: Putting all resources in one giant Stack
class MonolithStack(Stack):
    """Not recommended: Giant Stack containing all resources"""
    pass

2. Resource Naming Convention

class NamingConventionStack(Stack):
    def __init__(self, scope: Construct, construct_id: str,
                 env_name: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Use consistent naming convention
        self.app_name = "myapp"

        # S3 bucket naming
        bucket = s3.Bucket(
            self,
            "DataBucket",
            bucket_name=f"{self.app_name}-{env_name}-data-{self.account}-{self.region}"
        )

        # Lambda function naming
        function = lambda_.Function(
            self,
            "ProcessorFunction",
            function_name=f"{self.app_name}-{env_name}-processor",
            # ... other configurations
        )

3. Dependency Management

# Explicitly declare dependencies through constructor
class WebStack(Stack):
    def __init__(self, scope: Construct, construct_id: str,
                 vpc: ec2.Vpc,  # Explicit dependency
                 database: rds.Database,  # Explicit dependency
                 **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # Use passed dependencies
        self.vpc = vpc
        self.database = database

        # Create resources that depend on VPC and database
        self.create_application_resources()

    def create_application_resources(self):
        # Create application resources here
        pass

This chapter deeply introduces CDK’s core architectural concepts, laying a solid foundation for subsequent practical development. Understanding these concepts is crucial for building maintainable and scalable CDK applications.