terraform-HCL语法

本文记录一些关于terraform的语法,只记录一下我认为有必要记录的东西,本文绝大部分来自教程: https://lonegunmanb.github.io/introduction-terraform/

数据类型

原始类型

  • string
  • number
  • bool

复杂类型

  • list(type): 下标从0开始,list(number)/list(string)/list(bool),用于声明时指定list的元素类型
  • map(...): 字典/映射,key必须是string,value任意类型.声明方式推荐使用=: {foo="bar", bar="baz"}
  • set(...): 集合类型,代表一组不重复的值

结构化类型

一个结构化类型允许多个不同类型的值组成一个类型.结构化类型需要提供一个schema结构信息作为参数来指明元素的结构.

  • object(...): 允许多个不同类型作为值的map

    1
    2
    3
    4
    # 声明{<KEY\=<TYPE>, <KEY>=<TYPE>,...}
    object({age=number, name=string})
    # 赋值时必须赋全,可以多,多的会被忽略
    { age=18, name="john", gender="male" } # 多出来的gender会被忽略
  • tuple(...): 允许多个不同类型作为值的list

    1
    2
    3
    4
    # 声明
    tuple([string, number, bool])
    # 赋值时类型和数量要一致
    ["a", 15, true]

any

terraform中一个特殊的类型约束,本身不是一个类型而是一个占位符,每当一个值被赋予any这个类型约束时,terraform会自动识别一个最精确的类型去取代any.

例如把["a", "b", "c"]赋值给list(any), terraform最终赋值的类型是list(string).

1
2
3
4
# 声明一个无约束类型的变量,给terraform自己识别
variable "no_type_constraint" {
type = any
}

null

无类型,代表数据缺失,如果我们将一个参数设置为null, terraform会认为你忘记给它赋值,如果有默认值就用默认值,没有默认值但是又是必填则会报错.

一般用于条件表达式中,在某项条件不满足时跳过对某参数的赋值.

optional

terraform >= 1.3引入的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 声明一个object
variable "an_object" {
type = object({
a = string
b = string
c = number
})
}
# 赋值时,必须赋齐全,但是又不准备给b,c赋值,可以用null
{
a = "a"
b = null
c = null
}
# 但是加入object含有的参数很多且复杂,我们赋值时就会很麻烦,于是引入optional,在定义时候使用
variable "with_optional_attribute" {
type = object({
a = string # a required attribute
b = optional(string) # an optional attribute
c = optional(number, 127) # an optional attribute with default value
})
}

optional有两个参数:

  • 类型: 必填,第一个参数标明属性的类型
  • 默认值: 选填,如果object没有定义该属性,就使用默认值,没有默认值就使用null

所以optional作用如下: 定义这个参数的类型和默认值,如果没有赋值就使用默认值,如果没有默认值就赋值null.

例子一

1
2
3
4
5
6
7
8
9
10
11
12
# 定义一个bucket变量,获取bucket需要的属性
variable "buckets" {
type = list(object({
name = string
enabled = optional(bool, true)
website = optional(object({
index_document = optional(string, "index.html")
error_document = optional(string, "error.html")
routing_rules = optional(string)
}), {})
}))
}

这里定义嵌套比较复杂,只是为了演示用法.

首先buckets是一个类型为list的变量,list里面元素类型是object

website是一个optional属性,类型为object,里面又有三个optional属性.

下面是一个赋值例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
buckets = [
{
name = "production"
website = {
routing_rules = <<-EOT
[
{
"Condition" = { "KeyPrefixEquals": "img/" },
"Redirect" = { "ReplaceKeyPrefixWith": "images/" }
}
]
EOT
}
},
{
name = "archived"
enabled = false
},
{
name = "docs"
website = {
index_document = "index.txt"
error_document = "error.txt"
}
},
]

上面定义三个存储桶

production桶配置了一条重定向的路由规则

archived桶使用默认配置,但是是关闭的

docs桶使用文本文件取代索引页和错误页

例子二

下面结合表达式有条件地设置一个默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
variable "legacy_filenames" {
type = bool
default = false
# 不能为空
nullable = false
}

module "buckets" {
source = "./modules/buckets"

buckets = [
{
name = "maybe_legacy"
website = {
# 根据legacy_filenames的值设置默认值
error_document = var.legacy_filenames ? "ERROR.HTM" : null
index_document = var.legacy_filenames ? "INDEX.HTM" : null
}
},
]
}

变量

声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

