メインコンテンツへスキップ

OpenTofu レジストリの構築

Building the OpenTofu Registry

先日、OpenTofu レジストリ検索のベータ版を公開しました。これは、OpenTofu レジストリ内のプロバイダーとモジュールのドキュメントを検索および表示できるユーザーインターフェースです。この重要なマイルストーンに到達した今、OpenTofu レジストリと検索がどのように機能するのか、そしてパブリックレジストリを運営する上での落とし穴について話す時が来ました。

レジストリ API

OpenTofu とその前身である Terraform は、さまざまな API と対話するためにコミュニティによって作成されたプロバイダーバイナリに依存しています。現在、OpenTofu レジストリには 4,000 を超えるプロバイダーがあり、クラウドプロバイダーから GitHub アカウントの管理まで、幅広い統合を可能にしています。何らかのリソースを作成するための API があれば、OpenTofu と統合できます。

プロバイダーに加えて、コミュニティは、いくつかの構成オプションだけでクラウドプロバイダーを使用してインフラストラクチャ全体をプロビジョニングするなど、より高レベルの機能を実装する 20,000 を超える再利用可能なモジュールも作成しています。

これらのプロバイダーとモジュールはコミュニティによって作成されているため、OpenTofu はそれらをどこからダウンロードするか、および利用可能なバージョンを知る必要があります。ここでレジストリが登場します。利用可能なプロバイダーとモジュール、ダウンロード URL、チェックサム、および整合性検証用の GPG キーに関する情報を保持します。

ステップ 1: サービスディスカバリ

プロバイダーとモジュールの両方について、OpenTofu は最初に https://registry.opentofu.org/.well-known/terraform.json ファイルを要求します。このファイルには、次の内容が含まれています。

コードブロック
{
"modules.v1": "/v1/modules/",
"providers.v1": "/v1/providers/"
}

このファイルには、モジュールとプロバイダーに関する情報を取得するために OpenTofu がクエリする必要があるエンドポイントが一覧表示されています。プライベートレジストリの場合、login.v1 という 3 番目のエンドポイントもあり、認証に使用する OAuth エンドポイントに関する情報を提供します。詳細に関心がある場合は、このプロトコルについてOpenTofuドキュメントで詳しく読むことができます。

ステップ 2: バージョンリスト

エンドポイント情報を使用して、OpenTofu は目的のプロバイダーまたはモジュールのバージョンリストエンドポイントをクエリできます。

  • プロバイダーの場合、このエンドポイントは /v1/providers/NAMESPACE/NAME/versions になります ()。
  • モジュールの場合、/v1/modules/NAMESPACE/NAME/SYSTEM/versions になります ()。

ステップ 3: ダウンロード情報

受信した情報に基づいて、OpenTofu は特定のバージョン、オペレーティングシステム、およびアーキテクチャに関する情報を要求できます。

  • プロバイダーの場合、/v1/providers/NAMESPACE/NAME/VERSION/download/OS/ARCH () にあります。
  • モジュールの場合、/v1/modules/NAMESPACE/NAME/SYSTEM/VERSION/download () にあります。

OpenTofu は、提供された GitHub リリース URL からプロバイダーをダウンロードし、チェックサムと署名を確認するか、モジュールの場合は返された Git リポジトリをクローンします。

レジストリの(無料)ホスティング

OpenTofu 自体の開発に取り組む少人数のコアチームを持つオープンソースプロジェクトとして、レジストリの運用コストを帯域幅と人的コストの両面で可能な限り低く抑えることが最も重要でした。ただし、何千人もの開発者がダウンした場合にインフラストラクチャを更新する手段がなくなるため、レジストリの稼働時間が 100% に近いことを確認する必要もありました。

ここで Cloudflare に大いに感謝します。R2 の非常に競争力のある価格設定と、OpenTofu のスポンサーシップにより、サーバーやスケーリングの問題を心配することなく、事実上無料でレジストリを実行できるようになりました。レジストリのコードベース(Go で記述)は、上記の API の考えられるすべての回答を事前生成し、静的ファイルを R2 バケットにアップロードします。

レジストリへの入力

HashiCorp が Terraform のライセンスを変更したことに伴い、HashiCorp は Terraform ではないソフトウェアのための Terraform Registry を閉鎖しました。そのため、OpenTofu Registry のデータソースとして利用することは不可能になりました。しかし、Terraform Registry は GitHub にのみ結びついていたため、少し時間がかかるものの、GitHub の検索 API を使用して Registry を作成するのは比較的簡単でした。

