土法炼钢兴趣小组的算法知识备份

【系统架构设计】基础设施即代码:Terraform、Pulumi 与 GitOps

文章导航

分类入口
architecture
标签入口
#IaC#Terraform#Pulumi#CDK#GitOps#drift-detection

目录

2023 年 12 月,一家金融科技公司的运维工程师在 AWS 控制台上手动修改了一条安全组规则,把某个内部服务的端口从仅限 VPC 内访问改成了 0.0.0.0/0。这次修改的目的是临时排查一个跨区域的连接问题,本打算五分钟后改回来。结果工程师被另一个紧急工单打断,忘记了这件事。三天后,自动化扫描工具发现该端口暴露在公网上,所幸没有造成数据泄漏,但公司不得不启动一次完整的安全审计流程,耗时两周,直接成本超过 15 万美元。

这类事故在依赖手动操作的团队中反复发生。根据 HashiCorp 2024 年的 State of Cloud Strategy Survey,72% 的受访企业报告过因手动配置导致的生产环境事故,其中 34% 涉及安全漏洞。手动操作的根本问题不是操作者的能力,而是人类在重复性任务上无法保证一致性——同一个人在不同时间执行同一个操作,结果可能不同。

基础设施即代码(Infrastructure as Code,IaC)的核心主张是:用可版本控制、可审查、可测试、可重复执行的代码来定义和管理基础设施。这个理念从 2011 年 Chef 和 Puppet 时代就已存在,但真正让 IaC 成为工程标配的,是 Terraform 在 2014 年引入的声明式多云模型,以及 2019 年前后 GitOps 工作流的成熟。

本文从 IaC 的核心理念出发,深入拆解 Terraform、Pulumi、AWS CDK 三个主流工具的架构设计,分析状态管理这个最困难的工程问题,然后讨论 GitOps 工作流和漂移检测机制,最后给出测试策略和真实工程案例。


一、声明式与命令式:IaC 的两种范式

基础设施管理的代码化有两条根本不同的路径。

声明式(Declarative) 模型要求工程师描述”最终状态应该是什么”,由工具负责计算当前状态与目标状态之间的差异,并生成执行计划。Terraform 和 CloudFormation 是这个范式的代表。声明式的核心优势是幂等性(Idempotency)——无论执行多少次,只要声明不变,结果就不变。

# 声明式:描述"我需要一个 VPC,CIDR 是 10.0.0.0/16"
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name        = "production-vpc"
    Environment = "prod"
  }
}

命令式(Imperative) 模型要求工程师编写”一步一步怎么做”的指令序列。早期的 Shell 脚本和 Ansible 的部分模块属于这个范式。命令式的优势是灵活性——可以处理复杂的条件逻辑和动态决策。

# 命令式:一步一步创建 VPC
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text)
aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames '{"Value": true}'
aws ec2 create-tags --resources "$VPC_ID" --tags Key=Name,Value=production-vpc Key=Environment,Value=prod

两种范式的本质区别在于状态管理的责任归属。声明式工具内部维护一个状态模型,自动处理资源的创建、更新和删除;命令式脚本把这个责任完全交给工程师——你必须自己判断资源是否已存在,自己处理部分成功的回滚。

实践中,纯粹的声明式或命令式很少单独出现。Terraform 虽然是声明式的,但 provisioner 和 null_resource 本质上是命令式的逃生舱。Pulumi 用通用编程语言编写,看起来像命令式,但底层执行引擎仍然是声明式的——它先构建一个资源依赖图(Directed Acyclic Graph,DAG),然后计算差异并执行变更。

维度 声明式 命令式
核心问题 “是什么” “怎么做”
幂等性 工具保证 工程师保证
状态管理 工具内建 手动维护
学习曲线 需要理解 DSL 复用已有编程技能
调试难度 抽象层厚,错误信息不直观 逐步执行,可断点调试
适用场景 稳态基础设施 一次性迁移、复杂编排

二、Terraform 深度:架构、State 与并发控制

Terraform 是目前市场占有率最高的 IaC 工具。根据 2024 年 CNCF 调查,在使用 IaC 的组织中,Terraform 的使用率超过 60%。理解 Terraform 的架构设计,是理解整个 IaC 领域的基础。

2.1 核心架构

Terraform 的架构由三个层次组成:

  1. Core 引擎:负责解析 HCL(HashiCorp Configuration Language)配置文件,构建资源依赖图,计算执行计划(Plan),执行变更(Apply)。
  2. Provider 插件:通过 gRPC 协议与 Core 通信,封装对具体云平台 API 的调用。每个 Provider 独立发布和版本管理。
  3. State 存储:记录 Terraform 管理的所有资源的当前状态,作为”真实世界”与”期望状态”之间的桥梁。
flowchart LR
    A["HCL 配置文件"] --> B["Terraform Core"]
    B --> C{"构建依赖图"}
    C --> D["生成执行计划 Plan"]
    D --> E{"用户确认"}
    E -->|approve| F["执行变更 Apply"]
    F --> G["Provider gRPC 调用"]
    G --> H["云平台 API"]
    F --> I["更新 State 文件"]
    I --> J["远程后端存储"]

    style B fill:#4a90d9,color:#fff
    style G fill:#e8a838,color:#fff
    style J fill:#50c878,color:#fff

Terraform 的执行流程严格遵循 init → plan → apply 三个阶段:

2.2 HCL 语法与实战配置

