📚

ゼロから始めるTerraform Custom Provider

2024/09/04に公開

日々データカタログを運用されている皆様、お疲れ様です。
データカタログの運用大変じゃないですか?
私もOpenMetadata(OSSのカタログ製品)の試験導入が始まった段階で、今後の運用面をどうにかしないと・・・と漠然と思ってました。
特に日々の運用で発生しそうな次のユースケースはなるべく自動化/省力化したいと思っていました。

  • ユーザ管理(作成/更新/削除)
  • Database Servicesの管理(追加/更新)
  • Termの管理(追加/更新/削除)
  • TestCase周りの管理(作成/更新/削除)
  • Policy/Roleの管理
  • Classificationの管理

などなど・・・・

これらの作業を手でやるには限界があり、依頼がある度に開発の手を止められてしまい、チームとしての運用開発スピードに妨げにもなります。
さらに、手動はオペミスの温床にもなるので絶対に避けたい・・・

なので、自動化/省力化のために、OpenMetadata上のリソースは全てTerraformで管理する方向で検討を始めました。
幸いなことに、SnowflakeやAWSでもTerraformを採用しているので、導入障壁はかなり低いのが嬉しいポイント!!

ただし、OpenMetadataはTerraformのプロバイダー一覧に存在しない・・・???

プロバイダー一覧でOpenMetadata検索してもヒットしない。。。
私の探し方が悪いのか???でもネットで検索してもそれらしい記事が出てこないので、プロバイダー無いのかも??
https://registry.terraform.io/browse/providers

無ければ作れば良いんだよの精神

ということで、タイトルにもある通り、OpenMetadataのCustom Providerを作ることにしました。

はい。前置きが長くなりましたが、この記事はTerraformちょっと触ったことのある私がノリと勢いでCustom Providerを作ってみたお話です。
基本的に以下のチュートリアルを実践した後に、OpenMetadata用のカスタムプロバイダーを作りました。
https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework/providers-plugin-framework-provider

1. Custom Providerの仕組みについて

TerraformはTerraform CoreTerraform Providerにより構成されています。

1-1. Terraform Core

Terraform CoreはGo言語で書かれた静的にコンパイルされたバイナリです。
コンパイルされたバイナリはコマンドラインツール(CLI)であるterraformであり、Terraformを利用するためのエントリーポイントとして動作します。
その他、Terraform Coreはリソースの現在の状態を管理し、計画ファイル(terraform.plan)や状態ファイル(terraform.state)を操作します。
これにより、Terraformはどのリソースが作成されたか、どのリソースが変更されたか、そしてどのリソースが削除されたかを追跡します。

Terraform Coreの役割

  • コードとしてのインフラストラクチャ: 構成ファイルとモジュールの読み取りと補間
  • リソース状態管理
  • リソースグラフの構築
  • 計画の実行
  • RPC 経由のプラグインとの通信

1-2. Terraform Provider

Terraform Providerは、Terraformと外部のAPIやインフラストラクチャサービス(今回の場合はOpenMetadata)との間のインターフェースを提供します。
インタフェースを経由し、インフラ上の実際のリソースの作成、更新、削除、読み取りの処理を行います。

1-3. Terraform Plugin

Terraform Pluginは、APIクライアントの初期化や、特定のサービスに対する認証の管理を行います。この初期化処理には、必要な設定や認証トークンの取得、通信プロトコルの設定などが含まれます。

Terraform Pluginの役割

  • API呼び出しを行うために使用されるすべてのライブラリの初期化
  • インフラストラクチャプロバイダーによる認証
  • 特定のサービスにマップする管理対象リソースとデータソースを定義する
  • 実務者の構成の計算ロジックを有効にしたり簡素化したりする関数を定義する

上記役割をもう少し噛み砕くと、以下の流れで動作します

  1. terraform planを実行すると、Terraform Coreはプロバイダーに対して、計画されたリソースの状態を問い合わせ、変更点を表示します
  2. プランに基づいて、プロバイダーがリソースの操作を定義します
  3. terraform applyを実行すると、プロバイダーはリソースの操作を実際に行い、その結果をTerraform Coreに返します
  4. Terraform Coreはその結果を受け取り、状態ファイルを更新します

2. Custom Providerの設計原則

ここではCustom Providerを設計する際の、原則を紹介します。
これ以降でCustom Providerを作成するのですが、その際はこの設計原則に基づいて作成します。
https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles

2-1. プロバイダーは単一のAPIまたは問題領域に焦点を当てる

Custom Providerを作成する際は、単一のAPIに焦点を当てます。
例えば、AWS ,GCP ,Kubernetes, etcなどです。
単一のAPIに絞ることで、次のような利点があります。

  • プロバイダーの接続性と認証要件の簡素化
  • 実務者にとっての発見を簡素化
  • 関連システムや依存システムを新しい革新的な方法で構成できるようにする
  • メンテナーが単一のシステムまたはエコシステムの専門家になれるようにします