variable "name" {
# 类型
type = string
# 默认值
default = "John Doe"
# 描述(描述不是注释)
description = "The id of the machine image (AMI) to use for the server."
# 在命令行输出中隐藏敏感值(依旧会记录在statefile中) terraform >= 0.14 在某些情况下还是会暴露出来
sensitive = true
# 不允许为空
nullable = false
# 断言terraform >= 0.13, condition的值必须是bool
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来判断获取bool
validation {
condition = can(regex("^ami-", var.image_id))
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

赋值

按以下优先级排序

  1. 环境变量: TF_VAR_name

  2. terraform.tfvars: 使用HCL语法

    1
    2
    3
    4
    5
    image_id = "ami-abc123"
    availability_zone_names = [
    "us-east-1a",
    "us-west-1c",
    ]
  3. terraform.tfvars.json: 使用json语法

    1
    2
    3
    4
    {
    "image_id": "ami-abc123",
    "availability_zone_names": ["us-west-1a", "us-west-1c"]
    }
  4. *.auto.tfvars*.auto.tfvars.json,以文件名字母排序,同样一个HCL一个json

  5. -var参数或者-var-filr

  6. 交互界面获取值

调用

1
var.availability_zone_names

输出

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

# 一般output不需要定义depends_on,如果要用到请写好注释
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,
]
# 类似validation块的validation,确保输出值满足某种要求,防止terraform把不合法的值写入状态文件
# Precondition
condition = var.enable_example_output
}

关于depends_on: 依赖关系是通过terraform的provider去分析的,一般是不需要显式定义,但有时候它不能计算出来(比如terraform或者provider的版本过旧,对一些新资源不能很好地计算,就需要用到depends_on)

locals

locals块用于定义本地变量,只能在同一模块内的代码中引用.

1
2
3
4
5
6
7
locals {
common_tags = {
# 注意引用是使用local,声明使用locals
Service = local.service_name
Owner = local.owner
}
}

一般来说,只有需要重复引用同一个复杂表达式的场景才使用locals

resource

定义具体资源的块,也是接触最多的块.不同平台的不同资源的定义都有所不同,具体要查看具体平台具体资源的文档.

terraform-registry文档

1
2
3
4
5
# resource关键字 resource_type local_name
resource "aws_instance" "web" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
}

local_name只能在同一个模块内被引用,resource_type+local_name在同一个模块内必须是唯一

资源行为

当terraform对一个resource块执行apply操作,创建一个新的基础设施对象,会给这个对象分配一个id,并写入statefile.有了这个id,后续就可以对它对更新修改删除等操作.

resource的输出

通过以下语法获取resource的输出

1
<RESOURCE TYPE>.<NAME>.<ATTRIBUTE>

具体的resource有哪些attribute供访问可以通过查阅registry文档获取.

依赖与并行

depends_on用于显式声明资源的依赖关系.

一般情况下,terraform自身可以通过分析resource块内的表达式,根据表达式的引用链获取资源在创建,更新,销毁时的执行顺序.

但是有些时候是计算不出来的:

例如,Terraform必须要创建一个访问控制权限资源,以及另一个需要该权限才能成功创建的资源。后者的创建依赖于前者的成功创建,然而这种依赖在代码中没有表现为数据引用关联,此时就需要depends_on

默认情况下,terraform可以并行创建10无依赖关系的资源.

元参数

定义在resource块内,用于改变资源的行为,上面提到的depends_on就是其中一个.

depends_on

显示声明资源之间的依赖关系,只有当资源间确实存在依赖关系,但是彼此间又没有数据引用的场景下才有必要使用

count

count指创建多少个相似的对象,创建的对象有单独的id和下标(0开始)

1
2
3
4
5
6
7
8
9
10
resource "aws_instance" "server" {
count = 4 # create four similar EC2 instances

ami = "ami-a1b2c3d4"
instance_type = "t2.micro"

tags = {
Name = "Server ${count.index}"
}
}
  • count.index:代表当前对象对应的下标
  • 访问: <TYPE>.<NAME>[<INDEX>]
  • count的值可以是任意自然数也可以是表达式,但是不能引用任何其他资源的输出属性,不过可以应用data查询出来的输出属性(只要该data不依赖其他resource查询)

for_each

用于创建多个相似但有些属性不一样的资源

terraform >= 0.12.6, 不能与count同时用在一个resource块内

for_each的值可以是map(通过each.keyeach.value获取值)或者set(string)(通过each.key获取值)

