freeeの開発情報ポータルサイト

freeeにおけるAuthlete Terraform運用プラクティス

こんぺこ。freeeでID Federationを担当している てらら です。ぺこ。

最近はバイオハザードレクイエムの追加シナリオがあるのかないのかどっちなのかを心待ちにして過ごしています。今日はそんな中、freeeで採用している Authlete のTerraform運用についてまとめてみました。特にfreeeではfreeeアプリストアを使った特殊な要件が多いためそういった事情も踏まえて紹介していきます。

はじめに

freeeでは、OAuth 2.0 / OpenID Connect に基づく認可基盤(以降、「認可基盤」と呼びます)をマイクロサービスにて運用しています。この認可基盤のバックエンドには Authlete を採用しています。

Authleteは「Component as a Service(CaaS)」と呼ばれる形態のサービスで、認可基盤そのものではなく、認可基盤を構築するためのコンポーネントをAPI経由で提供します。つまり、認可基盤のエンドポイント実装はfreee側で行いつつ、OAuthクライアントの管理やトークン発行といったコアロジックはAuthleteに委譲するアーキテクチャです。

Authleteには Terraform Registry が公式に提供されており、認可基盤に格納するデータをTerraformで管理することが可能です。

前提: freeeにおける認可基盤の利用者

freeeの認可基盤には、大きく4種類の利用者がいます。

  • Public API開発者: freee APIを外部公開するにあたり、API単位のOAuthスコープ設計に関わる
  • サードパーティ開発者: freee APIを利用するOAuthクライアントを開発する外部の開発者
  • 自社プロダクト開発者: freee内部のプロダクトチームが開発するクライアント。サードパーティとは異なるクライアント設定が必要な場合が多い
  • グループジョインプロダクト開発者: M&A等で新たにグループに加わったプロダクトが、まずはOIDCでfreeeのID連携を利用する

これらの利用者に対して、Authleteで管理するデータは大きく2つに分かれます。認可基盤の設定(Authlete Service: OAuthスコープ、エンドポイント、JWKなど)は全利用者に共通で影響するもの、OAuthクライアントの設定(Authlete Client: client_id、client_secret、redirect_uriなど)は主に自社プロダクト開発者やグループジョインプロダクト開発者が日常的に追加・変更するものです。

IaC化の背景と動機

IaC導入以前の運用は、大きく2つのフェーズを経ています。

初期は、認可基盤の管理画面(admin画面)にOAuthクライアントの登録・変更を行う専用機能を実装して運用していました。しかし、admin画面の機能は設定項目の増加やプロダクト側の要件変化に追従し続ける必要があり、メンテナンスコストが課題となりました。

さらに突発的な変更要件や結合テスト環境への設定変更に関してはKubernetes Pod内で認可基盤APIを直接呼び出すCLIベースの運用もありました。手動でのCLIコマンド実行には設定ドリフト、操作の属人化、再現性の欠如といった別の課題がありました。

IaC化はこれらの課題を一度にすべて解決しようとしたわけではなく、段階的に進みました。

第1段階: 本番と開発の設定値を揃える(Service)

最初の動機は、本番環境と開発環境で認可基盤の設定値を一致させ、環境依存の問題を防ぐことでした。OAuthスコープ100以上、サポートするクレームや各種エンドポイントの設定が環境間で食い違っていると、「開発環境では動くが本番では動かない」という問題が起きます。Authlete Serviceの設定をTerraformモジュール化し、全環境で同じ定義を再利用することで、この問題を根本的に解消しました。

第2段階: 結合テスト環境の運用コスト削減(Service)

次の動機は、複数ある結合テスト環境ごとのService作成・運用コストを下げることでした。freeeでは10以上の開発環境が存在します。これらすべてにAuthleteのServiceを手動で構築・維持するのは大きな負担です。Terraformモジュールの再利用により、新環境の追加はたった6行の定義ファイルで済むようになりました。

第3段階: 自社プロダクト開発の特殊要件への対応(Client)

