文章

IaC 方法论与实践总览

IaC 方法论与实践总览

概述

IaC(Infrastructure as Code,基础设施即代码)是用声明式配置文件而非手动操作来管理基础设施的实践。它将服务器、网络、存储、数据库等资源的定义版本化、可审计、可复用,是 SRE 稳定性工程的基石之一。

一句话:手动建基础设施是”艺术品”(一次性、不可复现),IaC 是”工业品”(可批量、可回滚、可审计)。

IaC 核心理念

三大原则

原则含义反模式
声明式 (Declarative)描述”我要什么”(期望状态),不描述”怎么做”(步骤)命令式脚本:先 apt install,再 systemctl start,再 ufw allow
幂等性 (Idempotent)同一份代码执行 1 次和 100 次结果相同Shell 脚本重复执行报错或产生副作用
不可变基础设施 (Immutable)修改配置 = 构建新镜像 + 替换旧实例,而非在运行中修改SSH 登录服务器手动 apt upgrade

声明式 vs 命令式

命令式 (Imperative) — Shell / Python 脚本:
  1. aws ec2 create-vpc ...        → 创建 VPC
  2. aws ec2 create-subnet ...     → 创建子网
  3. aws ec2 run-instances ...     → 启动 EC2
  4. ansible-playbook setup.yml    → 配置服务器
  # 问题:步骤 3 失败后,VPC 和子网已创建但没被记录 → 脏状态

声明式 (Declarative) — Terraform HCL:
  resource "aws_vpc" "main" { ... }
  resource "aws_subnet" "main" { ... }
  resource "aws_instance" "web" { ... }
  # Terraform 自动处理依赖关系、失败回滚、状态同步

不可变基础设施演进

graph LR
    A["手动配置<br/>SSH + apt install"] --> B["配置管理<br/>Ansible Playbook"]
    B --> C["镜像即代码<br/>Packer + Ansible"]
    C --> D["不可变部署<br/>替换而非修改"]

    style A fill:#f8d7da,stroke:#dc3545
    style B fill:#fff3cd,stroke:#ffc107
    style C fill:#d1ecf1,stroke:#17a2b8
    style D fill:#d4edda,stroke:#28a745
阶段做法问题
手动配置SSH 登录逐台配置不可复现、不可审计、配置漂移
配置管理Ansible 批量配置仍是”在运行中修改”,有漂移风险
镜像即代码Packer 构建预制镜像镜像构建慢,但部署快且一致
不可变部署新版本 = 新镜像 + 替换旧实例零漂移,但需要蓝绿/滚动发布支持

IaC 工具全景图

graph TB
    subgraph "IaC 五层工具栈"
        L5["Policy as Code<br/>安全合规扫描"]
        L4["Testing<br/>IaC 代码测试"]
        L3["Image as Code<br/>不可变镜像"]
        L2["Configuration Management<br/>OS 层配置"]
        L1["Infrastructure Provisioning<br/>资源创建"]
    end

    L1 --> L2 --> L3
    L3 -.-> L4
    L1 -.-> L4
    L4 -.-> L5
    L1 -.-> L5

    subgraph "L1: Provisioning"
        T["Terraform<br/>多云 HCL"]
        P["Pulumi<br/>通用语言 TS/Go/Py"]
        CF["CloudFormation<br/>AWS 原生"]
        CR["Crossplane<br/>K8s 原生 IaC"]
    end

    subgraph "L2: Configuration"
        AN["Ansible<br/>Agentless SSH"]
        SA["SaltStack<br/>Agent + MQ"]
        CH["Chef/Puppet<br/>Agent + DSL"]
    end

    subgraph "L3: Image"
        PK["Packer<br/>多云镜像"]
    end

    subgraph "L4: Testing"
        TT["Terratest<br/>Go 测试"]
        KT["kitchen-terraform"]
    end

    subgraph "L5: Policy"
        OPA["OPA / Conftest<br/>通用策略引擎"]
        TS["tfsec / Checkov<br/>Terraform 安全扫描"]
        SEN["Sentinel<br/>Terraform Cloud"]
    end

Provisioning 工具对比

工具语言多云支持状态管理适用场景学习曲线
TerraformHCL (DSL)✅ 200+ ProviderState 文件通用多云 IaC 标准选择
PulumiTS/Go/Python/JS✅ 复用 TF ProviderState 服务团队有编程能力、需要逻辑复用中高
CloudFormationJSON/YAML❌ AWS only服务端管理纯 AWS 环境、无第三方依赖
CrossplaneYAML (K8s CRD)✅ 多云 ProvideretcdK8s 原生团队、GitOps 统一管理中高
CDKTS/Python/Java❌ AWS only→ CFNAWS + 编程语言偏好

