输入变量 variable

通常在代码书写中使用字面量硬编码,如果想要在创建、修改基础设施时动态传入一些值呢?
比如说在代码中定义Provider时用变量替代硬编码的访问密钥,或是由创建基础设施的用户来决定创建什么样尺寸的主机?
我们需要的是输入变量。

如果把一组Terraform代码想像成一个函数,那么输入变量就是函数的入参。输入变量用variable块进行定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
variable "availability_zone_names" {
type = list(string)
default = ["us-west-1a"]
}

variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

validation {
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

紧跟variable关键字的就是变量名。
在一个Terraform模块(同一个文件夹中的所有Terraform代码文件,不包含子文件夹)中变量名必须是唯一的。
我们在代码中可以通过var.的方式引用变量的值。
有一组关键字不可以被用作输入变量的名字:

  • source
  • version
  • providers
  • count
  • for_each
  • lifecycle
  • depends_on
  • locals

输入变量只能在声明该变量的目录下的代码中使用。
输入变量块中可以定义一些属性。
通过type指定值类型,通过default设置默认值,
通过description描述变量的作用/用途,
通过validation验证输入的值是否符合预期。
上述例子中,如果输入的image_id不符合正则表达式的要求,那么regex函数调用会抛出一个错误,
这个错误会被can函数捕获,输出false。
condition表达式如果为false,Terraform会返回error_message中定义的错误信息。
error_message应该完整描述输入变量校验失败的原因,以及输入变量的合法约束条件。

对输入变量赋值

  1. 对输入变量赋值有几种途径,一种是在调用terraform plan或是terraform apply命令时以参数的形式传入:
    1
    2
    3
    $ terraform apply -var="image_id=ami-abc123"
    $ terraform apply -var='image_id_list=["ami-abc123","ami-def456"]'
    $ terraform plan -var='image_id_map={"us-east-1":"ami-abc123","us-east-2":"ami-def456"}'
  2. 第二种方法是使用参数文件。
    参数文件的后缀名可以是.tfvars或是.tfvars.json。.tfvars文件使用HCL语法,.tfvars.json使用JSON语法。
    以.tfvars为例,参数文件中用HCL代码对需要赋值的参数进行赋值,例如:
    1
    2
    3
    4
    5
    image_id = "ami-abc123"
    availability_zone_names = [
    "us-east-1a",
    "us-west-1c",
    ]
    1
    2
    3
    4
    5
    6
    后缀名为.tfvars.json的文件用一个JSON对象来对输入变量赋值,例如:

    {
    "image_id": "ami-abc123",
    "availability_zone_names": ["us-west-1a", "us-west-1c"]
    }
    调用terraform命令时,通过-var-file参数指定要用的参数文件,例如:
    1
    terraform apply -var-file="testing.tfvars"
    1
    terraform apply -var-file="testing.tfvars.json"
    有两种情况,你无需指定参数文件:
  • 当前模块内有名为terraform.tfvars或是terraform.tfvars.json的文件
  • 当前模块内有一个或多个后缀名为.auto.tfvars或是.auto.tfvars.json的文件
    Terraform会自动使用这两种自动参数文件对输入参数赋值。
  1. 可以通过设置名为TF_VAR_的环境变量为输入变量赋值,例如:

    1
    2
    $ export TF_VAR_image_id=ami-abc123
    $ terraform plan

    在环境变量名大小写敏感的操作系统上,Terraform要求环境变量中的\与Terraform代码中定义的输入变量名大小写完全一致。
    环境变量传值非常适合在自动化流水线中使用,尤其适合用来传递敏感数据,类似密码、访问密钥等。

  2. 交互界面传值
    在前面介绍validation的例子中我们看到过,当从命令行界面执行terraform操作,Terraform无法通过其他途径获取一个输入变量的值,而该变量也没有定义默认值时,Terraform会进行最后的尝试,在交互界面上要求我们给出变量值。

输入变量赋值优先级

当上述的赋值方式同时存在时,同一个变量可能会被赋值多次。Terraform会使用新值覆盖旧值。

Terraform加载变量值的顺序是:

  1. 环境变量
  2. terraform.tfvars文件(如果存在的话)
  3. terraform.tfvars.json文件(如果存在的话)
  4. 所有的.auto.tfvars或者.auto.tfvars.json文件,以字母顺序排序处理
  5. 通过-var或是-var-file命令行参数传递的输入变量,按照在命令行参数中定义的顺序加载

假如以上方式均未能成功对变量赋值,那么Terraform会尝试使用默认值;
对于没有定义默认值的变量,Terraform会采用交互界面方式要求用户输入一个。
对于某些Terraform命令,如果执行时带有-input=false参数禁用了交互界面传值方式,那么就会报错。

输出值 output

同样的我们把一组Terraform代码想像成一个函数,那么输入变量就是函数的入参;
函数可以有入参,也可以有返回值,同样的,Terraform代码也可以有返回值,这就是输出值。
Terraform支持多返回值,在当前模块apply一段Terraform代码,运行成功后命令行会输出代码中定义的返回值。
另外我们也可以通过terraform output命令来输出当前模块对应的状态文件中的返回值。

输出值的声明

输出值的声明使用输出块,例如:

1
2
3
output "instance_ip_addr" {
value = aws_instance.server.private_ip
}

output关键字后紧跟的就是输出值的名称。在当前模块内的所有输出值的名字都必须是唯一的。
output块内的value参数即为输出值,它可以像是上面的例子里那样某个resource的输出属性,也可以是任意合法的表达式。

输出值只有在执行terraform apply后才会被计算,光是使用terraform plan并不会计算输出值。

Terraform代码中无法引用本目录下定义的输出值。
output块还有一些可选的属性:

  • description, 与输入变量的description类似
  • sensitive, 一个输出值可以标记sensitive为true,表示该输出值含有敏感信息。被标记sensitive的输出值只是在执行terraform apply命令成功后会打印”"以取代真实的输出值,执行terraform output时也会输出”",但仍然可以通过执行terraform output -json看到实际的敏感值。
    需要注意的是,标记为sensitive输出值仍然会被记录在状态文件中,任何有权限读取状态文件的人仍然可以读取到敏感数据。
  • depends_on, terraform会解析代码所定义的各种data、resource,以及他们之间的依赖关系,例如,创建虚拟机时用的image_id参数是通过data查询而来的,那么虚拟机实例就依赖于这个镜像的data,Terraform会首先创建data,得到查询结果后,再创建虚拟机resource。一般来说,data、resource之间的创建顺序是由Terraform自动计算的,不需要代码的编写者显式指定。但有时有些依赖关系无法通过分析代码得出,这时我们可以在代码中通过depends_on显式声明依赖关系。

一般output很少会需要显式依赖某些资源,但有一些特殊场景,例如在当前代码中调用一个模块(可以理解成调用另一个目录中的Terraform代码创建一些资源)时,调用者希望在模块资源全部创建完毕以后才继续后续的创建工作,这时我们可以为模块设计一个output,通过depends_on显式声明依赖关系,以确保该output必须在所有模块资源成功创建以后才能被读取,这样我们就可以在模块尺度上控制资源的创建顺序。

depends_on的用法如下:

1
2
3
4
5
6
7
8
9
10
output "instance_ip_addr" {
value = aws_instance.server.private_ip
description = "The private IP address of the main server instance."

depends_on = [
# Security group rule must be created before this IP address could
# actually be used, otherwise the services will be unreachable.
aws_security_group_rule.local_access,
]
}

不鼓励针对output定义depends_on,只能作为最后的手段加以应用。如果不得不针对output定义depends_on,请务必通过注释说明原因,方便后人进行维护。

  • precondition, output 块上的 precondition 对应于 variable 块中的 validation 块。validation 块检查输入变量值是否符合模块的要求,precondition 则确保模块的输出值满足某种要求。我们可以通过 precondition 来防止 Terraform 把一个不合法的处置值写入状态文件。我们可以在合适的场景下通过 precondition 来保护上一次 apply 留下的合法的输出值。

Terraform 在计算输出值的 value 表达式之前执行 precondition 检查,这可以防止 value 表达式中的潜在错误被激发。

资源 resource

资源是Terraform最重要的组成部分,资源通过resource块来定义,一个resource可以定义一个或多个基础设施资源对象,例如VPC、虚拟机,或是DNS记录、Consul的键值对数据等。

资源语法

资源通过resource块定义,我们首先讲解通过resource块定义单个资源对象的场景。

1
2
3
4
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}

