1.6.2.1. 引用模块

在 Terraform 代码中引用一个模块,使用的是 module 块。

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

1.6.2.1.1. 模块源

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

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

我们后面会一一讲解这些模块源的使用。source 使用的是 URL 风格的参数,但某些源支持在 source 参数中通过额外参数指定模块版本。

出于消除重复代码的目的我们可以重构我们的根模块代码,将一些拥有重复模式的代码重构为可反复调用的嵌入模块,通过本地路径来引用。

许多的模块源类型都支持从当前系统环境中读取认证信息,例如环境变量或系统配置文件。我们在介绍模块源的时候会介绍到这方面的信息。

我们建议每个模块把期待被重用的基础设施声明在各自的根模块位置上,但是直接引用其他模块的嵌入模块也是可行的。

1.6.2.1.1.1. 本地路径

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

module "consul" {
  source = "./consul"
}

一个本地路径必须以 ./ 或者 ../ 为前缀来标明要使用的本地路径,以区别于使用 Terraform Registry 路径。

本地路径引用模块和其他源类型有一个区别,本地路径引用的模块不需要下载相关源代码,代码已经存在于本地相关路径的磁盘上了。

1.6.2.1.1.2. Terraform Registry

Registry 目前是 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 块中指定模块的版本约束。

如果要引用私有仓库的模块,你需要首先通过配置命令行工具配置文件来设置访问凭证。

1.6.2.1.1.3. GitHub

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

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

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

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

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

1.6.2.1.1.4. Bitbucket

Terraform 发现 source 参数的值如果是以 bitbucket.org 为前缀时,会将其自动识别为一个 Bitbucket 源:

module "consul" {
  source = "bitbucket.org/hashicorp/terraform-consul-aws"
}

这种捷径方法只针对公共仓库有效,因为 Terraform 必须访问 ButBucket API 来了解仓库使用的是 Git 还是 Mercurial 协议。

Terraform 根据仓库的类型来决定将它作为一个 Git 仓库还是 Mercurial 仓库来处理。后面的章节会介绍如何为访问仓库配置访问凭证以及指定要使用的版本号。

1.6.2.1.1.5. 通用 Git 仓库

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

使用 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"
}

1.6.2.1.1.6. 通用 Mercurial 仓库

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

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"
}

1.6.2.1.1.7. HTTP 地址

当我们使用 HTTP 或 HTTPS 地址时,Terraform 会向指定 URL 发送一个 GET 请求,期待返回另一个源地址。这种间接的方法使得 HTTP 可以成为一个更复杂的模块源地址的指示器。

然后 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 文件,详见相关文档

也有一种特殊情况,如果 Terraform 发现地址有着一个常见的存档文件的后缀名,那么 Terraform 会跳过 terraform-get=1 重定向的步骤,直接将响应内容作为模块代码使用。

module "vpc" {
  source = "https://example.com/vpc-module.zip"
}

目前支持的后缀名有:

  • zip
  • tar.bz2tbz2
  • tar.gztgz
  • tar.xztxz

如果 HTTP 地址不以这些文件名结尾,但又的确指向模块存档文件,那么可以使用 archive 参数来强制按照这种行为处理地址:

module "vpc" {
  source = "https://example.com/vpc-module?archive=zip"
}

1.6.2.1.1.8. 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_IDAWS_SECRET_ACCESS_KEY 环境变量
  • HOME 目录下 .aws/credentials 文件内的默认 profile
  • 如果是在 AWS EC2 主机内运行的,那么会尝试使用搭载的 IAM 主机实例配置。

1.6.2.1.1.9. GCS Bucket

你可以把模块存档保存在谷歌云 GCS 储桶里,使用 gcs:: 作为地址前缀,后面跟随一个 GCS 对象访问地址

module "consul" {
  source = "gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zip"
}

模块安装器会使用谷歌云 SDK 的凭据来访问 GCS。要设置凭据,你可以:

  • 通过 GOOGLE_APPLICATION_CREDENTIALS 环境变量配置服务账号的密钥文件
  • 如果是在谷歌云主机上运行的 Terraform,可以使用默认凭据。访问相关文档获取完整信息
  • 可以使用命令行 gcloud auth application-default login 设置