しかし、Registry の更新ははるかに難しい問題でした。GitHub は API のリクエストを 1 時間あたり約 5,000 件に制限しており、これは約 30,000 件のプロバイダーとモジュールを迅速に更新するには不十分です。特に、一部の更新には複数のリクエストが必要でした。

プロバイダーの作成者に GitHub アカウントでログインしてもらい、より高いレート制限で使用できるアクセストークンを付与することも可能でしたが、OpenTofu がまだリリースされていない時点で、何千人もの開発者に一度にログインを求める必要があったため、Registry の立ち上げ時には現実的ではありませんでした。

解決策は、レート制限のない GitHub の RSS フィードを利用することでした。http://github.com/USERNAME/REPO/releases.atom にあるリリース RSS には、常に最新の 5 つのリリースが含まれています。これは、最新バージョンのみを追加する必要がある場合に十分です。プロバイダーやモジュールが 1 時間以内に 5 つ以上のリリースを持つ可能性は低いからです。モジュールの場合、リリースではなくタグをクエリする必要がありました。これは、git ls-remote コマンドでこの情報をすべて取得でき、レート制限もなかったため、さらに簡単でした。(この状態を維持してくれますよね、GitHubさん?)

提出プロセス

OAuth を使用してプロバイダーやモジュールの作成者に資格情報を求める必要がなかったため、GitHub アカウントを持つ人なら誰でも利用できるシンプルな提出プロセスを作成することができました。

誰でもプロバイダーやモジュールを提出できましたが、プロバイダーのバイナリを検証できるように、プロバイダーの作成者に GPG キーを提出してもらう必要がありました。OAuth ログインを求める代わりに、再び GitHub API を独創的な方法で使用することにしました。プロバイダーの GPG キーを提出するには、作成者はプロバイダーのリポジトリの組織メンバーシップを公開に設定し、GitHub issue を提出する必要がありました。私たちは、プロバイダー組織でのメンバーシップを確認し、GPG キーを処理します。

GitHub issue を開くだけというシンプルな提出プロセスが、非常に好評であることがわかりました。今日まで、コミュニティは Registry リポジトリで 1,000 件近くのプルリクエストと issue をオープンしています。また、大規模な組織で働くプロバイダーとモジュールの作成者は、OpenTofu アカウントにサインアップするために組織のリソースを費やす必要がなくなり、結果として、非常に多くの著名なプロバイダーが GPG キーを追加しました。

ユーザーインターフェースの構築

Registry の開発に取り組み、OpenTofu 自体のいくつかの重要な作業のために数ヶ月の休止期間を挟んで、検索とドキュメント閲覧インターフェースの構築に戻りました。すぐにわかることですが、これはより大きなタスクであり、3 倍のコードを書く必要がありました。

A screenshot of the OpenTofu Registry Search

以前と同様に、静的ファイルを生成するアーキテクチャを選択しました。初期段階では、単純に静的 HTML ファイルを生成するか、シングルページアプリケーションを使用して API からデータをロードするかを決定する必要がありました。レイアウトの変更ですべてのファイルの再生成が必要になり、何度もアップロードするのは費用がかかると考えたため、後者を選択しました。

この決定を下した後、バックエンドとフロントエンドのコンポーネントを構築することにしました。前者は後者が消費するデータを生成し、後者は前者のデータを消費します。GitHub と、データを取得するために構築したさまざまな独創的な API 統合の上に便利な抽象化レイヤーを提供し、Registry リポジトリに保存されたメタデータへのアクセスを容易にする、標準化された Go ライブラリである libregistry を構築しました。

ドキュメントの前処理

Registry は常に生のメタデータからすべての API レスポンスを再生成しますが、処理する必要のあるデータ量が膨大であるため、ドキュメントにはこのアプローチは実現できませんでした。数万のプロバイダーとモジュールに対してドキュメントレスポンスを作成する必要があるだけでなく、その一部には数百のバージョンがあり、それぞれにドキュメントのコピーを保存する必要がありました。

中間データを保存せずに、ソースリポジトリから R2 バケットに直接データを処理することにしました。このアプローチには、独自の問題がありました。Registry は git を利用して中間データの変更を追跡できましたが、R2 バケットへのアップロードが可能な限りアトミックになるようにし、部分的なアップロードが残らないようにする必要がありました。私たちは、アップロードを続行できる部分的な解決策を実装しましたが、これは依然として Registry の未解決の問題の 1 つです。

ドキュメントを表示するために必要なフロントエンドロジックを簡素化するために、バックエンドの主な仕事は、各ドキュメントファイルの名前を変更し、標準化された場所に移動することです。プロバイダーがドキュメントを保存する方法について形式化された説明はないため、この情報を抽出するためのロジックは 経験的にしか知られていません。取り込むリポジトリの数を増やし、新しくエッジケースを発見するにつれて、さまざまなバグを回避するためにいくつかの反復処理が必要でした。

