My New Hugo Site

terraform note

原始类型:

  • string
  • number
  • bool

复杂类型:

  • list(…)
  • map(…) 键类型必须为string, 值类型任意。值声明方式:{“foo”: “bar”, “bar”: “baz”} {foo=“bar”, bar=“baz”}
  • set(…)

结构化类型:

  • object(…) 对象是指一组由具有名称和类型的属性所构成的复合类型
  • tuple(…) 元组类似于list,是一组值的连续集合,但每个元素都有独立的类型,可以使用下标访问内部元素

any

null

object 的optional 成员,可以在objet类型中使用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
  })
}
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)
    }), {})
  }))
}

根据其他数据的值来动态决定是否要为一个optional参数设置值,有条件地设置一个默认属性

variable "legacy_filenames" {
  type     = bool
  default  = false
  nullable = false
}

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

  buckets = [
    {
      name = "maybe_legacy"
      website = {
        error_document = var.legacy_filenames ? "ERROR.HTM" : null
        index_document = var.legacy_filenames ? "INDEX.HTM" : null
      }
    },
  ]
}

配置语法

参数

参数赋值就是将一个值赋给一个特定的名称:

image_id="abc123"

块:包含一组其他内容的容器

标识符: 参数名、块类型名以及其他terraform规范中定义的结构的名称,例如resource, variable等,都是标识符。合法的标识符可以包含字母、数字、下划线(_)以及减号(-)。标识符首字母不可以为数字。

注释:

  • #
  • //
  • /**/

输入变量:

variable "image_id" {
  type = string
}

variable "availability_zone_names" {
  type    = list(string)
  default = ["us-west-1a"]
}

variable "docker_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 8300
      external = 8300
      protocol = "tcp"
    }
  ]
}

以下一组关键字不可以被用作输入变量的名字:

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

类型: 可以在输入变量块中通过type 定义类型

variable "name" {
  type = string
}

variable "ports" {
  type = list(number)
}

默认值:

variable "name" {
  type = string
  default = "John Doe"
}

描述:

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

断言:

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 "image_id" {
  type        = string
  description = "The id of the machine image (AMI) to use for the server."

  validation {
    # regex(...) fails if it cannot find a match
    # 使用can函数来判定表达式的执行是否抛错
    condition     = can(regex("^ami-", var.image_id))
    
    # 如果condition表达式为false, Terraform就会返回error_message 中定义的错误信息
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}

声明一个变量包含敏感数据值需要将 sensitive 参数设置为true:

variable "user_information" {
  type = object({
    name    = string
    address = string
  })
  sensitive = true
}

resource "some_resource" "a" {
  name    = var.user_information.name
  address = var.user_information.address
}

禁止输入变量为空:

variable "example" {
  type     = string
  nullable = false
}

nullable参数控制模块调用者是否可以将null分配给变量,默认值为true

对输入变量赋值

命令行参数:

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

参数文件: 使用.tfvars参数文件,后缀名为.tfvars.json

image_id="ami-abc123"
availability_zone_names = [
  "us-east-1a",
  "us-west-1c"
]

通过-var-file参数指定要用的参数文件

terraform apply -var-file="testing.tfvars"
terraform apply -var-file="testing.tfvars.json"

两种情况,无需指定参数文件:

  • 当前模块中有名为 terraform.tfvars 或terraform.tfvars.json文件
  • 当前模块内有一个或多个后缀名为.auto.tfvars 或 .auto.tfvars.json的文件

环境变量:通过设置TF_VAR_NAME环境变量为输入变量赋值

export TF_VAR_image_id=ami-abc123

输入变量赋值优先级 Terraform加载变量值的顺序:

  • 环境变量
  • terraform.tfvars文件
  • terraform.tfvars.json 文件
  • 通过-var或者-var-file命令行参数传递的输入变量,按照在命令行参数中定义的顺序加载

复杂类型传值:

export TF_VAR_availability_zone_names='["us-west-1b","us-west-1d"]'

输出值 Terraform支持多返回值,在当前模块apply一段Terraform代码,运行成功后命令行会输出代码中定义的返回值 ,可以使用terraform output命令来输出当前模块对应的状态文件中的返回值。

输出值的声明

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

output关键字后紧跟输出值的名称,在当前模块内所有输出值名称唯一

description

output "instance_ip_addr" {
  value       = aws_instance.server.private_ip
  description = "The private IP address of the main server instance."
}

sensitive: 一个输出值可以标记sensitive为true,表示该输出值含有敏感信息。被标记sensitive的输出值只是会在执行 terraform apply和terraform output时会输出 \

depends_on: 显示声明依赖关系

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 块可以包含一个 precondition块:output中的precondition对应于variable中的validation 块,validation块检查输入变量值是否符合模块的要求

局部值

局部值通过locals定义

locals {
  service_name = "forum"
  owner        = "Community Team"
}

一个locals块可以定义多个局部值,也可以定义任意多个locals块。

locals {
  # Ids for multiple sets of EC2 instances, merged together
  instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}

locals {
  # Common tags to be assigned to all resources
  common_tags = {
    Service = local.service_name
    Owner = local.owner
  }
}

引用局部值的表达式时local.Name,

resource "aws_instance" "example" {
  tags = local.common_tags
}

局部值只能在同一模块内的代码中引用。

资源

资源通过resource块来定义,一个resource可以定义一个或多个基础设施资源对象。

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

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

元参数

resource块支持几种元参数声明,这些元参数可以被声明在所有类型的resource块中,它们将会改变资源的行为

  • depends_on
  • count 创建多个资源实例
  • for_each 迭代集合,为集合中每个元素创建一个对应的资源实例
  • provider 指定非默认的provider实例
  • lifecycle 自定义资源的生命周期行为
  • provisionerconnection 在资源创建后执行额外的操作

depends_on

资源之间存在依赖关系,但是没有数据引用的情况下可以使用depends_on

resource "aws_iam_role" "example" {
  name = "example"

  # assume_role_policy is omitted for brevity in this example. See the
  # documentation for aws_iam_role for a complete example.
  assume_role_policy = "..."
}

resource "aws_iam_instance_profile" "example" {
  # Because this expression refers to the role, Terraform can infer
  # automatically that the role must be created first.
  role = aws_iam_role.example.name
}

resource "aws_iam_role_policy" "example" {
  name   = "example"
  role   = aws_iam_role.example.name
  policy = jsonencode({
    "Statement" = [{
      # This policy allows software running on the EC2 instance to
      # access the S3 API.
      "Action" = "s3:*",
      "Effect" = "Allow",
    }],
  })
}

resource "aws_instance" "example" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  # Terraform can infer from this that the instance profile must
  # be created before the EC2 instance.
  iam_instance_profile = aws_iam_instance_profile.example

  # However, if software running in this EC2 instance needs access
  # to the S3 API in order to boot properly, there is also a "hidden"
  # dependency on the aws_iam_role_policy that Terraform cannot
  # automatically infer, so it must be declared explicitly:
  depends_on = [
    aws_iam_role_policy.example,
  ]
}

