概要
Cloud Runを例にアクセスに認証情報が必要な非公開サービスと通信するための手順をまとめる。
参照ドキュメント(Cloud Run)
⑴クイックスタート: ビルドとデプロイ
https://cloud.google.com/run/docs/quickstarts/build-and-deploy/
⑵認証の概要
https://cloud.google.com/run/docs/authenticating/overview
⑶サービス間認証
https://cloud.google.com/run/docs/authenticating/service-to-service
全体像
手順
- アプリケーションを Cloud Run にデプロイ
- サービスアカウントの設定
- IDトークンの取得
- テスト
1.アプリケーションをデプロイ
まず、呼び出し側のアプリケーションを作成してコンテナ イメージにパッケージ化し、コンテナ イメージを Container Registry にアップロードして Cloud Run にデプロイする。*⑴
2. サービスアカウントの設定
呼び出し側サービスからのリクエストを受け入れるようにするためには、呼び出し元のサービスアカウントを受信側サービスのメンバーにする必要がある。*⑵
今回は、呼び出し元のサービスアカウントに Cloud Run 起動元(roles/run.invoker)のロールを付与する。*⑶
3. IDトークンの取得
Google 署名付きIDトークンを取得して、リクエストにセットする方法はドキュメントによると以下になる。
・受信側のサービスの URL に設定されたオーディエンスクレーム(aud)を使用して、Google 署名付き ID トークンを取得します。
( 現在、カスタム ドメインは aud 値でサポートされていません。)
・受信側サービスに対するリクエストの Authorization: Bearer ID_TOKEN ヘッダーに ID トークンを含めます。
オーディエンスクレーム(aud)ってなんぞや?が分からない。。。(分かる人コメントいただけると助かります)
受信側のサービスURLだということは分かる。
とりあえず受信側サービスのURLをもとにIDトークンを取得し、リクエストの Authorization: Bearer ID_TOKEN ヘッダーにセットすればいい。*⑶
以下のサンプルコードをもとに呼び出し側のサービスを実装。
- Javaのサンプルコード(GitHub)
https://github.com/GoogleCloudPlatform/java-docs-samples/tree/702077dcfe619cc3764a213f253beaefcdb04b43/run/authentication
package com.example.cloudrun;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import java.io.IOException;
public class Authentication {
// makeGetRequest makes a GET request to the specified Cloud Run or
// Cloud Functions endpoint, serviceUrl (must be a complete URL), by
// authenticating with an Id token retrieved from Application Default Credentials.
public static HttpResponse makeGetRequest(String serviceUrl) throws IOException {
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
if (!(credentials instanceof IdTokenProvider)) {
throw new IllegalArgumentException("Credentials are not an instance of IdTokenProvider.");
}
IdTokenCredentials tokenCredential =
IdTokenCredentials.newBuilder()
.setIdTokenProvider((IdTokenProvider) credentials)
.setTargetAudience(serviceUrl)
.build();
GenericUrl genericUrl = new GenericUrl(serviceUrl);
HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(tokenCredential);
HttpTransport transport = new NetHttpTransport();
HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl);
return request.execute();
}
}
- Goのサンプルコード(GitHub)
https://github.com/GoogleCloudPlatform/golang-samples/tree/2a6361b94cd77803b9a581422bdd827814576975/functions/security
package security
import (
"context"
"fmt"
"io"
"google.golang.org/api/idtoken"
)
// makeGetRequest makes a request to the provided targetURL with an authenticated client.
func makeGetRequest(w io.Writer, targetURL string) error {
// functionURL := "https://TARGET_URL"
ctx := context.Background()
// client is a http.Client that automatically adds an "Authorization" header
// to any requests made.
client, err := idtoken.NewClient(ctx, targetURL)
if err != nil {
return fmt.Errorf("idtoken.NewClient: %v", err)
}
resp, err := client.Get(targetURL)
if err != nil {
return fmt.Errorf("client.Get: %v", err)
}
defer resp.Body.Close()
if _, err := io.Copy(w, resp.Body); err != nil {
return fmt.Errorf("io.Copy: %v", err)
}
return nil
}
4. テスト
以下の手順にそってテスト開始
- サービスアカウントの認証情報の取得(呼び出し側サービスがGoogle Cloudの外部である場合)
参照:https://cloud.google.com/docs/authentication/production - サービス間認証を行わなかった時に、アクセスができないことを確認
- サービス間認証を行った時に、アクセスができることを確認
以下のコードはJavaでのテストコード
(受信側であるCloud RunのサービスURLをAuthentication.makeGetRequestの引数として渡して、レスポンスが返ってこれば、アクセスできたことが確認できる)
package com.example.cloudrun;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.core.StringContains.containsString;
import com.google.api.client.http.HttpResponse;
import java.io.IOException;
import org.junit.Test;
public class AuthenticationTest {
@Test
public void canMakeGetRequest() throws IOException {
String url = "https://helloworld-gqnrt6tdna-an.a.run.app"; // Cloud RunのサービスURL
HttpResponse response = Authentication.makeGetRequest(url);
// "Hello World!"と返すだけのアプリケーションを受信側のサービスとした場合、
assertThat(response.parseAsString(), containsString("Hello World!"));
assertThat(response.getContentType(), containsString("text/html"));
assertThat(response.getStatusCode(), equalTo(200));
}
@Test
public void failsMakeGetRequestWithoutProtocol() throws IOException {
String url = "helloworld-gqnrt6tdna-an.a.run.app/";
try {
Authentication.makeGetRequest(url);
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString("no protocol"));
}
}
}