从零开始实现一个 terraform plugin

terraform 作为一个优秀的开源基础设施管理、构建工具,官方或第三方提供了很多 plugin 来对接各种云平台(IaaS)。然而在我们平时开发和测试过程中,需要使用内部的 IaaS 服务频繁创建和删除 VM,而目前人工操作的方式比较费时费力,且没有现成的 plugin 可以使用。为了更方便地利用 terraform 工具来对内部 IaaS 资源进行管理和操作,我们决定自己开发一个 terraform plugin。

定义 Provider Schema

首先,我们定义入口文件 main.go

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"github.com/hashicorp/terraform/plugin"
qvm "qiniu.com/kirk-deploy/pkg/qvm/terraform"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProviderFunc: qvm.Provider,
})
}

其中 qvm.Provider 函数负责创建一个 provider resource。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func Provider() terraform.ResourceProvider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"url": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("QVM_URL", ""),
Description: descriptions["url"],
},
"ak": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("QVM_AK", ""),
Description: descriptions["ak"],
},
"sk": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("QVM_SK", ""),
Description: descriptions["sk"],
},
},
ResourcesMap: map[string]*schema.Resource{
"compute_instance": resourceComputeInstance(),
},
ConfigureFunc: configureProvider,
}
}

Schema 声明了 provider 配置文件的定义,对应的 tf 文件这样写:

1
2
3
4
5
provider qvm {
url = "https://qvm.qiniuapi.com"
ak = "your app key"
sk = "your app secret"
}

如果不在 tf 文件里指定 aksk,则 terraform 会根据 DefaultFunc,从环境变量 QVM_AKQVM_SK 中获取。Optional 代表字段是可选的,即使用户没有填也不会报错。

ResourcesMap 声明了 provider 支持的资源和对应资源的工厂函数,例如这里我们只实现了计算资源,工厂函数的定义我们稍后再解释。

定义 Resource Schema

上面提到的 resourceComputeInstance 负责创建一个 compute instance resource,对于计算资源我们可以这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
func resourceComputeInstance() *schema.Resource {
return &schema.Resource{
Create: resourceComputeInstanceCreate,
Read: resourceComputeInstanceRead,
Update: resourceComputeInstanceUpdate,
Delete: resourceComputeInstanceDelete,
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(30 * time.Minute),
Update: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},
Schema: map[string]*schema.Schema{
"image_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"instance_name": {
Type: schema.TypeString,
Optional: true,
},
"system_disk": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"category": {
Type: schema.TypeString,
Optional: true,
Default: enums.DiskCategoryCloudEfficiency,
ForceNew: true,
},
"size": {
Type: schema.TypeInt,
Optional: true,
Default: 40,
},
},
},
},
"data_disk": {
Type: schema.TypeList,
Optional: true,
MinItems: 1,
MaxItems: 15,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"category": {
Type: schema.TypeString,
Optional: true,
Default: enums.DiskCategoryCloudEfficiency,
ForceNew: true,
},
"size": {
Type: schema.TypeInt,
Optional: true,
Default: 40,
},
},
},
},
},
}
}

Create Read Update Delete 分别是管理资源的回调函数,terraform 框架会在合适的时间调用这几个函数,Timeouts 定义了每个操作的超时时间,Schema 与上面一样,是定义 tf 文件的具体结构。

ForceNew 代表一旦这个字段改变,则 terraform 会删除并重新创建该资源。TypeList 定义了一个列表,如果 MaxItems: 1 时,列表退化为单个资源。

为了简化起见,Schema 我们省略了很多字段,对应的 tf 文件可以这样写:

1
2
3
4
5
6
7
8
9
10
resource "compute_instance" "test" {
count = "${var.count}"
provider = "qvm"
image_id = "${var.image}"
instance_name = "${var.instance_name}-${count.index}"
system_disk {
category = "efficiency"
size = 40
}
}

