4. 基础设施即代码与AWS CDK入门

当平台团队开始把“能跑起来”的环境变成“可重复交付”的产品时,手工点控制台的方式就会迅速失效:同一个 VPC 在不同账号里子网划分不一致,安全组规则靠记忆补,S3 桶策略改过之后没人知道原始版本。基础设施即代码(Infrastructure as Code,IaC)要解决的核心问题,是把基础设施的期望状态写成可版本化、可评审、可重复执行的代码,让环境本身成为交付物的一部分。

在 AWS 上,IaC 有多种实现路径。CloudFormation 是原生的声明式模板引擎,YAML/JSON 描述资源,由 AWS 负责编排与状态跟踪;Terraform 通过 HCL 和 provider 管理多云资源;而 AWS CDK(Cloud Development Kit)则把基础设施定义提升到通用编程语言层面——你可以用 TypeScript、Python、Java、C#、Go 编写代码,CDK 在合成阶段将其转换为 CloudFormation 模板,再交由 CloudFormation 部署。对平台工程团队而言,CDK 的价值在于:可以用类型系统、循环、条件、函数和包管理来组织复杂基础设施,同时保留 CloudFormation 的漂移检测、回滚和变更集能力。

CDK 的三个核心概念:构造器、堆栈与阶段

理解 CDK,关键是理解它的三层抽象。最底层是 Construct(构造器),它是 CDK 应用的基本组成单元,代表一个或多个云资源的集合。CDK 提供了 L1、L2、L3 三个层次的构造器:L1 构造器(Cfn 前缀)与 CloudFormation 资源一一对应,参数最全但最原始;L2 构造器在 L1 之上封装了合理默认值、便捷方法和资源间关联,例如 s3.Bucket 会自动处理加密、版本控制、访问日志等常见配置;L3 构造器也叫 Pattern,用于表达常见架构模式,例如 ApplicationLoadBalancedFargateService 一次性创建 ALB、ECS 集群、任务定义、服务和安全组。

中间层是 Stack(堆栈),它对应一个 CloudFormation 堆栈,是部署的最小单元。一个 Stack 内的资源会一起被创建、更新或删除,因此堆栈边界往往决定了故障域和部署节奏。例如把网络层放在 NetworkStack,把应用层放在 AppStack,把数据层放在 DataStack,可以让网络变更与应用发布解耦。

最上层是 Stage(阶段),它把一个或多个 Stack 组合成一个可部署的环境单元,例如 DevStage、StagingStage、ProdStage。Stage 的意义在于:同一套代码可以通过不同参数合成出多个环境,并且每个 Stage 可以独立部署、独立回滚。CDK 还支持 cdk deploy --all 按依赖顺序部署整个 Stage,也支持 cdk diff 查看某个 Stage 与当前实际状态的差异。

AWS CDK 三层抽象与合成部署链路 Stage(阶段)— 环境单元 DevStage 账号 1111-2222-3333 / us-east-1 StagingStage 账号 4444-5555-6666 / us-east-1 ProdStage 账号 7777-8888-9999 / us-east-1 Stack(堆栈)— 部署最小单元 NetworkStack VPC / 子网 / 路由表 NAT 网关 / 安全组 AppStack ECS 集群 / Fargate 服务 ALB / 目标组 / 监听器 DataStack RDS 实例 / 参数组 S3 桶 / 备份策略 Construct(构造器)— 资源抽象层级 L1 · CfnBucket 与 CFN 资源一一对应 参数最全,无默认值 L2 · s3.Bucket 合理默认值 + 便捷方法 自动加密 / 版本控制 L3 · Pattern ALB + Fargate 一体化 常见架构模式封装 cdk synth → CloudFormation 模板 → cdk deploy → AWS 资源栈(支持 cdk diff / cdk destroy)

编写第一个平台基础设施项目

下面以一个平台工程中常见的“网络 + 对象存储”最小项目为例,演示从初始化到部署的完整过程。假设使用 TypeScript,Node.js 18 以上,已配置好 AWS CLI 凭证。

# 1. 安装 CDK CLI 并初始化项目
npm install -g aws-cdk
mkdir platform-infra && cd platform-infra
cdk init app --language typescript

# 2. 安装所需构造器库
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-s3 @aws-cdk/aws-iam

# 3. 查看项目结构
# bin/platform-infra.ts   —— 应用入口,定义 Stage 与 Stack
# lib/network-stack.ts    —— 网络堆栈
# lib/storage-stack.ts    —— 存储堆栈
# cdk.json                —— CDK 应用配置

在 bin/platform-infra.ts 中定义 Stage 与 Stack 的组合关系。这里把 Dev 和 Prod 两个环境用同一个 Stage 类参数化,避免复制粘贴:

import * as cdk from 'aws-cdk-lib';
import { NetworkStack } from '../lib/network-stack';
import { StorageStack } from '../lib/storage-stack';

const app = new cdk.App();

interface EnvProps {
  account: string;
  region: string;
  cidr: string;
  bucketName: string;
}

const environments: Record<string, EnvProps> = {
  dev:  { account: '111122223333', region: 'us-east-1', cidr: '10.10.0.0/16', bucketName: 'platform-dev-assets' },
  prod: { account: '777788889999', region: 'us-east-1', cidr: '10.20.0.0/16', bucketName: 'platform-prod-assets' },
};

for (const [name, props] of Object.entries(environments)) {
  const env = { account: props.account, region: props.region };
  const network = new NetworkStack(app, `${name}-NetworkStack`, { env, cidr: props.cidr });
  new StorageStack(app, `${name}-StorageStack`, {
    env,
    bucketName: props.bucketName,
    vpc: network.vpc,
  });
}

在 lib/network-stack.ts 中,使用 L2 构造器 ec2.Vpc 创建带公有子网和私有子网的 VPC。注意 maxAzs: 2 表示跨两个可用区,natGateways: 1 在 Dev 环境控制成本,Prod 环境可改为 2 实现高可用:

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

interface NetworkStackProps extends cdk.StackProps {
  cidr: string;
}

export class NetworkStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: Construct, id: string, props: NetworkStackProps) {
    super(scope, id, props);

    this.vpc = new ec2.Vpc(this, 'PlatformVpc', {
      ipAddresses: ec2.IpAddresses.cidr(props.cidr),
      maxAzs: 2,
      natGateways: 1,
      subnetConfiguration: [
        { name: 'Public',  subnetType: ec2.SubnetType.PUBLIC,  cidrMask: 24 },
        { name: 'Private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, cidrMask: 24 },
      ],
    });

    cdk.Tags.of(this.vpc).add('Environment', this.node.id.split('-')[0]);
    cdk.Tags.of(this.vpc).add('ManagedBy', 'aws-cdk');
  }
}

在 lib/storage-stack.ts 中创建 S3 桶,并显式开启版本控制、服务端加密和生命周期规则。这里使用 L2 构造器,它会自动生成合理的桶策略和阻止公共访问配置:

import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

interface StorageStackProps extends cdk.StackProps {
  bucketName: string;
  vpc: ec2.Vpc;
}

export class StorageStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: StorageStackProps) {
    super(scope, id, props);

    const bucket = new s3.Bucket(this, 'AssetBucket', {
      bucketName: props.bucketName,
      versioned: true,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      lifecycleRules: [
        { transitions: [{ storageClass: s3.StorageClass.INFREQUENT_ACCESS, transitionAfter: cdk.Duration.days(30) }] },
        { expiration: cdk.Duration.days(365) },
      ],
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    new cdk.CfnOutput(this, 'BucketArn', { value: bucket.bucketArn });
  }
}

代码写完后,执行 cdk synth 会在 cdk.out/ 下生成 CloudFormation 模板。建议先查看模板中的资源逻辑 ID 和属性,确认没有意外资源。随后执行 cdk diff dev-NetworkStack 查看与当前账号实际状态的差异,再执行 cdk deploy dev-NetworkStack dev-StorageStack 部署。部署完成后,在 CloudFormation 控制台可以看到两个堆栈,在 S3 控制台可以看到桶已开启版本控制和加密。

实践建议:首次部署前先执行 cdk bootstrap aws://账号ID/区域,CDK 会在目标账号创建用于存放模板和资产的 S3 桶及 IAM 角色。每个账号+区域只需执行一次。如果使用多个环境,需要对每个账号分别 bootstrap。
注意:示例中的账号 ID、CIDR 和桶名均为教学假设值,实际使用时必须替换为你自己的账号和全局唯一的桶名。生产环境的 removalPolicy 建议保持 RETAIN,避免误删堆栈导致数据丢失。

如何判断项目结构是否合理

一个可维护的 CDK 项目通常满足几个特征:Stack 边界与变更频率一致,网络、数据、应用分层清晰;环境差异通过参数或 Stage 表达,而不是复制多份代码;构造器优先使用 L2 而非 L1,除非确实需要 L1 的完整参数控制;所有资源都打上 Environment、ManagedBy、CostCenter 等标签,便于成本分摊和审计。部署后可以通过 cdk diff 验证代码与实际状态是否一致,如果 diff 输出为空,说明没有漂移;如果出现非预期差异,应优先排查是否有人通过控制台手工修改了资源。

掌握构造器、堆栈与阶段这三个概念之后,后续就可以在此基础上引入 CI/CD 流水线,把 cdk deploy 纳入自动化流程,实现从代码提交到环境更新的完整闭环。