4. 基础设施即代码入门

在数据平台建设中,数据湖、ETL 集群、调度器往往不是一次性资源,而是需要反复创建、更新和销毁的基础设施。如果每次都靠控制台点击,环境之间就会出现配置漂移,回滚和审计也无从谈起。基础设施即代码(Infrastructure as Code,IaC)的核心思路,是把这些资源用声明式模板或编程语言描述出来,交给版本控制和自动化流水线管理。AWS 上最常用的两种工具是 CloudFormation 和 AWS CDK:前者是原生声明式模板,后者是用编程语言生成 CloudFormation 模板的抽象层。

CloudFormation 与 CDK 的分工

CloudFormation 以 YAML 或 JSON 模板为输入,模板中声明资源、参数、映射、条件和输出。它天然支持变更集(Change Set),可以在真正执行前预览资源增删改。CDK 则允许用 TypeScript、Python、Java 等语言编写“构造块”(Construct),最终通过 cdk synth 生成 CloudFormation 模板。两者不是替代关系,而是同一套底层引擎的不同表达方式:CDK 适合复杂逻辑、复用和类型检查,CloudFormation 适合直接控制模板细节和跨账号部署。

对于数据管道,典型资源包括:S3 数据湖分层存储桶、Glue 数据目录与 ETL 作业、EMR 或 Glue 集群、Step Functions 或 MWAA 调度器、IAM 角色与策略、KMS 加密密钥、CloudWatch 日志与告警。这些资源之间存在明确的依赖关系,例如 Glue 作业需要 IAM 角色访问 S3,调度器需要调用 Glue 作业,数据湖桶需要加密和生命周期策略。IaC 的价值就在于把这些依赖显式化,而不是靠人工记忆。

数据管道 IaC 资源依赖与部署链路 源码层 CDK App / CFN 模板 Git 版本控制 参数与环境配置 合成与部署 cdk synth → template Change Set 预览 cdk deploy / CFN 执行 CloudFormation 资源栈 S3 数据湖 Glue ETL 调度器 IAM 角色 KMS 密钥 日志告警 资源依赖关系 S3 数据湖桶 Glue 数据目录 Glue ETL 作业 调度器 读取 注册 触发 IAM 角色与策略贯穿所有资源:S3 读写、Glue 执行、调度器调用、KMS 加解密 最小权限原则 · 资源级 ARN 限定 · 避免通配符 * 部署命令示例: cdk synth DataPipelineStack && cdk deploy DataPipelineStack --parameters Env=prod 执行前通过 Change Set 预览资源变更,确认后再执行

用 CDK 定义数据湖与 ETL 资源

下面以 TypeScript CDK 为例,定义一个包含 S3 数据湖桶、Glue 作业和 Step Functions 调度器的栈。示例参数为教学假设,实际部署时需替换为真实值。

import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as glue from 'aws-cdk-lib/aws-glue';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as sfn from 'aws-cdk-lib/aws-stepfunctions';
import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks';

export class DataPipelineStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 数据湖分层桶:raw / curated / analytics
    const rawBucket = new s3.Bucket(this, 'RawBucket', {
      bucketName: `data-lake-raw-${this.account}-${this.region}`,
      encryption: s3.BucketEncryption.KMS_MANAGED,
      versioned: true,
      lifecycleRules: [{ expiration: cdk.Duration.days(365) }],
    });

    const curatedBucket = new s3.Bucket(this, 'CuratedBucket', {
      bucketName: `data-lake-curated-${this.account}-${this.region}`,
      encryption: s3.BucketEncryption.KMS_MANAGED,
      versioned: true,
    });

    // Glue 作业角色
    const glueRole = new iam.Role(this, 'GlueJobRole', {
      assumedBy: new iam.ServicePrincipal('glue.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSGlueServiceRole'),
      ],
    });
    rawBucket.grantRead(glueRole);
    curatedBucket.grantReadWrite(glueRole);

    // Glue ETL 作业
    const etlJob = new glue.CfnJob(this, 'EtlJob', {
      name: 'raw-to-curated-etl',
      role: glueRole.roleArn,
      command: {
        name: 'glueetl',
        scriptLocation: `s3://${rawBucket.bucketName}/scripts/etl.py`,
        pythonVersion: '3',
      },
      glueVersion: '4.0',
      workerType: 'G.1X',
      numberOfWorkers: 10,
      timeout: 60,
    });

    // Step Functions 调度器
    const startJob = new tasks.GlueStartJobRun(this, 'StartEtl', {
      glueJobName: etlJob.name!,
      integrationPattern: sfn.IntegrationPattern.RUN_JOB,
    });

    new sfn.StateMachine(this, 'EtlScheduler', {
      definition: startJob,
      timeout: cdk.Duration.minutes(30),
    });
  }
}

