Terraform handbook
Terraform handbook
参考链接:https://blog.gmem.cc/terraform
一、简介
Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码(配置文件)来描述基础设施的拓扑结构,并确保云上资源和此结构完全对应。Terraform有三个版本,我们主要关注Terraform CLI。
Terraform CLI主要包含以下组件:
-
命令行前端
-
Terraform Language(以下简称TL,衍生自HashiCorp配置语言HCL)编写的、描述基础设施拓扑结构的配置文件。配置文件的组织方式是模块。本文使用术语“配置”(Configuration)来表示一整套描述基础设施的Terraform配置文件
-
针对各种云服务商的驱动(Provider),实现云资源的创建、更新和删除
云上资源不单单包括基础的IaaS资源,还可以是DNS条目、SaaS资源。事实上,通过开发Provider,你可以用Terraform管理任何资源。
Terraform会检查配置文件,并生成执行计划。计划描述了那些资源需要被创建、修改或删除,以及这些资源之间的依赖关系。Terraform会尽可能并行的对资源进行变更。当你更新了配置文件后,Terraform会生成增量的执行计划。
二、命令行
1、安装命令行
直接到https://www.terraform.io/downloads.html下载,存放到$PATH下即可。
2、基本特性
1)切换工作目录
使用选项 -chdir=DIR
2)Shell自动补全
使用 terraform -install-autocomplete安装自动完成脚本,使用 terraform -uninstall-autocomplete删除自动完成脚本。
3、资源地址
很多子命令接受资源地址参数,下面是一些例子:
# 资源类型.资源名
aws_instance.foo
# 资源类型.资源列表名[索引]
aws_instance.bar[1]
# 子模块foo的子模块bar中的
module.foo.module.bar.aws_instance.baz
4、配置文件
配置文件的路径可以通过环境变量 TF_CLI_CONFIG_FILE设置。非Windows系统中, $HOME/.terraformrc为默认配置文件路径。配置文件语法类似于TF文件:
# provider缓存目录
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
#
disable_checkpoint = true
# 存放凭证信息,包括模块仓库、支持远程操作的系统的凭证
credentials "app.terraform.io" {
token = "xxxxxx.atlasv1.zzzzzzzzzzzzz"
}
# 改变默认安装逻辑
provider_installation {
# 为example.com提供本地文件系统镜像,这样安装example.com/*/*的provider时就不会去网络上请求
# 默认路径是:
# ~/.terraform.d/plugins/${host_name}/${namespace}/${type}/${version}/${target}
# 例如:
# ~/.terraform.d/plugins/hashicorp.com/edu/hashicups/0.3.1/linux_amd64/terraform-provider-hashicups_v0.3.1
filesystem_mirror {
path = "/usr/share/terraform/providers"
include = ["example.com/*/*"]
}
direct {
exclude = ["example.com/*/*"]
}
# Terraform会在terraform init的时候,校验Provider的版本和checksum。Provider从Registry或者本地
# 目录下载Provider。当我们开发Provider的时候,常常需要方便的测试临时Provider版本,这种Provider还
# 没有关联版本号,也没有在Registry中注册Chencksum
# 为了简化开发,可以配置dev_overrides,它能覆盖所有配置的安装方法
dev_overrides {
"hashicorp.com/edu/hashicups-pf" = "$(go env GOBIN)"
}
}
5、init
配置工作目录,为使用其它命令做好准备。
Terraform命令需要在一个编写了Terraform配置文件的目录(配置根目录)下执行,它会在此目录下存储设置、缓存插件/模块,以及(默认使用Local后端时)存储状态数据。此目录必须进行初始化。
初始化后,会生成以下额外目录/文件:
.terraform目录,用于缓存provider和模块 如果使用Local后端,保存状态的terraform.tfstate文件。如果使用多工作区,则是terraform.tfstate.d目录。
对配置的某些变更,需要重新运行初始化,包括provider需求的变更、模块源/版本约束的变更、后端配置的变更。需要重新初始化时,其它命令可能会无法执行并提示你进行初始化。
命令 terraform get可以仅仅下载依赖的模块,而不执行其它init子任务。
运行 terraform init -upgrade会强制拉取最新的、匹配约束的版本并更新依赖锁文件。
6、validate
校验配置是否合法。
7、plan
显示执行计划,即当前配置将请求(结合state)哪些变更。Terraform的核心功能时创建、修改、删除基础设施对象,使基础设施的状态和当前配置匹配。当我们说运行Terraform时,主要是指plan/apply/destroy这几个命令。
terraform plan命令评估当前配置,确定其声明的所有资源的期望状态。然后比较此期望状态和真实基础设施的当前状态。它使用state来确定哪些真实基础设施对象和声明资源的对应关系,并且使用provider的API查询每个资源的当前状态。当确定到达期望状态需要执行哪些变更后,Terraform将其打印到控制台,它并不会执行任何实际的变更操作。
计划模式
plan命令支持两种备选的工作模式:
-
销毁模式:创建一个计划,其目标是销毁所有当前存在于配置中的远程对象,留下一个空白的state。对应选项 -destroy
-
仅刷新模式:创建一个计划,其目标仅仅是更新state和根模块的输出值,以便和从Terraform之外对基础设施对象的变更匹配。对应选项 -refresh-only
指定输入变量
使用选项 -var ‘NAME=VALUE’可以指定输入变量,该选项可以使用多次。
使用选项 -var-file=FILENAME可以从文件读取输入变量,某些文件会自动读取
8、apply
应用执行计划,创建、更新设施对象。
apply会做plan的任何事情,并在其基础上,直接执行变更操作。默认情况下,apply即席的执行一次plan,你也可以直接使用已保存的plan
命令格式: terraform apply [options] [plan file]
自动确认
选项 -auto-approve可以自动确认并执行所需操作,不需要人工确认。
使用已有计划
如果指定plan file参数,则读取先前保存的计划并执行。
计划模式
支持plan命令中关于计划模式的选项。
三、TF语言
1、块
配置文件由若干块(Block)组成,块的语法如下:
# Block header, which identifies a block
<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" "..." {
# Block body
<IDENTIFIER> = <EXPRESSION> # Argument
}
块是一个容器,它的作用取决于块的类型。块常常用来描述某个资源的配置。
取决于块的类型,标签的数量可以是0-N个。对于resource块,标签数量为两个。某些特殊的块,可能支持任意数量的标签。某些内嵌的块,例如network_interface,则不支持标签。
块体中可以包含若干参数(Argument),或者其它内嵌的块。参数用于将一个表达式分配到一个标识符,常常对应某个资源的一条属性。表达式可以是字面值,或者引用其它的值,正是这种引用让Terraform能够识别资源依赖关系。
直接位于配置文件最外层的块,叫做顶级块(Top-level Block),Terraform支持有限种类的顶级块。大部分Terraform特性,例如resource,基于顶级块实现。
下面是一个例子:
resource "aws_vpc" "main" {
cidr_block = var.base_cidr_block
}
2、数据类型
类型 | 说明 |
---|---|
string | Unicode字符序列,基本形式 “hello” |
number | 数字,形式 6.02 |
bool | true或 false |
list/tuple | 一系列的值,形式 [“us-west-1a”, “us-west-1c”] |
map/object | 键值对,形式 {name = “Mabel”, age = 52} |
3、空值
空值使用 null表示。
4、字符串和模板
转义字符
\n 换行
\r 回车
\t 制表
\" 引号
\\ 反斜杠
\uNNNN Unicode字符
\UNNNNNNNN Unicode字符
注意,在Heredoc中反斜杠不用于转义,可以使用:
$${ 字符串插值标记${ %%{ 模板指令标记%{
支持unix风格的字符串
block {
value = <<EOT
hello
world
EOT
}
block {
value = <<-EOT
hello
world
EOT
}
要将对象转换为JSON或YAML,可以调用函数:
example = jsonencode({
a = 1
b = "hello"
})
5、操作符
逻辑操作符: ! && || 算数操作符: * / % + - 比较操作符: >, >=, <, <= ==, !=
条件表达式
condition ? true_val : false_val
var.a != "" ? var.a : "default-a"
for表达式
使用for表达式可以通过转换一种复杂类型输出,生成另一个复杂类型结果。输入中的每个元素,可以对应结果的0-1个元素。任何表达式可以用于转换,下面是使用upper函数将列表转换为大写:
[for s in var.list : upper(s)]
输入类型
作为for表达式的输入的类型可以是list / set / tuple / map / object。可以为for声明两个临时符号,前一个表示index或key:
[for k, v in var.map : length(k) + length(v)]
结果类型
结果的类型取决于包围for表达式的定界符:
[] 表示生成的结果是元组
{} 表示生成的结果是object,你必须使用 =>
符号: {for s in var.list : s => upper(s)}
输入过滤
包含一个可选的if子句可以对输入元素进行过滤: [for s in var.list : upper(s) if s != ""]
示例:
variable "users" {
type = map(object({
is_admin = boolean
}))
}
locals {
admin_users = {
for name, user in var.users : name => user
if user.is_admin
}
}
splat表达式
splat表达式提供了更简单语法,在某些情况下代替for表达式:
[for o in var.list : o.id]
# 等价于
var.list[*].id
[for o in var.list : o.interfaces[0].name]
# 等价于
var.list[*].interfaces[0].name
可选object属性
variable "with_optional_attribute" {
type = object({
a = string # 必须属性
b = optional(string) # 可选属性
})
}
版本约束
版本约束是一个特殊的字符串值,在引用module、使用provider时,或者通过terraform块的required_version时,需要用到版本约束:
# 版本范围区间
version = ">= 1.2.0, < 2.0.0"
# 操作符
= 等价于无操作符,限定特定版本
!= 排除特定版本
> >= < <= 限制版本范围
~> 允许最右侧的版本号片段的变化
depends_on
该元参数用于处理隐含的资源/模块依赖,这些依赖无法通过分析Terraform配置文件得到。从0.13版本开始,该元参数可用于模块。之前的版本仅仅用于资源。
depends_on的值是一个列表,其元素具必须是其它资源的引用,不支持任意表达式。
depends_on应当仅仅用作最后手段,避免滥用。
resource "aws_iam_role" "example" {
name = "example"
assume_role_policy = "..."
}
# 这个策略允许运行在EC2中的实例访问S3 API
resource "aws_iam_role_policy" "example" {
name = "example"
role = aws_iam_role.example.name
policy = jsonencode({
"Statement" = [{
"Action" = "s3:*",
"Effect" = "Allow",
}],
})
}
resource "aws_iam_instance_profile" "example" {
# 这是可以自动分析出的依赖
role = aws_iam_role.example.name
}
resource "aws_instance" "example" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
# 这是可以自动分析出的依赖,包括传递性依赖
iam_instance_profile = aws_iam_instance_profile.example
# 如果这个实例中的程序需要访问S3接口,我们需要用元参数显式的声明依赖
# 从而分配策略
depends_on = [
aws_iam_role_policy.example,
]
}
count
默认情况下,一个resource块代表单个云上基础设施对象。如果你想用一个resource块生成多个类似的资源,可以用count或for_each参数。
设置了此元参数的上下文中,可以访问名为 count的变量,它具有属性 index,为从0开始计数的资源实例索引。 示例:
resource "aws_instance" "server" {
# 创建4个类似的实例
count = 4
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
# 实例的索引作为tag的一部分
Name = "Server ${count.index}"
}
}
for_each
如果资源的规格几乎完全一致,可以用count,否则,需要使用更加灵活的for_each元参数。
for_each的值必须是一个映射或set(string),你可以在上下文中访问 each对象, 它具有 key和 value两个属性,如果for_each的值是集合,则key和value相等。示例:
resource "azurerm_resource_group" "rg" {
for_each = {
a_group = "eastus"
another_group = "westus2"
}
# 对于每个键值对都会生成azurerm_resource_group资源
name = each.key
location = each.value
}
resource "aws_iam_user" "the-accounts" {
# 数组转换为集合
for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
name = each.key
}
variable "vpcs" {
# 这里定义了map类型的变量,并且限定了map具有的键
type = map(object({
cidr_block = string
}))
}
# 创建多个VPC资源
resource "aws_vpc" "example" {
for_each = var.vpcs
cidr_block = each.value.cidr_block
}
# 上述资源作为下面那个for_each的值
# 创建对应数量的网关资源
resource "aws_internet_gateway" "example" {
# 为每个VPC创建一个网关
# 资源作为值
for_each = aws_vpc.example
# 映射的值,在这里是完整的VPC对象
vpc_id = each.value.id
}
# 输出所有VPC ID
output "vpc_ids" {
value = {
for k, v in aws_vpc.example : k => v.id
}
# 显式依赖网关资源,确保网关创建后,输出才可用
depends_on = [aws_internet_gateway.example]
}