HCL 是一种专门为基础设施配置设计的领域特定语言(Domain-Specific Language,DSL)。它的设计目标是在 JSON 的机器可读性和 YAML 的人类可读性之间取得平衡,同时支持表达式、条件和循环。

以下是一个完整的 VPC + EKS 集群配置示例:

# providers.tf - Provider 配置
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.30"
    }
  }

  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "prod/eks/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "terraform-lock"
    encrypt        = true
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Project     = var.project_name
      Environment = var.environment
    }
  }
}

# variables.tf - 变量定义
variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "project_name" {
  type    = string
  default = "fintech-platform"
}

variable "environment" {
  type    = string
  default = "prod"
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "availability_zones" {
  type    = list(string)
  default = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}

variable "cluster_version" {
  type    = string
  default = "1.29"
}

# vpc.tf - 网络层
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.project_name}-${var.environment}-vpc"
  }
}

resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 4, count.index)
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name                                           = "${var.project_name}-private-${var.availability_zones[count.index]}"
    "kubernetes.io/role/internal-elb"               = "1"
    "kubernetes.io/cluster/${var.project_name}-eks" = "shared"
  }
}

resource "aws_subnet" "public" {
  count                   = length(var.availability_zones)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(var.vpc_cidr, 4, count.index + length(var.availability_zones))
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name                                           = "${var.project_name}-public-${var.availability_zones[count.index]}"
    "kubernetes.io/role/elb"                        = "1"
    "kubernetes.io/cluster/${var.project_name}-eks" = "shared"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-${var.environment}-igw"
  }
}

resource "aws_nat_gateway" "main" {
  count         = length(var.availability_zones)
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "${var.project_name}-nat-${var.availability_zones[count.index]}"
  }
}

resource "aws_eip" "nat" {
  count  = length(var.availability_zones)
  domain = "vpc"
}

# eks.tf - EKS 集群
resource "aws_eks_cluster" "main" {
  name     = "${var.project_name}-eks"
  role_arn = aws_iam_role.eks_cluster.arn
  version  = var.cluster_version

  vpc_config {
    subnet_ids              = aws_subnet.private[*].id
    endpoint_private_access = true
    endpoint_public_access  = true
    security_group_ids      = [aws_security_group.eks_cluster.id]
  }

  encryption_config {
    provider {
      key_arn = aws_kms_key.eks.arn
    }
    resources = ["secrets"]
  }

  enabled_cluster_log_types = [
    "api", "audit", "authenticator",
    "controllerManager", "scheduler"
  ]

  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster_policy,
    aws_iam_role_policy_attachment.eks_service_policy,
  ]
}

resource "aws_eks_node_group" "workers" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "${var.project_name}-workers"
  node_role_arn   = aws_iam_role.eks_node.arn
  subnet_ids      = aws_subnet.private[*].id

  scaling_config {
    desired_size = 3
    max_size     = 10
    min_size     = 2
  }

  instance_types = ["m6i.xlarge"]
  disk_size      = 50

  update_config {
    max_unavailable = 1
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks_worker_node_policy,
    aws_iam_role_policy_attachment.eks_cni_policy,
    aws_iam_role_policy_attachment.eks_ecr_policy,
  ]
}

resource "aws_kms_key" "eks" {
  description             = "EKS Secret Encryption Key"
  deletion_window_in_days = 7
  enable_key_rotation     = true
}

resource "aws_security_group" "eks_cluster" {
  name_prefix = "${var.project_name}-eks-cluster-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

2.3 State 文件:IaC 最脆弱的环节

Terraform State 文件(terraform.tfstate)是一个 JSON 文件,记录了 Terraform 管理的每一个资源的属性、ID 和依赖关系。它承担三个关键职责:

  1. 资源映射:将 HCL 中的逻辑名称(如 aws_vpc.main)映射到真实的云资源 ID(如 vpc-0a1b2c3d4e5f67890)。
  2. 性能优化:避免每次 plan 时都调用所有资源的 Read API,通过缓存减少 API 调用次数。
  3. 依赖追踪:记录资源之间的依赖关系,在删除或更新时确定正确的执行顺序。

State 管理是 Terraform 工程化中最困难的问题,核心挑战包括:

并发控制:两个工程师同时对同一个 State 执行 apply,可能导致资源冲突或 State 损坏。Terraform 通过后端锁(Backend Locking)解决这个问题。以 S3 + DynamoDB 为例:

# 后端配置:S3 存储 State,DynamoDB 提供分布式锁
terraform {
  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "terraform-lock"
    encrypt        = true
  }
}

DynamoDB 表的锁机制基于条件写入(Conditional Put):terraform apply 启动时向 DynamoDB 写入一条锁记录,包含操作者 ID 和时间戳;如果已存在未释放的锁,操作会被拒绝并提示等待。这和数据库的悲观锁原理相同。

State 分割:单个 State 文件管理太多资源会导致 plan 时间过长、blast radius 过大。最佳实践是按环境和职责分割 State:

terraform-state/
├── network/
│   ├── prod/terraform.tfstate
│   └── staging/terraform.tfstate
├── eks/
│   ├── prod/terraform.tfstate
│   └── staging/terraform.tfstate
├── database/
│   ├── prod/terraform.tfstate
│   └── staging/terraform.tfstate
└── monitoring/
    └── shared/terraform.tfstate

不同 State 之间通过 terraform_remote_state 数据源或 output + data source 的方式共享信息:

# eks/main.tf 中引用 network State 的输出
data "terraform_remote_state" "network" {
  backend = "s3"

  config = {
    bucket = "company-terraform-state"
    key    = "prod/network/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

resource "aws_eks_cluster" "main" {
  # ...
  vpc_config {
    subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
  }
}

State 中的敏感数据:Terraform State 以明文存储所有资源属性,包括数据库密码、API 密钥等敏感信息。这意味着 State 文件本身就是一个高价值攻击目标。必须启用后端加密(S3 SSE-KMS),严格限制 State 存储的访问权限,绝不能将 State 文件提交到 Git 仓库。

状态管理方式 适用团队规模 并发安全 敏感数据保护 运维复杂度
本地文件 单人开发 无 无 低
S3 + DynamoDB 中型团队 分布式锁 SSE-KMS 加密 中
Terraform Cloud 大型团队 内建锁 内建加密 + RBAC 低
Consul 自建运维团队 内建锁 ACL + TLS 高
PostgreSQL 已有数据库基础设施 行级锁 依赖数据库加密 中

三、Terraform 模块化设计

当 Terraform 配置超过 500 行时,把所有资源放在一个目录里会变得难以维护。模块化(Module)是 Terraform 组织大规模配置的核心机制。

3.1 模块结构与接口设计

一个设计良好的 Terraform 模块遵循”接口最小化”原则——通过 variables 暴露必要的可配置参数,通过 outputs 暴露下游需要的信息,内部实现细节对调用者不可见。

# modules/eks-cluster/variables.tf
variable "cluster_name" {
  type        = string
  description = "EKS 集群名称"
}

variable "cluster_version" {
  type        = string
  description = "Kubernetes 版本"
  default     = "1.29"
}

variable "vpc_id" {
  type        = string
  description = "VPC ID"
}

variable "subnet_ids" {
  type        = list(string)
  description = "集群子网 ID 列表"
}

variable "node_instance_types" {
  type        = list(string)
  description = "节点实例类型"
  default     = ["m6i.xlarge"]
}

variable "node_scaling" {
  type = object({
    desired = number
    min     = number
    max     = number
  })
  description = "节点组伸缩配置"
  default = {
    desired = 3
    min     = 2
    max     = 10
  }
}

# modules/eks-cluster/outputs.tf
output "cluster_endpoint" {
  value       = aws_eks_cluster.this.endpoint
  description = "EKS API Server 端点"
}

output "cluster_ca_certificate" {
  value       = aws_eks_cluster.this.certificate_authority[0].data
  description = "集群 CA 证书(Base64 编码)"
  sensitive   = true
}

output "cluster_security_group_id" {
  value       = aws_eks_cluster.this.vpc_config[0].cluster_security_group_id
  description = "集群安全组 ID"
}

调用模块时通过 source 和 version 指定来源:

# environments/prod/main.tf
module "eks" {
  source  = "git::https://github.com/company/terraform-modules.git//modules/eks-cluster?ref=v2.3.1"

  cluster_name        = "prod-fintech"
  cluster_version     = "1.29"
  vpc_id              = module.network.vpc_id
  subnet_ids          = module.network.private_subnet_ids
  node_instance_types = ["m6i.2xlarge"]

  node_scaling = {
    desired = 5
    min     = 3
    max     = 20
  }
}

3.2 模块版本管理与组合模式

模块版本管理遵循语义化版本(Semantic Versioning):

大型组织通常建立 Composition Module(组合模块)模式——底层是原子模块(单一职责),上层是组合模块(把多个原子模块编排成一个完整的环境):

terraform-modules/
├── modules/           # 原子模块
│   ├── vpc/
│   ├── eks-cluster/
│   ├── rds/
│   ├── elasticache/
│   └── s3-bucket/
├── compositions/      # 组合模块
│   ├── microservice-platform/   # VPC + EKS + RDS + 监控
│   └── data-pipeline/           # VPC + EMR + S3 + Glue
└── environments/      # 环境实例
    ├── prod/
    ├── staging/
    └── dev/

四、Pulumi 深度:通用编程语言定义基础设施

Pulumi 的核心创新是用通用编程语言(TypeScript、Python、Go、C#、Java)替代 DSL 来定义基础设施。这不仅仅是”换一种语法”——它根本性地改变了 IaC 的工程实践。

4.1 架构与执行模型

Pulumi 的执行模型和 Terraform 有本质区别。Pulumi 程序是一个真正的程序——它启动一个语言运行时(如 Node.js 或 Python),执行用户代码,构建资源声明树,然后将这棵树提交给 Pulumi 引擎。引擎对比当前 State 和期望状态,计算差异并执行变更。

// Pulumi TypeScript:与上面 Terraform 等价的 VPC + EKS 配置
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as eks from "@pulumi/eks";

const config = new pulumi.Config();
const projectName = config.require("projectName");
const environment = config.require("environment");

// VPC 配置
const vpc = new aws.ec2.Vpc(`${projectName}-vpc`, {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
    tags: {
        Name: `${projectName}-${environment}-vpc`,
        ManagedBy: "pulumi",
    },
});

const availabilityZones = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"];

// 使用循环创建子网——这是编程语言的天然优势
const privateSubnets = availabilityZones.map((az, index) => {
    return new aws.ec2.Subnet(`${projectName}-private-${az}`, {
        vpcId: vpc.id,
        cidrBlock: `10.0.${index}.0/24`,
        availabilityZone: az,
        tags: {
            Name: `${projectName}-private-${az}`,
            "kubernetes.io/role/internal-elb": "1",
        },
    });
});

const publicSubnets = availabilityZones.map((az, index) => {
    return new aws.ec2.Subnet(`${projectName}-public-${az}`, {
        vpcId: vpc.id,
        cidrBlock: `10.0.${index + 10}.0/24`,
        availabilityZone: az,
        mapPublicIpOnLaunch: true,
        tags: {
            Name: `${projectName}-public-${az}`,
            "kubernetes.io/role/elb": "1",
        },
    });
});

// EKS 集群——Pulumi 的高层组件封装了大量底层资源
const cluster = new eks.Cluster(`${projectName}-eks`, {
    vpcId: vpc.id,
    subnetIds: privateSubnets.map(s => s.id),
    instanceType: "m6i.xlarge",
    desiredCapacity: 3,
    minSize: 2,
    maxSize: 10,
    version: "1.29",
    encryptRootBockDevice: true,
    enabledClusterLogTypes: [
        "api", "audit", "authenticator",
        "controllerManager", "scheduler",
    ],
});

// 条件逻辑:只在生产环境创建多 AZ 的 NAT Gateway
const natGateways = environment === "prod"
    ? availabilityZones.map((az, index) => {
        const eip = new aws.ec2.Eip(`nat-eip-${az}`, { domain: "vpc" });
        return new aws.ec2.NatGateway(`nat-${az}`, {
            allocationId: eip.id,
            subnetId: publicSubnets[index].id,
        });
    })
    : [(() => {
        const eip = new aws.ec2.Eip("nat-eip-single", { domain: "vpc" });
        return new aws.ec2.NatGateway("nat-single", {
            allocationId: eip.id,
            subnetId: publicSubnets[0].id,
        });
    })()];

// 导出集群访问信息
export const kubeconfig = cluster.kubeconfig;
export const clusterEndpoint = cluster.eksCluster.endpoint;

4.2 通用编程语言的优势与代价

优势:

  1. 原生测试:可以用 Jest、pytest、Go testing 等标准框架编写单元测试和集成测试。
  2. 类型安全:TypeScript 和 Go 的类型系统在编译阶段捕获配置错误。
  3. 代码复用:函数、类、包——所有软件工程的抽象手段都可以使用。
  4. 条件与循环:原生 if/for/map,不需要 HCL 的 count/for_each 这种受限语法。

代价:

  1. Plan 不确定性:程序逻辑中如果调用了外部 API 或读取了环境变量,两次执行的 plan 结果可能不同。Terraform 的 HCL 在这一点上更可预测。
  2. 代码审查难度:300 行 TypeScript 比 300 行 HCL 更难一眼看出”这段代码创建了什么资源”。
  3. 运行时依赖:需要安装语言运行时和包管理器,增加了 CI/CD 环境的复杂度。

4.3 Pulumi State 管理

Pulumi 的 State 管理与 Terraform 类似,但默认提供 Pulumi Cloud 作为托管后端。也支持自托管后端:

# 使用 S3 作为 State 后端
pulumi login s3://company-pulumi-state

# 使用本地文件系统(仅限开发)
pulumi login --local

Pulumi 的 State 也是 JSON 格式,也面临并发控制和敏感数据的问题。区别在于 Pulumi Cloud 内建了加密、RBAC 和审计日志,不需要像 Terraform 那样手动配置 DynamoDB 锁表。


五、AWS CDK 深度:Construct 层级与 CloudFormation

AWS Cloud Development Kit(CDK)是 AWS 官方的 IaC 工具,底层编译到 CloudFormation 模板。CDK 的核心概念是 Construct(构件),分为三个层级。

5.1 Construct 层级

# AWS CDK Python:创建 VPC + EKS
from aws_cdk import (
    App, Stack, Environment,
    aws_ec2 as ec2,
    aws_eks as eks,
    aws_iam as iam,
)
from constructs import Construct


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

        # L2 Construct:VPC,内建了公私子网、NAT Gateway、路由表
        vpc = ec2.Vpc(
            self, "PlatformVpc",
            ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"),
            max_azs=3,
            nat_gateways=3,
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="Public",
                    subnet_type=ec2.SubnetType.PUBLIC,
                    cidr_mask=24,
                ),
                ec2.SubnetConfiguration(
                    name="Private",
                    subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS,
                    cidr_mask=24,
                ),
                ec2.SubnetConfiguration(
                    name="Isolated",
                    subnet_type=ec2.SubnetType.PRIVATE_ISOLATED,
                    cidr_mask=24,
                ),
            ],
        )

        # L2 Construct:EKS 集群
        cluster = eks.Cluster(
            self, "EksCluster",
            cluster_name="fintech-platform-eks",
            version=eks.KubernetesVersion.V1_29,
            vpc=vpc,
            vpc_subnets=[ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS)],
            default_capacity=3,
            default_capacity_instance=ec2.InstanceType.of(
                ec2.InstanceClass.M6I,
                ec2.InstanceSize.XLARGE,
            ),
            endpoint_access=eks.EndpointAccess.PUBLIC_AND_PRIVATE,
            secrets_encryption_key=self._create_eks_key(),
        )

        # 添加托管节点组
        cluster.add_nodegroup_capacity(
            "SpotWorkers",
            instance_types=[
                ec2.InstanceType.of(ec2.InstanceClass.M6I, ec2.InstanceSize.XLARGE),
                ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.XLARGE),
            ],
            capacity_type=eks.CapacityType.SPOT,
            min_size=2,
            max_size=20,
            desired_size=5,
        )

    def _create_eks_key(self):
        from aws_cdk import aws_kms as kms
        return kms.Key(
            self, "EksSecretsKey",
            alias="eks-secrets-encryption",
            enable_key_rotation=True,
        )


