模块

简单来讲模块就是包含一组Terraform代码的文件夹。
Terraform模块是编写高质量Terraform代码,提升代码复用性的重要手段,可以说,一个成熟的生产环境应该是由数个可信成熟的模块组装而成的。
实际上所有包含Terraform代码文件的文件夹都是一个Terraform模块。我们如果直接在一个文件夹内执行terraform apply或者terraform plan命令,那么当前所在的文件夹就被称为根模块(root module)。我们也可以在执行Terraform命令时通过命令行参数指定根模块的路径。

模块结构

模块旨在被重用,在一个模块中,会有:

  • 一个README文件,用来描述模块的用途。文件名可以是README或者README.md,可以考虑在README中用可视化的图形来描绘创建的基础设施资源以及它们之间的关系。README中不需要描述模块的输入输出,因为工具会自动收集相关信息。如果在README中引用了外部文件或图片,请确保使用的是带有特定版本号的绝对URL路径以防止未来指向错误的版本。
  • 一个LICENSE描述模块使用的许可协议。如果你想要公开发布一个模块,最好考虑包含一个明确的许可证协议文件,许多组织不会使用没有明确许可证协议的模块
  • 一个examples文件夹用来给出一个调用样例(可选)
  • 一个variables.tf文件,包含模块所有的输入变量。输入变量应该有明确的描述说明用途
  • 一个outputs.tf文件,包含模块所有的输出值。输出值应该有明确的描述说明用途
  • 嵌入模块文件夹,出于封装复杂性或是复用代码的目的,我们可以在modules子目录下建立一些嵌入模块。所有包含README文件的嵌入模块都可以被外部用户使用;不含README文件的模块被认为是仅在当前模块内使用的(可选)
  • 一个main.tf,它是模块主要的入口点。对于一个简单的模块来说,可以把所有资源都定义在里面;如果是一个比较复杂的模块,我们可以把创建的资源分布到不同的代码文件中,但引用嵌入模块的代码还是应保留在main.tf里
  • 其他定义了各种基础设施对象的代码文件(可选)
    如果模块含有多个嵌入模块,那么应避免它们彼此之间的引用,由根模块负责组合它们。

由于examples/中的代码经常会被拷贝到其他项目中进行修改,所有在examples/代码中引用本模块时使用的引用路径应使用外部调用者可以使用的路径,而非相对路径。
一个完整的模块结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│ ├── nestedA/
│ │ ├── README.md
│ │ ├── variables.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ ├── nestedB/
│ ├── .../
├── examples/
│ ├── exampleA/
│ │ ├── main.tf
│ ├── exampleB/
│ ├── .../

避免过深的模块结构

上面提到可以在modules/子目录下创建嵌入模块。Terraform倡导”扁平”的模块结构,只应保持一层嵌入模块,防止在嵌入模块中继续创建嵌入模块。应将嵌入模块设计成易于组合的结构,使得在根模块中可以通过组合各个嵌入模块创建复杂的基础设施。

引用模块

在Terraform代码中引用一个模块,使用的是module块。
每当在代码中新增、删除或者修改一个module块之后,都要执行terraform init命令来获取模块代码并安装到本地磁盘上。

模块源

module块定义了一个source参数,指定了模块的源;Terraform目前支持如下模块源:

  • 本地路径
  • Terraform Registry
  • GitHub
  • Bitbucket
  • 通用Git、Mercurial仓库
  • HTTP地址
  • S3 buckets
  • GCS buckets

source使用的是URL风格的参数,但某些源支持在source参数中通过额外参数指定模块版本。

本地路径

使用本地路径可以使我们引用同一项目内定义的子模块:

1
2
3
module "consul" {
source = "./consul"
}

一个本地路径必须以./或者../为前缀来标明要使用的本地路径,以区别于使用Terraform Registry路径。
本地路径引用模块和其他源类型有一个区别,本地路径引用的模块不需要下载相关源代码,代码已经存在于本地相关路径的磁盘上了。

Terraform Registry

Registry目前是Terraform官方力推的模块仓库方案,采用了Terraform定制的协议,支持版本化管理和使用模块。

公共仓库的的模块可以用//形式的源地址来引用,在公共仓库上的模块介绍页面上都包含了确切的源地址,例如:

1
2
3
4
module "consul" {
source = "hashicorp/consul/aws"
version = "0.1.0"
}

GitHub

erraform发现source参数的值如果是以github.com为前缀时,会将其自动识别为一个GitHub源:

1
2
3
module "consul" {
source = "github.com/hashicorp/example"
}

上面的例子里会自动使用HTTPS协议克隆仓库。如果要使用SSH协议,那么请使用如下的地址:

1
2
3
module "consul" {
source = "git@github.com:hashicorp/example.git"
}

GitHub源的处理与后面要介绍的通用Git仓库是一样的,所以他们获取git凭证和通过ref参数引用特定版本的方式都是一样的。如果要访问私有仓库,你需要额外配置git凭证。

通用Git仓库

可以通过在地址开头加上特殊的git::前缀来指定使用任意的Git仓库。在前缀后跟随的是一个合法的Git URL。

使用HTTPS和SSH协议的例子:

1
2
3
4
5
6
7
module "vpc" {
source = "git::https://example.com/vpc.git"
}

module "storage" {
source = "git::ssh://username@example.com/storage.git"
}

Terraform使用git clone命令安装模块代码,所以Terraform会使用本地Git系统配置,包括访问凭证。要访问私有Git仓库,必须先配置相应的凭证。

如果使用了SSH协议,那么会自动使用系统配置的SSH证书。通常情况下我们通过这种方法访问私有仓库,因为这样可以不需要交互式就可以访问私有仓库。

如果使用HTTP/HTTPS协议,或是其他需要用户名、密码作为凭据,你需要配置Git凭据存储来选择一个合适的凭据源。

默认情况下,Terraform会克隆默认分支。可以通过ref参数来指定版本:

1
2
3
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0"
}

ref参数会被用作git checkout命令的参数,可以是分支名或是tag名。

使用SSH协议时,我们更推荐ssh://的地址。你也可以选择scp风格的语法,故意忽略ssh://的部分,只留git::,例如:

1
2
3
module "storage" {
source = "git::username@example.com:storage.git"
}

S3 Bucket

你可以把模块存档保存在AWS S3桶里,使用s3::作为地址前缀,后面跟随一个S3对象访问地址

1
2
3
module "consul" {
source = "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"
}

Terraform识别到s3::前缀后会使用AWS风格的认证机制访问给定地址。这使得这种源地址也可以搭配其他提供了S3协议兼容的对象存储服务使用,只要他们的认证方式与AWS相同即可。

保存在S3桶内的模块存档文件格式必须与上面HTTP源提到的支持的格式相同,Terraform会下载并解压缩模块代码。

模块安装器会从以下位置寻找AWS凭证,按照优先级顺序排列:

AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY环境变量
HOME目录下.aws/credentials文件内的默认profile
如果是在AWS EC2主机内运行的,那么会尝试使用搭载的IAM主机实例配置。

使用模块

我们可以把模块理解成类似函数,如同函数有输入参数和输出值一样,我们之前介绍过Terraform代码有输入变量和输出值。我们在module块的块体内除了source参数,还可以对该模块的输入变量赋值:

1
2
3
4
5
module "servers" {
source = "./app-cluster"

servers = 5
}

例子中创建./app-cluster文件夹下Terraform声明的一系列资源,该模块的servers输入变量的值被我们设定成了5。
在代码中新增、删除或是修改一个某块的source,都需要重新运行terraform init命令。默认情况下,该命令不会升级已安装的模块(例如source未指定版本,过去安装了旧版本模块代码,那么执行terraform init不会自动更新到新版本);可以执行terraform init -upgrade来强制更新到最新版本模块。

模块版本约束

使用registry作为模块源时,可以使用version元参数约束使用的模块版本:

1
2
3
4
5
6
module "consul" {
source = "hashicorp/consul/aws"
version = "0.0.5"

servers = 3
}

version元参数的格式与Provider版本约束的格式一致。在满足版本约束的前提下,Terraform会使用当前已安装的最新版本的模块实例。如果当前没有满足约束的版本被安装过,那么会下载符合约束的最新的版本。

version元参数只能配合registry使用,公共的或者私有的模块仓库都可以。其他类型的模块源可能支持版本化,也可能不支持。本地路径模块不支持版本化。