📱

Nature Remo Terraform Provider を作った

2023/12/08に公開

発端

Remo ExporterとPrometheusを使って、室内環境の監視をした。その際に、Nature Remoの設定値もIaCしたいと思ったのだが、意外にもTerraform Providerがこの世になかったので、Terraformへの理解を深めるついでに自分で作ることにした。

成果物

Terrafrom Registryに公開できるのは嬉しいものがある。
https://registry.terraform.io/providers/aiwasaki126/natureremo/latest/docs

GitHubにポートフォリオっぽく見えるのはいいことだ。
https://github.com/aiwasaki126/terraform-provider-natureremo

開発ロードマップ

  1. HashiCorp公式のTerraform Provider作成チュートリアルを一通りやる
  2. Nature RemoのOpenAPIを見て、Terraform化できそうなresource, data sourceを探す
  3. Nature RemoのAPIクライアントライブラリを探し、とりあえず使ってみる
  4. data sourceを作る(resourceよりも作るのが簡単)
  5. resourceを作る(状態の管理が入るため多少面倒がある)
  6. APIクライアントを自作する
  7. 拡張する

当初、APIクライアントにはtenntennさんのNature Remo APIクライアントを使わせてもらっていた。しかし、このクライアントの開発後に多少OpenAPIが更新されのか、微妙に食い違う部分が出てきたりしたので、最終的に自作することになった。

APIクライアントを自作する上でだいぶ参考にさせていただいたので頭があがらない。

困難

Terraform Providerは粗く言ってしまえばAPIクライアントのラッパーなので、当然APIへリクエストを投げ、レスポンスをもらう。APIの挙動如何で作りやすさが変わってくる。

Nature Remo APIがOpenAPI通りの挙動をしない

実際にリクエストをAPIに送ると受け付けてくれないことがあった。以下はその一例。

  • どれだけ権限を与えても、権限エラーでデータの取得ができない(GET /1/homes
  • パラメータが微妙に違う(OpenAPI: tempUnit=... 実際に通るもの: temp_unit=...
  • レスポンスに、OpenAPIには書かれているフィールドがない(GET /1/users/mtemp_unitなど)

APIが許容する値がドキュメントで示されていない

例えば、Nature Remoの温度表記を変える場合、POST /1/uses/meを利用する。リクエストボディのフィールドtemp_unitに設定できる値はc, fのいずれかで、それ以外の値を投げるとエラーレスポンスになる。

前者は摂氏(Celsius)、後者は華氏(Fahrenheit)を表しているのだが、それぞれをc, fと表す、というような記述は見つからなかったので試行錯誤で突き止めた。

こういうフィールドがいくつかあり、その度に試行錯誤で受け付ける値を割り出していった。

試行錯誤で割り出した対応表はガイドに書かれている。

意外とTerraform化できるものが少ない

Nature Remoは物理的なデバイスなので、勝手にRemoの追加もできないし、ネットワークに載せるにしても実機での操作が必要になる。対応する家電によってRemo内部の設定値がだいぶ変わるようで、Remoが操作する家電の作成、更新は不可能だった。

機器の更新や削除はできても作成ができないものが多く、結局プロフィールとNature Remoの設定値くらいしかTerraform制御下におけそうになかった。

当初の見込みだと、Appliance(家電)、Homeくらいはやれるとしていたが想定が甘かった。日々使っているGCPやAWSが、いかにハードウェアを抽象化してソフトウェアに近づけているか実感する機会にもなった。

開発の概要

Providerの作り方は多くの場所で説明されているし、公式チュートリアルも分かりやすいので詳細は省く。
ここではNature Remo Providerを作る上での要点をまとめる。

HashiCorp製Plugin Frameworkは優秀だ

公式に推奨されているフレームワークを元に開発をすると、自分で書くコードをだいぶ減らせる。
APIクライアント開発以外はこのフレームワークに乗っかればいいので、楽をできる。

クライアント作成はスキーマ駆動で

tenntennさんのNature Remo APIクライアントから自作のAPIクライアントに乗り換える際に、APIクライアントの生成はoapi-codegenを利用した。

前述のようにAPIがOpenAPI通りの挙動を示さないので、どこかに見つけ出した実際の仕様を残しておく必要がある。加えて、それをクライアントに反映する必要がある(そうでないとリクエストが通らない)。

実際の仕様をOpenAPIの形で残しつつ、そこからクライアントコードのベースを生成すれば、仕様が散らばることもないし、仕様とコードの乖離が減る。開発は徐々に進んでいくので実際の仕様が徐々に判明していく側面があり、随時OpenAPIを更新してクライアントコードに新たに分かった事実を反映できるのが便利だった。

クライアントはドメイン駆動設計を意識

internal/client/以下のディレクトリ構成を見れば雰囲気は掴めるのではないかと思う。

Home(Nature Remoを置いている家のこと)がDevice(Nature Remoのこと)を持ちDeviceが複数のAppliance(家電のこと)を管理する、というように、Nature Remoの管理は階層的で、リソースの使い回しがされることが分かっていた。

逆に考えると、これらのオブジェクトが絡み合ってNature Remoの世界観を形成している。つまり数種類のメインオブジェクトをおさえればクライアントのユースケースを網羅できそうだったので、ドメイン駆動で構成した。

実際のところ、前述の通りあまりTerraform化できるものがなかったので、やり過ぎだったかもしれない。今後のAPI利用範囲が解放されたら威力を発揮することを願う。

利用例

現実問題としてNature Remoのリソース(ユーザーのプロフィールなど)は大抵の場合、Createができないので、最初はNature Remoのアプリなどでリソースを作成し、そのリソースをimportすることで使う。

importする場合、そのリソースのIDを把握するために最初にデータソースからIDを把握する必要があり、面倒なステップを踏む必要がある。
以下に、Nature RemoデバイスをTF制御下におくやり方を例示する。プロフィールのやり方はGetting Startedに書いた。

  1. https://home.nature.global/ からアクセストークンを取得する。

  2. 下記のようにTF環境変数を定義する。

    terraform.tfvars
    access_token="XXX"
    
  3. データソースを定義する。

    main.tf
    terraform {
      required_providers {
        natureremo = {
          source = "registry.terraform.io/hashicorp/natureremo"
        }
      }
    }
    
    provider "natureremo" {
      access_token = var.access_token
    }
    
    variable "access_token" {
      type      = string
      sensitive = true
    }
    
    data "natureremo_devices" "mine" {}
    
    output "my_devices" {
      value = data.natureremo_devices.mine
    }
    
  4. terraform initで初期化する。

  5. terraform planでコンソールにdevice IDが表示されるので、それを記録しておく。

  6. リソースの記述を追加する。

    main.tf
    + resource "natureremo_device" "mine" {
    +  name               = "Remo"
    +  temperature_offset = 0
    +  humidity_offset    = 0
    +}
    
  7. 記録したdevice IDを元にリソースのimportをする。

    terraform import resource.natureremo_device.mine $DEVICE_ID
    
  8. 不要であればデータソースの記述を消す。

Discussion