Elido
3分で読了エンジニアリング
コア記事

短縮リンクをTerraformで管理する

URLショートナー分野で唯一のTerraformプロバイダー - terraform-provider-elidoをリリースしました。それが何をするか、リソースのライフサイクルがどのように機能するか、そしてその背後にあるエンジニアリングのトレードオフについて説明します。

Marius Voß
DevRel · edge infra
Three-stage diagram: HCL config flowing into terraform-provider-elido, then into the api-core REST surface, with a side panel listing the four resources and data sources shipped today

小さな主張をして、それを裏付けます。現在、ファーストクラスのTerraformプロバイダーを提供しているURLショートナーは存在しません。 Bitly、TinyURL、Rebrandly、Short.io、Dub.co - 5つすべてがREST APIを公開し、いくつかはWebhookを公開していますが、terraform-provider-* を公開しているものはありません。Bitlyのv3 API用のコミュニティプロバイダーがGitHubに存在しますが、メンテナンスされておらず、APIサーフェスの4分の1程度しかカバーしていません。それがギャップです。

数週間前、私たちはそれを埋めるために取り組みました。結果は terraform-provider-elido で、Elido APIは今日から elido_link(リソース)、elido_workspace(データソース)、elido_custom_domain(今のところデータソース - 続きをお読みください)として公開しています。以下は、リリースされたもの、背後にあるエンジニアリングの選択、そしてv0.1.0では意図的にリリースしなかった部分のツアーです。プロバイダーはElidoの残りの部分と同じライセンスのオープンソースで、tools/terraform-provider-elido/にあります。

短縮リンクがTerraformに属する理由#

議論は短いです。マーケティングのリダイレクトを実行している場合、同じキャンペーンに収束する他のインフラコンポーネントがすでにあります:

  • ランダーを指すCloudflare DNSレコード。
  • そのランダーを提供するS3バケットとCloudFrontディストリビューション。
  • 署名付きURLを生成するLambdaまたはCloud Runサービス。
  • Google Tag ManagerまたはSegmentにベイクされたキャンペーンタグ。

2026年には、これら5つはすべてTerraformで管理されています。ファネルの先頭に置かれた短縮リンク - ユーザーが実際にクリックするエントリポイント - はGoogle Docにあります。そのギャップがドリフトの源です。ランダーが廃止され、それを指すリダイレクトが誰かがSlackでマーケティングに連絡するまで、404を積み上げながら生き続けます。

そのギャップを修正する方法は2つあります。TerraformのアウトプットとREST APIの間に座るTypeScriptのグルースクリプトを書くことができます。それは機能します。まさにそれをしている顧客がいます。または、リダイレクトがCloudflareレコードの隣の resource ブロックで、terraform plan / terraform destroy が他のすべてのものを知っているのと同じ方法でそれを知っている、本物のTerraformプロバイダーを提供することができます。私たちは2番目のパスを選びました。1番目のパスはすでにあなたの担当でした。

terraform-provider-elido が今日できること#

最小限のv0.1.0サーフェス(HCLで):

terraform {
  required_providers {
    elido = {
      source  = "elidoapp/elido"
      version = "~> 0.1"
    }
  }
}

provider "elido" {
  # api_url   defaults to https://api.elido.app
  # api_token reads ELIDO_API_TOKEN
}

data "elido_workspace" "main" {
  id = 42
}

data "elido_custom_domain" "links" {
  workspace_id = data.elido_workspace.main.id
  hostname     = "links.example.com"
}

resource "elido_link" "spring_campaign" {
  workspace_id    = data.elido_workspace.main.id
  domain_id       = data.elido_custom_domain.links.id
  slug            = "spring-2026"
  destination_url = "https://example.com/landing/spring"
  title           = "Spring 2026 email campaign"
  tags            = ["spring-2026", "email"]
  redirect_status = 301
}
宣言的なリンクのライフサイクル:HCLのelido_linkブロックがterraform planに渡され、差分を生成し、terraform applyがElido api-core RESTサーフェスを呼び出してリンクを作成・更新・削除する。スラグ変更時はPATCHを発行し、workspace_idまたはdomain_idが別のエッジルートに移動する場合のみ置き換えが行われる

terraform apply すれば完了です。ドリフト検出はAPIがエコーバックするフィールドで機能します。Terraformリソースラベルの名前変更や、途中での slug の変更は置き換えを強制しません - プロバイダーは同じ数値IDに対してPATCHを発行します。workspace_iddomain_id の変更は置き換えを強制します。なぜなら、その時点で別のエッジルートについて話しているからです。これは常識的なライフサイクルであり、HashiCorpのプラグインフレームワークガイドが向かわせる方向でもあります。

一括ロールアウトの形状は、ほとんどのチームにとって作業を正当化する部分です:

locals {
  channels = ["email", "twitter", "linkedin", "reddit", "hn"]
  regions  = ["us", "eu", "apac", "latam"]
}

resource "elido_link" "campaign_launch" {
  for_each = {
    for pair in setproduct(local.channels, local.regions) :
    "${pair[0]}-${pair[1]}" => pair
  }
  workspace_id    = data.elido_workspace.main.id
  domain_id       = data.elido_custom_domain.links.id
  slug            = "launch-${each.key}"
  destination_url = "https://example.com/launch?ch=${each.value[0]}&r=${each.value[1]}"
  tags            = ["launch-2026", each.value[0], each.value[1]]
}

20のリンク、1回のapply。ブロックを削除すると、20の削除、1回のapply。これはほぼ先四半期の3つの顧客コールのメモに現れたユースケースです。マーケティングはローンチのためにチャンネルごと・リージョンごとのUTMリンクを欲しがり、エンジニアリングは毎回Sheets-to-APIスクリプトを構築し、スクリプトが古くなり、スクリプトの作者が会社を去ります。Terraformの強みはここでは新規性ではありません - パターンを退屈にしたことです。

属性リファレンスとインポートの例を含む完全なガイドは/docs/guides/terraformにあります。プロバイダーのソースには、上記のスニペットのより詳細なバージョンである examples/main.tf が付属しています。

プロバイダーのビルド方法#

約600行のGoで、そのうち約200はスキーマ定義です。構造:

tools/terraform-provider-elido/
├── main.go                                   # plugin entrypoint
├── internal/provider/
│   ├── provider.go                           # config + auth
│   ├── link_resource.go                      # CRUD + import
│   ├── workspace_data_source.go              # GET /v1/workspaces/{id}
│   ├── custom_domain_data_source.go          # GET /v1/workspaces/{id}/domains
│   ├── helpers.go                            # tag conversion
│   └── provider_test.go                      # 7 unit tests
├── go.mod                                    # depends on packages/sdk-go
├── .goreleaser.yml                           # signed-checksum builds
├── terraform-registry-manifest.json          # protocol_versions: ["6.0"]
├── Makefile                                  # build + install-local + testacc
└── examples/main.tf

いくつかの選択を指摘する価値があります。

レガシーSDKではなくプラグインフレームワークを使用しています。 HashiCorpは2023年に新しいプロバイダーを明示的にterraform-plugin-frameworkに向けました。人気のあるプロバイダー(awscloudflaregoogle)のほとんどは移行の途中です。より小さく新しいプロバイダーはフレームワークネイティブです。レガシーSDKでグリーンフィールドを構築することは、リリースした瞬間に移行タスクを抱えることを意味しました。移行を作らないことでそれを回避しました。フレームワークには厳密な型システム、プラグインプロトコルレベルでの本物のスキーマ検証、そしてはるかにクリーンなプランニングモデル(CustomizeDiff コールバックの代わりに PlanModifiers)があります。小さなプロバイダーにとって、エルゴノミクスのギャップは大きいです。

プロバイダーはSDKを複製しません。 すべてのリソースメソッドはpackages/sdk-goに委任します。これは私たちがプレーンGoの統合に公開しているのと同じSDKです。プロバイダーは設計上、薄いSchema-to-SDKアダプターです。これには2つの結果があります。良い方:SDKで修正したバグはプロバイダーに無料で適用されます。悪い方:SDKのギャップはプロバイダーのギャップです。率直な例はカスタムドメインです。api-core はまだ /v1/workspaces/{id}/domains のPOST/DELETEを公開していません。書き込みパスはダッシュボードの背後の domain-manager にあります。api-coreが書き込みをプロキシするまで、SDKに Domains.Create がなく、プロバイダーに elido_custom_domain リソースがありません - ホスト名で既存のものを検索するデータソースのみです。v0.2.0でそのギャップを埋めます。プロキシシムは1週間以内の変更で、SDK + プロバイダーのPRはすでに下書き済みです。

認証は他のすべてのElidoクライアントと同じ形状です。 Authorization ヘッダーにベアラーAPIキーを使用し、環境の ELIDO_API_TOKEN にフォールバックします。プロバイダーでCookie認証や X-Dev-User-ID を公開しません。これらはローカル開発の便利機能で、コンフィグがバージョン管理に保存されCIで実行されるIaCには関係ありません。CIにはトークンがあるかないかのどちらかです。

ドリフト検出:見た目より難しい部分#

明らかな部分を読み過ごしたなら、これが読む価値のあるセクションです。Terraformのdiffingは根本的に次の質問です:ユーザーが書いたもの(Plan)、サーバーが前回返したもの(State)、サーバーが今返すもの(Read)が与えられたとき、何を提案すべきか?

ドリフト検出は3つの入力を比較する:HCLプランの望ましい状態、記録されたTerraformのstate、api-coreからのライブ読み取り。ライブ読み取りが乖離した場合、プロバイダーは修正PATCHを提案する。サーバーでデフォルト設定されたフィールドはUseStateForUnknownプランモディファイアで保持されるため、偽のドリフトとして表示されない

elido_link のようなリソースにとって、3つのことがこれを非自明にします:

サーバーデフォルト付きのOptional + Computedフィールド。 ユーザーは redirect_status を省略できます。サーバーは 302 を埋めます。次の Read302 を返します。注意しないと、これはすべてのplanでドリフトのように見えます - 「何も要求しなかったのに302が返ってきた、何もない状態に戻すよう提案する」。フレームワークは UseStateForUnknown プランモディファイアを提供します。「プランされた値がない場合、stateにあるものを保持する」というものです。サーバーがデフォルトを持つすべてのフィールドに使用します。これは些細に聞こえますが、エコシステムで最も頻繁なプロバイダーバグ(「プロバイダーがapply後に一貫性のない結果を生成した」)の源です。

サーバーサイドの正規化を持つタグ。 APIはタグをセットとして保存します。Terraformはそれらを順序付きリストとして見ます。今のところ、これは棚上げにしています。サーバーはエコー時に順序を保持するため、実際にはdiffは安定していますが、HCLでタグを並べ替えたユーザーはno-opのアップデートを見ることになります。これは正しい動作です。代替案 - 入力時に黙ってソートする - は terraform planterraform apply が変更点について意見が合わないことを意味します。これはTerraformにおける最も重大な罪です。実際の顧客が不満を言ったら再検討します。HashiCorpのベストプラクティスガイドは「予期しないことはしない」側に完全に立っています。

三状態としてのステータス。 リンクは activepaused、または archived になれます。HCLで status = "paused" を設定しているのにCreateでは(サーバーはデフォルトで active)ない場合、同じ Create 内でフォローアップのPATCHを発行する必要があります。これはCreate後の調整ステップとして実装されています - ソースを読んでいる場合は覚えておいてください。代替案 - ステータスを別のリソース(link_id でキー付けされた elido_link_status)として公開する - はAWSプロバイダーがいくつかのリソースでやっていることです。検討しましたが、1つのオプションフィールドの場合、コストがベネフィットを上回ります。2番目のCreate後のノブを追加したら再考します。

