Terraform入门介绍

Terraform是什么

老规矩,学习一个新组件或者技术,先要知道它是什么?干嘛用的?
一言以蔽之,Terraform是一个基于IaC理念的云服务编排工具,也是云服务编排事实上的标准了。
它是HashCorp公司开源的云服务编排工具,基于Golang语言开发。
通俗一点来说,可以使用Terraform来做各种云服务资源的编排,通过配置文件形式的声明方式,将云服务资源的管理操作实现自动化,提高了生产效率,也解放了人力。
当然,HashCorp公司还直接提供了商业版本的Terrafor云服务

Terraform实践

很不幸的是,在国内要想流畅地使用Terraform不太现实。受网络因素的影响,一方面:下载和安装Terraform就很慢甚至不可行,另一方面:许多Terraform插件也无法正常下载。针对学习,建议购买一台境外的云主机来作为Terraform的实践环境。

如下以在CentOS 8中实践Terraform为例进行说明。

安装Terraform

$ cat /etc/redhat-release 
CentOS Linux release 8.5.2111
$ yum install -y yum-utils
$ yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
$ yum -y install terraform
# 查看Terraform版本
$ terraform version
Terraform v1.5.6
on linux_amd64

Terraform示例

Terraform对云服务资源的操作是通过与Provider通信来实现的,它们的关系如下图。

Terraform与Provider之间通过RPC通信,Provider使用云服务厂商提供的SDK(或者Http(s) API)完成对自家云服务资源的管理操作(CRUD)。

如下通过使用HashCorp官方提供的Terraform插件hashicorp/local(操作本地文件)演示如何对云服务资源的全生命周期进行管理。

Terraform对资源的管理依赖于配置文件,在本示例中将这个配置文件命名为main.tf,其内容是使用HCL语言编写的资源配置信息。

如下是本次示例中main.tf文件的内容:

terraform { # terraform配置
  required_providers { # 声明需要使用的插件及其版本
    local = {
      source = "hashicorp/local"
      version = "2.4.0"
    }
  }
}

provider "local" { # 插件详细配置
  # Configuration options
}

resource "local_file" "terraform-introduction" { # 定义资源信息
  filename = "${path.module}/terraform-sample.txt"  # 即将创建的资源(文件)名称
  content = "Hi guys, this is the sample of Terraform" # 即将创建的资源(文件)内容
}

首先,需要执行初始化命令:terraform init

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/local versions matching "2.4.0"...
- Installing hashicorp/local v2.4.0...
- Installed hashicorp/local v2.4.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

执行上述命令后Terraform会自动下载在配置文件中声明的插件hashicorp/local,版本为:2.4.0
将在当前目录下自动生成一个.terraform目录和一个.terraform.lock.hcl文件。

$ ls -al .
total 20
drwxr-xr-x 3 root root 4096 Sep  5 14:36 .
drwxr-xr-x 3 root root 4096 Sep  5 14:02 ..
-rw-r--r-- 1 root root  325 Sep  5 14:36 main.tf
drwxr-xr-x 3 root root 4096 Sep  5 14:13 .terraform          # 
-rw-r--r-- 1 root root 1181 Sep  5 14:13 .terraform.lock.hcl # .terraform目录和.terraform.lock.hcl文件是在执行terraform init命令之后自动生成的
# 查看.terraform目录内容
$ tree .terraform
.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── local
                └── 2.4.0
                    └── linux_amd64
                        └── terraform-provider-local_v2.4.0_x5 # 这是一个文件,也就是说Terraform插件其实都是一些编译好的可执行文件,它们与Terraform之间使用RPC进行通信

6 directories, 1 file

其次,执行命令terraform plan查看要执行的变更计划。

$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.terraform-introduction will be created
  + resource "local_file" "terraform-introduction" {
      + content              = "Hi guys, this is the sample of Terraform" # 要创建的文件内容
      + content_base64sha256 = (known after apply) # known after apply 表示要创建完毕之后才知道
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "./terraform-sample.txt" # 要创建的文件名称
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform
apply" now.

输出日志中提示需要创建,修改,销毁多少个资源:

Plan: 1 to add, 0 to change, 0 to destroy. # 创建1个资源,修改0个资源,删除0个资源

执行terraform plan命令类似于查看SQL语句的查询计划,并不会真正执行对资源的管理操作。

再次,执行变更命令:terraform apply

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.terraform-introduction will be created
  + resource "local_file" "terraform-introduction" {
      + content              = "Hi guys, this is the sample of Terraform"
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "./terraform-sample.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes # 在最后一步会提示确认是否执行变更操作,输入yes表示确认执行

local_file.terraform-introduction: Creating...
local_file.terraform-introduction: Creation complete after 0s [id=20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

执行上述命令之后,将会在当前目录下生成2个文件:

  • terraform-sample.txt :这个是需要生成的资源
  • terraform.tfstate:这个是Terraform的状态管理文件,Terraform通过这个文件来判断是否存在变更

文件terraform-sample.txt的内容正是我们在Terraform配置文件中指定的值:

$ cat terraform-sample.txt 
Hi guys, this is the sample of Terraform

紧接着再一次执行:terraform apply看看会发生什么?

$ terraform apply
local_file.terraform-introduction: Refreshing state... [id=20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

这一次Terraform什么动作也没做,这是因为Terraform具备状态管理机制,所以它知道当前操作不存在资源变更,故而不执行任何动作。

最后,执行terraform destroy命令删除通过Terraform创建的资源。

$ terraform destroy
local_file.terraform-introduction: Refreshing state... [id=20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # local_file.terraform-introduction will be destroyed
  - resource "local_file" "terraform-introduction" {
      - content              = "Hi guys, this is the sample of Terraform" -> null
      - content_base64sha256 = "RDTeUFwAR6kKp9SvQiB71mfVRs8bW5FSujKmQ2sru/M=" -> null
      - content_base64sha512 = "CivqLJk+R0oPDFYapFst8Gah7s+8606yBqQii3VumRYSsK0QUe9bi19jKFJMNs2uKCe+MzfwsnN+XBoWLc9Crg==" -> null
      - content_md5          = "ad7d91f67b80a7425aa5ad4be7c0ce9a" -> null
      - content_sha1         = "20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab" -> null
      - content_sha256       = "4434de505c0047a90aa7d4af42207bd667d546cf1b5b9152ba32a6436b2bbbf3" -> null
      - content_sha512       = "0a2bea2c993e474a0f0c561aa45b2df066a1eecfbceb4eb206a4228b756e991612b0ad1051ef5b8b5f6328524c36cdae2827be3337f0b2737e5c1a162dcf42ae" -> null
      - directory_permission = "0777" -> null
      - file_permission      = "0777" -> null
      - filename             = "./terraform-sample.txt" -> null
      - id                   = "20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes # 在最后也需要确认是否执行

local_file.terraform-introduction: Destroying... [id=20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab]
local_file.terraform-introduction: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

删除资源跟创建资源一样,在最后一步都会让用户确认是否执行,这是有必要的,避免操作失误删除掉资源。

总结起来,使用Terraform管理资源的生命周期,需要用到如下几个命令:

  • terraform init:执行初始化,Terraform会根据配置文件的内容自动下载需要的插件及模块
  • terraform plan:这一步不是必须的,相当于是一个预览计划操作,类似于MySQL的执行计划一样,让用户从这个命令结果中知道要执行的操作和影响的内容
  • terraform apply:真正执行资源创建或变更操作,默认情况下会在最后一步让用户确认是否执行,如果不希望交互提示,可以加上选项参数:-auto-approve
  • terraform destroy:删除通过Terraform创建的资源,也会在最后一步让用户确认操作是否执行,如果不希望交互提示,可以加上选项参数:-auto-approve

Provider插件机制

Terraform被设计成一个多云基础设施编排工具,可以同时编排各种云平台或是其他基础设施的资源,Terraform实现多云编排的方法就是Provider插件机制。
Terraform使用的是HashiCorp自研的go-plugin库,本质上各个Provider插件都是独立的进程,与Terraform进程之间通过rpc进行调用。
Terraform引擎首先读取并分析用户编写的Terraform代码,形成一个由dataresource组成的图(Graph),再通过rpc调用这些dataresource所对应的Provider插件;Provider插件的编写者根据Terraform所制定的插件框架来定义各种dataresource,并实现相应的CRUD方法;在实现这些CRUD方法时,可以调用目标平台提供的SDK,或是直接通过调用Http(s) API来操作目标平台。

下载Provider

在上述实践示例中,写完代码后在apply之前,首先执行了一次terraform initterraform init会分析代码中所使用到的Provider,并尝试下载Provider插件到本地。观察执行完示例的文件夹,会发现有一个.terraform文件夹。

# ls -al
total 28
drwxr-xr-x 3 root root 4096 Nov  4 08:30 .
drwxr-xr-x 3 root root 4096 Nov  4 08:28 ..
-rw-r--r-- 1 root root  511 Nov  4 08:29 main.tf
drwxr-xr-x 3 root root 4096 Nov  4 08:29 .terraform # 这个就是保存插件的文件夹
-rw-r--r-- 1 root root 1181 Nov  4 08:29 .terraform.lock.hcl
-rwxr-xr-x 1 root root   40 Nov  4 08:30 terraform-sample.txt
-rw-r--r-- 1 root root 1488 Nov  4 08:30 terraform.tfstate

在示例中所使用的插件hashicorp/local就被放在文件夹.terraform中。

# tree .terraform
.terraform
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── local
                └── 2.4.0
                    └── linux_amd64
                        └── terraform-provider-local_v2.4.0_x5

6 directories, 1 file

有的时候下载某些Provider会非常缓慢,或是在开发环境中存在许多的Terraform项目,每个项目都保有自己独立的插件文件夹非常浪费磁盘,这时可以使用插件缓存。
有两种方式可以启用插件缓存:
第一种方法是配置TF_PLUGIN_CACHE_DIR这个环境变量:

export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"

第二种方法是使用命令行配置文件。Windows下是在相关用户的%APPDATA%目录下创建名为”terraform.rc”的文件,Macos和Linux用户则是在用户的HOME路径下创建名为”.terraformrc”的文件,在文件中配置如下内容:

plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"

当启用插件缓存之后,每当执行terraform init命令时,Terraform引擎会首先检查期望使用的插件在缓存文件夹中是否已经存在,如果存在,那么就会将缓存的插件拷贝到当前工作目录下的.terraform文件夹内。如果插件不存在,那么Terraform仍然会像之前那样下载插件,并首先保存在插件文件夹中,随后再从插件文件夹拷贝到当前工作目录下的.terraform文件夹内。为了尽量避免同一份插件被保存多次,只要操作系统提供支持,Terraform就会使用符号连接而不是实际从插件缓存目录拷贝到工作目录。

需要特别注意的是,Windows系统下plugin_cache_dir的路径也必须使用/作为分隔符,应使用C:/somefolder/plugin_cahce而不是C:\somefolder\plugin_cache

Terrafom引擎永远不会主动删除缓存文件夹中的插件,缓存文件夹的尺寸可能会随着时间而增长到非常大,这时需要手工清理。

搜索Provider

想要了解有哪些被官方接纳的Provider,有两种方法:
第一种方法是访问Terraform官方Provider文档
第二种方法就是前往registry.terraform.io进行搜索:

目前推荐在registry搜索Provider,因为大量由社区开发的Provider都被注册在了那里。
一般来说,相关Provider如何声明,以及相关dataresource的使用说明,都可以在registry上查阅到相关文档。
registry.terraform.io不但可以查询Provider,也可以用来发布Provider;并且它也可以用来查询和发布模块(Module)。

声明Provider

一组Terraform代码要被执行,相关的Provider必须在代码中被声明。
不少的Provider在声明时需要传入一些关键信息才能被使用,在如下示例中,必须给出访问密钥以及期望执行的Region信息。

terraform {
  required_providers {
    ucloud    = {
      source  = "ucloud/ucloud" # 声明Provider源
      version = ">=1.24.1"      # 声明Provider版本
    }
  }
}

provider "ucloud" {
  public_key  = "your_public_key"
  private_key = "your_private_key"
  project_id  = "your_project_id"
  region      = "cn-bj2"
}

在这段Provider声明中,首先在terraform节的required_providers里声明了本段代码必须要名为ucloud的Provider才可以执行,source = "ucloud/ucloud"这一行声明了ucloud这个插件的源地址。一个源地址是全球唯一的,它指示了Terraform如何下载该插件。一个源地址由三部分组成:[<HOSTNAME>/]<NAMESPACE>/<TYPE>HOSTNAME是选填的,默认是官方的registry.terraform.io,也可以构建自己私有的Terraform仓库;NAMESPACE是在Terraform仓库内的组织名,这代表了发布和维护插件的组织或个人;TYPE是代表插件的一个短名,在特定的HOSTNAME/NAMESPACETYPE必须唯一。required_providers中的插件声明还声明了所需要的插件版本约束,在例子里就是version = ">=1.24.1"。Terraform插件的版本号采用MAJOR.MINOR.PATCH的语义化格式,版本约束通常使用操作符和版本号表达约束条件,条件之间可以用逗号拼接,表达AND关联,例如">= 1.2.0, < 2.0.0"。可以采用的操作符有:

  • =(或者不加=,直接使用版本号):只允许特定版本号,不允许与其他条件合并使用
  • !=:不允许特定版本号
  • >,>=,<,<=:与特定版本号进行比较,可以是大于、大于等于、小于、小于等于
  • ~>:锁定MAJOR与MINOR,允许PATCH号大于等于特定版本号,例如,~>0.9等价于>=0.9, <0.9,\~>0.8.4等价于>=0.8.4, <0.9

Terraform会检查当前工作环境或是插件缓存中是否存在满足版本约束的插件,如果不存在,那么Terraform会尝试下载。如果Terraform无法获得任何满足版本约束条件的插件,那么它会拒绝继续执行任何后续操作。
可以用添加后缀的方式来声明预览版,例如:1.2.0-beta。预览版只能通过”=”操作符(或是空缺操作符)后接明确的版本号的方式来指定,不可以与>=、~>等搭配使用。
推荐使用>=操作符约束最低版本,如果在编写旨在由他人复用的模块代码时,请避免使用~>操作符,即使知道模块代码与新版本插件会有不兼容。

内建Provider

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

多Provider实例

在如下示例代码中,provider "ucloud"required_providersucloud = {...}块里的ucloud,都是Provider的Local Name,一个Local Name是在一个模块中对一个Provider的唯一标识。

terraform {
  required_providers {
    ucloud    = {
      source  = "ucloud/ucloud" # 声明Provider源
      version = ">=1.24.1"      # 声明Provider版本
    }
  }
}

provider "ucloud" {
  public_key  = "your_public_key"
  private_key = "your_private_key"
  project_id  = "your_project_id"
  region      = "cn-bj2"
}

也可以声明多个同类型的Provider,并给予不同的Local Name:

terraform {
  required_version = ">=0.13.5"
  required_providers {
    ucloudbj  = {
      source  = "ucloud/ucloud"
      version = ">=1.24.1"
    }
    ucloudsh  = {
      source  = "ucloud/ucloud"
      version = ">=1.24.1"
    }
  }
}

provider "ucloudbj" {
  public_key  = "your_public_key"
  private_key = "your_private_key"
  project_id  = "your_project_id"
  region      = "cn-bj2"
}

provider "ucloudsh" {
  public_key  = "your_public_key"
  private_key = "your_private_key"
  project_id  = "your_project_id"
  region      = "cn-sh2"
}

data "ucloud_security_groups" "default" {
  provider = ucloudbj
  type     = "recommend_web"
}

data "ucloud_images" "default" {
  provider          = ucloudsh
  availability_zone = "cn-sh2-01"
  name_regex        = "^CentOS 6.5 64"
  image_type        = "base"
}

如上示例中声明了两个UCloud Provider,分别定位在北京区域和上海区域。在接下来的data声明中显式指定了provider的Local Name,这将可以在一组配置文件中同时操作不同区域、不同账号的资源。

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

terraform {
  required_version = ">=0.13.5"
  required_providers {
    ucloud    = {
      source  = "ucloud/ucloud"
      version = ">=1.24.1"
    }
  }
}

provider "ucloud" {
  public_key  = "your_public_key"
  private_key = "your_private_key"
  project_id  = "your_project_id"
  region      = "cn-bj2"
}

provider "ucloud" {
  alias       = "ucloudsh"
  public_key  = "your_public_key"
  private_key = "your_private_key"
  project_id  = "your_project_id"
  region      = "cn-sh2"
}

data "ucloud_security_groups" "default" {
  type = "recommend_web"
}

data "ucloud_images" "default" {
  provider          = ucloud.ucloudsh
  availability_zone = "cn-sh2-01"
  name_regex        = "^CentOS 6.5 64"
  image_type        = "base"
}

和多Local Name相比,使用别名可以区分provider的不同实例。terraform节的required_providers中只声明了一次ucloud,并且在data中指定provider时传入的是ucloud.ucloudsh,多实例Provider请使用别名。

每一个不带alias属性的provider声明都是一个默认provider声明,没有显式指定provider的data以及resource都使用默认资源名第一个单词所对应的provider,例如,ucloud_images这个data对应的默认provider就是ucloudaws_instance这个resource对应的默认provider就是aws

假如代码中所有显式声明的provider都有别名,那么Terraform运行时会构造一个所有配置均为空值的默认provider。假如provider有必填字段,并且又有资源使用了默认provider,那么Terraform会抛出一个错误,提示默认provider缺失了必填字段。

状态管理

初探状态文件

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

tree .
.
├── main.tf
├── terraform-sample.txt
└── terraform.tfstate # 这个就是Terraform的状态管理文件

0 directories, 3 files

如下示例代码:

terraform {
  required_providers {
    local = {
      source = "hashicorp/local"
      version = "2.4.0"
    }
  }
}

provider "local" {
  # Configuration options
}

resource "local_file" "terraform-introduction" {
  filename = "${path.module}/terraform-sample.txt"
  content = "Hi guys, this is the sample of Terraform"
}

执行terraform apply后,可以看到terraform.tfstate的内容:

{
  "version": 4,
  "terraform_version": "1.6.3",
  "serial": 1,
  "lineage": "2305fabd-e19b-e864-d41f-a8364ed714a2",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "local_file",
      "name": "terraform-introduction",
      "provider": "provider[\"registry.terraform.io/hashicorp/local\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "content": "Hi guys, this is the sample of Terraform",
            "content_base64": null,
            "content_base64sha256": "RDTeUFwAR6kKp9SvQiB71mfVRs8bW5FSujKmQ2sru/M=",
            "content_base64sha512": "CivqLJk+R0oPDFYapFst8Gah7s+8606yBqQii3VumRYSsK0QUe9bi19jKFJMNs2uKCe+MzfwsnN+XBoWLc9Crg==",
            "content_md5": "ad7d91f67b80a7425aa5ad4be7c0ce9a",
            "content_sha1": "20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab",
            "content_sha256": "4434de505c0047a90aa7d4af42207bd667d546cf1b5b9152ba32a6436b2bbbf3",
            "content_sha512": "0a2bea2c993e474a0f0c561aa45b2df066a1eecfbceb4eb206a4228b756e991612b0ad1051ef5b8b5f6328524c36cdae2827be3337f0b2737e5c1a162dcf42ae",
            "directory_permission": "0777",
            "file_permission": "0777",
            "filename": "./terraform-sample.txt",
            "id": "20d3f52ed0afe6a197c6fd3447c8e01ec6d605ab",
            "sensitive_content": null,
            "source": null
          },
          "sensitive_attributes": []
        }
      ]
    }
  ],
  "check_results": null
}

可以看到,创建的resource信息都被以json格式保存在tfstate文件里。

由于tfstate文件的存在,在执行terraform apply之后立即再次apply是不会执行任何变更的。那么如果删除了这个tfstate文件,然后再执行apply会发生什么呢?Terraform读取不到tfstate文件,会认为这是第一次创建这组资源,所以它会再一次创建代码中描述的所有资源。更加麻烦的是,由于前一次创建的资源所对应的状态信息被删除了,所以再也无法通过执行terraform destroy来销毁和回收这些资源,实际上产生了资源泄漏,所以妥善保存这个状态文件是非常重要的。

另外,如果对Terraform的代码进行了一些修改,导致生成的执行计划将会改变状态,那么在实际执行变更之前,Terraform会复制一份当前的tfstate文件到同路径下的terraform.tfstate.backup中,以防止由于各种意外导致的tfstate损毁。

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

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

data "ucloud_security_groups" "default" {
  type = "recommend_web"
}

data "ucloud_images" "default" {
  availability_zone = "cn-sh2-02"
  name_regex        = "^CentOS 6.5 64"
  image_type        = "base"
}

resource "ucloud_instance" "normal" {
  availability_zone = "cn-sh2-02"
  image_id          = data.ucloud_images.default.images[0].id
  instance_type     = "n-basic-2"
  root_password     = "supersecret1234"
  name              = "tf-example-normal-instance"
  tag               = "tf-example"
  boot_disk_type    = "cloud_ssd"
  security_group = data.ucloud_security_groups.default.security_groups[0].id
  delete_disks_with_instance = true
}

在代码中明文传入了root_password的值是supersecret1234,执行了terraform apply后观察tfstate文件中相关段落:

{
  "mode": "managed",
  "type": "ucloud_instance",
  "name": "normal",
  "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
  "instances": [
    {
      "schema_version": 0,
      "attributes": {
        "allow_stopping_for_update": null,
        "auto_renew": false,
        "availability_zone": "cn-sh2-02",
        "boot_disk_size": 20,
        "boot_disk_type": "cloud_ssd",
        "charge_type": null,
        "cpu": 2,
        "cpu_platform": "Intel/Broadwell",
        "create_time": "2020-11-16T18:06:32+08:00",
        "data_disk_size": null,
        "data_disk_type": null,
        "data_disks": [],
        "delete_disks_with_instance": true,
        "disk_set": [
          {
            "id": "bsi-krv0ilrc",
            "is_boot": true,
            "size": 20,
            "type": "cloud_ssd"
          }
        ],
        "duration": null,
        "expire_time": "1970-01-01T08:00:00+08:00",
        "id": "uhost-u2byoz4i",
        "image_id": "uimage-ku3uri",
        "instance_type": "n-basic-2",
        "ip_set": [
          {
            "internet_type": "Private",
            "ip": "10.25.94.58"
          }
        ],
        "isolation_group": "",
        "memory": 4,
        "min_cpu_platform": null,
        "name": "tf-example-normal-instance",
        "private_ip": "10.25.94.58",
        "remark": "",
        "root_password": "supersecret1234",
        "security_group": "firewall-a0lqq3r3",
        "status": "Running",
        "subnet_id": "subnet-0czucaf2",
        "tag": "tf-example",
        "timeouts": null,
        "user_data": null,
        "vpc_id": "uvnet-0noi3kun"
      },
      "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0=",
      "dependencies": [
        "data.ucloud_images.default",
        "data.ucloud_security_groups.default"
      ]
    }
  ]
}

可以看到root_password的值supersecret1234是以明文形式被写在tfstate文件里的。这是Terraform从设计之初就确定的,并且在可见的未来不会有改善。不论是在代码中明文硬编码,还是使用输入参数,亦或是妙想天开地使用函数在运行时从外界读取,都无法改变这个结果。

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

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

另外如果是一个团队在使用Terraform管理一组资源,团队成员之间要如何共享这个状态文件?能不能把tfstate文件签入源代码管理工具进行保存?
把tfstate文件签入管代码管理工具是非常错误的,这就好比把数据库签入了源代码管理工具,如果两个人同时签出了同一份tfstate,并且对代码做了不同的修改,又同时apply了,这时想要把tfstate签入源码管理系统可能会遭遇到无法解决的冲突。

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

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

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

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

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用户:

# 对于Windows用户,官方推荐的包管理器是choco,可以去https://chocolatey.org/ 下载安装好chocolatey后,以管理员身份启动powershell,然后如下命令
# 如果只想纯手动安装,那么可以前往Consul官网https://developer.hashicorp.com/consul/downloads 下载对应操作系统的可执行文件
choco install consul

安装完成后的验证:

$ consul
Usage: consul [--version] [--help] <command> [<args>]

Available commands are:
    acl            Interact with Consul's ACLs
    agent          Runs a Consul agent
    catalog        Interact with the catalog
    config         Interact with Consul's Centralized Configurations
    connect        Interact with Consul Connect
    debug          Records a debugging archive for operators
    event          Fire a new event
    exec           Executes a command on Consul nodes
    force-leave    Forces a member of the cluster to enter the "left" state
    info           Provides debugging information for operators.
    intention      Interact with Connect service intentions
    join           Tell Consul agent to join cluster
    keygen         Generates a new encryption key
    keyring        Manages gossip layer encryption keys
    kv             Interact with the key-value store
    leave          Gracefully leaves the Consul cluster and shuts down
    lock           Execute a command holding a lock
    login          Login to Consul using an auth method
    logout         Destroy a Consul token created with login
    maint          Controls node or service maintenance mode
    members        Lists the members of a Consul cluster
    monitor        Stream logs from a Consul agent
    operator       Provides cluster-level tools for Consul operators
    reload         Triggers the agent to reload configuration files
    rtt            Estimates network round trip time between nodes
    services       Interact with services
    snapshot       Saves, restores and inspects snapshots of Consul server state
    tls            Builtin helpers for creating CAs and certificates
    validate       Validate config files/directories
    version        Prints the Consul version
    watch          Watch for changes in Consul

安装完Consul后,可以启动一个测试版Consul服务:

$ consul agent -dev

Consul会在本机8500端口开放Http访问点,我们可以通过浏览器访问http://localhost:8500。

使用Backend

如下Terraform示例代码:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }

  # 配置Backend,用于保存状态文件内容
  backend "consul" {
    address = "localhost:8500"
    scheme  = "http"
    path    = "my-ucloud-project"
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
}

terraform节中添加了backend配置,指定使用localhost:8500为地址,使用http协议访问该地址,指定tfstate文件存放在Consul键值存储服务的my-ucloud-project路径下。

当执行完terraform apply后,访问http://localhost:8500/ui/dc1/kv

可以看到my-ucloud-project,点击进入:

可以看到,原本保存在工作目录下tfstate文件的内容,被保存在了Consul中名为my-ucloud-project的键下。

在执行terraform destroy后,重新访问http://localhost:8500/ui/dc1/kv

可以看到,my-ucloud-project这个键仍然存在,点击进去查看:

可以看到内容为空,代表基础设施已经被成功销毁。

观察锁文件

那么在上述实验过程里,锁究竟在哪里?如何能够体验到锁的存在?如下对代码进行一点修改:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
  backend "consul" {
    address = "localhost:8500"
    scheme  = "http"
    path    = "my-ucloud-project"
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

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

这次的变化是在ucloud_vpc的定义上添加了一个local-exec类型的provisioner,Terraform进程在成功创建了该VPC后,会在执行Terraform命令行的机器上执行一条命令:sleep 1000,这个时间足以将Terraform进程阻塞足够长的时间,以便观察锁信息了。

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

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # ucloud_vpc.vpc will be created
  + resource "ucloud_vpc" "vpc" {
      + cidr_blocks  = [
          + "10.0.0.0/16",
        ]
      + create_time  = (known after apply)
      + id           = (known after apply)
      + name         = (known after apply)
      + network_info = (known after apply)
      + remark       = (known after apply)
      + tag          = "Default"
      + update_time  = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

ucloud_vpc.vpc: Creating...
ucloud_vpc.vpc: Provisioning with 'local-exec'...
ucloud_vpc.vpc (local-exec): Executing: ["/bin/sh" "-c" "sleep 1000"]
ucloud_vpc.vpc: Still creating... [10s elapsed]
...

重新访问http://localhost:8500/ui/dc1/kv

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

在这里成功观测到了.lock.lockinfo文件,点击.lock看看:

Consul UI提醒该键值对目前正被锁定,而它的内容是空,查看.lockinfo的内容:

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

另起一个新的命令行窗口,在同一个工作目录下尝试另一次执行terraform apply

$ terraform apply
Acquiring state lock. This may take a few moments...

Error: Error locking state: Error acquiring the state lock: Lock Info:
  ID:        563ef038-610e-85cf-ca89-9e3b4a830b67
  Path:      my-ucloud-project
  Operation: OperationTypeApply
  Who:       byers@ByersMacBook-Pro.local
  Version:   0.13.5
  Created:   2020-11-16 11:53:50.473561 +0000 UTC
  Info:      consul session: 9bd80a12-bc2f-1c5b-af0f-cdb07e5e69dc


Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.

可以看到,同时另一个人试图对同一个tfstate执行变更的尝试失败了,因为它无法顺利获取到锁。

ctrl+c终止被阻塞的terraform apply执行,然后重新访问http://localhost:8500/ui/dc1/kv

可以看到,包含锁的文件夹消失了。
Terraform命令行进程在接收到ctrl+c信号时,会首先把当前已知的状态信息写入Backend内,然后释放Backend上的锁,再结束进程。但是如果Terraform进程是被强行杀死,或是机器掉电,那么在Backend上就会遗留一个锁,导致后续的操作都无法执行,这时需要用terraform force-unlock命令强行删除锁。

假如一开始Backend配置写错了

假设有一个干净的工作目录,新建了一个main.tf代码文件,在terraform配置节当中配置了如下Backend:

backend "consul" {
    address = "localhost:8600"
    scheme  = "http"
    path    = "my-ucloud-project"
}

address参数写错了,端口号从8500写成了8600,这时执行一次terraform init

$ terraform init

Initializing the backend...

Successfully configured the backend "consul"! Terraform will automatically
use this backend unless the backend configuration changes.

Error: Failed to get existing workspaces: Get "http://localhost:8600/v1/kv/my-ucloud-project-env:?keys=&separator=%2F": EOF

并不奇怪,Terraform提示无法连接到localhost:8600。这时把Backend配置的端口纠正回8500,重新执行init看看:

$ terraform init

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends.



Error: Error inspecting states in the "consul" backend:
    Get "http://localhost:8600/v1/kv/my-ucloud-project-env:?keys=&separator=%2F": EOF

Prior to changing backends, Terraform inspects the source and destination
states to determine what kind of migration steps need to be taken, if any.
Terraform failed to load the states. The data in both the source and the
destination remain unmodified. Please resolve the above error and try again.

还是错误,Terraform还是试图连接localhost:8600,并且这次的报错信息提示需要帮助它解决错误,以便它能够决定如何进行状态数据的迁移。
这是因为Terraform发现Backend的配置发生了变化,所以它尝试从原先的Backend读取状态数据,并且尝试将之迁移到新的Backend,但因为原先的Backend是错的,所以它会再次提示连接不上localhost:8600

如果此时检查工作目录下的.terraform目录,会看到其中多了一个本地的terraform.tfstate文件:

其内容如下:

{
    "version": 3,
    "serial": 2,
    "lineage": "aa296584-3606-f9b0-78da-7c5563b46c7b",
    "backend": {
        "type": "consul",
        "config": {
            "access_token": null,
            "address": "localhost:8600",
            "ca_file": null,
            "cert_file": null,
            "datacenter": null,
            "gzip": null,
            "http_auth": null,
            "key_file": null,
            "lock": null,
            "path": "my-ucloud-project",
            "scheme": "http"
        },
        "hash": 3939494596
    },
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        }
    ]
}

可以看到它把最初的Backend配置记录在了里面,地址仍然是localhost:8600,这就导致即使修正了Backend配置,也无法成功init。在这个场景下,解决方法也很简单,直接删除这个本地tfstate文件即可。

状态迁移

先重启一下测试版Consul服务,清除旧有的状态。
假如一开始没有声明backend:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
}

provider "ucloud" {
  public_key  = "JInqRnkSY8eAmxKFRxW9kVANYThfIW9g2diBbZ8R8"
  private_key = "8V5RClzreyKBxrJ2GsePjfDYHy55yYsIIy3Qqzjjah0C0LLxhXkKSzEKFWkATqu4U"
  project_id  = "org-a2pbab"
  region      = "cn-sh2"
}

resource "ucloud_vpc" "vpc" {
  cidr_blocks = ["10.0.0.0/16"]
}

然后执行terraform init,继而执行terraform apply,那么将成功创建云端资源,并且在工作目录下会有一个terraform.tfstate文件:

{
  "version": 4,
  "terraform_version": "0.13.5",
  "serial": 1,
  "lineage": "a0335546-0039-cccc-467b-5dc3050c8212",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "ucloud_vpc",
      "name": "vpc",
      "provider": "provider[\"registry.terraform.io/ucloud/ucloud\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "cidr_blocks": [
              "10.0.0.0/16"
            ],
            "create_time": "2020-11-16T22:24:38+08:00",
            "id": "uvnet-ssgiofxv",
            "name": "tf-vpc-20201116142437539000000001",
            "network_info": [
              {
                "cidr_block": "10.0.0.0/16"
              }
            ],
            "remark": null,
            "tag": "Default",
            "update_time": "2020-11-16T22:24:38+08:00"
          },
          "private": "bnVsbA=="
        }
      ]
    }
  ]
}

随后加上了之前写过的指向本机测试Consul服务的Backend声明,然后执行terraform init

$ terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "consul" backend. No existing state was found in the newly
  configured "consul" backend. Do you want to copy this state to the new "consul"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Terraform成功地检测到Backend类型从local变为了consul,并且确认了Consul里同名路径下没有状态文件存在,于是Terraform可以把本机的状态文件迁移到新的Backend里,但这需要我们手工确认。输入yes并且回车:

Enter a value: yes


Successfully configured the backend "consul"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Using previously-installed ucloud/ucloud v1.22.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

这时访问http://localhost:8500/ui/dc1/kv/my-ucloud-project/edit

本机的状态数据被成功地迁移到了Consul里(虽然和本机的文件并不完全相同,但状态数据是相同的)。

那假如试图迁移状态时,新Backend的目标路径上已经存在tfstate会发生什么呢?简单地说一下结果,就是Terraform会把tfstate和新Backend上既有的tfstate下载到本机的一个临时目录下,然后要求人工核对以后决定是否覆盖既有的tfstate。

Backend配置的动态赋值

Terraform可以通过variable变量来传值给providerdataresource,但有一个例外,那就是Backend配置。Backend配置只允许硬编码,或者不传值。
这个问题是因为Terraform运行时本身设计的运行顺序导致的,一直到2019年05月官方才给出了解决方案,那就是“部分配置”(partial configuration)。

