Zenn
🏗️

Terraform プロバイダーを配布する自前のプライベートリポジトリーの構築

に公開

概要

  • Terraform プロバイダーを公開するプライベートレジストリーを構築する手順例を記載しました。
  • プライベートレジストリーは、静的コンテンツを提供できる Web サーバーがあれば構築することができます。
    • ただし、Web サーバーについていくつかの条件を満たす必要があります。

プライベートレジストリー

  • Terraform でプロバイダーを配布する HTTPS サーバーのことをレジストリーと呼びます。自前のプロバイダーを作った場合、レジストリーで公開することで、プロバイダーを色んな場所で使えるようになります。
  • 公式で提供される Terraform Registry を使うのが簡単ですが、登録したプロバイダーは全世界に公開されるため、社内でのみ使用したいプロバイダーの運用などには不都合があります。
  • HCP Terraform (旧名 Terraform Cloud) であればプライベートレジストリーが利用可能ですが、以下の課題があります:
    • 利用に際してはユーザー登録や、商用利用には有償プランが必要になる。
    • アクセスに認証が必要になる。IP アドレス制限で社内からだけ利用可能にする、ということはできない。
  • プライベートレジストリーを自前で構築するには、 Provider Registry Protocol に従ったフォーマットでプロバイダーの情報を提供する Web サーバーを用意します。
  • Provider Registry Protocol はファイルのフォーマットやファイルの内容について定義したもので、動的なリクエスト・レスポンスを必要とはしていないため、 静的なコンテンツを提供する Web サーバーがあれば構築可能です。
    • ただしファイルの配置場所についての制限があるため、Web サイトにある程度の条件が求められます (後述)。
    • 例えば以下のような方法で構築できると考えられます (試してはいない):
      • GitHub Pages
      • AWS S3 による Web サイト
      • Google Cloud Storage (GCS) による Web サイト

プライベートレジストリー構築のための要件

Web サイトの要件

プライベートレジストリーを既存の Web サイトで構築する場合、以下の要件を満たす必要があります:

  • サイトのルートディレクトリーの /.well-known/terraform.json にファイルを設置できること。
    • terraform は最初に Remote Service Discovery という手順で、固定でこのファイルを見に行くため、このファイルの管理権限を持っていないとプライベートレジストリーとしては使用できません。
    • ユーザーごとのホームディレクトリーが提供されるタイプの共用 Web サーバーだと、サイトのルートディレクトリーのファイルを自由に編集できないので、この要件を満たすのが難しくなります。
  • HTTPS で動作していること。
  • HTTPS の証明書が Subject Alternative Name (SAN) を使って作成されていること。
    • 一般的な SSL 証明書ではこの要件は満たされていますが、自己署名証明書を使ってサイトを構築する場合は注意が必要です。
    • ただ SAN で作られていない証明書を使った Web サイトは、 Chrome などのブラウザーで怒られるので、フツーにサイトを構築すれば SAN を使った証明書になっていると思ってよいはずです。けれどもほんとうのフツーは一体なんだろう。
  • レスポンスの Content-Type を application/json にできること。
    • プライベートレジストリーは、 https://.../versionshttps://.../amd64 といった URL でアクセスされますが、 この時の Content-Type が application/json である必要があります。
    • これは意外と難しいのですが、例えば以下のような設定をすることで解決します:
      • index.json をインデックスファイルとして提供できるようにして、 versions/index.json といったファイルを置く。
        • .htaccess で設定できるサイトの場合、以下のような設定になります:

          DirectoryIndex index.json
          
      • Content-Type を強制的に application/json にする設定を行う。
        • これによって versions といった拡張子なしのファイルも application/json として返すようになります。
      • Web サーバーでアクセスされたときに .json を補完してしてファイルを返すようにする (URL のリライト処理)。

その他の要件

  • GnuPG による署名を行うので、GnuPG で鍵ペアを作っておく必要があります。
  • SHA256 チェックサムを使用します。このため sha256sum などが必要です。

デモ

プライベートレジストリーを構築するデモを以下のリポジトリーに作りました:

https://github.com/ikedam/terraform-private-registry-demo

  • example.com/example/hello という独自プロバイダーをプライベートレジストリーで配布します。
  • あくまでローカルでの検証として提供しているので、 docker compose を使用して example.com というサイトを偽装して Terraform に参照させます。
  • 構築したサイトで提供するファイルは registry/ 以下から参照できます。
  • サイトのファイルを構築するためのスクリプトを build-registry.sh として作成したので、構築方法はこのスクリプトを見ればわかります。

プライベートレジストリーの構築手順

Terraform がプロバイダーのアクセスを行う手順を追いつつ、プライベートレジストリーの構築手順について説明します。

Terraform の公式レジストリーの動作と対比できるよう、 hashicorp/aws での動作を見るための curl コマンドも併記しています。

プロバイダーアドレスを決める

リファレンス: Provider Registry Protocol Reference#Provider Addresses

プロバイダーのフルネーム (プロバイダーアドレス) を決めておく必要があります。

プロバイダーアドレスは以下のフォーマットになります:

(ドメイン名)/(ネームスペース)/(タイプ)

例えば AWS プロバイダー であれば、 registry.terraform.io/hashicorp/aws になります。
特記事項として、ドメイン名が registry.terraform.io の場合は省略して良いので、通常、 AWS プロバイダーは hashicorp/aws として参照します。

今回は example.com/example/hello にしました。

なお一度配布したプロバイダーを別のプライベートレジストリーで配布し直す場合はドメイン名を変更することになりますが、その場合にバイナリーの再ビルドが必要になるのかは未検証です (特に、 terraform-plugin-framework だとプロバイダーアドレスをコードにいれることになる)。
ただ、たぶん、ダウンロード処理に使われるだけで、プロバイダーの処理自体ではドメイン名は考慮されていないように思うので、バイナリーの再ビルドは不要な気がしています。

プロバイダーのバージョンとプラットフォームを決める

リファレンス: Publish providers#Creating a GitHub Release

プライベートレジストリーに設置する際に、このプロバイダーをなんというバージョンで提供するかの情報が必要となります。

バージョンはセマンティックバージョニングに従って X.Y.Z と 3 つの数値で設定します。
バージョンはプロバイダー自体には含まれない情報なので、プロバイダーのバイナリーを作った後で勝手に決めればいい情報となります (実際にはそれなりに計画的にバージョンを考えながら開発したり、 go 言語のバージョニングのポリシーに従ったりすると思うけれど、わかりやすさのための極端な説明)。

プライベートレジストリーの文脈では、バージョンの表記が以下の 2 パターンあるので注意が必要です。
※これに準じなかった場合に動作不良が起きるのかは未検証です。

  • v がつかないバージョン: 1.0.0 など。プライベートレジストリー上のバージョン一覧ファイルや、パッケージファイルを設置するパスで使用するバージョンではこの表記を使う。
  • v がついたバージョン: v1.0.0 など。プロバイダーのバイナリーや zip ファイルではこの表記を使う。

また、プロバイダーを利用できるプラットフォーム (OS と CPU アーキテクチャー) を決定します。
多くの場合、プロバイダーは go 言語で実装しているため、 GOOSGOARCH を指定することで簡単にいろんなプラットフォーム向けのバイナリーをビルドできます。

今回は、バージョンを 1.0.0 (v1.0.0)、 OS は linux 固定、アーキテクチャーは amd64arm64 として構築していきます。

Discover Protocol 用のファイルを設置する

リファレンス:

hashicorp/aws での例:

curl https://registry.terraform.io/.well-known/terraform.json

terraform は最初に、 https://(プロバイダーアドレスのドメイン名)/.well-known/terraform.json にアクセスに行きます。

例えば以下のようなファイルを置きます:

{
  "providers.v1": "/providers/"
}

ここで別のホストを指定することもでき、プロバイダーアドレスで指定したホストとは別のホストにプロバイダーの本体を置くこともできます。

注意が必要な点として、 Content-Type が application/json である必要があります。これは以降で設定するすべての JSON ファイルで同様です。

バージョン一覧ファイルを置く

リファレンス: Provider Registry Protocol Reference#List Available Versions

hashicorp/aws での例:

curl https://registry.terraform.io/v1/providers/hashicorp/aws/versions