其中 ${var.} 代表在 varaibles.tf 文件里定义的变量,具体可以用法可以参考 terraform 官方文档,这里不过多地介绍。

定义 Resource Operation Function

Create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
client, err := config.computeClient()
if err != nil {
return err
}

systemDisk := d.Get("system_disk").([]interface{})[0].(map[string]interface{})

systemDiskParameters := params.CreateInstanceSystemDiskParameters{
Category: enums.DiskCategory(systemDisk["category"].(string)),
Size: systemDisk["size"].(int),
}

parameters := &params.CreateInstanceParameters{
ImageId: d.Get("image_id").(string),
SystemDisk: systemDiskParameters,
InstanceName: enums.InstanceName(d.Get("instance_name").(string)),
}

log.Printf("[DEBUG] CreateInstanceParameters: %#v", parameters)
rsp, err := client.CreateInstance(parameters)
if err != nil {
log.Printf("[ERROR] create instance error, %v", err)
return err
}
log.Printf("[INFO] Instance ID: %s", rsp.Data.InstanceId)
d.SetId(rsp.Data.InstanceId)

return resourceComputeInstanceRead(d, meta)
}

Create 的实现最重要的一个操作是 SetId,如果服务端资源创建成功,会返回一个 InstanceId,SetId 会将这个 InstanceId 保存,作为以后判断资源是否更新的 key。

return 前又进行了一次 Read 操作,是为了防止有些状态字段没有通过 CreateResponse 返回,再尝试通过一次 Read 来获取这些状态信息。

Delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func resourceComputeInstanceDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
client, err := config.computeClient()
if err != nil {
return err
}

p := &params.DeleteInstanceParameters{
InstanceId: d.Id(),
}

_, err = client.DeleteInstance(p)
if err != nil {
return err
}

return nil
}

Update

1
2
3
func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
return resourceComputeInstanceRead(d, meta)
}

我们暂时不实现 Update 操作,因此这里只是简单地返回 Read。

Read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
client, err := config.computeClient()
if err != nil {
return err
}

p := &params.DescribeInstanceParameters{
InstanceId: d.Id(),
}

rsp, err := client.GetInstance(p)
if err != nil {
return err
}

instance := &rsp.Data
d.Set("image_id", instance.ImageId)
d.Set("instance_name", instance.InstanceName)
// ...

return nil
}

Read 通过 InstanceId 对资源状态进行查询,保存至 resource data。

编译和构建

上面基本代码框架实现后,我们就可以对 plugin 进行编译和构建了:

1
go build -o terraform-provider-qvm

二进制文件的命名必须遵守以下命名规则:

1
terraform-provider-<NAME>

构建后,我们手动将二进制拷贝至 terraform 默认的插件目录:${HOME}/.terraform/plguins

使用

进入工作目录,即 tf 文件保存的目录,假设这个目录的结构为:

1
2
3
4
5
terraform/qvm
├── provider.tf
├── resources.tf
├── variables.tf
└── terraform.tfvars

初始化

1
terraform init

修改配置

可以通过 export 或创建 .tfvars 文件,对配置进行修改:

1
2
export QVM_AK=
export QVM_SK=

创建 terraform.tfvars 文件:

1
2
3
instance_name = ""
count = 1
image = ""

查看更改

1
terraform plan

执行后 terraform 会对配置进行合法性校验。

应用更改

1
terraform apply

或者指定 .tfvars 文件:

1
terraform apply -var-file="terraform.tfvars"

销毁

1
terraform destroy

或者指定 .tfvars 文件:

1
terraform destroy -var-file="terraform.tfvars"

参考

https://www.terraform.io/docs/extend/writing-custom-providers.html

https://www.terraform.io/docs/extend/how-terraform-works.html

从零开始实现一个 terraform plugin

http://whypro.github.io/hexo-blog/2019/05/22/94a5cdecd69a/

Author

whypro

Posted on

2019-05-22

Updated on

2021-07-26

Licensed under

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×