Terraform Provider実装 入門(1): Custom Providerの基礎

今回はTerraformから提供されているprovider frameworkを利用した独自のプロバイダーの実装について扱います。
カスタムプロバイダーについての基本的な知識〜実装上の注意点などをサンプル実装を通じて見ていきます。

注:この記事はTerraform v0.11に対応しています。

=== UPDATE: 2022/05
この記事執筆当時と筆者の状況が変わり、この一連の記事は完結する見込みがほぼ無くなっています。
また、Terraform側も進化しており、Terraform SDKが生まれ、SDK v2に変わり、現在ではTerraform Plugin Frameworkというものが生まれています。

この一連の記事の内容はSDK v2でも若干の調整(エラーハンドリングなど)だけで通用しますが、Plugin Frameworkについては全く新しい作りとなっていること、新規にプロバイダーを作成する場合はPlugin Frameworkの利用を検討するように公式ドキュメントに書かれていますので、これらの点に留意しつつお読みください。

www.terraform.io

=== UPDATE ここまで

はじめに

この記事は主にカスタムプロバイダーの実装をする方やAWS/GCP/Azureなどの既存のプロバイダーで発生した問題の解決のためにソースを読む/修正するといった方向けです。
このため、Terraform自体についての説明はかなり省いています。
とはいえ必要に応じて触れますので最低限以下の2点を押さえておけばOKです。

  • Terraformの基本的なコマンド(init/apply/destroyなど)の利用方法について
  • tfファイルの基本的な書き方について

Terraformを利用したことがある方であれば問題ないでしょう。

また、この記事は以下のような内容には触れず、より実装寄りの内容を中心とします。

  • どんな場合にカスタムプロバイダーが必要なのか
  • リソース提供側はどのようなAPIがあると望ましいのか

ということで早速本題に入っていきます。

目次(未確定)

1ポストでは収まらない量になりそうなので複数回に分けて投稿します。
今の所は以下のような感じになる予定です。(確定したら目次を更新します)

Terraform Custom Providerの基本

まずは前提知識としてTerraformでのプロバイダーの扱いについて押さえておきます。

前提知識: Terraformでのプロバイダーの扱い

Terraformはコアな処理を担当するTerraform本体とAWS/GCP/Azureといった各プラットフォームに依存した処理を担当するプロバイダーとで実行ファイルが分離されています。
Terraform本体は必要に応じてプロバイダーの実行ファイルを見つけだしてRPCを行います。

Terraform v0.11の時点ではRPCにnet/rpcが使われています。 v0.12以降ではgRPCに切り替える方向に進んでいます。 参考: v0.12でのprotoファイル

プロバイダーの実行ファイルの探し方についてはこの記事の本筋から外れるため省略します。 細かい例外はありますが、ひとまず以下のルールを覚えておけばOKです。

  • HashiCorp社が配布しているプロバイダーについてはterraform init時に自動でインストールされる
  • サードパーティにより配布されているプロバイダーについては以下のディレクトリから検索される

参考: https://www.terraform.io/docs/extend/how-terraform-works.html#discovery

いくつか例を通じて動きを見ておきます。

HashiCorp社により配布されているプロバイダーのインストール

この例はHashiCorp社により配布されているArukasプロバイダーをインストールする例です。

www.terraform.io

tfファイルを用意しterraform initを実行することでカレントディレクトリの.terraform/配下にプラグインがインストールされます。

# tfファイルを作成
$ echo "provider arukas{}" > test.tf

# プロバイダーをインストール
$ terraform init

# 確認
$ tree -a .

.
├── .terraform
│   └── plugins
│       └── darwin_amd64
│           ├── lock.json
│           └── terraform-provider-arukas_v1.0.0_x4
└── test.tf

なお、今回はtfファイルにproviderブロックを記載していますが、providerブロックは省略可能です。(プロバイダーの実装によります)
以下のようにいきなりリソース定義のみを書く形でもterraform init実行時にtfファイルが解析され必要なプロバイダーの検出が行われます。

resource "arukas_container" "foobar" {
  name      = "example"
  image     = "nginx:latest"
  ports = {
    protocol = "tcp"
    number   = "80"
  }
}

サードパーティにより配布されているプロバイダーのインストール

次にサードパーティにより配布されているプロバイダーをインストールする例です。
Terraform v0.11時点ではサードパーティプロバイダーを自動でインストールする仕組みはまだありません。
なので、自分でプロバイダーの実行ファイルをダウンロードし、前述の検索対象ディレクトリに格納しておく必要があります。