选型建议:多云环境选 Terraform(生态最成熟);K8s 原生团队考虑 Crossplane(用 kubectl 管基础设施);团队编程能力强选 Pulumi。

Configuration Management 对比

工具架构协议优势劣势
AnsibleAgentlessSSH/WinRM零部署成本、学习曲线低、Playbook 可读性好大规模(1000+ 节点)较慢
SaltStackAgent + MasterZeroMQ事件驱动、速度快、实时执行需部署 Agent、架构复杂
ChefAgent + ServerHTTPSDSL 灵活、社区成熟Ruby 依赖、学习曲线高
PuppetAgent + MasterHTTPS模型驱动、合规检查强DSL 学习曲线高

选型建议:SRE 团队首选 Ansible(SSH 即可用,与 Terraform 天然互补);超大规模集群可评估 SaltStack。

IaC 成熟度模型

级别名称特征典型表现
L1手动操作点击控制台、SSH 逐台配置”上次怎么配的?” → 翻聊天记录
L2脚本化Shell/Python 脚本批量操作有脚本但无版本管理、无幂等保证
L3IaC 工具Terraform/Ansible 管理基础设施代码版本化、有 State、可 Plan
L4CI/CD 集成PR 触发 Plan、审批后 ApplyAtlantis/Terraform Cloud、代码审查
L5全自动化GitOps + Policy + Testing + 自愈PR → 自动测试 → 安全扫描 → 审批 → Apply → 监控

L4→L5 关键差距

L4 (CI/CD 集成):
  PR → terraform plan → 人工 review → apply
  缺少:安全扫描、自动化测试、策略合规检查

L5 (全自动化):
  PR → terraform plan
     → tfsec 安全扫描 (Policy as Code)
     → terratest 自动化测试 (Testing)
     → OPA 合规检查 (Policy as Code)
     → 人工审批 (高敏感环境)
     → terraform apply
     → 监控告警自动验证

Policy as Code

为什么需要

graph LR
    subgraph "无 Policy as Code"
        A1["工程师写 TF"] --> A2["Plan 输出"] --> A3["人工 review"] --> A4["Apply"]
        A4 -.-> A5["❌ S3 bucket 公开访问<br/>❌ 安全组开放 0.0.0.0/0<br/>❌ 无加密的 RDS"]
    end

    subgraph "有 Policy as Code"
        B1["工程师写 TF"] --> B2["Plan 输出"] --> B3["tfsec 扫描"]
        B3 --> B4{"合规?"}
        B4 -->|否| B5["❌ 阻断 PR<br/>指出违规项"]
        B4 -->|是| B6["人工 review"] --> B7["Apply"]
    end

工具选型

工具扫描对象集成方式特点
tfsecTerraform HCLCLI / CI/CD专用、速度快、规则丰富(300+)
CheckovTF / CFN / K8s / HelmCLI / CI/CD多框架支持、可自定义策略
OPA / Conftest通用(JSON/YAML/HCL)独立服务 / CI/CD通用策略引擎、Rego DSL
SentinelTerraformTF Cloud / EnterpriseHashiCorp 官方、嵌入式

tfsec 实战

# 安装
brew install tfsec

# 扫描当前目录
tfsec .

# 扫描指定目录并输出 JSON
tfsec ./terraform/ --format json --out tfsec-report.json

# 只扫描特定检查
tfsec . --include-tests aws-s3-no-public-buckets,aws-security-group-no-public-ingress

# 在 CI 中使用(GitHub Actions)
tfsec . --format sarif --out tfsec.sarif
# 上传到 GitHub Security tab

OPA 策略示例

# policy.rego — 禁止 S3 bucket 公开访问
package terraform.s3

deny[msg] {
    resource := input.resource.aws_s3_bucket[name]
    resource.acl == "public-read"
    msg := sprintf("S3 bucket '%s' has public-read ACL", [name])
}

deny[msg] {
    resource := input.resource.aws_s3_bucket_public_access_block[name]
    not resource.block_public_acls
    msg := sprintf("S3 bucket '%s' does not block public ACLs", [name])
}

# 禁止安全组开放 0.0.0.0/0 入站
deny[msg] {
    resource := input.resource.aws_security_group[name]
    ingress := resource.ingress[_]
    ingress.cidr_blocks[_] == "0.0.0.0/0"
    ingress.from_port == 22
    msg := sprintf("Security group '%s' allows SSH from 0.0.0.0/0", [name])
}
# 使用 conftest 检查 Terraform Plan
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
conftest test plan.json --policy policy.rego

IaC 测试策略

测试金字塔

                    ┌─────────┐
                    │ E2E 测试 │  ← Terratest: 创建真实资源 → 验证 → 销毁
                    │  (少)    │     成本高、慢、最真实
                    ├─────────┤
                    │ 集成测试 │  ← kitchen-terraform: converge + verify
                    │ (中)    │     中等成本、验证收敛性
                    ├─────────┤
                    │ 单元测试 │  ← terraform validate + fmt + 自定义
                    │  (多)    │     快、免费、基础检查
                    └─────────┘

