目次(未確定)
- 第1回: Terraform Custom Provider 基礎
- 第2回: リソース実装 基礎 -
schema.Resource
でのリソース実装の基礎 - 第3回: スキーマ定義 前編-
schema.Schema
でのスキーマ定義 - 第4回: スキーマ定義 後編-
schema.Schema
でのスキーマ定義 (当記事) - 第5回: リソース操作 -
schema.ResourceData
でのリソース操作 - 第6回: リソース定義 -
schema.Resource
の高度な機能(差分調整/マイグレーション/インポート) - 第7回: テスティングフレームワーク
前回はスキーマ定義として必須項目であるType
(データ型)について扱いました。
今回はschema.Schema
の他のフィールドについて見ていきます。
前回、*schema.Schema
に最低限指定しないといけないフィールドとして以下2つを挙げました。
Type
Optional
/Required
/Computed
を1つ以上
Type
は前回扱いましたので、今回はもう一方のOptional
/Required
/Computed
について扱います。
入出力動作(Required
/Optional
/Computed
)
項目の入力/出力動作を決めるフィールドです。 これらは1つ以上指定(trueに設定)する必要があります。
それぞれの意味は以下の通りです。
Optional : bool
: trueの場合、省略可能になるRequired : bool
: trueの場合、必須となるComputed : bool
: trueの場合、値は算出される(値が未指定の場合のみ)
これらを組み合わせて指定するのですが中には組み合わせ出来ないものもありますので、取りうるパターンは以下4つだけです。
入出力動作で指定可能な組み合わせ
Required: true
: 入力必須な項目Optional: true
: 省略可能な項目Optional:true かつ Computed: true
: 省略可能な項目(省略した場合はリソースが算出する)Computed: true
: 出力専用の項目(値はリソースが算出する)
このうち、Computed
は出力専用、それ以外は入力項目となります。
特殊な項目: 入力専用
また、少々特殊なものとして、Required
/Optional
を使いつつResourceData.Set()
を呼ばない事で入力専用の項目を作ることも可能です。
詳しくはサンプル実装を動かしながら見ていきます。
サンプル実装inout
リソースでの動作確認
これらの項目の動作確認のためにminimum_provider
でinout
というリソースを実装しています。
今回も手を動かしながら確認できるようにソースを以下に置いています。
前回ソースコードをクローンされた方はinout
ブランチをチェックアウトしビルドしておいてください。
# チェックアウト $ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git fetch -a $ git checkout inout # ビルド $ dep ensure $ go build -o terraform-provider-minimum $ terraform init
inout
リソースでのスキーマ定義
inout
リソースはRequired
/Optional
/Computed
の組み合わせを確認するために以下のようにスキーマ定義しています。
func resourceMinimumInOut() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ "required": { Type: schema.TypeString, Required: true, }, "optional": { Type: schema.TypeString, Optional: true, }, "optional_computed": { Type: schema.TypeString, Optional: true, Computed: true, }, "computed": { Type: schema.TypeString, Computed: true, }, "input_only": { Type: schema.TypeString, Optional: true, }, }, } }
先ほどの指定可能な組み合わせ4つそれぞれに対応した項目があります。
また、入力専用の項目としてinput_only
という項目も定義しています。
このうち、computed
については現在の日時(RFC3339形式)が値として設定されます。
また、optional_computed
はtfファイルで値が指定されなかった場合に現在の日時を値として設定するようにしています。
実際に動きを見てみましょう。
Required
の動作確認
まずは以下のようにinout
リソースに対してどの項目も未指定の状態のtfファイルを作成してapply
してみます。
resource minimum_inout "inout" { }
apply
するとRequired: true
の項目が未指定である旨を表示してエラーとなってくれます。
$ terraform apply Error: minimum_inout.inout: "required": required field is not set
Required
の注意点
注意点として、Required
は値がtfファイル上で指定されていればOKと判定します。
例として、以下のようにスキーマ定義していた場合、
Schema: map[string]*schema.Schema{ "string": { Type: schema.TypeString, Required: true, }, "int": { Type: schema.TypeInt, Required: true, }, "bool": { Type: schema.TypeBool, Required: true, }, "list": { Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, Required: true, }, },
次のようなtfファイルであればapply
出来てしまいます。
resource dummy_required "example" { string = "" bool = false int = 0 list = [] }
Required: true
でも有効な値が指定されたとは限らない点にご注意ください。
この辺りは必要であれば後述するValidateFunc
フィールドを用いてバリデーションを行う事で対応可能です。
Computed
の動作確認
次にComputed
な項目について確認してみます。
tfファイルで必須項目であるrequired
項目に値を設定してみましょう。
resource minimum_inout "inout" { required = "required" }
apply
するとcomputed
とoptional_computed
に現在日時が値として設定されているはずです。
$ terraform apply # ... Apply complete! Resources: 1 added, 0 changed, 0 destroyed. # Stateを確認 $ terraform show minimum_inout.inout: id = 596549384 computed = 2018-09-17T20:49:49+09:00 #現在日時が設定されている optional = optional_computed = 2018-09-17T20:49:49+09:00 #現在日時が設定されている required = required
なお、Computed: true
のみの項目にtfファイル上で値を設定しようとすると当然ながらエラーになります。
# !正しくないtfファイルの例! resource minimum_inout "inout" { required = "required" // Computed: trueのみの項目には値の指定は出来ない computed = "computed" }
$ terraform apply Error: minimum_inout.inout: "computed": this field cannot be set
Optional
+Computed
の動作確認
次にtfファイル上でoptional_computed
に値を設定してみます。
resource minimum_inout "inout" { required = "required" optional_computed = "optional_computed" }
apply
すると今度はoptional_computed
に現在日時ではなくtfファイルで指定した値が設定されているはずです。
$ terraform apply # ... Apply complete! Resources: 0 added, 1 changed, 0 destroyed. # Stateを確認 $ terraform show minimum_inout.inout: id = 596549384 computed = 2018-09-17T20:54:06+09:00 optional = optional_computed = optional_computed #tfファイルで設定した値が反映された required = required
Optional
+Computed
の使い所
なぜこんな項目が指定するのか疑問に思う方もいらっしゃるかもしれません。
これは、指定しなければプラットフォーム側が自動で割り当ててくれる、といった項目のためのものです。
例えばAWSのEC2におけるAZ(アベイラビリティゾーン)などが該当します。
参考: AWS: EC2リソースでのスキーマ定義(AZ部分抜粋)
func resourceAwsInstance() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ // ... "availability_zone": { Type: schema.TypeString, Optional: true, Computed: true, ForceNew: true, }, // ... }, } }
入力専用の項目
スキーマ定義をしてResourceData.Set()
を呼ばない項目は入力専用の項目となります。
この場合、tfファイルからの入力でStateには保存されますが、ResourceData.Set()
が呼ばれないので実リソースの値が反映される事がないということです。
この場合でもtfファイルを変更すればTerraformが変更を検知してくれますので実リソースへの反映は可能です。
使いどころとしては実リソースから値が取得できないような項目、例えばパスワードといった項目があります。
入力専用の項目はソースコードやtfschemaといったツールでスキーマ定義を確認しただけではそうと判断できないので注意が必要です。
また、入力専用の項目は実リソースの値がStateに反映されないため、実リソースを(コントロールパネルなどを利用して)直接変更しても検知できない問題があります。
このため、実リソースの状態とtfファイルやterraform.tfstateの内容が食い違う可能性があります。
これを避けるにはTerraformで管理している実リソースについてはTerraform以外の手段での変更をなるべく避けるなど、運用面での工夫も必要となります。
これでschema.Schema
の最低限の設定項目を押さえました。
残りのフィールドについても見ていきましょう。
なお、ここからはサンプルを別ブランチに格納しています。
schema
ブランチをチェックアウトしてビルドしておいてください。
# チェックアウト $ cd $GOPATH/github.com/yamamoto-febc/terraform-provider-minimum $ git fetch -a $ git checkout schema # ビルド $ dep ensure $ go build -o terraform-provider-minimum $ terraform init
デフォルト値(Default
/DefaultFunc
)
その名の通り未指定の場合のデフォルト値を指定します。
Default
には値を直接指定し、DefaultFunc
にはデフォルト値を返すfuncを指定します。funcのシグニチャはfunc() (interface{}, error)
です。
なぜか両方指定することも可能ですが、両方指定した場合はDefault
が優先されます。
該当部分のソース: https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L257-L273
それぞれを詳しく見ていきましょう。
Default
/DefaultFunc
で共通の部分
まず両者で共通しているのは以下の点です。
Computed: true
の場合は指定できないTypeList
またはTypeSet
の場合は指定できない
基本的にOptional: true
なプリミティブ型の項目に指定しますが、DefaultFunc
については例外的にRequired: true
でも指定できます。
DefaultFunc
のみRequired: true
でも設定できる
DefaultFunc
は値の決定時に指定のfuncを実行する事で動的に値を決定するものです。
これを利用して、「APIキーなどのtfファイルに直接記載して欲しく無いけど必要な情報」を環境変数から取得する、といった使い方ができます。
サンプルで動きを確認してみましょう。
今回はminimum
プロバイダーにschema
リソースというschema.Schema
の様々なフィールドを試すためのリソースを用意しました。
Required: true
かつDefaultFunc
を設定しているdefault_func_required
という項目を以下のように定義しています。
func resourceMinimumSchema() *schema.Resource { return &schema.Resource{ // ... Schema: map[string]*schema.Schema{ // ... "default_func_required": { Type: schema.TypeString, Required: true, DefaultFunc: schema.EnvDefaultFunc("MINIMUM_DEFAULT", nil), }, // ... }, } }
DefaultFunc
にschemaパッケージで提供されているヘルパー関数EnvDefaultFunc
というfuncを指定しています。
これは与えられた環境変数名(第1引数)の値を参照し、設定されていればそれを、なければ第2引数(ここではnil)の値を返してくれるものです。
default_func_required
はRequired: true
なので指定しないとterraform apply
時にエラーになります。
tfファイルまたは環境変数MINIMUM_DEFAULT
で値を指定するとterraform apply
出来るようになります。
DefaultFunc
で利用できるヘルパー関数
先ほども利用していましたが、schemaパッケージではDefaultFunc
で利用できるヘルパー関数を提供しています。
EnvDefaultFunc
: 前述の通りMultiEnvDefaultFunc
: シグニチャはfunc ([]string, interface{}) SchemaDefaultFunc
第1引数が配列になっており、環境変数名を複数指定可能。第2引数はEnvDefaultFunc
と同様
Default
/DefaultFunc
の定義を変更する際の注意点
Default
に指定する値、またはDefaultFunc
で返す値を変更するのはbreaking-changeになります。
例えばリソースが以下のように定義されているとします。
Schema: map[string]*schema.Schema{ // ... "value": { Type: schema.TypeString, Optional: true, Default: "before", }, // ... },
デフォルト値が設定されているため、tfファイルにvalue
項目は指定しなくてもよいですよね。
resource dummy_resource { name = "foobar" # value = "" # valueは指定せずデフォルトのまま }
この状態でプロバイダーのバージョンアップを行いデフォルト値を変更したとします。
Schema: map[string]*schema.Schema{ // ... "value": { Type: schema.TypeString, Optional: true, Default: "after", }, // ... },
すると、これまでこのリソースを利用していた場合はプロバイダーのバージョンアップをしただけで影響を受けてしまいます。
特に後述するForceNew: true
(変更時に削除/作成が行われる)な項目の場合だと影響が大きくなる可能性があります。
このため、出来るだけ変更しないで済むような値を設定する事が望ましいです。
とはいえどうしても変更を避けられない場合もあります。
そのような場合はRefresh
処理(第2回を参照)で呼ばれるRead
の中で適切にResourceData.Set()
する、
または次回以降に扱うスキーマのマイグレーション処理を行なって適切な値を設定するといった対応が必要となります。
値変更/保存時の挙動(DiffSuppressFunc
/ForceNew
/StateFunc
)
次に値の変更や保存時の挙動を指定するためのフィールドです。
差分の検出を抑制するDiffSuppressFunc
Terraformはplan
やapply
実行時に実リソースとtfファイルでの指定の差分を検出してくれます。
通常はデフォルトの差分検出を利用すれば良いですが、中には差分を無視したいケースもあります。
例えばtfファイルと実リソースで表記方法が違うだけで意味合いが同じといった場合などです。 具体的な用途としては
- 大文字/小文字の違いを無視する
- SSH公開鍵で末尾の改行有無を無視する
といったものがあります。
型はschema.SchemaDiffSuppressFunc
となっており、シグニチャはfunc(key, old, new string, d *ResourceData) bool
です。
第1〜3引数は項目のキー、変更前の値、変更後の値となっています。
第4引数はCRUD操作時に渡されるものと同じ*ResourceData
です。
同じとみなす場合はtrueを、差分ありとみなす場合はfalseを返すようにします。
例として、大文字/小文字の違いを無視したい場合は以下のようにします。
Schema: map[string]*schema.Schema{ // ... "diff_suppress_func": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: func(key, old, new string, d *ResourceData) bool { return strings.ToLower(old) == strings.ToLower(new) // 小文字に変換して比較 }, }, // ... },
なお、DiffSuppressFunc
はスキーマ内の項目単位で制御しますが、リソース全体で制御するためのものとしてschema.Resource
にCustomizeDiff
というフィールドもあります。
こちらについては次回以降の記事で扱います。
変更されたらリソースを削除-作成するForceNew
ForceNew: true
にすると変更後のapply
実行時にリソースのUpdate
ではなく、Destroy
してCreate
するようになります。
これは作成時のみ指定が可能で以後変更できないような項目などで利用します。
また、第2回で触れた、Update
を省略する時の条件にも利用されます。
Update
を省略するにはComputed: true
でない項目全てにForceNew: true
をつけておく必要があります。
Update
を省略 = 更新できない = 更新時は削除->作成が必要なリソース、ということですね。
乱用すると思わぬところでリソースの削除が行われる原因となりえますので適度で適切な利用を心がけましょう。
Stateへの格納の際に呼ばれるフックStateFunc
Stateに値を格納する時にフックを通じて値の加工を行う事ができます。
例えば長い文字列をStateに格納する代わりにハッシュ値を保存するといった使い方です。
以下のように利用します。
Schema: map[string]*schema.Schema{ // ... "state_func": { Type: schema.TypeString, Optional: true, StateFunc: func(value interface{}) string { return hash(value.(string)) // 値のハッシュ値を返す }, }, // ... }, // ... func hash(v string) string { return fmt.Sprintf("%x", sha256.Sum256([]byte(v))) }
StateFunc
はResourceData.Set()
を呼んだ際には利用されないことに注意が必要です。
ResourceData.Set()
を呼ぶ際はStateFunc
と同様の処理を自分で行う必要があります。
d.Set("state_func", hash(value.StateFunc)) // StateFuncと同様の処理
結構面倒ですね。なのでStateFunc
は基本的にアップロードするファイルのコンテンツといった入力専用(ResourceData.Set()
をしない)項目に対して用いられる事が多いです。
本来パスワードなどの難読化といった用途でも利用できるはずなのですが、ざっとTerraform Providers配下を探してみても見当たりませんでした(よく探せばあるのかも?)。
パスワードなどのセンシティブなデータの扱いについては次のSensitive
も参照してください。
センシティブなデータをマスクするSensitive
Sensitive: true
にすると、ログや標準出力への出力時に値がマスクされるようになります。
マスクされている様子
$ terraform apply + minimum_schema.schema_example id: <computed> sensitive: <sensitive>
注意点として、Sensitive: true
にしてもStateに保存される際にはマスクされません。
例えばバックエンドがローカルの場合に作成されるterraform.tfstate
ファイルには生の値がそのまま保存されます。
また、Outputとして利用する場合にも同様にマスクされません。
(Output側で改めてSensitive指定すればマスクされます。参考:https://www.terraform.io/docs/configuration/outputs.html)
一応ソースコードのコメントにはFuture versions of Terraform may encrypt these values
とありますので、今後対応されるのかもしれません。
https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L195-L199
バリデーション(ValidateFunc
/ConflictsWith
)
項目のバリデーションで利用するフィールドです。
入力値のバリデーションを行うValidateFunc
入力値に対するバリデーションを行うためのfuncを指定可能です。
型はschema.SchemaValidateFunc
で、シグニチャはfunc(value interface{},key string) (warns []string,errs []error)
です。
以下は入力された文字列の長さが1から5の間でなければエラーとする例です。
Schema: map[string]*schema.Schema{ // ... "validate_func": { Type: schema.TypeMap, Optional: true, // ValidateFunc: validation.IntBetween(1, 5)と同じ ValidateFunc: func(value interface{}, k string) (ws []string, es []error) { v , ok := value.(string) if !ok { es = append(es, fmt.Errorf("expected type of %s to be string", k)) return } if !(1 <= len(v) && len(v) <= 5) { es = append(es, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, 1, 5, v)) } return }, }, // ... },
引数に項目の値とキーが渡されますのでそれらを利用してバリデーションを行います。 戻り値は警告とエラーを配列です。バリデーションの結果をそれぞれに格納してください。
組み込みのバリデーション関数
github.com/hashicorp/terraform/helper/validation
パッケージにはValidateFunc
で利用できる関数が用意されています。
なるべくこれらを利用し、不足する機能についてのみ自前実装するようにしましょう。
先ほどのvalidate_func
の例であればvalidation.IntBetween(1, 5)
のように利用します。
ValidateFunc
の注意点
現在はTypeList
とTypeSet
は非対応です。
https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/schema.go#L192
これらのバリデーションを行いたい場合はElem
で複合型を指定してそちらにValidateFunc
を書く(もちろんプリミティブ型のみ)か、
Create
/Update
などの中で自分でバリデーションを実装するかしないといけません。
コメントには
ValidateFunc currently only works for primitive types
と書いてますが実はTypeMap
はバリデーションできたりします。
複数の項目の同時指定を防ぐConflictsWith
ConflictsWith
は同時に指定できない項目の名称を文字列の配列で指定します。
Schema: map[string]*schema.Schema{ // ... "conflicts_with1": { Type: schema.TypeString, Optional: true, ConflictsWith: []string{"conflicts_with2"}, }, "conflicts_with2": { Type: schema.TypeString, Optional: true, ConflictsWith: []string{"conflicts_with1"}, }, // ... },
両方を指定すると以下のようなエラーメッセージを出してくれます。
$ terraform validate Error: minimum_schema.schema_example: "conflicts_with1": conflicts with conflicts_with2 Error: minimum_schema.schema_example: "conflicts_with2": conflicts with conflicts_with1
警告/エラーメッセージ(Deprecated
/Removed
)
プロバイダーのバージョンアップなどでスキーマ定義が変更になり利用しなくなる/なった項目が発生した場合に利用します。
どちらも文字列を指定しておくと、その項目を利用している場合に警告/エラーを表示してくれます。
なお、Removed
の場合はterraform
コマンド自体を異常終了してくれます。
Schema: map[string]*schema.Schema{ // ... "deprecated": { Type: schema.TypeString, Optional: true, Deprecated: "deprecated", }, "removed": { Type: schema.TypeString, Optional: true, Removed: "removed", }, // ... },
これらを指定していると以下のような警告/エラーメッセージを出してくれます。
$ terraform validate Warning: minimum_schema.schema_example: "deprecated": [DEPRECATED] deprecated Error: minimum_schema.schema_example: "removed": [REMOVED] removed
なお、両者ともOptional: true
でデフォルト値なしにしておかないと常に警告/エラーが出ますのでご注意ください。
第4回 まとめ
第4回ではスキーマ定義に利用するschena.Schema
型のフィールドについて扱いました。
次回はスキーマを持つリソースの操作を行うためのschema.ResourceData
について見ていきます。
以上です。
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件) を見る