Terraform模块设计原则

Terraform模块是独立的基础设施即代码片段,抽象了基础设施部署的底层复杂性。Terraform用户通过使用预置的配置代码加速采用IaC,并降低了使用门槛。
所以,模块的作者应尽量遵循诸如清晰的代码结构以及DRY(“Dont’t Repeat Yourself”)原则的代码最佳实践。

模块创建的工作流

创建一个新模块需要谨记两点:

  1. 将需求范围划分成合适的模块。
  2. 创建模块的最小可行产品(Minimum Viable Product, MVP)

将需求范围划分成合适的模块

在最初确定模块的范围时,目标应当足够小且简单,易于开始编写。
当构建一个模块时,需要考虑以下三个方面:

  • 封装: 一组始终被一起部署的基础设施资源,在模块中包含更多的基础设施资源简化了终端用户部署基础设施的工作,但会使得模块的目的与需求变得更难理解。
  • 职责: 限制模块职责的边界,如果模块中的基础设施资源由多个组来负责,使用该模块可能会意外违反职责分离原则,模块中仅包含职责边界内的一组资源可以提升基础设施资源的隔离性,并保护我们的基础设施。
  • 变化频率: 隔离长短生命周期基础设施资源,举例来说,数据库基础设施资源相对来说较为静态,而团队可能在一天内多次部署更新应用程序服务器,在同一个模块中同时管理数据库与应用程序服务器使得保存状态数据的重要基础设施没有必要地暴露在数据丢失的风险之中。

创建模块的最小可行产品

如同所有类型的代码一样,模块的开发永远不会完成,永远会有新的模块需求以及变更。拥抱变化,最初的模块版本应致力于满足最小可行产品(MVP)的标准。
以下是在设计最小可行产品时需要谨记的指导清单:

  • 永远致力于交付至少可以满足80%场景的模块
  • 模块中永远不要处理边缘场景,边缘场景是很少见的,一个模块应该是一组可重用的代码
  • 在最小可行产品中避免使用条件表达式,最小可行产品应缩小范围,不应该同时完成多种任务
  • 模块应该只将最常被修改的参数公开为输入变量,一开始时,模块应该只提供最可能需要的输入变量

尽可能多输出

在最小可行产品(MVP)中输出尽可能多的信息,哪怕目前没有用户需要这些信息。这使得那些通常使用多个模块的终端用户在使用该模块时更加轻松,可以使用一个模块的输出作为下一个模块的输入。
请记住在模块的README文档中记录输出值的文档。

模块创建的提示

除了范围界定之外,在创建模块时还应牢记以下几点:

嵌套模块

嵌套模块是指在当前模块中对另一个模块的引用,嵌套模块可以是外部的,也可以是当前工作空间内的。
使用嵌套模块是一项强大的功能,然而必须谨慎实践以避免引入错误。

对于所有类型的嵌套模块,请考虑以下事项:

  • 嵌套模块可以加速开发速度,但可能会引发未知以及意料之外的结果,请在文档中清晰地记录输入变量、模块行为以及输出值。
  • 通常来说,不要让主模块的嵌套深度超过两层,常用且简单的工具模块,例如专门用来定义Tag的模块,则不受此限制制约。
  • 嵌套模块必须包含必要的用来创建指定的资源配置的输入参数以及输出值。
  • 输入参数以及输出值的命名应遵循一致的命名约定,以使得模块可以更容易地被分享,以及将一个模块的的输出值作为另一个模块的输入参数。
  • 嵌套模块可能会导致代码冗余,必须同时在父模块与嵌套模块中声明输入参数和输出值。

嵌套的外部模块

当需要使用那些定义了被多个应用程序堆栈、应用程序和团队复用的标准化资源的通用模块时,嵌套的外部模块会很有用。外部模块通被集中管理和版本化控制,以使得消费者在使用新版本之前可以对其进行验证。
当依赖或希望使用位于外部的子模块时,请注意以下几点:

  • 外部模块必须被独立维护,并可供任何需要调用它的模块使用,使用模块注册表可以确保这一点。
  • 根据模块注册要求,嵌套模块将拥有自己的版本控制代码仓库,独立于调用模块进行版本控制。
  • 对嵌套模块的变更可能会影响调用模块,即使调用模块的调用代码及版本没有发生变化,这会破坏调用代码的信任。
  • 对调用模块如何使用外部模块在文档中进行记录,使得模块行为以及调用关系可以被轻松理解。
  • 对外部模块的变更应该是向后兼容的,如果向后兼容是不可能的,则应清楚地记录需要对任何调用模块进行的更改,并将之分发给所有模块使用者以避免意外。

