Terraform provider
开发
Terraform
是 IAC
(基础设施即代码) 的最佳开源工具之一,几乎所有的云平台都会提供 Terraform Provider
来供使用者能自动化的创建各种云资源。 同时 Terraform
是一个高度可扩展的工具,通过 Provider
来支持新的基础架构,我们可以通过自己开发 Provider
对大多数资源进行管理。下面就展示一个 和Elasticsearc
相关的 Terraform Provider
的demo
。
项目结构
根据 Terraform
官方文档,一个基本的 Terraform provider
大致会有以下几个部分组成:
provider.go
:这是插件的根源,用于描述插件的属性,如:配置的秘钥,支持的资源列表,回调配置等data_source_*.go
:定义的一些用于读调用的资源,主要是查询接口resource_*.go
:定义的一些写调用的资源,包含资源增删改查接口service_*.go
:按资源大类划分的一些公共方法import_*.go
:导入现有资源
这些部分也不是必须的,例如 import_*.go
,data_source_*.go
等。比如我们这个demo
。
Provider
首先需要做的就是实现一个
terraform.ResourceProvider
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
66
67
68
69
70
71package es
import (
"context"
"net/url"
"strings"
elastic "github.com/elastic/go-elasticsearch/v7"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func Provider() *schema.Provider {
//TODO
return &schema.Provider{
Schema: map[string]*schema.Schema{
"urls": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("ELASTICSEARCH_URLS", nil),
Description: "Elasticsearch URLs",
},
"username": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ELASTICSEARCH_USERNAME", nil),
Description: "Username",
},
"password": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("ELASTICSEARCH_PASSWORD", nil),
Description: "Password",
},
},
ResourcesMap: map[string]*schema.Resource{
"elasticsearch_snapshot_repository": resourceElasticsearchSnapshotRepository(),
},
ConfigureContextFunc: providerConfigure,
}
}
func providerConfigure(c context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) {
URLs := strings.Split(data.Get("urls").(string), ",")
username := data.Get("username").(string)
password := data.Get("password").(string)
var diags diag.Diagnostics
for _, rawURL := range URLs {
_, err := url.Parse(rawURL)
if err != nil {
return nil, diag.FromErr(err)
}
}
cfg := elastic.Config{
Addresses: URLs,
}
if username != "" && password != "" {
cfg.Username = username
cfg.Password = password
}
client, err := elastic.NewClient(cfg)
if err != nil {
return nil, diag.FromErr(err)
}
return client, diags
}以上的代码表示函数
Provider()
会返回一个带有必要configuration
的terraform.ResourceProvider
:Schema: provider
所需的参数列表,map
类型;ResourcesMap:provider
所管理的resource
;ConfigureContextFunc
:提供了实例化、配置客户端API
调用的函数;
测试:
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
42package es
import (
"context"
"testing"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
var testAccProviders map[string]*schema.Provider
var testAccProvider *schema.Provider
func init() {
testAccProvider = Provider()
configureContextFunc := testAccProvider.ConfigureContextFunc
testAccProvider.ConfigureContextFunc = func(c context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
return configureContextFunc(c, d)
}
testAccProviders = map[string]*schema.Provider{
"elasticsearch": testAccProvider,
}
}
func TestProvider(t *testing.T) {
if err := Provider().InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvider_impl(t *testing.T) {
var _ *schema.Provider = Provider()
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("ELASTICSEARCH_URLS"); v == "" {
t.Fatal("ELASTICSEARCH_URLS must be set for acceptance tests")
}
}
资源的 CRUD
Terraform provider
实际是对上游 API
的抽象,因此我们利用 go
的 es
客户端操作 ES
实现对 snpashot repo
的操作;
Schema
, 定义了三个参数:name
:ForceNew
, 如果设置为true
,当资源属性值发生变化时,不会触发修改动作,而是删除该资源,再创建新的资源。type
;- ·settings· ,是一个
map
类型;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"type": {
Type: schema.TypeString,
Required: true,
},
"settings": {
Type: schema.TypeMap,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
}Resource
的CRUD
:- 分别定义了
Create
,Read
,Update
,Delete
四个方法,这些方法实际上就是对ES
的具体操作;
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
31func resourceElasticsearchSnapshotRepository() *schema.Resource {
return &schema.Resource{
Create: resourceElasticsearchSnapshotRepositoryCreate,
Read: resourceElasticsearchSnapshotRepositoryRead,
Update: resourceElasticsearchSnapshotRepositoryUpdate,
Delete: resourceElasticsearchSnapshotRepositoryDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Description: "Terraform plugin demo for es",
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"type": {
Type: schema.TypeString,
Required: true,
},
"settings": {
Type: schema.TypeMap,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}- 分别定义了
验收测试
如果要成为 Terraform
官方认证的 provider
,测试用例,也是必不可少的。
测试用例:
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132package es
import (
"context"
"fmt"
"testing"
elastic "github.com/elastic/go-elasticsearch/v7"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
func TestAccElasticsearchSnapshotRepository(t *testing.T) {
resource.Test(t, resource.TestCase{
// 环境的预检测,看需求,我们定义了需要先设置ES的地址
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testAccProviders,
// 配置 资源销毁结果检查函数
CheckDestroy: testCheckElasticsearchSnapshotRepositoryDestroy,
// 配置 测试步骤
Steps: []resource.TestStep{
{
// 配置 配置内容
Config: testElasticsearchSnapshotRepository,
// 配置 验证函数
Check: resource.ComposeTestCheckFunc(
testCheckElasticsearchSnapshotRepositoryExists("elasticsearch_snapshot_repository.test"),
),
},
{
Config: testElasticsearchSnapshotRepositoryUpdate,
Check: resource.ComposeTestCheckFunc(
testCheckElasticsearchSnapshotRepositoryExists("elasticsearch_snapshot_repository.test"),
),
},
{
ResourceName: "elasticsearch_snapshot_repository.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
// 自定义的测试函数,也就是查询 ES
func testCheckElasticsearchSnapshotRepositoryExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
// 这用到了 s.RootModule().Resources 数组
// 这个数组的属性反应的就是资源状态文件 terraform.tfstate
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No snapshot repository ID is set")
}
meta := testAccProvider.Meta()
client := meta.(*elastic.Client)
res, err := client.API.Snapshot.GetRepository(
client.API.Snapshot.GetRepository.WithContext(context.Background()),
client.API.Snapshot.GetRepository.WithPretty(),
client.API.Snapshot.GetRepository.WithRepository(rs.Primary.ID),
)
if err != nil {
return err
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("error when get snapshot repository %s: %s", rs.Primary.ID, res.String())
}
return nil
}
}
// testAccProviders 在测试前会根据 Config 建立测试资源,测试结束后又会全部销毁
// 这个函数就是检查资源是否销毁用的,就是根据ID查询资源是否存在
func testCheckElasticsearchSnapshotRepositoryDestroy(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "elasticsearch_snapshot_repository" {
continue
}
meta := testAccProvider.Meta()
client := meta.(*elastic.Client)
res, err := client.API.Snapshot.GetRepository(
client.API.Snapshot.GetRepository.WithContext(context.Background()),
client.API.Snapshot.GetRepository.WithPretty(),
client.API.Snapshot.GetRepository.WithRepository(rs.Primary.ID),
)
if err != nil {
return err
}
defer res.Body.Close()
if res.IsError() {
if res.StatusCode == 404 {
return nil
}
}
return fmt.Errorf("Snapshot repository %q still exists", rs.Primary.ID)
}
return nil
}
var testElasticsearchSnapshotRepository = `
resource "elasticsearch_snapshot_repository" "test" {
name = "terraform-test"
type = "fs"
settings = {
"location" = "/opt/es/snapshot_repo"
}
}
`
var testElasticsearchSnapshotRepositoryUpdate = `
resource "elasticsearch_snapshot_repository" "test" {
name = "terraform-test"
type = "fs"
settings = {
"location" = "/opt/es/snapshot_repo"
"test" = "test"
}
}
`启动一个
ES
:注意需要配置
repo
地址:1
2
3
4[root@Docker es7.8]# cat config/elasticsearch.yml
cluster.name: "docker-cluster"
network.host: 0.0.0.0
path.repo: ["/opt/es/snapshot_repo"]
验收测试,可以看到结果已经通过:
使用:
编译成
provider
使用:1
2[root@Docker terraform-es-plugin]# set version=v1.0.0
[root@Docker terraform-es-plugin]# go build -o terraform-provider-es_$version将编译好的插件放入指定目录:
测试文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22provider "es" {
urls = "http://192.168.30.11:9200"
}
terraform {
required_providers {
es = {
source = "terraform.local/local/es"
version = "1.0.0"
}
}
}
resource "elasticsearch_snapshot_repository" "test" {
provider = es
name = "terraform-test"
type = "fs"
settings = {
"location" = "/opt/es/snapshot_repo/test_backup"
"compress" : "true"
}
}terraform init
terraform plan
terraform apply
执行
验证,我们看到已经创建了一个
snapshot repo
后续我们更改
resource
资源,测试更新等;
terraform destroy
由此,我们开发了一个 最简单的provider
,之后可以通过resource
其中扩展任意 ES
的操作。同时也能帮助我们理解各云厂商的terraform provider
后面的资源创建流程。