使用 Terraform 的最佳实践

本文档提供了在多个团队成员和工作流之间使用 Terraform 进行有效开发的准则和建议。

本指南未介绍 Terraform。如需了解如何将 Terraform 与 Google Cloud 搭配使用,请参阅 Terraform 使用入门

一般样式和结构准则

以下建议涵盖了 Terraform 配置的基本样式和结构。建议适用于可重复使用的 Terraform 模块和根配置。

遵循标准模块结构

  • Terraform 模块必须遵循标准模块结构
  • 使用 main.tf 文件启动每个模块,默认情况下,资源位于此文件中。
  • 在每个模块中,添加 Markdown 格式的 README.md 文件。在 README.md 文件中,添加有关模块的基本文档。
  • 将示例放在 examples/ 文件夹中,并为每个示例提供单独的子目录。对于每个示例,请添加详细的 README.md 文件。
  • 使用资源各自的文件和描述性名称(例如 network.tfinstances.tfloadbalancer.tf)创建资源的逻辑分组。
    • 避免为每个资源提供自己的文件。按共享用途对资源进行分组。例如,在 dns.tf 中组合使用 google_dns_managed_zonegoogle_dns_record_set
  • 在模块的根目录中,仅添加 Terraform (*.tf) 和代码库元数据文件(例如 README.mdCHANGELOG.md)。
  • 将任何其他文档放在 docs/ 子目录中。

采用命名惯例

  • 使用下划线命名所有配置对象,以分隔多个字词。这种做法可确保与资源类型、数据源类型和其他预定义值的命名惯例保持一致。此惯例不适用于名称参数

    推荐:

    resource "google_compute_instance" "web_server" {
      name = "web-server"
    }
    

    不推荐:

    resource "google_compute_instance" "web-server" {
      name = "web-server"
    }
    
  • 如需简化对唯一属于其类型的资源(例如,整个模块的单个负载均衡器)的引用,请将该资源命名为 main

    • 记住 some_google_resource.my_special_resource.idsome_google_resource.main.id 需要花费额外的精力。
  • 如需区分属于同一类型的资源(例如 primarysecondary),请提供有意义的资源名称。

  • 将资源名称设为单数形式。

  • 在资源名称中,请不要重复资源类型。例如:

    推荐:

    resource "google_compute_global_address" "main" { ... }
    

    不推荐:

    resource "google_compute_global_address" "main_global_address" { … }
    

谨慎使用变量

  • variables.tf 中声明所有变量。
  • 为变量提供与其用法或用途相关的描述性名称:
    • 表示数值的输入、局部变量和输出(例如磁盘大小或 RAM 大小)必须使用单位命名(例如 ram_size_gb)。Google Cloud API 没有标准单位,因此使用单位命名变量可让配置维护人员清楚地了解预期输入单位。
    • 对于存储单位,请使用二进制单位前缀(1024 的幂)- kibimebigibi。对于所有其他计量单位,请使用十进制单位前缀(1000 的幂)- kilomegagiga。此用法与 Google Cloud 中的用法一致。
    • 如需简化条件逻辑,请为布尔值变量提供正名称,例如 enable_external_access
  • 变量必须有说明。说明会自动包含在已发布模块的自动生成的文档中。说明为新开发者添加了描述性名称无法提供的其他背景信息。
  • 为变量提供定义的类型。
  • 在适当情况下,请提供默认值:
    • 对于具有与环境无关的值的变量(例如磁盘大小),请提供默认值。
    • 对于具有特定于环境的值的变量(例如 project_id),请勿提供默认值。这样,调用模块必须提供有意义的值。
  • 仅当变量保留为空是底层 API 不拒绝的有效偏好设置时才对变量(例如空字符串或列表)使用空默认值。
  • 请谨慎使用变量。参数化的值必须根据每个实例或环境而变化。在决定是否公开某个变量时,请确保您拥有更改该变量的具体用例。如果只有极少数可能需要变量,请不要公开该变量。
    • 添加具有默认值的变量是向后兼容的。
    • 移除变量是向后不兼容的。
    • 如果在多个位置重复使用字面量,您可以使用局部值,而无需将其作为变量公开。

公开输出

  • outputs.tf 文件中整理所有输出。
  • 为所有输出提供有意义的说明。
  • README.md 文件中记录输出说明。使用 terraform-docs 等工具在提交时自动生成说明。
  • 输出根模块可能需要引用或共享的所有有用值。特别是对于开源模块或频繁使用的模块,请公开所有可能消耗的输出。
  • 请勿直接通过输入变量传递输出,因为这样做会阻止输出正确地添加到依赖关系图中。为确保已创建隐式依赖项,请务必从资源中输出引用特性。传递特性,而不是直接引用实例的输入变量,如下所示:

    推荐:

    output "name" {
      description = "Name of instance"
      value       = google_compute_instance.main.name
    }
    

    不推荐:

    output "name" {
      description = "Name of instance"
      value       = var.name
    }
    

使用数据源

  • 数据源放在引用它们的资源旁边。例如,如果要提取要在启动实例中使用的映像,请将其与实例放在一起,而不是在各自的文件中收集数据资源。
  • 如果数据源数量很大,请考虑将它们移动到专用 data.tf 文件中。
  • 如需提取相对于当前环境的数据,请使用变量或资源插值类型

限制自定义脚本的使用

  • 仅在必要时使用脚本。Terraform 不会考虑或管理通过脚本创建的资源的状态。
    • 尽可能避免使用自定义脚本。仅在 Terraform 资源不支持所需行为时使用它们。
    • 使用的任何自定义脚本都必须有明确记录的存在原因,并且最好有弃用方案。
  • Terraform 可以通过预配工具(包括 local-exec 预配工具)调用自定义脚本。
  • 将 Terraform 调用的自定义脚本放在 scripts/ 目录中。

在单独的目录中添加辅助脚本

  • helpers/ 目录中整理非 Terraform 调用的辅助脚本。
  • README.md 文件中记录辅助脚本,其中包含说明和调用示例。
  • 如果辅助脚本接受参数,请提供参数检查和 --help 输出。

将静态文件放在单独的目录中

  • Terraform 引用但未执行(例如加载到 Compute Engine 实例上的启动脚本)的静态文件必须整理到 files/ 目录中。
  • 将冗长的 HereDoc 放在外部文件(独立于其 HCL)中。使用 file() 函数引用它们。
  • 对于使用 Terraform templatefile 函数读入的文件,请使用文件扩展名 .tftpl
    • 模板必须放在 templates/ 目录中。

保护有状态资源

对于有状态资源(例如数据库),请确保启用删除保护。例如:

resource "google_sql_database_instance" "main" {
  name = "primary-instance"
  settings {
    tier = "D0"
  }

  lifecycle {
    prevent_destroy = true
  }
}

使用内置格式设置

所有 Terraform 文件都必须符合 terraform fmt 的标准。

限制表达式的复杂性

  • 限制任何单个插值表达式的复杂性。如果单个表达式中需要许多函数,请考虑使用局部值将其拆分为多个表达式。
  • 一行中不能有多个三元运算。请改用多个本地值来构建逻辑。

为条件值使用 count

如需有条件地实例化资源,请使用 count 元参数。例如:

variable "readers" {
  description = "..."
  type        = list
  default     = []
}

resource "resource_type" "reference_name" {
  // Do not create this resource if the list of readers is empty.
  count = length(var.readers) == 0 ? 0 : 1
  ...
}

使用用户指定的变量为资源设置 count 变量时,请务必小心。如果为此类变量(如 project_id)提供了资源特性,但该资源尚不存在,则 Terraform 无法生成计划。相反,Terraform 会报告错误 value of count cannot be computed。在这种情况下,请使用单独的 enable_x 变量来计算条件逻辑。

为迭代资源使用 for_each

如果要根据输入资源创建资源的多个副本,请使用 for_each 元参数。

将模块发布到注册表

可重复使用的模块

对于设计为重复使用的模块,除了上述准则之外,还请使用以下准则。

激活模块中必需的 API

Terraform 模块可以使用 google_project_service 资源或 project_services 模块激活任何必需的服务。添加 API 激活可让演示更加简单。

  • 如果模块中添加了 API 激活,则通过公开默认为 trueenable_apis 变量,API 激活必须是可停用的。
  • 如果模块中添加了 API 激活,则 API 激活必须disable_services_on_destroy 设置为 false,因为此特性在使用模块的多个实例时可能会导致问题。

    例如:

    module "project-services" {
      source  = "terraform-google-modules/project-factory/google//modules/project_services"
      version = "~> 12.0"
    
      project_id  = var.project_id
      enable_apis = var.enable_apis
    
      activate_apis = [
        "compute.googleapis.com",
        "pubsub.googleapis.com",
      ]
      disable_services_on_destroy = false
    }
    