/providers/example/hello/versions に、プロバイダーのバージョン一覧を記載したファイルを設置します。

  • /providers/ の部分は前の手順で設置した Discovery Protocol 用のファイルで指定したパスです。
  • example/hello はプロバイダーアドレスから決定します。

例えば以下のようなファイルになります:

{
  "versions": [
    {
      "version": "1.0.0",
      "platforms": [
        {"os": "linux", "arch": "amd64"},
        {"os": "linux", "arch": "arm64"}
      ]
    }
  ]
}

terraform はこのファイルと、 required_providers に記載されたバージョン制約、実行している OS とアーキテクチャーから、使用するプロバイダーのバージョン、OS、アーキテクチャーを決定します。

このファイルは以降で作成するプロバイダーパッケージ情報を収集して自動構築することもできると思います。

プロバイダーパッケージ情報を置く

リファレンス:

hashicorp/aws での例:

curl https://registry.terraform.io/v1/providers/hashicorp/aws/5.93.0/download/linux/amd64
curl https://releases.hashicorp.com/terraform-provider-aws/5.93.0/terraform-provider-aws_5.93.0_SHA256SUMS
curl https://releases.hashicorp.com/terraform-provider-aws/5.93.0/terraform-provider-aws_5.93.0_SHA256SUMS.72D7468F.sig
curl -O https://releases.hashicorp.com/terraform-provider-aws/5.93.0/terraform-provider-aws_5.93.0_linux_amd64.zip

/providers/example/hello/1.0.0/download/linux/amd64 に、プロバイダーのバージョン一覧を記載したファイルを設置します。

  • /providers/ の部分は前の手順で設置した Discovery Protocol 用のファイルで指定したパスです。
  • example/hello はプロバイダーアドレスから決定します。
  • 1.0.0, linux/amd64 の部分は、前の手順で設置したバージョン一覧ファイルから terraform が決定したバージョン、OS、アーキテクチャーが使用されます。

例えば以下のようなファイルになります:

{
  "protocols": [
    "6.0"
  ],
  "os": "linux",
  "arch": "amd64",
  "filename": "terraform-provider-hello_v1.0.0_linux_amd64.zip",
  "download_url": "terraform-provider-hello_v1.0.0_linux_amd64.zip",
  "shasums_url": "terraform-provider-hello_v1.0.0_linux_amd64_SHA256SUMS",
  "shasums_signature_url": "terraform-provider-hello_v1.0.0_linux_amd64_SHA256SUMS.sig",
  "shasum": "f57f6a379c014dcf3c522964aef8fe38e1b1afb1f781256539c4baa10e77ed0f",
  "signing_keys": {
    "gpg_public_keys": [
      {
        "key_id": "4794E56AC17DADB3",
        "ascii_armor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEZ/HX3RYJKwYBBAHaRw8BAQdAwtgb5Yc+Eyg5AurKOIzt0dMcXUX3tsV7Xilw\nU/QgRpu0IlByb3ZpZGVyIERlbW8gPGlrZWRhbUBleGFtcGxlLmNvbT6IkwQTFgoA\nOxYhBIl8EvH+RE0YUDVCgUeU5WrBfa2zBQJn8dfdAhsDBQsJCAcCAiICBhUKCQgL\nAgQWAgMBAh4HAheAAAoJEEeU5WrBfa2ziNEA/A3kcQhN4rhkWDkz+r9aCzFQove0\nqNfOpE/HrOdVG8p9AP9LYwjqe4EMFecCahi3tk5MzlLg/mcgSewutDuYwMLcDA==\n=mcPP\n-----END PGP PUBLIC KEY BLOCK-----\n"
      }
    ]
  }
}

このファイルに記述する情報を作成するのがプライベートレジストリー構築の山場になります。

プロバイダー本体とzipファイルの構築

filenamedownload_url で使用する zip ファイルを構築します。