Service側のIaC化が軌道に乗った後、Authlete ClientもIaC管理の対象に拡大しました。自社プロダクト開発では、サードパーティとは異なる設定が必要であり、さらに各結合テスト環境間で同じclient_idとclient_secretを使いたいという開発者サイドからのニーズがあります。特に後者に関しては操作の属人化問題が大きな課題となりました。これらに関してもClientのIaC化により、シード値のコード管理と環境間の自動同期が実現しました。

リポジトリ構成とモジュール設計

Terraform管理用のリポジトリは以下のような構成になっています。

authlete-console/
├── module/                          # 再利用可能なTerraformモジュール
│   ├── authlete_service/            # 認可基盤(サービス)の設定
│   │   ├── main.tf                  # サービスリソース定義
│   │   └── supported_scopes.tf      # OAuthスコープ定義
│   ├── authlete_client/             # OAuthクライアントの設定
│   │   └── main.tf                  # クライアントリソース定義
│   └── client_seeds/                # クライアントのシード値定義
│       ├── local/main.tf            # ローカル開発用シード
│       └── integration/main.tf      # 結合テスト環境用シード
├── development/                     # 開発環境の定義
│   ├── env_alpha.tf, env_beta.tf, ...  # 各開発環境
│   ├── staging.tf                   # ステージング環境
│   └── local/                       # ローカル開発環境
│       ├── alice.tf, bob.tf, ...    # 各開発者のローカル環境
│       └── locals.tf                # シード値の読み込み
├── production/                      # 本番環境の定義
│   └── main.tf
└── .github/workflows/               # CI/CDパイプライン

アーキテクチャの核となるのは3つのモジュールです。

authlete_service モジュール は、Authleteの「サービス」(= 1つの認可基盤インスタンス)を定義します。issuer URL、各種エンドポイント、JWK(JSON Web Key)、サポートするOAuthスコープやクレーム情報を管理します。

# module/authlete_service/main.tf(抜粋)
resource "authlete_service" "as" {
  issuer       = var.issuer
  service_name = "${var.stage}_${var.project_name}"  # 例: "env_alpha_auth_platform"
  supported_grant_types    = var.supported_grant_types
  authorization_endpoint   = "${var.issuer}/public_api/authorize"
  token_endpoint           = "${var.issuer}/public_api/token"
  revocation_endpoint      = "${var.issuer}/public_api/revoke"
  dynamic "supported_scopes" {
    for_each = coalesce(var.supported_scopes, local.supported_scopes)
    content {
      name = supported_scopes.value.name
      dynamic "attributes" {
        for_each = supported_scopes.value.attributes
        content {
          key   = attributes.value.key
          value = attributes.value.value
        }
      }
    }
  }
  jwk { TF_VAR_... }  # JWKの設定(署名鍵)
}
output "api_key" {
  value = authlete_service.as.id
}
output "api_secret" {
  value     = authlete_service.as.api_secret
  sensitive = true
}

authlete_client モジュール は、個々のOAuthクライアント(Relying Party)を定義します。client_id_alias、client_secret、redirect_uri、grant type、トークン有効期限などを管理します。

client_seeds モジュール は、各環境に投入するOAuthクライアントのシード値(初期データ)を定義します。ローカル開発用とステージング用でモジュールを分離しています。

メリット1: 環境別に認可基盤の設定値を統一

authlete_service モジュールを全環境で再利用することで、全ての環境で認可基盤の設定値が一貫して管理されています。

各環境の定義ファイルは、stage(環境名)、issuer(issuer URL)、jwk_json_string(署名鍵)のみが異なるシンプルな構成です。なお、JWKの署名鍵はリポジトリに直接格納せず、SecretsからTF_VAR_環境変数として注入しています。

