tencent cloud

文档反馈

编写 Resource 和 DataSource

最后更新时间:2023-05-29 17:49:06
    Terraform 中 Resource 和 DataSource 是最基本的可拓展数据集合。您可以通过这两种集合管理资源。

    编写 Resource

    Resource 用来描述一类资源实体,这类实体可以被添加、查询、修改和移除,例如云服务器 CVM、硬盘、数据库、TKE 集群、COS 桶等可以被增删改查的实体都是典型的 Resource。
    本文以 CVM 为例。一台 CVM 实例的简单配置如下:
    名称: my-cvm
    可用区: 广州四区
    机型: SA2.MEDUIM2
    镜像: TencentOS Server 3.2 (Final)
    所属网络: vpc-xxxxxxxx
    所属子网: subnet-xxxxyyyy
    计费类型: 按量计费
    是否分配公网IP: 是
    公网带宽: 10M
    您可以使用高级配置语法 HCL 进行描述,代码示例如下:
    resource "tencentcloud_instance" "cvm1" {
    instance_name = "my-cvm"
    availability_zone = "ap-guangzhou-4"
    instance_type = "SA2.MEDIUM2"
    instance_charge_type = "POSTPAID_BY_HOUR"
    image_id = "img-9qrfy1xt"
    allocate_public_ip = true
    internet_max_bandwidth_out = 10
    }
    当您第一次执行命令 terraform apply 时,已声明的资源会发起创建流程。在创建完毕后,若您改动这段代码中的配置,并再次执行命令 terraform apply,将会发起更新流程。执行命令 terraform destroy 则会进入销毁流程。

    编写 Resource 代码

    若要使上面这段 HCL 生效,您需要注册腾讯云 Provider,在 tencentcloud/provider.go 中找到 Provider/ResourceMap 字段,添加名为 tencentcloud_instance 的资源结构体,按照 HCL 定义参数样板 (Schema),代码如下:
    package tencentcloud
    
    import (
    "github.com/hashicorp/terraform-plugin-sdk/helper/schema"
    "github.com/hashicorp/terraform-plugin-sdk/terraform"
    )
    
    func Provider() *schema.Provider {
    return &schema.Provider{
    ResourcesMap: map[string]*schema.Resource{
    "tencentcloud_xxx": { /* 其他声明好的资源 */ },
    "tencentcloud_yyy": { /* 其他声明好的资源 */ },
    "tencentcloud_instance": {
    Schema: map[string]*schema.Schema{
    "instance_name": {
    Optional: true, // 可选字段
    Type: schema.TypeString, // 字段类型
    Description: "Instance Name.",
    },
    "availability_zone": {
    Required: true, // 必填字段
    Type: schema.TypeString,
    ForceNew: true, // 当这个字段更改,提交变更后资源销毁重建,因为服务器实例无法切换可用区。
    Description: "Instance available zone.",
    },
    "instance_type": {
    Optional: true,
    Type: schema.TypeString,
    Description: "Instance Type.",
    },
    "instance_charge_type": {
    Optional: true,
    Type: schema.TypeString,
    Description: "Instance charge type.",
    },
    "image_id": {
    Required: true,
    Type: schema.TypeString,
    Description: "Instance OS image Id.",
    },
    "allocate_public_ip": {
    Optional: true,
    Type: schema.TypeBool, // 布尔类型
    Description: "Specify whether to allocate public IP.",
    },
    "internet_max_bandwidth_out": {
    Optional: true,
    Type: schema.TypeInt, // 整数类型
    Description: "Specify maximum bandwith.",
    },
    },
    },
    },
    }
    }
    声明资源和参数样板后,Terraform 执行 apply / plan / destroy 时触发资源的增、删、改和同步逻辑也需要您自行定义。Terraform SDK (v1) 中定义了资源的 CRUD 四种方法:
    Create 执行 terraform apply 且为新建时调用。
    Read 执行 terraform plan / import 时调用,用来同步远端状态。
    Update 执行 terraform apply 且为更新时调用。
    Delete 执行 terraform destroy 时调用。
    接下来向schema.Resource 结构体中添加这四个方法,代码如下:
    package tencentcloud
    
    import (
    "github.com/hashicorp/terraform-plugin-sdk/helper/schema"
    "github.com/hashicorp/terraform-plugin-sdk/terraform"
    )
    
    func Provider() *schema.Provider {
    return &schema.Provider{
    ResourcesMap: map[string]*schema.Resource{
    "tencentcloud_xxx": { /* 其他声明好的资源 */ },
    "tencentcloud_yyy": { /* 其他声明好的资源 */ },
    "tencentcloud_instance": {
    Create: resourceTencentCloudInstanceCreate,
    Read: resourceTencentCloudInstanceRead,
    Update: resourceTencentCloudInstanceUpdate,
    Delete: resourceTencentCloudInstanceDelete,
    Schema: map[string]*schema.Schema{
    // 上文写好的声明,略
    },
    },
    },
    }
    }
    
    func resourceTencentCloudInstanceCreate (d *schema.ResouceData, m interface{}) error {
    instanceName := d.Get("instance_name").(string)
    instanceType := d.Get("instance_type").(string)
    // ...
    // terraform apply 新建资源的时候执行
    instanceId := createInstance(&param{
    Name: &instanceName,
    Type: &instanceType,
    // ...
    })
    // 创建完成后,给资源设置唯一 Id
    d.SetId(instanceId)
    
    // 资源创建完毕后,需要再次同步远端状态,验证是否一致
    return resourceTencentCloudInstanceRead(d, m)
    }
    
    func resourceTencentCloudInstanceRead (d *schema.ResouceData, m interface{}) error {
    // terraform plan 时和 terraform 创建/更新资源完毕时执行
    instanceId := d.Id() // Create 中设置的 Id
    instanceInfo := getInstance(instanceId)
    d.Set("instance_name", instanceInfo.Name)
    d.Set("instance_type", instanceInfo.Type)
    return nil
    }
    
    func resourceTencentCloudInstanceUpdate (d *schema.ResouceData, m interface{}) error {
    // terraform apply 更新已有资源时执行
    updateParam := &param{}
    // 逐项检查字段是否更新,组合更新参数
    if d.HasChange("instance_name") {
    name := d.Get("instance_name").(string)
    updateParam.Name = &name
    }
    if d.HasChange("instance_type") {
    insType := d.Get("instance_type").(string)
    updateParam.Type = &insType
    }
    // ...
    updateInstance(d.Id(), updateParam)
    // 资源创建完毕后,需要再次同步远端状态,验证是否一致
    return resourceTencentCloudInstanceRead(d, m)
    }
    
    func resourceTencentCloudInstanceDelete (d *schema.ResouceData, m interface{}) error {
    // terraform destroy 时执行
    destroyInstance(d.Id())
    return nil
    }
    可以看到在函数中,引用 d.Get 参数可以获取 HCL 字段的值,d.Set 可以回写(同步)这些值,此外每个资源创建完毕后还要调用 d.SetId 设置资源的唯一索引,以保证后续操作和远程真实资源的映射关系一致。
    完成四个函数的主逻辑骨架后,terraform 执行 plan apply destroy 即可正常调用,接下来您可以发起云 API 调用,真实地操作资源。 云服务器的 CVM 的 CRUD 接口分别为:
    RunInstances :购买新实例
    DescribeInstances :查询实例列表
    ModifyInstancesAttribute :修改实例属性
    TerminateInstances :退还实例
    您可以将从 HCL 获取的参数传给 CVM API 所需参数发起调用,代码如下:
    package main
    
    import (
    cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
    )
    
    func resourceTencentCloudInstanceCreate(d *schema.ResourceData, meta interface{}) error {
    // 从 HCL 中读取字段的值
    instanceName := d.Get("instance_name").(string)
    instanceType := d.Get("instance_type").(string)
    imageId := d.Get("image_id").(string)
    instanceChargeType := d.Get("instance_charge_type").(string)
    zone := d.Get("availability_zone").(string)
    allocatePublicIp := d.Get("allocate_public_ip").(bool)
    internetBandWith := int64(d.Get("internet_max_bandwidth_out").(int))
    client, _ := cvm.NewClient(credential, "ap-guangzhou")
    request := cvm.NewRunInstancesRequest()
    // 组合创建 CVM 所需的参数
    request.InstanceName = &instanceName
    request.InstanceType = &instanceType
    request.ImageId = &imageId
    request.InstanceChargeType = &instanceChargeType
    request.Placement = &cvm.Placement {
    Zone: &zone,
    }
    request.InternetAccessible = &cvm.InternetAccessible {
    InternetMaxBandwidthOut: &internetBandWith,
    PublicIpAssigned: &allocatePublicIp,
    }
    // 发起调用
    id, err := client.RunInstances(request)
    if err != nil {
    return err
    }
    // 将创建返回的 ID 设置为资源 ID
    d.SetId(id)
    // 回写状态
    return resourceTencentCloudInstanceRead(d, meta)
    }
    实现 Create 后,再接着实现 Read / Update / Delete :
    package main
    
    import (
    cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312"
    )
    
    func resourceTencentCloudInstanceRead(d *schema.ResourceData, meta interface{}) error {
    // Create 中设置或导入时拿到的 ID
    id := d.Id()
    client, _ := cvm.NewClient(credential, "ap-guangzhou")
    request, _ := cvm.NewDescribeInstancesRequest()
    request.InstanceIds = []*string{&id}
    response, err := client.DescribeInstances(request)
    if err != nil {
    return err
    }
    if len(response.Response.InstanceSet) == 0 {
    return fmt.Errorf("instance %s not exists.", id)
    }
    instance := response.Response.InstanceSet[0]
    d.Set("instance_name", instance.InstanceName)
    d.Set("instance_type", instance.InstanceType)
    // d.Set 回写其他如 `image_id` `instance_charge_type` `availability_zone` 等字段,略
    return nil
    }
    
    func resourceTencentCloudInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
    id := d.Id()
    client, _ := cvm.NewClient(credential, "ap-guangzhou")
    request, _ := cvm.NewModifyInstancesAttributeRequest()
    request.InstanceIds = []*string{&id}
    
    // 检查字段是否变更且添加参数
    if d.HasChange("instance_name") {
    name := d.Get("instance_name").(string)
    request.InstanceName = &name
    }
    // 处理其他 HasChange 字段,略
    _, err := client.ModifyInstanceAttribute(request)
    if err != nil {
    return err
    }
    return resourceTencentCloudInstanceRead(d, meta)
    }
    
    func resourceTencentCloudInstanceDelete(d *schema.ResourceData, meta interface{}) error {
    id := d.Id()
    
    client, _ := cvm.NewClient(credential, "ap-guangzhou")
    request, _ := cvm.NewTerminateInstancesRequest()
    request.InstanceIds = []*string{&id}
    _, err := client.TerminateInstances(request)
    if err != nil {
    return err
    }
    return nil
    }
    至此,在腾讯云 Provider 下一个包含名称、字段声明和赠删改查逻辑的资源全部实现。

    实现 Import 逻辑

    成功实现了资源的增删改查后,可以实现资源的导入逻辑。导入单个资源的命令格式为terraform import <type>.<index> <id>,例如:
    terraform import tencentcloud_instance.cvm1 ins-abcd1234
    可以看到,资源导入只有一个 ins-abcd1234 输入,回想我们已实现的 Read 方法,可以得知:当 Id 确定时,可以根据 Id 查询远端资源的详细配置,自动将配置同步到本地。所以我们只需要在 schema.Resource 中声明 Importer 表示该资源可以被导入,后续同步操作由 Read 接管:
    package tencentcloud
    
    import (
    "github.com/hashicorp/terraform-plugin-sdk/helper/schema"
    "github.com/hashicorp/terraform-plugin-sdk/terraform"
    )
    
    func Provider() *schema.Provider {
    return &schema.Provider{
    ResourcesMap: map[string]*schema.Resource{
    "tencentcloud_xxx": { /* 其他声明好的资源 */ },
    "tencentcloud_yyy": { /* 其他声明好的资源 */ },
    "tencentcloud_instance": {
    Create: resourceTencentCloudInstanceCreate,
    Read: resourceTencentCloudInstanceRead,
    Update: resourceTencentCloudInstanceUpdate,
    Delete: resourceTencentCloudInstanceDelete,
    Schema: map[string]*schema.Schema{
    // 上文写好的声明,略
    },
    // 绝大部分情况,只需要添加 schema.ImportStatePassthrough 即可
    Importer: &schema.ResourceImporter{
    State: schema.ImportStatePassthrough,
    },
    },
    },
    }
    }

    DataSource

    DataSource 代表只读实体,仅用做查询,无论调用多少次都不影响现有资源,如云服务器列表、镜像列表、可用区列表、DB 列表和参数模板、审计日志等。理论上只要能够通过 API 查询出来的数据,都可看作 DataSource ,不知您是否注意到:上文的 CVM 镜像对外显示为 TencentOS Server 3.2 (Final),但是实际传递给 API 的参数却是它的 ID img-9qrfy1xt,如何通过镜像名称获取对应的 ID 呢?这时候就可以借助 DataSource 来查询并引用了:
    data "tencentcloud_images" "fav_os" {
    filters = {
    image_name: "TencentOS Server 3.2",
    image_type: "PUBLIC_IMAGE"
    }
    }
    
    resource "tencentcloud_instance" "cvm1" {
    image_id = data.tencentcloud_images.fav_os.images.0.image_id
    }
    如上所示:将 image_id 写成对 data.tencentcloud_images.fav_os 的引用,Terraform 就会先读取 DataSource 的信息,再将结果计算求值。相比于静态的写法,编写 DataSource 能有效地将带有依赖的资源组织起来。

    编写 DataSource

    实现 DataSource 的方法和 Resource 差不多,同样需要在腾讯云 Provider 中注册,回到 tencentcloud/provider.go 源码,这次我们找到 Provider/DataSourceMap 字段,添加名为 tencentcloud_images 的结构体(所有 DataSource 名字都应该以 s 结尾),按照 HCL 定义参数样板 (Schema):
    package tencentcloud
    
    import (
    "github.com/hashicorp/terraform-plugin-sdk/helper/schema"
    "github.com/hashicorp/terraform-plugin-sdk/terraform"
    )
    
    func Provider() *schema.Provider {
    return &schema.Provider{
    DataSourceMap: map[string]*schema.Resource{
    "tencentcloud_xxxs": { /* 其他声明好的数据源 */ },
    "tencentcloud_yyys": { /* 其他声明好的数据源 */ },
    "tencentcloud_images": {
    Schema: map[string]*schema.Schema{
    "filters": {
    Optional: true,
    Type: schema.TypeMap, // 定义为 Map 类型
    Description: "Query filter",
    },
    // 用来将结果以 JSON 的形式保存在本地。
    // TencentCloud Provider 约定每个 DataSource 都应带有 `result_output_file`
    "result_output_file": {
    Optional: true,
    Type: schema.TypeString,
    Description: "Used for store as local file.",
    },
    "result": {
    Computed: true, // Computed 可以表示该字段会进行被动更新
    Type: schema.TypeList,
    Description: "",
    Elem: &schema.Resource{
    Schema: map[string]*schema.Schema{
    "id": {
    Type: schema.TypeString,
    Computed: true,
    Description: "Image Id.",
    },
    "name": {
    Type: schema.,
    Computed: true,
    Description: "Image name.",
    },
    },
    },
    },
    },
    },
    },
    }
    }
    
    与 Resource 相比, DataSource 只需要实现 Read 方法即可:
    package tencentcloud
    
    import (
    "encoding/json"
    "github.com/hashicorp/terraform-plugin-sdk/helper/schema"
    "github.com/hashicorp/terraform-plugin-sdk/terraform"
    )
    
    const ImgNameFilterKey = "image-name"
    
    func Provider() *schema.Provider {
    return &schema.Provider{
    DataSourceMap: map[string]*schema.Resource{
    "tencentcloud_xxxs": { /* 其他声明好的数据源 */ },
    "tencentcloud_yyys": { /* 其他声明好的数据源 */ },
    "tencentcloud_images": {
    Schema: map[string]*schema.Schema{ /* 略 */},
    Read: func(d *schema.ResourceData, meta interface{}) {
    // 声明 Client 和 request
    client, _ := cvm.NewClient(credential, "ap-guangzhou")
    request := cvm.NewDescribeImagesRequest()
    // 获取 Filters ,为 Map 类型
    filters := d.Get("filters").(map[string]interface{})
    // 检查 filters["name"] 并添加参数
    if name, ok := filters["name"].(string); ok {
    request.Filters = append(request.Filters, &cvm.Filter{
    Name: &ImgNameFilterKey,
    Values: []*string{&name}
    })
    }
    // 发起调用
    response, err := client.DescribeImages(request)
    if err != nil {
    return err
    }
    // 组装 `result` 字段并写入
    imageSet := response.Response.ImageSet
    result := make([]map[string]interface{}, 0, len(imageSet))
    for i := range imageSet {
    item := imageSet[i]
    result = append(result, map[string]interface{}{
    "id": *item.Id,
    "name": *item.Name,
    })
    }
    d.Set("result", result)
    // 如果 `result_output_file` 有定义,则往指定文件写入结果
    if v, ok := d.GetOk("result_output_file"); ok {
    writeToFile(v.(string), result)
    }
    return nil
    }
    },
    },
    }
    }
    
    这样我们变完成了 tencentcloud_images 的编写,后续可以使用这个 DataSource ,查询特定的 OS 镜像。

    编写验收测试

    为了确保您新增的 Resource 或 DataSource 能正常运作,您需要编写验收测试,详情请参考编写测试用例
    联系我们

    联系我们,为您的业务提供专属服务。

    技术支持

    如果你想寻求进一步的帮助,通过工单与我们进行联络。我们提供7x24的工单服务。

    7x24 电话支持