在Terraform代码中引用一个模块,使用的是module
块。
每当在代码中新增、删除或者修改一个module
块之后,都要执行terraform init
或是terraform get
命令来获取模块代码并安装到本地磁盘上。
模块源
module
块定义了一个source
参数,用于指定模块的源。
Terraform目前支持如下模块源如下:
- 本地路径
- Terraform Registry
- GitHub
- Bitbucket
- 通用Git、Mercurial仓库
- HTTP地址
- S3 buckets
- GCS buckets
source
使用的是URL风格的参数,但某些源支持在source
参数中通过额外参数指定模块版本。
出于消除重复代码的目的可以重构根模块代码,将一些拥有重复模式的代码重构为可反复调用的嵌入模块,通过本地路径来引用。
许多的模块源类型都支持从当前系统环境中读取认证信息,例如环境变量或系统配置文件。
本地路径
使用本地路径可以引用同一项目内定义的子模块:
module "consul" {
source = "./consul"
}
一个本地路径必须以./
或者../
为前缀来标明要使用的本地路径,以区别于使用Terraform Registry路径。
本地路径引用模块和其他源类型有一个区别,本地路径引用的模块不需要下载相关源代码,代码已经存在于本地相关路径的磁盘上了。
Terraform Registry
Registry目前是Terraform官方力推的模块仓库方案,采用了Terraform定制的协议,支持版本化管理和使用模块。
Terraform官方提供的公共仓库保存和索引了大量公共模块,在这里可以很容易地搜索到各种官方和社区提供的高质量模块。
也可以通过Terraform Cloud服务维护一个私有模块仓库,或是通过实现Terraform模块注册协议来实现一个私有仓库。
公共仓库的的模块可以用<NAMESPACE>/<NAME>/<PROVIDER>
形式的源地址来引用,在公共仓库上的模块介绍页面上都包含了确切的源地址,例如:
module "consul" {
source = "hashicorp/consul/aws"
version = "0.1.0"
}
对于那些托管在其他仓库的模块,在源地址头部添加<HOSTNAME>/
部分,指定私有仓库的主机名:
module "consul" {
source = "app.terraform.io/example-corp/k8s-cluster/azurerm"
version = "1.1.0"
}
如果使用的是SaaS版本的Terraform Cloud,那么托管在上面的私有仓库的主机名是app.terraform.io
;如果使用的是私有部署的Terraform企业版,那么托管在上面的私有仓库的主机名就是Terraform企业版服务的主机名。
模块仓库支持版本化,可以在module
块中指定模块的版本约束。
GitHub
如果source
参数的值是以github.com
为前缀时,Terraform会将其自动识别为一个GitHub源:
module "consul" {
source = "github.com/hashicorp/example"
}
如上示例会自动使用HTTPS协议克隆仓库,如果要使用SSH协议,那么请使用如下的地址:
module "consul" {
source = "git@github.com:hashicorp/example.git"
}
GitHub源的处理与通用Git仓库是一样的,所以他们获取git凭证和通过ref参数引用特定版本的方式都是一样的。如果要访问私有仓库,需要额外配置git凭证。
Bitbucket
如果source
参数的值是以bitbucket.org
为前缀时,会将其自动识别为一个Bitbucket源:
module "consul" {
source = "bitbucket.org/hashicorp/terraform-consul-aws"
}
这种捷径方法只针对公共仓库有效,因为Terraform必须访问ButBucket API来了解仓库使用的是Git还是Mercurial协议,Terraform根据仓库的类型来决定将它作为一个Git仓库还是Mercurial仓库来处理。
通用Git仓库
可以在地址开头加上特殊的git::
前缀来指定使用任意的Git仓库,在前缀后跟随的是一个合法的Git地址。
使用 HTTPS 和 SSH 协议的例子:
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
参数来指定版本:
module "vpc" {
source = "git::https://example.com/vpc.git?ref=v1.2.0"
}
ref
参数会被用作git checkout
命令的参数,可以是分支名或是tag名。
使用SSH协议时,更推荐ssh://
的地址。也可以选择scp风格的语法,故意忽略ssh://
的部分,只留git::
,例如:
module "storage" {
source = "git::username@example.com:storage.git"
}
通用Mercurial仓库
可以在地址开头加上特殊的hg::
前缀来指定使用任意的Mercurial仓库,在前缀后跟随的是一个合法的Mercurial地址:
module "vpc" {
source = "hg::http://example.com/vpc.hg"
}
Terraform会通过运行hg clone
命令从Mercurial仓库安装模块代码,所以Terraform会使用本地Mercurial系统配置,包括访问凭证。要访问私有Mercurial仓库,必须先配置相应的凭证。
如果使用了SSH协议,那么会自动使用系统配置的SSH证书。通常情况下通过这种方法访问私有仓库,因为这样可以不需要交互式提示就可以访问私有仓库。
类似Git源,可以通过ref
参数指定非默认的分支或者标签来选择特定版本:
module "vpc" {
source = "hg::http://example.com/vpc.hg?ref=v1.2.0"
}
HTTP地址
当使用HTTP或HTTPS地址时,Terraform会向指定URL发送一个GET请求,期待返回另一个源地址。然后Terraform会再发送一个GET请求到之前响应的地址上,并附加一个查询参数terraform-get=1
,这样服务器可以选择当Terraform来查询时可以返回一个不一样的地址。
如果相应的状态码是成功的(200范围的成功状态码),Terraform就会通过以下位置来获取下一个访问地址:
- 响应头部的
X-Terraform-Get
值 - 如果响应内容是一个HTML页面,那么会检查名为
terraform-get
的html meta元素:<meta name="terraform-get" content="github.com/hashicorp/example" />
不管用哪种方式返回的地址,Terraform都会像本章提到的其他的源地址那样处理它。
如果HTTP/HTTPS地址需要认证凭证,可以在HOME
文件夹下配置一个.netrc
文件,详见使用.NETRC文件管理私人仓库。
也有一种特殊情况,如果Terraform发现地址有着一个常见的存档文件的后缀名,那么Terraform会跳过`terraform-get=1 重定向的步骤,直接将响应内容作为模块代码使用。
module "vpc" {
source = "https://example.com/vpc-module.zip"
}
目前支持的后缀名有:
zip
tar.bz2
和tbz2
tar.gz
和tgz
tar.xz
和txz
如果HTTP地址不以这些文件名结尾,但又的确指向模块存档文件,那么可以使用archive
参数来强制按照这种行为处理地址:
module "vpc" {
source = "https://example.com/vpc-module?archive=zip"
}
S3 Bucket
可以把模块存档保存在AWS S3桶里,使用s3::
作为地址前缀,后面跟随一个S3对象访问地址:
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主机实例配置
GCS Bucket
可以把模块存档保存在谷歌云GCS储桶里,使用gcs::
作为地址前缀,后面跟随一个GCS对象访问地址:
module "consul" {
source = "gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zip"
}
模块安装器会使用谷歌云SDK的凭据来访问GCS,要设置凭据,可以使用如下方式:
- 通过
GOOGLE_APPLICATION_CREDENTIALS
环境变量配置服务账号的密钥文件 - 如果是在谷歌云主机上运行的Terraform,可以使用默认凭据,详见向Compute Engine 进行身份验证
- 可以使用命令行
gcloud auth application-default login
设置
引用子文件夹中的模块
引用版本控制系统或是对象存储服务中的模块时,模块本身可能存在于存档文件的一个子文件夹内。可以使用特殊的//
语法来指定Terraform使用存档内特定路径作为模块代码所在位置,例如:
hashicorp/consul/aws//modules/consul-cluster
git::https://example.com/network.git//modules/vpc
https://example.com/network-module.zip//modules/vpc
s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/network.zip//modules/vpc
如果源地址中包含有参数,例如指定特定版本号的ref
参数,那么把子文件夹路径放在参数之前:
git::https://example.com/network.git//modules/vpc?ref=v1.2.0
Terraform会解压缩整个存档文件后,读取特定子文件夹。所以,对于一个存在于子文件夹中的模块来说,通过本地路径引用同一个存档内的另一个模块是安全的。
使用模块
可以把模块理解成类似函数,如同函数有输入参数表和输出值一样,Terraform代码有输入变量和输出值。
在module
块的块体内除了source
参数,还可以对该模块的输入变量赋值:
module "servers" {
source = "./app-cluster"
servers = 5
}
如上所述,将会创建./app-cluster
文件夹下Terraform声明的一系列资源,该模块的servers
输入变量的值被设定成了5。
在代码中新增、删除或是修改一个某块的source
,都需要重新运行terraform init
命令。默认情况下,该命令不会升级已安装的模块(例如source
未指定版本,过去安装了旧版本模块代码,那么执行 terraform init
不会自动更新到新版本);可以执行terraform init -upgrade
来强制更新到最新版本模块。
访问模块输出值
在模块中定义的资源和数据源都是被封装的,所以模块调用者无法直接访问它们的输出属性。然而,模块可以声明一系列输出值,来选择性地输出特定的数据供模块调用者使用。
例如,如果./app-cluster
模块定义了名为instance_ids
的输出值,那么模块的调用者可以像这样引用它:
module "servers" {
source = "./app-cluster"
servers = 5
}
resource "aws_elb" "example" {
# 访问被引用模块的输出值
instances = module.servers.instance_ids
}
模块元参数
除了source
以外,目前Terraform还支持在module
块上声明其他一些可选元参数:
version
:指定引用的模块版本count
和for_each
:这是Terraform 0.13开始支持的特性,类似resource
与data
,可以创建多个module
实例providers
:通过传入一个map可以指定模块中的Provider配置depends_on
:创建整个模块和其他资源之间的显式依赖,直到依赖项创建完毕,否则声明了依赖的模块内部所有的资源及内嵌的模块资源都会被推迟处理,模块的依赖行为与资源的依赖行为相同
除了上述元参数以外,lifecycle
参数目前还不能被用于模块,但关键字被保留以便将来实现。
模块版本约束
使用Terraform Registry作为模块源时,可以使用version
元参数约束使用的模块版本:
module "consul" {
source = "hashicorp/consul/aws"
version = "0.0.5"
servers = 3
}
version元参数的格式与Provider版本约束的格式一致。
在满足版本约束的前提下,Terraform会使用当前已安装的最新版本的模块实例。如果当前没有满足约束的版本被安装过,那么会下载符合约束的最新的版本。
version
元参数只能配合Terraform Registry使用,公共的或者私有的模块仓库都可以;其他类型的模块源可能支持版本化,也可能不支持;本地路径模块不支持版本化。
多实例模块
可以通过在module
块上声明for_each
或者count
来创造多实例模块,与资源、数据源块上的使用是一样的。
# 如下是模块publish_bucket内文件内容
variable "name" {} # 模块输入变量
resource "aws_s3_bucket" "example" {
bucket = var.name
# ...
}
resource "aws_iam_user" "deploy_user" {
# ...
}
# 外部调用模块
module "bucket" {
for_each = toset(["assets", "media"])
source = "./publish_bucket"
name = "${each.key}_bucket"
}
这个例子定义了一个位于./publish_bucket
目录下的本地子模块,模块创建了一个S3存储桶,封装了桶的信息以及其他实现细节。
通过for_each
参数声明了模块的多个实例,传入一个map
或是set
作为参数值。另外,因为使用了for_each
,所以在module
块里可以使用each
对象,例子中使用了each.key
;如果使用的是count
参数,那么可以使用count.index
。
子模块里创建的资源在执行计划或UI中的名称会以module.module_name[module index]
作为前缀,如果一个模块没有声明count
或者for_each
,那么资源地址将不包含module index
。
如上例,./publish_bucket
模块包含了aws_s3_bucket.example
资源,所以两个S3桶实例的名字分别是module.bucket["assets"].aws_s3_bucket.example
和module.bucket["media"].aws_s3_bucket.example
。
模块内的Provider
当代码中声明了多个模块时,资源如何与Provider实例关联就需要特殊考虑。
每一个资源都必须关联一个Provider配置。不像Terraform其他概念,Provider配置在Terraform项目中是全局的,可以跨模块共享。Provider配置声明只能放在根模块。
Provider有两种方式传递给子模块:隐式继承,显式通过module
块的providers
参数传递。
一个旨在被复用的模块不允许声明任何provider块,只有使用”代理 Provider”模式的情况除外。
Provider配置被用于相关资源的所有操作,包括销毁远程资源对象以及更新状态信息等。Terraform会在状态文件中保存针对最近用来执行所有资源变更的Provider配置的引用。当一个resource
块被删除时,状态文件中的相关记录会被用来定位到相应的配置,因为原来包含provider参数(如果声明了的话)的resource
块已经不存在了。
这导致了,必须确保删除所有相关的资源配置定义以后才能删除一个Provider配置。如果Terraform发现状态文件中记录的某个资源对应的Provider配置已经不存在了会报错,要求重新给出相关的Provider配置。
模块内的Provider版本限制
虽然Provider配置信息在模块间共享,每个模块还是得声明各自的模块需求,这样Terraform才能决定一个适用于所有模块配置的Provider版本。
为了定义这样的版本约束要求,可以在terraform
块中使用required_providers
块:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 2.7.0"
}
}
}
隐式Provider继承
为了方便,在一些简单的代码中,一个子模块会从调用者那里自动地继承默认的Provider配置。这意味着显式provider
块声明仅位于根模块中,并且下游子模块可以简单地声明使用该类型Provider的资源,这些资源会自动关联到根模块的Provider配置上。
例如,根模块可能只含有一个provider
块和一个module
块:
provider "aws" {
region = "us-west-1"
}
module "child" {
source = "./child"
}
子模块可以声明任意关联aws类型Provide 的资源而无需额外声明Provider配置:
resource "aws_s3_bucket" "example" {
bucket = "provider-inherit-example"
}
当每种类型的Provider都只有一个实例时推荐使用这种方式。
要注意的是,只有Provider配置会被子模块继承,Provider的source
或是版本约束条件则不会被继承。每一个模块都必须声明各自的Provider需求条件,这在使用非HashiCorp的Provider时尤其重要。
显式传递Provider
当不同的子模块需要不同的Provider实例,或者子模块需要的Provider实例与调用者自己使用的不同时,需要在module
块上声明providers
参数来传递子模块要使用的Provider实例。
如下示例:
# 在根模块中声明Provider
provider "aws" {
region = "us-west-1"
}
# 在根模块中声明另一个Provider
provider "aws" {
alias = "usw2"
region = "us-west-2"
}
# 通过参数给子模块传递Provider
module "example" {
source = "./example"
providers = {
aws = aws.usw2
}
}
module
块里的providers参数类似resource
块里的provider参数,区别是前者接收的是一个map而不是单个string,因为一个模块可能含有多个不同的Provider。providers
的ma 的键就是子模块中声明的Provider需求中的名字,值就是在当前模块中对应的Provider配置的名字。
如果module
块内声明了providers
参数,那么它将重载所有默认的继承行为,所以需要确保给定的map覆盖了子模块所需要的所有Provider,这避免了显式赋值与隐式继承混用时带来的混乱和意外。
额外的Provider配置(使用alias 参数的)将永远不会被子模块隐式继承,所以必须**显式**通过
providers`传递。比如,一个模块配置了两个AWS区域之间的网络打通,所以需要配置一个源区域Provider和目标区域Provider。这种情况下,根模块代码看起来是这样的:
provider "aws" {
alias = "usw1"
region = "us-west-1"
}
provider "aws" {
alias = "usw2"
region = "us-west-2"
}
module "tunnel" {
source = "./tunnel"
providers = {
aws.src = aws.usw1
aws.dst = aws.usw2
}
}
子目录./tunnel
必须包含像下面的例子那样声明”Provider 代理”,声明模块调用者必须用这些名字传递的Provider配置:
provider "aws" {
alias = "src"
}
provider "aws" {
alias = "dst"
}
./tunnel
模块中的每一种资源都应该通过provider
参数声明它使用的是aws.src还是aws.dst。
Provider代理配置块
一个Provider代理配置只包含alias
参数,它就是一个模块间传递Provider配置的占位符,声明了模块期待显式传递的额外(带有alias的)Provider配置。
需要注意的是,一个完全为空的Provider配置块也是合法的,但没有必要。只有在模块内需要带alias的Provider时才需要代理配置块。如果模块中只是用默认Provider时请不要声明代理配置块,也不要仅为了声明 Provider版本约束而使用代理配置块。
【参考】
Using Modules
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,在下面评论区告诉我^_^^_^