app = App()
EksPlatformStack(
    app, "EksPlatform",
    env=Environment(account="123456789012", region="ap-northeast-1"),
)
app.synth()

CDK 的 synth 命令将 Python/TypeScript 代码编译为 CloudFormation JSON 模板,然后通过 CloudFormation 执行部署。这意味着 CDK 的状态管理完全依赖 CloudFormation Stack——不存在独立的 State 文件,CloudFormation 服务本身就是状态存储。

5.2 CDK 与 CloudFormation 的关系

CDK 本质上是 CloudFormation 的代码生成器。这带来两个后果:

  1. 资源覆盖率受限于 CloudFormation:CloudFormation 不支持的资源,CDK 也无法管理。虽然可以通过 Custom Resource(Lambda 函数)扩展,但工程成本很高。
  2. 错误信息穿透困难:CDK 部署失败时,错误信息来自 CloudFormation,需要在 CDK 代码和 CloudFormation 模板之间做映射,调试体验不如 Terraform 直接。

六、Terraform vs Pulumi vs CDK:全方位对比

选择 IaC 工具是一个需要考虑多个维度的架构决策。以下是基于工程实践的全方位对比:

维度 Terraform Pulumi AWS CDK CloudFormation
语言 HCL(DSL) TypeScript/Python/Go/C#/Java TypeScript/Python/Go/C#/Java JSON/YAML
多云支持 优秀(数千个 Provider) 良好(主流云 + K8s) 仅 AWS 仅 AWS
学习曲线 中(需学 HCL) 低(复用已有语言技能) 低(复用已有语言技能) 高(YAML 模板膨胀)
抽象能力 模块(Module) 类/函数/包 Construct L1/L2/L3 嵌套栈(Nested Stack)
类型安全 有限(变量验证) 完整(编译器检查) 完整(编译器检查) 无
测试能力 Terratest(Go 集成测试) 原生单元测试 + 集成测试 assertions 库 + 集成测试 TaskCat(有限)
State 管理 自管理(S3/Cloud) Pulumi Cloud 或自管理 CloudFormation 管理 CloudFormation 管理
Plan 可读性 优秀(terraform plan 输出清晰) 良好(pulumi preview) 中(cdk diff 基于 CFN) 差(Change Set 信息有限)
执行速度 快(直接 API 调用) 快(直接 API 调用) 慢(经 CloudFormation) 慢
社区生态 最大(Module Registry) 增长中 AWS 官方支持 AWS 官方支持
漂移检测 terraform plan(被动) pulumi refresh(被动) CFN drift detection CFN drift detection
许可证 BSL 1.1(1.6+)/ MPL 2.0(fork OpenTofu) Apache 2.0 Apache 2.0 N/A(AWS 服务)
适用场景 多云、大型团队、合规要求高 开发者体验优先、复杂逻辑 纯 AWS、已有 CDK 生态 纯 AWS、简单部署