以下はさくらのクラウド向けプロバイダーをダウンロードして利用する例です。 ここではプロバイダーの実行ファイルは~/.terraform.d/plugins配下に格納します。

# プロバイダーの実行ファイルをダウンロード
$ curl -sL -o provider.zip https://github.com/sacloud/terraform-provider-sakuracloud/releases/download/v1.6.1/terraform-provider-sakuracloud_1.6.1_darwin-amd64.zip

# 展開して配置
$ unzip provider.zip ; rm provider.zip
$ mv terraform-provider-sakuracloud_v1.6.1_x4 ~/.terraform.d/plugins/

# tfファイルを作成
$ echo "provider sakuracloud{}" > test.tf

# プロバイダーをインストール
$ terraform init

# 確認
$ tree -a .

.
├── .terraform
│   └── plugins
│       └── darwin_amd64
│           └── lock.json
└── test.tf

先ほどと違い、.terraformディレクトリ配下にプラグインの実行ファイルが格納されないことに注意してください。

当記事ではサンプルのカスタムプロバイダーを作成し実際に動かして動作を確認していきます。
その際にこちらの方法を用いてカスタムプロバイダーをインストールし利用するようにします。

Custom Providerの実装

それでは本題のカスタムプロバイダーの実装について見ていきます。

最初に、公式ドキュメントのガイドの中にWriting Custom Providersというドキュメントが 用意されていますので目を通しておきます。

www.terraform.io

このドキュメントによると、カスタムプロバイダーを作成するには最低限以下が必要ということです。

  • (1) terraform.ResourceProviderを返すfunc
  • (2) plugin.Serve()に(1)のfuncを指定して呼び出すエントリーポイント
  • (3) terraform-provider-<プロバイダー名>という名前でビルド

それぞれを詳しく見ていきます。実際に手を動かしながら確認出来るようにソース一式を以下に準備しました。

github.com

サンプルとしてterraform-provider-minimumというカスタムプロバイダーを作成してみます。 以降はこのサンプルを元に各項目を見ていきます。

カスタムプロバイダーのサンプルのソースコードを取得

まずはサンプルをローカルマシンにクローンし、strucrureブランチをチェックアウトしてください。

# *GOPATHを設定していない場合は$HOME/goに読み替えてください*

# ソース一式を取得 & structureブランチのチェックアウト
$ go get github.com/yamamoto-febc/terraform-provider-minimum
$ cd $GOPATH/src/github.com/yamamoto-febc/terraform-provider-minimum
$ git checkout structure

以下のようなファイルが含まれているはずです。

$ tree .
 .
├── Gopkg.lock
├── Gopkg.toml
├── main.go
└── minimum
    └── provider.go

それでは順番に見ていきます。

(1) terraform.ResourceProviderを返すfunc

まずは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{
        ResourcesMap: map[string]*schema.Resource{
            // ここにリソースの定義を書いていく
        },
    }
}

ここではterraform.ResourceProviderを返すfuncProvider()を定義しています。terraform.ResourceProviderはinterfaceです。

https://github.com/hashicorp/terraform/blob/v0.11.8/terraform/resource_provider.go#L19

このインターフェースを実装することでTerraform本体とのやり取りが可能になります。
実際にはこのインターフェースを実装したstructschema.Providerが用意されていますので、これを利用します。

https://github.com/hashicorp/terraform/blob/v0.11.8/helper/schema/provider.go#L25

現在はリソースの定義は空になっています。次回記事でここに定義を追記していきます。

(2) plugin.Serve()に(1)のfuncを指定して呼び出すエントリーポイント

次にエントリーポイントであるmain.goを見てみます。

package main

import (
    "github.com/hashicorp/terraform/plugin"
    "github.com/yamamoto-febc/terraform-provider-minimum/minimum"
)

func main() {
    plugin.Serve(&plugin.ServeOpts{
        ProviderFunc: minimum.Provider,
    })
}

plugin.Serveに引数として(1)で定義したProvider()を指定しています。
plugin.Serve()を実行することで、Terraform本体とRPCでやり取りするための諸々を行ってくれます。
これによりカスタムプロバイダーの作成者はRPCについて意識することなくそれぞれが担当するリソースの操作に集中できます。

(3) terraform-provider-<プロバイダー名>という名前でビルド

