Terraform学习2

在上一篇文章中我们已经会使用Terraform创建一个优雅的项目了,这一章中,我们讲探讨一下Terraform的一些更深入的内容。

推荐文章&文档内容来源:Introduction · 《Terraform入门教程》 (lonegunmanb.github.io)

、https://developer.hashicorp.com/terraform/language

官网文档

工作目录下的文件

.terraform文件夹

存储不同provider的云厂商

这是云厂商对应提供的SDK文件包位置,是Terraform根据provider下载相关插件到本地。

建议

有的时候下载某些Provider会非常缓慢,或是在开发环境中存在许多的Terraform项目,每个项目都保有自己独立的插件文件夹非常浪费磁盘,这时我们可以使用插件缓存

Provider · 《Terraform入门教程》 (lonegunmanb.github.io)

terraform.tfstate

在这里,Terraform引入了一个独特的概念——状态管理,这是Ansible等配置管理工具或是自研工具调用SDK操作基础设施的方案所没有的。简单来说,Terraform将每次执行基础设施变更操作时的状态信息保存在一个状态文件中,默认情况下会保存在当前工作目录下的terraform.tfstate文件里。

查询到的data以及创建的resource信息等都被以json格式保存在tfstate文件里。

极其重要的安全警示——tfstate是明文的

关于Terraform状态,还有极其重要的事,所有考虑在生产环境使用Terraform的人都必须格外小心并再三警惕:Terraform的状态文件是明文的,这就意味着代码中所使用的一切机密信息都将以明文的形式保存在状态文件里。

这是Terraform从设计之初就确定的,并且在可见的未来不会有改善。不论你是在代码中明文硬编码,还是使用参数(variable,我们之后的章节会介绍),亦或是妙想天开地使用函数在运行时从外界读取,都无法改变这个结果。

解决之道有两种,一种是使用Vault或是AWS Secret Manager这样的动态机密管理工具生成临时有效的动态机密(比如有效期只有5分钟,即使被他人读取到,机密也早已失效);另一种就是我们下面将要介绍的——Terraform Backend。

生产环境的tfstate管理方案——Backend

到目前为止我们的tfstate文件是保存在当前工作目录下的本地文件,假设我们的计算机损坏了,导致文件丢失,那么tfstate文件所对应的资源都将无法管理,而产生资源泄漏。

另外如果我们是一个团队在使用Terraform管理一组资源,团队成员之间要如何共享这个状态文件?能不能把tfstate文件签入源代码管理工具进行保存?

把tfstate文件签入管代码管理工具是非常错误的,这就好比把数据库签入了源代码管理工具,如果两个人同时签出了同一份tfstate,并且对代码做了不同的修改,又同时apply了,这时想要把tfstate签入源码管理系统可能会遭遇到无法解决的冲突。

为了解决状态文件的存储和共享问题,Terraform引入了远程状态存储机制,也就是Backend。Backend是一种抽象的远程存储接口,如同Provider一样,Backend也支持多种不同的远程存储服务:

Terraform Remote Backend分为两种:

  • 标准:支持远程状态存储与状态锁
  • 增强:在标准的基础上支持远程操作(在远程服务器上执行plan、apply等操作)

目前增强型Backend只有Terraform Cloud云服务一种

状态锁是指,当针对一个tfstate进行变更操作时,可以针对该状态文件添加一把全局锁,确保同一时间只能有一个变更被执行。不同的Backend对状态锁的支持不尽相同,实现状态锁的机制也不尽相同,例如consul backend就通过一个.lock节点来充当锁,一个.lockinfo节点来描述锁对应的会话信息,tfstate文件被保存在backend定义的路径节点内;s3 backend则需要用户传入一个Dynamodb表来存放锁信息,而tfstate文件被存储在s3存储桶里。名为etcd的backend对应的是etcd v2,它不支持状态锁;etcdv3则提供了对状态锁的支持,等等等等。读者可以根据实际情况,挑选自己合适的Backend。

后续我将以consul为范例为读者演示Backend机制

terraform.lock.hcl

Terraform 入门知识: terraform.lock.hcl 简介 · Ruby China (ruby-china.org)

写好了 Terraform 代码,需要先执行 terraform init 初始化,这条命令会创建一个文件 .terraform.lock.hcl。

熟悉其他编程语言的程序员,往往会误解这个文件,认为它是 module 的版本锁定文件。

事实上它并不锁定 module 的版本,它只锁定 provider 的版本。

这是一个 .terraform.lock.hcl 范例,里面并没有各种 module 的版本信息,那它有什么?

1. provider "registry.terraform.io/hashicorp/aws" {
2.   version     = "3.63.0"
3.   constraints = ">= 2.7.0, >= 2.42.0, >= 2.49.0, >= 3.27.0, 3.63.0"
4.   hashes = [
5.     "h1:Z+2GvXLgqQ/uPMH8dv+dXJ/t+jd6sriYjhCJS6kSO6g=",
6.     "h1:lf8Qex8bhCmh8TUEAU6H4brzjy3+d4BXB6gcOYnNtNY=",
7.     "zh:42c6c98b294953a4e1434a331251e539f5372bf6779bd61ab5df84cac0545287",
8.     "zh:5493773762a470889c9a23db97582d3a82035847c8d3bd13323b4c3012abf325",
9.     "zh:550d22ff9fed4d817a922e7b84bd9d1f2ef8d3afa00832cf66b8cd5f0e6dc748",
10.    "zh:632cb5e2d9d5041875f57174236eafe5b05dbf26750c1041ab57eb08c5369fe2",
11.    "zh:7cfeaf5bde1b28bd010415af1f3dc494680a8374f1a26ec19db494d99938cc4e",
12.    "zh:99d871606b67c8aefce49007315de15736b949c09a9f8f29ad8af1e9ce383ed3",
13.    "zh:c4fc8539ffe90df5c7ae587fde495fac6bc0186fec2f2713a8988a619cef265f",
14.    "zh:d0a26493206575c99ca221d78fe64f96a8fbcebe933af92eea6b39168c1f1c1d",
15.    "zh:e156fdc964fdd4a7586ec15629e20d2b06295b46b4962428006e088145db07d6",
16.    "zh:eb04fc80f652b5c92f76822f0fec1697581543806244068506aed69e1bb9b2af",
17.    "zh:f5638a533cf9444f7d02b5527446cdbc3b2eab8bcc4ec4b0ca32035fe6f479d3",
18.   ]
19. }

让我们来逐行分析这个文件。

第 1 行,你的 terraform 代码依赖 aws provider。

第 2 行,provider 的 version。由 terraform 根据一些规则帮你选定。

第 3 行,constraints 表示 provider 版本的限制条件。你的代码中引入了多个 module 后,每个 module 都对 aws provider 有版本要求,于是 Terraform 就把它们汇总了起来。所以第 2 行的版本其实是由第 3 行的 constraints 决定的。

第 4 行,hashes 代表 checksum,用来确保下载的 provider 并没有被人恶意篡改过。

Consul简介以及安装

Consul是HashiCorp推出的一个开源工具,主要用来解决服务发现、配置中心以及Service Mesh等问题;Consul本身也提供了类似ZooKeeper、Etcd这样的分布式键值存储服务,具有基于Gossip协议的最终一致性,所以可以被用来充当Terraform Backend存储。

安装Consul十分简单,如果你是Ubuntu用户:

curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
sudo apt-get update && sudo apt-get install -y consul

对于CentOS用户:

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install consul

对于Macos用户:

brew tap hashicorp/tap
brew install hashicorp/tap/consul

对于Windows用户,如果按照前文安装Terraform教程已经配置了Chocolatey的话:

choco install consul

安装完成后的验证:

$ consul
Usage: consul [--version] [--help] <command> [<args>]Available commands are:acl            Interact with Consul's ACLsagent          Runs a Consul agentcatalog        Interact with the catalogconfig         Interact with Consul's Centralized Configurationsconnect        Interact with Consul Connectdebug          Records a debugging archive for operatorsevent          Fire a new eventexec           Executes a command on Consul nodesforce-leave    Forces a member of the cluster to enter the "left" stateinfo           Provides debugging information for operators.intention      Interact with Connect service intentionsjoin           Tell Consul agent to join clusterkeygen         Generates a new encryption keykeyring        Manages gossip layer encryption keyskv             Interact with the key-value storeleave          Gracefully leaves the Consul cluster and shuts downlock           Execute a command holding a locklogin          Login to Consul using an auth methodlogout         Destroy a Consul token created with loginmaint          Controls node or service maintenance modemembers        Lists the members of a Consul clustermonitor        Stream logs from a Consul agentoperator       Provides cluster-level tools for Consul operatorsreload         Triggers the agent to reload configuration filesrtt            Estimates network round trip time between nodesservices       Interact with servicessnapshot       Saves, restores and inspects snapshots of Consul server statetls            Builtin helpers for creating CAs and certificatesvalidate       Validate config files/directoriesversion        Prints the Consul versionwatch          Watch for changes in Consul