1.6.2.1.2. 直接引用子文件夹中的模块

引用版本控制系统或是对象存储服务中的模块时,模块本身可能存在于存档文件的一个子文件夹内。我们可以使用特殊的 // 语法来指定 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 会解压缩整个存档文件后,读取特定子文件夹。所以,对于一个存在于子文件夹中的模块来说,通过本地路径引用同一个存档内的另一个模块是安全的。

1.6.2.1.3. 使用模块

我们刚才介绍了如何用 source 指定模块源,下面我们继续讲解如何在代码中使用一个模块。

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

module "servers" {
  source = "./app-cluster"

  servers = 5
}

在这个例子里,我们将会创建 ./app-cluster 文件夹下 Terraform 声明的一系列资源,该模块的 servers 输入变量的值被我们设定成了5。

在代码中新增、删除或是修改一个某块的 source,都需要重新运行 terraform init 命令。默认情况下,该命令不会升级已安装的模块(例如 source 未指定版本,过去安装了旧版本模块代码,那么执行 terraform init 不会自动更新到新版本);可以执行 terraform init -upgrade 来强制更新到最新版本模块。

1.6.2.1.4. 访问模块输出值

在模块中定义的资源和数据源都是被封装的,所以模块调用者无法直接访问它们的输出属性。然而,模块可以声明一系列输出值,来选择性地输出特定的数据供模块调用者使用。

举例来说,如果 ./app-cluster 模块定义了名为 instance_ids 的输出值,那么模块的调用者可以像这样引用它:

resource "aws_elb" "example" {
  # ...

  instances = module.servers.instance_ids
}

1.6.2.1.5. 其他的模块元参数

除了 source 以外,目前 Terraform 还支持在 module 块上声明其他一些可选元参数:

  • version:指定引用的模块版本,在后面的部分会详细介绍
  • countfor_each:这是 Terraform 0.13 开始支持的特性,类似 resourcedata,我们可以创建多个 module 实例
  • providers:通过传入一个 map 我们可以指定模块中的 Provider 配置,我们将在后面详细介绍
  • depends_on:创建整个模块和其他资源之间的显式依赖。直到依赖项创建完毕,否则声明了依赖的模块内部所有的资源及内嵌的模块资源都会被推迟处理。模块的依赖行为与资源的依赖行为相同

除了上述元参数以外,lifecycle 参数目前还不能被用于模块,但关键字被保留以便将来实现。

1.6.2.1.6. 模块版本约束

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

module "consul" {
  source  = "hashicorp/consul/aws"
  version = "0.0.5"

  servers = 3
}

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

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

1.6.2.1.7. 多实例模块

可以通过在 module 块上声明 for_each 或者 count 来创造多实例模块。在使用上 module 上的 for_eachcount 与资源、数据源块上的使用是一样的。

# my_buckets.tf
module "bucket" {
  for_each = toset(["assets", "media"])
  source   = "./publish_bucket"
  name     = "${each.key}_bucket"
}
# publish_bucket/bucket-and-cloudfront.tf
variable "name" {} # this is the input parameter of the module

resource "aws_s3_bucket" "example" {
  # Because var.name includes each.key in the calling
  # module block, its value will be different for
  # each instance of this module.
  bucket = var.name

  # ...
}

resource "aws_iam_user" "deploy_user" {
  # ...
}

这个例子定义了一个位于 ./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

1.6.2.1.8. 模块内的 Provider

当代码中声明了多个模块时,资源如何与 Provider 实例关联就需要特殊考虑。

每一个资源都必须关联一个 Provider 配置。不像 Terraform 其他的概念,Provider 配置在 Terraform 项目中是全局的,可以跨模块共享。Provider 配置声明只能放在根模块中。

Provider 有两种方式传递给子模块:隐式继承,或是显式通过 module 块的 providers 参数传递。

一个旨在被复用的模块不允许声明任何 provider 块,只有使用"代理 Provider"模式的情况除外,我们后面会介绍这种模式。