Terratest 实战

// terraform_test.go — 用 Go 测试 Terraform 模块
package test

import (
    "testing"
    "time"

    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/gruntwork-io/terratest/modules/http-helper"
)

func TestVPCModule(t *testing.T) {
    terraformOptions := &terraform.Options{
        // 模块路径
        TerraformDir: "../modules/vpc",

        // 变量
        Vars: map[string]interface{}{
            "name":          "terratest-vpc",
            "cidr_block":    "10.50.0.0/16",
            "environment":   "test",
        },

        // 测试结束后自动销毁
        NoColor: true,
    }

    // 延迟销毁(即使测试失败也执行)
    defer terraform.Destroy(t, terraformOptions)

    // init + apply
    terraform.InitAndApply(t, terraformOptions)

    // 验证输出
    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    if vpcId == "" {
        t.Fatal("VPC ID should not be empty")
    }

    // 验证 VPC CIDR
    vpcCidr := terraform.Output(t, terraformOptions, "vpc_cidr")
    if vpcCidr != "10.50.0.0/16" {
        t.Fatalf("Expected CIDR 10.50.0.0/16, got %s", vpcCidr)
    }
}

// 测试 Web 服务器是否正常响应
func TestWebServer(t *testing.T) {
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../examples/web-server",
    })

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

    // 获取实例公网 IP
    publicIp := terraform.Output(t, terraformOptions, "public_ip")
    url := fmt.Sprintf("http://%s:8080/health", publicIp)

    // 重试验证 HTTP 200(等实例启动)
    http_helper.HttpGetWithRetryWithCustomValidation(
        t, url, nil, 30, 5*time.Second,
        func(status int, body string) bool {
            return status == 200 && strings.Contains(body, "healthy")
        },
    )
}

CI/CD 中的 IaC 测试流水线

# .github/workflows/terraform-ci.yml
name: Terraform CI Pipeline

on:
  pull_request:
    paths: ["terraform/**"]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform fmt -check -recursive
      - run: terraform validate

  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: tfsec
        uses: aquasecurity/tfsec-pr-commenter-action@v1.2.0
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

  policy-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Conftest
        run: |
          curl -sL https://github.com/open-policy-agent/conftest/releases/download/v0.45.0/conftest_0.45.0_Linux_x86_64.tar.gz | tar xz
          terraform init
          terraform plan -out=tfplan
          terraform show -json tfplan > plan.json
          ./conftest test plan.json --policy policies/

  plan:
    needs: [lint, security-scan, policy-check]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan -no-color 2>&1 | tee plan-output.txt
      - name: Comment plan
        uses: actions/github-script@v7
        with:
          script: |
            const output = require('fs').readFileSync('plan-output.txt', 'utf8');
            github.rest.issues.createComment({
              ...context.repo,
              issue_number: context.issue.number,
              body: `## Terraform Plan\n\`\`\`\n${output}\n\`\`\``
            });

  # Terratest 只在合并到 main 后运行(成本高)
  e2e-test:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: plan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
      - name: Run Terratest
        run: |
          cd test && go test -v -timeout 30m

IaC 与 GitOps 的关系

graph TB
    subgraph "IaC + GitOps 协作模型"
        Dev["开发者"] -->|"git push"| Git["Git 仓库"]

        Git -->|"PR Trigger"| CI["CI 流水线"]
        CI -->|"fmt + validate"| L1["Lint 检查"]
        CI -->|"tfsec + OPA"| L2["安全策略扫描"]
        CI -->|"terratest"| L3["E2E 测试"]
        CI -->|"terraform plan"| L4["Plan 预览"]

        L4 -->|"人工审批"| Apply["terraform apply"]
        Apply --> Cloud["云基础设施"]

        Git -->|"Webhook"| ArgoCD["ArgoCD"]
        ArgoCD -->|"sync"| K8s["K8s 集群"]
    end

    style Git fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

分工边界

层级工具管理对象触发方式
基础设施层TerraformVPC、子网、K8s 集群、RDS、LBPR → Plan → 审批 → Apply
平台层ArgoCD (GitOps)K8s 内 Deployment/Service/IngressGit Push → 自动 Sync
配置层AnsibleOS 级配置(包安装、服务管理)Packer 构建时 / 手动触发
镜像层Packer不可变机器镜像CI 定时 / 手动触发

IaC 最佳实践

1. 目录结构与模块化

