1. Terraform language fomat

官网中对于terraform的语言格式描述如下:

<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
  # Block body
  <IDENTIFIER> = <EXPRESSION> # Argument
}

官网的另一个页面,有更为详细的语法说明。其中需要注意的是 block body 中的内容除了 argument某些地方 也称为 attribute )以外,也允许是内嵌的 block

因此,上面这个spec应该更新为:

<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
  # Block body

  <IDENTIFIER> = <EXPRESSION> # Argument

  OR

  <IDENTIFIER> {              # Nested Block
      # Block body
  }
}

与之前不同的是,我在 block body 中加入了 Nested Block。需要注意的是这里 argument 的syntax(有 = 号) 和 block 的syntax(没有 = )不同。这个限制在tf 0.12 以后是强制的。

另外, argument 除了Provider定义的以外,不同的 block type 还会有一些 meta argument.

建议,为了区分这两个 block body 中仅有的两类元素,遵循官方以及源码中的命名,称呼它们为:attribute/argumentblock.

98 How to implement Azure Resource

98.1 Managed Resource

Resource 基本只要:

98.1.1 onCreate/onUpdate

在AZureRM的SDK中,Client大多会将create和update实现在一个叫做CreateOrUpdate()的函数中。因此,onCreate()onUpdate()赋值为同一个callback。

注意: Resource.Update callback是可选参数,如果没有被实现,那么说明该资源不支持update

这个callback接收两个参数:

这个callback主要做的事情是:

  1. 读取configuration中的设置
  2. 检查指定的resource是否已经在azure存在,但是没有被tf接管:

    这部分代码在azure中大概结构如下:

     // 如果当前被apply的resource tf没有接管,那么 `d.IsNewResource()` 返回 `true`
     // `features.ShouldResourcesBeImported()`: 用户可以选择opt-in(AzureRM 2.0以后默认为true),
     // 如果opt-in,代表对于已经在azure存在的资源用户必须先import才可以通过apply修改它。否则,下面这段代码
     // 就会被用于阻止用户apply一个已经存在,但tf未接管的资源。
     if features.ShouldResourcesBeImported() && d.IsNewResource() {
       resp, err := client.Get(ctx, resourceGroup, serverName, name)
       if err != nil {
         if !utils.ResponseWasNotFound(resp.Response) {
           return fmt.Errorf("xxx", name, resourceGroup, serverName, err)
         }
         // * MARK
       }
    
       if resp.ID != nil && *resp.ID != "" {
         return tf.ImportAsExistsError("xxx", *resp.ID)
       }
     }
    

    上面注释的 Mark 的这段代码的错误处理由于azure历史问题,是有违Go语言惯例的。在Go中,当一个函数返回时,例如 result, err := Foo(),一般 result的值仅当 err == nil 的时候才会有意义。

    而这里azure的go sdk代码中,client.Get() 返回的 err 的语义为:仅当请求返回200,才认为 err == nil。例如下面的定义:

     func (client FirewallRulesClient) Get(ctx context.Context, resourceGroupName string, serverName string, firewallRuleName string) (result FirewallRule, err error) {
       //...
       req, err := client.GetPreparer(ctx, resourceGroupName, serverName, firewallRuleName)
       if err != nil {
         err = autorest.NewErrorWithError(err, "sql.FirewallRulesClient", "Get", nil, "Failure preparing request")
         return
       }
    
       //...
       resp, err := client.GetSender(req)
       if err != nil {
         result.Response = autorest.Response{Response: resp}
         err = autorest.NewErrorWithError(err, "sql.FirewallRulesClient", "Get", resp, "Failure sending request")
         return
       }
    
       //...
       result, err = client.GetResponder(resp)
       if err != nil {
         err = autorest.NewErrorWithError(err, "sql.FirewallRulesClient", "Get", resp, "Failure responding to request")
       }
    
       return
     }
        
     // ...
    
     func (client FirewallRulesClient) GetResponder(resp *http.Response) (result FirewallRule, err error) {
       err = autorest.Respond(
         resp,
         client.ByInspecting(),
         azure.WithErrorUnlessStatusCode(http.StatusOK), // DEFINED HERE!!!
         autorest.ByUnmarshallingJSON(&result),
         autorest.ByClosing())
       result.Response = autorest.Response{Response: resp}
       return
     }
    

    个人认为将 err 的语义改为只要服务端正确接收了请求并且返回,那么err就应该置为nil。至于请求的结果则由result定义。

  3. 将读取的config转换成sdk client的参数,调用client的 CreateOrUpdate()

    注意

    • CreateOrUpdate()的参数构造的过程称为 expand,这个过程简单来说就是: configuration -> sdk struct 的过程。可能只是简单的转换,也可能包含额外的 client.Get() 调用 (例如: virtual network
    • CreateOrUpdate()函数在某些实现中返回一个future对象,需要等待
  4. 调用client的Get()函数,从response中获取ID并且设置ID(d.SetId())
  5. return onRead()

98.1.2 onRead

在Azure中,client.Get()返回的ID,也即记录在tf state中的ID是resource的长ID,记录了包括:subscription, resource group, resource name 等信息。在Azure Provider中有一个叫做 ParseAzureResourceID() 的函数用来将这个长ID解析为一个ResourceID的结构体:

type ResourceID struct {
	SubscriptionID string
	ResourceGroup  string
	Provider       string
	Path           map[string]string
}  

Azure的资源的长ID总是有偶数个字段,因为每个component都是以 key/value 的形式表现在ID中。ResourceID.Path 的map中存的内容为 长ID中除了 SubscriptionID, ResourceGroup, Provider 以外的那些component(例如:资源的name)。

说了这些背景以后再来看看这个onRead() callback需要做哪些事情:

  1. ResourceData 中读取长ID,并解析得到 ResourceID
  2. 调用 client.Get() 获取该当前信息

    注意: 如果返回的resp是http.StatusNotFound,那么需要将 ResourceDataId置空,这会通知terrafrom该资源在azure中已经被删除,terraform于是也会从它的state中删除该资源(refer)

  3. 将获取的信息转换为tf configuration的格式(这个过程称为 flatten),用来更新ResourceData

98.1.3 onDelete

主要步骤:

  1. ResourceData 中读取长ID,并解析得到 ResourceID
  2. 调用client.Delete()将资源同时从azure和tf state删除 (这个函数在某些实现中返回一个future对象,需要等待)

    注意: 由于terrafrom对Destroy函数的定义是:

    • If the Destroy callback returns without an error, the resource is assumed to be destroyed, and all state is removed.
    • If the Destroy callback returns with an error, the resource is assumed to still exist, and all prior state is preserved.

    因此,如果client.Delete()返回的 err != nil,需要特殊地判断资源是否已经从azure删除了,如果已经删除,则直接返回 nil.

98.2 Data Resource

data resource 本质上就是一个只有Read()的managed resource,实现的唯一区别在于 data resource 的 Read() 需要设置ResourceData的ID,而这个ID实际就是提供这些data的后端resource的长ID。

98.3 Azure Virtual Resource

在Azure中有一些resource,它们的后端并不是该resource特定的service,甚至有一些resource没有后端service提供其CRUD。我将这些service称为 Virtual Resource.

98.3.1 Association Resource

例如:azurerm_network_interface_application_gateway_association 是一个 Association Resource, 它将 azurerm_network_interfaceazurerm_application_gateway关联起来(通过各自的ID)。这种情况一般属于两个已经存在的资源,在后续的迭代中发现某些use case下需要两者关联使用。

这时候,有两种关联的做法:

  1. 将其中一个resource的ID加入到另一个resource的schema。这样有个弊端:将两个资源一定程度耦合
  2. 定义一个association resource,将两个资源关联

Azure中几乎全部的 association resource 都没有实现 Resource.Update callback。这表明这些resource一旦修改,就需要删除并重建。由于它们本来就是定义association这种关联关系的,所以这是可以接受的。

98.3.2 Attaching Resource

例如:azurerm_servicebus_event_authorization_rule 是一个 Attaching Resource,这个resource实际上作为另一个有后端service,azurerm_servicebus_event,对应的resource的一部分存在。在这个后端service的API spec中,attaching resource实际上对应的是API的某个sub-path。因此,在其tf resource的实现中,它使用的 azure go sdk的client就是那个实际的service(即,azurerm_servicebus_event)。

可见,这种resource在tf中实现的前提是后端service的API对其有直接的支持。

98.4 Test

98.5 Document

99 TIP

99.1 block body nested map

官网有提到:

Due to the limitation of tf-11115 it is not possible to nest maps. So the workaround is to let only the innermost data structure be of the type TypeMap

这里的workaround在tf 0.12版本以后就被修复了,具体可以看上面那个issue。同时,有另外一个与之关联的issue要求把上面这个章节删除。So…如果你打不开上面那个官网的章节的话就代表已经被删除了,如果你依然可以访问,请无视 (我在这里看了好一会儿 😶)

99.2 tf provider 没有输出

Provider中的日志输出需要将 TF_LOG level 设置为 >= DEBUG

99.3 The Computed Attribute

Computed: true means “a value will be assigned during Create, so there is no record in local state file (see here)”.

Because there is no local state, if user changes this property in outside of terraform, a refresh in terraform will not detect that change.

If combined with Optional: true, then it means “if no value is given in configuration then a value will be assigned during Create. But if a value is specified, then use that value. But still no record in local state file”. Here, the purpose to add Computed: true instead of barely Optional: true is for cases like following:

A NSG has in-line NSR settings, while NSR can also seperately created. So if NSG’ NSR property is without Computed: true, then if user created an empty NSG, and created a NSR1 into NSG seperately. Then when another apply happened to NSG, it will thought user specify a null NSR rule sets, while remote has 1 NSR, so tf will resolve to delete that NSR. Hence, with Computed: true set, if user has not explicitly specify NSG’s NSR property, tf will just take the remote state as current state.

Note: the property here is essentially a nested block. I don’t find a use case where Computed:true is helpful for attribute.

However, there exists an issue with above case, that is once a Computed: true & Optional: true block is specified explicitly (e.g. the in-line NSR in NSG). User is only able to update it by explicitly changing the property, but can’t delete it.

From terraform 0.12, there comes a new schema config mode named: schema.SchemaConfigModeAttr, which if set will allow user to distinguish between a non-existent block and a force-zeroed block. Read official document for more info.