嵌套的嵌入模块

在当前工作空间中嵌入一个模块能够清晰地分离模块的逻辑组件,或是创建可在调用模块执行期间多次调用的可重用代码块。
在下面的例子中,ec2-instance是一个嵌入模块,根模块的main.tf引用了该模块:

root-module-directory
├── README.md
├── main.tf
└── ec2-instances
    └── main.tf

如果需要或者倾向于使用嵌入模块,需要考虑以下几点:

  • 在“根模块”中添加嵌入模块意味着子模块与根模块被放在一起进行版本控制。
  • 任何影响两个模块间兼容性的变更都会被快速发现,因为它们必须被一同测试和发布。
  • 嵌入的子模块不能被代码树之外的其他模块调用,所以可能会增加重复的代码。例如,如果嵌入的ec2-instance模块是用来创建一台被用在多个地方的标准化的计算实例,该模块无法以这种形式被分享。

标签化模块名并记录在文档中

为模块创建并遵循一个命名约定将使得模块易于理解与使用,这将促进模块的采用和贡献。
以下是一个用以提升模块元素一致性的建议列表:

  • 使用一个对人类来说一致且易于理解的模块命名约定,示例如下:

    terraform cloud provider function full name
    terraform aws consul cluster terraform-aws-consul_cluser
    terraform aws security module terraform-aws-security
    terraform azure database terraform-azure-database
  • 使用人类可以理解的输入变量命名约定,模块是编写一次并多次使用的代码,因此请完整命名所有内容以提升可读性,并在编写代码时在文档中进行记录。

  • 对所有模块进行文档记录,确保文档中包含有:

    • 必填的输入变量:这些输入变量应该是经过深思熟虑后的选择。如果这些输入变量值未定义,模块运行将失败。只在必要时为这些输入变量设置默认值。例如var.vpc_id永远不应该有默认值,因为每次使用模块时值都会不同。
    • 可选的输入变量:这些输入变量应该有一个合理的,适用于大多数场景的默认值,同时又可以根据需求进行调整。公告输入变量的默认值,例如var.elb_idle_timeout会有一个合理的默认值,但调用者也可以根据需求修改它的值。
    • 输出值:列出模块的所有输出值,并将重要的输出和信息性的输出包装在对用户友好的输出模板中。

定义并使用一个一致的模块结构

应当将模块的结构记录在文档中,并且在所有模块之间保持统一的结构。
为了要维持模块结构的一致:

  • 定义一组模块必须包含的.tf文件,定义它们应包含哪些内容
  • 为模块定义一个.gitignore(或类似作用的)文件
  • 创建供样例代码所使用的输入变量值的标准方式(例如:一个terraform.tfvars.example文件)
  • 使用具有固定子目录的一致的目录结构,即使它们可能是空的
  • 所有模块目录都必须包含一个README文件详细记述目录存在的目的以及如何使用其中的文件

模块的协作

随着团队模块的开发工作,简化我们的协作。

  1. 为每个模块创建路线图。
  2. 从用户处收集需求信息,并按受欢迎程度进行优先级排序。
    • 不使用模块的最常见原因是“它不符合我的要求”,收集这些需求并将它们添加到路线图或对用户的工作流程提出建议
    • 检查每一项需求以确认它引用的用例是否正确
    • 公布和维护需求列表,分享该列表并让用户参与列表管理过程
    • 不要为边缘用例排期
  3. 将每一个决策记录进文档。
  4. 在公司内部采用开源社区原则,一些用户希望尽可能高效地使用这些模块,而另一些用户则希望帮助创建这些模块。
    • 创建一个社区
    • 维护一份清晰和公开的贡献指引
    • 最终,我们将允许可信的社区成员获得某些模块的所有权

模块的版本管理

一个Terraform模块应遵守所有良好的代码实践:

  • 将模块置于源代码控制中以管理版本发布、协作、变更的审计跟踪。
  • 为所有main分支的发布版本建立版本标签,记录文档(最起码在CHANGELOGREADME中记录)
  • main分支的所有变更进行代码审查
  • 鼓励模块的用户通过版本标签引用模块
  • 为每一个模块指派一位负责人
  • 一个代码仓库只负责一个模块
    • 这对于模块的幂等性和作为库的功能至关重要
    • 应该对模块打上版本标签或是版本化控制,打上版本标签或是版本化的模块应该是不可变的
    • 发布到私有模块注册表的模块必须要有版本标签

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,在下面评论区告诉我^_^^_^