简单来说就是可以在tf代码的backend声明中不给出具体的配置:

terraform {
  required_version = "~>0.13.5"
  required_providers {
    ucloud = {
      source  = "ucloud/ucloud"
      version = ">=1.22.0"
    }
  }
  backend "consul" {

  }
}

而在另一个独立的文件中给出相关配置,例如在工作目录下创建一个名为backend.hcl的文件:

address = "localhost:8500"
scheme  = "http"
path    = "my-ucloud-project"

本质上就是把原本属于Backend Consul节的属性赋值代码搬迁到一个独立的hcl文件内,然后执行terraform init时附加backend-config参数:

$ terraform init -backend-config=backend.hcl

这样也可以初始化成功,通过这种打补丁的方式,可以复用他人预先写好的Terraform代码,在执行时把属于自己的Backend配置信息以独立的backend-config文件的形式传入来进行初始化。

Backend的权限及版本控制

Backend本身并没有设计任何的权限以及版本控制,这方面完全依赖于具体的Backend实现。
以AWS S3为例,可以针对不同的Bucket设置不同的IAM,用以防止开发测试人员直接操作生产环境,或是给予部分人员对状态信息的只读权限;另外也可以开启S3的版本控制功能,以防错误修改了状态文件(Terraform命令行有修改状态的相关指令)。