# development/env_alpha.tf(開発環境の例 — たった6行)
module "env_alpha_authlete_service" {
  source          = "../module/authlete_service"
  stage           = "env_alpha"
  issuer          = "https://env-alpha.example.co.jp"
  jwk_json_string = var.DEV_OPENID_CONNECT_JWK
}
# production/main.tf(本番環境 — 同じモジュール、異なるパラメータ)
module "production_authlete_service" {
  source          = "../module/authlete_service"
  stage           = "production"
  issuer          = "https://example.co.jp"
  jwk_json_string = var.PRODUCTION_OPENID_CONNECT_JWK
}

freeeでは結合テスト環境にウイスキーの銘柄名やツバメの名前をつける文化があり、10以上の結合テスト環境がそれぞれ独自の名前を持っています。これだけの数の環境があっても、各環境の定義ファイルはたった6行で済みます。

これはIaCの基本的なメリットではありますが、OAuthスコープ、サポートするクレーム、各種エンドポイント設定を含む認可基盤の設定が全環境で一貫していることの価値は大きいです。スコープの追加や変更をモジュール側で行えば、全環境に自動で反映されます。

メリット2: ローカルシード値のコード管理

IaC化により、ローカル開発環境のOAuthクライアント(シード値)をコードで管理できるようになりました。アプリケーション開発におけるDB Seedsと同様の考え方で、認可基盤の初期データをコードとして定義・投入する仕組みです。

module/client_seeds/local/main.tf に全クライアントのシード値をコードとして定義しています。

# module/client_seeds/local/main.tf(抜粋・値は例示)
output "value" {
  value = {
    "alpha-dev" = {
        client_id_alias       = "alpha_app_uid"
        client_secret         = "alpha_app_dev_secret"
        client_name           = "alpha-app-dev"
        redirect_uris         = ["com.example.app.debug:///auth-redirect-dev"]
        client_type           = "CONFIDENTIAL"
        client_grant_types    = ["AUTHORIZATION_CODE", "REFRESH_TOKEN"]
        client_response_types = ["CODE", "TOKEN"]
        token_auth_method     = "CLIENT_SECRET_POST"
        description           = "Alpha app development client"
        attributes = {
          freee_client_type = "internal"
        }
    },
    "beta-dev" = {
        client_id_alias       = "beta_uid"
        client_secret         = "beta_dev_secret"
        # ... 以下同様
    },
    # 他にも多数のクライアント定義が続く
  }
}

各ローカル環境は、このシード値を for_each で読み込んで全クライアントを自動プロビジョニングします。

# development/local/locals.tf
module "client_seeds" {
  source = "../../module/client_seeds/local"
}
locals {
  client_seeds = module.client_seeds.value
}
# development/local/alice.tf(開発者 alice のローカル環境)
module "alice_authlete_service" {
  source          = "../../module/authlete_service"
  stage           = "alice"
  issuer          = "https://accounts.dev.example.co.jp"
  jwk_json_string = var.DEV_OPENID_CONNECT_JWK
}
module "alice_local_common_authlete_client" {
  source   = "../../module/authlete_client"
  for_each = local.client_seeds  # 全シード値をループ
  service_api_key    = module.alice_authlete_service.api_key
  service_api_secret = module.alice_authlete_service.api_secret
  client_id_alias    = each.value.client_id_alias
  client_secret      = each.value.client_secret
  client_name        = each.value.client_name
  redirect_uris      = each.value.redirect_uris
  # ... その他のパラメータ
}

新しいOAuthクライアントが必要になった場合、client_seeds/local/main.tf にエントリを追加するだけで、全ローカル環境に自動的に反映されます。個別環境でseedコマンドを打つ必要はありません。

メリット3: 環境間のclient_id / client_secret同期とセキュアなオペレーション

3つ目のメリットは、複数のローカル環境間でclient_idとclient_secretを自動的に同期でき、しかもそれが人間の手作業を介さないためセキュアに実行できることです。

環境ティアに応じたシークレット戦略