あとはgo buildコマンドでビルドするだけです。Terraformは前提知識の項で触れたようにカスタムプロバイダーを検出してくれる仕組みがありますが、
そのためにはカスタムプロバイダーの実行ファイルを特定のディレクトリに配置することに加え、 実行ファイルの名前をterraform-provider-<プロバイダー名>という形式にしておく必要があります。

このため、ビルド時に-oオプションで出力されるファイル名を指定する必要があります。 また、ビルドのために依存ライブラリを用意しておく必要があります。

今回はdepで依存関係を管理するようにしておきましたので、dep ensureを実行してからビルドするようにしてください。

# depがない場合は以下でインストール
# go get -u github.com/golang/dep/cmd/dep

$ dep ensure
$ go build -o terraform-provider-minimum main.go

これでカレントディレクトリにterraform-provider-minimumという実行ファイルが作成されているはずです。

余談1: カスタムプロバイダーでの依存ライブラリの管理方法

Terraform本体はgovendorを利用して依存ライブラリを管理していますが、 本体とカスタムプロバイダーは実行ファイルが分かれているためビルドさえできれば無理にgovendorを利用する必要はありません。

terraform-providers配下のプロバイダーにおいても統一されてない状況で、 AWS/GCP/AzureRMといったプロバイダーはgovendorを利用していますが、 Herokuプロバイダーなどではdepが使われています。

動作確認

ビルドしたカスタムプロバイダーが動作するか確認します。

実際にtfファイルを作成しterraform initを実行してみましょう。 terraform versionでminimumプロバイダーが表示されていればOKです。

# tfファイルを作成
$ echo "provider minimum{}" > test.tf

# プロバイダーをインストール
$ terraform init

# 確認
$ terraform version

Terraform v0.11.8
+ provider.minimum (unversioned)

余談2: プロバイダーのバージョンについて

terraform versionを実行するとminimumプロバイダーのバージョンがunversionedになっているのに気づかれたかもしれません。
実はプロバイダーの実装としてはバージョン情報を持っておらず、プロバイダーのファイル名からバージョン情報を取得しています。

先ほどArukasプロバイダーを自動でインストールする例が出てきましたが、そこではプロバイダーのファイル名は以下のようになっていました。

terraform-provider-arukas_v1.0.0_x4

プロバイダーのファイル名は正式には以下の形式を持ちます。

terraform-<プラグインのタイプ>-<名称>_v<プラグインのバージョン>_x<プラグインプロトコルのバージョン>

リリース時のバージョニング処理

HashiCorp社が配布しているプロバイダーの場合はリリース時にCIサーバによってバージョンが決定されファイル名が付与されます。
(CIにはTeamCityが利用されており、CI/CDパイプラインの中でCHANGELOG.mdの解析をしてリリースするバージョンを決定しています)

サードパーティのプロバイダーについてはこの辺の処理を自前で行う必要があります。
参考までに、さくらのクラウド向けプロバイダーではこの辺の処理を行なっていますので興味のある方はMakefileあたりから眺めてみてください。

余談3: ソースコードのレイアウト/パッケージ構成について

公式ガイドのWriting Custom Providersexampleプロバイダーとminimumプロバイダーとで ソースコードのレイアウトが違うことにお気付きの方もいらっしゃるかと思いますが、これはterraform-providersでのレイアウトに合わせているからです。

公式ガイドでのサンプルexampleプロバイダーのソースコードレイアウト
.
├── main.go
└── provider.go
minimumプロバイダーのソースコードレイアウト
 .
├── main.go
└── minimum          # プロバイダー固有コードを格納するパッケージ(リソースを追加する際もここに追加する)
    └── provider.go

公開予定の無い自作プロバイダーであればどちらを選んでも良いですが、特に理由がなければ後者のように別途パッケージを設けて その中にプロバイダー固有のコードを置き、ルートディレクトリにはmain.goだけおく形にしておくのが良さそうです。


ここまででカスタムプロバイダーとしての最低限の体裁が整いました。
しかし、リソースの実装が空ですので現時点では特に何もできない状態です。
実装は次回行います。

第1回 まとめ

今回はカスタムプロバイダーの基礎知識をおさえ、カスタムプロバイダーとしての最小限のコードを書いた上でビルドしてTerraformから利用してみました。
今回はリソースの実装が空のためtfファイルを書く機会もなかったですが、次回の記事でリソース実装を追加していくことでよりプロバイダーらしくしていきます。

以上です。

Next: リソース実装 基礎 - schema.Resourceでのリソース実装の基礎

[asin:B06XKHGJHP:detail]