Setiap lingkungan komputasi Google Cloud yang memiliki akses ke server metadata instance (Cloud Run, Cloud Functions, App Engine, Compute Engine (GCE), dan GKE dengan Workload Identity) dapat meminta token identitas yang ditandatangani Google untuk service account yang terpasang padanya. Issuer token tersebut adalah https://accounts.google.com, dan Juglow dapat memvalidasinya secara langsung melalui OIDC discovery standar, tanpa memerlukan konfigurasi Google Cloud tambahan.
Panduan ini menunjukkan cara mendaftarkan issuer Google ke Juglow, mengikat service account Google ke service account Juglow, dan membuat workload Anda menukarkan token identitasnya dengan token akses Haijun API yang berumur pendek.
Prasyarat
- Pemahaman tentang konsep WIF: service account, federation issuer, dan federation rule.
- Sebuah project Google Cloud dengan workload yang berjalan di Cloud Run, Cloud Functions, App Engine, Compute Engine, atau GKE.
- Sebuah service account Google yang dikelola pengguna dan terpasang pada workload tersebut (bukan service account default Compute Engine).
- Izin untuk membuat service account, federation issuer, dan federation rule di Haijun Console untuk organisasi Juglow Anda.
Mengonfigurasi Google Cloud
Google menerbitkan token identitas secara otomatis untuk setiap workload yang memiliki service account terpasang. Tidak ada yang perlu diaktifkan di sisi Google selain memasang service account yang tepat, tetapi langkah-langkahnya sedikit berbeda antara komputasi standar dan GKE.
Cloud Run, Cloud Functions, App Engine, GCE
Pasang service account khusus ke layanan atau instance Anda:
gcloud run deploy my-service \
--service-account inference-worker@my-project.iam.gserviceaccount.comDi dalam workload, server metadata mengembalikan token identitas yang ditandatangani sesuai permintaan. Mintalah token tersebut dengan audience yang ingin Anda daftarkan di sisi Juglow, dan sertakan format=full agar respons membawa klaim email:
GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full
Metadata-Flavor: GoogleAtau, dengan gcloud CLI:
gcloud auth print-identity-token \
--audiences="https://haijun.my.id/" \
--include-emailPadanan SDK-nya ditunjukkan di Memperoleh dan menggunakan token.
Payload token yang telah didekode terlihat seperti ini:
{
"iss": "https://accounts.google.com",
"aud": "https://haijun.my.id/",
"sub": "104892...",
"azp": "104892...",
"email": "inference-worker@my-project.iam.gserviceaccount.com",
"email_verified": true,
"exp": 1775527120
}Klaim sub adalah ID unik numerik opaque milik service account Google. Klaim email adalah alamat service account yang dapat dibaca manusia. Cocokkan sub dan email keduanya dalam federation rule Anda.
GKE dengan Workload Identity
Aktifkan Workload Identity pada cluster Anda dan ikat service account Kubernetes Anda ke service account Google dengan anotasi iam.gke.io/gcp-service-account:
apiVersion: v1
kind: ServiceAccount
metadata:
name: inference-worker
namespace: prod
annotations:
iam.gke.io/gcp-service-account: inference-worker@my-project.iam.gserviceaccount.comDengan pengikatan ini, server metadata GKE mengembalikan token yang ditandatangani Google yang identik dengan kasus Cloud Run dan GCE: issuer https://accounts.google.com yang sama, klaim email yang sama, URL pengambilan yang sama. Konfigurasikan Juglow persis seperti pada bagian berikutnya.
Token format=full dari GKE juga menyertakan klaim google.compute_engine.project_id, google.compute_engine.zone, dan google.compute_engine.instance_name, yang dapat Anda referensikan dalam matcher condition pada federation rule (ekspresi CEL seperti claims.google.compute_engine.project_id == "my-project") untuk membatasi akses ke cluster atau node pool tertentu.
Note: Jika Anda tidak ingin mengikat service account Kubernetes ke service account Google, pod GKE dapat menggunakan issuer OIDC milik cluster itu sendiri (
https://container.googleapis.com/v1/projects/PROJECT/locations/REGION/clusters/CLUSTER) dengan volumeserviceAccountTokenyang diproyeksikan. Jalur tersebut menggunakan issuer per-cluster alih-alihaccounts.google.com. Lihat Menggunakan WIF dengan Kubernetes untuk pola tersebut.
Mengonfigurasi Juglow
Di Haijun Console, buka Settings → Workload identity, klik Connect workload, dan pilih tile Google Cloud. Wizard akan memandu Anda mendaftarkan issuer, membuat service account, dan membuat federation rule.
Wizard membuat sumber daya ini untuk Anda. Gunakan nilai-nilai berikut, baik Anda memasukkannya di wizard maupun mengirimkannya ke Admin API:
Federation issuer: Google memublikasikan dokumen OIDC discovery-nya secara publik, jadi gunakan mode discovery. Satu issuer ini mencakup setiap permukaan Google Cloud (Cloud Run, GCE, Cloud Functions, App Engine, dan GKE dengan Workload Identity). Bedakan workload dengan rule, bukan dengan issuer.
{
"name": "gcp",
"issuer_url": "https://accounts.google.com",
"jwks": { "type": "discovery" }
}Federation rule: Cocokkan klaim sub dan email keduanya. email adalah alamat service account yang dapat dibaca; sub adalah ID unik numerik service account, yang tidak pernah digunakan ulang oleh Google, sehingga menguncinya melindungi rule jika service account dihapus dan service account baru kemudian dibuat dengan email yang sama. Temukan ID unik tersebut dengan gcloud iam service-accounts describe SA_EMAIL --format='value(uniqueId)'.
{
"name": "gcp-inference-worker",
"issuer_id": "fdis_...",
"match": {
"audience": "https://haijun.my.id/",
"claims": {
"sub": "104892101234567890123",
"email": "inference-worker@my-project.iam.gserviceaccount.com"
}
},
"target": {
"type": "service_account",
"service_account_id": "svac_..."
},
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}Memperoleh dan menggunakan token
Di dalam workload Google Cloud Anda, ambil token identitas dari server metadata, tukarkan di POST /v1/oauth/token, dan gunakan bearer token yang dikembalikan untuk memanggil Haijun API. Setiap SDK Juglow menangani pertukaran dan loop refresh untuk Anda ketika Anda meneruskan callable yang mengembalikan token identitas baru dari server metadata ke identity_token_provider (typescript, php: identityTokenProvider; csharp: IdentityTokenProvider; go: option.WithFederationTokenProvider; java: federationTokenProvider), seperti yang ditunjukkan dalam contoh berikut.
# Ambil token identitas yang ditandatangani Google dari server metadata
JWT=$(curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full")
# Tukarkan token tersebut dengan token akses Juglow
RESPONSE=$(curl -sS https://haijun.my.id/v1/oauth/token \
-H "content-type: application/json" \
--data @- <<JSON
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "$JWT",
"federation_rule_id": "$JUGLOW_FEDERATION_RULE_ID",
"organization_id": "$JUGLOW_ORGANIZATION_ID",
"service_account_id": "$JUGLOW_SERVICE_ACCOUNT_ID",
"workspace_id": "$JUGLOW_WORKSPACE_ID"
}
JSON
)
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)
# Panggil API Haijun
curl -sS https://haijun.my.id/v1/messages \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "juglow-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "haijun-opus-5-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello from Cloud Run"}]
}' | jq -r '.content[] | select(.type == "text") | .text' import os
import juglow
import google.auth.transport.requests
import google.oauth2.id_token
from juglow import WorkloadIdentityCredentials
AUDIENCE = "https://haijun.my.id/"
def fetch_google_identity_token() -> str:
request = google.auth.transport.requests.Request()
return google.oauth2.id_token.fetch_id_token(request, AUDIENCE)
client = juglow.Juglow(
credentials=WorkloadIdentityCredentials(
identity_token_provider=fetch_google_identity_token,
federation_rule_id=os.environ["JUGLOW_FEDERATION_RULE_ID"],
organization_id=os.environ["JUGLOW_ORGANIZATION_ID"],
service_account_id=os.environ["JUGLOW_SERVICE_ACCOUNT_ID"],
workspace_id=os.environ.get("JUGLOW_WORKSPACE_ID"),
),
)
message = client.messages.create(
model="haijun-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello from Cloud Run"}],
)
print(next(block.text for block in message.content if block.type == "text")) import Juglow from "@juglow-ai/sdk";
import { oidcFederationProvider } from "@juglow-ai/sdk/lib/credentials/oidc-federation";
const METADATA_URL =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full";
async function fetchGoogleIdentityToken(): Promise<string> {
const response = await fetch(METADATA_URL, {
headers: { "Metadata-Flavor": "Google" }
});
return response.text();
}
const client = new Juglow({
credentials: oidcFederationProvider({
identityTokenProvider: fetchGoogleIdentityToken,
federationRuleId: process.env.JUGLOW_FEDERATION_RULE_ID!,
organizationId: process.env.JUGLOW_ORGANIZATION_ID!,
serviceAccountId: process.env.JUGLOW_SERVICE_ACCOUNT_ID,
workspaceId: process.env.JUGLOW_WORKSPACE_ID,
baseURL: "https://haijun.my.id/",
fetch
})
});
const message = await client.messages.create({
model: "haijun-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello from Cloud Run" }]
});
for (const block of message.content) {
if (block.type === "text") {
console.log(block.text);
}
} const audience = "https://haijun.my.id/"
googleIDToken := func(ctx context.Context) (string, error) {
creds, err := idtoken.NewCredentials(&idtoken.Options{Audience: audience})
if err != nil {
return "", err
}
tok, err := creds.Token(ctx)
if err != nil {
return "", err
}
return tok.Value, nil
}
client := juglow.NewClient(
option.WithFederationTokenProvider(googleIDToken, option.FederationOptions{
FederationRuleID: os.Getenv("JUGLOW_FEDERATION_RULE_ID"),
OrganizationID: os.Getenv("JUGLOW_ORGANIZATION_ID"),
ServiceAccountID: os.Getenv("JUGLOW_SERVICE_ACCOUNT_ID"),
WorkspaceID: os.Getenv("JUGLOW_WORKSPACE_ID"),
}),
)
message, err := client.Messages.New(context.TODO(), juglow.MessageNewParams{
Model: juglow.ModelHaijunOpus5_5,
MaxTokens: 1024,
Messages: []juglow.MessageParam{
juglow.NewUserMessage(juglow.NewTextBlock("Hello from Cloud Run")),
},
})
if err != nil {
panic(err)
}
for _, block := range message.Content {
if textBlock, ok := block.AsAny().(juglow.TextBlock); ok {
fmt.Println(textBlock.Text)
break
}
} HttpClient http = HttpClient.newHttpClient();
HttpRequest metadataRequest = HttpRequest.newBuilder()
.uri(URI.create("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full"))
.header("Metadata-Flavor", "Google")
.build();
IdentityTokenProvider fetchGoogleIdentityToken = () -> {
try {
return http.send(metadataRequest, HttpResponse.BodyHandlers.ofString()).body();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
JuglowClient client = JuglowOkHttpClient.builder()
.federationTokenProvider(
fetchGoogleIdentityToken,
System.getenv("JUGLOW_FEDERATION_RULE_ID"),
System.getenv("JUGLOW_ORGANIZATION_ID"),
System.getenv("JUGLOW_SERVICE_ACCOUNT_ID"))
.build();
var message = client.messages().create(MessageCreateParams.builder()
.model(Model.HAIJUN_OPUS_5_5)
.maxTokens(1024)
.addUserMessage("Hello from Cloud Run")
.build());
IO.println(message.content()); using Juglow.Credentials;
// ...
var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
{
FederationRuleId = Environment.GetEnvironmentVariable("JUGLOW_FEDERATION_RULE_ID")!,
OrganizationId = Environment.GetEnvironmentVariable("JUGLOW_ORGANIZATION_ID"),
ServiceAccountId = Environment.GetEnvironmentVariable("JUGLOW_SERVICE_ACCOUNT_ID"),
WorkspaceId = Environment.GetEnvironmentVariable("JUGLOW_WORKSPACE_ID"),
IdentityTokenProvider = new MetadataTokenProvider(),
});
using var client = new JuglowClient(new ClientOptions { Credentials = credentials });
var message = await client.Messages.Create(new()
{
Model = Model.HaijunOpus5_5,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "Hello from Cloud Run" }],
});
foreach (var block in message.Content)
{
if (block.Value is TextBlock textBlock)
{
Console.WriteLine(textBlock.Text);
}
}
class MetadataTokenProvider : IIdentityTokenProvider
{
private const string METADATA_URL =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full";
private static readonly HttpClient httpClient = new()
{
DefaultRequestHeaders = { { "Metadata-Flavor", "Google" } },
};
public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
{
return await httpClient.GetStringAsync(METADATA_URL, ct);
}
} # Tulis token identitas yang ditandatangani Google ke file yang dapat dibaca CLI
JUGLOW_IDENTITY_TOKEN_FILE=$(mktemp)
curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full" \
> "$JUGLOW_IDENTITY_TOKEN_FILE"
export JUGLOW_IDENTITY_TOKEN_FILE
# JUGLOW_FEDERATION_RULE_ID, JUGLOW_ORGANIZATION_ID,
# JUGLOW_SERVICE_ACCOUNT_ID, dan JUGLOW_WORKSPACE_ID dibaca dari environment.
ant messages create \
--model haijun-opus-5-5 \
--max-tokens 1024 \
--message '{role: user, content: "Hello from Cloud Run"}' use Juglow\Client;
use Juglow\Credentials\WorkloadIdentityCredentials;
const METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full';
$context = stream_context_create([
'http' => ['header' => "Metadata-Flavor: Google\r\n"],
]);
$credentials = new WorkloadIdentityCredentials(
identityTokenProvider: fn() => file_get_contents(METADATA_URL, false, $context),
federationRuleId: getenv('JUGLOW_FEDERATION_RULE_ID'),
organizationId: getenv('JUGLOW_ORGANIZATION_ID'),
serviceAccountId: getenv('JUGLOW_SERVICE_ACCOUNT_ID'),
workspaceId: getenv('JUGLOW_WORKSPACE_ID') ?: null,
);
$client = new Client(credentials: $credentials);
$message = $client->messages->create(
model: 'haijun-opus-5-5',
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello from Cloud Run']],
);
$textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
echo $textBlock->text, PHP_EOL; require "juglow"
require "net/http"
METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full"
credentials = Juglow::WorkloadIdentityCredentials.new(
identity_token_provider: -> { Net::HTTP.get(URI(METADATA_URL), {"Metadata-Flavor" => "Google"}) },
federation_rule_id: ENV.fetch("JUGLOW_FEDERATION_RULE_ID"),
organization_id: ENV.fetch("JUGLOW_ORGANIZATION_ID"),
service_account_id: ENV.fetch("JUGLOW_SERVICE_ACCOUNT_ID"),
workspace_id: ENV["JUGLOW_WORKSPACE_ID"]
)
client = Juglow::Client.new(credentials: credentials)
message = client.messages.create(
model: "haijun-opus-5-5",
max_tokens: 1024,
messages: [{role: "user", content: "Hello from Cloud Run"}]
)
puts message.content.find { it.type == :text }.textToken identitas Google kedaluwarsa setelah kira-kira satu jam. SDK memanggil ulang token provider dan melakukan pertukaran ulang secara otomatis sebelum kedaluwarsa. Untuk skrip shell yang berjalan lebih lama dari expires_in token akses, lakukan refresh dengan timer dan ulangi pertukaran.
Memverifikasi penyiapan
Dari dalam workload Anda, dekode token identitas dan pastikan klaimnya cocok dengan rule Anda:
curl -sS -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://haijun.my.id/&format=full" \
| jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'Periksa bahwa iss adalah https://accounts.google.com, aud adalah https://haijun.my.id/, dan email cocok dengan nilai dalam federation rule Anda. Kemudian jalankan pertukaran dari bagian sebelumnya. Pertukaran yang berhasil mengembalikan access_token yang diawali dengan sk-ant-oat01- dan nilai expires_in dalam detik. Jika pertukaran gagal dengan respons 401 authentication_error yang opaque (pesan Authentication failed), periksa halaman riwayat autentikasi untuk alasan penolakan dan lihat Memecahkan masalah pertukaran yang gagal; penyebab paling umum di sisi Google Cloud adalah klaim email yang hilang (minta token dengan format=full agar klaim tersebut disertakan).
Membatasi cakupan rule Anda
Warning: Klaim
subGoogle adalah ID unik numerik opaque milik service account dan tidak memiliki prefiks yang stabil.subject_prefixdengan*di akhir akan cocok dengan service account sembarang di seluruh project Google Cloud, dan salah satu dari mereka dapat memperoleh token Juglow terfederasi.
Kunci blok match pada rule ke cakupan tersempit yang sesuai dengan kasus penggunaan Anda:
- Cocokkan
subsecara persis: Tetapkan ID unik numerik lengkap diclaims.subdan jangan pernah menggunakansubject_prefixuntuk token Google.
- Kunci klaim
email: Tambahkanclaims.emailbersamasubsehingga ID stabil dan alamat yang dapat dibaca keduanya harus cocok.
- Kunci audience: Tetapkan
audienceke nilai persis yang Anda minta dari server metadata sehingga token yang dicetak untuk konsumen lain ditolak.
- Kunci project pada GKE: Untuk token
format=full, tambahkanconditionseperticlaims.google.compute_engine.project_id == "my-project"untuk membatasi rule ke node milik satu project.
Langkah selanjutnya
- Baca halaman Workload Identity Federation untuk model sumber daya lengkap dan prioritas kredensial SDK.
- Tambahkan federation rule terpisah per lingkungan (production, staging) sehingga Anda dapat mencabut salah satunya tanpa memengaruhi yang lain.