资源参数

不同资源定义了不同的可赋值的属性,官方文档将之称为参数(Argument),有些参数是必填的,有些参数是可选的。使用某项资源前可以通过阅读相关文档了解参数列表以及他们的含义、赋值的约束条件。

参数值可以是简单的字面量,也可以是一个复杂的表达式。

资源类型的文档

每一个Terraform Provider都有自己的文档,用以描述它所支持的资源类型种类,以及每种资源类型所支持的属性列表。

大部分公共的Provider都是通过Terraform Registry连带文档一起发布的。当我们在Terraform Registry站点上浏览一个Provider的页面时,我们可以点击”Documentation”链接来浏览相关文档。Provider的文档都是版本化的,我们可以选择特定版本的Provider文档。

资源的行为

一个resource块声明了作者想要创建的一个确切的基础设施对象,并且设定了各项属性的值。如果我们正在编写一个新的Terraform代码文件,那么代码所定义的资源仅仅只在代码中存在,并没有与之对应的实际的基础设施资源存在。

对一组Terraform代码执行terraform apply可以创建、更新或者销毁实际的基础设施对象,Terraform会制定并执行变更计划,以使得实际的基础设施符合代码的定义。

每当Terraform按照一个resource块创建了一个新的基础设施对象,这个实际的对象的id会被保存进Terraform状态中,使得将来Terraform可以根据变更计划对它进行更新或是销毁操作。如果一个resource块描述的资源在状态文件中已有记录,那么Terraform会比对记录的状态与代码描述的状态,如果有必要,Terraform会制定变更计划以使得资源状态能够符合代码的描述。

