首次使用的Terraform提供程序

本篇文章是由エイチーム引越し侍/エイチームコネクト的员工所撰写的,是Ateam Hikkoshi samurai 公司和 Ateam Connect 公司的2021年圣诞节日历的第23篇文章。
今天由引越侍公司的基础设施工程师@dd511805担任负责!

首先

引越侍正在使用Terraform来构建基础设施。由于许多服务都在AWS上,我们也考虑过使用CloudFormation或AWS CDK进行部署。最近,我们有时也会使用其他云服务,如GCP,或CDN服务,如Fastly。因为Terraform可以支持许多服务提供商,它确实非常方便。
然而,并不是所有的服务都有相应的Terraform提供商。如果过于依赖Terraform的代码管理,手动操作可能会变得繁琐。
我们希望通过学习自定义Terraform提供商的方法,为今后构建内部系统等提供商打下基础。

创建Terraform Provider的方法

创建Terraform Provider的方法在Plugin Development中有介绍,大致可以分为两种方法。
– Terraform Plugin SDKv2
– Terraform Plugin Framework
Terraform Plugin SDKv2是目前常用的方法,而Terraform Plugin Framework仍在开发中,所以只能在Terraform v1.0及更高版本中使用。
考虑到向后兼容性,主要的Provider可能还需要很长时间才能转移到Terraform Plugin Framework上。
但是,如果我们是在新创建Provider,就不需要使用旧版的Terraform,可以尝试使用Terraform Plugin Framework来创建Provider。

Terraform 插件框架的关键概念是什么?

Terraform Plugin Framework拥有以下四个关键概念:
– 提供者 (Providers)
– 模式 (Schemas)
– 资源 (Resources)
– 数据源 (Data Sources)

与Terraform Plugin SDKv2的关键概念不同,它的结构变得更加详细复杂。

创建自定义的提供者

在Terraform插件框架中实现创建和读取功能时,有一个Provider的制作教程。因此,可以先创建一个空的Provider来学习配置,并参考该教程。

ubuntu:~/terraform-provider-mybotip$ tree
.
├── go.mod
├── go.sum
├── main.go
├── mybotip
│   └── provider.go
└── mybotpkg
    └── client.go

我們將創建如下所示的目錄結構,並在main.go中進行以下描述:
透過Name指定提供者的名稱,並讀取mybotip目錄中的provider.go文件,
然後調用mybotip的New方法。

package main

import (
    "context"
    "github.com/hashicorp/terraform-plugin-framework/tfsdk"
    "terraform-provider-mybotip/mybotip"
)

func main() {
    tfsdk.Serve(context.Background(), mybotip.New, tfsdk.ServeOpts{
        Name: "mybotip",
    })
}

由于mybotip.New需要返回符合tfsdk.Provider接口的实现,所以需要实现GetSchema、Configure、GetResources和GetDataSources方法。GetSchema需要返回Attributes,所以定义了适当的Attributes,但GetResources和GetDataSources还没有实现。provider的struct定义了一个名为client的属性,这是实现被Resource和DataSource调用的处理的地方。

package mybotip

import (
    "context"
    "os"

    "github.com/hashicorp/terraform-plugin-framework/diag"
    "github.com/hashicorp/terraform-plugin-framework/tfsdk"
    "github.com/hashicorp/terraform-plugin-framework/types"
    "terraform-provider-mybotip/mybotpkg"
)

var stderr = os.Stderr

func New() tfsdk.Provider {
    return &provider{}
}

type provider struct {
    configured bool
    client     *pkg.Client
}

// GetSchema
func (p *provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
    return tfsdk.Schema{
        Attributes: map[string]tfsdk.Attribute{
            "name": {
                Type:     types.StringType,
                Optional: true,
                Computed: true,
            },
        },
    }, nil
}

// Provider schema struct
type providerData struct {
    Name types.String `tfsdk:"name"`
}

func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) {
    // Retrieve provider data from configuration
    var config providerData
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }

    p.configured = true
}

func (p *provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) {
    return map[string]tfsdk.ResourceType{
    }, nil
}

func (p *provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) {
    return map[string]tfsdk.DataSourceType{
    }, nil
}

“从提供者调用的客户端应具有以下结构。”

package mybotpkg

// Client -
type Client struct {
}

因为之前已经完成了一个什么也不做的提供者,所以要进行构建。

go build -o terraform-provider-mybotip_v0.0.1
mkdir -p ~/.terraform.d/plugins/local/edu/mybotip/0.0.1/linux_amd64
mv terraform-provider-mybotip_v0.0.1 ~/.terraform.d/plugins/local/edu/mybotip/0.0.1/linux_amd64

执行自制的Provider

我們將在前一步驟中建立的Provider進行實際測試,以確認其是否可用。
我們將使用以下方式來使用provider。

terraform {
  required_providers {
    mybotip = {
      source  = "local/edu/mybotip"
      version = "0.0.1"
    }
  }
  required_version = "~> 1.1.0"
}

provider "mybotip" {
}

如果直接执行,则会出现以下错误,所以需要通过-plugin-dir选项以绝对路径指定插件的位置来执行init和apply操作。

ubuntu:~/terraform-provider-mybotip/example$ terraform init -plugin-dir=/home/ubuntu/.terraform.d/plugins

Initializing the backend...

Initializing provider plugins...
- Finding local/edu/mybotip versions matching "0.0.1"...
- Installing local/edu/mybotip v0.0.1...
- Installed local/edu/mybotip v0.0.1 (unauthenticated)

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.

ubuntu:~/terraform-provider-mybotip/example$ terraform apply
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

