目次(未確定)
- 第1回: Terraform Custom Provider 基礎
- 第2回: リソース実装 基礎 -
schema.Resource
でのリソース実装の基礎 (当記事) - 第3回: スキーマ定義 前編-
schema.Schema
でのスキーマ定義 - 第4回: スキーマ定義 後編-
schema.Schema
でのスキーマ定義 - 第5回: リソース操作 -
schema.ResourceData
でのリソース操作 - 第6回: リソース定義 -
schema.Resource
の高度な機能(差分調整/マイグレーション/インポート) - 第7回: テスティングフレームワーク
前回はCustom Providerとして最低限の実装を行いTerraformから利用できるようにするところまででした。
今回は引き続きTerraformでのCustom Providerの実装の基礎的な部分について扱います。
前回作成したterraform-provider-minimum
にリソース操作のための実装を追加していきます。
プロバイダーで扱うリソースの実装
それでは早速minimum
プロバイダーで扱うリソースを実装してみます。
minimum
プロバイダーの仕様
Terraformプロバイダーの多くは対象プラットフォーム(例:AWS/Azure/GCP)が提供するAPIを用いてプラットフォーム上のリソースに対するCRUD操作を行うという形で実装されますが、
minimum
プロバイダーはサンプル実装ということで簡易的にローカルマシン上にファイルを作成するように実装します。
ここでは例としてminimum
プロバイダーに対しbasic
というリソースを実装してみます。
以下のようにname
という属性一つだけをもつシンプルなリソースです。
resource minimum_basic "example" { name = "foobar" }
terraform apply
を実行するとカレントディレクトリに以下のようなJSONファイルが作成されます。
{ "Name": "foobar" }
それでは早速リソースの実装を行なっていきます。
なお、basic
リソースを実装したソース一式は以下に置いています。
前回ソースコードをクローンされた方はbasic
ブランチをチェックアウトしておいてください。
$ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git checkout basic
basic
リソースの実装
リソース実装の手順は以下の通りです。
- (1)
*schema.Resource
を返すfuncを作成 - (2)
Provider()
に(1)で実装したfuncを追記
順に見ていきます。
(1) *schema.Resource
を返すfuncを作成
schema.Resource
とは、リソースに対するCRUDを行うfuncや入出力項目のスキーマ定義を持つ構造体です。
ソース: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L23-L137
リソースとしての最低限の実装は以下の通りです。
package minimum import "github.com/hashicorp/terraform/helper/schema" func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ // CRUD操作用のfuncをそれぞれ指定 Create: resourceMinimumBasicCreate, Read: resourceMinimumBasicRead, Update: resourceMinimumBasicUpdate, // 省略可 Delete: resourceMinimumBasicDelete, Schema: map[string]*schema.Schema{ // ここに入出力項目を定義 }, } } func resourceMinimumBasicCreate(d *schema.ResourceData, meta interface{}) error { return nil } func resourceMinimumBasicRead(d *schema.ResourceData, meta interface{}) error { return nil } func resourceMinimumBasicUpdate(d *schema.ResourceData, meta interface{}) error { return nil } func resourceMinimumBasicDelete(d *schema.ResourceData, meta interface{}) error { return nil }
schema.Resource
はいくつかのフィールドを持っていますが、リソースとして動作させるには最低限以下の項目をセットする必要があります。
Create
: リソース作成用のfuncRead
: リソース参照用のfuncUpdate
: リソース更新用のfunc(特殊なリソースでは省略可)Delete
: リソース削除用のfuncSchema
: 入出力項目の定義
CRUD用のfuncはfunc (*schema.ResourceData, interface{}) error
というシグニチャを持ちます。
このうちUpdate
は省略可能ですが、省略するにはいくつか条件がありますので次回以降の記事で改めて解説します。
それぞれのfuncの書き方にはお作法がありますので後ほどCRUDそれぞれについて見ていきます。
Schema
にはこのリソースで扱う入出力項目をmap[string]*schema.Schema
として記述します。
mapのキーが項目名となり、項目のデータ型や制約などを*schema.Schema
として記述します。
Schema
の定義
今回のbasic
リソースではname
という文字列型の項目を一つだけ定義しておきます。
func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, }, } }
*schema.Schema
には他にもいくつかのフィールドがありますが、それらは次回以降に扱います。
Create
の実装
続いてCreate
用のfuncを実装します。
Create
では以下の処理を行う必要があります。
- tfファイルからの入力を取得
- 各プラットフォームのAPIなどを用いて実リソースの作成
*schema.ResourceData
のSetId()
を呼び出してリソースIDを設定- 必要に応じて
*schema.ResourceData
のSet()
を呼び出してリソースの値を設定
basic
リソースでは以下のように実装します。
var client = state.NewDriver("basic") func resourceMinimumBasicCreate(d *schema.ResourceData, meta interface{}) error { // tfファイルからの入力を取得 name := d.Get("name").(string) // プラットフォームのAPIなどを用いて実リソースの作成 // (ここではローカルマシン上にJSONファイルを保存) value := &state.BasicValue{ Name: name, } id, err := client.Create(value) // 実リソース作成 - リソースのIDを戻り値として受け取る if err != nil { return err } // リソースIDを設定 d.SetId(id) // リソースの値を設定 return resourceMinimumBasicRead(d, meta) }
第1引数のd
は*schema.ResourceData
型となっています。
これはリソースに対する入出力値を扱う構造体で、tfファイルからの入力値の取得やTerraformが管理するリソースの値=State
へのアクセスなどを担当します。
TerraformでのState
については以下を参照してください。
第2引数のmeta
はAPIキーといったプロバイダー固有の設定を各リソースに受け渡すためのものです。
通常APIクライアントなどが設定されています。
今回は利用していませんが、次回の記事でこの引数を利用するようにリファクタしてみます。
tfファイルからの入力値の取得
まずはd.Get("項目名")
でtfファイルからの入力値を取得しています。戻り値はinterface{}
になっていますので、適切な型にキャストします。
プラットフォームのAPIを利用して実リソースの作成
入力値が出揃ったらプラットフォームのAPIを利用して実リソースの作成を行います。
この際にリソースごとに一意なIDを発行する必要があります。
今回はサンプルとしてプラットフォームのAPIを呼び出す代わりにローカルマシン上にJSONファイルとして保存しておくようにしています。
実際の処理はstate
パッケージに実装していますので興味のある方はソースを眺めてみてください。
(値をinterface{}
型として受け取り、JSONにして保存、IDはランダムな数値を返す実装です)
リソースIDの設定
実リソースを作成しIDが発行されたらd.SetId()
を呼び出してリソースIDの設定を行います。
Terraformはこのfuncを抜けた後にd.SetId()
を通じてリソースIDが設定されていか判定し、設定されている場合はリソースの持つ値をState
に保存します。
(特にバックエンドの設定をしていない場合はカレントディレクトリにterraform.tfstate
というファイルが作成されます)
リソースの値の設定
最後にd.Set()
を呼び出してリソースの値をキーごとに設定していきます。
設定した値は最終的にState
に保存されます。
(なお、d.SetId()
とd.Set()
の順番はどちらが先でもOKです)
ここで直接d.Set()
を呼んでも良いのですが、後ほどRead
を実行するときにもd.Set()
を実行することになりますので、実装の重複を避けるためにRead
用のfuncを直接呼び出すようにしています。
Read
の実装
続いてRead
を実装していきます。
Read
では以下の処理を行う必要があります。
*schema.ResourceData
のId()
を呼び出してリソースのIDを取得- 取得したIDを用いて各プラットフォームのAPIなどを呼び出し実リソースのデータを参照
*schema.ResourceData
のSet()
を呼び出してリソースの値を設定
一言で言うと、リソースのIDを用いて実リソースの最新リソースを参照し、リソースの値を設定
を行います。
basic
リソースでは以下のように実装します。
func resourceMinimumBasicRead(d *schema.ResourceData, meta interface{}) error { // リソースのIDを取得 id := d.Id() // IDを用いてプラットフォームのAPIを呼び出し、実リソースを参照する data, err := client.Read(id) if err != nil { // 実リソースが見つからなかったら if state.IsStateNotFoundError(err) { // *Point* IDを空にしてnilを返す d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } // 実リソースの最新の情報をリソースデータに設定 d.Set("name", value.Name) // nolint return nil }
実装時の注意点: 実リソースが存在しなかった場合の対応
ポイントは実リソースが見つからなかった場合の処理です。
IDを元にプラットフォーム上を検索してみたけど見つからなかったと言う場合には
d.SetId(""")
を呼びリソースIDをクリアした上でnilを返します。
こうすることでTerraformとしてはリソースは存在しないものとして認識し、再度作成が必要といった判断を行ってくれます。
ここでは、IDによる検索で予期せぬエラーが発生した場合と実リソースが見つからなかった場合を戻り値などから区別できることが必要です。
実リソースが存在しない場合にもエラーを返してしまった場合、terraform plan
やterraform destroy
の実行ができないという状況になる可能性もあります。
というのも、Terraformはplan
やapply
、destroy
の実行の前にState
を最新の状態にするためにRefresh
という処理を行います。
Refresh
は単に各リソースのRead
を呼び出すのですが、Read
がエラーを返すとそこで処理が中断されてしまうからです。
実リソースが存在しない場合はTerraformではもはや手の打ちようがないですので、d.SetId("")
を呼び出すことでState
からリソースの値をクリアします。
ごく稀にこの辺の処理を正しく行えていないリソースがあり、applyもdestroyもできないという状況になることがあります。
そのような場合の応急処置としてterraform.tfstate
ファイルから該当リソース部分を手作業で削除という泥臭い解決方法もあります。
IDを元に最新の実リソースを参照できるようになった後はd.Set()
を呼び出してリソースの値を設定していきます。
Update
の実装
続いてUpdate
の実装を行います。
Update
では以下の処理を行う必要があります。
*schema.ResourceData
のId()
を呼び出してリソースのIDを取得- 各プラットフォームのAPIなどを呼び出しIDに対応する実リソースの値を更新
*schema.ResourceData
のSet()
を呼び出してリソースの値を設定
Create
と同じくd.Get()
などでtfファイルからの入力値を取得して実リソースの値を更新していきます。
*schema.ResourceData
には以下のようないくつかのヘルパー関数がありますので、変更のあった項目だけ更新するといったことも可能です。
*schema.ResourceData
の変更検知用関数
d.HasChange("<項目名>")
: 対象項目が変更されているかを判定(戻り値: bool)d.GetChange("<項目名>")
: 対象項目の変更前/変更後の値を取得(戻り値: interface{}, interface{})
basic
リソースでは以下のように実装します。
func resourceMinimumBasicUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() // IDを元に実リソースを検索 data, err := client.Read(id) if err != nil { // 実リソースが見つからなかったら if state.IsStateNotFoundError(err) { d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } // set updated values if d.HasChange("name") { value.Name = d.Get("name").(string) } // save values if err := client.Update(id, value); err != nil { return err } return resourceMinimumBasicRead(d, meta) }
ここでもRead
と同じくIDを用いて実リソースを検索し、見つからなかったらd.SetId("")
を呼び出した上でnilを返しています。
あとはd.HasChange()
で変更があった項目をセットした上でプラットフォームのAPIを呼び出し実リソースを更新します。
Create
と同じくRead
用のfuncを直接呼び出すことでd.Set()
を行っています。
Delete
の実装
続いてDelete
の実装を行います。
Delete
では以下の処理を行う必要があります。
*schema.ResourceData
のId()
を呼び出してリソースのIDを取得- 各プラットフォームのAPIなどを呼び出しIDに対応する実リソースの値を削除
Create
やUpdate
と比較するとシンプルな実装になることが多いですね。
basic
リソースでは以下のように実装します。
func resourceMinimumBasicDelete(d *schema.ResourceData, _ interface{}) error { id := d.Id() if err := client.Delete(id); err != nil { if !state.IsStateNotFoundError(err) { return err } } d.SetId("") return nil }
IDを用いてプラットフォームのAPIで実リソースの削除を行うだけですね。
なお、Delete
ではエラーを返さなければd.SetId("")
があとで呼ばれるようになっていますのでここでのd.SetId("")
は省略可能だったりします。
(公式ガイドにもその旨が書いてあります。明確にするためにあえて書いているとのことです。)
参考: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L197-L203
以上でリソースの基本的な実装ができました。
余談: Exists
は実装すべき?
schema.Resource
のソースを読むと、Exists
というfuncがあり、実装を推奨(recommended)すると言うコメントが書かれています。
参考: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L59-L86
// The functions below are the CRUD operations for this resource. // // [...中略...] // // Exists is a function that is called to check if a resource still // exists. If this returns false, then this will affect the diff // accordingly. If this function isn't set, it will not be called. It // is highly recommended to set it. The *ResourceData passed to Exists // should _not_ be modified. Create CreateFunc Read ReadFunc Update UpdateFunc Delete DeleteFunc Exists ExistsFunc
これはRead
で行っていた、IDによる実リソースの存在確認を実装するためのもので、セットされているときだけ呼ばれるようになっています。
推奨という割には実装していないリソースもチラホラと散見されます。(例: AWSプロバイダーのEC2リソース aws_instance
など)
実際にはRead
が正しく実装されていればExists
は実装しなくても構いません。
Read
との違いは呼ばれるタイミングです。
Exists
とRead
の間にはスキーマのマイグレーション処理(次回以降に扱う予定)が行われています。
参考: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/resource.go#L319-L352
このあたりの処理が重くなるようであればExists
を実装した方が良いですが、最初はRead
の実装のみで十分でしょう。
(2) Provider()
に(1)で実装したfuncを追記
リソースの実装ができましたので、Provider()
にリソースを登録しておきます。
minimum/provider.go
を以下のようにします。
package minimum import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) // Provider returns a terraform.ResourceProvider. func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{}, ResourcesMap: map[string]*schema.Resource{ "minimum_basic": resourceMinimumBasic(), // この行を追記 }, } }
これでbasic
リソースの実装ができました。
動作確認
実際にビルドして試してみましょう。
tfファイルはbasic
ブランチの直下にexample.tf
がというファイルを置いていますのでそれを利用します。
# ビルド $ go build -o terraform-provider-minimum main.go # init & apply $ terraform init $ 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_basic.example id: <computed> name: "foobar" Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: < yesを入力 minimum_basic.example: Creating... name: "" => "foobar" minimum_basic.example: Creation complete after 0s (ID: 2012225891) Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
apply
が成功するとカレントディレクトリにbasic-nnnnnnnn.json
(nはランダムな数字)というファイルが作成されているはずです。
tfファイルやJSONファイルを書き換えてapply
を実行したりdestroy
を実行したりと色々試してみてください。
basic
リソースのソース全体
まとめとしてbasic
リソースのソース全体を載せておきます。
package minimum import ( "encoding/json" "github.com/hashicorp/terraform/helper/schema" "github.com/yamamoto-febc/terraform-provider-minimum/state" ) var client = state.NewDriver("basic") func resourceMinimumBasic() *schema.Resource { return &schema.Resource{ Create: resourceMinimumBasicCreate, Read: resourceMinimumBasicRead, Update: resourceMinimumBasicUpdate, Delete: resourceMinimumBasicDelete, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, }, } } func resourceMinimumBasicCreate(d *schema.ResourceData, meta interface{}) error { name := d.Get("name").(string) value := &state.BasicValue{ Name: name, } id, err := client.Create(value) if err != nil { return err } d.SetId(id) return resourceMinimumBasicRead(d, meta) } func resourceMinimumBasicRead(d *schema.ResourceData, _ interface{}) error { id := d.Id() data, err := client.Read(id) if err != nil { if state.IsStateNotFoundError(err) { d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } d.Set("name", value.Name) // nolint return nil } func resourceMinimumBasicUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() // read current state data, err := client.Read(id) if err != nil { if state.IsStateNotFoundError(err) { d.SetId("") return nil } return err } value := &state.BasicValue{} if err := json.Unmarshal(data, &value); err != nil { return err } // set updated values if d.HasChange("name") { value.Name = d.Get("name").(string) } // save values if err := client.Update(id, value); err != nil { return err } return resourceMinimumBasicRead(d, meta) } func resourceMinimumBasicDelete(d *schema.ResourceData, _ interface{}) error { id := d.Id() if err := client.Delete(id); err != nil { if !state.IsStateNotFoundError(err) { return err } } d.SetId("") return nil }
第2回 まとめ
第2回では単純なリソースの実装を行ってみました。 操作したいリソースにAPIさえ提供されていれば意外と簡単に実装できますね。
次回はスキーマ定義(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件) を見る