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 工具对比
| 工具 | 语言 | 多云支持 | 状态管理 | 适用场景 | 学习曲线 |
|---|
| Terraform | HCL (DSL) | ✅ 200+ Provider | State 文件 | 通用多云 IaC 标准选择 | 中 |
| Pulumi | TS/Go/Python/JS | ✅ 复用 TF Provider | State 服务 | 团队有编程能力、需要逻辑复用 | 中高 |
| CloudFormation | JSON/YAML | ❌ AWS only | 服务端管理 | 纯 AWS 环境、无第三方依赖 | 低 |
| Crossplane | YAML (K8s CRD) | ✅ 多云 Provider | etcd | K8s 原生团队、GitOps 统一管理 | 中高 |
| CDK | TS/Python/Java | ❌ AWS only | → CFN | AWS + 编程语言偏好 | 中 |
选型建议:多云环境选 Terraform(生态最成熟);K8s 原生团队考虑 Crossplane(用 kubectl 管基础设施);团队编程能力强选 Pulumi。
Configuration Management 对比
| 工具 | 架构 | 协议 | 优势 | 劣势 |
|---|
| Ansible | Agentless | SSH/WinRM | 零部署成本、学习曲线低、Playbook 可读性好 | 大规模(1000+ 节点)较慢 |
| SaltStack | Agent + Master | ZeroMQ | 事件驱动、速度快、实时执行 | 需部署 Agent、架构复杂 |
| Chef | Agent + Server | HTTPS | DSL 灵活、社区成熟 | Ruby 依赖、学习曲线高 |
| Puppet | Agent + Master | HTTPS | 模型驱动、合规检查强 | DSL 学习曲线高 |
选型建议:SRE 团队首选 Ansible(SSH 即可用,与 Terraform 天然互补);超大规模集群可评估 SaltStack。
IaC 成熟度模型
| 级别 | 名称 | 特征 | 典型表现 |
|---|
| L1 | 手动操作 | 点击控制台、SSH 逐台配置 | ”上次怎么配的?” → 翻聊天记录 |
| L2 | 脚本化 | Shell/Python 脚本批量操作 | 有脚本但无版本管理、无幂等保证 |
| L3 | IaC 工具 | Terraform/Ansible 管理基础设施 | 代码版本化、有 State、可 Plan |
| L4 | CI/CD 集成 | PR 触发 Plan、审批后 Apply | Atlantis/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
工具选型
| 工具 | 扫描对象 | 集成方式 | 特点 |
|---|
| tfsec | Terraform HCL | CLI / CI/CD | 专用、速度快、规则丰富(300+) |
| Checkov | TF / CFN / K8s / Helm | CLI / CI/CD | 多框架支持、可自定义策略 |
| OPA / Conftest | 通用(JSON/YAML/HCL) | 独立服务 / CI/CD | 通用策略引擎、Rego DSL |
| Sentinel | Terraform | TF Cloud / Enterprise | HashiCorp 官方、嵌入式 |
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
分工边界:
| 层级 | 工具 | 管理对象 | 触发方式 |
|---|
| 基础设施层 | Terraform | VPC、子网、K8s 集群、RDS、LB | PR → Plan → 审批 → Apply |
| 平台层 | ArgoCD (GitOps) | K8s 内 Deployment/Service/Ingress | Git Push → 自动 Sync |
| 配置层 | Ansible | OS 级配置(包安装、服务管理) | 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 不入 State | 用 sensitive = 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.")
关联知识
参考资源
学习路线
状态: ✅ 已完成
学习时间: 2026-08-03