Oracle Identity Cloud Service(IDCS) 액세스 토큰을 수명이 짧은 OpenAI 액세스 토큰으로 교환하면 Oracle Cloud Infrastructure(OCI)를 워크로드 ID 공급자로 사용할 수 있습니다. OCI 인스턴스 주체는 같은 테넌시의 ID 도메인으로 보내는 토큰 교환 요청에 서명합니다. OpenAI는 이 요청으로 발급된 토큰을 검증한 후 OCI 워크로드가 매핑된 OpenAI 서비스 계정으로 작업을 수행하도록 허용합니다.
이 구성에는 OpenAI API 키, 사용자 지정 Oracle OAuth 리소스 애플리케이션, 사용자 지정 애플리케이션에 대한 동적 그룹 권한 부여가 필요하지 않습니다.
인스턴스 주체가 있는 OCI Compute 인스턴스에서 워크로드를 실행하세요. Oracle Kubernetes Engine(OKE)을 사용하는 경우 어떤 ID로 요청에 서명하는지 확인하세요. 표준 인스턴스 주체 서명기를 사용하면 일반적으로 개별 Kubernetes 파드가 아닌 워커 노드가 요청 주체로 식별됩니다.
서명기는 OCI 인스턴스 메타데이터 서비스에서 자격 증명을 가져옵니다. 워크로드에서 다음과 같이 링크 로컬 메타데이터 엔드포인트에 접근할 수 있는지 확인하세요.
123curl --fail --silent \
--header "Authorization: Bearer Oracle" \
http://169.254.169.254/opc/v2/instance/id
워크로드는 같은 테넌시의 ID 도메인으로 아웃바운드 HTTPS 요청도 보낼 수 있어야 합니다. 메타데이터 엔드포인트 자체에 접근하는 데는 NAT 게이트웨이나 인터넷 연결이 필요하지 않습니다.
OCI Python SDK의 InstancePrincipalsSecurityTokenSigner를 사용해 ID 도메인으로 보내는 다음 OAuth 토큰 교환 요청에 서명하세요.
POST https://<identity-domain>/oauth2/v1/token
Content-Type: application/x-www-form-urlencoded;charset=utf-8
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
scope=urn:opc:idm:__myscopes__
requested_token_type=urn:ietf:params:oauth:token-type:access_token
urn:opc:idm:__myscopes__ 범위는 인스턴스 주체에 이미 부여된 권한을 사용합니다. 반환된 IDCS 액세스 토큰을 OpenAI 워크로드 ID 페더레이션의 주체 토큰으로 사용하세요. Oracle 토큰의 대상 값을 https://api.openai.com/v1로 바꾸지 마세요. 실제 Oracle 토큰에 포함된 대상 값으로 OpenAI 공급자를 구성하세요.
TOKEN에 실제 OCI 워크로드에서 생성한 액세스 토큰을 설정한 다음, 기존 로컬 JWT 디코더로 토큰의 클레임을 확인하세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
throw new Error("Expected a compact JWT with three segments");
}
if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {
throw new Error("JWT payload is not valid Base64URL");
}
const bytes = Buffer.from(parts[1], "base64url");
if (bytes.toString("base64url") !== parts[1]) {
throw new Error("JWT payload is not valid Base64URL");
}
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const claims = JSON.parse(decoded);
if (claims === null || Array.isArray(claims) || typeof claims !== "object") {
throw new Error("JWT payload is not a JSON object");
}
console.log(decoded);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import base64
import json
import os
import re
def reject_non_json_constant(value):
raise ValueError(f"JWT payload contains non-JSON constant: {value}")
parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
raise ValueError("Expected a compact JWT with three segments")
payload = parts[1]
if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:
raise ValueError("JWT payload is not valid Base64URL")
padded_payload = payload + "=" * (-len(payload) % 4)
decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)
if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:
raise ValueError("JWT payload is not valid Base64URL")
decoded_text = decoded.decode("utf-8")
claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)
if not isinstance(claims, dict):
raise ValueError("JWT payload is not a JSON object")
print(decoded_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
"unicode/utf8"
)
func decodeSegment(segment string) (json.RawMessage, error) {
if !isBase64URLSegment(segment) {
return nil, fmt.Errorf("JWT segment is not valid Base64URL")
}
decoded, err := base64.RawURLEncoding.DecodeString(segment)
if err != nil {
return nil, err
}
if base64.RawURLEncoding.EncodeToString(decoded) != segment {
return nil, fmt.Errorf("JWT segment is not valid Base64URL")
}
if !utf8.Valid(decoded) {
return nil, fmt.Errorf("JWT segment is not valid UTF-8")
}
var value json.RawMessage
if err := json.Unmarshal(decoded, &value); err != nil {
return nil, err
}
if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
return nil, fmt.Errorf("JWT segment is not a JSON object")
}
return value, nil
}
func isBase64URLSegment(segment string) bool {
if segment == "" || len(segment)%4 == 1 {
return false
}
for _, character := range segment {
if !('A' <= character && character <= 'Z') &&
!('a' <= character && character <= 'z') &&
!('0' <= character && character <= '9') &&
character != '-' &&
character != '_' {
return false
}
}
return true
}
func main() {
parts := strings.Split(os.Getenv("TOKEN"), ".")
if len(parts) != 3 {
panic("Expected a compact JWT with three segments")
}
payload, err := decodeSegment(parts[1])
if err != nil {
panic(err)
}
formatted, err := json.MarshalIndent(payload, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(formatted))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public final class DecodeJwtPayloadExample {
private static final ObjectMapper JSON =
new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
private DecodeJwtPayloadExample() {}
static String decodeUtf8(byte[] bytes) throws IOException {
try {
return StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString();
} catch (CharacterCodingException exception) {
throw new IOException("JWT segment is not valid UTF-8", exception);
}
}
static String decodeSegment(String segment) throws IOException {
if (!isBase64UrlSegment(segment)) {
throw new IllegalArgumentException("JWT segment is not valid Base64URL");
}
byte[] bytes = Base64.getUrlDecoder().decode(segment);
if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
throw new IllegalArgumentException("JWT segment is not valid Base64URL");
}
String decoded = decodeUtf8(bytes);
JsonNode value = JSON.readTree(decoded);
if (value == null || value.isMissingNode() || !value.isObject()) {
throw new IOException("JWT segment is not a JSON object");
}
return decoded;
}
static boolean isBase64UrlSegment(String segment) {
if (segment.isEmpty() || segment.length() % 4 == 1) {
return false;
}
return segment
.chars()
.allMatch(
character ->
character >= 'A' && character <= 'Z'
|| character >= 'a' && character <= 'z'
|| character >= '0' && character <= '9'
|| character == '-'
|| character == '_');
}
static String[] requireCompactJwt(String token) {
if (token == null) {
throw new IllegalArgumentException("Expected a compact JWT with three segments");
}
String[] parts = token.split("\\.", -1);
if (parts.length != 3) {
throw new IllegalArgumentException("Expected a compact JWT with three segments");
}
return parts;
}
public static void main(String[] args) throws IOException {
String[] parts = requireCompactJwt(System.getenv("TOKEN"));
System.out.println(decodeSegment(parts[1]));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59using System.Text;
using System.Text.Json;
static string DecodeSegment(string segment)
{
if (
segment.Length % 4 == 1 ||
segment.Any(
character =>
!(
character is >= 'A' and <= 'Z' ||
character is >= 'a' and <= 'z' ||
character is >= '0' and <= '9' ||
character is '-' or '_'
)
)
)
{
throw new FormatException("JWT segment is not valid Base64URL");
}
byte[] decoded = Convert.FromBase64String(
segment.Replace('-', '+').Replace('_', '/') +
new string('=', (4 - segment.Length % 4) % 4)
);
string canonicalSegment = Convert
.ToBase64String(decoded)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
if (canonicalSegment != segment)
{
throw new FormatException("JWT segment is not valid Base64URL");
}
string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
using JsonDocument document = JsonDocument.Parse(decodedJson);
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
throw new FormatException("JWT segment is not a JSON object");
}
return decodedJson;
}
string? token = Environment.GetEnvironmentVariable("TOKEN");
if (token is null)
{
throw new InvalidOperationException(
"Expected a compact JWT with three segments"
);
}
string[] parts = token.Split('.');
if (parts.Length != 3)
{
throw new InvalidOperationException(
"Expected a compact JWT with three segments"
);
}
Console.WriteLine(DecodeSegment(parts[1]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26require "base64"
require "json"
parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3
unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1
raise "JWT payload is not valid Base64URL"
end
begin
payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))
rescue ArgumentError
raise "JWT payload is not valid Base64URL"
end
unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]
raise "JWT payload is not valid Base64URL"
end
payload.force_encoding(Encoding::UTF_8)
raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?
claims = JSON.parse(payload)
raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)
puts(payload)
디코더는 토큰의 내용을 확인하지만 서명은 검증하지 않습니다. 토큰 원문은 민감한 정보이므로 로그에 기록하지 마세요. 프로덕션 토큰을 타사 JWT 디코더에 붙여 넣지 마세요.
디코딩한 Oracle 액세스 토큰에는 다음과 같은 클레임이 포함될 수 있습니다.
123456789101112131415{
"iss": "https://identity.oraclecloud.com/",
"aud": [
"https://idcs-example.us-phoenix-1.identity.oraclecloud.com",
"https://idcs-example.identity.oraclecloud.com"
],
"sub_type": "instance",
"ipst_instance": "ocid1.instance.oc1.phx.<instance-id>",
"ipst_compartment": "ocid1.compartment.oc1..<compartment-id>",
"domain_id": "ocid1.domain.oc1..<domain-id>",
"ca_ocid": "ocid1.tenancy.oc1..<tenancy-id>",
"tenant": "idcs-example",
"exp": 1782369434,
"iat": 1782365834
}
설정값은 자체 ID 도메인에서 발급한 토큰을 기준으로 정하세요. 토큰의 iss 값은 그대로 사용하고, 토큰에 포함된 aud 값 중 하나를 선택하세요. 워크로드에 권한을 부여할 때는 값이 바뀌지 않는 ipst_instance, ipst_compartment, domain_id, ca_ocid 클레임을 우선 사용하세요.
Oracle ID 도메인에 대한 워크로드 ID 공급자를 만든 다음, 대상 OpenAI 서비스 계정을 사용할 수 있는 OCI 인스턴스 또는 구획에 대한 매핑을 추가하세요.
-
워크로드 ID 공급자를 만드세요. 이름 필드에 oracle-cloud-prod와 같은 고유한 값을 입력하세요. 설명 필드에는 Production OCI instance principal과 같이 신뢰할 워크로드를 식별할 수 있는 설명을 입력하세요.
-
발급자와 대상을 설정하세요. OIDC 발급자 URL 필드에 토큰의 iss 클레임 값(예: https://identity.oraclecloud.com/)을 입력하세요. 대상 필드에는 같은 토큰에 있는 aud 값 중 하나를 입력하세요.
-
테넌트별 OIDC 검색을 사용할 수 있으면 설정하세요. OIDC 검색에 사용자 지정 URL 사용 옵션이 고급 설정에 표시되면 켜세요. 사용자 지정 OIDC 검색 URL 필드에 https://idcs-example.identity.oraclecloud.com과 같은 테넌트별 ID 도메인 URL을 입력하세요. OpenAI는 https://idcs-example.identity.oraclecloud.com/.well-known/openid-configuration에서 검색 문서를 가져온 다음, 문서의 jwks_uri에서 테넌트의 서명 검증용 공개 키를 가져옵니다. 사용자 지정 검색 옵션이 표시되지 않으면 토큰 검증에 업로드한 JWKS 사용 옵션을 켜고 https://<identity-domain>/admin/v1/SigningCert/jwk에서 가져온 공개 JWKS를 업로드하세요.
-
파생 속성이 필요할 때만 속성 변환을 추가하세요. 서비스 계정 매핑의 어설션에는 ipst_instance, ipst_compartment, domain_id, ca_ocid와 같은 Oracle 클레임을 그대로 사용할 수 있습니다. 인스턴스 속성을 명시적으로 정의하려면 속성 이름에 instance를, 표현식에 assertion.ipst_instance를 입력해 openai.instance를 만드세요.
Oracle의 OpenID Connect 검색 레퍼런스를 보면 사용자 지정 검색이 중요한 이유를 알 수 있습니다. 검색 문서에는 전역 발급자가 https://identity.oraclecloud.com/로 선언되어 있어도 토큰 엔드포인트와 jwks_uri는 테넌트별 ID 도메인에 있을 수 있습니다. OIDC 발급자 URL 필드에는 전역 발급자 URL을 유지하고 사용자 지정 OIDC 검색 URL 필드에는 테넌트 도메인을 사용하세요.
ID 도메인이 토큰 발급자 URL에서 검색 메타데이터를 제공하는 경우 사용자 지정 검색을 끈 상태로 두고 표준 OIDC 검색을 사용하세요. OpenAI가 테넌트의 검색 문서나 서명 키 엔드포인트에 접근할 수 없다면 사용자 지정 검색을 끄고 토큰 검증에 업로드한 JWKS 사용 옵션을 켠 다음 https://<identity-domain>/admin/v1/SigningCert/jwk에서 가져온 테넌트의 공개 JWKS를 업로드하세요. 사용자 지정 검색과 업로드한 JWKS 사용 옵션은 동시에 켤 수 없습니다. Oracle이 서명 인증서를 교체하면 업로드한 키도 갱신하세요.
-
서비스 계정 매핑을 만드세요. 이름 필드에 oracle-instance-prod와 같은 고유한 값을 입력하고, 신뢰할 OCI 워크로드를 식별할 수 있는 설명을 추가하세요.
-
범위가 가장 좁고 값이 바뀌지 않는 OCI ID를 매핑 조건으로 사용하세요. 인스턴스 하나에 액세스 권한을 부여하려면 키 필드에 ipst_instance를 입력하고 값 필드에 검증한 토큰의 인스턴스 OCID를 그대로 입력하세요. 한 구획에 속한 인스턴스들에 액세스 권한을 부여하려면 키 필드에 ipst_compartment를 입력하고 값 필드에 해당 구획의 정확한 OCID를 입력하세요.
-
필요에 따라 도메인과 테넌시를 제한하세요. domain_id 또는 ca_ocid에 대한 매핑 행을 추가해 특정 Oracle ID 도메인 또는 테넌시에 속한 워크로드만 허용하세요. 토큰에 sub_type 클레임이 있고 인스턴스 주체만 허용하려면 해당 클레임의 값이 instance인지 확인하는 행을 추가하세요. 모든 매핑 행의 조건이 일치해야 합니다.
-
대상 OpenAI 서비스 계정을 선택하세요. 프로젝트 필드에서 서비스 계정이 속한 프로젝트를 선택한 다음, 신뢰할 OCI 워크로드가 사용할 서비스 계정을 선택하세요.
-
필요에 따라 API 권한을 제한하세요. 워크로드에 필요한 권한만 선택하세요. 매핑 권한으로 선택한 서비스 계정의 권한을 제한할 수 있지만, 서비스 계정에 없는 권한을 새로 부여할 수는 없습니다.
표준 인스턴스 주체 서명기를 사용하는 OKE 워크로드는 워커 노드의 ID를 상속합니다. 인스턴스 수준으로 매핑하면 권한이 파드 하나에 한정되지 않고 해당 노드 전체에 부여됩니다. 같은 워커 노드의 파드를 서로 격리해야 한다면 OCI에서 지원하는 워크로드 ID 중 범위가 더 좁은 ID를 사용하세요.
OpenAI, OCI, Requests Python 패키지를 설치하세요.
pip install openai oci requests
Ruby를 사용하는 경우 OpenAI와 OCI gem을 설치하세요.
gem install openai oci
OCI_IDENTITY_DOMAIN_URL에 워크로드와 같은 테넌시에 있는 ID 도메인의 기본 URL을 설정하세요. OPENAI_IDENTITY_PROVIDER_ID와 OPENAI_SERVICE_ACCOUNT_ID에는 OpenAI 공급자와 서비스 계정 매핑에서 확인한 ID를 각각 설정하세요.
다음 예제에서는 OCI 인스턴스 주체로 Oracle 토큰 교환 요청에 서명하고 IDCS 액세스 토큰을 OpenAI SDK에 반환합니다. SDK는 필요할 때 이 토큰을 수명이 짧은 OpenAI 액세스 토큰으로 교환합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53import os
import oci
import requests
from openai import OpenAI
from openai.auth import SubjectTokenProvider
def oracle_instance_principal_token_provider(
identity_domain_url: str,
) -> SubjectTokenProvider:
def get_token() -> str:
signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
response = requests.post(
f"{identity_domain_url.rstrip('/')}/oauth2/v1/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"scope": "urn:opc:idm:__myscopes__",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
},
headers={
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
auth=signer,
timeout=30,
)
response.raise_for_status()
token = response.json().get("access_token")
if not isinstance(token, str) or not token:
raise RuntimeError("Oracle IDCS did not return an access token.")
return token
return {"token_type": "jwt", "get_token": get_token}
client = OpenAI(
workload_identity={
"identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
"service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
"provider": oracle_instance_principal_token_provider(
os.environ["OCI_IDENTITY_DOMAIN_URL"]
),
},
)
response = client.responses.create(
model="gpt-5.6-terra",
input="Say hello from Oracle Cloud Infrastructure workload identity federation.",
)
print(response.output_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105require "json"
require "net/http"
require "oci"
require "openai"
require "uri"
class OracleInstancePrincipalTokenProvider
include OpenAI::Auth::SubjectTokenProvider
def initialize(identity_domain_url:)
@identity_domain_url = identity_domain_url.sub(%r{/+\z}, "")
end
def token_type
OpenAI::Auth::TokenType::JWT
end
def get_token
uri = URI("#{@identity_domain_url}/oauth2/v1/token")
unless uri.is_a?(URI::HTTPS)
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Oracle identity domain URL must use HTTPS",
provider: "oracle-instance-principal"
)
end
body = URI.encode_www_form(
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
scope: "urn:opc:idm:__myscopes__",
requested_token_type: "urn:ietf:params:oauth:token-type:access_token"
)
headers = {
"content-type": "application/x-www-form-urlencoded;charset=utf-8"
}
signer = OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new
signer.sign(:post, uri.to_s, headers, body)
request = Net::HTTP::Post.new(uri)
headers.each { |name, value| request[name.to_s] = value }
request.body = body
response = Net::HTTP.start(
uri.hostname,
uri.port,
use_ssl: true,
open_timeout: 10,
read_timeout: 30
) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Oracle identity token request failed with status #{response.code}",
provider: "oracle-instance-principal"
)
end
token = JSON.parse(response.body).fetch("access_token")
unless token.is_a?(String) && !token.empty?
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Oracle identity domain did not return an access token",
provider: "oracle-instance-principal"
)
end
token
rescue JSON::ParserError
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Oracle identity token response was not valid JSON",
provider: "oracle-instance-principal"
), cause: nil
rescue KeyError
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Oracle identity domain did not return an access token",
provider: "oracle-instance-principal"
), cause: nil
rescue SystemCallError, Timeout::Error => error
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Failed to request Oracle identity token: #{error.message}",
provider: "oracle-instance-principal",
cause: error
)
end
end
provider = OracleInstancePrincipalTokenProvider.new(
identity_domain_url: ENV.fetch("OCI_IDENTITY_DOMAIN_URL")
)
workload_identity = OpenAI::Auth::WorkloadIdentity.new(
identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
provider: provider
)
client = OpenAI::Client.new(workload_identity: workload_identity)
response = client.responses.create(
model: "gpt-5.6-terra",
input: "Say hello from Oracle Cloud Infrastructure workload identity federation."
)
puts(response.output_text)
OpenAI SDK가 워크로드 ID 자격 증명을 갱신해야 할 때 주체 토큰 공급자가 새 Oracle 토큰을 요청합니다. Oracle 주체 토큰이나 교환을 통해 발급된 OpenAI 액세스 토큰을 절대 출력하거나 영구 저장하지 마세요.
- 워크로드 하나에만 액세스를 허용해야 한다면
ipst_instance로 인스턴스 하나를 매핑하세요.
- 해당 구획에서 조건을 충족하는 모든 인스턴스가 같은 매핑을 사용해야 할 때만
ipst_compartment를 사용하세요.
domain_id 또는 ca_ocid를 추가해 허용할 ID 도메인과 테넌시의 범위를 제한하세요.
- 애플리케이션과 환경별로 별도의 OpenAI 서비스 계정을 사용하세요.
- 파드 수준의 격리를 전제로 하기 전에 OKE 토큰이 워커 노드를 식별하는지 확인하세요.
- 대상이 OpenAI 전용이라고 가정하지 말고 발급된 Oracle 토큰에 포함된 대상 값을 사용하세요.
- ID 도메인에서 OIDC 검색을 사용할 수 없다면 Oracle이 서명 키를 교체할 때 업로드한 공개 키도 교체하세요.