では逆にバッドケースはどのようなものでしょうか?

  • 汎用APIプロバイダー
    AWS ,GCPを組み合わせた汎用的なプロバイダー
  • 複数の問題領域をカバーするプロバイダー
    ネットワークやサーバのプロビジョニングなどの領域を1つのプロバイダーでカバーする

などが考えられます。

2-2. リソースは単一のAPIオブジェクトを表す必要がある

Terraformリソースは、通常、作成、読み取り、削除、およびオプションで更新メソッドを備えた単一のコンポーネントの宣言的表現である必要があります。
Terraformのリソースは具体的な作成オブジェクトと一対一の関係性になります。
例えば、main.tfでユーザ作成のresourceを記述したなら、ユーザ作成を呼び出すエンドポイントと一対一の関係になるよう推奨しています。
単一のリソースで複数のAPIリクエストをまとめて行うような複雑な操作は、リソース自体で行うのではなく、Terraformモジュールとして構成するべきです。
モジュールは、複数のリソースを組み合わせた抽象化を提供し、それらを組み合わせて一つのまとまりとすることで、ユーザーはより簡潔に複雑なインフラストラクチャを管理できるようになります。
リソースを単一のAPIオブジェクトに絞ることで、次のような利点があります。

  • 関連コンポーネントや依存コンポーネントを新しい革新的な方法で構成できるようにする
  • 予測可能性を最大化し、書き込み/削除操作の影響範囲を最小化する
  • 複数の基盤コンポーネントを管理するメンテナーの負担を防止します
    これは、Terraform プラグイン SDK のネイティブ設計パターンではありません

2-3. リソースと属性のスキーマは、基盤となるAPIと厳密に一致している必要がある

Terraform リソースと関連スキーマは、ユーザー エクスペリエンスを低下させたり、Terraform に対するユーザーの期待に反する動作をしない限り、APIの命名と構造に従う必要があります。
つまり、APIとTerraformのSchemaで定義する構造を一致させるということです。
例えば、API側にID,Name,Emailなどのフィールドを持つならば、それはTerraformのSchemaにも定義することを推奨しています。

スキーマを一致させることで、次のような利点があります。

  • 運用チームは、Terraform 固有の用語や処理を学習することなく、同じAPIと対話する複数のツールから簡単に変換したり、それらを利用したりできるようになります。
  • Terraform 固有の抽象化と将来の API 変更によって発生する予期しない名前の衝突を防止します。
  • Terraform 固有の抽象化による運用者の負担を防ぎます。
  • 部分的または完全なコード生成を容易にします。

さらに注意事項として

  • 日付と時刻は可能な限り常にRFC 3339でモデル化されるべきである。
  • Terraform は再帰型をサポートしていません (ただし、場合によっては動的型を使用してモデル化できます)
  • ブール属性は、true が何かを実行することを意味し、false が何も実行しないことを意味するように方向付けられるべきであり、これは API を反転することを意味する場合がある。

2-4. リソースはインポート可能である必要がある

terraform importをサポートする必要があります。

利点は次のとおりです。
ユーザーは手動と自動のプロビジョニングを組み合わせることができ、ブラウンフィールド環境で運用しているユーザーはプロバイダーを引き続き活用できます。

2-5. 状態とバージョン管理を考慮する

Terraformプロバイダーをバージョン管理し、リリースの際にセマンティックバージョニングを適用することを推奨します。
作成したCustom Providerをリリースすると、ユーザーはそのプロバイダーを使用してインフラ管理を行います。
日々の運用であらゆる不具合や要望が出た際にプロバイダーの修正や更新が必要になると思います。
その際に、更新したプロバイダーが既存のTerraformの状態に影響を与えないことや、下位バージョンと互換性を保つ必要があります。
そうでなければ、修正の影響でユーザからのクレームが出ることになるでしょうし、Terraform管理にも不具合が生じます。

また、どうしても大きな変更が必要な場合は、段階的な移行をサポートすることを推奨します。
そうすることで、ユーザ側に変更に備える時間を確保できます。

2-6. 関数は単一の計算操作を表す必要がある

プロバイダー定義の関数には、単一の計算目的で定義する必要があります。
「オプション」引数を介して公開される関数内の条件付きロジックではなく、個別の関数を使用することをお勧めします。
例えば、単純な税込金額を算出するような関数です。複雑なロジックを持つ関数は避けましょう。
この方法の利点は次のとおりです。

  • 作業者にとっての機能結果の予測可能性を最大化する
  • エコシステム全体でさまざまな「オプション」の議論スタイルを管理する実務者の負担を防ぐ
  • 複数のロジックの分岐を管理するメンテナーの負担を防ぐ

2-7. 関数は純粋かつオフラインであるべきである

