老 宋

人生最难是相逢,各位珍重!

0%

terraform provider开发

Terraform provider 开发

TerraformIAC (基础设施即代码) 的最佳开源工具之一,几乎所有的云平台都会提供 Terraform Provider 来供使用者能自动化的创建各种云资源。 同时 Terraform 是一个高度可扩展的工具,通过 Provider 来支持新的基础架构,我们可以通过自己开发 Provider 对大多数资源进行管理。下面就展示一个 和Elasticsearc 相关的 Terraform Providerdemo

项目结构

根据 Terraform 官方文档,一个基本的 Terraform provider 大致会有以下几个部分组成:

  • provider.go :这是插件的根源,用于描述插件的属性,如:配置的秘钥,支持的资源列表,回调配置等
  • data_source_*.go :定义的一些用于读调用的资源,主要是查询接口
  • resource_*.go :定义的一些写调用的资源,包含资源增删改查接口
  • service_*.go :按资源大类划分的一些公共方法
  • import_*.go :导入现有资源

这些部分也不是必须的,例如 import_*.godata_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
    71
    package 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()会返回一个带有必要configurationterraform.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
    42
    package 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 的抽象,因此我们利用 goes 客户端操作 ES 实现对 snpashot repo 的操作;

  • Schema, 定义了三个参数:

    • nameForceNew, 如果设置为 true,当资源属性值发生变化时,不会触发修改动作,而是删除该资源,再创建新的资源。
    • type;
    • ·settings· ,是一个 map 类型;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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,
    },
    },
    }
  • ResourceCRUD

    • 分别定义了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
    31
    func 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
    132
    package 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
    22
    provider "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 后面的资源创建流程。

欢迎关注我的其它发布渠道