Terraform으로 서비스 계정 관리하기
Terraform으로 서비스 계정 관리하기 (Manage service accounts with Terraform)
출처: 문서
OpenAI 서비스 계정은 프로젝트가 소유하는 비사람(nonhuman) identity예요. Terraform은 기본 역할 없이 계정을 만들고, 최소 권한 권한 묶음(permission bundle)을 정의하고, 그룹을 통해 그 묶음을 할당할 수 있어요. 서비스 계정 API 키의 생성과 관리는 Administration API를 통해 Terraform 외부에서 수행해요.
이 가이드는 일반적인 서비스 계정 온보딩 워크플로를 따릅니다:
- 기본 프로젝트 역할이나 API 키가 없는 서비스 계정을 만들어요.
- 그룹을 통해 커스텀 프로젝트 역할을 할당해서, 워크로드가 필요한 권한만 부여해요.
- 범위가 지정된(scoped) API 키를 만들고 시크릿 매니저에 저장해요.
시작하기 전에
Terraform 프로바이더 설정을 완료하고 Admin API 키를 OPENAI_ADMIN_KEY로, 기존 프로젝트 ID를 PROJECT_ID로 export해요.
서비스 계정 생성, import, 교체, 삭제를 평가할 때는 테스트 조직을 사용해요.
기본 역할 없는 서비스 계정 만들기
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 키를 생성하거나 기본 프로젝트 역할을 할당하지 않고 서비스 계정 identity를 만들어요. 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를 워크로드에 승인된 가장 작은 권한 집합으로 바꿔요. 그룹 기반 프로젝트 접근에 대한 자세한 내용은 Projects and access를 참고해요.
구성을 검토하고 적용해요:
terraform plan
terraform apply
커스텀 프로젝트 역할이 워크로드에 필요한 권한을 제공한다면 내장 member 또는 owner 역할을 할당하지 마세요. 승인된 권한 묶음으로 접근을 제한해 두세요.
범위가 지정된 API 키 만들기
Terraform 구성을 적용한 후 Create project service account API key 엔드포인트를 통해 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 ***" \
-H "Content-Type: application/json" \
-d '{
"name": "Production App",
"scopes": ["api.responses.write"]
}' \
--output service-account-api-key.json
워크로드에 필요한 가장 좁은 scopes를 선택해요. API-key scopes는 서비스 계정의 권한을 더 제한할 수 있지만, 할당된 프로젝트 역할 밖의 권한을 부여할 수는 없어요.
service-account-api-key.json의 value를 출력하지 않고 승인된 시크릿 매니저 워크플로에 전달해요. 시크릿 매니저가 시크릿을 저장하고 검증한 후 응답 파일을 제거해요:
rm service-account-api-key.json
service-account-api-key.json은 존재하는 동안 시크릿으로 취급해요. 커밋하거나, Terraform 구성에 키를 쓰거나, Terraform 출력으로 노출하거나, Terraform 변수로 전달하지 마세요.
API reference에는 응답 형태와 언어별 예시가 포함돼 있어요. workload identity federation을 지원하는 워크로드는 API 키를 만들지 않고도 동일한 서비스 계정과 최소 권한 역할을 사용할 수 있어요.
기존 서비스 계정 가져오기
Terraform이 만든 서비스 계정은 import할 필요가 없어요. Terraform 외부에서 만든 서비스 계정을 도입하려면 같은 프로젝트 ID와 이름으로 선언해요:
resource "openai_project_service_account" "application" {
project_id = "proj_123"
name = "example-application-development-service-account"
}
일반 apply를 실행하기 전에 기존 identity를 import해요:
SERVICE_ACCOUNT_ID="<existing-service-account-id>"
terraform import \
openai_project_service_account.application \
"$PROJECT_ID/$SERVICE_ACCOUNT_ID"
terraform plan
Import 후 첫 번째 계획은 서비스 계정에 변경 사항이 없음을 제안해야 해요. 교체를 제안한다면 적용 전에 구성된 이름과 프로젝트가 기존 계정과 일치하도록 해요.
Import는 API 키를 복구하거나 저장하지 않고, 서비스 계정의 기존 프로젝트 역할을 변경하지 않으며, 그룹 멤버십을 import하지 않아요. Terraform이 관리해야 한다면 기존 openai_project_role, openai_group, openai_group_user, openai_project_group_role 리소스를 선언하고 import해요. 워크로드는 시크릿 매니저에서 기존 시크릿을 계속 읽어요.
리소스 선언을 적용하기 전에 서비스 계정을 import해요. 먼저 apply하면 Terraform은 기존 identity를 도입하는 대신 다른 서비스 계정을 만들어요.
자격 증명 복구 또는 교체하기
API 키의 전체 값은 API-key create 응답에서만 사용할 수 있어요. 이후의 프로젝트 API 키 검색은 수정된(redacted) 값을 반환하므로 분실한 키는 복구할 수 없어요.
분실했거나 회전(rotate)해야 하는 자격 증명을 워크로드를 중단하지 않고 교체해요:
- 교체 계정을 기존 계정과 다른 Terraform 리소스 이름으로 새
openai_project_service_account리소스로 선언해요. - 구성을 적용해 교체 서비스 계정을 만들어요.
openai_group_user로 교체 계정을 기존 그룹에 추가해서 최소 권한 프로젝트 역할을 상속하게 해요.- Administration API를 통해 교체 계정용 API 키를 만들고 승인된 시크릿 매니저 워크플로로 키를 저장해요.
- 교체 키를 배포하고 교체 계정으로 워크로드를 검증해요.
- Terraform 구성에서 이전
openai_project_service_account와 그openai_group_user리소스를 제거해요. 교체 서비스 계정이 여전히 사용하는 역할, 그룹, 그룹 역할 할당은 유지해요. - 이전 서비스 계정과 그룹 멤버십을 삭제하는 계획을 검토하고 적용한 다음
terraform plan을 실행해 no-op 결과를 요구해요.
openai_project_service_account 리소스를 삭제하면 원격 서비스 계정이 삭제돼요. 특히 이전 자격 증명이 여전히 트래픽을 처리하는 동안에는 해당 변경에 명시적 검토를 요구해요.
더 넓은 상태 도입과 제거 동작은 Import and reconciliation을 참고해요.
완전한 예시 실행하기
집중된 예시들은 서비스 계정 생성, 역할 할당, 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
}
기존 프로젝트 ID, 고유한 서비스 계정 이름, 승인된 가장 작은 프로젝트 권한 집합으로 terraform.tfvars를 만들어요:
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
첫 번째 계획에는 추가할 리소스 다섯 개가 포함되어야 해요: 서비스 계정, 그 커스텀 프로젝트 역할, 그룹, 그룹 멤버십, 그룹 역할 할당. 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 ***" \
-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 구성, 상태, 또는 출력에 저장하지 마세요.