GCP 内部から外部へ公開しない HTTP Cloud Functions へ request を送信できるようにしたい
GCP の project 内部から、public には公開しない private な HTTP Cloud Functions (2nd gen) に対して secure に request を送信したい。
VPC Connector を利用する方法もあるみたいだが、今回は IAM の設定のみで実現する方法を試してみる。
ドキュメントはこれ↓
To configure the receiving function to accept requests from a specific calling function, you need to grant the appropriate invoker role to the calling function's service account on the receiving function. For 1st gen functions, the invoker role is Cloud Functions Invoker (roles/cloudfunctions.invoker). For 2nd gen functions, the invoker role is Cloud Run Invoker (roles/run.invoker) and must be granted on the underlying service.
invoker role を付与した service account による認証を行う。
2nd gen の場合、gcloud functions add-invoker-policy-binding で設定すると、roles/run.invoker が付与される。
gcloud functions add-invoker-policy-binding RECEIVING_FUNCTION \
--member='serviceAccount:CALLING_FUNCTION_IDENTITY'
Create a Google-signed ID token with the audience field (aud) set to the URL of the receiving function.
Include the ID token in an Authorization: Bearer ID_TOKEN header in the request to the function.
ID Token を作成し、Authorization: Bearer ID_TOKEN でリクエストする。
Generating tokens programmatically
By far the easiest and most reliable way to manage this process is to use the authentication libraries, as shown below, to generate and employ this token.
google-auth-library を利用するのがおすすめらしい。
sample code↓
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
// Cloud Functions uses your function's url as the `targetAudience` value
// const targetAudience = 'https://project-region-projectid.cloudfunctions.net/myFunction';
// For Cloud Functions, endpoint (`url`) and `targetAudience` should be equal
// const url = targetAudience;
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
async function request() {
console.info(`request ${url} with target audience ${targetAudience}`);
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}
request().catch(err => {
console.error(err.message);
process.exitCode = 1;
});
sample code 通りにやったがうまくいかない....
404 返ってくるぞ...
After the following code generates an ID token, it calls your Cloud Function with that token on your behalf. This code works in any environment where the libraries can obtain authentication credentials, including environments that support local Application Default Credentials.
わかったぞ
上記のsample code は
- ID token の生成
- 生成した ID Token を Header に付与した状態で request の送信
の二つを一気にやってくれてるんだな。
今回テストした functions は REST API で、存在しない route なので 404 になってたみたい。
しかし、request 用の client は自前のものを利用したいから、今回は ID Token だけ取りたい...
これでいけた↓
const auth = new GoogleAuth();
const targetAudience = 'https://project-region-projectid.cloudfunctions.net/myFunction';
const client = await auth.getIdTokenClient(targetAudience);
const token = await client.idTokenProvider.fetchIdToken(targetAudience);
IdTokenClient に IdTokenProvider.fetchIdToken が生えてたので利用。
export interface IdTokenProvider {
fetchIdToken: (targetAudience: string) => Promise<string>;
}
export declare class IdTokenClient extends OAuth2Client {
targetAudience: string;
idTokenProvider: IdTokenProvider;
/**
* Google ID Token client
*
* Retrieve access token from the metadata server.
* See: https://developers.google.com/compute/docs/authentication
*/
constructor(options: IdTokenOptions);
protected getRequestMetadataAsync(url?: string | null): Promise<RequestMetadataResponse>;
private getIdTokenExpiryDate;
}
client.credentials.id_token も生えているが、この値は一度 request を送信しない限り undefined になるので、client.idTokenProvider.fetchIdToken の方が良さげ
const targetAudience =
'https://project-region-projectid.cloudfunctions.net/myFunction';
const client = await auth.getIdTokenClient(targetAudience);
console.log(client.credentials.id_token);
// => undefined
await client.request({
url: targetAudience,
});
console.log(client.credentials.id_token)
// => idToken あり
あとは、この ID Token を Header Authorization: Bearer ID_TOKEN につけて request を送信すればOK
getClient を利用するパターン
ID token の取得に関するドキュメントあった↓
const googleAuth = new GoogleAuth();
const targetAudience =
'https://project-region-projectid.cloudfunctions.net/myFunction';
const client = (await googleAuth.getClient()) as Compute;
const idToken = await client.fetchIdToken(targetAudience);
console.log(idToken);
純粋に ID token を取得するだけならこれだけでいける。
ただし、googleAuth.getClient() の戻り値が Compute | JSONClient で、fetchIdToken は Compute にのみ存在するメソッドなので、型的には Compute にキャストする必要がある。
getClient(): Promise<Compute | JSONClient | T>;
metadata server を直接叩く
google-auth-library を利用しないで、metadata server を直接叩く方法もある。
curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=AUDIENCE" \
-H "Metadata-Flavor: Google"
AUDIENCE: function の URL など
firebase functions で動作確認する例
import { onRequest } from 'firebase-functions/v2/https';
export const printIdToken = onRequest(async (req, res) => {
const audience =
'https://project-region-projectid.cloudfunctions.net/myFunction';
const response = await fetch(
`http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=${audience}`,
{
headers: {
'Metadata-Flavor': 'Google',
},
},
);
if (!response.ok) {
throw new Error(`Failed to fetch id token: ${response.statusText}`);
}
const idToken = await response.text();
console.log('idToken:', idToken);
res.send('ok');
});