モジュールスキーマ

プロバイダーは通常、terraform-docs のようなツールによって生成された独自の明示的なドキュメントを持っていますが、モジュールにはすぐに利用できるそのような情報はありません。これにより、入力、出力、依存関係に関する情報を生成することが困難になります。

HashiCorp は、Terraform からモジュール解析ロジックの一部を複製して、この情報を抽出するための terraform-schema を公開しました。しかし、重複したコードベースを維持すると、将来的にはメンテナンスの問題が発生する可能性が高いため、この機能を OpenTofu に直接実装することにしました。このパッチは現在 ブランチに存在しますが、後日、実験的な機能としてメインブランチに統合される予定です。

ライセンス

インジェストプロセス中に、ライセンスについても考慮する必要がありました。このようなドキュメントデータセットがどのような法的基準に該当するかわからなかったため、Registry のドキュメントに受け入れる 制限付きのライセンスセットを意図的に選択しました。潜在的に問題のあるライセンスのコンテンツをインジェストしないように、各プロバイダーおよびモジュールリポジトリで自動ライセンス検出を実行しました。

OpenTofu Docs API (とその使い方)

バックエンドでこれらの作業をすべて行い、表示ロジックをデータから分離したことにも、意図していなかったものの非常に歓迎される副次的な効果がありました。それは、プロバイダーおよびモジュールドキュメント用の API を提供できるようになったことです。これは、わずか数週間後に Jetbrains が OpenTofu 統合のためにそのような API を要求したため、非常に役立ちました。

バックエンドはこの形式でデータセットを生成し、registry-ui リポジトリからローカルで簡単に実行できます。一方、パブリック API は、誰でも統合を構築するために利用できます。ブラウザのみの統合を構築したい場合は、正しい CORS ヘッダーも含めました。もし何かクールなものを構築したら、ぜひ教えてください!

バックエンドを構築している間、常に頭にあった問題が 1 つありました。それは、このような膨大なデータセットをどのように検索可能にするかです。検索インデックスを生成し、クライアントの JavaScript に検索問題全体を処理させるだけで可能になることを期待していました。検索のための堅牢で成熟したライブラリである lunr.js を調べていたところ、この方法はまったく実現不可能であることがすぐにわかりました。限られたデータセットでも、検索インデックスのダウンロードサイズはすぐに数百メガバイトに膨れ上がり、これはスナップの効いた検索機能には理想的ではなく、データ上限がある人にとっては本当に不快なものです。

独自のデータベースサーバーを実行したり、サービス依存関係を増やしたりしたくなかったため、Cloudflare の D1 (SQLite データベースサービス) と ワーカーを調べて検索クエリを処理しました。最初は有望に見えましたが、トランザクションで実行するためには、すべての更新を 1 つの HTTP リクエストで送信する必要があることがわかりました。

これを回避して、アトミックな検索インデックス更新を実行することは可能でしたが、代わりにデータベースサービスとしての Neon を使用することにしました。彼らは OpenTofu を明示的にスポンサーしているわけではありませんが、彼らの無料プランは検索インデックスにとって十分に快適で、次のプランもかなり手頃な価格でした。Cloudflare との緊密な統合も非常に歓迎される追加機能でした。

Neonでホストされているデータベースをクエリするために、Cloudflare Workerを作成しました。このWorkerは、api.opentofu.orgへのすべてのリクエストを処理し、静的リクエストをR2に転送し、検索クエリ自体を処理することになりました。

バックエンドは、https://api.opentofu.org/registry/docs/search.ndjsonでデータフィードを含む行区切りJSON(ndjson)ファイルを準備し、Workerが取り込んでデータベースに投入できる検索インデックスへの最近の更新をすべて含みます。

これからどうする?

OpenTofu Registry Searchは現在ベータ版であり、まだすべてが機能しているわけではないことを示しています。インデックス化されるプロバイダーとモジュールの量を拡大するにつれて、修正が必要なエッジケースがさらに発見されるでしょう。

libregistryライブラリも、レジストリのコードベースに存在する機能の多くを重複させています。長期的なメンテナンスのため、そしてlibregistryを使用すると、ユーザーが独自のミラーやレジストリの独立したコピーを実行しやすくなるため、この機能を重複排除したいと考えています。

このプロセスにおいて、皆様からのフィードバックは優先順位付けに非常に役立ちます。バグを発見した場合や、私たちが考慮していないユースケースがある場合は、GitHub Issuesを利用してお知らせください。