安装完Consul后,我们可以启动一个测试版Consul服务,注意,使用阿里云等云服务器启动consul时,需要填入私网ip的信息。

consul agent -dev -ui -node=consul-dev -client=172.18.168.62

访问 : 公网IP + 8500端口

由于我们的环境是在阿里云,云服务器上使用的,Consul有多个端口需要开放,使用安全组策略选择网段都开放方便使用。

(16条消息) consul各端口作用分析_huchao_lingo的博客-CSDN博客_consul 端口

我们切回Terrform的工作目录下,修改主目录下version.tf的内容【同时我还修改了ecs的main.tf的内容,用来证明我的tfstate文件在我重新init之后不会在本地修改,而是讲tfstate文件保存至consul中。】:

# 定义云厂商(版本约束)
terraform {required_providers {alicloud = {source = "aliyun/alicloud"version = "1.195.0"}}
#设置backend方案backend "consul" {address = "39.108.210.93:8500"scheme = "http"path = "my-first-test"}
}

重新terraform init,plan,apply

我们看到terraform.tfstate如我们所想的一样,最后的修改时间并未修改,我们去consul的ui看看吧。

Key/Value成功存入consul中,点进去my-first-test看看

确实是tfstate文件内容无疑,此次使用的是Consul的方案去解决生产环境下tfstate文件的敏感问题,我们将资源destroy掉再看看Consul中tfstate内容。

至此,Consul方案的初步使用学习完成,如果需要使用Consul去进行服务发现等内容,后续可以自行查询相关内容。本次的案例是为了给读者们一个清晰的案例去感受backend。

观察锁文件

resource "ucloud_vpc" "vpc" {cidr_blocks = ["10.0.0.0/16"]provisioner "local-exec" {command = "sleep 1000"}
}

这次的变化是我们在ucloud_vpc的定义上添加了一个local-exec类型的provisioner。provisioner我们在后续的章节中会专门叙述,在这里读者只需要理解,Terraform进程在成功创建了该VPC后,会在执行Terraform命令行的机器上执行一条命令:sleep 1000,这个时间足以将Terraform进程阻塞足够长的时间,以便让我们观察锁信息了。

让我们执行terraform apply,这一次apply将会被sleep阻塞,而不会成功完成

这一次情况发生了变化,我们看到除了my-ucloud-project这个键之外,还多了一个同名的文件夹。让我们点击进入文件夹:

lock的内容为空:Consul UI提醒我们,该键值对目前正被锁定,而它的内容是空。

.lockinfo内容: .lockinfo里记录了锁ID、我们执行的操作,以及其他的一些信息。

让我们另起一个新的命令行窗口,在同一个工作目录下尝试另一次执行terraform apply,另一个人试图对同一个tfstate执行变更的尝试失败,因为它无法顺利获取到锁。

Terraform命令行进程在接收到ctrl-c信号时,会首先把当前已知的状态信息写入Backend内,然后释放Backend上的锁,再结束进程。但是如果Terraform进程是被强行杀死,或是机器掉电,那么在Backend上就会遗留一个锁,导致后续的操作都无法执行,这时我们需要用terraform force-unlock命令强行删除锁,我们将在后续的章节中详细叙述。

backend配置只允许硬编码,或者不传值。

workspace

当前我们有一个状态文件,名字是my-ucloud-project。然后我们在工作目录下执行这样的命令:

$ terraform workspace new feature1
Created and switched to workspace "feature1"!You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

通过调用workspace命令,我们成功创建了名为feature1的Workspace。这时我们观察.terraform文件夹:

.terraform
├── environment
├── modules
│   └── modules.json
└── plugins├── registry.terraform.io│   ├── ucloud
......

我们会发现多了一个environment文件,它的内容是feature1。这实际上就是Terraform用来保存当前上下文环境使用的是哪个Workspace的文件。

我们可以通过以下命令来查询当前Backend下所有的Workspace:

$ terraform workspace listdefault
* feature1

我们有default和feature1两个Workspace,当前我们工作在feature1上。我们可以用以下命令切换回default:

$ terraform workspace select default
Switched to workspace "default".

我们可以用以下命令确认我们成功切换回了default:

$ terraform workspace show
default

我们可以用以下命令删除feature1:

$ terraform workspace delete feature1
Deleted workspace "feature1"!

一些语法

any

声明类型时如果不想有任何的约束,那么可以用 any

variable "no_type_constraint" {type = any
}

这样的话,Terraform 可以将任何类型的数据赋予它。

null

存在一种特殊值是无类型的,那就是 nullnull 代表数据缺失。如果我们把一个参数设置为 null,Terraform 会认为你忘记为它赋值。如果该参数有默认值,那么 Terraform 会使用默认值;如果没有又恰巧该参数是必填字短,Terraform 会报错。null 在条件表达式中非常有用,你可以在某项条件不满足时跳过对某参数的赋值。

object 的 optional 成员

自 Terraform 1.3 开始,我们可以在 object 类型定义中使用 optional 修饰属性。

在 1.3 之前,如果一个 variable 的类型为 object,那么使用时必须传入一个结构完全相符的对象。例如:

variable "an_object" {type = object({a = stringb = stringc = number})
}

如果我们想传入一个对象给 var.an_object,但不准备给 bc 赋值,我们必须这样:

{a = "a"b = nullc = null
}

Terraform 1.3 允许我们为一个属性添加 optional 声明,还是用上面的例子:

variable "with_optional_attribute" {type = object({a = string                # a required attributeb = optional(string)      # an optional attributec = optional(number, 127) # an optional attribute with default value})
}

在这里我们将 b 声明为 optional,如果传入的对象没有 b,则会使用 null 作为值;c 不但声明为 optional 的,还添加了 127 作为默认值,传入的对象如果没有 c,那么会使用 127 作为它的值。

optional 修饰符有这样两个参数:

  • 类型:(必填)第一个参数标明了属性的类型
  • 默认值:(选填)第二个参数定义了 Terraform 在对象中没有定义该属性值时使用的默认值。默认值必须与类型参数兼容。如果没有指定默认值,Terraform 会使用 null 作为默认值。

Terraform 采用自上而下的顺序来设置对象的默认值,也就是说,Terraform 会先应用 optional 修饰符中的指定的默认值,然后再为其中可能存在的内嵌对象设置默认值。

website,不但其自身是 optional 的,其内部包含了数个 optional 的属性以及默认值。

condition

condition参数是一个bool类型的参数,我们可以用一个表达式来定义如何界定输入变量是合法的。当contidion为true时输入变量合法,反之不合法。condition表达式中只能通过var.\引用当前定义的变量,并且它的计算不能产生错误。

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

假如表达式的计算产生一个错误是输入变量验证的一种判定手段,那么可以使用can函数来判定表达式的执行是否抛错。例如:

variable "image_id" {type        = stringdescription = "The id of the machine image (AMI) to use for the server."validation {# regex(...) fails if it cannot find a matchcondition     = can(regex("^ami-", var.image_id))error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."}
}

上述例子中,如果输入的image_id不符合正则表达式的要求,那么regex函数调用会抛出一个错误,这个错误会被can函数捕获,输出false

condition表达式如果为false,Terraform会返回error_message中定义的错误信息。error_message应该完整描述输入变量校验失败的原因,以及输入变量的合法约束条件。

禁止输入变量为空

该功能自 Terraform v1.1.0 开始被引入

输入变量的 nullable 参数控制模块调用者是否可以将 null 分配给变量。

variable "example" {type     = stringnullable = false
}

nullable 的默认值为 true。当 nullabletrue 时,null 是变量的有效值(可以为空),并且模块代码必须始终考虑变量值为 null 的可能性。将 null 作为模块输入参数传递将覆盖输入变量上定义的默认值。

nullable 设置为 false 可确保变量值在模块内永远不会为空。如果 nullablefalse 并且输入变量定义有默认值,则当模块输入参数为 null 时,Terraform 将使用默认值。

source{}补充

depends_on

使用depends_on可以显式声明资源之间哪些Terraform无法自动推导出的隐含的依赖关系。只有当资源间确实存在依赖关系,但是彼此间又没有数据引用的场景下才有必要使用depends_on

count

一般来说,一个resource块定义了一个对应的实际基础设施资源对象。但是有时候我们希望创建多个相似的对象,比如创建一组虚拟机。Terraform提供了两种方法实现这个目标:countfor_each

count参数可以是任意自然数,Terraform会创建count个资源实例,每一个实例都对应了一个独立的基础设施对象,并且在执行Terraform代码时,这些对象是被分别创建、更新或者销毁的:

resource "aws_instance" "server" {count = 4 # create four similar EC2 instancesami           = "ami-a1b2c3d4"instance_type = "t2.micro"tags = {Name = "Server ${count.index}"}
}

for_each

for_each是Terraform 0.12.6开始引入的新特性。一个resource块不允许同时声明countfor_each

for_each参数可以是一个map或是一个set(string),Terraform会为集合中每一个元素都创建一个独立的基础设施资源对象,和count一样,每一个基础设施资源对象在执行Terraform代码时都是独立创建、修改、销毁的。

使用map的例子:

resource "azurerm_resource_group" "rg" {for_each = {a_group = "eastus"another_group = "westus2"}name     = each.keylocation = each.value
}

使用set(string)的例子:

resource "aws_iam_user" "the-accounts" {for_each = toset( ["Todd", "James", "Alice", "Dottie"] )name     = each.key

我们可以在声明了for_each参数的resource块内使用each对象来访问当前的迭代器对象

  • each.key:map的键,或是set中的值
  • each.value:map的值,或是set中的值

如果for_each的值是一个set,那么each.keyeach.value是相等的。

当一个resource声明了for_each时,Terraform会把这种多资源实例对象与没有count参数的单资源实例对象区别开:

  • 访问单资源实例对象:<TYPE>.<NAME>(例如:aws_instance.server)
  • 访问多资源实例对象:<TYPE>.<NAME>[<KE>] (例如:aws_instance.server["ap-northeast-1"]aws_instance.server["ap-northeast-2"])

由于Terraform没有用以声明set的字面量,所以我们有时需要使用toset函数把list(string)转换为set(string):

locals {subnet_ids = toset(["subnet-abcdef","subnet-012345",])
}resource "aws_instance" "server" {for_each = local.subnet_idsami           = "ami-a1b2c3d4"instance_type = "t2.micro"subnet_id     = each.key # note: each.key and each.value are the same for a settags = {Name = "Server ${each.key}"}
}

在这里我们用toset把一个list(string)转换成了set(string),然后赋予for_each在转换过程中,list中所有重复的元素会被抛弃,只剩下不重复的元素,例如toset(["b", "a", "b"])的结果只有"a""b",并且set的元素没有特定顺序。

如果我们要把一个输入变量赋予for_each,我们可以直接定义变量的类型约束来避免显式调用toset转换类型:

variable "subnet_ids" {type = set(string)
}resource "aws_instance" "server" {for_each = var.subnet_ids# (and the other arguments as above)
}

在for_each和count之间选择

如果创建的资源实例彼此之间几乎完全一致,那么count比较合适。如果彼此之间的参数差异无法直接从count的下标派生,那么使用for_each会更加安全。

lifecycle

lifecycle配置影响了Terraform如何构建并遍历依赖图。作为结果,lifecycle内赋值仅支持字面量,因为它的计算过程发生在Terraform计算的极早期。这就是说,例如prevent_destroycreate_before_destroy的值只能是true或者falseignore_changesreplace_triggered_by的列表内只能是硬编码的属性名。

Precondition 与 Postcondition

请注意,Precondition 与 Postcondition 是从 Terraform v1.2.0 开始被引入的功能。

lifecycle 块中声明 preconditionpostcondition 块可以为资源、数据源以及输出值创建自定义的验证规则。

Terraform 在计算一个对象之前会首先检查该对象关联的 precondition,并且在对象计算完成后执行 postcondition 检查。

每一个 preconditionpostcondition 块都需要一个 condition 参数。该参数是一个表达式,在满足条件时返回 true,否则返回 false

预置器provisioner

默认情况下,资源对象被创建时会运行预置器,在对象更新、销毁时则不会运行。预置器的默认行为时为了引导一个系统。

如果我们设置预置器的when参数为destroy,那么预置器会在资源被销毁时执行:

resource "aws_instance" "web" {# ...provisioner "local-exec" {when    = destroycommand = "echo 'Destroy-time provisioner'"}
}

销毁时预置器在资源被实际销毁前运行。如果运行失败,Terraform会报错,并在下次运行terraform apply操作时重新执行预置器。在这种情况下,需要仔细关注销毁时预置器以使之能够安全地反复执行。

操作超时设置

有些资源类型提供了特殊的timeouts内嵌块参数,它允许我们配置我们允许操作持续多长时间,超时将被认定为失败。比如说,aws_db_instance资源允许我们分别为createupdatedelete操作设置超时时间。

provider{}补充

Provider配置不能依赖于数据源的输出。

内建Provider

绝大多数Provider是以插件形式单独分发的,但是目前有一个Provider是内建于Terraform主进程中的,那就是terraform_remote_state data source。该Provider由于是内建的,所以使用时不需要在terraform中声明required_providers。这个内建Provider的源地址是terraform.io/builtin/terraform

多Provider实例

一个Local Name是在一个模块中对一个Provider的唯一的标识。

我们也可以使用alias别名来区隔同类Provider的不同实例:

有条件地设置一个默认属性

可以根据新输入参数 var.legacy_filenames 的值来有条件地覆盖 website 对象中 index_document 以及 error_document 的设置:

variable "legacy_filenames" {type     = booldefault  = falsenullable = false
}module "buckets" {source = "./modules/buckets"buckets = [{name = "maybe_legacy"website = {error_document = var.legacy_filenames ? "ERROR.HTM" : nullindex_document = var.legacy_filenames ? "INDEX.HTM" : null}},]
}

var.legacy_filenames 设置为 true 时,调用会覆盖 document 的文件名。当它的值为 false 时,调用不会指定这两个文件名,这样就会使得模块使用定义的默认值。

output{}补充

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

output "instance_ip_addr" {value = aws_instance.server.private_ip
}

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

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

output块还有一些可选的属性:description(描述)、sensitive(敏b感)、depends_on(依赖【不建议写这个,如果需要方便后续的可读性,可以用注释的方式去说明依赖】)

precondition

output 块从 Terraform v1.2.0 开始也可以包含一个 precondition 块。

output 块上的 precondition 对应于 variable 块中的 validation 块。

validation 块检查输入变量值是否符合模块的要求,precondition 则确保模块的输出值满足某种要求。

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

data{}补充

如果数据源的查询参数涉及到的表达式只引用了字面量或是在执行terraform plan时就已知的数据(比如输入变量),那么数据源会在执行Terraform的"refersh"阶段时被读取,然后Terraform会构建变更计划。这保证了在制定变更计划时Terraform可以使用这些数据源的返回数据。

如果查询参数的表达式引用了那些只有执行部分执行变更计划以后才能知晓的数据,比如另一个还未被创建的托管资源的输出,那么数据源的读取操作会被推迟到"apply"阶段,任何引用该数据源输出的表达式的值在执行到数据源被读取完之前都是未知的。

data也可以设置depends_on、count、for_each。也能指定特定的provider实例,暂时不支持生命周期lifecycle

一个数据源定义例子如下:

# Find the latest available AMI that is tagged with Component = web
data "aws_ami" "web" {filter {name   = "state"values = ["available"]}filter {name   = "tag:Component"values = ["web"]}most_recent = true
}

引用数据源

引用数据源数据的语法是data.<TYPE>.<NAME>.<ATTRIBUTE>

resource "aws_instance" "web" {ami           = data.aws_ami.web.idinstance_type = "t1.micro"
}

module{}补充

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

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

使用方式

https://developer.hashicorp.com/terraform/language/modules/sources

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

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

使用模块

我们刚才介绍了如何用 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 来强制更新到最新版本模块。

其他的模块元参数

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

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

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

模块版本约束

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

module "consul" {source  = "hashicorp/consul/aws"version = "0.0.5"servers = 3
}

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

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

可以通过在 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 moduleresource "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

模块内的 Provider

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

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

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

模块内的 Provider 版本限制

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

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

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

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

隐式 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 时尤其重要。

显式传递 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.usw1aws.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 版本约束而使用代理配置块。

重构

请注意,本节介绍的通过 moved 块进行模块重构的功能是从 Terraform v1.1 开始被引入的。如果要在之前的版本进行这样的操作,必须通过 terraform state mv 命令来完成。

对一些旨在被人复用的老模块来说,最初的模块结构和资源名称可能会逐渐变得不再合适。例如,我们可能发现将以前的一个子模块分割成两个单独的模块会更合理,这需要将现有资源的一个子集移动到新的模块中。

Terraform 将以前的状态与新代码进行比较,资源与每个模块或资源的唯一地址相关联。因此,默认情况下,移动或重命名对象会被 Terraform 理解为销毁旧地址的对象并在新地址创建新的对象。

当我们在代码中添加 moved 块以记录我们移动或重命名对象过去的地址时,Terraform 会将旧地址的现有对象视为现在属于新地址。

moved 块语法

moved可以直接重定向,重命名资源,Terraform会自动识别新旧资源的信息。

moved 块只包含 fromto 参数,没有名称:

moved {from = aws_instance.ato   = aws_instance.b
}

上面的例子演示了模块先前版本中的 aws_instance.a 如今以 aws_instance.b 的名字存在。

在为 aws_instance.b 创建新的变更计划之前,Terraform 会首先检查当前状态中是否存在地址aws_instance.a 的记录。如果存在该记录,Terraform 会将之重命名aws_instance.b 然后继续创建变更计划。最终生成的变更计划中该对象就好像一开始就是以 aws_instance.b 的名字被创建的,防止它在执行变更时被删除。

fromto 的地址使用一种特殊的地址语法,该语法允许选定模块、资源以及子模块中的资源。下面是几种不同的重构场景中所需要的地址语法:

重命名一个资源

考虑模块代码中这样一个资源:

resource "aws_instance" "a" {count = 2# (resource-type-specific configuration)
}

第一次应用该代码时 Terraform 会创建 aws_instance.a[0] 以及 aws_instance.a[1]

如果随后我们修改了该资源的名称,并且把旧名字记录在一个 moved 块里:

resource "aws_instance" "b" {count = 2# (resource-type-specific configuration)
}moved {from = aws_instance.ato   = aws_instance.b
}

当下一次应用使用了该模块的代码时,Terraform 会把所有地址为 aws_instance.a 的对象看作是一开始就以 aws_instance.b 的名字创建的:aws_instance.a[0] 会被看作是 aws_instance.b[0]aws_instance.a[1] 会被看作是 aws_instance.b[1]

新创建的模块实例中,因为从来就不存在 aws_instance.a,于是会忽略 moved 块而像通常那样直接创建 aws_instance.b[0] 以及 aws_instance.b[1]

重命名对模块的调用

我们可以用类似重命名资源的方式来重命名对模块的调用。假设我们开始用以下代码调用一个模块:

module "a" {source = "../modules/example"# (module arguments)
}

当应用该代码时,Terraform 会在模块内声明的资源路径前面加上一个模块路径前缀 module.a。比方说,模块内的 aws_instance.example 的完整地址为 module.a.aws_instance.example

如果我们随后打算修改模块名称,我们可以直接修改 module 块的标签,并且在一个 moved 块内部记录该变更:

module "b" {source = "../modules/example"# (module arguments)
}moved {from = module.ato   = module.b
}

当下一次应用包含该模块调用的代码时,Terraform 会将所有路径前缀为 module.a 的对象看作从一开始就是以 module.b 为前缀创建的。module.a.aws_instance.example 会被看作是 module.b.aws_instance.example

该例子中的 moved 块中的两个地址都代表对模块的调用,而 Terraform 识别出将原模块地址中所有的资源移动到新的模块地址中。如果该模块声明时使用了 count 或是 for_each,那么该移动也将被应用于所有的实例上,不需要逐个指定。

为模块调用添加 count 或 for_each 声明

考虑一下单实例的模块:

module "a" {source = "../modules/example"q# (module arguments)
}

应用该段代码会导致 Terraform 创建的资源地址都拥有 module.a 的前缀。

随后如果我们可能需要再通过添加 count 来创建多个资源实例。为了保留先前的 aws_instance.a 实例不受影响,我们可以添加一个 moved 块来设置在新代码中该实例的对应的键。

module "a" {source = "../modules/example"count  = 3# (module arguments)
}moved {from = module.ato   = module.a[2]
}

上面的代码引导 Terraform 将所有 module.a 中的资源看作是从一开始就是以 module.a[2] 的前缀被创建的。结果就就是,Terraform 生成的变更计划中只会创建 module.a[0] 以及 module.a[1]

moved 块的两个地址中的至少一个包含实例键时,例如上面例子中的 [2]那样,Terraform 会理解将这两个地址理解为对模块的特定实例的调用而非对模块所有实例的调用。这意味着我们可以使用 moved 块在不同键之间切换来添加或是删除键,该机制可用于 countfor_each,或删除模块上的这种声明。

当模块提供的功能越来越多时,就可以使用moved进行模块的切分。

删除 moved 块

随着时间的推移,一些老模块可能会积累大量 moved 块。

删除 moved 块通常是一种破坏性变更,因为删除后所有使用旧地址引用的对象都将被删除而不是被移动。我们强烈建议保留历史上所有的 moved 块来保存用户从任意版本升级到当前版本的升级路径信息。

如果我们决定要删除 moved 块,需要谨慎行事。对于组织内部的私有模块来说删除 moved 块可能是安全的,因为我们可以确认所有用户都已经使用新版本模块代码运行过 terraform apply 了。

如果我们需要多次重命名或是移动一个对象,我们建议使用串联moved 块来记录完整的变更信息,新的块引用已有的块:

moved {from = aws_instance.ato   = aws_instance.b
}moved {from = aws_instance.bto   = aws_instance.c
}

像这样记录下移动的序列可以使 aws_instance.a 以及 aws_instance.b 两种地址的资源都得到成功更新,Terraform 会将他们视作从一开始就是以 aws_instance.c 的地址创建的。

模块元参数

在 Terraform 0.13 之前,模块在使用上存在一些限制。由于 Module 不支持元参数,所以我们只能手动拷贝模块代码。

Terraform 0.13 开始,模块也像资源一样,支持countfor_eachdepends_on三种元参数。

module "ec2_instance" {count = 2source  = "terraform-aws-modules/ec2-instance/aws"version = "~> 3.0"name = "single-instance-${count.index}"ami                    = "ami-ebd02392"instance_type          = "t2.micro"key_name               = "user1"monitoring             = truevpc_security_group_ids = ["sg-12345678"]subnet_id              = "subnet-eddcdzz4"tags = {Terraform   = "true"Environment = "dev"}
}

要注意的是 Terraform 0.13 之后在模块上声明depends_on,列表中也可以传入另一个模块。声明depends_on的模块中的所有资源的创建都会发生在被依赖的模块中所有资源创建完成之后。

表达式

下标和属性

list和tuple可以通过下标访问成员,例如local.list[3]var.tuple[2]。map和object可以通过属性访问成员,例如local.object.attrnamelocal.map.keyname。由于map的key是用户定义的,可能无法成为合法的Terraform标识符,所以访问map成员时我们推荐使用方括号:local.map["keyname"]

局部命名值

在某些特定表达式或上下文当中,有一些特殊的命名值可以被使用,他们是局部命名值。几种比较常见的局部命名值有:

  • count.index:表达当前count下标序号
  • each.key:表达当前for_each迭代器实例
  • self:在预置器中指代声明预置器的资源

引用资源输出属性

当一个资源声明了count参数,那么资源本身就成了一个资源对象列表而非单个资源。这种情况下要访问资源输出属性,要么使用展开表达式,要么使用下标索引:

  • aws_instance.example[*].id:返回所有instance的id列表
  • aws_instance.example[0].id:返回第一个instance的id

当一个资源声明了for_each参数,那么资源本身就成了一个资源对象字典而非单个资源。这种情况下要访问资源的输出属性,要么使用特定键,要么使用for表达式:

  • aws_instance.example["a"].id:返回"a"对应的实例的id
  • [for value in aws_instance.example: value.id]:返回所有instance的id

注意不像使用count,使用for_each的资源集合不能直接使用展开表达式,展开表达式只能适用于列表。你可以把字典转换成列表后再使用展开表达式:

  • values(aws_instance.example)[*].id

尚不知晓的值

在一章的学习中我们知道申请资源的时候会有一个unknown value的值,大部分时候你不需要特意理会它们,因为Terraform语言会自动处理这些尚不知晓的值,比如说使两个尚不知晓的值相加得到的会是一个尚不知晓的值。

有些情况下表达式中含有尚不知晓的值会有明显的影响:

  • count元参数不可以为尚不知晓,因为变更计划必须明确地知晓到底要维护多少个目标实例
  • 如果尚不知晓的值被用于数据源,那么数据源在计算变更计划阶段就无法读取,它会被推迟到执行阶段读取。这种情况下,在计划阶段该数据源的一切输出均为尚不知晓
  • 如果声明module块时传递给模块输入变量的表达式使用了尚不知晓值,那么在模块代码中任何使用了该输入变量值的表达式的值都将是尚不知晓
  • 如果模块输出值表达式中含有尚不知晓值,任何使用该模块输出值的表达式都将是尚不知晓
  • Terraform会尝试验证尚不知晓值的数据类型是否合法,但仍然有可能无法正确检查数据类型,导致执行阶段发生错误

尚不知晓值在执行terraform plan时会被输出为"(not yet known)"。

算数和逻辑操作符

当一个表达式中含有多个操作符时,它们的优先级顺序时:

  1. !- (负号)
  2. */%
  3. +- (减号)
  4. >>=<<=
  5. ==!=
  6. &&
  7. ||

可以使用小括号覆盖默认优先级。如果没有小括号,高优先级操作符会被先计算,例如1+23会被解释成1+(23)而不是(1+2)*3。

条件表达式

条件表达式是判断一个布尔表达式的结果以便于在后续两个值当中选择一个:

condition ? true_val : false_val

如果condition表达式为true,那么结果是true_value,反之则为false_value。

var.a != "" ? var.a : "default-a"

如果输入变量a的值是空字符串,那么结果会是(false)default-a,否则返回输入变量a的值。

条件表达式的判断条件可以使用上述的任意操作符。供选择的两个值也可以是任意类型,但它们的类型必须相同,这样Terraform才能判断条件表达式的输出类型。

函数调用

Terraform支持在计算表达式时使用一些内建函数,函数调用表达式类似操作符,通用语法是:

<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)

函数名标明了要调用的函数。每一个函数都定义了数量不等、类型不一的入参以及不同类型的返回值。

有些函数定义了不定长的入参表,例如,min函数可以接收任意多个数值类型入参,返回其中最小的数值:

min(55, 3453, 2)

展开函数入参

如果想要把列表或元组的元素作为参数传递给函数,那么我们可以使用展开符:

min([55, 2453, 2]...)

展开符使用的是三个独立的.号组成的...,不是Unicode中的省略号。展开符是一种只能用在函数调用场景下的特殊语法。

https://developer.hashicorp.com/terraform/language/functions<官网函数传送门>

for表达式

for表达式是将一种复杂类型映射成另一种复杂类型的表达式。输入类型值中的每一个元素都会被映射为一个或零个结果。

举例来说,如果var.list是一个字符串列表,那么下面的表达式将会把列表元素全部转为大写:

[for s in var.list : upper(s)]

在这里for表达式迭代了var.list中每一个元素(就是s),然后计算了upper(s),最后构建了一个包含了所有upper(s)结果的新元组,元组内元素顺序与源列表相同。

for表达式周围的括号类型决定了输出值的类型。上面的例子里我们使用了方括号,所以输出类型是元组。如果使用的是花括号,那么输出类型是对象for表达式内部冒号后面应该使用以=>符号分隔的表达式:

{for s in var.list : s => upper(s)}

该表达式返回一个对象,对象的成员属性名称就是源列表中的元素,值就是对应的大写值。

被for迭代的也可以是对象或者字典,这样的话迭代器就会被表示为两个临时变量:

[for k, v in var.map : length(k) + length(v)]

最后,如果返回类型是对象(使用花括号)那么表达式中可以使用...符号实现group by:

{for s in var.list : substr(s, 0, 1) => s... if s != ""}

元素排序

因为for表达式可以从无序类型(映射、对象、集合)转换为有序类型(列表、元组),所以 Terraform 必须为无序集合的元素选择隐含的顺序。

对于地图和对象,Terraform 使用词法排序按键或属性名称对元素进行排序。

对于字符串集,Terraform 使用词法排序按元素的值对元素进行排序。

对于其他类型的集合,Terraform 使用可能在未来版本中更改的任意顺序。我们建议将表达式结果转换为一个集合,以便在配置的其他地方清楚地表明结果是无序的。您可以使用该toset函数将表达式结果 简洁地转换为for集合类型。

toset([for e in var.set : e.example])

分组结果

如果结果类型是一个对象(使用{}分隔符),那么通常给定的键表达式在结果中的所有元素中必须是唯一的,否则 Terraform 将返回错误。

有时生成的键不是唯一的,因此为了支持这种情况,Terraform 支持一种特殊的分组模式,该模式更改结果以支持每个键的多个元素。

要激活分组模式,请...在值表达式后添加符号。例如:

variable "users" {type = map(object({role = string}))
}locals {users_by_role = {for name, user in var.users : user.role => name...}
}

上面代表了一种情况,其中一个模块需要一个描述各种用户的映射,每个用户都有一个“角色”,其中映射键是用户名。用户名保证是唯一的,因为它们是输入中的映射键,但许多用户可能都共享一个角色名称。

表达式反转输入映射,local.users_by_role使键是角色名称,值是用户名,但表达式处于分组模式(由于...after name),因此结果将是字符串列表的映射,如下所示:

{"admin": ["ps",],"maintainer": ["am","jb","kl","ma",],"viewer": ["st","zq",],
}

由于元素排序规则,作为评估for表达式的一部分,Terraform 将按用户名词法对用户进行排序,因此与每个角色关联的用户名将在分组后按词法排序。

展开表达式(Splat Expression)

展开表达式提供了一种类似for表达式的简洁表达方式。比如说var.list包含一组对象,每个对象有一个属性id,那么读取所有id的for表达式会是这样:

[for o in var.list : o.id]

与之等价的展开表达式是这样的:

var.list[*].id

这个特殊的[*]符号迭代了列表中每一个元素,然后返回了它们在.号右边的属性值。

展开表达式只能被用于列表(所以使用for_each参数的资源不能使用展开表达式,因为它的类型是字典)。然而,如果一个展开表达式被用于一个既不是列表又不是元组的值,那么这个值会被自动包装成一个单元素的列表然后被处理。

比如说,var.single_object[*].id 等价于 [var.single_object][*].id。大部分场景下这种行为没有什么意义,但在访问一个不确定是否会定义count参数的资源时,这种行为很有帮助,例如:

aws_instance.example[*].id

上面的表达式不论aws_instance.example定义了count与否都会返回实例的id列表,这样如果我们以后为aws_instance.example添加了count参数我们也不需要修改这个表达式。

遗留的旧有展开表达式

曾经存在另一种旧的展开表达式语法,它是一种比较弱化的展开表达式,现在应该尽量避免使用。

这种旧的展开表达式使用.*而不是[*]

var.list.*.interfaces[0].name

要特别注意该表达式与现有的展开表达式结果不同,它的行为等价于:

[for o in var.list : o.interfaces][0].name

而现有[*]展开表达式的行为等价于:

[for o in var.list : o.interfaces[0].name]

注意两者右方括号的位置。

为什么[*]*效率高?因为最后.*.会被转换成[*]

重复配置块

表达式机制用于从for表达式中的其他集合值构造集合值,然后您可以将其分配给需要复杂值的各个资源参数。

一些资源类型还定义了嵌套块类型,它们通常代表以某种方式属于包含资源的单独对象。您不能使用for表达式动态生成嵌套块,但可以使用dynamicblocks 为资源动态生成嵌套 块。

dynamic块

在顶级块,例如resource块当中,一般只能以类似name = expression的形式进行一对一的赋值。大部分情况下这已经够用了,但某些资源类型包含了可重复的内嵌块无法使用表达式循环赋值

resource  "aws_elastic_beanstalk_environment" "tfenvtest" {name = "tf-test-name" # can use expressions heresetting {# but the "setting" block is always a literal block}
}

你可以用dynamic块来动态构建重复的setting这样的内嵌块:

resource "aws_elastic_beanstalk_environment" "tfenvtest" {name                = "tf-test-name"application         = "${aws_elastic_beanstalk_application.tftest.name}"solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6"dynamic "setting" {for_each = var.settingscontent {namespace = setting.value["namespace"]name = setting.value["name"]value = setting.value["value"]}}
}

dynamic可以在resourcedataproviderprovisioner块内使用。一个dynamic块类似于for表达式,只不过它产生的是内嵌块。它可以迭代一个复杂类型数据然后为每一个元素生成相应的内嵌块。在上面的例子里:

  • dynamic的标签(也就是"setting")确定了我们要生成的内嵌块种类
  • for_each参数提供了需要迭代的复杂类型值
  • iterator参数(可选)设置了用以表示当前迭代元素的临时变量名。如果没有设置iterator,那么临时变量名默认就是dynamic块的标签(也就是setting)
  • labels参数(可选)是一个表示块标签的有序列表,用以按次序生成一组内嵌块。有labels参数的表达式里可以使用临时的iterator变量
  • 内嵌的content块定义了要生成的内嵌块的块体。你可以在content块内部使用临时的iterator变量

由于for_each参数可以是集合或者结构化类型,所以你可以使用for表达式或是展开表达式来转换一个现有集合的类型。

iterator变量(上面的例子里就是setting)有两个属性:

  • key:迭代容器如果是map,那么就是当前元素的键;迭代容器如果是list,那么就是当前元素在list中的下标序号;如果是由for_each表达式产出的set,那么key和value是一样的,这时我们不应该使用key
  • value:当前元素的值

一个dynamic块只能生成属于当前块定义过的内嵌块参数。无法生成诸如lifecycleprovisioner这样的元参数,因为Terraform必须在确保对这些元参数求值的计算是成功的。

for_each的值必须是不为空的map或者set。如果你需要根据内嵌数据结构或者多个数据结构的元素组合来声明资源实例集合,你可以使用Terraform表达式和函数来生成合适的值。

过度使用dynamic块会导致代码难以阅读以及维护,所以我们建议只在需要构造可重用的模块代码时使用dynamic块。尽可能手写内嵌块。

表达式 · 《Terraform入门教程》 (lonegunmanb.github.io)

字符串字面量

Terraform有两种不同的字符串字面量。最通用的就是用一对双引号包裹的字符,比如"hello"。在双引号之间,反斜杠\被用来进行转义。Terraform支持的转义符有:

Sequence Replacement
\n 换行
\r 回车
\t 制表符
" 双引号 (不会截断字符串)
\ 反斜杠
\uNNNN 普通字符映射平面的Unicode字符(NNNN代表四位16进制数)
\UNNNNNNNN 补充字符映射平面的Unicode字符(NNNNNNNN代表八位16进制数)

另一种字符串表达式被称为"heredoc"风格,是受Unix Shell语言启发。它可以使用自定义的分隔符更加清晰地表达多行字符串:

<<EOT
hello
world
EOT

上面例子里的heredoc风格字符串要求内容必须对齐行头,这在块内声明时看起来会比较奇怪:

block {value = <<EOT
hello
world
EOT
}

为了改进可读性,Terraform也支持***缩进的***heredoc,只要把<<改成<<-:

block {value = <<-EOThelloworldEOT
}

上面的例子里,Terraform会以最靠近行头的行作为基准来调整行头缩进,得到的字符串是这样的:

helloworld

heredoc中的反斜杠不会被解释成转义,而只会是简单的反斜杠。

双引号和heredoc两种字符串都支持字符串模版,模版的形式是${...}以及%{...}。如果想要表达${或者%{的字面量,那么可以重复第一个字符:$${%%{

字符串模版

字符串模版允许我们在字符串中嵌入表达式,或是通过其他值动态构造字符串。

插值(Interpolation) $

一个${...}序列被称为插值,插值计算花括号之间的表达式的值,有必要的话将之转换为字符串,然后插入字符串模版,形成最终的字符串:

"Hello, ${var.name}!"

上面的例子里,输入变量var.name的值被访问后插入了字符串模版,产生了最终的结果,比如:"Hello, Juan!"

命令(Directive) %

一个%{...}序列被称为命令,命令可以是一个布尔表达式或者是对集合的迭代,类似条件表达式以及for表达式。有两种命令:

  • if \<BOOL\> / else /endif 命令根据布尔表达式的结果在两个模版中选择一个:
"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"

else部分可以省略,这样如果布尔表达结果为false那么就会插入空字符串。

  • for \<NAME\> in \<COLLECTION\> / endfor 命令迭代一个结构化对象或者集合,用每一个元素渲染模版,然后把它们拼接起来:
<<EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT

for关键字后紧跟的名字被用作代表迭代器元素的临时变量,可以用来在内嵌模版中使用。

为了在**不添加额外空格和换行的前提下提升可读性,所有的模版序列都可以在首尾添加~符号。**如果有~符号,那么模版序列会去除字符串左右的空白(空格以及换行)。如果~出现在头部,那么会去除字符串左侧的空白;如果出现在尾部,那么会去除字符串右边的空白:

<<EOT
%{ for ip in aws_instance.example.*.private_ip ~}
server ${ip}
%{ endfor ~}
EOT

上面的例子里,命令符后面的换行符被忽略了,但是server ${ip}后面的换行符被保留了,这确保了每一个元素生成一行输出:

server 10.1.16.154
server 10.1.16.1
server 10.1.16.34

当使用模版命令时,我们推荐使用heredoc风格字符串,用多行模版提升可读性。双引号字符串内最好只使用插值。

重载文件

Terraform一开始加载代码文件时会跳过这些重载文件,然后才会按照字典序一个一个处理重载文件。对重载文件中定义的所有顶级块(resource、data等),Terraform会尝试找到对应的已有对象并且将重载内容合并进已有对象。

重载文件只应使用于特殊场景,过度使用会使得读者在阅读原始代码文件时被迫还要阅读所有的重载文件才能理解对象配置,从而降低了代码的可读性。使用重载文件时,请在原始文件被重载的部分添加相应注释,提醒未来的读者哪些部分会被重载文件修改。

例子

如果我们有一个名为example.tf的代码文件:

resource "aws_instance" "web" {instance_type = "t2.micro"ami           = "ami-408c7f28"
}

然后我们创建一个名为override.tf的文件:

resource "aws_instance" "web" {ami = "foo"
}

Terraform随后会合并两者,实际的配置会是这样的:

resource "aws_instance" "web" {instance_type = "t2.micro"ami           = "foo"
}

不同模块之间的合并会有差异,详情请看:

重载文件 · 《Terraform入门教程》 (lonegunmanb.github.io)

命令行

命令行配置文件

命令行配置文件 · 《Terraform入门教程》 (lonegunmanb.github.io)

环境变量

TF_LOG: 该环境变量可以设定Terraform内部日志的输出级别,例如TF_LOG=TRACE

TF_LOG_PATH:该环境变量可以设定日志文件保存的位置。注意,如果TF_LOG_PATH被设置了,那么TF_LOG也必须被设置。举例来说,想要始终把日志输出到当前工作目录,我们可以这样:TF_LOG_PATH=./terraform.log

TF_INPUT: 该环境变量设置为"false"或"0"时,等同于运行terraform相关命令行命令时添加了参数-input=false。

TF_VAR_name:我们在介绍输入变量赋值时介绍过,可以通过设置名为TF_VAR_name的环境变量来为名为"name"的输入变量赋值:

$ export TF_VAR_region=us-west-1

TF_IN_AUTOMATION:如果该变量被设置为非空值,Terraform会意识到自己运行在一个自动化环境下,从而调整自己的输出以避免给出关于该执行什么子命令的建议。这可以使得输出更加一致且减少非必要的信息量。

TF_REGISTRY_DISCOVERY_RETRY:该变量定义了尝试从registry拉取插件或模块代码遇到错误时的重试次数。

TF_REGISTRY_CLIENT_TIMEOUT:该变量定义了发送到registry连接请求的超时时间,默认值为10秒。

TF_CLI_CONFIG_FILE:该变量设定了Terraform命令行配置文件的位置.

apply

Terraform最重要的命令就是apply。apply命令被用来生成执行计划(可选)并执行之,使得基础设施资源状态符合代码的描述。

默认情况下,apply会扫描当前目录下的代码文件,并执行相应的变更。然而,也可以通过参数指定其他代码文件目录。在设计自动化流水线时也可以显式分为创建执行计划、使用apply命令执行该执行计划两个独立步骤。

如果没有显式指定变更计划文件,那么terraform apply会自动创建一个新的变更计划,并提示用户是否批准执行。如果生成的计划不包含任何变更,那么terraform apply会立即退出不会提示用户输入。

该命令有以下参数可以使用:

  • -backup-path:保存备份文件的路径。默认等于-state-out参数后加上".backup"后缀。设置为"-"可关闭
  • -compact-warnings:如果Terraform生成了一些告警信息而没有伴随的错误信息,那么以只显示消息总结的精简形式展示告警
  • -lock=true:执行时是否先锁定状态文件
  • -lock-timeout=0s:尝试重新获取状态锁的间隔
  • -input=true:在无法获取输入变量的值是是否提示用户输入
  • -auto-approve:跳过交互确认步骤,直接执行变更
  • -no-color:禁用输出中的颜色
  • -parallelism=n:限制Terraform遍历图时的最大并行度,默认值为10(考试高频考点)
  • -refresh=true:指定变更计划及执行变更前是否先查询记录的基础设施对象现在的状态以刷新状态文件。如果命令行指定了要执行的变更计划文件,该参数设置无效
  • -state=path:保存状态文件的路径,默认值是"terraform.tfstate"。如果使用了远程Backend该参数设置无效。该参数不影响其他命令,比如执行init时会找不到它设置的状态文件。如果要使得所有命令都可以使用同一个特定位置的状态文件,请使用Local Backend(译者也不知道这是什么,官方文档相关链接404,并且搜索不到)
  • -state-out=path:写入更新的状态文件的路径,默认情况使用-state的值。该参数在使用远程Backend时设置无效
  • -target=resource:通过指定资源地址指定更新目标资源。我们会在随后介绍plan命令时详细介绍
  • -var ‘foo=bar’:设置一组输入变量的值。该参数可以反复设置以传入多个输入变量值
  • -var-file=foo:指定一个输入变量文件。具体内容我们在介绍输入变量的章节已有介绍,在此不再赘述

console

有时我们想要一个安全的调试工具来帮助我们确认某个表达式是否合法,或者表达式的值是否符合预期,这时我们可以使用terraform console启动一个交互式控制台。

destroy

terraform destroy命令可以用来销毁并回收所有Terraform管理的基础设施资源。

Terraform管理的资源会被销毁,在执行销毁动作前会通过交互式界面征求用户的确认。

该命令可以接收所有apply命令的参数,除了不可以指定plan文件。

如果-auto-approve参数被设置为true,那么将不会征求用户确认直接销毁。

**如果用-target参数指定了某项资源,那么不但会销毁该资源,同时也会销毁一切依赖于该资源的资源。**我们会在随后介绍plan命令时详细介绍。

terraform destroy将执行的所有操作都可以随时通过执行terraform plan -destroy命令来预览。

import

terraform import命令用来将已经存在的资源对象导入Terraform。

需要尤其注意的是,Terraform设想的是每一个资源对象都仅对应一个独一无二的实际基础设施对象,通常来说如果我们完全使用Terraform创建并管理基础设施时这一点不是问题;但如果你是通过导入的方式把基础设施对象导入到Terraform里,要绝对避免将同一个对象导入到两个以及更多不同的地址上,这会导致Terraform产生不可预测的行为。

terraform import会根据资源ID找到相应资源,并将其信息导入到状态文件中ADDRESS对应的资源上。

init

terraform init命令被用来初始化一个包含Terraform代码的工作目录。在编写了一些Terraform代码或是克隆了一个Terraform项目后应首先执行该命令。反复执行该命令是安全的(考点)。

常用参数

  • -input=true:是否在取不到输入变量值时提示用户输入
  • -lock=false:是否在运行时锁定状态文件
  • -lock-timeout=\:尝试获取状态文件锁时的超时时间,默认为0,意为一旦发现锁已被其他进程获取立即报错
  • -no-color:禁止输出中包含颜色
  • -upgrade:是否升级模块代码以及插件

Backend初始化

在执行init时,会分析根模块代码以寻找Backend配置,然后使用给定的配置设定初始化Backend存储。

要跳过Backend配置,可以使用-backend=false。注意某些其他init步骤需要已经被初始化的Backend,所以推荐只在已经初始化过Backend后使用该参数。

初始化子模块

init会搜索module块,然后通过source参数取回模块代码。

模块安装之后重新运行init命令会继续安装那些自从上次init之后新增的模块,但不会修改已被安装的模块。使用-upgrade可以改变这种行为,将所有模块升级到最新版本的代码。

要跳过子模块安装步骤,可以使用-get=false参数。要注意其他一些init步骤需要模块树完整,所以建议只在成功安装过模块以后使用该参数。

plan

terraform plan命令被用来创建变更计划。Terraform会先运行一次refresh,然后决定要执行哪些变更使得现有状态迁移到代码描述的期待状态。

可选参数-out可以将变更计划保存在一个文件中,以便日后使用terraform apply命令来执行该计划。

参数跟apply差不多

providers

terraform providers [config-path]

可以通过显式传递config-path参数来指定根模块路径,默认为当前工作目录。

terraform providers mirror:可以通过配置Provider镜像存储来使得在这样的环境下Terraform可以从本地插件镜像存储中获取插件。

terraform providers schema命令被用来打印当前代码使用的Provider的架构。Provider架构包含了该Provider本身的参数信息,以及所提供的resource、data的架构信息。

terraform providers schema [options]

可选参数为:

  • -json:用机器可读的JSON格式打印架构

请注意,目前-json参数是必填的,未来该命令将允许使用其他参数。

refresh

terraform refresh命令将实际存在的基础设施对象的状态同步到状态文件中记录的对象状态。它可以用来检测真实状态与记录状态之间的漂移并更新状态文件。

注意:由于用户主动去refresh会存在很大的风险,现在官网已经废弃了。

show

terraform show命令从状态文件或是变更计划文件中打印人类可读的输出信息。这可以用来检查变更计划以确定所有操作都是预期的,或是审查当前的状态文件。

可以通过添加-json参数输出机器可读的JSON格式输出。

需要注意的是,使用-json输出时所有标记为sensitive的敏感数据都会以明文形式被输出。

state

terraform state命令可以用来进行复杂的状态管理操作。随着你对Terraform的使用越来越深入,有时候你需要对状态文件进行一些修改。由于我们在状态管理章节中提到过的,状态文件的格式属于HashiCorp未公开的私有格式,所以直接修改状态文件是不适合的,我们可以使用terraform state命令来执行修改。

terraform state list命令可以列出状态文件中记录的资源对象。

terraform state mv命令可以在状态文件中移动资源。

terraform state pull命令可以从远程Backend中人工下载状态并输出。该命令也可搭配本地状态文件使用。

terraform push命令被用来手动上传本地状态文件到远程Backend。该命令也可以被用在当前使用的本地状态文件上。

terraform state replace-provider命令可以替换状态文件中资源对象所使用的Provider的源.

terraform state rm命令可以用来从状态文件中删除对象。该命令可以删除单个资源、多实例资源中特定实例、整个模块以及等等。

terraform state show命令可以展示状态文件中单个资源的属性。

taint

terrform taint命令可以手动标记某个Terraform管理的资源有"污点",强迫在下一次执行apply时删除并重建之。

terraform untaint命令可以手动清除一个Terraform管理的资源对象上的污点,恢复它在状态文件中的状态。它是terraform taint的逆向操作。

validate :terraform validate命令可以检查目录下Terraform代码,只检查语法文件,不会访问诸如远程Backend、Provider的API等远程资源。

get :terraform get命令被用来下载以及更新根模块中使用的模块。

workspace

terraform workspace list命令列出当前存在的工作区。

terraform workspace select命令用来选择使用的工作区。

terraform workspace new命令用来创建新的工作区。

terraform workspace delete命令被用以删除已经存在的工作区。

terraform workspace show命令被用以输出当前使用的工作区。

实用案例一:多可用区分布

这是一个相当常见的小技巧。多数公有云为了高可用性,都在单一区域内提供了多可用区的设计。一个可区是一个逻辑上的数据中心,单个可用区可能由于各种自然灾害、网络故障而导致不可用,所以公有云应用部署高可用应用应时刻考虑跨可用区设计。

假如我们想要创建N台不同的云主机实例,在Terraform 0.12之前的版本中,我们只能用count配合模运算来达成这个目的

variable "az" {type    = list(string)default = ["cn-bj2-03","cn-bj2-04",]
}variable "instance_count" {type    = numberdefault = 4
}data "ucloud_images" "centos" {name_regex = "^CentOS 7"
}resource "ucloud_instance" "web" {count             = var.instance_countavailability_zone = var.az[count.index % length(var.az)]image_id          = data.ucloud_images.centos.images[0].idinstance_type     = "n-standard-1"charge_type       = "dynamic"name              = "${var.az[count.index % length(var.az)]}-${floor(count.index/length(var.az))}"
}

简单来说就是使用count创建多实例资源时,用var.az[count.index % length(var.az)]可以循环使用每个可用区,使得机器尽可能均匀分布在各个可用区。

可以看到,主机的确是均匀地分散在两个可用区了。

但是这样做在调整可用区时会发生大问题,例如:

variable "az" {type = list(string)default = ["cn-bj2-03",
#    "cn-bj2-04",]
}

我们禁用了cn-bj2-04可用区,按道理我们期待的变更计划应该是将两台原本属于cn-bj2-04的主机删除,在cn-bj2-03可用区新增两台主机。

但实际上原本04可用区的机子也进行了变更

仔细想想,实际上这是一个比较低效的变更计划。原本属于cn-bj2-03的两台主机应该不做任何变更,只需要删除cn-bj2-04的主机,再补充两台cn-bj2-03的主机即可。这是因为我们使用的是count,而count只看元素在列表中的序号。当我们删除一个可用区时,实际上会引起主机序号的重大变化,导致出现大量低效的变更,这就是我们在讲countfor_each时强调过的,如果创建的资源实例彼此之间几乎完全一致,那么count比较合适。否则,那么使用for_each会更加安全。

让我们尝试使用for_each改写这段逻辑:

variable "az" {type    = list(string)default = ["cn-bj2-03","cn-bj2-04",]
}variable "instance_count" {type    = numberdefault = 4
}locals {instance_names = [for i in range(var.instance_count):"${var.az[i%length(var.az)]}-${floor(i/length(var.az))}"]
}data "ucloud_images" "centos" {name_regex = "^CentOS 7"
}resource "ucloud_instance" "web" {for_each          = toset(local.instance_names)name              = each.valueavailability_zone = var.az[index(local.instance_names, each.value) % length(var.az)]image_id          = data.ucloud_images.centos.images[0].idinstance_type     = "n-standard-1"charge_type       = "dynamic"
}

为了生成主机独一无二的名字,我们首先用range函数生成了一个序号集合,比如目标主机数是4,那么range(4)的结果就是[0, 1, 2, 3];然后我们通过取模运算使得名字前缀在可用区列表之间循环递增,最后用floor(i/length(var.az))计算出当前序号对应在当前可用区是第几台。例如4号主机在第二个可用区就是第二台,生成的名字应该就是cn-bj-04-1

最终:原来属于cn-bj2-03的两台主机原封不动,删除了属于cn-bj2-04的两台主机,并且在cn-bj2-03可用区新增两台主机。

实用案例一:有条件创建

Terraform被设计成声明式而非命令式,例如没有常见的if条件语句,后来才加上了countfor_each实现的循环语句(但循环的次数必须是在plan阶段就能够确认的,无法根据其他resource的输出动态决定)

有时候我们需要根据某种条件来判断是否创建一个资源。虽然我们无法使用if来完成条件判断,但我们还有countfor_each可以帮助我们完成这个目标。

我们以UCloud为例,假如我们正在编写一个旨在被复用的模块,模块的逻辑要创建一台虚拟机,我们的代码可以是这样的:

data ucloud_vpcs "default" {name_regex = "^Default"
}data "ucloud_images" "centos" {name_regex = "^CentOS 7"
}resource "ucloud_instance" "web" {availability_zone = "cn-bj2-02"image_id = data.ucloud_images.centos.images[0].idinstance_type = "n-basic-2"
}output "uhost_id" {value = ucloud_instance.web.id
}

非常简单。但是如果我们想进一步,让模块的调用者决定创建的主机是否要搭配一个弹性公网IP该怎么办?

我们可以在上面的代码后面接上这样的代码:

variable "allocate_public_ip" {description = "Decide whether to allocate a public ip and bind it to the host"type = booldefault = false
}resource "ucloud_eip" "public_ip" {count = var.allocate_public_ip ? 1 : 0name = "public_ip_for_${ucloud_instance.web.name}"internet_type = "bgp"
}resource "ucloud_eip_association" "public_ip_binding" {count = var.allocate_public_ip ? 1 : 0eip_id = ucloud_eip.public_ip[0].idresource_id = ucloud_instance.web.id
}

我们首先创建了名为allocate_public_ip的输入变量,然后在编写弹性IP相关资源代码的时候都声明了count参数,值使用了条件表达式,根据allocate_public_ip这个输入变量的值决定是1还是0.这实际上实现了按条件创建资源。

需要注意的是,由于我们使用了count,所以现在弹性IP相关的资源实际上是多实例资源类型的。我们在ucloud_eip_association.public_ip_binding中引用ucloud_eip.public时,还是要加上访问下标。由于ucloud_eip_association.public_ip_bindingucloud_eip.public实际上是同生同死,所以在这里他们之间的引用还比较简单;如果是其他没有声明count的资源引用它们的话,还要针对allocate_public_ipfalseucloud_eip.public实际为空做相应处理,比如在output中:

output "public_ip" {value = join("", ucloud_eip.public_ip[*].public_ip)
}

使用join函数就可以在即使没有创建弹性IP时也能返回空字符串。或者我们也可以用条件表达式:

output "public_ip" {value = length(ucloud_eip.public_ip[*].public_ip) > 0 ? ucloud_eip.public_ip[0].public_ip : ""
}
机。# 实用案例一:有条件创建Terraform被设计成声明式而非命令式,例如没有常见的`if`条件语句,后来才加上了`count`和`for_each`实现的循环语句(但循环的次数必须是在plan阶段就能够确认的,无法根据其他resource的输出动态决定)有时候我们需要根据某种条件来判断是否创建一个资源。虽然我们无法使用if来完成条件判断,但我们还有`count`和`for_each`可以帮助我们完成这个目标。我们以UCloud为例,假如我们正在编写一个旨在被复用的模块,模块的逻辑要创建一台虚拟机,我们的代码可以是这样的:```hcl
data ucloud_vpcs "default" {name_regex = "^Default"
}data "ucloud_images" "centos" {name_regex = "^CentOS 7"
}resource "ucloud_instance" "web" {availability_zone = "cn-bj2-02"image_id = data.ucloud_images.centos.images[0].idinstance_type = "n-basic-2"
}output "uhost_id" {value = ucloud_instance.web.id
}

非常简单。但是如果我们想进一步,让模块的调用者决定创建的主机是否要搭配一个弹性公网IP该怎么办?

我们可以在上面的代码后面接上这样的代码:

variable "allocate_public_ip" {description = "Decide whether to allocate a public ip and bind it to the host"type = booldefault = false
}resource "ucloud_eip" "public_ip" {count = var.allocate_public_ip ? 1 : 0name = "public_ip_for_${ucloud_instance.web.name}"internet_type = "bgp"
}resource "ucloud_eip_association" "public_ip_binding" {count = var.allocate_public_ip ? 1 : 0eip_id = ucloud_eip.public_ip[0].idresource_id = ucloud_instance.web.id
}

我们首先创建了名为allocate_public_ip的输入变量,然后在编写弹性IP相关资源代码的时候都声明了count参数,值使用了条件表达式,根据allocate_public_ip这个输入变量的值决定是1还是0.这实际上实现了按条件创建资源。

需要注意的是,由于我们使用了count,所以现在弹性IP相关的资源实际上是多实例资源类型的。我们在ucloud_eip_association.public_ip_binding中引用ucloud_eip.public时,还是要加上访问下标。由于ucloud_eip_association.public_ip_bindingucloud_eip.public实际上是同生同死,所以在这里他们之间的引用还比较简单;如果是其他没有声明count的资源引用它们的话,还要针对allocate_public_ipfalseucloud_eip.public实际为空做相应处理,比如在output中:

output "public_ip" {value = join("", ucloud_eip.public_ip[*].public_ip)
}

使用join函数就可以在即使没有创建弹性IP时也能返回空字符串。或者我们也可以用条件表达式:

output "public_ip" {value = length(ucloud_eip.public_ip[*].public_ip) > 0 ? ucloud_eip.public_ip[0].public_ip : ""
}

Terraform学习2相关推荐

  1. Terraform学习

    Terraform学习 Terraform 与IaC 什么是Terraform Terraform 是 HashiCorp 的基础设施即代码工具.它允许您在人类可读的声明性配置文件中定义资源和基础设施 ...

  2. Terraform 学习总结(6)—— 基于阿里云平台上的 Terraform 实战

    Terraform 是什么 Terraform(https://www.terraform.io/)是 HashiCorp 旗下的一款开源(Go 语言开发)的 DevOps 基础架构资源管理运维工具, ...

  3. Terraform 学习总结(4)—— Terraform 实战

    前言 Terraform 是一种部署技术,任何想要通过基础设施即代码(Infrastructure as Code,IaC)方法来置备和管理基础设施的人,都可以使用这种技术.基础设施指的主要是基于云的 ...

  4. Terraform学习总结(2)——Terraform 语法详解

    一.Azure Providers Terraform 作为同一个可以可扩展的工具,是通过 Provider 来支持新的基础架构.Provider 是上游API的逻辑抽象.他们负责理解API交互并暴露 ...

  5. Terraform 学习总结(9)—— 如何解决存量云资源的管理难题

    一.背景信息 通常在以下四种运维场景需要考虑如何解决存量云资源的管理: 场景一:长期使用控制台.阿里云 CLI.资源编排服务或者直接调用 API 创建和管理资源,初次使用 Terraform 的场景. ...

  6. Terraform学习总结(1)——Terraform 简介

    前言 众所周知,当企业将项目整体架构资源迁移到云上,云基础设施架构师就要根据现有项目搭建整体项目的基础设施资源的架构,然后我们的云运维工程师就要根据设计好基础设施的架构图来创建云上资源,但是在构筑的过 ...

  7. Terraform 学习总结(3)——Terraform 命令详解

    前言 Terraform 是用于安全有效地构建,更改和版本控制基础结构的工具.Terraform可以管理现有和流行的服务提供商以及定制的内部解决方案.配置文件向 Terraform 描述了运行单个应用 ...

  8. Terraform 学习笔记

    Terraform 是一个 IT 基础架构自动化编排工具,它的口号是 "Write, Plan, and create Infrastructure as Code", 基础架构即 ...

  9. Terraform 学习总结(7)—— 基于 AWS 云平台上的 Terraform 实战

    一.AWS 上基础环境介绍 相信大家已经知道 Terraform 的基本使用方式了.在我们以后的场景中,主要讨论一些典型架构的实现.在实际使用中,Terraform 在多云基础架构创建中,是非常方便和 ...

最新文章

  1. 卷积神经网络(CNN)简介
  2. 毕飞宇:我是靠阅读支撑起来的作家 因为生活没有给我那么多
  3. Socket 网络编程实践经验
  4. Lingo 优化实例 出版社问题
  5. 我们为什么要尝试前后端分离
  6. 电脑销售渠道_为园区企业搭建服务麦格米特,长城电脑等企业产业链销售渠道...
  7. Java写入大字符串到oracle数据库_java程序如何提高oracle百万级数据的insert效率
  8. 【C语言】scanf()输入浮点型数据
  9. c语言用指针求Amn,[工学]第5章数据结构C语言描述耿国华.ppt
  10. spring加载ApplicationContext.xml的四种方式
  11. java使用场景什么意思_Java开发中类隐藏及应用场景分析是什么?
  12. Windows 7环境下网站性能测试小工具 Apache Bench 和 Webbench使用和下载
  13. 佳能Canon Pixma MX420 打印机驱动
  14. jsp酒店客房预订系统带前端
  15. C# Excel 条件格式
  16. vue导出word模版文档
  17. 融合泊车占比超过60%,除了德赛西威,还有哪些本土供应商
  18. 【U8+】win10/11系统注册用友U8硬加密
  19. Kubernetes多节点二进制部署
  20. VsCode创建第一个vue项目

热门文章

  1. excel sql python_报表自动化,三流用Excel,二流用Python,一流用它
  2. 判断Linux服务器是否被攻击以及相应的预防措施
  3. js获取指定字符前/后的字符串简单实例
  4. 数值法求六轴机械臂逆运动学解
  5. 数据库仓工具箱及阿里大数据之路--阅读总结
  6. Flex 布局教程:语法篇
  7. 华为服务器如何开机自动启动不了,华为手机开不了机怎么办 开机后一直停留在开机画面的解决方法(3)...
  8. 2021年第一天的祝福送给大家
  9. 浅谈 Web 网站架构演变过程
  10. 官宣!2022汇佳学校毕业生捷报汇总