서비스 간 인증

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

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

비동기 통신에는 다음 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) 역할을 부여합니다. 이 두 태스크를 모두 수행하려면 적절한 탭의 안내를 따르세요.

Console UI

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

    Google Cloud Console로 이동

  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 코드는 공개로 의도된 초기 Cloud Run 서비스를 만듭니다.

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

  template {
    spec {
      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_service.private.status[0].url
        }
      }

      # Give the "public" Cloud Run service
      # a service account's identity
      service_account_name = 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_service.public.location
  project  = google_cloud_run_service.public.project
  service  = google_cloud_run_service.public.name

  policy_data = data.google_iam_policy.public.policy_data
}

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

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

  template {
    spec {
      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_service.private.location
  project  = google_cloud_run_service.private.project
  service  = google_cloud_run_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_service.private.location
  project  = google_cloud_run_service.private.project
  service  = google_cloud_run_service.private.name

  policy_data = data.google_iam_policy.private.policy_data
}

ID 토큰 획득 및 구성

호출 서비스 계정에 적절한 역할이 부여되면 다음을 수행해야 합니다.

  1. 대상 클레임(aud)이 수신 서비스의 URL로 설정된 Google 서명 ID 토큰을 가져옵니다. aud 값은 특정 트래픽 태그에 대한 요청을 수행할 때도 해당 서비스의 URL로 유지되어야 합니다.

  2. ID 토큰을 수신 서비스에 대한 요청의 Authorization: Bearer ID_TOKEN 헤더에 포함합니다.

이 프로세스를 관리하는 가장 쉽고 안정적인 방법은 다음에 표시된 것처럼 인증 라이브러리를 사용하여 이 토큰을 생성 및 사용하는 것입니다. 이 코드는 라이브러리가 로컬 애플리케이션 기본 사용자 인증 정보를 지원하는 환경을 포함하여 사용자 인증 정보를 가져올 수 있는 모든 환경에서(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);
  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: %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
}

자바

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"

여기서 AUDIENCE는 호출할 서비스의 URL입니다.

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

인증된 요청 수신

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

Python


from google.auth import jwt

def receive_authorized_get_request(request):
    """
    receive_authorized_get_request takes the "Authorization" header from a
    request, decodes it using the google-auth client library, and returns
    back the email from the header to the caller.
    """
    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 = jwt.decode(creds, verify=False)
            return f"Hello, {claims['email']}!\n"

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

Google Cloud 외부에서 호출

워크로드 아이덴티티 제휴 또는 다운로드한 서비스 계정 키를 사용하여 Google Cloud 외부에서 비공개 서비스를 호출할 수 있습니다. 두 가지 방법 모두 다음 섹션에 설명되어 있습니다.

워크로드 아이덴티티 제휴 사용

워크로드 아이덴티티 제휴에서 지원되는 ID 공급업체를 사용하는 환경인 경우 이 방법을 사용하여 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은 이전 단계의 워크로드 아이덴티티 제휴 안내에서 받은 보안 토큰 서비스 토큰입니다.

ID 토큰이 있으면 수신 서비스에 대한 요청의 Authorization: Bearer ID_TOKEN 헤더에 포함할 수 있습니다.

다운로드한 서비스 계정 키 사용

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

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

  1. target_audience 클레임이 수신 서비스의 URL로 설정된 서비스 계정 JWT에 자체 서명합니다. target_audience 값은 특정 트래픽 태그에 대한 요청을 수행할 때도 해당 서비스의 URL로 유지되어야 합니다.

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

  3. ID 토큰을 서비스에 대한 요청의 Authorization: Bearer ID_TOKEN 헤더에 포함합니다.

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