ここで使用しているファイル名は、公式ドキュメントの Publish providers#Manually Preparing a Release に記載されたものに準じています。他のファイル名でも動作する可能性がありますが、公式ドキュメントの命名に従っておくのが安全でしょう。

  1. プロバイダーのバイナリーを terraform-provider-{NAME}_v{VERSION} という名称で作成します。

    CGO_ENABLED=0 go build -o terraform-provider-hello_v1.0.0 main.go
    
  2. バイナリーを terraform-provider-{NAME}_{VERSION}_{OS}_{ARCH}.zip という名前で zip にする。

    zip terraform-provider-hello_v1.0.0_linux_arm64.zip terraform-provider-hello_v1.0.0
    
    • 他にライセンスファイルなどを同梱しても良いです。

最終的に使用するのは zip ファイルだけなので、バイナリーファイルは削除してよいです。

SHA256 チェックサムファイルの作成

shasums_urlshasum で使用するために、 zip の SHA256 チェックサムを計算します。

shasums_url には sha256sum コマンドが出力する、以下のフォーマットのファイルを設置します:

f57f6a379c014dcf3c522964aef8fe38e1b1afb1f781256539c4baa10e77ed0f  terraform-provider-hello_v1.0.0_linux_amd64.zip

そして shasum には上記のチェックサムの部分、f57f6a379c014dcf3c522964aef8fe38e1b1afb1f781256539c4baa10e77ed0f を設定します。
このため shasums_urlshasum は冗長な関係にあります。

なお、 shasums_url は複数のプロバイダーパッケージで共有できます。このため、全プラットフォームのパッケージの SHA256 チェックサムを 1 つのファイルにまとめるなどのつくりにしてもよいでしょう。
今回は簡単のため、パッケージ毎に別のファイルで用意します。

例えば以下のようにして shasums_url に使用するファイルを出力します:

sha256sum terraform-provider-hello_v1.0.0_linux_arm64.zip \
  > terraform-provider-hello_v1.0.0_linux_arm64_SHA256SUMS

SHA256 チェックサムファイルの署名の作成

shasums_signature_url で使用する、 shasums_url のファイルの GnuPG 署名ファイルを作成します。
GnuPG の鍵ペアは事前に作ってあるものとします。

以下のように detach-sig コマンドを使用して署名ファイルを作成します:

gpg --output terraform-provider-hello_v1.0.0_linux_arm64_SHA256SUMS.sig \
  --detach-sig \
  terraform-provider-hello_v1.0.0_linux_arm64_SHA256SUMS

署名に使った鍵の ID の取得と公開鍵の出力

signing_keys.gpg_public_keys[].key_id に署名に使った鍵の ID を、
signing_keys.gpg_public_keys[].ascii_armor に署名に使った鍵の公開鍵を記載します。

鍵の ID は以下のようにして取得します:

$ gpg --list-secret-keys --keyid-format=long
[keyboxd]
---------
sec   ed25519/4794E56AC17DADB3 2025-04-06 [SC]
      897C12F1FE444D18503542814794E56AC17DADB3
uid                 [ultimate] Provider Demo <ikedam@example.com>

4794E56AC17DADB3 が鍵の ID です。

また、公開鍵を以下のようにして出力します:

$ gpg --armor --export 4794E56AC17DADB3
-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEZ/HX3RYJKwYBBAHaRw8BAQdAwtgb5Yc+Eyg5AurKOIzt0dMcXUX3tsV7Xilw
U/QgRpu0IlByb3ZpZGVyIERlbW8gPGlrZWRhbUBleGFtcGxlLmNvbT6IkwQTFgoA
OxYhBIl8EvH+RE0YUDVCgUeU5WrBfa2zBQJn8dfdAhsDBQsJCAcCAiICBhUKCQgL
AgQWAgMBAh4HAheAAAoJEEeU5WrBfa2ziNEA/A3kcQhN4rhkWDkz+r9aCzFQove0
qNfOpE/HrOdVG8p9AP9LYwjqe4EMFecCahi3tk5MzlLg/mcgSewutDuYwMLcDA==
=mcPP
-----END PGP PUBLIC KEY BLOCK-----

もちろん、 GnuPG の鍵は基本的に変更しないものなので、ここの出力は一度行ったものをずっと再利用できます。

パッケージ情報の JSON の構築

JSON を作成します:

{
  "protocols": [
    "6.0"
  ],
  "os": "linux",
  "arch": "amd64",
  "filename": "terraform-provider-hello_v1.0.0_linux_amd64.zip",
  "download_url": "terraform-provider-hello_v1.0.0_linux_amd64.zip",
  "shasums_url": "terraform-provider-hello_v1.0.0_linux_amd64_SHA256SUMS",
  "shasums_signature_url": "terraform-provider-hello_v1.0.0_linux_amd64_SHA256SUMS.sig",
  "shasum": "f57f6a379c014dcf3c522964aef8fe38e1b1afb1f781256539c4baa10e77ed0f",
  "signing_keys": {
    "gpg_public_keys": [
      {
        "key_id": "4794E56AC17DADB3",
        "ascii_armor": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEZ/HX3RYJKwYBBAHaRw8BAQdAwtgb5Yc+Eyg5AurKOIzt0dMcXUX3tsV7Xilw\nU/QgRpu0IlByb3ZpZGVyIERlbW8gPGlrZWRhbUBleGFtcGxlLmNvbT6IkwQTFgoA\nOxYhBIl8EvH+RE0YUDVCgUeU5WrBfa2zBQJn8dfdAhsDBQsJCAcCAiICBhUKCQgL\nAgQWAgMBAh4HAheAAAoJEEeU5WrBfa2ziNEA/A3kcQhN4rhkWDkz+r9aCzFQove0\nqNfOpE/HrOdVG8p9AP9LYwjqe4EMFecCahi3tk5MzlLg/mcgSewutDuYwMLcDA==\n=mcPP\n-----END PGP PUBLIC KEY BLOCK-----\n"
      }
    ]
  }
}
  • protocols
    • このプロバイダーがサポートする terraform の GRPC プロトコルのバージョンのリストを記載します。
    • ここについての明確な資料が見当たらず、どういうフォーマットで指定するのか (X.X なのか、 X.X.X なのかとか)、マイナーバージョン以下のバージョンがどういうものがあるのか、プロバイダーのプロトコルバージョンをどうやって確認すればいいのかが不明です。
    • ただ、 Terraform plugin protocol を見る限り、以下の規則で設定すれば良さそうです:
      • terraform-plugin-framework で作成している: 6.0
        • terraform-plugin-framework は 5.0 にも対応しているようなのですが、その切替がどのように行われるのかが不明です。
      • terraform-plugin-sdk/v2 で作成している: 5.0
  • os, arch
    • このパッケージのプラットフォームを記載します。
  • filename
    • zip ファイルのファイル名を記載します。
  • download_url
    • zip ファイルをダウンロードするための URL を記載します。相対パスでの指定も可能なので、ファイルをこの JSON ファイルと同じディレクトリー内において、ファイル名だけ指定するのが簡単でしょう。
  • shasums_url
    • sha256sum で作成したファイルをダウンロードするための URL を記載します。相対パスでの指定も可能なので、ファイルをこの JSON ファイルと同じディレクトリー内において、ファイル名だけ指定するのが簡単でしょう。
  • shasums_signature_url
    • gpg --detatch-sig で作成したファイルをダウンロードするための URL を記載します。相対パスでの指定も可能なので以下略。
  • shasum
    • sha256sum で作成したチェックサムの値を記載します。
  • signing_keys.gpg_public_keys[0].key_id
    • 署名に使用した GnuPG 鍵の ID を記載します。
    • 実際にはこの情報は ascii_armor にも含まれているはずなので、単に冗長に情報を含めているものと思われます。
  • signing_keys.gpg_public_keys[0].ascii_armor
    • 署名に使用した GnuPG 鍵の公開鍵を記載します。

完成

ここまでの手順で作ったファイルを Web サイトに配置し、Terraform テンプレートで以下のように required_providers で指定を行えば、自作プロバイダーをレジストリーからダウンロードする形で Terraform で利用できるようになります。

terraform {
  required_providers {
    hello = {
      source  = "example.com/example/hello"
      version = "~> 1.0.0"
    }
  }
}

terraform init などの処理でエラーが出る場合は、 TF_LOG=debug を設定して詳細ログを出力して動作を確認するとよいでしょう。

Discussion

ログインするとコメントできます