目次(未確定)
- 第1回: Terraform Custom Provider 基礎
- 第2回: リソース実装 基礎 -
schema.Resource
でのリソース実装の基礎 - 第3回: スキーマ定義 前編 -
schema.Schema
でのスキーマ定義 (当記事) - 第4回: スキーマ定義 後編 -
schema.Schema
でのスキーマ定義 - 第5回: リソース操作 -
schema.ResourceData
でのリソース操作 - 第6回: リソース定義 -
schema.Resource
の高度な機能(差分調整/マイグレーション/インポート) - 第7回: テスティングフレームワーク
前回はCustom Providerにリソースを追加し基本となるCRUD操作を実装してみました。
今回はリソースで扱う入出力項目であるスキーマの定義について詳しくみていきます。
なお、今回も手を動かしながら確認できるようにソースを以下に置いています。
https://github.com/yamamoto-febc/terraform-provider-minimum/tree/types
前回ソースコードをクローンされた方はtypes
ブランチをチェックアウトしビルドしておいてください。
#チェックアウト $ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git fetch -a $ git checkout types #ビルド $ dep ensure $ go build -o terraform-provider-minimum main.go
schema.Schema
でのスキーマ定義
前回作成したbasic
リソースではスキーマを以下のように定義していました。
import ( // [...中略...] "github.com/hashicorp/terraform/helper/schema" ) func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ // [...中略...] Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, }, } }
文字列型で必須なname
という項目一つだけの単純なものです。
項目を増やすにはSchema
に項目を追加していくだけでOKです。(もちろんCRUD操作の中でd.Set()
を適度に呼び出すなど適切に実装する必要があります)
各項目はSchema
に設定するmapの要素であるschema.Schema
のフィールドを適切に設定することでデータ型や制約、デフォルト値などを柔軟に制御可能です。
まずはschema.Schema
型について詳しくみていきます。
schema.Schema
型
schema.Schema
型はリソースが扱う入出力項目それぞれの振る舞いを定義するための構造体です。
- 公式ドキュメント: https://www.terraform.io/docs/extend/schemas/index.html
- ソース: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L34-L200
以下のようなフィールドが定義されています。
("フィールド名
(型) : 説明" で表記)
- データ型
Type
(schema.ValueType) : データ型
- 入出力動作の指定(1つ以上の指定必須)
Optional
(bool) : trueの場合、省略可能になるRequired
(bool) : trueの場合、必須となるComputed
(bool) : trueの場合、作成時に計算(算出)される(値が未指定の場合のみ)
- デフォルト値関連
Default
(interface{}) : デフォルト値(値で指定)DefaultFunc
(schema.SchemaDefaultFunc) : デフォルト値(funcで指定)
- 値変更/保存時の挙動関連
DiffSuppressFunc
(schema.SchemaDiffSuppressFunc) : 差分検出で使用するfuncForceNew
(bool) : trueの場合は変更時にUpdate
ではなくDestroy
->Create
が行われるStateFunc
(schema.SchemaStateFunc) :State
保存の際に呼ばれるフック
- センシティブなデータの扱い
Sensitive
(bool) : trueの場合、この項目をログ/標準出力に出力する際にマスクされる
Type
がTypeList or TypeSetの場合に使用するフィールドElem
(interface{}) : ListまたはSetの各要素のデータ型MaxItems
(int) : ListまたはSetに格納できる最大数(境界含む/1以上の場合のみ有効)MinItems
(int) : ListまたはSetに格納できる最小数(境界含む/1以上の場合のみ有効)PromoteSingle
(bool) : trueの場合、単一の値として指定された場合に自動的にリストに変換する(プリミティブ型のみで有効)
Type
がTypeSetの場合に使用するフィールドSet
(schema.SchemaSetFunc) : 項目のハッシュ値算出で使用するfunc
- バリデーション関連
ConflictsWith
([]string) : 同時に指定できない項目の名称を指定ValidateFunc
(schena.SchemaValidateFunc) : 入力値のバリデーションで使用するfunc(プリミティブ型のみで有効)
- 警告/エラーメッセージ関連
Deprecated
(string) : 設定されている場合、この項目を利用すると警告メッセージを出すRemoved
(string) : 設定されている場合、この項目を利用するとエラーメッセージを出す(validate
やplan
、apply
を異常終了させる)
- v0.11時点では実装されていないフィールド
Description
(string)InputDefault
(string)ComputedWhen
([]string)
必須項目
これらのうち、最低限指定しないといけないのは以下2つです。
Type
Optional
/Required
/Computed
を1つ以上
まずはType
からみていきます。
データ型を決めるType
Type
はデータ型を表します。以下の値が指定可能です。
TypeBool
- boolTypeInt
- intTypeFloat
- float64TypeString
- stringTypeList
- []interface{}TypeMap
- map[string]interface{}TypeSet
- *schema.Set
右側はResourceData.Get()
を呼んだ時の戻り値の実際の型を表しています。
ResourceData.Get()
の戻り値はinterface{}
ですので、項目のデータ型に応じて適切にキャストする必要があります。
それぞれの特徴を順に見ていきます。
プリミティブ型(Bool/Int/Float/String)
それぞれのプリミティブ型を示します。特に説明不要ですね。
tfファイル上は値の書き方にバリエーションがある点には留意しておいてください。
(このへんはTerraformの実装というよりHCLの実装によります)
Terraform v0.12ではHCLの後継であるHCL2への切り替えが進められています。
以下にtfファイルの書き方例を記載しておきます。
TypeBool
resource minimum_bool "bool" { value = true // 文字列で指定してもOK #value = "true" #OK #value = "foobar" #これはNG // 数値もOK(true=1,false=0) #value = 0 # OK #value = 2 # これはNG #value = -1 # これはNG // 数値を文字列で指定してもOK #value = "0" # OK #value = "2" # これはNG #value = "-1" # これはNG }
TypeInt
resource minimum_int "int" { value = 1 // 8進数/16進数/指数もOK #value = 0777 # 8進数 #value = 0xFFFF # 16進数 #value = 1e10 # 指数表記 // 文字列で指定してもOK #value = "1" // 範囲(golangのintの範囲) #value = -9223372036854775808 #value = 9223372036854775807 #value = 1.1 # これはNG }
TypeString
resource minimum_string "string" { value = "foobar" // 数値で指定してもOK(文字列に変換される) #value = 0777 # 8進数 #value = 0xFFFF # 16進数 #value = 1e10 # 指数表記 }
List型(TypeList)
次にTypeList
を見ていきます。TypeList
はプリミティブ型、または複合型のリストを表します。
スキーマ定義は以下のようになります。
func resourceMinimumList() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, // MaxItems: 1, // MinItems: 1, }, }, } }
TypeListにする場合、Elem
は必須です。Elem
でリストの各要素のデータ型を指定する必要があります。
リスト内の要素がプリミティブ型の場合は*schema.Schema
を、複合型の場合は*schema.Resource
を指定します。
多段ネストも可能です。
なお、後述するTypeMap
と違いElem
にschema.ValueType
を直接指定することはできません。
正しくないElem指定の例
// !正しくないElem指定の例! func resourceMinimumList() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeList, Optional: true, Elem: schema.TypeString, // TypeListではValueTypeを直接指定できない }, }, } }
次にリスト内に複合型を利用する場合のスキーマ定義です。
リスト内に複合型を利用する例
func resourceMinimumNestedList() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Optional: true, }, "value": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, }, }, }, } }
また、リストの要素数をMinItems
/MaxItems
で制限可能です。
いずれもデフォルトは0で、1以上の数値を指定すると有効になります。
(これらは後述するTypeSet
でも利用可能です)
List型を含むリソースを利用するtfファイルは以下のようになります。
# プリミティブ型のリスト resource minimum_list "list" { value = ["list0", "list1", "list2"] } # 複合型のリスト resource minimum_nested_list "nested-list" { value = [ { name = "name1" value = "value1" description = "description1" }, { name = "name2" value = "value2" description = "description2" // 定義していない項目はちゃんとエラーにしてくれる # foo = "bar" }, ] }
List型の項目に対しResourceData.Get()
すると[]interface{}
が返ってきます。
各要素がプリミティブ型の場合は単にキャスト(TypeString
ならstringへ)すればOKです。
複合型の場合はmap[string]interface{}
になります。
上記のminimum_nested_list
リソースの場合は以下のようにします。
func resourceMinimumNestedListRead(d *schema.ResourceData, meta interface{}) error { // ... values := d.Get("value").([]interface{}) // まず[]interface{}にキャスト for _ , v := range values { element := v.(map[string]interface{}) // 各要素はmap[string]interface{} name := element["name"].(string) value := element["value"].(string) desc := element["description"].(string) // ... } }
Map型(TypeMap)
続いてTypeMap
を見ていきます。TypeMap
はその名の通りキーと値をペアで持ちます。キーは文字列である必要があります。
スキーマ定義は以下のようになります。
func resourceMinimumMap() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeMap, Optional: true, }, }, } }
リソースの実装時、Mapの項目に対しResourceData.Get()
するとmap[string]interface{}
が返ってきます。
また、Map型でも要素の型をElem
で指定できますが、少々クセがある点に注意が必要です。
Map型の注意点
現状はMap型の項目に対しElem
を指定した際は以下のような挙動となっています。
Elem
が未指定の場合は各要素をTypeString
とみなすElem
にschema.ValueType
(TypeIntとかTypeStringとか)が指定された場合- プリミティブ型であればそのまま使う
- 以外の場合は
TypeString
とみなす
Elem
に*schema.Schema
が指定された場合*schema.Schema
のType
がプリミティブ型であればそのまま使う- 以外の場合は
TypeString
とみなす
Elem
に*schema.Resource
が指定された場合TypeString
とみなす
要はMapの各要素はプリミティブ型でないとダメということです。
この挙動はリストのように複合型を利用したい場合に問題が発生します。
例えば以下のようにスキーマ定義します。
// !問題のあるスキーマ定義! func resourceMinimumInvalidMap() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeMap, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "value1": { Type: schema.TypeString, Required: true, }, "value2": { Type: schema.TypeInt, Optional: true, }, "value3": { Type: schema.TypeBool, Optional: true, }, }, }, }, }, } }
Map型の各要素として以下のような複合型を指定しています。
value1
: 文字列型/必須value2
: Int型value3
: Bool型
このリソースを利用するtfファイルは以下の通りです。
resource minimum_invalid_map "invalid" { value = { value1 = "value1" value2 = 2 value3 = true } }
terraform validate
もterraform apply
も問題なく行えるはずです。
しかし、以下のようにした場合はどうでしょうか?
resource minimum_invalid_map "invalid" { value = { # value1 = "value1" # Required=trueの項目をコメントアウト value2 = "foo" # Int型の項目に文字列を指定 value3 = "bar" # Bool型の項目に文字列を指定 value4 = "not exists" # 定義していない項目を指定 } }
なんとterraform validate
もterraform apply
も問題なく行えてしまいました。
これはElem
にプリミティブ型以外を指定してしまったために、各要素がTypeString
とみなされてしまうからです。
このようにMap型でElem
を使う場合には直感的でない挙動となりますので、
もしtfファイル上で複合型を利用したい場合は後述のTypeSet
か、MaxItems=1
にしたTypeList
を使う方法を検討してください。
なお、この辺のTypeMapの挙動については対応が進められてはいるのですが現時点では止まっちゃってるっぽいです。
Set型(TypeSet
)
最後にTypeSet
をみていきます。TypeSet
は値のハッシュ機能付きのリストです。
スキーマ定義は以下のようになります。
func resourceMinimumSet() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "value": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Optional: true, }, "value": { Type: schema.TypeString, Optional: true, }, "description": { Type: schema.TypeString, Optional: true, }, }, }, Set: func(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) // nameとvalueの値からハッシュ値を生成 keys := []string{"name", "value"} for _, key := range keys { if v, ok := m[key]; ok { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } } return hashcode.String(buf.String()) }, }, }, } }
TypeList
の時と同じくElem
が必須となっています。
違いはSet
の部分ですね。
これは値からハッシュ値を算出する部分となっています。
(なおSet
を利用せず、ResourceData.Set()
するときにschema.NewSet()
を利用する方法もあります。)
ポイントはハッシュ値が同一なTypeSet
の要素はまとめられるという性質を持っている点です。
tfファイルでの例を見てみましょう。
この例のminimum_set
リソースでは、各要素のname
とvalue
を元にハッシュ値を算出しています。
まずはハッシュ値が異なる要素の場合です。
resource minimum_set "set" { value = [ { name = "name1" value = "value1" description = "description1" }, { name = "name2" value = "value2" description = "description2" }, ] }
terraform apply
すると以下のようになります。
$ terraform apply An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + minimum_set.set id: <computed> value.#: "2" value.1049404950.description: "description1" value.1049404950.name: "name1" value.1049404950.value: "value1" value.741132560.description: "description2" value.741132560.name: "name2" value.741132560.value: "value2" Plan: 1 to add, 0 to change, 0 to destroy.
minimum_set.set
のvalue
の要素として2つの要素が作成される様子が確認できますね。
次にtfファイルを修正しname
とvalue
を同一の値にしてハッシュ値が同じになるようにしてみます。
$ terraform apply An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: + minimum_set.set id: <computed> value.#: "1" value.1049404950.description: "description2" value.1049404950.name: "name1" value.1049404950.value: "value1" Plan: 1 to add, 0 to change, 0 to destroy.
今度はminimum_set.set
のvalue
の要素として1つの要素が作成される様子が確認できますね。
前述の通りハッシュ値が同じ要素はまとめられるという性質によりこうなっています。
一見するとめんどくさいだけなようにも思えますが、もちろん役に立つ場面が存在します。
例えばDNSのAレコードを扱う場合を考えてみます。
Aレコードは一つのホスト名に対し複数のIPアドレスを指定できますよね。
この場合単純にホスト名をキーにしたマップを利用するというわけにはいかないでしょう。
正しくない例: レコードをマップで表す場合
# !正しくない例! レコードをマップで表す場合 resource dummy_dns_zone "zone" { zone = "example.com" records = { "www" = "192.2.0.1", # mapなので重複したキーは指定できない "www" = "192.2.0.2", # } }
単純にリストにすればキーに当たるホスト名は重複させられますが、今度はホスト名/IPアドレスの組み合わせが重複する可能性があります。
正しくない(かもしれない)例: レコードをリストで表す場合
# !正しくない(かもしれない)例! レコードをリストで表す場合 resource dummy_dns_zone "zone" { zone = "example.com" # リストの場合は値の重複が起こり得る records = [ { name = "www", ip = "192.2.0.1", }, { name = "www", ip = "192.2.0.1", }, ] }
ResourceData.Get()
を行なった際に手動でバリデーションを行えば良いのですが、TypeSet
であればこの辺りが楽に行えるようになっています。
# レコードをセット(TypeSet)で表す場合 resource dummy_dns_zone "zone" { zone = "example.com" # TypeSetの場合はハッシュ生成に使う値が同じならまとめられる records = [ { name = "www", ip = "192.2.0.1", }, { name = "www", ip = "192.2.0.1", }, ] }
注意点として、TypeSet
を用いることで、tfファイルには2レコード書いたつもりなのに(書き間違えにより意図せず)1レコードしか作成されない!といったことが発生し得ます。
TypeList
を利用してバリデーションを実装することで重複チェックを行う方法もありますので、適度に使い分けましょう。
また、TypeSet
を利用した場合、リソースのテストが書きにくい(リソースIDがハッシュ値になるため)という問題もあります。
この辺りは次回以降の記事でテスティングフレームワークを扱う際に改めて触れます。
Type
は以上です。これらを組み合わせてリソースに対する入出力項目を定義していくことになります。
各データ型の特徴をしっかり押さえておきましょう。
第3回 まとめ
第3回ではスキーマ定義に利用するschena.Schema
型のフィールドのうち、データ型を表すType
について扱いました。
次回はschema.Schema
の他のフィールドについて見ていきます。
以上です。
Terraform: Up and Running: Writing Infrastructure as Code
- 作者: Yevgeniy Brikman
- 出版社/メーカー: O'Reilly Media
- 発売日: 2017/03/13
- メディア: Kindle版
- この商品を含むブログを見る
Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス
- 作者: Kief Morris,宮下剛輔,長尾高弘
- 出版社/メーカー: オライリージャパン
- 発売日: 2017/03/18
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る