添加所有者文件

对于所有共享模块,请添加 OWNERS 文件(或 GitHub 上的 CODEOWNERS),并记录谁负责该模块。在合并任何拉取请求之前,所有者应对其进行批准。

发布标记的版本

有时,模块需要破坏性更改,并且您需要将效果传达给用户,以便他们可以将其配置固定到特定版本。

在标记或发布新版本时,请确保共享模块遵循 SemVer v2.0.0

引用模块时,请使用固定到主要版本的版本限制条件。例如:

module "gke" {
  source  = "terraform-google-modules/kubernetes-engine/google"
  version = "~> 20.0"
}

不要配置提供商或后端

共享模块不得配置提供商或后端。请改为在根模块中配置提供商和后端。

对于共享模块,请在 required_providers 块中定义所需的最低提供商版本,如下所示:

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 4.0.0"
    }
  }
}

除非另有证明,否则假设新的提供商版本正常运行。

将标签作为变量公开

允许通过模块界面灵活地标记资源。考虑提供一个默认值为空映射的 labels 变量,如下所示:

variable "labels" {
  description = "A map of labels to apply to contained resources."
  default     = {}
  type        = "map"
}

公开所有资源的输出

借助变量和输出,您可以推断模块与资源之间的依赖关系。如果没有任何输出,用户就无法根据 Terraform 配置对模块进行正确排序。

对于共享模块中定义的每项资源,请至少包含一个引用该资源的输出。

为复杂逻辑使用内嵌子模块

  • 借助内嵌模块,您可以将复杂的 Terraform 模块整理成较小的单元,并删除重复的通用资源。
  • 将内嵌模块放在 modules/$modulename 中。
  • 除非内嵌模块的文档另有明确说明,否则将内嵌模块视为私有,不能由外部模块使用。
  • Terraform 不会跟踪重构资源。如果先从顶级模块中的多个资源开始,然后再将这些资源推送到子模块,则 Terraform 会尝试重新创建所有重构资源。为了缓解此行为,请在重构时使用 moved 块。
  • 内部模块定义的输出不会自动公开。如需共享内部模块的输出,请重新导出这些模块。

Terraform 根模块

根配置(根模块)是您在其中运行 Terraform CLI 的工作目录。确保根配置遵循以下标准(以及适用的上述 Terraform 准则)。针对根模块的明确建议取代了一般准则。

最大限度地减少每个根模块中的资源数量

请务必防止单个根配置过大,同时还要防止同一目录和状态下存储的资源过多。每次运行 Terraform 时,特定根配置中的所有资源都会刷新。如果单个状态中包含的资源过多,则可能会导致执行缓慢。一般规则:在单个状态下,不要添加超过 100 个资源(最好不要超过十几个)。

为每个应用使用单独的目录

如需相互独立地管理应用和项目,请将每个应用和项目的资源放在其各自的 Terraform 目录中。服务可能表示特定应用或通用服务,例如共享网络。将特定服务的所有 Terraform 代码嵌套在一个目录(包括子目录)下。

将应用拆分为特定于环境的子目录

在 Google Cloud 中部署服务时,将服务的 Terraform 配置拆分为两个顶级目录:包含服务的实际配置的 modules 目录和包含每个环境的根配置的 environments 目录。

-- SERVICE-DIRECTORY/
   -- OWNERS
   -- modules/
      -- <service-name>/
         -- main.tf
         -- variables.tf
         -- outputs.tf
         -- provider.tf
         -- README
      -- ...other…
   -- environments/
      -- dev/
         -- backend.tf
         -- main.tf

      -- qa/
         -- backend.tf
         -- main.tf

      -- prod/
         -- backend.tf
         -- main.tf

使用环境目录

如需跨环境共享代码,请引用模块。通常,这可能是一个服务模块,其中包含服务的基本共享 Terraform 配置。在服务模块中,对常见输入进行硬编码,并且只需要特定于环境的输入作为变量。