count

resource "aws_instance" "server" {
  count = 4 # create four similar EC2 instances

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

  tags = {
    Name = "Server ${count.index}"
  }
}

访问单资源实例对象:type.name 访问多资源实例对象:type.name[index]

声明了count或for_each的资源必须使用下标索引或者键来访问

for_each

一个resource块不允许同时声明count 和for_each. for_each参数可以是一个map或set(string)

# 使用map
resource "azurerm_resource_group" "rg" {
  for_each = {
    a_group = "eastus"
    another_group = "westus2"
  }
  # 使用each对象来访问当前的迭代器对象
  name     = each.key
  location = each.value
}
# 使用set(string)
resource "aws_iam_user" "the-accounts" {
  # 使用toset函数
  for_each = toset(["Told", "James", "Alice", "Dottie"])
}

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

  • 访问单实例对象:type.name
  • 访问多实例对象:type.name[key],例如 aws_instance.server[“ap-northeast-1”]

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

在for_each和count之间选择

variable "subnet_ids" {
  type = list(string)
}

resource "aws_instance" "server" {
  # Create one instance for each subnet
  count = length(var.subnet_ids)

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
  subnet_id     = var.subnet_ids[count.index]

  tags = {
    Name = "Server ${count.index}"
  }
}

如果创建的资源实例彼此之间几乎完全一致,那么count比较合适

provider

如果声明了同一类型Provider的多个实例,可以指定provider参数选择要使用的provider实例。如果没有指定provider参数,那么terraform默认使用资源类型名中的第一个单词对应的provider实例。

# default configuration
provider "google" {
  region = "us-central1"
}

# alternate configuration, whose alias is "europe"
provider "google" {
  alias  = "europe"
  region = "europe-west1"
}

resource "google_compute_instance" "example" {
  # This "provider" meta-argument selects the google provider
  # configuration whose alias is "europe", rather than the
  # default configuration.
  # provider 参数期待的赋值是 provider 或者是 provider.alias
  provider = google.europe

  # ...
}

lifecycle

一个资源对象的生命周期,可以用lifecycle块来定一个不一样的行为方式。

