おやかたの話
tatsuo48
tatsuo48
YokoyamaTatsuo

HameeのSRE, 日々の諸々を書きます。

Terraform Providerのつくり方


はじめに

普段の業務の中ではTerraformでインフラリソースを管理することが多い。
今回、SendGridを新規に利用するにあたり、IPアクセス制限をTerraformで管理したいなぁと思ったが、自分の用途に合ったProviderが見つからなかった。
既存のProviderにPRを送ってもよかったが、TerraformerとしてProviderを一から作る経験をしてみねばと思い、勢いで作った。

出来上がったProviderがこちら。

tatsuo48/sendgrid

目的通り、IPアクセス制限部分の管理だけができる状態となっている。
以下では、具体的な作り方について書いていく。

リソースの定義を決める

まず初めに決めるべきなのは何をリソースとして管理するのか?ということだ。
例えば、今回作成したProviderのケースだとアクセス許可IP1つずつを1リソースとして管理する形をとっている。

resource "sendgrid_whitelist_ip" "first" {
  ip = "192.168.0.1/32"
}

これは、例えば以下のようにIPリスト全体で1リソースという捉え方もあったかもしれない。

resource "sendgrid_whitelist_ips" "first" {
  ips = [
    "192.168.0.1/32"
    "192.168.0.2/32"
  ]
}

利用するAPI定義や実運用のことを考えて適切なリソース定義を作るようにする必要がある。
今回のケースだと、SendGridのAPIがそもそもアクセス許可IPを1つずつ管理する形をとっているため、前者を選択している。

CRUDの実装

リソース定義が決まったら、実際のリソースに対するCRUDの実装を行なっていく。

以下のレポジトリのsendgrid/resource_whitelist_ip.goresourceWhitelistIPのように*schema.Resourceを返す関数を定義する。 *schema.ResourceにはリソースのスキーマとCRUD操作を行う関数を定義していく。

tatsuo48/terraform-provider-sendgrid

リソースのスキーマはAPIの戻り値から必要なものだけ定義する。
次に、CRUD操作を行う関数について説明したいのだが、Terraformのリソース周りは言葉がややこしいのでここで言葉を整理しておく。

  • tfファイルに書かれたリソース構成 = リソース構成
こういうやつ
resource "sendgrid_whitelist_ip" "first" {
  ip = "192.168.0.1/32"
}
  • tfstateファイルに記録されるリソース状態 = リソース状態
こういうやつ
{
  "mode": "managed",
  "type": "sendgrid_whitelist_ip",
  "name": "first",
  "provider": "provider[\"hashicorp.com/tatsuo48/sendgrid\"]",
  "instances": [
    {
      "schema_version": 0,
      "attributes": {
        "created_at": 1601898379,
        "id": "1948555",
        "ip": "127.0.0.1/32",
        "last_updated": null,
        "rule_id": 1948555,
        "updated_at": 1601898379
      },
      "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
    }
  ]
}
  • API操作される実際のリソース = 実リソース

では、CRUD操作を行う関数の説明に入る。 それぞれ以下を行なっている。

  • C(reate)

    • リソース構成を元にAPIリクエストを行い、実リソースを作成
    • 作成した実リソースを一意に特定できるIDをリソース状態に記録
    • R(ead)を実行
  • R(ead)

    • リソース状態に記録されたIDを元にAPIリクエストを行い、実リソースの情報を取得
    • 取得した実リソースの情報をリソース状態に記録
  • U(pdate)

    • リソース状態に記録されたIDを元にAPIリクエストを行い、実リソースを更新
    • R(ead)を実行
  • D(elete)

    • リソース状態に記録されたIDを元にAPIリクエストを行い、実リソースを削除
    • リソース状態のIDを空にすることで、リソース状態からも削除

これらはterraformの以下のコマンドで次のように利用されている。

  • terraform apply

    • リソース状態を元にR(ead)を実施、リソース状態を実リソースの状態に合わせる
    • リソース状態に存在しない実リソースはC(reate)を実施
    • リソース状態に存在するリソースは、リソース構成と差分がある場合、U(pdate)を実施
    • リソースのスキーマでForceNew: trueが設定されている場合は、U(pdate)ではなく、D(elete)してC(reate)となる。
  • terraform destroy

    • リソース状態を元にR(ead)を実施、リソース状態を実リソースの状態に合わせる
    • リソース状態を元にD(elete)を実施

U(pdate)に関しては、API側でそもそもUpdateできる作りになっていないため、関数を定義しない場合もある。 今回はそのケースだったので*schema.ResourceUpdateContextが存在しない。

最後に、定義した関数(resourceWhitelistIP)をsendgrid/provider.goProvider関数の戻り値である*schema.ProviderResourcesMapに設定する。 こうすることで、Provider読み込み時にこのリソース定義が利用可能になる。

認証情報の渡し方

API利用に必要な認証情報の設定はsendgrid/provider.goProvider関数の戻り値である*schema.Providerに設定する、ConfigureContextFuncで行う。 この関数で返した値が各リソースのCRUD操作を行う関数に渡される。

func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
	apikey := d.Get("api_key").(string)
	var diags diag.Diagnostics
	if apikey == "" {
		return nil, diag.Errorf("api key is not set, please see this document https://registry.terraform.io/providers/tatsuo48/sendgrid/latest/docs#authentication")
	}
	return apikey, diags
}
func resourceWhitelistIPCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
    apiKey := m.(string) // これがproviderConfigureで返したapikeyとなる。

認証情報は以下のようにProvider関数で設定すると、環境変数SENDGRID_API_KEYがあればその値が使われ、存在しない場合は、terraform実行時に対話形式で設定するようになる。

func Provider() *schema.Provider {
	return &schema.Provider{
		Schema: map[string]*schema.Schema{
			"api_key": {
				Type:        schema.TypeString,
				Required:    true,
				Sensitive:   true,
				DefaultFunc: schema.EnvDefaultFunc("SENDGRID_API_KEY", nil),
			},
		},

テスト

テスト用のフレームワークが用意されており、それに即した形でテストを行う。

testing

基本的に実際のAPIを使った形でテストが行われるので、課金が発生するサービスの場合は注意が必要。 フレームワークの使い方については上記のドキュメントにあるのでここでは割愛させていただく。

参考資料

とても良い資料、というかこれがないとProvider作れなかったと思う。

このエントリーをはてなブックマークに追加