プロバイダープラグインシステム:gRPC、プロトコル、プロバイダーのライフサイクル
前提知識
- ›第1〜3回(アーキテクチャ、グラフエンジン、Plan & Apply)
- ›gRPC と Protocol Buffers の基本的な理解
- ›プロセスベースのプラグインアーキテクチャへの慣れ
プロバイダープラグインシステム:gRPC、プロトコル、プロバイダーのライフサイクル
主要なクラウドやサービスをカバーする 4,000 以上のプロバイダーからなる Terraform のエコシステムは、非常にシンプルなプラグインアーキテクチャによって成り立っています。各プロバイダーは独立した OS プロセスとして動作し、Terraform Core と gRPC を介して通信します。このプロセス分離によって、バグのあるプロバイダーが Terraform 本体をクラッシュさせることはなく、gRPC をサポートする任意の言語でプロバイダーを実装でき、エコシステムはコアバイナリとは独立して進化できます。
この記事では、プロバイダーの完全なライフサイクルを追います。検出・ダウンロードから始まり、プロセスの起動と gRPC 接続、そして Go の型と protobuf のワイヤーフォーマットを相互変換する 3 層の抽象化まで順を追って解説します。
providers.Interface:プロバイダーの契約
Terraform のプロバイダー抽象化の中心にあるのが、internal/providers/provider.go#L17-L119 の providers.Interface です。
type Interface interface {
GetProviderSchema() GetProviderSchemaResponse
ValidateProviderConfig(ValidateProviderConfigRequest) ValidateProviderConfigResponse
ValidateResourceConfig(ValidateResourceConfigRequest) ValidateResourceConfigResponse
ConfigureProvider(ConfigureProviderRequest) ConfigureProviderResponse
ReadResource(ReadResourceRequest) ReadResourceResponse
PlanResourceChange(PlanResourceChangeRequest) PlanResourceChangeResponse
ApplyResourceChange(ApplyResourceChangeRequest) ApplyResourceChangeResponse
ImportResourceState(ImportResourceStateRequest) ImportResourceStateResponse
ReadDataSource(ReadDataSourceRequest) ReadDataSourceResponse
Stop() error
// ... 合計 ~25 メソッド
}
classDiagram
class Interface {
<<interface>>
+GetProviderSchema()
+ValidateProviderConfig()
+ValidateResourceConfig()
+ConfigureProvider()
+ReadResource()
+PlanResourceChange()
+ApplyResourceChange()
+ImportResourceState()
+ReadDataSource()
+OpenEphemeralResource()
+CallFunction()
+ListResource()
+Stop()
}
class GRPCProvider {
-client proto.ProviderClient
-ctx context.Context
-schema GetProviderSchemaResponse
}
class GRPCProvider6 {
-client proto6.ProviderClient
}
Interface <|.. GRPCProvider : protocol v5
Interface <|.. GRPCProvider6 : protocol v6
このインターフェースが Terraform Core のプログラミング対象です。第3回で確認したように、グラフのノードは provider.PlanResourceChange() や provider.ApplyResourceChange() を呼び出します。コアエンジンは、プロバイダーがインプロセスで動いているのか、ネットワーク越しなのか、テストハーネス上なのかを一切関知しません。
このインターフェースは長年にわたって少しずつ拡張されてきました。OpenEphemeralResource()、CallFunction()、ListResource() といった新しいメソッドは、Terraform の最近の機能追加を反映しています。各メソッドが型付きのリクエスト構造体を受け取り、型付きのレスポンス構造体を返すパターンを採用しているため、契約の内容が明確になり、バージョン管理もしやすくなっています。
プロバイダーの検出:レジストリ、ミラー、開発用オーバーライド
プロバイダーを使用するには、まず Terraform がそれを見つけてダウンロードする必要があります。検出システムは internal/getproviders/source.go#L14-L18 の Source インターフェースを中心に構成されています。
type Source interface {
AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error)
PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error)
ForDisplay(provider addrs.Provider) string
}
provider_source.go#L26-L39 の providerSource() 関数は、起動時にソースのチェーンを組み立てます。
flowchart TD
PS["providerSource()"] --> Check{"明示的な設定あり?"}
Check -->|No| Implicit["implicitProviderSource()"]
Check -->|Yes| Explicit["explicitProviderSource()"]
Implicit --> Registry["RegistrySource<br/>registry.terraform.io"]
Implicit --> LocalMirror["FilesystemMirrorSource<br/>~/.terraform.d/plugins"]
Explicit --> Multi["MultiSource"]
Multi --> R2["RegistrySource"]
Multi --> FM["FilesystemMirrorSource"]
Multi --> NM["NetworkMirrorSource"]
CLI 設定に provider_installation ブロックが存在しない場合、Terraform はまずローカルのファイルシステムキャッシュを確認し、見つからなければ公開レジストリへフォールバックする暗黙的なソースを生成します。明示的な設定がある場合は、特定のプロバイダーを特定のミラーに向けるための include/exclude ルールを持つ MultiSource を構築します。
開発用オーバーライド(CLI 設定の dev_overrides)は、このチェーン全体を完全に迂回します。プロバイダーをローカルでビルドしている開発者は、ローカルのバイナリパスを直接指定した開発用オーバーライドを設定します。リアタッチ機構(環境変数 TF_REATTACH_PROVIDERS)はさらに一歩進んで、プロセスの起動自体をスキップし、すでに起動済みのプロバイダーにアタッチします。これは SDK の受け入れテストフレームワークが採用している方式です。
gRPC ブリッジ:GRPCProvider と型変換
internal/plugin/grpc_provider.go#L53-L78 の GRPCProvider は、各メソッド呼び出しを gRPC リクエストに変換することで providers.Interface を実装しています。
type GRPCProvider struct {
PluginClient *plugin.Client
TestServer *grpc.Server
Addr addrs.Provider
client proto.ProviderClient
ctx context.Context
mu sync.Mutex
schema providers.GetProviderSchemaResponse
}
32〜47行目 の GRPCProviderPlugin は、HashiCorp の go-plugin フレームワークと連携します。
type GRPCProviderPlugin struct {
plugin.Plugin
GRPCProvider func() proto.ProviderServer
}
func (p *GRPCProviderPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
return &GRPCProvider{
client: proto.NewProviderClient(c),
ctx: ctx,
}, nil
}
sequenceDiagram
participant Core as terraform.Context
participant Iface as providers.Interface
participant GRPC as GRPCProvider
participant Convert as plugin/convert
participant Proto as tfplugin5.ProviderClient
participant Process as Provider Process
Core->>Iface: PlanResourceChange(req)
Iface->>GRPC: PlanResourceChange(req)
GRPC->>Convert: cty.Value → DynamicValue (msgpack)
Convert-->>GRPC: proto request
GRPC->>Proto: PlanResourceChange(protoReq)
Proto->>Process: gRPC call
Process-->>Proto: gRPC response
Proto-->>GRPC: proto response
GRPC->>Convert: DynamicValue → cty.Value
Convert-->>GRPC: Go types
GRPC-->>Core: PlanResourceChangeResponse
internal/plugin/convert/ 配下の convert パッケージは、Terraform の型システム(cty.Value)と protobuf のワイヤーフォーマット(DynamicValue)を相互変換するという重要な役割を担っています。値は効率化のため JSON ではなく MessagePack でシリアライズされます。受信側では、スキーマを使って msgpack のバイト列を正しく型付けされた cty.Value オブジェクトに復元します。
ヒント: plan や apply 時に原因不明の型エラーが発生した場合、多くのケースで convert レイヤーが原因です。Terraform 側とプロバイダー側のスキーマが完全に一致していないと、デシリアライズ時に誤った値が生成されます。
UpgradeResourceStateRPC は、まさにこのスキーマバージョンの不一致に対処するために設けられています。
プロトコル v5 と v6、そして Protobuf の定義
Terraform は現在、2 つのプロバイダープロトコルを並行サポートしています。
- プロトコル v5(
internal/tfplugin5/)— 既存プロバイダーの大多数が採用している確立されたプロトコル - プロトコル v6(
internal/tfplugin6/)— いくつかの新機能を追加した新しいプロトコル
protobuf のサービス定義は docs/plugin-protocol/tfplugin5.proto と docs/plugin-protocol/tfplugin6.proto にあります。
v6 の主な追加機能は以下の通りです。
- リソースアイデンティティ — プロバイダーがリソースのアイデンティティスキーマを宣言でき、移動をまたいだ追跡が可能になる
- リソース状態の移動 — 状態を維持したままリソースタイプの変更をサポート
- 遅延アクション — プロバイダーがリソースをまだ plan できない旨を通知できる
- エフェメラルリソース — オペレーションの実行中のみ存在するリソース
- 関数 — HCL 式から呼び出せるプロバイダー提供の関数
- リソースの列挙 — リモートオブジェクトを列挙するクエリ型のリソース
classDiagram
class ProtocolV5 {
+GetSchema
+ValidateResourceTypeConfig
+ValidateDataSourceConfig
+UpgradeResourceState
+ConfigureProvider
+ReadResource
+PlanResourceChange
+ApplyResourceChange
+ImportResourceState
+ReadDataSource
+Stop
}
class ProtocolV6 {
+GetProviderSchema
+ValidateResourceConfig
+ValidateDataResourceConfig
+ValidateEphemeralResourceConfig
+UpgradeResourceState
+UpgradeResourceIdentity
+ConfigureProvider
+ReadResource
+PlanResourceChange
+ApplyResourceChange
+ImportResourceState
+MoveResourceState
+ReadDataSource
+OpenEphemeralResource
+RenewEphemeralResource
+CloseEphemeralResource
+CallFunction
+ListResource
+Stop
}
ProtocolV5 <|-- ProtocolV6 : extends
Terraform は go-plugin のハンドシェイク時にプロトコルバージョンを検出します。各プロトコルバージョンには専用の GRPCProvider 実装(v5 は GRPCProvider、v6 は GRPCProvider6)があり、どちらも同じ providers.Interface を実装しています。そのため、コアエンジンはプロバイダーがどちらのプロトコルバージョンを使っているかを一切意識する必要がありません。
サーバー側のラッピングとテストハーネス
internal/grpcwrap/provider.go の grpcwrap パッケージは GRPCProvider とは逆の役割を果たし、providers.Interface を gRPC サーバーとしてラップします。
flowchart LR
subgraph "Normal Operation"
Core1["Terraform Core"] -->|"gRPC client"| Plugin["Provider Process<br/>(gRPC server)"]
end
subgraph "Testing / Reattach"
Core2["Terraform Core"] -->|"providers.Interface"| Wrap["grpcwrap.Provider"]
Wrap -->|"gRPC server impl"| InProc["In-process gRPC"]
end
主な用途は次の 3 つです。
- インテグレーションテスト — サブプロセスを起動せずにインプロセスでプロバイダーを生成できる
- プロバイダーのリアタッチ —
TF_REATTACH_PROVIDERS機構によってすでに起動中のプロバイダープロセスに接続する。SDK の受け入れテストで広く使われている手法 - ビルトインプロバイダー —
terraformビルトインプロバイダーはインプロセスで動作する
プロバイダーのアドレッシングとスキーマの読み込み
すべてのプロバイダーは、ホスト名・名前空間・タイプの 3 要素からなる完全修飾名で識別されます。internal/addrs/provider.go#L13-L16 の addrs.Provider 型は tfaddr.Provider の型エイリアスです。
type Provider = tfaddr.Provider
たとえば registry.terraform.io/hashicorp/aws の場合、ホスト名は registry.terraform.io、名前空間は hashicorp、タイプは aws となります。ユーザーが設定に単に aws と書いた場合、Terraform は ImpliedProviderForUnqualifiedType() を通じて完全なアドレスを推論します。
スキーマの読み込みは contextPlugins を通じて行われ、プロバイダーごとにスキーマがキャッシュされます。グラフのノードがスキーマを必要とする場面は非常に多く(設定の評価、変更の plan、状態のエンコードなど、常に必要です)、まずキャッシュを確認し、スキーマがまだ読み込まれていない場合にのみプロバイダープロセスの GetProviderSchema() を呼び出します。スキーマの取得には別プロセスへの gRPC ラウンドトリップが発生するため、このキャッシュはパフォーマンス上きわめて重要です。
ContextOpts の PreloadedProviderSchemas フィールドを使うと、呼び出し側があらかじめキャッシュにスキーマを投入できます。plan の前のバリデーションなど、別の目的ですでにスキーマを読み込んでいる場合に、冗長なスキーマ取得を避けるための仕組みです。
次回予告
プロバイダーシステムの全体像を把握したところで、第5回は永続化のもう一方の側面であるステート管理とバックエンドに焦点を当てます。3 層のステートアーキテクチャ、バックエンドインターフェースの階層構造、そして terraform init がバックエンド設定とステートの移行をどのように調整するかを詳しく見ていきましょう。