インポート。 terraform import elido_link.spring_campaign 42:7 - これは <workspace_id>:<link_id> です。フレームワークの ImportState コールバックが単一の文字列を提供し、自分でパースするため、コロン区切りの形式を選びました。<id>:<id> の形状は、タプルでリソースをキー付けするプロバイダーで一般的です - 標準的なリファレンスについてはgoogle_compute_instance のインポートドキュメントを参照してください。人間が読める slug を多重使用しないことを意図的にしています。リソースステートは数値IDでキー付けされており、インポートに入れるべきものはそれだけです。

テスト、CI、レジストリ#

ユニットスイート(今日は7テスト)はスキーマ検証レイヤーと純粋な関数ヘルパー - splitImportIDlinkToModelapiErrorStringoptString - をカバーします。0.5秒で実行され、13のサービスをビルドする同じ go マトリクスを通じてすべてのPRをゲートします。TF_ACC=1 が設定されたときにライブの api-core に対して実行される testacc ターゲットもありますが、これはオプトインです:トークンが必要で、各テストが実際のリンクを作成・削除するため、すべてのコミットで実行しません。HashiCorpのテストフレームワークがパターンを文書化していて、私たちは逸脱しません。

ショートリンクのGitOpsワークフロー:HCLコンフィグの編集がプルリクエストを開き、CIがterraform planとgoのユニットテストを実行し、レビュアーが差分を承認してmainにマージし、マージがapi-coreに対してterraform applyをトリガーしてエッジが次のクリックで変更を反映する

リリースパイプラインはTerraformレジストリが期待する正確なビルドマトリクスで goreleaser に接続されています:linuxdarwinfreebsdwindows × amd64/arm64(LinuxではさらにARMと386も)、アーカイブ上のSHA256SUMS、SHA256SUMS上のGPG署名、protocol_versions: ["6.0"] を宣言する terraform-registry-manifest.json。コミットに terraform-provider-vX.Y.Z とタグを付けると、GitHub Actionsワークフローが goreleaser release --clean を実行し、GitHub Releaseが公開されます。Terraform Registryは自身のスケジュールでリリースをポーリングしてバージョンを取り込みます。現在唯一欠けているのはGPGキーです - 今週プロバイダーリリース専用のキーを発行しています。つまり v0.1.0 はこの記事とほぼ同時にレジストリに登録されます。

それまでは、~/.terraformrcdev_overrides 経由でインストールできます:

provider_installation {
  dev_overrides {
    "elidoapp/elido" = "/Users/<you>/.terraform.d/plugins/elidoapp/elido"
  }
  direct {}
}

その後 tools/terraform-provider-elido/ から make install-local を実行すると、terraform init なしで terraform plan がバイナリを直接解決します。これはプロバイダー開発のための公式のHashiCorpパターンで、v1.0.0までの暫定的なインストールパスとしても同様に機能します。

v0.1.0に意図的に含まれていないもの#

検討して、リリースしなかった3つのことを、誰も驚かないように言及しておきます。

elido_custom_domain をリソースとしては含まない。 上で説明しました。データソースは domain_idelido_link に連鎖させるのに十分で、それが重要なユースケースです。フルライフサイクル管理はapi-coreを待ちます。ETA:v0.2.0、2026年中頃。

elido_folderelido_api_key もない。 SDKは両方を持っています。v0.1.0ではスキーマを追加しないことを選択しました。そのライフサイクルが顧客の痛みのある場所ではないからです。フォルダーは組織のメタデータです。APIキーは通常1度発行されてダッシュボードでローテーションされます。誰かが要求したら追加します。

OpenAPI仕様からのコード生成はない。 HashiCorpはベータツールとしてterraform-plugin-codegen-openapiを提供しています。仕様でそれを試しました。生成されたスキーマは平凡でした - すべてのnullableフィールドが Optional + Computed になり、すべてのリストが Set になり、結果は手書きのスキーマと同じ量の修正が必要で、進化させるのが難しいです。テーブルに3つのリソースがある場合、手書きが勝ちます。6ヶ月後に、より多くの同僚が実戦テストした時点でジェネレーターを再検討します。

ビルド中に壊れたもの#

最初のパスで間違えた3つのこと。

最初は Optional + Computed のstateでした。当初 title をプレーンな Optional 文字列としてモデル化しました。HCLで省略した顧客はクリーンなCreateを得ましたが - その後 terraform plan のたびにnullに設定し直すよう提案されました。サーバーが空の文字列を保存して、Terraformがそれをドリフトとして読み取ったからです。修正は UseStateForUnknown プランモディファイアでした。教訓は、プロバイダーの「ユーザーが指定しなかった」の解釈がサーバーの「デフォルト値」の考えと一致しなければならないということです。フレームワークのドキュメントは導入部でこれについて警告しています。最初に警告を読み飛ばしました。ここに書いておくことで恥ずかしさを省きます。

2番目はインポート形式でした。最初はパスが自然に読めるという考えで <workspace_id>/<link_id> をスラッシュで提供しました。フレームワークには問題ありませんでしたが、HCLリンターとターミナルには問題がありました。1つのシェルクォートされた引数内に2つのスラッシュを持つパスは、サポートチケットのタイポのように見えます。コロンに切り替えました。コロンには曖昧さがなく、Googleのプロバイダーの慣習と一致します。教訓:インポート文字列はユーザー向けのUIであり、UIのように設計してください。

3番目はタグの順序付けでした。上で説明しました - 棚上げにしました。誰かが要求するまで棚上げし続けます。私たちがほぼリリースしたバージョンは入力時に黙ってタグをソートしていました。これは顧客が明らかにそれらを並べ替えたときに terraform plan が変更なしと報告することを意味しました。それはノイジーなdiffよりも悪いエクスペリエンスです。内部テスト中に発見しました。プロバイダーを書くときにユーザー入力を正規化することで「親切にする」誘惑は常にあり、ほぼ常に間違った判断だということを言っておく価値があります。

これをElidoの残りの部分と使う方法#

プロバイダーは1つの形状です。他の形状はまだ存在し、なくなりません:

  • REST APIが真実のソースです。プロバイダーがすることはすべて curl でもできます。
  • Go SDKはプロバイダー自体が内部的に使用するものです。ライブラリとして取り込むことができます。
  • TypeScriptとPythonのSDKは、たまたま使っている言語用に同じサーフェスをカバーします。
  • GraphQLエンドポイントは、画面に合わせた形で1回のラウンドトリップで同じ読み取りをカバーします。

問題の形状に合うものを選んでください。Terraformは管理すべきライフサイクルがある場合に適しています。SDKはスクリプトがある場合に適しています。REST APIは1つのことを1度だけする場合に適しています。これが明白であるべきだと思います。4つすべてを機能させ続けます。

欠けている好きなTerraformパターンがある場合 - data "external" ブロックでCSVから for_each を使った一括インポート、キャンペーントラッキングのためにLinear APIに形作られた for_each、複数テナントを管理するエージェンシー向けのラッピングモジュール - area:terraform ラベルを付けてGitHub リポジトリにIssueを開いてください。プロバイダーはこれらのパターンを退屈にするために存在します。どれがまだ驚くように感じるかを知りたいです。

どこから始めるか#

これを読んで試してみたい場合:ガイドに従ってプロバイダーをインストールし、サンドボックスワークスペースに向け、常にコードで宣言したかったリダイレクトのために resource "elido_link" を書いて、terraform apply します。最初に驚くのは(良い意味で)terraform destroy が期待通りに機能することだと賭けます。

これを読んでAlternativeと比較したい場合 - Bitlyオルタナティブ機能ギャップの記事にもっと長い説明があり、/compare/vs-bitlyのサイドバイサイドにTerraformがマトリクスのどこにあるかが示されています。この記事が公開されてから、彼らのマトリクスは短くなっています。

  • Marius

ブログの関連記事#

Elidoを試す

URLを貼り付けて短縮リンクを取得

登録不要。リンクは30日間有効。永久に保存するには登録してください。

Free、登録不要 · 1日あたり2件

Elidoを試す

EUホスティングのURL短縮サービス。カスタムドメイン、詳細な分析、オープンAPI付き。無料プラン - クレジットカード不要。

タグ
terraform
infrastructure as code
url shortener
developer experience
devops
iac

続きを読む