ローカル開発環境とステージング環境では、client_secretの管理戦略を意図的に分離しています。 - ローカル環境: 開発者の利便性を優先し、人間可読な固定値(例: "alpha_dev_secret")を使用します。 - ステージング環境: Terraformの Random Provider でclient_secretを自動生成し、keepers によるローテーション機構を備えています。 これにより、secret の値を人間が共有する必要がなく、かつ必要に応じて再発行が可能です。

# module/client_seeds/staging/main.tf
variable "secret_rotation_trigger" {
  description = "Change this value to trigger client_secret regeneration (e.g. increment: 1 -> 2)"
  type        = string
  default     = "1"
}
resource "random_bytes" "example_client_staging" {
  length = ...
  keepers = {
    rotation_trigger = var.secret_rotation_trigger
  }
}
locals {
  example_client_staging_secret = <random_bytesから生成した値>
}

ステージング環境でシークレットをローテーションしたい場合は、secret_rotation_trigger の値をインクリメントしてPRを作成するだけです。keepers の値が変わることで新しいランダム値が生成され、terraform apply で自動的に反映されます。このフロー全体がコードレビューとCI/CDを経由するため、監査可能です。

メリット4: その他の効果

PRベースのレビューワークフロー

OAuthクライアントの追加やスコープの変更など、認可基盤への設定変更がすべてPull Requestを経由するようになりました。PRが作成されると terraform plan が自動実行され、計画結果がPRコメントに投稿されます(tfcmt を利用)。チームメンバーが変更内容をレビューし、マージ後に terraform apply が自動実行されます。

認可基盤の設定変更は影響範囲が大きいため、このレビューワークフローはセキュリティと品質の両面で重要です。

パスベースフィルタリングによる選択的デプロイ

CI/CDパイプラインは変更されたディレクトリに基づいて、影響を受ける環境のみを対象にplan/applyを実行します。

# .github/modules-path-filter.yml
development:
    - 'development/*'
    - 'module/*'
    - 'module/*/*'
development/local:
    - 'development/local/*'
    - 'module/*'
    - 'module/*/*'
production:
    - 'production/*'
    - 'module/*'
    - 'module/*/*'

ローカル環境のみの変更であれば本番環境のplan/applyは実行されません。一方で、module/ 配下の変更は全環境に影響するため、すべての環境でplan/applyが実行されます。この設計により、安全性とCI/CDの実行効率を両立しています。

新環境の追加が容易

新しい開発者のローカル環境を追加する場合、既存のファイル(例: alice.tf)をコピーし、モジュール名と stage の値を変更するだけです。全OAuthクライアントが for_each により自動的にプロビジョニングされるため、数分で完全な認可基盤環境が構築されます。

スコープ定義の一元管理

OAuthスコープ定義が supported_scopes.tf に集約されています。freeeの場合、会計、人事労務、請求書など多数のプロダクトがそれぞれOAuthスコープを持っており、これらが一箇所で管理されるため、スコープの追加・変更時のレビューが容易で、全環境への反映漏れが防止されます。

まとめ

Authlete Terraform Providerを活用して認可基盤のIaC管理を導入した結果、以下の効果が得られました。

  1. 環境一貫性: 全ての環境で認可基盤の設定値を統一的に管理
  2. ローカルシード値のコード管理: 手動のPod操作を排除し、クライアント定義をコードとして管理
  3. セキュアな環境間同期: CI/CDを通じたclient_id/client_secretの自動同期で、人間の手作業を排除
  4. 運用品質の向上: PRレビュー、選択的デプロイ、新環境の迅速な構築を実現

昨今ではAIエージェントの台頭により認可基盤への依頼も増えつつあります。そういった中、IaCによる準備を十分に行なっておくことでスピーディーなプロダクトデリバリーへ貢献できます。Authleteはサービスそのものの価値で十二分の効果を得られますが、公式Terraform Providerが提供されていることで更にその価値を倍化させることが可能だと思います。

この記事が認可基盤の運用で苦戦してきた方々へのレクイエムになれば幸いです。(もっと良い例えが思い浮かんだ方はどこかで教えてください)