这段代码的关键点在于:S3 桶通过 grantRead 和 grantReadWrite 自动生成最小权限策略,而不是手写通配符;Glue 作业的 workerType 和 numberOfWorkers 是成本与性能的调节旋钮;Step Functions 使用 RUN_JOB 集成模式,会等待 Glue 作业完成后再进入下一状态。执行 cdk deploy 后,CloudFormation 会按依赖顺序创建资源,并在控制台输出资源栈事件。

CloudFormation 模板的等价表达

如果团队更倾向直接维护模板,可以用 YAML 表达同样的资源。下面只展示 S3 桶和 Glue 作业的核心片段,省略 IAM 角色的完整策略:

Resources:
  RawBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "data-lake-raw-${AWS::AccountId}-${AWS::Region}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
      VersioningConfiguration:
        Status: Enabled

  EtlJob:
    Type: AWS::Glue::Job
    Properties:
      Name: raw-to-curated-etl
      Role: !GetAtt GlueJobRole.Arn
      Command:
        Name: glueetl
        ScriptLocation: !Sub "s3://${RawBucket}/scripts/etl.py"
        PythonVersion: "3"
      GlueVersion: "4.0"
      WorkerType: G.1X
      NumberOfWorkers: 10
      Timeout: 60

对比可见,CloudFormation 更接近资源原貌,CDK 则把权限授予、命名拼接、依赖推断等重复逻辑封装起来。选择哪种方式,取决于团队的语言习惯、复用需求和审查流程。两者可以混用:用 CDK 生成模板,再交给 CloudFormation 做变更集审批。

可重复部署的关键实践

  • 环境参数化:把账号、区域、桶名后缀、作业并发数抽成参数或上下文变量,避免硬编码。CDK 中可用 cdk.json 的 context 或环境变量注入。
  • 变更集先行:生产环境部署前先执行 cdk diff 或创建 Change Set,确认没有意外删除或替换资源。
  • 状态与漂移检测:CloudFormation 会记录资源栈状态,定期用 detect-stack-drift 检查控制台手动修改造成的漂移。
  • 回滚策略:为资源栈配置失败回滚,关键资源启用保留策略(如 S3 桶的 DeletionPolicy: Retain),防止误删数据。
  • CI/CD 集成:把 cdk synth 和 cdk deploy 放入流水线,代码合并后自动部署到测试环境,生产环境加人工审批。
教学示例中的桶名、作业名、worker 数量均为假设值。实际部署时,S3 桶名必须全局唯一,Glue 作业的 worker 数量需根据数据量和预算调整,建议先用小规模数据验证再扩容。

从模板到数据管道的验证

部署完成后,验证顺序建议为:先在 S3 控制台确认 raw 和 curated 桶已创建且加密开启;再在 Glue 控制台查看作业定义和 IAM 角色是否正确绑定;然后手动启动一次 Step Functions 执行,观察 Glue 作业是否被触发并成功写入 curated 层;最后检查 CloudWatch 日志中是否有权限拒绝或脚本错误。如果调度器执行失败,优先查看 Step Functions 的执行事件历史和 Glue 作业的 CloudWatch 日志组,通常能定位到是 IAM 权限不足、脚本路径错误还是 worker 资源不足。

把数据湖、ETL 集群和调度器纳入 IaC 之后,环境重建从“半天手工操作”变成“一条命令加一次审批”。更重要的是,资源关系被写进代码,评审、回滚和审计都有了明确依据。下一阶段可以把这套模板接入持续交付流水线,让每次数据管道变更都经过自动化测试和分阶段发布。