出于向前兼容 Terraform 0.10 及更早版本的考虑,Terraform 目前在模块代码中只用到了 Terraform 0.10 及更早版本的功能时,不会针对模块代码中声明 provider 块报错,但这是一个不被推荐的遗留模式。一个含有自己的 provider 块定义的遗留模块与 for_eachcountdepends_on 等 0.13 引入的新特性是不兼容的。

Provider 配置被用于相关资源的所有操作,包括销毁远程资源对象以及更新状态信息等。Terraform 会在状态文件中保存针对最近用来执行所有资源变更的 Provider 配置的引用。当一个 resource 块被删除时,状态文件中的相关记录会被用来定位到相应的配置,因为原来包含 provider 参数(如果声明了的话)的 resource 块已经不存在了。

这导致了,你必须确保删除所有相关的资源配置定义以后才能删除一个 Provider 配置。如果 Terraform 发现状态文件中记录的某个资源对应的 Provider 配置已经不存在了会报错,要求你重新给出相关的 Provider 配置。

1.6.2.1.9. 模块内的 Provider 版本限制

虽然 Provider 配置信息在模块间共享,每个模块还是得声明各自的模块需求,这样 Terraform 才能决定一个适用于所有模块配置的 Provider 版本。

为了定义这样的版本约束要求,可以在 terraform 块中使用 required_providers 块:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 2.7.0"
    }
  }
}

有关 Provider 的 source 和版本约束的信息我们已经在前文中有所记述,在此不再赘述。

1.6.2.1.10. 隐式 Provider 继承

为了方便,在一些简单的代码中,一个子模块会从调用者那里自动地继承默认的 Provider 配置。这意味着显式 provider 块声明仅位于根模块中,并且下游子模块可以简单地声明使用该类型 Provider 的资源,这些资源会自动关联到根模块的 Provider 配置上。

例如,根模块可能只含有一个 provider 块和一个 module 块:

provider "aws" {
  region = "us-west-1"
}

module "child" {
  source = "./child"
}

子模块可以声明任意关联 aws 类型 Provider 的资源而无需额外声明 Provider 配置:

resource "aws_s3_bucket" "example" {
  bucket = "provider-inherit-example"
}

当每种类型的 Provider 都只有一个实例时我们推荐使用这种方式。

要注意的是,只有 Provider 配置会被子模块继承,Provider 的 source 或是版本约束条件则不会被继承。每一个模块都必须声明各自的 Provider 需求条件,这在使用非 HashiCorp 的 Provider 时尤其重要。

1.6.2.1.11. 显式传递 Provider

当不同的子模块需要不同的 Provider 实例,或者子模块需要的 Provider 实例与调用者自己使用的不同时,我们需要在 module 块上声明 providers 参数来传递子模块要使用的 Provider 实例。例如:

# The default "aws" configuration is used for AWS resources in the root
# module where no explicit provider instance is selected.
provider "aws" {
  region = "us-west-1"
}

# An alternate configuration is also defined for a different
# region, using the alias "usw2".
provider "aws" {
  alias  = "usw2"
  region = "us-west-2"
}

# An example child module is instantiated with the alternate configuration,
# so any AWS resources it defines will use the us-west-2 region.
module "example" {
  source    = "./example"
  providers = {
    aws = aws.usw2
  }
}

module 块里的 providers 参数类似 resource 块里的 provider 参数,区别是前者接收的是一个 map 而不是单个 string,因为一个模块可能含有多个不同的 Provider。

providersmap 的键就是子模块中声明的 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

1.6.2.1.12. Provider 代理配置块

一个 Provider 代理配置只包含 alias 参数,它就是一个模块间传递 Provider 配置的占位符,声明了模块期待显式传递的额外(带有 alias 的)Provider 配置。

需要注意的是,一个完全为空的 Provider 配置块也是合法的,但没有必要。只有在模块内需要带 alias 的 Provider 时才需要代理配置块。如果模块中只是用默认 Provider 时请不要声明代理配置块,也不要仅为了声明 Provider 版本约束而使用代理配置块。

results matching ""

    No results matching ""