OpenAI のサービスアカウントは、プロジェクトが所有し、ワークロードが認証に使う非人間 ID です。Terraform を使うと、デフォルトのロールを割り当てずにアカウントを作成し、必要最小限の権限を定義して、グループ経由で付与できます。サービスアカウントの API キーは、Terraform の外部で Administration API を使って作成・管理します。
このガイドでは、サービスアカウントを利用するまでの一般的な手順を説明します。
- デフォルトのプロジェクトロールも API キーも持たないサービスアカウントを作成します。
- グループ経由でカスタムプロジェクトロールを割り当て、ワークロードに必要な権限だけを付与します。
- スコープを限定した API キーを作成し、シークレットマネージャーに保存します。
事前準備
Terraform プロバイダーのセットアップを完了し、管理者用 API キーを環境変数 OPENAI_ADMIN_KEY に、既存のプロジェクトの ID を環境変数 PROJECT_ID に設定してエクスポートします。
サービスアカウントの作成、インポート、置き換え、削除を試す際は、テスト用の組織を使用してください。
デフォルトのロールを持たないサービスアカウントの作成
Terraform でサービスアカウントを作成します。
resource "openai_project_service_account" "application" {
project_id = "proj_123"
name = "example-application-development-service-account"
}
output "service_account_id" {
value = openai_project_service_account.application.service_account_id
}
proj_123 を、サービスアカウントを所有する既存のプロジェクトの ID に置き換えます。
プロバイダーはサービスアカウントを作成しますが、API キーの生成やデフォルトのプロジェクトロールの割り当ては行いません。Terraform は、サービスアカウントの ID と、機密情報に該当しないその他のメタデータをステートに保存します。この時点では、サービスアカウントはプロジェクトに対する権限を持っていません。
必要最小限の権限の付与
ワークロードに必要な権限だけを持つカスタムプロジェクトロールを定義します。グループを作成し、そこにサービスアカウントを追加してから、グループにロールを割り当てます。次の例では、グループのメンバーにレスポンスを作成する権限を付与します。
resource "openai_project_role" "application" {
project_id = openai_project_service_account.application.project_id
role_name = "Application response writer"
description = "Allows the application to create responses"
permissions = ["api.responses.write"]
}
resource "openai_group" "application_access" {
name = "example-application-development-access"
}
resource "openai_group_user" "application" {
group_id = openai_group.application_access.group_id
user_id = openai_project_service_account.application.id
}
resource "openai_project_group_role" "application_access" {
project_id = openai_project_service_account.application.project_id
group_id = openai_group.application_access.group_id
role_id = openai_project_role.application.role_id
}
openai_project_role リソースで必要最小限の権限を定義し、openai_group_user でサービスアカウントをグループに追加し、openai_project_group_role でそのグループにロールを割り当てます。グループに追加されたすべてのサービスアカウントが同じプロジェクトロールを継承します。api.responses.write は、ワークロードでの使用が承認された権限のうち、必要最小限のものに置き換えてください。グループ単位でプロジェクトへのアクセスを管理する方法については、プロジェクトとアクセスを参照してください。
変更内容を確認し、構成を適用します。
terraform plan
terraform apply
カスタムプロジェクトロールでワークロードに必要な権限を付与できる場合は、組み込みの member ロールや owner ロールを割り当てないでください。付与する権限は、承認されたものだけに限定してください。
スコープを限定した API キーの作成
Terraform の構成を適用したら、プロジェクトのサービスアカウント用 API キーを作成するエンドポイントで API キーを作成します。API がキーの値を完全な形で返すのは一度だけです。リクエストを送信する前に、レスポンスの保存先ファイルに適用されるアクセス権を制限してください。
SERVICE_ACCOUNT_ID="$(terraform output -raw service_account_id)"
umask 077
curl -X POST \
"https://api.openai.com/v1/organization/projects/$PROJECT_ID/service_accounts/$SERVICE_ACCOUNT_ID/api_keys" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production App",
"scopes": ["api.responses.write"]
}' \
--output service-account-api-key.json
スコープはワークロードに必要な最小限の範囲に設定してください。API キーのスコープを使うと、サービスアカウントの権限をさらに制限できます。ただし、割り当てられたプロジェクトロールに含まれない権限を付与することはできません。
value の値を service-account-api-key.json から取得し、出力せずに、承認済みの手順でシークレットマネージャーに渡します。シークレットマネージャーでシークレットの保存と検証が完了したら、レスポンスファイルを削除します。
rm service-account-api-key.json
service-account-api-key.json は、削除するまでシークレットとして扱ってください。このファイルをコミットしないでください。また、キーを Terraform の構成に書き込んだり、Terraform の出力として公開したり、Terraform の変数として渡したりしないでください。
API リファレンスには、レスポンスの形式と各言語の使用例が掲載されています。ワークロード ID フェデレーションに対応するワークロードでは、API キーを作成せずに、同じサービスアカウントと必要最小限の権限を持つロールを使用できます。
既存のサービスアカウントのインポート
Terraform で作成したサービスアカウントは、インポートする必要がありません。Terraform の外部で作成したサービスアカウントを管理対象にするには、既存のアカウントと同じプロジェクト ID と名前を指定してリソースを宣言します。
resource "openai_project_service_account" "application" {
project_id = "proj_123"
name = "example-application-development-service-account"
}
通常の手順で構成を適用する前に、既存のサービスアカウントをインポートします。
SERVICE_ACCOUNT_ID="<existing-service-account-id>"
terraform import \
openai_project_service_account.application \
"$PROJECT_ID/$SERVICE_ACCOUNT_ID"
terraform plan
インポート後の最初のプランで、サービスアカウントに対する変更がないことを確認します。置き換えが表示された場合は、構成を適用する前に、名前とプロジェクトの設定を既存のアカウントに合わせてください。
インポートでは、API キーの復元や保存、サービスアカウントの既存のプロジェクトロールの変更、グループへの所属情報のインポートは行われません。既存の openai_project_role、openai_group、openai_group_user、openai_project_group_role リソースも Terraform で管理する場合は、それぞれを宣言してインポートしてください。ワークロードは引き続き、既存のシークレットをシークレットマネージャーから読み取ります。
リソースの宣言を適用する前に、サービスアカウントをインポートしてください。先に適用すると、Terraform は既存のサービスアカウントを管理対象にせず、別のサービスアカウントを作成します。
認証情報の紛失時の対応とローテーション
API キーの値を完全な形で取得できるのは、キー作成時のレスポンスだけです。後からプロジェクトの API キーを取得しても、値の一部が伏せられているため、紛失したキーは復元できません。
認証情報の紛失時やローテーション時は、次の手順でワークロードを中断することなく認証情報を置き換えます。
- 代替のサービスアカウントを新しい
openai_project_service_accountリソースとして宣言します。Terraform のリソース名には、旧アカウントとは異なる名前を使用します。 - 構成を適用し、代替のサービスアカウントを作成します。
openai_group_userを使って代替アカウントを既存のグループに追加し、必要最小限の権限を持つプロジェクトロールを継承させます。- Administration API で代替アカウントの API キーを作成し、承認済みの手順でシークレットマネージャーに保存します。
- 新しいキーをデプロイし、代替アカウントでワークロードが正常に動作することを確認します。
- 旧アカウントの
openai_project_service_accountと、そのアカウントに対応するopenai_group_userリソースを Terraform の構成から削除します。代替アカウントが引き続き使用するロール、グループ、グループへのロールの割り当ては残します。 - 旧サービスアカウントとそのグループ所属情報を削除するプランを確認して適用します。その後、
terraform planを実行し、変更がないことを必ず確認します。
openai_project_service_account リソースを削除すると、リモートのサービスアカウントも削除されます。この変更は必ず明示的にレビューしてください。特に、旧認証情報がまだリクエストの処理に使われている場合は注意が必要です。
既存のリソースをステートの管理対象にする方法や、管理対象から外す際の動作については、インポートと整合性の確保を参照してください。
完全な構成例の実行
ここまでの例では、具体的な値を使ってサービスアカウントの作成、ロールの割り当て、API キーの作成を説明しました。以下の構成では、プロジェクト固有の値と権限を変数にしているため、複数の環境で再利用できます。
次の構成を main.tf として保存します。
terraform {
required_version = ">= 1.0"
required_providers {
openai = {
source = "openai/openai"
version = ">= 1.0.0"
}
}
}
provider "openai" {}
variable "project_id" {
type = string
description = "ID of the existing OpenAI project."
}
variable "service_account_name" {
type = string
description = "Name of the application service account."
}
variable "project_role_permissions" {
type = list(string)
description = "Least-privilege project permissions for the application."
validation {
condition = length(var.project_role_permissions) > 0
error_message = "Provide at least one approved project permission."
}
}
resource "openai_project_service_account" "application" {
project_id = var.project_id
name = var.service_account_name
}
resource "openai_project_role" "application" {
project_id = var.project_id
role_name = "Application API access"
description = "Least-privilege permissions approved for the application"
permissions = var.project_role_permissions
}
resource "openai_group" "application_access" {
name = "${var.service_account_name}-access"
}
resource "openai_group_user" "application" {
group_id = openai_group.application_access.group_id
user_id = openai_project_service_account.application.id
}
resource "openai_project_group_role" "application_access" {
project_id = var.project_id
group_id = openai_group.application_access.group_id
role_id = openai_project_role.application.role_id
}
output "project_id" {
value = var.project_id
}
output "service_account_id" {
value = openai_project_service_account.application.service_account_id
}
output "group_id" {
value = openai_group.application_access.group_id
}
output "project_role_id" {
value = openai_project_role.application.role_id
}
terraform.tfvars を作成し、既存のプロジェクトの ID、一意のサービスアカウント名、承認済みのプロジェクト権限のうち必要最小限のものを指定します。
project_id = "proj_123"
service_account_name = "example-application-development-service-account"
project_role_permissions = [
"api.responses.write",
]
Terraform を初期化し、保存したプランを確認してから適用します。
terraform init
terraform fmt
terraform validate
terraform plan -out=tfplan
terraform show tfplan
terraform apply tfplan
最初のプランで追加されるリソースは、サービスアカウント、そのカスタムプロジェクトロール、グループ、グループへの所属、グループへのロールの割り当ての 5 つです。terraform plan をもう一度実行し、この構成で追加の変更が発生しないことを確認します。
サービスアカウントの API キーは Terraform の外部で作成します。
PROJECT_ID="$(terraform output -raw project_id)"
SERVICE_ACCOUNT_ID="$(terraform output -raw service_account_id)"
umask 077
curl -X POST \
"https://api.openai.com/v1/organization/projects/$PROJECT_ID/service_accounts/$SERVICE_ACCOUNT_ID/api_keys" \
-H "Authorization: Bearer $OPENAI_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production App",
"scopes": ["api.responses.write"]
}' \
--output service-account-api-key.json
返された API キーの値を承認済みのシークレットマネージャーに保存してから、service-account-api-key.json を削除します。キーを Terraform の構成、ステート、出力に保存しないでください。