November 1, 2022

Self-Hosted Private Terraform Registryを作った

こんにちは、今回はPrivate Terraform Registryを作った話をします。

なぜPrivate Terraform Registry?

PrivateにTerraform Plugin(Pluginとは、ProviderおよびModuleの両方)を社内など、限られた人に提供したいからです。

わたしが所属する企業では大規模なプライベートクラウドを展開しています。ですが、Infrastructure as Code(IaC)が全く進んでいません。IaCが進んでいないので、全ては手作業ベースであり、(SREの観点からだと)リライアブルではなく、スノーフレークなインフラが構築されることは免れず、自動化により開発者に権限移譲などをできないので承認フローがとんでもなく長く、気が滅入るような状態です。

そのため、Terraform Providerを提供することによって状況の打開を試みたのですが、Hashicorp社が提供しているTerraform Registryを使うとPluginは全世界に公開されてしまいます。皆さんの多くの方が使っているであろうAWSやGCPのTerraform ProviderもこのRegistryに公開されてあるから使うことができます。本記事では、そのようなPluginをPublic Terraform Pluginと呼びますが、今回はその逆の機能を提供する必要があります。世界に公開せず、限られた人にTerraform Pluginを配布したいのです。

また、Terraform Cloudを使うとPrivate Registryの機能が付いてくる(Private Registry, Terraform Cloud)のですがそのようなツールを大企業でさくっと導入するのはかなり厳しいです(特に、Infrastructure as Codeの価値が知られていない企業で、導入の契約を勝ち取るのは相当ハードで)。プライベートクラウドでやってきているプライドがある組織なので、なかなかSaaSを導入するのは難しいです。

Self-Hostedできるものはないか、と考えた結果、自分で作るに至りました。

仕組みのおさらい

自作する前に、Terraformの仕組みをおさらいしましょう。

1. Terraformにレジストリを指定する方法

GCPのようなPlugin Terraform Pluginを使っている場合、以下のようにProviderの設定を書くと思います(環境変数などからクレデンシャルを読み取っている人はProviderの宣言を省略できるため、Terraformを使っているからと言って100%存在するものではないです)。

terraform {
    required_providers {
        keke = {
            source  = "keke/keke"
            version = "~> 1.0"
        }
    }
}

ここのsourceで、暗黙的にHashicorp Terraform RegistryからProviderのバイナリを取得するように指定されているのです。

source  = "keke/keke"

これは「Hashicorp Terraform registryのkekeネームスペースのkekeというProviderを取得する」と指定していて、

source = "registry.terraform.io/keke/keke"

と同値です。ここで指定できるフォーマットは以下の通りです(参考: Source Addresses, Terraform)。

source = [<HOSTNAME>/]<NAMESPACE>/<TYPE>

最初のホスト部分を省略すると、デフォルトであるHashicorp Terraform Registryに取りに行くようになります(registry.terraform.io)。つまり、Self-HostedなRegistryを指定するには以下のようにホスト部分を追加してあげます。

source = "registry.keisukeyamashita.com/keke/keke"

ここで注意点ですが、Terraformはレジストリとの通信でHTTPSを強制します。そのためHTTPで公開しているレジストリを使うことはできません。これはサプライチェーンセキュリティの中で、Providerの脆弱性が多大な影響を及ぼすためです。

2. TerraformはどのようにPluginを取ってくるのか?

TerraformのProvider Registry ProtocolおよびModule Registry Protocolに従って取りに行きます。

プロトコルの説明は省略しますが、Providerをインストールする場合、簡単に説明すると以下のようになります。

  1. 使えるバージョンを一覧取得する
  2. versionで指定された条件に合致するバージョンを決定する
  3. バイナリをRegistryから取得する
  4. バイナリの署名を検証する

また、このProtocolには具体的な説明がドキュメントされておらず、適宜Terraform CloudのAPI仕様を参考にレジストリを参照する必要があります (Hashicorpに限らず「Self-Hosted XXX」はサービスプロバイダにとってあまり旨味がないのでドキュメントされていることは少ないです)。

3. 認証認可はどのようにするのか

Hashicorp Terraform Registryに取りに行く場合、何も認証認可は必要ないです。なのでPublic Terraform Pluginだけ使っているのであれば「何も行わない」です。

その一方、Self-HostedなRegistryをインターネットを通して提供する場合は認証認可は必要になります。

terraform initなどTerraformがPluginを取得する初期化のフェーズではCredential Helperというヘルパー(バイナリ)を実行できます。

これを実行することによって、トークンを取得したり、実行環境に格納したりすることができます。これをすることによって、任意のスクリプトをTerraformの実行前に挟むことが出来て、Authorization: Bearer <TOKEN>という形で飛んでいきます。Registry側でこのトークンを検証して、認可を行います。

4. Providerはどのように登録されるのか

Provider Protocolに従うと、以下のようにProviderはRegistryに以下のように登録されます。

  1. GPG鍵を登録してネームスペースを作成する
  2. Providerを作成する
  3. Versionを作成する
  4. SHA256Sumsを作成する
  5. 署名付きSHA256Sumsを作成する
  6. Platformを作成する
  7. バイナリを実際にあげる

設計

実装はプロトコルに従うだけなので詳細のコードは記載しません。Goで実装しています。 いくつかの機能を折り込みました。

マルチテナント

大きな単位の組織に一つのRegistryのサーバーを立てることを想定しています。Providerは組織で共有されてもいいものだからです。 マルチテナントにするための機能として、ネームスペースがあります。これはHashicorp社のRegistryと同じです。

このようにすることによって、会社で色んな組織があってもRegistryを気軽に使うことが出来き、各チームがこのRegistryをホストする必要がなくなります。

2. OpenTelemetry Telemetries・Audit log

OpenTelemetryを使って、各種のテレメトリ(ログ、メトリクス、トレース)を出力しています。 また、監査ログも出力しており、社内のセキュリティチームがいつ誰が何をしたかを確認することが出来ます。

このような機能を提供することによって、信頼してRegistryを社内で提供することができます。

終わりに

Self Hosted Registryを実装してみて、TerraformのPluginの仕組みの素晴らしさを痛感できました。Terraformの内部の仕組みの話になってしまうので割愛しますが、go-pluginの仕組みは素晴らしいです。 プロトコルに従っていればHashicorp Terraform Registryにあるものだけではなく、自分でもバイナリを気軽に配布することができます。

その一方で、実装は自分でやらないといけないのが面倒ですが、作ることを通してより深くTerraformを知ることが出来ました。 もっと機能を拡充してオープンソースとして公開したいと考えています。

参考文献

© KeisukeYamashita 2023