for_each所使用的键集合不能够包含或依赖非纯函数,也就是反复执行会返回不同返回值的函数(uuid, bcrypt, timestamp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 使用map
resource "azurerm_resource_group" "rg" {
for_each = {
a_group = "eastus"
another_group = "westus2"
}
name = each.key
location = each.value
}
# 使用set(string)
resource "aws_iam_user" "the-accounts" {
for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
name = each.key
}

provider

resource块内可以通过provider关键字声明该资源使用的provider.默认情况下,使用资源类型第一个单词所对应的provider.

比如google_compute_instance的默认provider就是google,aws_instance的默认Provider就是aws.

lifecycle

针对一个资源对象定义不一样的行为方式

1
2
3
4
5
6
7
resource "azurerm_resource_group" "example" {
# ...

lifecycle {
create_before_destroy = true
}
}

lifecycle和它的参数都属于元参数,可以被声明于任意类型的资源块内部.有如下的参数:

  • create_before_destroy (bool): 默认情况下,需要修改一个由于服务端API限制无法直接升级的资源时,terraform会先删除旧对象创建新对象.本参数可以修改这个行为.设置为true,就会先创建新对象,新对象创建成功并取代老对象后再删除老对象.默认是false,因为大部分技术设施资源都需要有一个唯一的标识来防止冲突,即使在新老对象并存的时候也有有这个约束.但有些资源可以为每个对象添加一个随机的前缀来避免冲突,具体要看具体的资源类型在这方面的约束.

  • prevent_destroy (bool): 这是一个保险措施,避免资源因为错误的执行terraform destroy或者因为错误地修改了某个参数导致terraform决定删除并重建新的实例这种情况下资源被删除.

  • ignore_changes (list(string)):忽略改变,某些场景下,某资源可以会被第三方控制而不停被修改,此时terraform每次都会把它改回来,如果需要避免这种情况,可以使用这个参数指定要忽略的某些属性的变更.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # 忽视tags属性变更
    resource "aws_instance" "example" {
    # ...

    lifecycle {
    ignore_changes = [
    # Ignore changes to tags, e.g. because a management agent
    # updates these based on some ruleset managed elsewhere.
    tags,
    ]
    }
    }
    # 忽视map中特定元素的变更,必须确保map中含有这个元素
    resource "aws_instance" "example" {
    tags = {
    Name = "placeholder"
    }
    lifecycle {
    ignore_changes = [
    tags["Name"],
    ]
    }
    }

    除了使用list(string)指定忽略的属性还可以使用all忽略所有属性的变更

  • replace_triggered_by: 手动指定资源的哪些属性被修改时,强制触发替换资源这个操作.一般情况下用不到.

data

data,数据源,用于查询或计算数据供其他地方使用.数据源是provider定义的,不同的平台有不同的数据源可供查询.因此数据源的具体定义可以通过查registry文档获取.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义
data "aws_ami" "web" {
filter {
name = "state"
values = ["available"]
}

filter {
name = "tag:Component"
values = ["web"]
}

most_recent = true
}
# 使用
resource "aws_instance" "web" {
ami = data.aws_ami.web.id
instance_type = "t1.micro"
}

dynamic块

比如在resource块中,如果某些资源包含了可重复的内嵌块,此时可以使用dynamic循环赋值.

1
2
3
4
5
6
7
# 语法如下
dynamic "block_type" {
for_each = expression
content {
# Block content
}
}

下面举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
variable "subnets" {
type = list(object({
name = string
cidr = string
zone = string
resource = string
}))
}

resource "aws_subnet" "example_subnet" {
dynamic "subnet" {
for_each = var.subnets
content {
availability_zone = subnet.value.zone
cidr_block = subnet.value.cidr
tags = {
Name = subnet.value.name
}
}
}
}

上面的例子中,首先定义了一个subnets的变量,由子网所需属性(name,cidr,zone,resource)的对象构成.(list(object))

然后定义subnet这个dynamic块.for_each负责迭代对象var.subnets

content负责给属性赋值

值通过subnet.value.zone形式引用,注意引用值第一节字符串用的是dynamic块的名字

过多的dynamic块会导致代码难以阅读和维护,建议只在需要构造可重用的模块代码时使用.

check

terraform >= 1.5,check 块中可以定义一个有限作用范围的data块以及至少一个断言.用于验证基础设状态.

check会在每次planapply后执行的自定义验证,作为plan或apply的最后一步执行.

assert 是一种在 check 块中使用的断言机制。check 块用于定义验证规则,而 assert 块用于在验证规则中定义断言逻辑。断言是一种用于检查条件是否为真的方法。在 check 块中,你可以使用 assert 块来添加额外的断言逻辑,以确保特定条件为真。如果断言条件为假,Terraform 将输出错误消息。

1
2
3
4
5
6
7
8
9
10
11
# 定义一个名为health_check的check块
check "health_check" {
data "http" "terraform_io" {
url = "https://www.terraform.io"
}

assert {
condition = data.http.terraform_io.status_code == 200
error_message = "${data.http.terraform_io.url} returned an unhealthy status code"
}
}

module

创建

所有包含terraform代码文件的文件夹都是一个terraform模块.直接执行terraform plan/apply的文件夹称为根模块

举个例子说明模块的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
. # 根模块
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/ # modules文件夹用于存放子模块
│ ├── nestedA/ # 子模块1,名为nestedA
│ │ ├── README.md # 说明文档
│ │ ├── variables.tf # 模块使用的变量
│ │ ├── main.tf # 模块的主要入口
│ │ ├── outputs.tf # 模块的输出
│ ├── nestedB/ # 子模块2,名为nestedB
│ ├── .../
├── examples/ # 用来给出调用样例(可选),有example中代码经常会被复制到别的地方使用.所以里面的路径最好不要写相对路径
│ ├── exampleA/
│ │ ├── main.tf
│ ├── exampleB/
│ ├── .../

模块结构不宜过深,保持一层嵌入就可以了

引用

通过module块的source关键字来指定模块的源,支持的模块源有:

  • 本地

    • 本地路径必须以./../为前缀来声明使用的是本地路径,除了本地源,其他源的模块引用都要先下载代码才能使用

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

    • 官方仓库: 公共仓库,也可以通过terraform cloud维护一个私有模块仓库,或者通过实现 Terraform 模块注册协议来实现一个私有仓库.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      module "consul" {
      # 公共仓库的的模块可以用 <NAMESPACE>/<NAME>/<PROVIDER> 形式的源地址来引用,在公共仓库上的模块介绍页面上都包含了确切的源地址
      source = "hashicorp/consul/aws"
      version = "0.1.0"
      }
      # 托管在其他仓库的模块,在源地址头部添加 <HOSTNAME>/ 部分,指定私有仓库的主机名
      module "consul" {
      source = "app.terraform.io/example-corp/k8s-cluster/azurerm"
      version = "1.1.0"
      }
  • github

    • gitHub.com为前缀指定

      1
      2
      3
      4
      5
      6
      7
      8
      # 默认使用HTTPS协议克隆仓库
      module "consul" {
      source = "github.com/hashicorp/example"
      }
      # 使用ssh协议
      module "consul" {
      source = "git@github.com:hashicorp/example.git"
      }
  • bitbucket

    • bitbucket.org 为前缀
  • git/mercurial仓库

    • git仓库: git:: 为前缀

      1
      2
      3
      4
      5
      6
      7
      8
      # https, ref参数指定分支
      module "vpc" {
      source = "git::https://example.com/vpc.git?ref=v1.2.0"
      }
      # ssh
      module "storage" {
      source = "git::ssh://username@example.com/storage.git"
      }
    • Mercurial 仓库: hg:: 前缀

  • HTTP地址

  • S3

    • s3::
  • GCS

    • gcs::
  • 引用子文件夹中的模块,例如:

    • 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

使用

引用后直接再module块内给模块的变量赋值就能使用

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

servers = 5
}

