서비스 간 인증

아키텍처에서 여러 서비스를 사용하는 경우 이러한 서비스는 비동기 또는 동기 수단을 사용하여 서로 통신해야 할 수 있습니다. 이러한 서비스 대부분은 비공개일 수 있으며 따라서 액세스에 사용자 인증 정보가 필요합니다.

비동기 통신에는 다음 Google Cloud 서비스를 사용할 수 있습니다.

  • 일대일 비동기 통신을 위한 Cloud Tasks
  • 일대다, 일대일, 다대일 비동기 통신을 위한 Pub/Sub
  • 정기적으로 예약된 비동기 통신을 위한 Cloud Scheduler
  • 이벤트 기반 통신을 위한 Eventarc

위 모든 경우에 사용되는 서비스는 설정한 구성에 따라 수신 서비스와의 상호작용을 관리합니다.

하지만 동기 통신의 경우에는 서비스가 HTTP를 통해 엔드포인트 URL을 사용하여 다른 서비스를 직접 호출합니다. 이 사용 사례에서는 각 서비스가 특정 서비스에 대해서만 요청을 수행할 수 있는지 확인해야 합니다. 예를 들어 login 서비스가 있으면 user-profiles 서비스에 액세스할 수 있지만 search 서비스에는 액세스할 수 없습니다.

이 경우 IAM서비스별 사용자 관리형 서비스 계정에 따라 부여된 서비스 ID를 사용하여 작업을 수행하는 데 필요한 최소 권한 모음입니다.

또한 요청은 호출 서비스의 ID를 증명해야 합니다. 이렇게 하려면 호출 서비스가 Google 서명 OpenID Connect ID 토큰을 요청의 일부로 추가하도록 구성합니다.

서비스 계정 설정

서비스 계정을 설정하려면 수신 서비스에서 호출 서비스의 서비스 계정 주 구성원을 만들어 수신 서비스에서 호출 서비스의 요청을 수락하도록 구성합니다. 그런 다음 해당 서비스 계정에 Cloud Run 호출자(roles/run.invoker) 역할을 부여합니다. 이 두 태스크를 모두 수행하려면 적절한 탭의 안내를 따르세요.

콘솔 UI

  1. Google Cloud 콘솔로 이동합니다.

    Google Cloud 콘솔로 이동

  2. 수신 서비스를 선택합니다.

  3. 오른쪽 상단 모서리에 있는 정보 패널 표시를 클릭하여 권한 탭을 표시합니다.

  4. 주 구성원 추가를 클릭합니다.

    1. 호출 서비스의 ID를 입력합니다. 일반적으로 이메일 주소이며 기본적으로 PROJECT_NUMBER-compute@developer.gserviceaccount.com입니다.

    2. 역할 선택 드롭다운 메뉴에서 Cloud Run Invoker 역할을 선택합니다.

    3. 저장을 클릭합니다.

gcloud

gcloud run services add-iam-policy-binding 명령어를 사용합니다.

gcloud run services add-iam-policy-binding RECEIVING_SERVICE \
  --member='serviceAccount:CALLING_SERVICE_IDENTITY' \
  --role='roles/run.invoker'

여기서 RECEIVING_SERVICE는 수신 서비스의 이름이고 CALLING_SERVICE_IDENTITY는 기본적으로 서비스 계정의 이메일 주소입니다(PROJECT_NUMBER-compute@developer.gserviceaccount.com).

Terraform

Terraform 구성을 적용하거나 삭제하는 방법은 기본 Terraform 명령어를 참조하세요.

다음 Terraform 코드는 공개로 사용할 초기 Cloud Run 서비스를 만듭니다.

resource "google_cloud_run_v2_service" "public" {
  name     = "public-service"
  location = "us-central1"

  template {
    containers {
      # TODO<developer>: replace this with a public service container
      # (This service can be invoked by anyone on the internet)
      image = "us-docker.pkg.dev/cloudrun/container/hello"

      # Include a reference to the private Cloud Run
      # service's URL as an environment variable.
      env {
        name  = "URL"
        value = google_cloud_run_v2_service.private.uri
      }
    }
    # Give the "public" Cloud Run service
    # a service account's identity
    service_account = google_service_account.default.email
  }
}

us-docker.pkg.dev/cloudrun/container/hello를 컨테이너 이미지에 대한 참조로 바꿉니다.