infrastructure/
├── modules/                    # 可复用模块(不绑定环境)
│   ├── vpc/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   ├── eks/
│   └── rds/
├── environments/               # 环境配置(引用模块)
│   ├── prod/
│   │   ├── backend.tf          # 远程 State
│   │   ├── providers.tf
│   │   ├── vpc.tf              # module "vpc" { source = "../../modules/vpc" }
│   │   ├── eks.tf
│   │   └── prod.tfvars
│   ├── staging/
│   └── dev/
├── policies/                   # OPA / tfsec 策略
│   └── *.rego
├── test/                       # Terratest 测试
│   └── *_test.go
└── atlantis.yaml               # Atlantis 配置

2. State 安全

实践说明
远程 State + 加密S3 server-side encryption / GCS CMEK
State 锁DynamoDB / GCS native lock
State 版本控制S3 versioning / GCS object versioning
Secret 不入 Statesensitive = true + 外部 Secret Manager
定期 State 备份每日复制到独立备份 bucket
State 拆分按环境 + 按资源类型拆分,避免单体 State

3. 代码质量

# pre-commit hooks(.pre-commit-config.yaml)
repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.89.1
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tflint          # 最佳实践 lint
      - id: terraform_tfsec           # 安全扫描
      - id: terraform_docs            # 自动生成 README

4. 变更安全

实践说明
强制 Plan禁止 apply -auto-approve(生产环境)
审批门GitHub Environment / Atlantis apply_requirements
冻结窗口变更冻结期禁止 apply(参见 变更管理全流程
回滚预案terraform plan -destroy 预览销毁,确认后执行
Drift 检测定期 terraform plan -detailed-exitcode 检测配置漂移

IaC 漂移检测与管理

配置漂移 (Drift) 的来源

Drift = 实际基础设施状态 ≠ IaC 代码描述的状态

来源:
  1. 手动操作 — 有人 SSH 登录改了配置 / 控制台点了修改
  2. 云厂商变更 — 自动补丁、维护窗口修改了实例类型
  3. 第三方工具 — 监控 Agent 自动安装了额外组件
  4. 紧急修复 — 线上故障时手动改了安全组规则

Drift 检测自动化

#!/usr/bin/env python3
"""定期检测 Terraform State 漂移"""
import subprocess
import json
import sys
from dataclasses import dataclass
from typing import Optional

@dataclass
class DriftResult:
    workspace: str
    has_drift: bool
    changed_resources: list[str]
    summary: str

def check_drift(workspace_dir: str, workspace: str) -> DriftResult:
    """运行 terraform plan 检测漂移"""
    try:
        result = subprocess.run(
            ["terraform", "plan", "-detailed-exitcode", "-no-color"],
            cwd=workspace_dir,
            capture_output=True,
            text=True,
            timeout=600,
        )
        # exit code 0 = no changes, 2 = changes detected, 1 = error
        has_drift = result.returncode == 2
        changed = []
        if has_drift:
            for line in result.stdout.splitlines():
                if line.startswith("  # ") and "will be" in line:
                    changed.append(line.strip())
        return DriftResult(
            workspace=workspace,
            has_drift=has_drift,
            changed_resources=changed,
            summary=result.stdout[-500:] if has_drift else "No drift detected",
        )
    except subprocess.TimeoutExpired:
        return DriftResult(workspace, False, [], "Plan timed out")
    except Exception as e:
        return DriftResult(workspace, False, [], f"Error: {e}")

def run_drift_check(workspaces: dict[str, str]) -> list[DriftResult]:
    """批量检测多个 workspace 的漂移"""
    results = []
    for ws_name, ws_dir in workspaces.items():
        r = check_drift(ws_dir, ws_name)
        results.append(r)
        status = "⚠️ DRIFT" if r.has_drift else "✅ OK"
        print(f"[{status}] {ws_name}: {len(r.changed_resources)} resources changed")
        for res in r.changed_resources[:5]:
            print(f"  {res}")
    return results

if __name__ == "__main__":
    workspaces = {
        "prod-vpc": "environments/prod/vpc",
        "prod-eks": "environments/prod/eks",
        "prod-rds": "environments/prod/rds",
    }
    results = run_drift_check(workspaces)
    drift_count = sum(1 for r in results if r.has_drift)
    if drift_count > 0:
        print(f"\n⚠️ {drift_count} workspace(s) have drift!")
        sys.exit(1)
    else:
        print(f"\n✅ All {len(results)} workspaces are in sync.")

关联知识

参考资源

学习路线

阶段笔记重点
1Terraform 基础设施即代码HCL 语法、State、Module、基础工作流
2Terraform 生产级实践State 管理、Atlantis、Terragrunt、Import
3IaC 方法论与实践总览(本文)工具全景、Policy as Code、测试、漂移检测
4Ansible 配置管理实战配置管理、Playbook、Role、GPU 服务器配置
5Packer 不可变镜像构建不可变镜像、多云构建、CI/CD 集成

状态: ✅ 已完成 学习时间: 2026-08-03