数据平台基础设施即代码

用 Terraform 管理数据平台:数据湖的存储分层与生命周期策略、ETL 作业与编排资源的声明、数据仓库与 Lake Formation 行列级细粒度权限、多环境隔离策略与最小权限模型,以及分区裁剪、标签分摊与扫描量驱动的成本治理与常见故障排错。

1. 数据平台的资源版图

一句话总结: 数据平台的 IaC 边界比普通应用广得多——从对象存储、消息队列、ETL 作业、数据仓库到权限与目录,全都需要声明式管理。

典型组件按层划分:采集层(Kinesis / Kafka / DMS / Firehose)、存储层(S3 的原始区 / 清洗区 / 集市区)、计算层(Glue / EMR / Databricks / Flink)、编排层(Airflow / Step Functions)、仓库层(Redshift / Snowflake / BigQuery)、目录层(Glue Catalog / Hive Metastore)、权限层(Lake Formation / IAM / 行列级策略)。

为什么必须用 IaC 管理:

  • 权限是核心风险面:谁能读哪张表、哪个分区,必须可审计、可评审。
  • 环境差异导致「测试通过、生产失败」:Catalog、桶名、权限模型在 dev/prod 不一致时,作业迁移必然翻车。
  • 成本巨大且易失控:一个没配生命周期的桶、一个没限制的扫描量,账单能翻十倍。
  • 血缘与目录需要版本化:表结构、分区定义的变更应当走 PR。

目录结构上,把「一个存储区」「一个作业」封装成模块,环境目录只做参数组合:

data-platform/
├── modules/{lake-zone,glue-job,lake-permission}/
└── envs/{dev,prod}/

这与模块设计的一般原则一致,但数据平台的模块接口要额外暴露数据契约(表名、分区键、schema 版本)。

2. 数据湖与存储分层

一句话总结: 数据湖用「原始区 / 清洗区 / 集市区」三层划分职责,每层的生命周期、加密、权限都不同,Terraform 模块应把这三层固化下来。

2.1 三层划分

层别名内容保留期谁写
RawBronze原始数据,不可变长期/永久采集管道
CleanedSilver去重、规范化、格式统一中长ETL 作业
CuratedGold聚合、面向分析按需数仓/BI

不可变原则:Raw 层只追加不修改,任何清洗都在 Silver 层完成。这样出错时可以重放,而不必重新采集。

2.2 模块化存储区定义

# modules/lake-zone/main.tf
variable "name"           { type = string }
variable "env"            { type = string }
variable "retention_days" { type = number }
variable "kms_key_arn"    { type = string }

resource "aws_s3_bucket" "zone" {
  bucket = "${var.env}-lake-${var.name}"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "zone" {
  bucket = aws_s3_bucket.zone.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = var.kms_key_arn
    }
    bucket_key_enabled = true          # 降低 KMS 调用成本
  }
}