다음 Terraform 코드는 초기 서비스를 공개로 설정합니다.

data "google_iam_policy" "public" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "public" {
  location = google_cloud_run_v2_service.public.location
  project  = google_cloud_run_v2_service.public.project
  service  = google_cloud_run_v2_service.public.name

  policy_data = data.google_iam_policy.public.policy_data
}

다음 Terraform 코드는 비공개로 사용할 두 번째 Cloud Run 서비스를 만듭니다.

resource "google_cloud_run_v2_service" "private" {
  name     = "private-service"
  location = "us-central1"

  template {
    containers {
      // TODO<developer>: replace this with a private service container
      // (This service should only be invocable by the public service)
      image = "us-docker.pkg.dev/cloudrun/container/hello"
    }
  }
}

us-docker.pkg.dev/cloudrun/container/hello를 컨테이너 이미지에 대한 참조로 바꿉니다.

다음 Terraform 코드는 두 번째 서비스를 비공개로 만듭니다.

data "google_iam_policy" "private" {
  binding {
    role = "roles/run.invoker"
    members = [
      "serviceAccount:${google_service_account.default.email}",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "private" {
  location = google_cloud_run_v2_service.private.location
  project  = google_cloud_run_v2_service.private.project
  service  = google_cloud_run_v2_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

다음 Terraform 코드는 서비스 계정을 만듭니다.

resource "google_service_account" "default" {
  account_id   = "cloud-run-interservice-id"
  description  = "Identity used by a public Cloud Run service to call private Cloud Run services."
  display_name = "cloud-run-interservice-id"
}

다음 Terraform 코드는 서비스 계정에 연결된 서비스가 초기 비공개 Cloud Run 서비스를 호출하도록 허용합니다.

data "google_iam_policy" "private" {
  binding {
    role = "roles/run.invoker"
    members = [
      "serviceAccount:${google_service_account.default.email}",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "private" {
  location = google_cloud_run_v2_service.private.location
  project  = google_cloud_run_v2_service.private.project
  service  = google_cloud_run_v2_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

ID 토큰 획득 및 구성

호출 서비스 계정에 적절한 역할을 부여한 후 다음 단계를 수행합니다.

  1. 다음 섹션에 설명된 방법 중 하나를 사용하여 Google 서명 ID 토큰을 가져옵니다. 대상 클레임(aud)을 수신 서비스의 URL이나 구성된 커스텀 대상으로 설정합니다. 커스텀 대상을 사용하지 않는 경우 특정 트래픽 태그에 대한 요청을 수행할 때도 aud 값이 서비스의 URL로 유지되어야 합니다.

  2. 이전 단계에서 가져온 ID 토큰을 수신 서비스에 대한 요청의 다음 헤더 중 하나에 추가합니다.

    • Authorization: Bearer ID_TOKEN 헤더.
    • X-Serverless-Authorization: Bearer ID_TOKEN 헤더. 애플리케이션이 이미 커스텀 승인에 Authorization 헤더를 사용하는 경우 이 헤더를 사용할 수 있습니다. 이렇게 하면 토큰을 사용자 컨테이너에 전달하기 전에 서명이 삭제됩니다.

이 페이지에 설명되지 않은 ID 토큰을 가져오는 다른 방법은 ID 토큰을 가져오는 방법을 참조하세요.

인증 라이브러리 사용

이 프로세스를 관리하는 가장 쉽고 안정적인 방법은 다음에 표시된 것처럼 인증 라이브러리를 사용하여 이 토큰을 생성 및 사용하는 것입니다. 이 코드는 라이브러리가 로컬 애플리케이션 기본 사용자 인증 정보를 지원하는 환경을 포함하여 서비스 계정에 대한 사용자 인증 정보를 가져올 수 있는 모든 환경에서(Google Cloud 외부에서도) 작동합니다. 하지만 사용자 계정에 대한 사용자 인증 정보를 가져오는 데는 이 코드가 작동하지 않습니다.

Node.js

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// Example: https://my-cloud-run-service.run.app/books/delete/12345
// const url = 'https://TARGET_HOSTNAME/TARGET_URL';

// Example (Cloud Run): https://my-cloud-run-service.run.app/
// const targetAudience = 'https://TARGET_AUDIENCE/';

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);

  // Alternatively, one can use `client.idTokenProvider.fetchIdToken`
  // to return the ID Token.
  const res = await client.request({url});
  console.info(res.data);
}

request().catch(err => {
  console.error(err.message);
  process.exitCode = 1;
});

Python

import urllib

import google.auth.transport.requests
import google.oauth2.id_token

def make_authorized_get_request(endpoint, audience):
    """
    make_authorized_get_request makes a GET request to the specified HTTP endpoint
    by authenticating with the ID token obtained from the google-auth client library
    using the specified audience value.
    """

    # Cloud Run uses your service's hostname as the `audience` value
    # audience = 'https://my-cloud-run-service.run.app/'
    # For Cloud Run, `endpoint` is the URL (hostname + path) receiving the request
    # endpoint = 'https://my-cloud-run-service.run.app/my/awesome/url'

    req = urllib.request.Request(endpoint)

    auth_req = google.auth.transport.requests.Request()
    id_token = google.oauth2.id_token.fetch_id_token(auth_req, audience)

    req.add_header("Authorization", f"Bearer {id_token}")
    response = urllib.request.urlopen(req)

    return response.read()

Go


import (
	"context"
	"fmt"
	"io"

	"google.golang.org/api/idtoken"
)

// `makeGetRequest` makes a request to the provided `targetURL`
// with an authenticated client using audience `audience`.
func makeGetRequest(w io.Writer, targetURL string, audience string) error {
	// Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
	// (`targetURL` and `audience` will differ for non-root URLs and GET parameters)
	ctx := context.Background()

	// client is a http.Client that automatically adds an "Authorization" header
	// to any requests made.
	client, err := idtoken.NewClient(ctx, audience)
	if err != nil {
		return fmt.Errorf("idtoken.NewClient: %w", err)
	}

	resp, err := client.Get(targetURL)
	if err != nil {
		return fmt.Errorf("client.Get: %w", err)
	}
	defer resp.Body.Close()
	if _, err := io.Copy(w, resp.Body); err != nil {
		return fmt.Errorf("io.Copy: %w", err)
	}

	return nil
}

자바

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 using the specified `audience`.
  //
  // Example `audience` value (Cloud Run): https://my-cloud-run-service.run.app/
  public static HttpResponse makeGetRequest(String serviceUrl, String audience) 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(audience)
            .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();
  }
}

메타데이터 서버 사용

어떤 이유로든 인증 라이브러리를 사용할 수 없는 경우 컨테이너가 Cloud Run에서 실행되는 동안 Compute 메타데이터 서버에서 ID 토큰을 가져올 수 있습니다. 이 방법은 로컬 머신을 포함하여 Google Cloud 외부에서 작동하지 않습니다.

curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=[AUDIENCE]" \
     -H "Metadata-Flavor: Google"

여기서 대상은 호출하는 서비스의 URL 또는 구성된 커스텀 대상입니다.

다음 표에서는 메타데이터 쿼리 요청의 주요 부분을 요약해서 보여줍니다.

구성요소 설명
루트 URL

모든 메타데이터 값은 다음 루트 URL 아래의 하위 경로로 정의됩니다.


http://metadata.google.internal/computeMetadata/v1
요청 헤더

각 요청에 다음 헤더가 있어야 합니다.


Metadata-Flavor: Google

이 헤더는 요청이 안전하지 않은 소스에서 의도하지 않게 전송된 것이 아니라 메타데이터 값을 검색하기 위한 목적으로 전송되었음을 나타내며, 이 헤더가 있으면 메타데이터 서버가 요청된 데이터를 반환할 수 있습니다. 이 헤더를 제공하지 않으면 메타데이터 서버에서 요청이 거부됩니다.

이 서비스 간 인증 기술을 사용하는 애플리케이션에 대한 엔드 투 엔드 연습을 위해서는 Cloud Run 서비스 보안 튜토리얼을 참조하세요.

Google Cloud 외부에서 워크로드 아이덴티티 제휴 사용

워크로드 아이덴티티 제휴에서 지원되는 ID 공급업체를 사용하는 환경인 경우 다음 방법을 사용하여 Google Cloud 외부에서 Cloud Run 서비스에 안전하게 인증할 수 있습니다.

  1. 이 페이지의 서비스 계정 설정에 설명된 대로 서비스 계정을 설정합니다.

  2. 워크로드 아이덴티티 제휴 구성에 설명된 대로 ID 공급업체에 대해 워크로드 아이덴티티 제휴를 구성합니다.

  3. 외부 ID에 서비스 계정을 가장할 수 있는 권한 부여의 안내를 따릅니다.

  4. REST API를 사용하여 단기 토큰을 획득합니다. 하지만 generateAccessToken을 호출하여 액세스 토큰을 획득하는 대신 generateIdToken을 호출하여 ID 토큰을 가져옵니다.

    다음은 cURL 사용 예시입니다.

    ID_TOKEN=$(curl -0 -X POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SERVICE_ACCOUNT:generateIdToken \
      -H "Content-Type: text/json; charset=utf-8" \
      -H "Authorization: Bearer $STS_TOKEN" \
      -d @- <<EOF | jq -r .token
      {
          "audience": "SERVICE_URL"
      }
    EOF
    )
    echo $ID_TOKEN
    

    여기서 SERVICE_ACCOUNT는 워크로드 아이덴티티 풀이 액세스하도록 구성된 서비스 계정의 이메일 주소이고 SERVICE_URL은 호출하는 Cloud Run 서비스의 URL입니다. 이 값은 특정 트래픽 태그에 대한 요청을 수행할 때도 해당 서비스의 URL로 유지되어야 합니다. $STS_TOKEN은 이전 단계의 워크로드 아이덴티티 제휴 안내에서 받은 보안 토큰 서비스 토큰입니다.

Authorization: Bearer ID_TOKEN 헤더 또는 X-Serverless-Authorization: Bearer ID_TOKEN 헤더를 사용하여 서비스에 대한 요청에 이전 단계의 ID 토큰을 포함할 수 있습니다. 두 헤더를 모두 제공하면 X-Serverless-Authorization 헤더만 확인됩니다.

Google Cloud 외부에서 다운로드한 서비스 계정 키 사용

워크로드 아이덴티티 제휴가 환경에 적합하지 않은 경우에는 다운로드한 서비스 계정 키를 사용하여 Google Cloud 외부에서 인증하면 됩니다. 애플리케이션 기본 사용자 인증 정보와 관련하여 전에 설명한 대로 인증 라이브러리를 사용하도록 클라이언트 코드를 업데이트합니다.

자체 서명 JWT를 사용하여 Google 서명 ID 토큰을 가져올 수 있지만 이 방법은 매우 복잡하며 오류가 발생하기 쉽습니다. 기본 단계는 다음과 같습니다.

  1. target_audience 클레임이 수신 서비스의 URL로 설정되어 있거나 구성된 커스텀 대상이 설정된 서비스 계정 JWT에 자체 서명합니다. 커스텀 도메인을 사용하지 않는 경우 특정 트래픽 태그에 대한 요청을 수행할 때도 target_audience 값이 서비스의 URL로 유지되어야 합니다.

  2. Google이 서명한 ID 토큰과 자체 서명한 JWT를 교환합니다. 이 토큰에는 앞에 나온 URL로 설정된 aud 클레임이 있습니다.

  3. Authorization: Bearer ID_TOKEN 헤더 또는 X-Serverless-Authorization: Bearer ID_TOKEN 헤더를 사용하여 서비스에 대한 요청에 ID 토큰을 포함합니다. 두 헤더를 모두 제공하면 X-Serverless-Authorization 헤더만 확인됩니다.

Cloud Functions 예시를 통해 앞 단계의 샘플을 검사할 수 있습니다.

인증된 요청 수신

수신되는 비공개 서비스 내에서 Bearer 토큰이 전송하는 정보를 수신하도록 승인 헤더를 파싱할 수 있습니다.

Python


from google.auth.transport import requests
from google.oauth2 import id_token

def receive_authorized_get_request(request):
    """Parse the authorization header and decode the information
    being sent by the Bearer token.

    Args:
        request: Flask request object

    Returns:
        The email from the request's Authorization header.
    """
    auth_header = request.headers.get("Authorization")
    if auth_header:
        # split the auth type and value from the header.
        auth_type, creds = auth_header.split(" ", 1)

        if auth_type.lower() == "bearer":
            claims = id_token.verify_token(creds, requests.Request())
            return f"Hello, {claims['email']}!\n"

        else:
            return f"Unhandled header format ({auth_type}).\n"
    return "Hello, anonymous user.\n"