每个环境目录必须包含以下文件:

  • backend.tf 文件,用于声明 Terraform 后端状态位置(通常为 Cloud Storage
  • main.tf 文件,用于实例化服务模块

每个环境目录(devqaprod)对应于默认的 Terraform 工作区,并会将服务版本部署到该环境。这些工作区将特定于环境的资源隔离到各自的上下文中。仅使用默认工作区

不建议在一个环境中使用多个 CLI 工作区,原因如下:

  • 检查每个工作区中的配置并非易事。
  • 不建议为多个工作区使用单个共享后端,因为如果将共享后端用于环境分隔,它会成为单点故障。
  • 虽然代码可以重复使用,但必须根据当前工作区变量进行切换(例如 terraform.workspace == "foo" ? this : that),这增加了读取代码的难度。

详情请参阅以下内容:

通过远程状态公开输出

确保您从根模块公开模块实例的有用输出。

例如,以下代码段传递项目工厂模块实例中的项目 ID 输出,将其作为根模块的输出。

# Project root module
terraform {
  backend "gcs" {
    bucket  = "BUCKET"
  }
}

module "project" {
  source  = "terraform-google-modules/project-factory/google"
  ...
}

output "project_id" {
  value       = module.project.project_id
  description = "The ID of the created project"
}

其他 Terraform 环境和应用只能引用根模块级输出。

通过使用远程状态,您可以引用根模块输出。若要允许其他依赖应用使用配置,请确保将与服务的端点相关的信息导出到远程状态。

# Networks root module
data "terraform_remote_state" "network_project" {
  backend = "gcs"

  config = {
    bucket = "BUCKET"
  }
}

module "vpc" {
  source  = "terraform-google-modules/network/google"
  version = "~> 9.0"

  project_id   = data.terraform_remote_state.network_project.outputs.project_id
  network_name = "vpc-1"
  ...
}

有时,例如,从环境目录调用共享服务模块时,重新导出整个子模块是最适当的操作,如下所示:

output "service" {
  value       = module.service
  description = "The service module outputs"
}

固定到次要提供商版本

在根模块中,声明每个提供商并固定到次要版本。这样可以自动升级升级到新的补丁版本,同时仍保持可靠的目标。 为了保持一致性,请将版本文件命名为 versions.tf

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0.0"
    }
  }
}

将变量存储在 tfvars 文件中

对于根模块,请使用 .tfvars 变量文件提供变量。为了保持一致性,请将变量文件命名为 terraform.tfvars

请勿使用替代 var-filesvar='key=val' 命令行选项指定变量。命令行选项是临时性的,容易忘记。使用默认变量文件更容易预测。

签入 .terraform.lock.hcl 文件

对于根模块,.terraform.lock.hcl 依赖项锁文件应签入源代码控制系统中。这有助于跟踪和审核指定配置的提供商选择变化。

跨配置通信

使用 Terraform 时出现的一个常见问题是如何跨不同 Terraform 配置(可能由不同的团队维护)共享信息。通常,配置之间可以共享信息,而无需信息存储在单个配置目录(甚至单个代码库)中。

在不同 Terraform 配置之间共享信息的推荐方法是使用远程状态来引用其他根模块。Cloud StorageTerraform Enterprise 是首选状态后端。

如需查询非 Terraform 管理的资源,请使用 Google 提供商的数据源。例如,您可以使用数据源检索默认的 Compute Engine 服务账号。请勿使用数据源查询由其他 Terraform 配置管理的资源。这样做可能会创建对资源名称和结构的隐式依赖,正常的 Terraform 操作可能会意外中断。

使用 Google Cloud 资源

使用 Terraform 预配 Google Cloud 资源的最佳实践已集成到 Google 维护的 Cloud Foundation Toolkit 模块中。本部分重述了其中一些最佳实践。

烘焙虚拟机映像

通常,我们建议您使用 Packer 等工具烘焙虚拟机映像。然后,Terraform 只需要使用预先专烘焙的映像启动机器。

如果预先烘焙的映像不可用,则 Terraform 可以将新的虚拟机移交给具有 provisioner 块的配置管理工具。我们建议您避免使用此方法,而仅将其用作最后的补救手段。如需清理与实例关联的旧状态,需要清理逻辑的预配工具应在 when = destroy 时使用 provisioner 块。

Terraform 应通过实例元数据为配置管理提供虚拟机配置信息。

