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代码,形成一个由data
与resource
组成的图(Graph),再通过rpc调用这些data
与resource
所对应的Provider插件;Provider插件的编写者根据Terraform所制定的插件框架来定义各种data
和resource
,并实现相应的CRUD方法;在实现这些CRUD方法时,可以调用目标平台提供的SDK,或是直接通过调用Http(s) API来操作目标平台。
下载Provider
在上述实践示例中,写完代码后在apply之前,首先执行了一次terraform init
。terraform 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如何声明,以及相关data
、resource
的使用说明,都可以在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/NAMESPACE
下TYPE
必须唯一。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_providers
中ucloud = {...}
块里的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就是ucloud
,aws_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分为两种:
- 标准:支持远程状态存储与状态锁
- 增强:在标准的基础上支持远程操作(在远程服务器上执行
plan
、apply
等操作)
目前增强型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
变量来传值给provider
、data
和resource
,但有一个例外,那就是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
一共有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"!
再观察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 从入门到实践
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,在下面评论区告诉我^_^^_^