resource "aws_s3_bucket_versioning" "zone" {
  bucket                   = aws_s3_bucket.zone.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_public_access_block" "zone" {
  bucket                  = aws_s3_bucket.zone.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

(加密、版本控制、公共访问阻断在 Terraform 里都是独立资源,与 CloudFormation 的内联写法不同。)

2.3 生命周期策略

数据湖的成本大头在存储与请求,生命周期规则是控成本的第一手段:

resource "aws_s3_bucket_lifecycle_configuration" "zone" {
  bucket = aws_s3_bucket.zone.id
  rule {
    id     = "tiering"
    status = "Enabled"
    filter { prefix = "data/" }
    transition { days = 30, storage_class = "STANDARD_IA" }   # 30 天转低频
    transition { days = 90, storage_class = "GLACIER_IR" }    # 90 天转归档
    expiration { days = var.retention_days }
    noncurrent_version_expiration { noncurrent_days = 30 }     # 旧版本 30 天清理
    abort_incomplete_multipart_upload { days_after_initiation = 7 }
  }
}

注意后两条:开了版本控制后,旧版本和失败的分片上传会悄悄吃掉大量存储,它们几乎是必须的。

2.4 表格式与目录

现代数据湖普遍用表格式(Iceberg / Delta / Hudi)管理元数据。Terraform 侧需要管理 Catalog、表定义与底层存储路径的对应关系;表格式的选型与治理参见 数据湖技术 ,Terraform 负责把选定的方案落地成资源。

resource "aws_glue_catalog_database" "silver" {
  name = "${var.env}_silver"
}

resource "aws_glue_catalog_table" "events" {
  database_name = aws_glue_catalog_database.silver.name
  name          = "events"
  table_type    = "EXTERNAL_TABLE"
  storage_descriptor {
    location      = "s3://${var.env}-lake-silver/data/events/"
    input_format  = "org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat"
    output_format = "org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat"
    columns { name = "event_id" type = "string" }
    columns { name = "ts"       type = "timestamp" }
  }
  partition_keys { name = "dt" type = "string" }
}

一句话: 分区键的设计直接决定查询成本,它应当与「最常见的过滤条件」一致,且必须在 Terraform 里显式声明。

3. ETL 编排资源

一句话总结: 编排工具本身、作业定义、调度、依赖都应声明化,让「谁在什么时候跑什么」成为可评审的代码。

3.1 Glue 作业

resource "aws_glue_job" "clean_events" {
  name     = "${var.env}-clean-events"
  role_arn = aws_iam_role.glue.arn
  command {
    script_location = "s3://${var.artifacts_bucket}/scripts/clean_events.py"
    python_version  = "3"
  }
  glue_version      = "4.0"
  worker_type       = var.glue_worker_type
  number_of_workers = var.glue_workers        # 成本与性能旋钮,按环境参数化
  default_arguments = {
    "--enable-job-insights" = "true"
    "--SOURCE_PATH"         = "s3://${var.env}-lake-raw/data/events/"
    "--TARGET_PATH"         = "s3://${var.env}-lake-silver/data/events/"
  }
  execution_property { max_concurrent_runs = 3 }
}

number_of_workers 与 worker_type 应当参数化而非硬编码,让不同环境用不同规格(dev 用 G.1X 小规格,prod 用 G.2X)。

3.2 调度与依赖

resource "aws_glue_trigger" "daily" {
  name     = "${var.env}-daily-clean"
  type     = "SCHEDULED"
  schedule = "cron(0 2 * * ? *)"       # 每天 02:00 UTC
  actions { job_name = aws_glue_job.clean_events.name }
}

# 用 Workflow 表达多作业依赖
resource "aws_glue_workflow" "pipeline" { name = "${var.env}-lake-pipeline" }

resource "aws_glue_trigger" "on_success" {
  name          = "${var.env}-after-clean"
  type          = "CONDITIONAL"
  workflow_name = aws_glue_workflow.pipeline.name
  predicate { conditions { job_name = aws_glue_job.clean_events.name, state = "SUCCEEDED" } }
  actions { job_name = aws_glue_job.aggregate_events.name }
}

3.3 Airflow 的声明式管理

若用 MWAA 或自建 Airflow,基础设施(环境、网络、执行角色)由 Terraform 管,DAG 由代码仓库管——不要把 DAG 内容塞进 Terraform,那会让每次 DAG 变更都触发基础设施流水线。

resource "aws_mwaa_environment" "airflow" {
  name                  = "${var.env}-airflow"
  airflow_version       = "2.9.2"
  environment_class     = "mw1.medium"
  execution_role_arn    = aws_iam_role.mwaa.arn
  source_bucket_arn     = aws_s3_bucket.dags.arn
  dag_s3_path           = "dags/"
  webserver_access_mode = "PRIVATE_ONLY"
  max_workers           = 10
  min_workers           = 1
}

(网络配置通过 network_configuration 块传入安全组与私有子网 ID,此处从略。)

分界线:Terraform 管「环境的形状」,DAG 仓库管「环境里跑什么」。

4. 数据仓库与权限

一句话总结: 数据仓库的 IaC 重点不在建集群(那部分与托管数据库 类似),而在「库/表/角色的权限模型」。

resource "aws_redshift_cluster" "warehouse" {
  cluster_identifier          = "${var.env}-warehouse"
  node_type                   = var.redshift_node_type
  number_of_nodes             = var.redshift_nodes
  database_name               = "analytics"
  master_username             = "admin"
  manage_master_user_password = true     # 交给 Secrets Manager 托管并轮换
  encrypted                   = true
  publicly_accessible         = false
  final_snapshot_identifier   = "${var.env}-warehouse-final"
}

manage_master_user_password = true 让密码由 Secrets Manager 自动轮换,避免在 state 里出现明文口令——这与 密钥与 Vault 的整体思路一致。

4.1 Lake Formation 细粒度权限

Lake Formation 是数据湖细粒度权限的主流方案,按列授权与按行过滤都可完全用 Terraform 表达:

resource "aws_lakeformation_permissions" "analyst_select" {
  principal   = aws_iam_role.analyst.arn
  permissions = ["SELECT", "DESCRIBE"]
  table_with_columns {
    database_name = aws_glue_catalog_database.silver.name
    name          = aws_glue_catalog_table.events.name
    column_names  = ["event_id", "ts", "event_type"]   # 列级授权,排除 payload
  }
}

# 行级过滤让多租户共享一张表成为可能
resource "aws_lakeformation_data_cells_filter" "tenant_isolation" {
  table_data {
    database_name = aws_glue_catalog_database.silver.name
    table_name    = aws_glue_catalog_table.events.name
    name          = "tenant_a_only"
    column_names  = ["event_id", "ts", "tenant_id"]
    row_filter { filter_expression = "tenant_id = 'tenant-a'" }
  }
}

4.2 权限模型的组织

推荐「角色分层」:raw_reader(工程师排查)、silver_reader(分析师)、gold_reader(BI)、etl_writer(作业角色)、platform_admin(平台团队,数量极少)。每个角色是一组 IAM Role + Lake Formation 授权,用 for_each 批量生成:

locals {
  analyst_tables = { events = ["event_id", "ts"], sessions = ["session_id", "user_id"] }
}

resource "aws_lakeformation_permissions" "analyst" {
  for_each    = local.analyst_tables
  principal   = aws_iam_role.analyst.arn
  permissions = ["SELECT"]
  table_with_columns {
    database_name = aws_glue_catalog_database.silver.name
    name          = each.key
    column_names  = each.value
  }
}

5. 环境隔离

一句话总结: 数据平台的环境隔离比应用更严格——不只是资源隔离,还包括数据隔离与权限隔离,dev 绝不能读到 prod 的原始数据。

级别实现隔离强度成本
命名前缀同账号同桶,靠前缀弱最低
独立桶 + 独立 Catalog同账号,资源分离中低
独立账号跨账号,IAM 边界隔离强中高

数据平台推荐至少第二级,涉及敏感数据(PII)时用第三级。环境目录只做参数组合,关键差异用变量表达,不用 if/else:

# envs/prod/main.tf
module "raw_zone" {
  source         = "../../modules/lake-zone"
  name           = "raw"
  env            = "prod"
  retention_days = 3650
  kms_key_arn    = module.kms.prod_key_arn
}

module "silver_zone" {
  source         = "../../modules/lake-zone"
  name           = "silver"
  env            = "prod"
  retention_days = 1095
  kms_key_arn    = module.kms.prod_key_arn
}

5.1 跨环境数据流与最小权限

若确实需要「prod 脱敏后给 dev」,不要直接给桶权限,而是建一条显式的脱敏管道:prod-raw ──脱敏作业──► prod-anonymized ──复制──► dev-lake,每一步都是 Terraform 资源,权限边界清晰且可审计。

在数据平台里,显式的 Deny 比隐式的「没授权」更可靠——IAM 策略会叠加,显式拒绝能兜住意外的授权:

data "aws_iam_policy_document" "analyst" {
  statement {
    sid       = "ReadGoldOnly"
    actions   = ["s3:GetObject", "s3:ListBucket"]
    resources = [aws_s3_bucket.gold.arn, "${aws_s3_bucket.gold.arn}/data/*"]
  }
  statement {
    sid       = "DenyOtherZones"
    effect    = "Deny"
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.raw.arn}/*", "${aws_s3_bucket.silver.arn}/*"]
  }
}

6. 成本治理

一句话总结: 数据平台的成本由「存储量、请求数、扫描量、计算时长」四项驱动,Terraform 侧能控制的是生命周期、分区、计算规格与调度频率。

因素控制手段
存储量生命周期分层、旧版本清理、压缩格式(Parquet/ZSTD)
请求数小文件合并(compaction)、避免高频 list
扫描量分区裁剪、列式存储、只选需要的列
计算时长合理 worker 规格、自动伸缩、按需 vs 预留
# 1. 小文件合并:用定时作业而不是手工
resource "aws_glue_trigger" "compaction" {
  name     = "${var.env}-compaction"
  type     = "SCHEDULED"
  schedule = "cron(0 3 * * ? *)"
  actions { job_name = aws_glue_job.compact.name }
}

# 2. 预算告警
resource "aws_budgets_budget" "data_platform" {
  name         = "${var.env}-data-platform"
  budget_type  = "COST"
  limit_amount = var.monthly_budget
  limit_unit   = "USD"
  time_unit    = "MONTHLY"
  notification {
    comparison_operator = "GREATER_THAN"
    threshold           = 80
    threshold_type      = "PERCENTAGE"
    notification_type   = "FORECASTED"
    subscriber_sns_topic_arns = [aws_sns_topic.budget.arn]
  }
}

6.1 标签策略

数据平台的成本分摊依赖标签,用 default_tags 统一注入,避免每个资源手写:

provider "aws" {
  region = var.region
  default_tags {
    tags = {
      Environment = var.env
      Platform    = "data"
      CostCenter  = var.cost_center
      ManagedBy   = "terraform"
    }
  }
}

按 CostCenter 分摊后,能清楚看到「哪个业务的数据管道最贵」,这是优化的起点。更多通用手段参见 成本优化 。

7. 实战与排错

一句话总结: 数据平台的 Terraform 排错集中在「权限不足、目录不同步、资源被外部修改」三类,且很多问题表现为作业运行失败而非 apply 失败。

现象原因处理
作业 AccessDenied执行角色缺权限查 CloudTrail 的 denied 事件,补策略
分析师看不到表Lake Formation 未授权补 aws_lakeformation_permissions
能读表但读不了某列列级授权未包含该列扩展 column_names
Terraform 报「不能修改 LF 权限」表由非 LF 管理统一 LF 管理,避免混用 IAM 与 LF 授权

表在 Catalog 里但 Terraform 不认识时,用 terraform import aws_glue_catalog_table.events "123456789012:${ENV}_silver:events" 纳管;表被控制台改了,用 terraform plan -refresh-only 看漂移。作业会动态创建分区,因此这些字段要用 lifecycle { ignore_changes = [partition_index, parameters] } 收敛。

注意:修改 Glue 作业的 number_of_workers 会触发作业定义更新,正在运行的实例不受影响,下一次调度才用新规格,因此成本相关的变更不必等窗口期。

生产清单:

  • Raw 层不可变,仅追加;清洗逻辑只在 Silver 层。
  • 每个桶都有生命周期、版本控制、KMS 加密、公共访问阻断。
  • 分区键与常用过滤条件一致,并在 Terraform 中声明。
  • 权限按角色分层,敏感列用列级授权,多租户用行级过滤。
  • 敏感环境用独立账号隔离,跨环境数据流经显式脱敏管道。
  • default_tags 注入成本分摊标签,预算告警已配置。
  • 动态创建的分区/参数用 ignore_changes 收敛,避免 plan 噪声。
  • Terraform 管「环境形状」,DAG/脚本由代码仓库管,边界清晰。

一句话收尾: 数据平台的 IaC 难点不在「建资源」,而在「管契约」——表结构、分区、权限、生命周期都是长期演进的契约。把这些契约写进 Terraform 并走评审,数据平台才从「一堆脚本和手工配置」变成可治理的工程资产。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. 引导与状态后端自举
  2. 从 CloudFormation 迁移到 Terraform
  3. Terraform 与 Ansible 协同