这种行为适用于所有资源而无关其类型。创建、更新、销毁一个资源的细节会根据资源类型而不同,但是这个行为规则却是普适的。

访问资源输出属性

资源不但可以通过参数传值,成功创建的资源还对外输出一些通过调用API才能获得的只读数据,经常包含了一些我们在实际创建一个资源之前无法获知的数据,比如云主机的id等,官方文档将之称为属性(Attribute)。我们可以在同一模块内的代码中引用资源的属性来创建其他资源或是表达式。在表达式中引用资源属性的语法是..

要获取一个资源类型输出的属性列表,我们可以查阅对应的Provider文档,一般在文档中会专门记录资源的输出属性列表。

数据源 data

数据源允许查询或计算一些数据以供其他地方使用。使用数据源可以使得Terraform代码使用在Terraform管理范围之外的一些信息,或者是读取其他Terraform代码保存的状态。

每一种Provider都可以在定义一些资源类型的同时定义一些数据源。

使用数据源

1
2
3
4
5
6
7
8
9
data "aws_ami" "example" {
most_recent = true

owners = ["self"]
tags = {
Name = "app-server"
Tested = "true"
}
}

同资源resource类似,一个数据源类型以及它的名称一同构成了该数据源的标识符,所以数据源类型加名称的组合在同一模块内必须是唯一的。

在data块体(花括号中间的内容)是传给数据源的查询条件。查询条件参数的种类取决于数据源的类型,在上述例子中,most_recent、owners和tags都是定义查询aws_ami数据源时使用的查询条件。

与数据源这种特殊资源不同的是,资源(使用resource块定义的)是一种“托管资源”。这两种资源都可以接收参数并对外输出属性,但托管资源会触发Terraform对基础设施对象进行增删改操作,而数据源只会触发读取操作。

引用数据源

引用数据源数据的语法是data...

1
2
3
4
resource "aws_instance" "web" {
ami = data.aws_ami.web.id
instance_type = "t1.micro"
}