resource "azurerm_resource_group" "example" {
  # ...

  lifecycle {
    create_before_destroy = true
  }
}

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

  • create_before_destroy(bool): 默认情况下terraform会删除现有资源对象,然后用新的配置参数创建一个新的资源对象取代之。create_before_destroy参数可以修改这个行为,使得terraform首先创建新对象
  • prevent_destroy(bool): 被设置为true时,terraform会拒绝执行任何可能会摧毁该基础设施资源的变更计划。
  • ignore_changes(list(string)): 通过设定ignore_changes来指示terraform忽视某些属性的变更
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,
    ]
  }
}
resource "aws_instance" "example" {
  # ...

  tags = {
    # Initial value for Name is overridden by our automatic scheduled
    # re-tagging process; changes to this are ignored by ignore_changes
    # below.
    Name = "placeholder"
  }

  lifecycle {
    ignore_changes = [
      tags["Name"],
    ]
  }
}
  • 除了使用list(string),也可以使用关键字all,terraform会忽略资源的一切属性的变更

  • replace_triggered_by 可以在以下场景中使用:

    • 如果表达式指向多实例的资源声明(例如声明了count或者是for_each的资源),那么这组资源中任意实例发生变化变更或替换时都将引发声明 replace_triggered_by的资源被替换
    • 如果表达式指向单个资源实例,那么该实例发生变更或被替换时将引发replace_triggered_by的资源被替换
    • 如果表达式指向单个资源实例的单个属性,那么该属性值的任何变化都将引发声明
resource "aws_appautoscaling_target" "ecs_target" {
  lifecycle {
    replace_triggered_by = [
      # Replace `aws_appautoscaling_target` each time this instance of
      # the `aws_ecs_service` is replaced.
      aws_ecs_service.svc.id
    ]
  }
}

lifecycle 配置影响了terraform如何构建并遍历依赖图。

precondition & postcondition

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

每一个preconditionpostcondition块都需要一个condition 参数,该参数是一个表达式,在满足条件时返回true,否则返回false。该表达式可以引用同一模块内的任意其他对象,只要这种引用不会产生环依赖。在postcondition表达式中可以使用self对象引用声明postcondition的资源实例的属性。

# 通过precondition检测调用者是否不小心传入了错误的AMI参数
data "aws_ami" "example" {
  id = var.aws_ami_id
  lifecycle {
    # The AMI ID must refer to an existing AMI that has the tag "nomad-server".
    postcondition {
      condition     = self.tags["Component"] == "nomad-server"
      error_message = "tags[\"Component\"] must be \"nomad-server\"."
    }
  }
}

resourcedata 块中的 lifecycle 块可以同时包含 preconditionpostcondition 块。

  • Terraform 会在计算完 count 和 for_each 元参数后执行 precondition 块。这使得 Terraform 可以对每一个实例独立进行检查,并允许在表达式中使用 each.keycount.index 等。Terraform 还会在计算资源的参数表达式之前执行 precondition 检查。precondition 可以用来防止参数表达式计算中的错误被激发。
  • Terraform 在计算和执行对一个托管资源的变更之后执行 postcondition 检查,或是在完成数据源读取后执行它关联的 postcondition 检查。postcondition 失败会阻止其他依赖于此失败资源的其他资源的变更。

provisioner 和 connection

某些基础设施对象需要在创建后执行特定的操作才能正式运行。主机实例必须在上传了配置或是由配置管理工具初始化之后才能正常工作。

创建后执行的操作可以使用预置器(Provisioner)。预置器是由Terraform所提供的另一组插件,每种预置器可以在资源对象创建后执行不同类型的操作。

作为元参数provisioner和connection可以声明在任意类型的resource块内。

resource "aws_instance" "web" {

  # 在aws_instance中定义了类型为file 的预置器
  provisioner "file" {
    source       = "conf/myapp.conf"
    destination  = "/etc/myapp.conf"
    connection {
      type     = "ssh"
      user     = "root"
      password = var.root_password

      # self代表预置器所在的母块,也就是`aws_instance.web`
      # self.public_ip 代表 aws_instance.web.public_ip
      host     = self.public_ip
    }

  }
}

file类型预置器支持sshwinrm两种类型的connection

预置器根据运行的时机分为两种类型,创建时预置器以及销毁时预置器。

创建时预置器

默认情况下,资源对象被创建时会运行预置器,在对象更新、销毁时则不会运行。 如果创建时预置器失败了,那么资源对象会被标记污点(terraform taint) 一个被标记污点的资源在下次执行terraform apply命令时会被销毁并重建。 这种设计是因为当预置器运行失败时标志着资源处于半就绪的状态。由于terraform没法衡量预置器的行为,唯一能够完全确保资源被正确初始化的方式就是删除重建。

可以通过设置on_failure参数来改变这种行为。

销毁时预置器

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

resource "aws_instance" "web" {
  # ...

  provisioner "local-exec" {
    when    = destroy
    command = "echo 'Destroy-time provisioner'"
  }
}

摧毁时预置器在资源被实际销毁前运行。