引用模块输出值

1
2
3
4
5
resource "aws_elb" "example" {
# ...

instances = module.servers.instance_ids
}

模块内的provider

一般来说,旨在复用的模块内是不能定义任何provider的.

隐式继承

默认情况下子模块会自动从调用者那里继承默认的provider配置.

显式声明

如果不同的子模块需要不同的provider,或者子模块需要的provider与调用者自己使用的不同时,则需要显式声明.

比如一个模块配置了两个aws区域之间的网络打通,所以需要配置一个源区域provider和目标区域provider.根模块代码看起来应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
provider "aws" {
alias = "src"
}

provider "aws" {
alias = "dst"
}

alias是provider代理配置的参数,它是一个模块间传递provider配置的占位符.

实践例子

有条件创建

这是很常用的一个例子,使用count+表达式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
variable "allocate_public_ip" {
description = "Decide whether to allocate a public ip and bind it to the host"
type = bool
default = false
}

resource "ucloud_eip" "public_ip" {
count = var.allocate_public_ip ? 1 : 0
name = "public_ip_for_${ucloud_instance.web.name}"
internet_type = "bgp"
}

resource "ucloud_eip_association" "public_ip_binding" {
count = var.allocate_public_ip ? 1 : 0
eip_id = ucloud_eip.public_ip[0].id
resource_id = ucloud_instance.web.id
}

-target参数

多人团队执行terraform时,建议以module为单位运行.

1
terraform plan/apply --target module.<module-name>

–plugin-dir

所处网络环境比较严格,只能手动下载provider等插件,需要手动指定插件目录

1
terraform init --plugin-dir=<path-to-plugins>

terraform-HCL语法
http://example.com/2024/02/01/terraform-HCL/
作者
Peter Pan
发布于
2024年2月1日
许可协议