Sägetstrasse 18, 3123 Belp, Switzerland +41 79 173 36 84 info@ict.technology

    Terraform @ Scale - 第1d部分:多租户环境中的陷阱与最佳实践

    Remote States 是一种强大的工具,可用于在团队与租户之间有控制地传递信息。尤其是在具有多个责任领域的复杂云环境中,它能够实现透明性、可重用性和可扩展性。然而,它同时也带来了风险:错误的状态、访问问题以及未解决的依赖关系可能会危及整个基础架构的稳定性。本文将介绍如何避免这些挑战,并通过清晰的结构和经过验证的实践方法,为可靠的自动化基础架构打下坚实基础。

    多租户环境中的 State-Locking

    在一个有多个团队同时操作基础架构不同部分的环境中,State-Locking 是不可或缺的。如果没有锁机制,多个 Terraform 实例可能会同时尝试更新同一个 State,并相互覆盖。这几乎总是会导致灾难性的后果。因此,必须有一个机制,能够在某个用户完成写操作之前,独占地保留一个 Statefile,并在完成后释放供其他进程访问。

    State-Locking 的最佳实践

    • 选择具备健壮锁机制的后端:

      如本系列文章的前一部分所述,除单用户环境中使用本地文件外,Consul 作为 Remote Backend 以及 Terraform Cloud 和 Enterprise 是目前唯一被支持并官方认证的 Statefile 后端。Terraform 虽然也支持如 HTTPS 或 S3 等其他后端,但这些并不包含在支持协议中,使用风险自负,并且并非所有此类后端都支持健壮且无误的锁机制。相比之下,Consul 和 Terraform Enterprise 提供了企业级别的可靠锁机制。

      在使用 Consul 时,Terraform 会自动设置一个会话,以在 plan 和 apply 过程中保护 state。


      terraform {
        backend "consul" {
          address = "consul.example.com:8500"
          path    = "terraform/customer-a"
          lock    = true  # 显式启用锁机制
        }
      }

      因此,此锁机制无需手动设置会话或其他机制 - Terraform 会自动处理。

    • 识别并清除孤立锁:

      孤立锁通常出现在 Terraform 进程意外中断的情况下(例如因 CI/CD 中断、网络故障或用户终止操作)。

      为自动清除孤立锁,建议实施预防性清理流程。在 Terraform 运行前执行一次简单检查,通常就足够了:


       

      consul lock -delete "terraform/customer-a" || true


      此命令可在新的 Terraform 进程启动前清除已有锁 - 高效且无额外管理负担。

    • 配置自动锁超时:在 Consul 配置文件(consul.hcl)中,可以定义锁会话的超时值,以确保被锁定的资源在过长时间未释放时自动清除。


      推荐用于生产环境的超时设置为:


      session_ttl_min = "15m"
      session_ttl_max = "1h"

      这样可避免孤立锁长期占用资源。

    • 在 CI/CD 流水线中考虑锁冲突:

      在 CI/CD 环境中,当多个流水线同时尝试设置相同的锁时,可能会发生冲突。

      一个经过验证的模式是结合 重试机制回退 策略:


      for i in {1..5}; do
        terraform apply && break
        echo "检测到锁冲突。$((i * 10)) 秒后重试..."
        sleep $((i * 10))
      done

      此机制确保即使遇到短暂锁定问题,部署过程仍能保持稳定。脚本的具体逻辑说明如下:

      1. 循环执行五次:(for i in {1..5})
      2. 每次尝试执行 terraform apply
      3. 如果 terraform apply 成功执行(&&),使用 break 跳出循环
      4. 如果执行失败,则等待一段时间(sleep)后重试
      5. 每次等待时间递增($((i * 10))),依次为 10、20、30、40 和 50 秒


      如此一来,当遇到诸如网络问题、API 限额或资源冲突等临时性问题导致一次尝试失败时,Terraform 会自动重新执行部署。

      一个健壮的锁机制能够显著降低错误风险,并防止 Terraform 进程之间相互阻塞。通过上述措施,您可以有效保障环境的稳定性,避免孤立锁带来的问题和不必要的延迟。

    State-Versioning

    健全的版本控制机制是不可或缺的。一旦出现问题,您可以轻松回退到先前的版本。Terraform Cloud/Enterprise 默认提供此功能;而在使用 Consul 时,您需要额外实现备份机制(如 Snapshots)。

    处理 State 依赖关系与循环依赖的避免

    States 之间的依赖关系可能迅速变得复杂,最糟糕的情况下甚至会形成循环依赖,导致基础架构无法更新。

    由于 terraform_remote_state 中的 Remote State 依赖是静态的,因此每当相关的 Outputs 发生变化时,所有依赖配置都必须手动更新。

    避免循环依赖的最佳实践:

    • 采用层级依赖结构: 全局资源区域资源租户级资源应用级资源。这种清晰的单向依赖路径能够有效防止循环出现。
    • 使用间接引用: 如果直接引用会造成循环,通过中间层传递信息可避免此问题:
      # 避免 A → C 和 C → A 的直接循环引用:
      # A → B → C(B 作为中介)
      # 在配置 B 中:
      output "information_from_a" {
      value = data.terraform_remote_state.a.outputs.needed_value
      }




      # 在配置 C 中:
      data "terraform_remote_state" "b" {
      # ...
      }
      local {
      value_from_a = data.terraform_remote_state.b.outputs.information_from_a
      }

    • 使用 Data Sources 替代 Remote State: 若条件允许,请优先使用原生 Data Sources。其更具动态性,可减少 State 之间的耦合度。

      # 原用法:
      data "terraform_remote_state" "network" {
        # ...
      }
      
      # 更优方式(如可行):
      data "oci_core_subnet" "app_subnet" {
        subnet_id = "ocid1.subnet.oc1..."
      }

      不过需要注意:应有策略性地使用 Data Sources,并避免在 for_each 或其他循环结构中调用它们,因为每次执行都会触发 API 请求,扩展性较差。因此,在基础模块中使用 Data Sources 通常并不可取。而被 Root 模块调用的模块也不应直接访问 Statefiles。建议将 Data Sources 的调用也放在 Root 模块层级。

    最小化 State 信息以提升性能

    State 文件越大,每次执行 terraform plan 所需的时间就越长。Terraform 会加载整个 State,而在复杂环境中,这可能会显著降低执行效率。

    State 优化的最佳实践:

    • 细粒度划分 State: 一个良好的经验值是:每个 State 不应包含超过 100 至 250 个资源,以保证可接受的计划执行时间。请参考 Goldilocks-Principle(黄金中庸原则)(我们将在本系列后续文章中进一步探讨)。
    • 谨慎使用复杂 Outputs: 将 Outputs 限制在必要且核心的信息上:

      # 不推荐的方式:
      output "entire_vcn" {
        value = oci_core_vcn.main
      }
      
      # 更优方式:
      output "vcn_essential_info" {
        value = {
          id         = oci_core_vcn.main.id
          cidr_block = oci_core_vcn.main.cidr_block
        }
      }

    • 避免不必要的敏感 Outputs: 敏感 Outputs 会增大 State 文件,并导致 Terraform 存储额外的元数据。而且它们无法在其他模块中引用,因此通常可以省略。

    复杂 State 依赖的调试技巧

    调试 Remote State 问题可能相当棘手。以下是一些建议:

    • 启用详细日志:

      export TF_LOG=DEBUG
      export TF_LOG_PATH=./terraform.log

    • 在 CI/CD 中集成 State 校验:

      terraform state pull | jq '.outputs.network_config.value | has("vcn_id")'

    • 使用 terraform console:

      $ terraform console
      > data.terraform_remote_state.network.outputs.subnet_ids

    复杂场景中的 Provider-Aliasing

    在涉及多个账户(或同一账户中多个区域)时,使用 Provider-Aliasing 可显著简化配置:


    provider "oci" {
      alias  = "global"
      region = "eu-frankfurt-1"
    }
    
    provider "oci" {
      alias  = "customer_a"
      region = "eu-amsterdam-1"
    }
    
    module "customer_a_instance" {
      source     = "./modules/instance"
      provider   = oci.customer_a
      subnet_id  = module.global.outputs.subnet_id
    }

    总结

    掌握上述最佳实践,有助于您稳定高效地运营多租户基础架构。随着经验的积累,您将逐步认识到这些模式不仅能有效规避问题,还能改善团队间的协作方式,并整体提升基础架构的质量。