摧毁时预置器只有在存在于代码中的情况下才会在摧毁时被执行。如果一个resource块连带内部的销毁时预置器块一起被从代码中删除,那么被删除的预置器在资源被销毁时不会被执行。要解决这个问题,我们需要使用多个步骤来绕过这个限制:

  • 修改资源声明代码,添加count = 0参数
  • 执行terraform apply,运行删除时预置器,然后删除资源实例
  • 删除resource块
  • 重新执行terraform apply,此时应该不会有任何变更需要执行
  • 该限制在未来将会得到解决,但目前来说我们必须节制使用销毁时预置器

预置器失败行为

预置器运行失败会导致terraform apply 执行失败。可以通过设置on_failure 参数来改变这一行为。可以设置的值:

  • continue: 忽视错误,继续执行创建或是摧毁
  • fail: 报错并终止执行变更(默认行为)。如果是一个创建时预置器,则会在资源对象上标记污点。
resource "aws_instance" "web" {
  # ...

  provisioner "local-exec" {
    command    = "echo The server's IP address is ${self.private_ip}"
    on_failure = continue
  }
}

本地资源

虽然大部分资源类型都对应的是通过远程基础设施API控制的一个资源对象。 有一些资源对象存在于terraform进程自身内部,用来计算生成某些结果。

操作超时设置

timeouts内嵌块参数,允许我们设置我们允许操作持续多长时间,超时将被认定为失败。

超时完全由资源对应的Provider处理。使用timeouts的嵌入块参数定义超时设置。

resource "aws_db_instance" "example" {
  # 可配置超时的操作类别由每种支持超时设定的资源类型自行决定。大部分资源类型不支持设置超时。使用超时前先查阅文档。
  timeouts {
    create = "60m"
    delete = "2h"
  }
}

数据源

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

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

使用数据源

数据源通过一种特殊的资源访问:data 资源。 数据源通过data块声明:

data "aws_ami" "example" {
  most_recent = true

  owners = ["self"]

  tags = {
    Name   = "app-server"
    Tested = "true"
  }
}

一个data块请求terraform从一个指定的数据源aws_ami读取指定数据并将结果输出到Local Name为example的实例中。 我们可以在同一模块内的代码中通过数据源名称来引用数据源,但无法从模块外部直接访问数据源。

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

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

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

数据源参数

每一种数据源资源都关联到一种外部数据源,数据源类型决定了它接收的查询参数以及输出的数据。每一种数据源类型都属于一个Provider.大部分data块内的数据源参数都是由对应的数据源类型定义的,这些参数的赋值可以使用完整的Terraform表达式。类似资源,terraform也为所有类型的数据源定义了一些元参数。

数据源行为

本地数据源

绝大多数数据源都对应了一个通过远程基础设施API访问的外部数据源,但是也有一些特殊的数据源仅存在于Terraform进程内部,计算并对外输出一些数据。

本地数据源有:terraform_file, local_file, aws_iam_policy_document等 本地数据源的行为与其他数据源完全一致,但他们输出的结果数据只是临时存在于terraform运行时,每次计算一个新的变更计划时这些值都会被重新计算。

数据源的依赖关系

设置depends_on元参数来显示声明依赖关系。

多数据源实例

数据源也可以通过设置count, for_each元参数来创建一组多个数据源实例,并且terraform也会把每个数据源实例单独创建并读取相应的外部数据。

指定特定provider实例

可以通过provider元参数指定使用特定provider实例。

生命周期

数据源目前不可以通过设置lifecyle 块来定制化生命周期

# 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.id
  instance_type = "t1.micro"
}

表达式

表达式用来配置文件中进行一些计算。

terraform支持一些更加复杂的表达式,比如引用其他资源resource的输出值、数学计算、布尔条件计算,以及一些内建的函数。

下标和属性

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

引用命名值

terraform 定义了多种命名值,表达式中的每一个命名值都关联到一个具体的值,可以用单一命名值作为一个表达式,或是组合多个命名值来计算出一个新值。

命名值有以下种类:

  • resource_type.name:表示一个资源对象。凡是不符合后面列出的命名值模式的表达式都会被terraform解释为一个托管资源。如果资源声明了count元函数,那么该表达式表示的是一个对象实例的list。如果资源声明了for_each元参数,那么该表达式表示的是一个对象实例的map。
  • var.name:表示一个输入变量
  • local.name:表示一个局部值
  • module.module_name.output_name:表示一个模块的一个输出值
  • data.data_type.name:表示一个数据源实例。如果数据源声明了count元参数,那么该表达式表示的是一个数据源实例list。如果数据源声明了for_each元参数,那么该表达式表示的是一个数据源实例map
  • path.module:表示当前模块在文件系统中的路径
  • path.root:表示根模块在文件系统中的路径
  • path.cwd:表示当前工作目录的路径。一般来说该路径等同于path.root,但在调用terraform命令行时如果指定了代码路径,那么两者将会不同。
  • terraform.workspace: 当前使用的workspace

局部命名值