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

【系统架构设计百科】基础设施即代码: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-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .