多云部署编排与基础设施漂移检测

系统讲解用 GitHub Actions 编排多云部署与基础设施漂移检测的工程实践,涵盖统一编排抽象、AWS/GCP/Azure 的 OIDC 身份联邦差异、fan-out/fan-in 并行策略、terraform plan 漂移检测、状态锁一致性与部分失败回滚。

当一家公司同时使用 AWS 做主要业务、GCP 做数据分析、Azure 做企业集成时,“部署"就不再是一条线性流水线,而是一张有依赖关系的有向图:数据库要先于应用、主区域要先于从区域、网络层要先于计算层。更难的是,基础设施会被人在控制台上"手动改一下”,导致声明式配置(IaC)与真实状态之间产生漂移(drift)——而漂移如果不被检测,下一次 apply 就会把这些手改覆盖掉,或者产生冲突。

本文聚焦两个问题:如何用 GitHub Actions 编排跨云的多目标部署,以及如何把漂移检测变成一条持续运行的流水线。

一、多云部署的复杂度来源

1.1 三块复杂度

复杂度具体表现
身份每个云有自己的联合身份机制
状态每个云/区域有独立的 state 与锁
编排依赖顺序、并行度、部分失败处理

把这三块分开治理,是多云编排能被维护的前提。

1.2 不要试图"统一一切"

多云编排里最常见的反模式,是追求一个"万能抽象层"来抹平三家云的差异。结果是抽象层越来越厚、越来越难调试。务实的做法是:只统一编排层(workflow 的 job 图),不统一实现层(每个云用各自原生工具)。

统一编排层:GitHub Actions job 图(needs / matrix / environment)
        │
        ├── AWS   → terraform + aws-cli + OIDC(assume-role)
        ├── GCP   → terraform + gcloud   + OIDC(WIF)
        └── Azure → terraform + az-cli   + OIDC(federated credential)

二、统一编排的抽象

2.1 用矩阵表达"多目标"

多云部署最自然的抽象是矩阵:把"云 + 区域 + 环境"展开成组合。

jobs:
  deploy:
    strategy:
      fail-fast: false
      matrix:
        include:
          - cloud: aws
            region: us-east-1
            env: prod
          - cloud: aws
            region: eu-west-1
            env: prod
          - cloud: gcp
            region: us-central1
            env: prod
          - cloud: azure
            region: eastus
            env: prod
    runs-on: ubuntu-latest
    environment: ${{ matrix.env }}-${{ matrix.cloud }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: ./scripts/deploy.sh --cloud=${{ matrix.cloud }} --region=${{ matrix.region }}

fail-fast: false 让一个云失败不取消其他云——多云场景下,你能接受"AWS 成功、GCP 失败",但不能接受"GCP 失败导致 AWS 被取消在中间状态"。

2.2 用 reusable workflow 收敛差异

把每个云的部署逻辑封装成可复用工作流,调用方只传参数:

# .github/workflows/deploy-aws.yml
on:
  workflow_call:
    inputs:
      region:
        required: true
        type: string
      env:
        required: true
        type: string
jobs:
  apply:
    runs-on: ubuntu-latest
    environment: ${{ inputs.env }}-aws
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/gha-deploy
          aws-region: ${{ inputs.region }}
      - run: terraform -chdir=infra/aws apply -auto-approve

调用方:

jobs:
  aws:
    uses: ./.github/workflows/deploy-aws.yml
    with:
      region: us-east-1
      env: prod

可复用工作流的价值在于把"每个云各写一遍"的重复逻辑收敛到一处,调用方只关心参数。

2.3 用 environment 隔离权限

每个云 + 环境组合对应一个 GitHub Environment,在其中配置该环境的密钥与审批规则:

prod-aws    → AWS OIDC role + 需要 2 人审批
prod-gcp    → GCP WIF + 需要审批
staging-aws → AWS OIDC role + 无需审批

这样"权限"与"审批"都绑定在 environment 上,workflow 本身不持有任何长期密钥。环境门禁的机制可参考 /github-actions-environment-gates/。

三、OIDC 与多云端身份联邦

3.1 为什么必须用 OIDC

长期密钥(AWS Access Key、GCP Service Account JSON)一旦泄漏就是灾难,且轮换麻烦。OIDC 让 GitHub 在运行时签发一个短期令牌,云厂商验证该令牌的签发者与声明后,换发临时凭证。核心前提是:信任策略必须收紧到 repo 与分支。

3.2 三家云的差异

维度AWSGCPAzure
机制名OIDC + AssumeRoleWithWebIdentityWorkload Identity FederationFederated Credentials
主体标识repo:org/repo:refattribute.repositoryrepo:org/repo:ref
配置位置IAM Role 信任策略Workload Identity PoolApp Registration
认证 Actionaws-actions/configure-aws-credentialsgoogle-github-actions/authazure/login

3.3 AWS 配置

信任策略里必须精确限制 sub:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
      },
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
      }
    }
  }]
}