选型建议:


七、GitOps 工作流:PR 驱动的基础设施变更

GitOps 的核心理念是将 Git 仓库作为基础设施状态的唯一真实来源(Single Source of Truth)。所有变更通过 Pull Request(PR)提交、审查、合并,由自动化工具将 Git 中的声明同步到运行环境。

7.1 GitOps 工作流全景

flowchart TD
    A["工程师修改 IaC 代码"] --> B["提交 PR"]
    B --> C["CI 自动运行"]
    C --> C1["terraform fmt / lint"]
    C --> C2["terraform plan"]
    C --> C3["安全策略扫描 Checkov/OPA"]
    C1 --> D{"所有检查通过?"}
    C2 --> D
    C3 --> D
    D -->|否| E["PR 标记失败,通知作者"]
    D -->|是| F["Plan 结果作为 PR 评论发布"]
    F --> G["团队 Code Review"]
    G --> H{"至少 2 人 Approve?"}
    H -->|否| I["继续讨论修改"]
    I --> B
    H -->|是| J["合并到 main 分支"]
    J --> K["CD 流水线触发"]
    K --> L["terraform apply -auto-approve"]
    L --> M["Apply 结果通知 Slack/Teams"]
    M --> N["漂移检测定时任务"]
    N --> O{"检测到漂移?"}
    O -->|是| P["自动创建修复 PR"]
    O -->|否| Q["状态一致,无操作"]

    style A fill:#4a90d9,color:#fff
    style J fill:#50c878,color:#fff
    style L fill:#e8a838,color:#fff
    style P fill:#d94a4a,color:#fff

7.2 ArgoCD 与 Flux:Kubernetes 原生的 GitOps

对于 Kubernetes 上的基础设施(Helm Chart、Kustomize、YAML Manifest),ArgoCD 和 Flux 是两个主流的 GitOps 控制器。它们的核心机制是拉取模式(Pull Model)——控制器持续轮询 Git 仓库,检测变更并同步到集群,而不是由 CI/CD 管道推送变更。