因为我顺利地完成了申请,所以我现在可以使用自己制作的提供者了。

创建DataResource

由于我们能够创建Provider,接下来我们将尝试创建DataResource。
我们将创建一个返回Google公开的Google Bot IP的DataResource。
由于我们将在client.go中实现获取IP的部分,因此我们将按以下方式更改client.go。

package mybotpkg

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

// Client -
type Client struct {
    HostURL    string
    HTTPClient *http.Client
}

const HostURL string = "https://developers.google.com/search/apis/ipranges/googlebot.json"

// Client -

// NewClient -
func NewClient() (*Client, error) {
    c := Client{
        HTTPClient: &http.Client{Timeout: 10 * time.Second},
        HostURL: HostURL,
    }

    return &c, nil
}

type Prefixes []struct {
    Ipv6Prefix string `json:"ipv6Prefix,omitempty"`
    Ipv4Prefix string `json:"ipv4Prefix,omitempty"`
}

type AutoGenerated struct {
    CreationTime string `json:"creationTime"`
    Prefixes
}

func (c *Client) GetIPs() ([]string, error) {
    req, err := http.NewRequest("GET", c.HostURL, nil)
    res, err := c.HTTPClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }

    jsonBytes := ([]byte)(body)
    data := new(AutoGenerated)

    if err := json.Unmarshal(jsonBytes, data); err != nil {
        return nil,  fmt.Errorf("JSON Unmarshal error: %s", err)
    }

    if res.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("status: %d, body: %s", res.StatusCode, body)
    }

    var items []string
    for _, item := range data.Prefixes {
        if item.Ipv4Prefix != "" {
            items = append(items, item.Ipv4Prefix)
        }
    }

    return items, err
}

因为client.go 中的 GetIPs 现在可以返回一个 IP 范围的字符串切片,所以我们将创建一个名为 datasource_mybotip.go 的文件来利用它。
我们将按照 tfsdk.DataSourceType 接口的要求来实现 GetSchema、 NewDataSource 和 Read 方法。

package mybotip

import (
    "context"
    "fmt"

    "github.com/hashicorp/terraform-plugin-framework/diag"
    "github.com/hashicorp/terraform-plugin-framework/tfsdk"
    "github.com/hashicorp/terraform-plugin-framework/types"
)

type dataSourceMybotIPsType struct{}

func (r dataSourceMybotIPsType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) {
    return tfsdk.Schema{
        Attributes: map[string]tfsdk.Attribute{
            "ips": {
                Computed: true,
                Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{
                    "iprange": {
                        Type:     types.StringType,
                        Computed: true,
                    },
                }, tfsdk.ListNestedAttributesOptions{}),
            },
        },
    }, nil
}

func (r dataSourceMybotIPsType) NewDataSource(ctx context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) {
    return dataSourceMybotIPs{
        p: *(p.(*provider)),
    }, nil
}

type dataSourceMybotIPs struct {
    p provider
}

func (r dataSourceMybotIPs) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) {
    var resourceState struct {
        IPs []MybotIP `tfsdk:"ips"`
    }

    ips, err := r.p.client.GetIPs()
    if err != nil {
        resp.Diagnostics.AddError(
            "Error retrieving mybotip",
            err.Error(),
        )
        return
    }
    fmt.Fprintf(stderr, "[DEBUG]-ips:%+v", ips)

    for _, ip := range ips {
        c := MybotIP{
            Iprange:        types.String{Value: ip},
        }

        resourceState.IPs = append(resourceState.IPs, c)
    }

    fmt.Fprintf(stderr, "[DEBUG]-Resource State:%+v", resourceState)

    diags := resp.State.Set(ctx, &resourceState)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }   
}

在models.go文件中定义与DataSource模式相匹配的struct。

package mybotip

import (
    "github.com/hashicorp/terraform-plugin-framework/types"
)

type MybotIP struct {
    Iprange       types.String `tfsdk:"iprange"`
}

将provider.go中的GetDataSources的引用路径修改为新创建的datasource_mybotip.go。

...
func (p *provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) {
    return map[string]tfsdk.DataSourceType{
        "mybotip_ip": dataSourceMybotIPsType{},
    }, nil
}

只要建起这座大楼,就算完成了。

数据资源的操作确认

为了使用在前一个步骤中实施的DataResource mybotip_ip,需要在main.tf中添加以下描述。

...
data "mybotip_ip" "this" {
}

当执行 terraform apply 并且 apply 正常完成后,我们将使用 terraform console 命令来确认 DatResource。

ubuntu:~/terraform-provider-mybotip/example$ terraform console
> data.mybotip_ip.this
{
  "ips" = tolist([
    {
      "iprange" = "66.249.64.0/27"
    },
    {
      "iprange" = "66.249.64.128/27"
    },
      "iprange" = "66.249.75.224/27"
    },
...
    {
      "iprange" = "66.249.79.96/27"
    },
  ])
}
>  

现在可以在DatResource上顺利获取Google Bot的IP了。

总结

我看了一下使用Terraform Plugin Framework创建Provider和DataResource的方法。通过使用Terraform Plugin Framework,即使不是特别熟悉Terraform Plugin的结构,只需要实现所需的接口,就可以创建Provider。尽管Terraform Plugin Framework仍在开发中,可能会有很大的变化,但我认为在考虑创建Terraform Provider时,应该考虑将其作为其中一种选择。

明天

Ateam Hikkoshi Samurai Inc.和Ateam Connect Inc. 2021年的倒数第23个文章,你觉得如何呢?请继续期待明天 @kaitat 的文章。

广告
将在 10 秒后关闭
bannerAds