Read OSS

プロバイダープラグインシステム: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-L119providers.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-L18Source インターフェースを中心に構成されています。

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-L39providerSource() 関数は、起動時にソースのチェーンを組み立てます。

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-L78GRPCProvider は、各メソッド呼び出しを 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 側とプロバイダー側のスキーマが完全に一致していないと、デシリアライズ時に誤った値が生成されます。UpgradeResourceState RPC は、まさにこのスキーマバージョンの不一致に対処するために設けられています。

プロトコル v5 と v6、そして Protobuf の定義

Terraform は現在、2 つのプロバイダープロトコルを並行サポートしています。

  • プロトコル v5internal/tfplugin5/)— 既存プロバイダーの大多数が採用している確立されたプロトコル
  • プロトコル v6internal/tfplugin6/)— いくつかの新機能を追加した新しいプロトコル

protobuf のサービス定義は docs/plugin-protocol/tfplugin5.protodocs/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.gogrpcwrap パッケージは 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 つです。

  1. インテグレーションテスト — サブプロセスを起動せずにインプロセスでプロバイダーを生成できる
  2. プロバイダーのリアタッチTF_REATTACH_PROVIDERS 機構によってすでに起動中のプロバイダープロセスに接続する。SDK の受け入れテストで広く使われている手法
  3. ビルトインプロバイダーterraform ビルトインプロバイダーはインプロセスで動作する

プロバイダーのアドレッシングとスキーマの読み込み

すべてのプロバイダーは、ホスト名・名前空間・タイプの 3 要素からなる完全修飾名で識別されます。internal/addrs/provider.go#L13-L16addrs.Provider 型は tfaddr.Provider の型エイリアスです。

type Provider = tfaddr.Provider

たとえば registry.terraform.io/hashicorp/aws の場合、ホスト名は registry.terraform.io、名前空間は hashicorp、タイプは aws となります。ユーザーが設定に単に aws と書いた場合、Terraform は ImpliedProviderForUnqualifiedType() を通じて完全なアドレスを推論します。

スキーマの読み込みは contextPlugins を通じて行われ、プロバイダーごとにスキーマがキャッシュされます。グラフのノードがスキーマを必要とする場面は非常に多く(設定の評価、変更の plan、状態のエンコードなど、常に必要です)、まずキャッシュを確認し、スキーマがまだ読み込まれていない場合にのみプロバイダープロセスの GetProviderSchema() を呼び出します。スキーマの取得には別プロセスへの gRPC ラウンドトリップが発生するため、このキャッシュはパフォーマンス上きわめて重要です。

ContextOptsPreloadedProviderSchemas フィールドを使うと、呼び出し側があらかじめキャッシュにスキーマを投入できます。plan の前のバリデーションなど、別の目的ですでにスキーマを読み込んでいる場合に、冗長なスキーマ取得を避けるための仕組みです。

次回予告

プロバイダーシステムの全体像を把握したところで、第5回は永続化のもう一方の側面であるステート管理とバックエンドに焦点を当てます。3 層のステートアーキテクチャ、バックエンドインターフェースの階層構造、そして terraform init がバックエンド設定とステートの移行をどのように調整するかを詳しく見ていきましょう。