拉取模式的安全优势:CI/CD 环境不需要持有集群的写权限,减少了凭证泄漏的攻击面。

ArgoCD 与 Terraform 结合的典型模式:

# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: platform-infrastructure
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/company/infrastructure.git
    targetRevision: main
    path: kubernetes/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: platform
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

7.3 IaC + GitOps 的分层架构

在实际生产环境中,IaC 和 GitOps 通常分层管理:

这种分层的好处是blast radius 控制——Layer 3 的错误不会影响 Layer 1 和 Layer 2,而 Layer 1 的变更需要更严格的审批。


八、漂移检测(Drift Detection)

漂移(Drift)是指实际运行的基础设施状态与 IaC 代码定义的期望状态之间的偏差。漂移的来源包括:

  1. 手动修改:工程师在控制台上直接修改配置。
  2. 自动修改:云平台自动更新(如安全补丁、IP 地址回收)。
  3. 外部工具:其他自动化工具修改了同一资源。
  4. API 副作用:某些 API 调用会隐式修改关联资源的属性。

8.1 检测策略

被动检测:定期运行 terraform plan,检查是否有未预期的变更。这是最简单也最常用的方式。

#!/bin/bash
# drift-detection.sh - 定时漂移检测脚本
set -euo pipefail

SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"
STATE_DIRS=("network" "eks" "database" "monitoring")

for dir in "${STATE_DIRS[@]}"; do
    echo "检查 ${dir} 的漂移状态..."
    cd "/opt/infrastructure/${dir}"

    terraform init -backend=true -input=false -no-color > /dev/null 2>&1
    PLAN_OUTPUT=$(terraform plan -detailed-exitcode -no-color 2>&1) || EXIT_CODE=$?

    if [ "${EXIT_CODE:-0}" -eq 2 ]; then
        # Exit code 2 表示有变更(即漂移)
        DRIFT_SUMMARY=$(echo "$PLAN_OUTPUT" | grep -E "^Plan:|^  #" | head -20)
        curl -s -X POST "$SLACK_WEBHOOK" \
            -H "Content-Type: application/json" \
            -d "{
                \"text\": \"漂移告警:${dir} 环境检测到配置漂移\n\`\`\`${DRIFT_SUMMARY}\`\`\`\"
            }"
    fi

    cd -
done

主动检测:使用专门的工具持续监控资源状态。AWS Config Rules、Driftctl(现已合并到 Snyk)等工具提供实时或准实时的漂移检测。

8.2 修复策略

检测到漂移后有三种处理方式:

  1. 覆盖(Overwrite):执行 terraform apply,用代码中的期望状态覆盖实际状态。适用于确认手动修改是错误的场景。
  2. 导入(Import):将手动修改反映到代码中,使代码与实际状态一致。适用于手动修改是合理的场景(如紧急修复)。
  3. 忽略(Ignore):在 lifecycle 块中标记 ignore_changes,告知 Terraform 忽略特定属性的变更。适用于云平台自动管理的属性。
resource "aws_autoscaling_group" "workers" {
  # ...

  lifecycle {
    # Kubernetes Cluster Autoscaler 会动态修改 desired_capacity
    ignore_changes = [desired_capacity]
  }
}

8.3 合规性审计

漂移检测与合规审计的结合是企业级 IaC 的核心需求。典型的合规检查链路:

  1. PR 阶段:Checkov / tfsec 扫描代码中的安全违规(如公开的 S3 Bucket、未加密的 RDS)。
  2. Plan 阶段:OPA/Rego 策略检查执行计划(如禁止删除生产数据库、禁止创建 t2.micro 以外的实例类型)。
  3. 运行时:AWS Config Rules 持续检查资源配置是否符合策略。
  4. 审计日志:所有 terraform apply 的输入输出记录到不可篡改的审计系统。

九、IaC 测试策略

“基础设施代码不需要测试”是一个常见的错误认知。基础设施变更的影响范围通常比应用代码更大——一个错误的安全组规则可能暴露整个网络,一个错误的 IAM 策略可能导致权限提升。

9.1 测试金字塔

IaC 测试遵循与应用代码类似的测试金字塔,但每一层的含义不同:

第一层:静态分析(Static Analysis)

不需要执行任何基础设施操作,只分析代码本身。

# Checkov:静态安全扫描
checkov -d . --framework terraform --output cli

# tfsec:Terraform 专用安全扫描
tfsec . --minimum-severity HIGH

# terraform validate:语法和类型检查
terraform validate

第二层:Plan 测试(Plan-time Testing)

执行 terraform plan,然后对 Plan 输出进行断言。

// Terratest:验证 Plan 输出
package test

import (
    "testing"

    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestVpcPlan(t *testing.T) {
    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/vpc",
        Vars: map[string]interface{}{
            "vpc_cidr":     "10.0.0.0/16",
            "environment":  "test",
            "project_name": "terratest",
        },
        PlanFilePath: "./vpc.tfplan",
    }

    plan := terraform.InitAndPlanAndShowWithStruct(t, terraformOptions)

    // 验证 VPC CIDR
    vpc := plan.ResourcePlannedValuesMap["aws_vpc.main"]
    assert.Equal(t, "10.0.0.0/16", vpc.AttributeValues["cidr_block"])

    // 验证创建的子网数量
    subnetCount := 0
    for resourceKey := range plan.ResourcePlannedValuesMap {
        if len(resourceKey) > 10 && resourceKey[:10] == "aws_subnet" {
            subnetCount++
        }
    }
    assert.Equal(t, 6, subnetCount) // 3 公共 + 3 私有
}