管理 Identity and Access Management

在预配与 Terraform 的 IAM 关联时,可以使用几种不同的资源:

  • google_*_iam_policy(例如 google_project_iam_policy
  • google_*_iam_binding(例如 google_project_iam_binding
  • google_*_iam_member(例如 google_project_iam_member

google_*_iam_policygoogle_*_iam_binding 会创建权威 IAM 关联,其中 Terraform 资源用作可以分配给相关资源的权限的唯一可靠来源。

如果权限在 Terraform 之外更改,则 Terraform 在下次执行时会覆盖所有权限,以表示配置中定义的政策。这可能适用于完全由特定 Terraform 配置管理的资源,但这意味着由 Google Cloud 自动管理的角色会被移除,因而可能中断某些服务的功能。

为避免这种情况,我们建议直接使用 google_*_iam_member 资源或 来自 Google 的 IAM 模块

版本控制

与其他形式的代码一样,将基础架构代码存储在版本控制系统中以保留历史记录并允许轻松回滚。

使用默认分支策略

对于包含 Terraform 代码的所有代码库,请默认使用以下策略:

  • main 分支是主要开发分支,表示最新批准的代码。main 分支受到保护
  • 开发是在从 main 分支的功能和问题修复分支上进行的。
    • 将功能分支命名为 feature/$feature_name
    • 将问题修复分支命名为 fix/$bugfix_name
  • 完成功能或问题修复后,使用拉取请求将其合并回 main 分支。
  • 为防止合并冲突,请先合并分支,然后再进行变基。

将环境分支用于根配置

对于包含直接部署到 Google Cloud 的根配置的代码库,需要安全的发布策略。我们建议为每个环境使用单独的分支。因此,可以通过合并不同分支之间的更改来部署对 Terraform 配置的更改。

为每个环境使用单独的分支

允许广泛的公开范围

将 Terraform 源代码和代码库设为可供基础架构所有者(例如 SRE)和基础架构利益相关方(例如开发者)在工程组织之间广泛查看和访问。这样可以确保基础架构利益相关方能够更好地了解其所依赖的基础架构。

建议基础架构利益相关方在更改请求过程中提交合并请求。

永不提交密文

切勿将密文提交到源代码控制系统(包括 Terraform 配置)中。而是将其上传到 Secret Manager 等系统,并使用数据源进行引用。

请注意,此类敏感值可能仍会最终出现在状态文件中,也可能作为输出公开。

根据团队边界组织代码库

虽然您可以使用单独的目录来管理资源之间的逻辑边界,但组织边界和逻辑可确定代码库结构。通常,使用的设计原则是将具有不同批准和管理要求的配置分离到不同的源代码控制代码库中。为了说明此原则,提供了一些可能的代码库配置:

  • 一个中央存储库:在此模型中,所有 Terraform 代码都由单个平台团队集中管理。如果有专门的基础架构团队负责所有云管理并批准其他团队请求的任何更改,则此模型效果最佳。

  • 团队代码库:在此模型中,每个团队负责各自的 Terraform 代码库,在其中管理与其拥有的基础架构相关的所有内容。例如,安全团队可能有一个在其中管理所有安全控制的代码库,并且每个应用团队都有各自的 Terraform 代码库来部署和管理其应用。

    对于大多数企业场景而言,按团队边界组织代码库是最佳结构。

  • 分离式代码库:在此模型中,每个逻辑 Terraform 组件都会拆分到各自的代码库中。例如,网络可能有一个专用代码库,并且可能有一个单独的项目工厂代码库进行项目创建和管理。这在团队之间职责经常发生变化的高度分散化的环境中效果最佳。

示例代码库结构

您可以组合这些原则,将 Terraform 配置拆分到不同的代码库类型:

  • 基础
  • 特定于应用和团队
基础代码库

基础代码库,包含主要中心组件,例如文件夹或组织 IAM。此代码库可以由中央云团队管理。

  • 在此代码库中,为每个主要组件(例如文件夹、网络等)添加一个目录。
  • 在组件目录中,为每个环境添加单独的文件夹(反映前面提到的目录结构指导)。

基础代码库结构

特定于应用和团队的代码库

为每个团队单独部署特定于应用和团队的代码库来管理其唯一的特定于应用的 Terraform 配置。

特定于应用和团队的代码库结构

运维

确保基础架构的安全性取决于是否有一个稳定安全的应用 Terraform 更新的流程。

始终先规划

始终先为 Terraform 执行生成计划。将计划保存到输出文件中。获得基础架构所有者的批准后,执行计划。即使开发者在本地对更改进行原型设计,也应该生成计划并查看应用添加、修改和销毁的资源。

实现自动化流水线

为了确保一致的执行上下文,通过自动化工具执行 Terraform。如果构建系统(如 Jenkins)已被使用并广泛采用,请使用它自动运行 terraform planterraform apply 命令。如果没有现有系统,请采用 Cloud BuildTerraform Cloud

使用服务账号凭据实现持续集成

从 CI/CD 流水线中的机器执行 Terraform 时,Terraform 应从执行该流水线的服务沿用服务账号凭据。请尽可能在 Google Cloud 上运行 CI 流水线,因为 Cloud Build、Google Kubernetes Engine 或 Compute Engine 会注入凭据,而无需下载服务账号密钥。

对于在 Google Cloud 外部运行的流水线,首选工作负载身份联合来获取凭据,而无需下载服务账号密钥。

避免导入现有资源

请尽可能避免导入现有资源(使用 terraform import),因为这样做可能会很难完全了解手动创建的资源的来源和配置。应通过 Terraform 创建新资源并删除旧资源。

如果删除旧资源会带来大量重复劳动,请使用 terraform import 命令并明确批准。将资源导入 Terraform 后,只能使用 Terraform 对其进行管理。

Google 提供了一条工具,可用于将 Google Cloud 资源导入 Terraform 状态。如需了解详情,请参阅将 Google Cloud 资源导入 Terraform 状态

不要手动修改 Terraform 状态

Terraform 状态文件对于维护 Terraform 配置与 Google Cloud 资源之间的映射至关重要。中断可能会导致严重的基础架构问题。如果需要修改 Terraform 状态,请使用 terraform state 命令。

定期审核版本固定

固定版本可确保稳定性,但会阻止问题修复和其他改进整合到您的配置中。因此,请定期审核 Terraform、Terraform 提供商和模块的版本固定。

如需自动执行此过程,请使用 Dependabot 等工具。

在本地运行时使用应用默认凭据

当开发者在本地迭代 Terraform 配置时,他们应通过运行 gcloud auth application-default login 生成应用默认凭据来进行身份验证。请勿下载服务账号密钥,因为下载的密钥更难管理和确保安全。

将别名设置为 Terraform

为了简化本地开发,您可以在命令 shell 配置文件中添加别名:

  • alias tf="terraform"
  • alias terrafrom="terraform"

安全

Terraform 需要对您的云基础架构的敏感访问权限才能运行。遵循以下安全性方面的最佳实践有助于最大限度地降低相关风险并提高整体云安全性。

使用远程状态

对于 Google Cloud 客户,我们建议使用 Cloud Storage 状态后端。此方法锁定了状态,允许作为团队进行协作。它还将状态和所有可能的敏感信息与版本控制分开。

确保只有构建系统和具有高度特权的管理员才能访问用于远程状态的存储桶。

为防止意外将开发状态提交到源代码控制系统,请为 Terraform 状态文件使用 gitignore

加密状态

虽然 Google Cloud 存储桶已经过静态加密,但您可以使用客户提供的加密密钥提供另一层保护。为此,请使用 GOOGLE_ENCRYPTION_KEY 环境变量。即使状态文件中不应包含任何 Secret,也请始终将状态加密作为额外的防御措施。

不要将密文存储在状态中

Terraform 中有许多资源和数据提供商在状态文件中以明文形式存储密文值。请尽可能避免将密文存储在状态中。以下是以明文形式存储密文的一些提供商示例:

标记敏感输出

与其尝试手动加密敏感值,不如依靠 Terraform 对敏感状态管理的内置支持。将敏感值导出到输出时,请确保将这些值标记为敏感

确保职责分离

如果您无法从用户无权访问的自动化系统运行 Terraform,请通过分离权限和目录来履行职责分离。例如,网络项目对应于网络 Terraform 服务账号或其访问权限仅限于此项目的用户。

运行应用前检查

在自动化流水线中运行 Terraform 时,请使用 gcloud terraform vet 等工具针对政策检查计划输出,然后再应用计划。这样做可以提前检测安全回归。

运行持续审核

执行 terraform apply 命令后,请运行自动化安全检查。这些检查有助于确保基础架构不会偏移到不安全的状态。以下工具是此类检查的有效选择:

测试

测试 Terraform 模块和配置有时会遵循与测试应用代码不同的模式和惯例。虽然测试应用代码主要涉及测试应用本身的业务逻辑,但全面测试基础架构代码需要部署真实的云资源,以最大限度地降低生产失败的风险。运行 Terraform 测试时,需要考虑以下几点:

  • 运行 Terraform 测试会创建、修改和销毁实际的基础架构,因此您的测试可能非常耗时且昂贵。
  • 您不能只对端到端架构进行单元测试。最佳方法是将架构分解为多个模块并单独进行测试。此方法的好处包括由于测试运行时加快而提高了迭代开发速度,每个测试的费用降低,以及超出控制范围的因素导致测试失败的可能性降低。
  • 尽可能避免重复使用状态。在某些情况下,您可能要使用与其他配置共享数据的配置进行测试,但理想情况下,每个测试都应该是独立的,不应在测试之间重复使用状态。

先使用费用较少的测试方法

您可以使用多种方法来测试 Terraform。这些方法按费用、运行时间和深度升序排序,其中包括:

  • 静态分析:使用编译器、linter 和试运行等工具测试配置的语法和结构,而无需部署任何资源。为此,请使用 terraform validate
  • 模块集成测试:为了确保模块正常运行,请单独测试各个模块。模块的集成测试涉及将模块部署到测试环境中并验证是否创建了预期的资源。有几种测试框架可让您更轻松地编写测试,如下所示:
  • 端到端测试:通过将集成测试方法扩展到整个环境,您可以确认多个模块能否协同工作。使用此方法可以部署构成新测试环境中架构的所有模块。理想情况下,测试环境与生产环境尽可能相似。这种方法费用昂贵,但可以确信更改不会破坏生产环境。

先从小规模测试开始

确保测试以迭代方式在彼此的基础上进行构建。请考虑先运行较小规模的测试,然后使用快速失败方法进行更复杂的测试。

随机选择项目 ID 和资源名称

为避免命名冲突,请确保您的配置在每个项目中都具有全局唯一的项目 ID 和非重叠的资源名称。为了实现这一目的,请为资源使用命名空间。为此,Terraform 具有内置的随机提供程序

使用单独的环境进行测试

在测试期间,系统会创建和删除许多资源。确保此环境与开发或生产项目隔离,以避免在资源清理期间发生意外删除。最好的方法是让每个测试都创建一个新项目或文件夹。为避免配置错误,请考虑专门为执行每个测试创建服务账号。

清理所有资源

测试基础架构代码意味着您要部署实际资源。为避免产生费用,请考虑执行清理步骤。

如需销毁由特定配置管理的所有远程对象,请使用 terraform destroy 命令。一些测试框架具有内置的清理步骤。例如,如果您使用的是 Terratest,请将 defer terraform.Destroy(t, terraformOptions) 添加到测试中。如果您使用的是 Kitchen-Terraform,请使用 terraform kitchen delete WORKSPACE_NAME 删除工作区。

运行 terraform destroy 命令后,您还需要运行其他清理过程来移除 Terraform 未能销毁的任何资源。为此,请删除用于测试执行的任何项目或使用 project_cleanup 模块等工具。

优化测试运行时

如需优化测试执行时间,请使用以下方法:

  • 并行运行测试。某些测试框架支持同时运行多个 Terraform 测试。
    • 例如,您可以使用 Terratest 在测试函数定义后添加 t.Parallel() 来执行此操作。
  • 分阶段测试。将测试分为可以单独测试的独立配置。这种方法无需在运行测试时经历所有阶段,并可加快迭代开发周期。
    • 例如,在 Kitchen-Terraform 中,将测试拆分为单独的套件。迭代时,独立执行每个套件。
    • 同样,可以使用 Terratest 通过 stage(t, STAGE_NAME, CORRESPONDING_TESTFUNCTION) 封装测试的每个阶段。设置环境变量,以指示要运行哪些测试。例如 SKIPSTAGE_NAME="true"
    • 蓝图测试框架支持分阶段执行。

后续步骤