

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 代码库结构和组织的最佳实践
<a name="structure"></a>

随着大型团队和企业使用Terraform的增长，适当的代码库结构和组织至关重要。架构良好的代码库可实现大规模协作，同时增强可维护性。

本节提供有关支持质量和一致性的 Terraform 模块化、命名约定、文档和编码标准的建议。

指导包括按环境和组件将配置分解为可重复使用的模块，使用前缀和后缀建立命名约定，记录模块并清楚地解释输入和输出，以及使用自动样式检查来应用一致的格式化规则。

其他最佳实践包括在结构化层次结构中以逻辑方式组织模块和资源，在文档中对公共和私有模块进行分类，以及在模块中抽象不必要的实现细节以简化使用。

通过围绕模块化、文档、标准和逻辑组织实施代码库结构指南，您可以支持跨团队的广泛协作，同时随着使用情况在整个组织中的普及，Terraform 保持可维护性。通过强制执行惯例和标准，可以避免零碎代码库的复杂性。

**Topics**
+ [实现标准存储库结构](#repo-structure)
+ [模块化结构](#modularity)
+ [遵循命名惯例](#naming-conventions)
+ [使用附件资源](#attachment-resources)
+ [使用默认标签](#default-tags)
+ [满足 Terraform 注册表要求](#registry-reqs)
+ [使用推荐的模块源](#module-sources)
+ [遵循编码标准](#coding-standards)

## 实现标准存储库结构
<a name="repo-structure"></a>

我们建议您实现以下存储库布局。跨模块对这些一致性实践进行标准化可以提高可发现性、透明度、组织性和可靠性，同时支持在许多 Terraform 配置中重复使用。
+ **根模块或目录**：这应该是 Terraform [根](https://developer.hashicorp.com/terraform/language/files#the-root-module)模块和[可重用](https://developer.hashicorp.com/terraform/language/modules/develop)模块的主要入口点，并且应该是唯一的。如果您的架构更复杂，则可以使用嵌套模块来创建轻量级抽象。这可以帮助您用架构来描述基础架构，而不是直接用物理对象来描述基础架构。
+ **自述文件**：根模块和任何嵌套模块都应有自述文件。此文件必须命名`README.md`。它应包含对模块的描述以及该模块的用途。如果要包括将此模块与其他资源一起使用的示例，请将其放在`examples`目录中。可以考虑添加一个图表，描述该模块可能创建的基础架构资源及其关系。使用 [terraform-docs](https://github.com/terraform-docs/terraform-docs) 自动生成模块的输入或输出。
+ **main.tf**：这是主要的入口点。对于一个简单的模块，所有资源都可能在此文件中创建。对于复杂模块，资源创建可能分散在多个文件中，但任何嵌套的模块调用都应在`main.tf`文件中。
+ v@@ **ariables.tf** 和 **outputs.tf**：这些文件包含变量和输出的声明。所有变量和输出都应有一句话或两句话来说明其用途。这些描述用于文档。有关更多信息，请参阅[变量配置](https://developer.hashicorp.com/terraform/language/values/variables)和[输出配置 HashiCorp ](https://developer.hashicorp.com/terraform/language/values/outputs)文档。
  + 所有变量都必须具有已定义的类型。
  + 变量声明也可以包含默认参数。如果声明包含默认参数，则该变量被视为可选变量，如果您在调用模块或运行 Terraform 时未设置值，则使用默认值。默认参数需要文字值，并且不能引用配置中的其他对象。要使变量成为必填变量，请在变量声明中省略默认值，并考虑设置是否有`nullable = false`意义。
  + 对于具有与环境无关的值的变量（例如`disk_size`），请提供默认值。
  + 对于具有环境特定值的变量（例如`project_id`），请不要提供默认值。在这种情况下，调用模块必须提供有意义的值。
  + 只有当将变量留空是底层 APIs 不会拒绝的有效首选项时，才对空字符串或列表等变量使用空默认值。
  + 谨慎使用变量。仅当每个实例或环境的值必须有所不同时，才对其进行参数化。在决定是否公开变量时，请确保您有更改该变量的具体用例。如果需要变量的可能性很小，请不要公开它。
    + 添加具有默认值的变量是向后兼容的。
    + 移除变量不向后兼容。
    + 如果在多个地方重复使用文字，则应使用局部值而不将其作为变量公开。
  + 不要直接通过输入变量传递输出，因为这样做会阻止它们正确添加到依赖关系图中。为确保创建[隐式依赖关系](https://learn.hashicorp.com/terraform/getting-started/dependencies.html)，请确保输出引用资源中的属性。与其直接引用实例的输入变量，不如传递该属性。
+ **locals.tf**：此文件包含为表达式指定名称的本地值，因此可以在模块中多次使用一个名称，而不必重复该表达式。局部值就像函数的临时局部变量。局部值中的表达式不限于字面常量；它们还可以引用模块中的其他值，包括变量、资源属性或其他局部值，以便将它们组合起来。
+ [p@@ **roviders.tf**：此文件包含 [terraform](https://www.terraform.io/language/block/terraform) 块和提供程序块。](https://developer.hashicorp.com/terraform/language/providers/configuration#provider-configuration-1) `provider`块只能由模块的使用者在根模块中声明。

  如果你使用的是 HCP Terraform，还要添加一个空的[云](https://developer.hashicorp.com/terraform/cli/cloud/settings#the-cloud-block)块。作为 CI/CD 管道的一部分，该`cloud`区块应完全通过[环境变](https://developer.hashicorp.com/terraform/cli/cloud/settings#environment-variables)量和[环境变量凭据](https://developer.hashicorp.com/terraform/cli/config/config-file#environment-variable-credentials)进行配置。
+ **versions.tf**：此文件包含 [req](https://developer.hashicorp.com/terraform/language/providers/requirements#requiring-providers) uired\$1providers 块。所有 Terraform 模块都必须声明它需要哪些提供程序，这样 Terraform 才能安装和使用这些提供程序。
+ **data.tf**：要进行简单配置，请将[数据源](https://developer.hashicorp.com/terraform/language/data-sources)放在引用它们的资源旁边。例如，如果您要获取用于启动实例的图像，请将其放在实例旁边，而不是在它们自己的文件中收集数据资源。如果数据源数量过大，可以考虑将它们移动到专用`data.tf`文件中。
+ **.tfvars 文件**：对于根模块，您可以使用文件提供非敏感变量。`.tfvars`为了保持一致性，请命名变量文件`terraform.tfvars`。将常用值放在存储库的根目录下，将特定于环境的值放在文件夹中。`envs/`
+ **嵌套模块**：`modules/`子目录下应存在嵌套模块。任何具有的嵌套模块`README.md`都被外部用户视为可用。如果 a `README.md` 不存在，则该模块仅供内部使用。应使用嵌套模块将复杂的行为拆分为多个小模块，用户可以仔细选择这些模块。

  如果根模块包括对嵌套模块的调用，则这些调用应使用相对路径，例如，`./modules/sample-module`这样 Terraform 就会将它们视为同一个存储库或包的一部分，而不是单独下载它们。

  如果存储库或包包含多个嵌套模块，则理想情况下，它们应可由调用者组合，而不是直接相互调用并创建深度嵌套的模块树。
+ **示例**：使用可重用模块的示例应存在于存储库根`examples/`目录下的子目录下。对于每个示例，您可以添加自述文件来解释示例的目标和用法。子模块的示例也应放在根`examples/`目录中。

  由于示例通常被复制到其他存储库中进行自定义，因此模块块的源应设置为外部调用者使用的地址，而不是相对路径。
+ **服务命名文件**：用户通常希望在多个文件中按服务分隔 Terraform 资源。应尽量不鼓励这种做法，`main.tf`而应在其中界定资源。但是，如果资源集合（例如，IAM 角色和策略）超过 150 行，则可以合理地将其分解成自己的文件，例如`iam.tf`。否则，所有资源代码都应在中定义`main.tf`。
+ **自定义脚本**：仅在必要时使用脚本。Terraform 不考虑或管理通过脚本创建的资源的状态。仅当 Terraform 资源不支持所需行为时才使用自定义脚本。将 Terraform 调用的自定义脚本放在目录中。`scripts/`
+ **帮助脚本**：将 Terraform 未调用的帮助脚本整理到目录中。`helpers/`在文件中记录助手脚本，`README.md`并附有说明和示例调用。如果帮助脚本接受参数，则提供参数检查和`--help`输出。
+ **静态文件**：Terraform 引用但未运行的静态文件（例如加载到 EC2 实例上的启动脚本）必须组织到一个`files/`目录中。将冗长的文档放在外部文件中，与其 HCL 分开。使用 [file () 函数](https://www.terraform.io/language/functions/file)引用它们。
+ **模板**：对于 Terraform [模板文件函数读入的文件](https://www.terraform.io/docs/configuration/functions/templatefile.html)，请使用文件扩展名。`.tftpl`模板必须放在`templates/`目录中。

### 根模块结构
<a name="root-module"></a>

Terraform 总是在单个根模块的上下文中运行。完整的 Terraform 配置由根模块和子模块树（包括根模块调用的模块、这些模块调用的任何模块等）组成。

Terraform 根模块布局基本示例：

```
.
├── data.tf
├── envs
│   ├── dev
│   │   └── terraform.tfvars
│   ├── prod
│   │   └── terraform.tfvars
│   └── test
│       └── terraform.tfvars
├── locals.tf
├── main.tf
├── outputs.tf
├── providers.tf
├── README.md
├── terraform.tfvars
├── variables.tf
└── versions.tf
```

### 可重复使用的模块结构
<a name="module-structure"></a>

可重复使用的模块遵循与根模块相同的概念。要定义模块，请为其创建一个新目录并将`.tf`文件放入其中，就像定义根模块一样。Terraform 可以从本地相对路径或远程存储库加载模块。如果您希望某个模块可以被许多配置重复使用，请将其放在自己的版本控制存储库中。保持模块树相对平坦很重要，这样可以更轻松地以不同的组合重复使用模块。

Terraform 可重复使用的模块布局基本示例：

```
.
├── data.tf
├── examples
│   ├── multi-az-new-vpc
│   │   ├── data.tf
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── providers.tf
│   │   ├── README.md
│   │   ├── terraform.tfvars
│   │   ├── variables.tf
│   │   ├── versions.tf
│   │   └── vpc.tf
│   └── single-az-existing-vpc
│   │   ├── data.tf
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   ├── providers.tf
│   │   ├── README.md
│   │   ├── terraform.tfvars
│   │   ├── variables.tf
│   │   └── versions.tf
├── iam.tf
├── locals.tf
├── main.tf
├── outputs.tf
├── README.md
├── variables.tf
└── versions.tf
```

## 模块化结构
<a name="modularity"></a>

原则上，您可以将任何资源和其他结构组合到一个模块中，但是过度使用嵌套和可重用的模块会使您的整体 Terraform 配置更难理解和维护，因此请谨慎使用这些模块。

如果有意义，可以将您的配置分解为可重复使用的模块，这些模块通过描述架构中由资源类型构造的新概念来提高抽象级别。

当您将基础设施模块化为可重复使用的定义时，应瞄准逻辑资源集，而不是单个组件或过于复杂的集合。

### 不要封装单个资源
<a name="single-resources"></a>

你不应该创建围绕其他单一资源类型的薄包装模块。如果你在为模块找到与其中的主资源类型名称不同的名称时遇到困难，那么你的模块可能没有创建新的抽象，而是增加了不必要的复杂性。相反，直接在调用模块中使用资源类型。

### 封装逻辑关系
<a name="logical-relationships"></a>

对相关资源进行分组，例如网络基础、数据层、安全控制和应用程序。可重复使用的模块应封装协同工作的基础架构部分，以实现一项功能。

### 保持继承不变
<a name="inheritance"></a>

将模块嵌套在子目录中时，请避免深度超过一两个级别。深度嵌套的继承结构使配置和故障排除变得复杂。模块应该建立在其他模块之上，而不是通过它们构建隧道。

通过将模块重点放在代表架构模式的逻辑资源分组上，团队可以快速配置可靠的基础架构。在不进行过度设计或过度简化的情况下平衡抽象。

### 产出中的参考资源
<a name="output-resources"></a>

对于在可重用模块中定义的每个资源，至少包括一个引用该资源的输出。变量和输出允许您推断模块和资源之间的依赖关系。如果没有任何输出，用户就无法根据他们的 Terraform 配置正确订购您的模块。

结构良好的模块可提供环境一致性、以目的为导向的分组和导出的资源引用，可实现组织范围内的 Terraform 大规模协作。团队可以利用可重复使用的构造块组装基础架构。

### 不要配置提供商
<a name="configuration"></a>

尽管共享模块继承调用模块的提供程序，但模块不应自行配置提供程序设置。避免在模块中指定提供程序配置块。此配置只能在全局声明一次。

### 申报所需的提供商
<a name="required-providers"></a>

尽管提供程序配置在模块之间共享，但共享模块还必须声明自己的[提供者要求](https://developer.hashicorp.com/terraform/language/providers/requirements)。这种做法使 Terraform 能够确保提供程序的单一版本与配置中的所有模块兼容，并指定用作提供程序全局（与模块无关）标识符的源地址。但是，模块特定的提供程序要求并未指定任何用于确定提供程序将访问哪些远程端点的配置设置，例如。 AWS 区域

通过声明版本要求并避免使用硬编码的提供程序配置，模块使用共享提供程序提供跨Terraform配置的可移植性和可重用性。

对于共享模块，请在中的 requi [red\$1providers 块中定义所需的最低提供者](https://developer.hashicorp.com/terraform/language/modules/develop/providers#provider-version-constraints-in-modules)版本。`versions.tf`

要声明模块需要特定版本的 AWS 提供程序，请在`required_providers`块内使用一个`terraform`块：

```
terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 4.0.0"
    }
  }
}
```

如果共享模块仅支持 AWS 提供程序的特定版本，请使用*悲观约束运算符* (`~>`)，它只允许最右边的版本组件递增：

```
terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}
```

在本示例中，`~> 4.0`允许安装`4.57.1`和，`4.67.0`但不允许`5.0.0`。有关更多信息，请参阅 HashiCorp 文档中的[版本约束语法](https://developer.hashicorp.com/terraform/language/expressions/version-constraints#version-constraint-syntax)。

## 遵循命名惯例
<a name="naming-conventions"></a>

清晰的描述性名称可简化您对模块中资源之间关系和配置值用途的理解。与风格指南的一致性提高了模块用户和维护者的可读性。

### 遵循资源命名准则
<a name="resource-naming"></a>
+ 对所有资源名称使用 s *nake\$1cas* e（其中小写术语用下划线分隔），以匹配 Terraform 样式标准。这种做法可确保与资源类型、数据源类型和其他预定义值的命名约定保持一致。此约定不适用于[名称参数](https://www.terraform.io/docs/glossary#argument)。
+ 为了简化对资源类型的唯一资源（例如，整个模块的单个负载均衡器）的引用，`this`为清楚起见，请将该资源命名为 o `main` r。
+ 使用有意义的名称来描述资源的用途和上下文，并有助于区分相似的资源（例如，`primary`用于主数据库和`read_replica`数据库的只读副本）。
+ 使用单数名称，而不是复数名称。
+ 不要在资源名称中重复资源类型。

### 遵循变量命名指南
<a name="variable-naming"></a>
+ 在输入、局部变量和输出名称中添加表示磁盘大小或 RAM 大小等数值的单位（例如，以千兆字节`ram_size_gb`为单位的 RAM 大小）。这种做法使配置维护者可以清楚地了解预期的输入单元。
+ 使用二进制单位（例如 MiB 和 GiB）作为存储大小，使用十进制单位（例如 MB 或 GB）来表示其他指标。
+ 给布尔变量起正名，例如`enable_external_access`。

## 使用附件资源
<a name="attachment-resources"></a>

有些资源中嵌入了伪资源作为属性。在可能的情况下，应避免使用这些嵌入的资源属性，而是使用唯一的资源来附加该伪资源。这些资源关系可能导致每种资源都存在独特 cause-and-effect的问题。

使用嵌入式属性（避免这种模式）：

```
resource "aws_security_group" "allow_tls" {
  ...
  ingress {
    description      = "TLS from VPC"
    from_port        = 443
    to_port          = 443
    protocol         = "tcp"
    cidr_blocks      = [aws_vpc.main.cidr_block]
    ipv6_cidr_blocks = [aws_vpc.main.ipv6_cidr_block]
  }

  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}
```

使用附件资源（首选）：

```
resource "aws_security_group" "allow_tls" {
  ...
}

resource "aws_security_group_rule" "example" {
  type              = "ingress"
  description      = "TLS from VPC"
  from_port        = 443
  to_port          = 443
  protocol         = "tcp"
  cidr_blocks      = [aws_vpc.main.cidr_block]
  ipv6_cidr_blocks = [aws_vpc.main.ipv6_cidr_block]
  security_group_id = aws_security_group.allow_tls.id
}
```

## 使用默认标签
<a name="default-tags"></a>

为所有可以接受标签的资源分配标签。Terraform P AWS rovider 有一个 [aws\$1default\$1tags](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/default_tags) 数据源，你应该在根模块中使用它。

考虑向 Terraform 模块创建的所有资源添加必要的标签。以下是可能要附加的标签列表：
+ **名称**：人类可读的资源名称
+ **AppId**：使用该资源的应用程序的 ID
+ **AppRole**: 资源的技术功能；例如，“网络服务器” 或 “数据库”
+ **AppPurpose**: 资源的业务目的；例如，“前端用户界面” 或 “支付处理器”
+ **环境**：软件环境，例如开发、测试或生产
+ **项目**：使用资源的项目
+ **CostCenter**: 向谁收取资源使用费账单

## 满足 Terraform 注册表要求
<a name="registry-reqs"></a>

模块存储库必须满足以下所有要求才能发布到 Terraform 注册表。

即使您不打算在短期内将模块发布到注册表，也应始终遵循这些要求。这样，您就可以稍后将模块发布到注册表，而不必更改存储库的配置和结构。
+ 存储库名称：对于模块存储库，请使用由三部分组成的名称`terraform-aws-<NAME>`，其中`<NAME>`反映了模块管理的基础架构的类型。该`<NAME>`分段可以包含其他连字符（例如，`terraform-aws-iam-terraform-roles`）。
+ 标准模块结构：模块必须符合标准存储库结构。这允许注册表检查您的模块并生成文档、跟踪资源使用情况等。
  + 创建 Git 仓库后，将模块文件复制到仓库的根目录。我们建议您将每个打算重复使用的模块放在各自存储库的根目录中，但也可以从子目录中引用模块。
  + 如果您使用的是 HCP Terraform，请发布要共享到您的组织注册表的模块。注册表使用HCP Terraform API令牌处理下载并控制访问权限，因此，即使消费者从命令行运行Terraform，也无需访问模块的源存储库。
+ 位置和权限：存储库必须位于您配置的[版本控制系统 (VCS) 提供程序](https://developer.hashicorp.com/terraform/cloud-docs/vcs)中，并且 HCP Terraform VCS 用户帐户必须具有存储库的管理员访问权限。注册表需要管理员访问权限才能创建 webhook 以导入新的模块版本。
+ 发行版的 x.y.z 标签：要发布模块，必须至少有一个发布标签。注册表使用发布标签来识别模块版本。发布标签名称必须使用[语义版本控制](https://semver.org/)，您可以选择在语义版本控制前面加上`v`（例如`v1.1.0`和`1.1.0`）。注册表会忽略看起来不像版本号的标签。有关发布模块的更多信息，请参阅 [Terraform](https://developer.hashicorp.com/terraform/cloud-docs/registry/publish-modules#publishing-a-new-module) 文档。

有关更多信息，请参阅 Terraform [文档中的准备模块存储库](https://developer.hashicorp.com/terraform/cloud-docs/registry/publish-modules#preparing-a-module-repository)。

## 使用推荐的模块源
<a name="module-sources"></a>

Terraform 使用模块块中的`source`参数来查找和下载子模块的源代码。

我们建议您对密切相关的模块使用本地路径，这些模块的主要目的是分解重复的代码元素，对于打算由多个配置共享的模块，使用原生 Terraform 模块注册表或 VCS 提供程序。

以下示例说明了共享模块的最常见和最推荐的[源类型](https://developer.hashicorp.com/terraform/language/modules/sources)。注册表模块支持[版本控制。](https://developer.hashicorp.com/terraform/language/modules/syntax#version)您应始终提供特定的版本，如以下示例所示。

### 注册表
<a name="registry-examples"></a>

*Terraform 注册表：*

```
module "lambda" {
  source = "github.com/terraform-aws-modules/terraform-aws-lambda.git?ref=e78cdf1f82944897ca6e30d6489f43cf24539374" #--> v4.18.0

  ...

}
```

通过固定提交哈希，您可以避免偏离容易受到供应链攻击的公共注册表。

*HCP Terraform：*

```
module "eks_karpenter" {
  source = "app.terraform.io/my-org/eks/aws"
  version = "1.1.0"

  ...

  enable_karpenter = true
}
```

*Terraform Enterpr*

```
module "eks_karpenter" {
  source = "terraform.mydomain.com/my-org/eks/aws"
  version = "1.1.0"

  ...

  enable_karpenter = true
}
```

### VCS 提供商
<a name="vcs-examples"></a>

版本控制系统（VCS）提供程序支持选择特定版本的`ref`论点，如以下示例所示。

*GitHub (HTTPS)：*

```
module "eks_karpenter" {
  source = "github.com/my-org/terraform-aws-eks.git?ref=v1.1.0"

  ...

  enable_karpenter = true
}
```

*通用 Git 存储库 (HTTPS)：*

```
module "eks_karpenter" {
  source = "git::https://example.com/terraform-aws-eks.git?ref=v1.1.0"

  ...

  enable_karpenter = true
}
```

*通用 Git 存储库 (SSH)：*

**警告**  
您需要配置凭据才能访问私有仓库。

```
module "eks_karpenter" {
  source = "git::ssh://username@example.com/terraform-aws-eks.git?ref=v1.1.0"

  ...

  enable_karpenter = true
}
```

## 遵循编码标准
<a name="coding-standards"></a>

在所有配置文件中应用一致的 Terraform 格式规则和样式。通过在 CI/CD 管道中使用自动样式检查来强制执行标准。当您将编码最佳实践嵌入到团队工作流程中时，随着使用情况在整个组织中的广泛分布，配置仍保持可读性、可维护性和协作性。

### 遵循风格指南
<a name="style-guidelines"></a>
+ 使用 terraform fm [t 命令格式化所有 Terraform `.tf`](https://developer.hashicorp.com/terraform/cli/commands/fmt) 文件（文件）以匹配样式标准。 HashiCorp 
+ 使用 [terraform validat](https://developer.hashicorp.com/terraform/cli/commands/validate) e 命令来验证配置的语法和结构。
+ 使用[TFLint](https://github.com/terraform-linters/tflint)静态分析代码质量。这个 linter 会检查 Terraform 的最佳实践，而不仅仅是格式化，并且在遇到错误时会失败构建。

### 配置预提交挂钩
<a name="hooks"></a>

在允许提交之前，配置运行`terraform fmt`、`tflint``checkov`、和其他代码扫描和样式检查的客户端预提交挂钩。这种做法可以帮助您在开发人员工作流程中尽早验证标准一致性。

使用预提交框架（例如[预提交](https://pre-commit.com/)）在本地计算机上将 Terraform linting、格式化和代码扫描作为挂钩添加。Hooks 会在每个 Git 提交上运行，如果检查未通过，则提交失败。

将样式和质量检查移至本地预提交挂钩可在引入更改之前向开发人员提供快速反馈。标准成为编码工作流程的一部分。