StringLike 里的 ref:refs/heads/main 是安全关键——如果写成 repo:my-org/my-repo:*,那么任何分支(包括攻击者的 PR 分支)都能扮演该角色。

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
    aws-region: us-east-1

3.4 GCP 配置

- uses: google-github-actions/auth@v2
  with:
    workload_identity_provider: projects/123/locations/global/workloadIdentityPools/gha/providers/github
    service_account: gha-deploy@my-project.iam.gserviceaccount.com

GCP 的 WIF 需要配置 attribute.repository 的映射,把 GitHub 的 claim 映射为 GCP 的属性,再据此授予权限。

3.5 Azure 配置

- uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

注意:Azure 的 client-id 等本身不是密钥(它们不是机密),真正的机密是联邦凭证的绑定关系——即该 App 只信任来自指定 repo/分支的令牌。更系统的 OIDC 跨云认证可参考 /github-actions-oidc-cloud-auth/。

四、并行与串行编排

4.1 fan-out / fan-in

多云部署的典型形状是"先并行铺开,再汇总验证":

validate(串行,先做 plan 审阅)
        │
        ├────────┬────────┬────────┐
        ▼        ▼        ▼        ▼
      aws-eu   aws-us   gcp-us  azure-eu   ← fan-out 并行
        │        │        │        │
        └────────┴────────┴────────┘
                    │
                    ▼
            verify(fan-in 汇总)        ← 统一冒烟测试
jobs:
  validate:
    runs-on: ubuntu-latest
    steps: [...]

  deploy:
    needs: validate
    strategy:
      fail-fast: false
      matrix: { ... }
    steps: [...]

  verify:
    needs: deploy
    if: always()
    runs-on: ubuntu-latest
    steps:
      - run: ./scripts/smoke-test-all-regions.sh

verify 用 if: always() 保证即使部分部署失败,也会执行汇总验证并给出完整状态。

4.2 依赖顺序

有依赖关系的部署用 needs 表达:

jobs:
  network:
    runs-on: ubuntu-latest
    steps: [ ... ]        # 先建 VPC / 子网

  database:
    needs: network
    runs-on: ubuntu-latest
    steps: [ ... ]        # 再建数据库

  app:
    needs: database
    strategy:
      matrix: { ... }
    steps: [ ... ]        # 最后部署应用

4.3 并发控制

同环境的部署不能并发,否则 state 锁会冲突:

concurrency:
  group: deploy-prod-aws
  cancel-in-progress: false

cancel-in-progress: false 对 IaC 部署尤其重要——中途取消可能留下"资源建了一半"的状态。

4.4 金丝雀与分批

跨区域发布可以分批:先发一个区域观察,再发其余。

jobs:
  canary:
    environment: prod-aws-canary
    steps: [ ... ]        # 只发 us-east-1

  rollout:
    needs: canary
    strategy:
      matrix:
        region: [eu-west-1, ap-southeast-1]
    steps: [ ... ]        # 观察通过后再发其余

五、基础设施漂移检测

5.1 什么是漂移

漂移指真实基础设施状态与IaC 声明的期望状态不一致。来源通常是:有人在控制台手改了安全组、有人手动扩容了实例、或某个自动化脚本绕过 IaC 改了资源。

漂移的危险在于它让 IaC 不再可信:下次 apply 要么覆盖手改(可能造成故障),要么与之冲突。

5.2 用 plan 检测漂移

Terraform 的 plan 天然就是漂移检测器:如果配置没变但 plan 显示有变更,说明发生了漂移。

terraform plan -detailed-exitcode -out=tfplan
# exit code: 0=无变更  1=出错  2=有变更(含漂移)

-detailed-exitcode 是关键,它让 CI 能区分"无变更"与"有变更":

- name: Drift check
  id: plan
  run: |
    set +e
    terraform plan -detailed-exitcode -out=tfplan
    code=$?
    set -e
    echo "exitcode=$code" >> "$GITHUB_OUTPUT"
    if [ "$code" -eq 2 ]; then
      echo "::warning::检测到漂移或待应用的变更"
    elif [ "$code" -eq 1 ]; then
      echo "::error::plan 执行失败"; exit 1
    fi

5.3 定时漂移巡检

把漂移检测做成定时任务,而不是只在部署时才发现:

name: Drift Detection

on:
  schedule:
    - cron: "0 6 * * *"   # 每日 06:00 UTC
  workflow_dispatch:

jobs:
  drift:
    strategy:
      fail-fast: false
      matrix:
        target: [aws-prod, gcp-prod, azure-prod]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/drift-check.sh ${{ matrix.target }}
      - name: Report drift
        if: steps.check.outputs.drift == 'true'
        run: |
          gh issue create \
            --title "基础设施漂移: ${{ matrix.target }}" \
            --body "定时巡检发现 ${{ matrix.target }} 存在漂移,请核查。" \
            --label "infra,drift"

把漂移结果落成 issue,而不是直接自动 apply 修复——漂移修复必须人工确认,因为漂移背后可能是有人为了救火而做的紧急变更。

5.4 漂移的三种处置

处置适用风险
接受并回写配置变更合理需更新 IaC 代码
用 apply 覆盖变更是误操作可能中断业务
隔离并调查来源不明需人工介入

无论哪种,都不应该在无人知晓的情况下自动覆盖。

5.5 多云漂移的聚合

多云环境下,每个云的目标单独检测,最后聚合:

drift(aws-prod) ─┐
drift(gcp-prod) ─┼─► drift-summary(聚合,生成统一报告)
drift(azure-prod)─┘

聚合 job 负责把各云结果汇总成一张表,避免团队去翻三个云的控制台。

六、状态一致性与锁

6.1 每云独立 state

多云项目的 state 必须按云/区域切分,绝不能共用一个 state 文件:

infra/aws/us-east-1/  → s3://tf-state/aws/us-east-1/terraform.tfstate
infra/aws/eu-west-1/  → s3://tf-state/aws/eu-west-1/terraform.tfstate
infra/gcp/us-central1/→ gcs://tf-state/gcp/us-central1/default.tfstate

独立 state 的好处:一个区域的故障不会污染另一个区域的 state,plan/apply 可以完全并行。

6.2 锁与并发

后端锁防止两个 workflow 同时操作同一份 state:

terraform {
  backend "s3" {
    bucket         = "tf-state"
    key            = "aws/us-east-1/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-lock"
    encrypt        = true
  }
}

配合 workflow 的 concurrency,形成"CI 层 + 后端层"的双重防并发。状态与锁的更多细节可参考 Terraform 专题 。

6.3 部分失败的处置

多云部署最常见的故障是"部分成功":AWS 成功、GCP 失败。处置原则:

  • 不回滚已成功的云——回滚本身是高风险操作,可能引入新故障;
  • 让失败的云可重入——IaC 的 apply 应当幂等,重跑不会重复建资源;
  • 明确记录状态——在 issue 或 PR 中标注哪些云已成功、哪些待重试。

七、故障处理与回滚

7.1 幂等是前提

所有部署脚本必须是幂等的。terraform apply 天然幂等,但自定义脚本(如数据库迁移、缓存预热)往往不是。这类脚本要加"是否已执行"的判断。

7.2 回滚策略

层回滚方式
应用重新部署上一个镜像 tag
配置回退 IaC 代码并 apply
数据依赖备份,谨慎

数据层的回滚最难,因此数据库迁移要设计成向前兼容(先加列、后删列),而不是依赖回滚。

7.3 手动门禁

生产环境的多云部署应当在关键节点加人工审批:

environment:
  name: prod-aws
  # 在 GitHub Environment 设置中配置 required reviewers

审批发生在 apply 之前,而不是之后。审批人应当能看到 plan 输出,据此判断是否放行。

八、落地清单

  • 编排层统一、实现层各用原生工具;
  • 每个云+环境对应独立 GitHub Environment 与权限;
  • OIDC 信任策略收紧到具体 repo 与分支;
  • state 按云/区域切分,后端锁已启用;
  • concurrency 防止同环境并发部署;
  • 漂移检测定时运行,结果落成 issue 而非自动修复;
  • 部分失败不回滚、可重入、状态可见;
  • 生产 apply 前有人工审批。

总结

多云部署的难点不在"连上三个云",而在编排与一致性:用矩阵和 reusable workflow 统一编排层、用 OIDC 让每个云各自验证短期令牌、用独立 state 与后端锁保证并发安全、用 plan -detailed-exitcode 把漂移检测变成每日巡检。其中最容易被忽略的是漂移处置原则——检测可以自动化,修复必须人工确认,因为漂移背后往往藏着一次无人记录的手工救火。把 AWS 侧的落地细节做扎实之后,IaC 流水线的通用模式可进一步参考 /github-actions-iac-terraform/。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「github-actions」更多文章

  1. 文档站与静态站点发布流水线
  2. AI 代码审查与 PR 助手集成
  3. Actions Runner Controller 与 Kubernetes 自动扩缩