状态的隔离存储

假设Terraform代码可以创建一个通用的基础设施,比如说是云端的一个eks、aks集群,或者是一个基于S3的静态网站,那么可能要为很多团队创建并维护这些相似但要彼此隔离的Stack,又或者要为部署的应用维护开发、测试、预发布、生产四套不同的部署。那么该如何做到不同的部署,彼此状态文件隔离存储和管理呢?

一种简单的方法就是分成不同的文件夹存储(将代码复制到不同的文件夹中保存)。

可以把不同产品不同部门使用的基础设施分成不同的文件夹,在文件夹内维护相同的代码文件,配置不同的backend-config,把状态文件保存到不同的Backend上。这种方法可以给予最大程度的隔离,缺点是需要拷贝许多份相同的代码。

第二种更加轻量级的方法就是Workspace(注:Terraform开源版的Workspace与Terraform Cloud云服务的Workspace实际上是两个不同的概念,这里介绍的是开源版的Workspace)。Workspace允许在同一个文件夹内,使用同样的Backend配置,但可以维护任意多个彼此隔离的状态文件。
如下图所示,当前有一个状态文件,名字是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的文件。

重新观察Consul存储会发现多了一个文件:my-ucloud-project-env:feature1,这就是Terraform为feature1这个Workspace创建的独立的状态文件。执行一下terraform apply,然后再看这个文件的内容:

可以看到,状态被成功写入了feature1的状态文件。

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

$ terraform workspace list
  default
* feature1

一共有defaultfeature1两个Workspace,当前工作在feature1上,可以用以下命令切换回default

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

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

$ terraform workspace show
default

可以用以下命令删除feature1:

$ terraform workspace delete feature1
Deleted workspace "feature1"!

再观察Consul存储,就会发现feature1的状态文件被删除了:

目前支持多工作区的Backend有:

  • AzureRM
  • Consul
  • COS
  • GCS
  • Kubernetes
  • Local
  • Manta
  • Postgres
  • Remote
  • S3

该使用哪种隔离方式

相比起多文件夹隔离的方式来说,基于Workspace的隔离更加简单,只需要保存一份代码,不需要为Workspace编写额外代码,用命令行就可以在不同工作区之间来回切换。但是Workspace的缺点也同样明显,由于所有工作区的Backend配置是一样的,所以有权读写某一个Workspace的人可以读取同一个Backend路径下所有其他Workspace;另外Workspace是隐式配置的(调用命令行),所以有时会忘记自己工作在哪个Workspace下。

Terraform官方为Workspace设计的场景是:有时开发人员想要对既有的基础设施做一些变更,并进行一些测试,但又不想直接冒险修改既有的环境。这时可以利用Workspace复制出一个与既有环境完全一致的平行环境,在这个平行环境里做一些变更,并进行测试和实验工作。

Workspace对应的源代码管理模型里的主干—分支模型,如果团队希望维护的是不同产品之间不同的基础设施,或是开发、测试、预发布、生产环境,那么最好还是使用不同的文件夹以及不同的Backend配置进行管理。

【参考】
Terraform Language Documentation
Terraform Registry
Terraform Registry Publishing
Terraform State
Terraform介绍
Terraform 101 从入门到实践


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,在下面评论区告诉我^_^^_^