腾讯云支持 Terraform 开发实践-深蓝源码网


时间: 2020-09-03 00:08:26 人气: 2286 评论: 0

作者:谢世亮

导语 Terraform是国际著名的开源的资源编排工具,据不完全统计,全球已有超过一百家云厂商及服务提供商支持Terraform。这篇文章从Terraform-Provider系统架构开始,到Terraform核心库讲解,到实践Terraform-Provider开发,再到单元测试,比较完整的描述了支持Terraform的开发全过程

本项目已经发布在Github(https://github.com/tencentyun/terraform-provider-tencentcloud)上,感兴趣的同学欢迎Star哟~

1. Terraform是什么?

Terraform是一款基于Golang的开源的资源编排工具,可以让用户管理配置任何基础架构,可以管理公有云和私有云服务的基础架构,也可以管理外部服务。

如果你不知道什么叫资源编排,那 AWS控制台腾讯云控制台 你一定知道,你可以在这些控制台管理你的所有云资源,Terraform和控制台作用一样,本质都是管理你的云资源,只不过,控制台是界面化的操作,而Terraform是通过配置文件来实现

当你的基础架构很复杂时,当你在某云厂商采买了规模较大的云资源或云服务时,当你的基础架构是基于混合云时,…,控制台的界面化操作,也许并不是最佳的管理工具,这时候,Terraform可能就是上古神器了

2. 怎么使用Terraform管理基础架构?

在开始开发之前,我们先了解下用户是怎么玩的,这尤其重要,这有助于更好的理解我们后续的开发流程和开发思路

简单来说,用户就是维护一些类似 json 格式的 .tf 配置文件,通过对配置的增删改查,实现对基础架构资源的增删改查。

下面这个文档讲述了terraform-provider-tencentcloud在腾讯云的应用

https://github.com/tencentyun/terraform-provider-tencentcloud/blob/master/website/docs/index.html.markdown

3. 配置开发环境

Terraform支持插件模型,并且所有 provider 实际就是插件,插件以Go二进制文件的形式分发。虽然技术上可以用另一种语言编写插件,但几乎所有的Terraform插件都是用Golang编写的。

本文是在下列版本开发和测试的

  • Terraform 0.11.x
  • Go 1.9 (to build the provider plugin)

为了不使本文篇幅太长,环境相关请直接参考我们 Github 上的 README.md,这里就不重复写了,假设你已经准备好了开发环境

4. Provider架构

按照Go的开发习惯和Github路径,我把开发目录放在了

cd  $GOPATH/src/github.com/tencentyun/terraform-provider-tencentcloud

接下来,我们了解下 tencentcloud 的插件目录,以此了解 Provider 架构

├─terraform-provider-tencentcloud                    根目录
│  ├─main.go                                         程序入口文件
│  ├─AUTHORS                                         作者信息
│  ├─CHANGELOG.md                                    变更日志
│  ├─LICENSE                                         授权信息
│  ├─debug.tf.example                                调试配置文件示例
│  ├─examples                                        示例配置文件目录
│  │  ├─tencentcloud-eip                             EIP示例tf文件
│  │  ├─tencentcloud-instance                        CVM示例tf文件
│  │  ├─tencentcloud-nat                             NAT网关示例tf文件
│  │  ├─tencentcloud-vpc                             VPC示例tf文件
│  │  └─ ...                                         更多examples目录
│  ├─tencentcloud                                    Provider核心目录
│  │  ├─basic_test.go                                基础单元测试
│  │  ├─config.go                                    公共配置文件
│  │  ├─data_source_tc_availability_zones.go         可用区查询
│  │  ├─data_source_tc_availability_zones_test.go
│  │  ├─data_source_tc_nats.go                       NAT网关列表查询
│  │  ├─data_source_tc_nats_test.go
│  │  ├─data_source_tc_vpc.go                        VPC查询
│  │  ├─data_source_tc_vpc_test.go
│  │  ├─...                                          更多Data Source
│  │  ├─helper.go                                    一些公共函数
│  │  ├─provider.go                                  Provider核心文件
│  │  ├─provider_test.go
│  │  ├─resource_tc_eip.go                           EIP资源管理程序
│  │  ├─resource_tc_eip_test.go
│  │  ├─resource_tc_instance.go                      CVM实例资源管理程序
│  │  ├─resource_tc_instance_test.go
│  │  ├─resource_tc_nat_gateway.go                   NAT网关资源管理程序
│  │  ├─resource_tc_nat_gateway_test.go
│  │  ├─resource_tc_vpc.go                           VPC网关资源管理程序
│  │  ├─resource_tc_vpc_test.go
│  │  ├─...                                          更多资源管理程序
│  │  ├─service_eip.go                               封装的EIP相关Service
│  │  ├─service_instance.go                          封装的CVM实例相关Service
│  │  ├─service_vpc.go                               封装的VPC相关Service
│  │  ├─...
│  │  ├─validators.go                                公共的参数校验函数
│  ├─vendor                                          依赖的第三方库
│  ├─website                                         Web相关文件
│  │  ├─tencentcloud.erb                             文档左侧菜单栏
│  │  ├─docs                                         文档markdown源文件目录
│  │  │  ├─d                                         data相关文档(data_source_*)
│  │  │  │  ├─availability_zones.html.md
│  │  │  │  ├─nats.html.markdown
│  │  │  │  ├─vpc.html.markdown
│  │  │  │  ├─...
│  │  │  ├─index.html.markdown
│  │  │  ├─r                                         resource相关文档(resource_*)
│  │  │  │  ├─instance.html.markdown
│  │  │  │  ├─nat_gateway.html.markdown
│  │  │  │  ├─vpc.html.markdown
│  │  │  │  └─...

结构主要分五部分

  • main.go,插件入口
  • examples,示例目录,因为你的插件最终是给用户用的,一个比较理想的示例,是用户拉到代码后,可以直接跑起来
  • tencentcloud,最重要的目录,也就是我们的插件目录,里面都是Go文件,其中
    • provider.go 这是插件的根源,用于描述插件的属性,如:配置的秘钥,支持的资源列表,回调配置等
    • data_source_*.go 定义的一些用于读调用的资源,主要是查询接口
    • resource_*.go 定义的一些写调用的资源,包含资源增删改查接口
    • service_*.go 按资源大类划分的一些公共方法
  • vendor,依赖的第三方库
  • website,文档,重要性同examples

5. 生命周期

下图是Terraform的整个执行过程:

  • ① ~ ④ 是在寻找 Providertencentcloud 插件就是这时候加载的
  • ⑤ 是读取用户的配置文件,通过配置文件,可以获得分别属于哪种资源,以及每个资源的状态
  • ⑥ 根据资源的状态,调用不同的函数,Create Update Delete 都属于写操作,而 Read操作,只在 Update 的时候,作为前置操作

何谓 Create ?

当在 .tf 文件增加一个新的资源配置时,这时候 Terraform 认为是 Create

何谓 Update ?

当在 .tf 文件针对已经创建好的资源,修改其中一个或多个参数时,这时候 Terraform 认为是 Update

何谓 Delete ?

当把 .tf 文件中已经创建好的资源配置删掉后,或执行 terraform destroy 命令时,这时候 Terraform 认为是 Delete

何谓 Read ?

顾名思义,这是一个查询资源的操作,如前述 Read 只在 Update 的时候,作为前置操作,实际作用就是检查资源是否存在,以及更新资源属性到本地

细心的你一定注意到了 tencentcloud-sdk-go 这个 packagetencentcloud-sdk-go 是我们封装的一个独立于 Terraform 之外的基于 Tencent Cloud API 的Go版SDK 其作用就是负责调用 Tencent Cloud API 当然,你也可以不用它,直接在你的 terraform-provider 里组装参数、发送请求,但我们不建议这么做,使用SDK方式,可以让你的代码更加优雅,可以实现对出入参、HTTP请求的集中管理,可以让你的常用接口更好的复用,减少代码冗余

6. 定义资源

Terraform官网有个从 main.go 入口开始编写自定义Provider的指引 Writing Custom Providers,建议先浏览一遍。

成为Terraform提供商(开发Terraform插件),实际是对上游 API 的抽象,而所谓的资源就是我们的服务,比如云主机、私有网络、NAT网关。按惯例,我们要把每个资源放在自己的插件目录下,并以资源命名,前缀为 resource_data_source_,比如

tencentcloud/resource_tc_nat_gateway.go

package tencentcloud

import (
    "github.com/hashicorp/terraform/helper/schema"
)

func resourceTencentCloudNatGateway() *schema.Resource {
    return &schema.Resource{
        Create: resourceTencentCloudNatGatewayCreate,
        Read:   resourceTencentCloudNatGatewayRead,
        Update: resourceTencentCloudNatGatewayUpdate,
        Delete: resourceTencentCloudNatGatewayDelete,

        Schema: map[string]*schema.Schema{
            "vpc_id": &schema.Schema{
                Type:     schema.TypeString,
                Required: true,
                ForceNew: true,
            },
            "name": &schema.Schema{
                Type:         schema.TypeString,
                Required:     true,
                ValidateFunc: validateStringLengthInRange(1, 60),
            },
            "max_concurrent": &schema.Schema{
                Type:     schema.TypeInt,
                Required: true,
            },
            "bandwidth": &schema.Schema{
                Type:     schema.TypeInt,
                Required: true,
            },
            "assigned_eip_set": &schema.Schema{
                Type:     schema.TypeSet,
                Required: true,
                Elem: &schema.Schema{
                    Type: schema.TypeString,
                },
                MinItems: 1,
                MaxItems: 10,
            },
        },
    }
}

func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error {
    return nil
}

这里实际就是返回了一个 schema.Resource 类型的结构体,结构体中我们定义了资源参数和CRUD操作

  • Create
  • Read
  • Update
  • Delete
  • Schema

其中 Schema 就是定义的资源参数,是 map[string]*schema.Schema 类型的嵌套数组,这是一个非常重要的数组,在Terraform里,你也理解为这些就是一个资源的属性

在我们本次的示例中,就是一个NAT网关的所有属性(这些属性,我们可以在NAT网关的云API中看到)

每个属性,它的值都是一个结构体,包含了若干属性,这些属性,都是围绕资源属性值的,下面逐一介绍

Type schema.ValueType

定义这个属性的值的数据类型,可选值及对应的数据类型

  • TypeBool - bool
  • TypeInt - int
  • TypeFloat - float64
  • TypeString - string
  • TypeList - []interface{}
  • TypeMap - mapstringinterface{}
  • TypeSet - *schema.Set

Required bool

也就我们经常在 API 里说的 参数是否必填,默认 false,当设置为 true 后,用户对资源增删改操作时,都需要配置该参数

Optional bool

是否可选的,和 Required 互斥的,不能同时配置 RequiredOptional,即一个属性(参数)要么必填,要么可选

ForceNew bool

如果设置为 true,当资源属性值发生变化时,不会触发修改动作,而是删除该资源,再创建新的资源,即:

修改 = 删除 + 创建

这是一个非常有用的属性,我们很多云资源的很多属性都不支持修改,比如

  • 一个CVM实例创建时指定的子网,创建后,是不支持修改的
  • 一个NAT网关创建时指定的VPC,创建后,是无法修改的

在控制台可以通过前端技术实现这样的限制,Terraform 同样可以做到这样的限制,但 ForceNew 实现了更高级的用法,给用户提供了更多选择,

一个有趣的事情,如果某种云资源的所有属性,都是Required,并且属性联合起来,具有唯一性,比如路由表的路由策略、DNAT规则、KeyPair、…,都是这类特性,这时候你修改一个属性,实际就等价于删除旧资源,创建新资源 这时候,你就可以把所有属性的ForceNew 设为 true,然后不用实现 Update 函数了,因为无论用户修改哪个属性,都是走 Delete - Create 的流程,根本不会走到 Update 的流程里,但实现的效果,都是一样的,用户是无感知的

ValidateFunc SchemaValidateFunc

属性值的扩展验证函数,验证IP合法性示例:

func validateIp(v interface{}, k string) (ws []string, errors []error) {
    value := v.(string)
    ip := net.ParseIP(value)
    if ip == nil {
        errors = append(errors, fmt.Errorf("%q must contain a valid IP", k)) 
    }   
    return
}

MinItems、MaxItems int

TypeTypeSetTypeList 类型时,可以给 MinItemsMaxItems 赋值,限定属性值元素的最小个数和最大个数,上述代码中,我们限定了NAT网关的关联EIP个数范围是1~10个

CRUD操作

这4个操作 Create Read Update Delete,指向的是4个函数,也是我们重点要实现的。

在”生命周期”一节中,我们知道了Terraform是根据资源的模式和状态,来决定是否需要创建新资源,更新现有资源或销毁资源的,而最终就是调用这4个函数来实现的

7. CRUD实现

了解了用户行为、Terraform执行流程、资源管理逻辑,现在就是实现这些功能的时候了

因为这块内容较多,这里继续用NAT网关作为示例,详述一个资源CURD的实现

开始之前,我们需要引入更多的包,都是我们后面要用到的

import (
    "encoding/json"
    "errors"
    "fmt"
    "log"

    "github.com/hashicorp/terraform/helper/schema"
    "github.com/zqfan/tencentcloud-sdk-go/common"
    vpc "github.com/zqfan/tencentcloud-sdk-go/services/vpc/unversioned"
)

//...

func resourceTencentCloudNatGatewayCreate(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayRead(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error {
    return nil
}

func resourceTencentCloudNatGatewayDelete(d *schema.ResourceData, meta interface{}) error {
    return nil
}

上述代码中,我们看到,我们要实现的资源管理函数,出参都是 error 类型,说明Terraform都是根据 error 来判断成功与否的,返回 nil 时表示操作成功,否则就报错

入参都是 *schema.ResourceData 类型的参数 d,和 interface{} 类型的参数 meta,具体这两个参数有什么用呢?

这是我们这节的关键!

参数 d 是我们开发过程中用的最多的参数,它的数据类型是个对象,包含了非常的方法,下面我们介绍几个常用的方法

func (*ResourceData) Get

func (d *ResourceData) Get(key string) interface{}

用来获取给定 Key 的数据,如果给定的 Key 不存在,会返回 nil

通过 Set 方法设置的数据,以及用户配置的参数,都可以通过这个方法获得

一般,我们在 Create 资源的时候,用的比较多

func (*ResourceData) GetOk

func (d *ResourceData) GetOk(key string) (interface{}, bool)

检查给定的 Key 是否设置为一个非0的值,一般我们在获取 Optional 类型的属性值的时候,会用到

func (*ResourceData) SetId

func (d *ResourceData) SetId(v string)

Terraform对资源的管理都是围绕ID实现的,每个资源都有一个唯一ID,一个ID代表一个资源,因此,当创建资源后,需要调用这个方法写入资源ID,一般服务端都会返回资源唯一ID,比如我们的示例中,这个ID就是NAT网关的ID,eg: nat-79r5e43i

这时候,你是不是有一个疑惑?我们的资源没有唯一 ID 怎么办?

对于没有唯一ID的资源,比如路由策略、安全组规则的增删改查,我们就需要自己构造ID了。

可以用某个参数作为ID;也可以多个参数联合起来;也可以自己实现一个算法生成ID。

前提条件就是一定要唯一 ,然后我们在用到ID的时候,再反解出来,这就间接实现了我们所需要的唯一 ID

func (*ResourceData) Id

func (d *ResourceData) Id() string

获取当前的资源ID,也就是 SetId 方法写入的值,比如我们在 Read Update Delete 的时候,都需要用到ID,映射到对应的资源,从而完成对某个资源的读取,修改,删除

func (*ResourceData) Set

func (d *ResourceData) Set<										

技术沙龙 教程文章 热点综合

评论