プロバイダー定義の関数は、同じ引数に対して常に同じ結果を生成する必要があります。
関数は、環境ベース、時間ベース、またはネットワークベースのロジックを避ける必要があります。
代わりに、プロバイダー構成にアクセスし、Terraform の操作グラフに完全に参加できるため、これらの実装にはデータ ソースを優先します。
関数をオフラインにすることで、次の利点があります。

  • 実務家にとっての機能結果の予測可能性を最大化する
  • Terraformがどこでも構成全体を静的に検証できることを保証する
  • Terraformコマンド間で環境が変わった場合に発生する問題を防ぐ
  • ネットワークやサービスが利用できなくなった場合に専門家の問題を防ぐ

3. 実践Custom Provider作成

APIクライアントの作成

ここまで辛抱強く呼んでくれた人には想像できると思いますが、OpenMetadataへAPI処理をするクライアントありませんので、まずはAPIクライアントの自作から始まります。
Custom Providerチュートリアルで出てきたHashicups-Clientを参考に自作します。
https://github.com/hashicorp-demoapp/hashicups-client-go/tree/main

Custom Provider作成

私自身がGo言語初心者ということもあり、チュートリアルで作成したコードをベースにOpenMetadata用に書き換えるという方針で作成しました。
チュートリアルのコードのGithubを紹介しておきます。
https://github.com/hashicorp/terraform-provider-hashicups/tree/main

4. 作成したCustom Provider動作確認

今回は時間が無くてユーザ管理のリソースしか作れなかったのはご容赦ください。。。

ユーザの新規作成

ユーザ新規作成時のmain.tfです。
一応providerのhostとTokenは環境変数からも呼び出し可能になっています。

main.tf
terraform {
  required_providers {
    openmetadata = {
      source = "hashicorp.com/dev/openmetadata"
    }
  }
}

provider "openmetadata" {
  host     = "http://192.168.0.19:8585"
  token = "Token"
}

resource "openmetadata_createuser" "edu" {
    display_name = "terraform_user"
    email        = "terraform@example.com"
    name         = "terraform"
}

output "edu_user" {
  value = openmetadata_createuser.edu
}

terraform planを実行すると、

terraform applyでユーザを作成実行します。

yesを押すと、無事ユーザを作成できました。

OpenMetadata側でも確認してみます。
CurlでユーザIDを指定してGETした結果です。

ユーザ情報のアップデート

main.tfにdescriptionを追加しました。

resource "openmetadata_createuser" "edu" {
    display_name = "terraform_user"
    email        = "terraform@example.com"
    name         = "terraform"
    description  = "terraform_desc" ★ここを追加
}

terraform applyを実行し更新します。
内容もin-placeとUpdate処理となっていることがわかります。

問題ないので「yes」で実行します。
無事更新ができました。

再度CurlコマンドでOpenMetadata側を確認します。
Descriptionに「terraform_desc」と設定されています。

ユーザ削除処理

main.tfからユーザ設定を削除後、terraform applyを実行すると無事削除ができました。

今後の課題

正直、今回ユーザのみのプロバイダーリソースを作成しましたが、やり残した部分があるのでそれは今後の課題です。
1つは、PATCH処理の実装です。実はリソース作成時に設定値を記述したフィールド(上記ユーザ作成でいえば、Display_Name)はPUT処理での更新をサポートしていません。
その場合は、PATCHメソッドで更新する必要があります。
今回作成したプロバイダーにPATCHメソッドによる部分更新は未実装なので、今後実装したいです。
2つ目は、本当に最低限動くだけを目指して作成したので、Go言語のイディオム的に違反している部分とか結構あるので、そこを修正したいです。
3つ目は、チームも作って、そのチームにユーザを所属させる際にTerraformのリソースIDを指定して所属させるようにしたい。

カスタムプロバイダーに入門してみた感想

1リソースを作るだけでも、まぁまぁ大変だったので、管理したいリソース分のプロバイダーを作るとなると結構辛い部分があると思います。
ただ、できることならカタログツールもTerraformで管理できると開発運用チームとしては助かる部分はあると思います。
公式のプロバイダーが出るのを待つ間は、自作プロバイダーで運用管理するというのもアリではないでしょうか。
チーム内にGo言語に詳しい人がいれば、かなり導入障壁は下がると思います!

最後に

今回、Go言語初心者が分からないなりにも試行錯誤して、Custom Providerを1リソース分を作成してみました。
これまでなんと無くで動かしていたTerraformの動作原理などを理解できた点は非常に良かったと思います。
是非Terraformのことをもっと知りたいと思われたら是非Custom Provider作成にチャレンジしてみて下さい!!

注意ポイント

  • OpenMetadataのバグで躓いた話
    main.tfのName値がEmailの@の前半部分と完全一致していないと、terraformコマンドがコケます。
    原因はNameとEmailの前半部分が一致していないことだと思います。
    そこはフレームワーク側でエラーハンドリングされていました。
    さらにOpenMetadata v1.4.3では、POSTメソッドのBodyに@の前半と異なる値をNameに設定しても、POSTが成功します。
    しかも、実際に設定される値はEmailの前半部分となります。
    なので、APIクライアントでユーザを取得するメソッドを書いている時に、Nameの検索で404 Not Foundが出るエラーと戦ってました。。。

Discussion