第三层:集成测试(Integration Testing)

真正创建基础设施,验证其行为,然后销毁。这是成本最高但信心最强的测试方式。

// Terratest:集成测试——创建真实 VPC 并验证
func TestVpcIntegration(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/vpc",
        Vars: map[string]interface{}{
            "vpc_cidr":     "10.99.0.0/16",
            "environment":  "integration-test",
            "project_name": "terratest",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)

    // 验证 VPC 的 DNS 设置
    vpc := aws.GetVpcById(t, vpcId, "ap-northeast-1")
    assert.True(t, vpc.EnableDnsHostnames)
    assert.True(t, vpc.EnableDnsSupport)
}

9.2 策略即代码(Policy as Code)

OPA(Open Policy Agent)和 Rego 语言是策略即代码的事实标准。以下是一个禁止创建公开 S3 Bucket 的策略:

# policy/s3_public_access.rego
package terraform.s3

import rego.v1

deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket"
    resource.change.after.acl == "public-read"
    msg := sprintf(
        "S3 Bucket '%s' 不允许设置 public-read ACL。所有 S3 Bucket 必须为私有访问。",
        [resource.address]
    )
}

deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket_public_access_block"
    after := resource.change.after
    not after.block_public_acls
    msg := sprintf(
        "S3 Bucket '%s' 必须启用 block_public_acls。",
        [resource.address]
    )
}

在 CI 中集成策略检查:

# .github/workflows/terraform.yml
name: Terraform CI
on:
  pull_request:
    paths:
      - 'infrastructure/**'

jobs:
  plan-and-validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.0

      - name: Terraform Init
        run: terraform init -backend=false
        working-directory: infrastructure/

      - name: Terraform Format Check
        run: terraform fmt -check -recursive
        working-directory: infrastructure/

      - name: Terraform Validate
        run: terraform validate
        working-directory: infrastructure/

      - name: Checkov Security Scan
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: infrastructure/
          framework: terraform
          soft_fail: false

      - name: Terraform Plan
        run: terraform plan -out=tfplan -no-color
        working-directory: infrastructure/

      - name: Convert Plan to JSON
        run: terraform show -json tfplan > tfplan.json
        working-directory: infrastructure/

      - name: OPA Policy Check
        uses: open-policy-agent/opa-github-action@v2
        with:
          input: infrastructure/tfplan.json
          policy: policy/

十、工程案例:从手动运维到全 IaC 的转型

10.1 背景

某东南亚电商平台在 2022 年管理着 AWS 上的约 800 个云资源,分布在 3 个区域(新加坡、东京、悉尼)。运维团队 8 人,所有基础设施变更通过 AWS 控制台手动操作,变更记录在 Confluence 文档中。

核心问题:

10.2 转型路径

团队用了 9 个月完成从手动运维到全 IaC 的转型,分四个阶段:

第一阶段(第 1-2 月):导入现有资源

使用 terraformer 和 terraform import 将现有资源导入 Terraform State。这是最枯燥但最关键的阶段——800 个资源的导入和配置对齐花了两个全职工程师 6 周时间。

关键教训:不要试图一次性导入所有资源。按”blast radius”从小到大导入——先导入 S3 Bucket、CloudWatch 告警等低风险资源,积累经验后再导入 VPC、RDS、EKS 等核心资源。

第二阶段(第 3-5 月):模块化与 CI/CD

第三阶段(第 6-7 月):策略即代码与合规自动化

第四阶段(第 8-9 月):GitOps 与自助服务

10.3 转型成果

指标 转型前 转型后 改善幅度
新环境创建时间 3-5 工作日 45 分钟(自动化) 减少 98%
月均配置事故数 2.3 次 0.3 次 减少 87%
安全审计耗时 4 人 × 2 周 自动报告 + 1 人 × 2 天复核 减少 93%
环境配置差异率 23% < 1% 减少 96%
变更可追溯性 约 60%(依赖文档) 100%(Git 历史) 完整覆盖
MTTR(平均修复时间) 47 分钟 12 分钟 减少 74%

最深刻的教训是:IaC 转型的最大障碍不是技术,而是习惯。团队中有工程师在导入阶段仍然习惯性地去控制台修改配置,导致 State 与实际不一致。最终通过两个措施解决——一是收紧 IAM 权限,控制台只有只读权限,所有写操作必须通过 CI/CD;二是漂移检测告警直接通知到个人,形成反馈闭环。


十一、IaC 的常见陷阱与最佳实践

11.1 State 文件损坏

State 文件损坏是 Terraform 用户遇到的最严重的问题之一。常见原因包括 apply 过程中的网络中断、并发写入冲突、手动编辑 State 文件。

预防措施:

# 安全的 State 操作
terraform state list                           # 列出所有资源
terraform state show aws_vpc.main              # 查看单个资源
terraform state mv aws_vpc.main module.vpc.aws_vpc.main  # 移动资源(重构时使用)
terraform state rm aws_s3_bucket.legacy        # 从 State 中移除(不删除实际资源)

11.2 Provider 版本锁定

Provider 的 API 变更可能导致已有配置失效。始终在 .terraform.lock.hcl 中锁定 Provider 版本,并将锁文件提交到 Git:

# .terraform.lock.hcl(由 terraform init 自动生成,必须提交到 Git)
provider "registry.terraform.io/hashicorp/aws" {
  version     = "5.31.0"
  constraints = "~> 5.30"
  hashes = [
    "h1:abc123...",
    "zh:def456...",
  ]
}

11.3 循环依赖

Terraform 的依赖图必须是有向无环图(DAG)。循环依赖会导致 plan 失败。常见的循环依赖场景是安全组互相引用:

# 错误:循环依赖
resource "aws_security_group" "app" {
  ingress {
    security_groups = [aws_security_group.db.id]
  }
}

resource "aws_security_group" "db" {
  ingress {
    security_groups = [aws_security_group.app.id]
  }
}

# 正确:使用独立的安全组规则资源打破循环
resource "aws_security_group" "app" {
  name_prefix = "app-"
  vpc_id      = aws_vpc.main.id
}

resource "aws_security_group" "db" {
  name_prefix = "db-"
  vpc_id      = aws_vpc.main.id
}

resource "aws_security_group_rule" "app_to_db" {
  type                     = "ingress"
  from_port                = 5432
  to_port                  = 5432
  protocol                 = "tcp"
  security_group_id        = aws_security_group.db.id
  source_security_group_id = aws_security_group.app.id
}

resource "aws_security_group_rule" "db_to_app" {
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  security_group_id        = aws_security_group.app.id
  source_security_group_id = aws_security_group.db.id
}

11.4 Terraform 与 OpenTofu

2023 年 8 月,HashiCorp 将 Terraform 的许可证从 MPL 2.0 改为 BSL 1.1,引发社区分裂。Linux Foundation 支持的 OpenTofu 项目是 Terraform 的开源分支(fork),保持 MPL 2.0 许可。

两者在功能上高度兼容,但长期走向可能分化。选型建议:如果组织对开源许可有严格要求,评估 OpenTofu;如果更看重商业支持和 Terraform Cloud 生态,继续使用 Terraform。


十二、IaC 的未来趋势

12.1 AI 辅助的基础设施管理

大语言模型(Large Language Model,LLM)正在改变 IaC 的编写方式。GitHub Copilot 和 Amazon CodeWhisperer 已经能够根据注释生成 Terraform/CDK 代码。但 AI 生成的基础设施代码面临比应用代码更高的安全风险——一个错误的 IAM 策略可能导致权限提升,一个错误的安全组规则可能暴露内部服务。因此,AI 辅助生成 + 策略即代码自动审查 的组合将成为标准工作流。

12.2 平台工程(Platform Engineering)与内部开发者平台

IaC 的下一步演进是将底层复杂性封装在内部开发者平台(Internal Developer Platform,IDP)中。开发者不需要编写 Terraform 代码——他们在平台门户上选择”创建一个 PostgreSQL 数据库”,平台自动调用经过审计和合规检查的 Terraform 模块。Backstage、Port、Humanitec 等平台工程工具正在推动这个方向。

12.3 不可变基础设施(Immutable Infrastructure)

IaC 的终极形态之一是不可变基础设施——基础设施组件一旦创建就不修改,所有变更都通过创建新组件并替换旧组件来实现。这与容器化理念一脉相承:不在运行中的容器里打补丁,而是构建新镜像并重新部署。

不可变基础设施与 IaC 的结合意味着 terraform apply 的结果应该是创建全新的资源组并切换流量,而不是原地更新。蓝绿部署(Blue-Green Deployment)和金丝雀发布(Canary Release)是这个理念在基础设施层面的具体体现。


导航

上一篇:Serverless 架构

下一篇:Service Mesh


参考资料

  1. Brikman, Y. “Terraform: Up & Running, 3rd Edition.” O’Reilly, 2022.
  2. HashiCorp. “Terraform Documentation: State.” https://developer.hashicorp.com/terraform/language/state
  3. Pulumi. “Pulumi Documentation: Architecture & Concepts.” https://www.pulumi.com/docs/concepts/
  4. AWS. “AWS CDK Developer Guide.” https://docs.aws.amazon.com/cdk/v2/guide/
  5. HashiCorp. “State of Cloud Strategy Survey 2024.” https://www.hashicorp.com/state-of-the-cloud
  6. Morris, K. “Infrastructure as Code, 2nd Edition.” O’Reilly, 2020.
  7. OpenTofu. “OpenTofu Documentation.” https://opentofu.org/docs/
  8. CNCF. “Cloud Native Survey 2024.” https://www.cncf.io/reports/
  9. Open Policy Agent. “OPA Documentation: Terraform.” https://www.openpolicyagent.org/docs/latest/terraform/
  10. Gruntwork. “Terratest: Automated Tests for Infrastructure Code.” https://terratest.gruntwork.io/
  11. ArgoCD. “Argo CD Documentation: GitOps.” https://argo-cd.readthedocs.io/
  12. Bridgecrew. “Checkov: Policy-as-Code for Infrastructure.” https://www.checkov.io/

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-07 · database / distributed

【分布式 OLAP 查询引擎】引擎选型与数据平台阅读地图

用决策树收束 Trino/Spark/ClickHouse/DuckDB/DataFusion/PostgreSQL 的适用边界:交互式联邦、批 ETL、嵌入式分析、流批一体各走哪条路径;给出能力对照表(无吞吐排名)与 postgresql→columnar→lakehouse→stream→query-engine 全栈阅读顺序,闭合数据平台栈。


By .