서비스 간 인증

gRPC 서비스에서 서비스 계정을 사용하여 서비스 간 인증을 구현할 수 있습니다. 이 페이지에서는 gRPC 서비스에서 Extensible Service Proxy(ESP)를 구성하여 인증된 요청을 지원하는 방법과 gRPC 클라이언트에서 서비스를 호출하는 방법 등 전체적인 예시를 통해 서비스 간 인증 과정을 보여줍니다.

모든 서비스에서 Cloud Endpoints API에 인증 호출을 실행하려면 호출 서비스에 서비스 계정이 있어야 하고 호출 시 호출 서비스가 인증 토큰을 전송해야 합니다. 호출자는 호출자 서비스 계정으로 서명된 Google ID 토큰 또는 커스텀 JSON 웹 토큰(JWT)만 사용해야 합니다. ESP는 JWT의 iss 클레임이 서비스 구성의 issuer 설정과 일치하는지 검증합니다. ESP는 서비스 계정에 ID 및 액세스 관리 권한이 부여되었는지 여부는 확인하지 않습니다.

이 예에서는 클라이언트가 Google Cloud 서비스 계정을 사용하여 인증 JWT를 생성하는 가장 간단한 형태의 서비스 간 인증을 설정하고 사용합니다. 다른 인증 방법도 이와 유사하지만 유효한 인증 토큰을 얻기 위한 클라이언트 측 과정은 사용되는 인증 방식에 따라 다릅니다.

시작 전 유의사항

이 가이드에서는 Google의 가이드에서 사용한 Bookstore의 예시를 사용합니다.

  1. gRPC 예시 코드가 호스팅되는 Git 저장소를 복제합니다.

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
    
  2. 작업 디렉터리를 변경합니다.

    cd python-docs-samples/endpoints/bookstore-grpc/
    
  3. 아직 프로젝트가 없다면 가이드의 안내에 따라 프로젝트를 만듭니다.

이 예시에서는 Google Kubernetes Engine 배포를 사용하지만 인증 설정은 Compute Engine에서도 동일합니다.

이 예에서는 두 개의 Google Cloud Platform 프로젝트가 언급됩니다.

  • gRPC Endpoints 서비스를 소유하는 프로젝트인 서비스 생산자 프로젝트
  • gRPC 클라이언트를 소유하는 프로젝트인 서비스 소비자 프로젝트

소비자 서비스 계정과 키 만들기

소비자 프로젝트용 서비스 계정과 키를 만들려면 다음 안내를 따르세요.

  1. Google Cloud Console에서 API 및 서비스로 이동합니다.

    API 및 서비스

    현재 위치가 소비자 프로젝트인지 확인합니다.
  2. 사용자 인증 정보 페이지의 사용자 인증 정보 만들기 드롭다운 목록에서 서비스 계정 키를 선택합니다.
  3. 서비스 계정 키 만들기 페이지에 사용하려는 기존 서비스 계정이 있다면 해당 계정을 선택합니다. 그렇지 않다면 서비스 계정 드롭다운 목록에서 새 서비스 계정을 선택하고 계정 이름을 입력합니다.

    해당 서비스 계정 ID가 생성됩니다. 다음 섹션에서 필요하기 때문에 ID를 적어둡니다. 예를 들면 다음과 같습니다.

    service-account-name@YOUR_PROJECT_ID.iam.gserviceaccount.com
    
  4. 역할 드롭다운 목록을 클릭하고 다음 역할을 선택합니다.

    • 서비스 계정 > 서비스 계정 사용자
    • 서비스 계정 > 서비스 계정 토큰 생성자
  5. JSON 키 유형이 선택되었는지 확인합니다.

  6. 만들기를 클릭합니다. 서비스 계정 JSON 키 파일이 로컬 머신에 다운로드됩니다. 나중에 토큰을 생성할 때 사용해야 하므로 해당 위치를 확인하고 안전하게 저장되었는지 확인합니다.

서비스를 위한 인증 구성

이 섹션의 모든 단계에서 생산자 프로젝트를 사용합니다.

gRPC API 구성에 인증 설정

ESP 인증은 gRPC API 구성 YAML 파일의 authentication 섹션에서 구성됩니다. 이 예시 서비스의 인증 구성은 api_config_auth.yaml에 있습니다.

authentication:
  providers:
  - id: google_service_account
    # Replace SERVICE-ACCOUNT-ID with your service account's email address.
    issuer: SERVICE-ACCOUNT-ID
    jwks_uri: https://www.googleapis.com/robot/v1/metadata/x509/SERVICE-ACCOUNT-ID
  rules:
  # This auth rule will apply to all methods.
  - selector: "*"
    requirements:
      - provider_id: google_service_account

providers 섹션은 사용할 인증 공급자를 지정합니다. 이 경우에는 Google 서비스 계정을 인증 공급자로 사용하려 한다고 지정합니다. rules 섹션은 모든 서비스 메서드에 액세스하기 위해 이 공급자의 토큰이 필요하다고 지정합니다.

클론된 저장소에 있는 이 파일 사본에서 다음을 수행하세요.

  • MY_PROJECT_ID생산자 프로젝트 ID로 변경합니다.
  • authentication 섹션에서 SERVICE-ACCOUNT-ID를 이전 단계에서 적어둔 소비자 계정 ID로 변경합니다(issuer 값과 jwks_uri 값 모두에서). 이렇게 하면 이 특정 서비스 계정의 유효한 토큰을 제공하는 사용자에게 서비스 액세스 권한이 부여된다는 것을 ESP에서 알 수 있습니다.

다음 단계를 위해 파일을 저장합니다.

구성 및 서비스 배포

아래의 단계는 GKE에서 gRPC 시작하기와 동일합니다.

  1. 서비스 구성을 Endpoints에 배포: 가이드에서 이 과정을 이미 수행했더라도 구성이 다르기 때문에 이 작업을 수행해야 합니다. 반환되는 서비스 이름을 적어둡니다.

    gcloud endpoints services deploy api_descriptor.pb api_config_auth.yaml --project PRODUCER_PROJECT
    
  2. 컨테이너 클러스터를 만들고 아직 인증하지 않았다면 kubectl을 클러스터에 인증합니다.

  3. 샘플 API 및 ESP를 클러스터에 배포합니다. 생산자 프로젝트와 소비자 프로젝트를 별도로 사용하는 경우 먼저 gcloud 명령줄 도구 내에 적절한 프로젝트를 설정했는지 확인합니다.

    gcloud config set project PRODUCER_PROJECT
    

gRPC 클라이언트에서 인증된 메서드 호출

마지막으로 클라이언트 측에서 서비스 계정 키를 사용하여 JWT 토큰을 생성한 후에 토큰을 사용하여 인증된 Bookstore 메소드를 호출할 수 있습니다. 먼저 적절한 Python 요구사항을 설치하여 토큰을 생성하고 예시 클라이언트를 실행해야 합니다. 현재 위치가 클론된 클라이언트의 python-docs-samples/endpoints/bookstore-grpc 폴더인지 확인하고 다음을 수행합니다.

virtualenv bookstore-env
source bookstore-env/bin/activate
pip install -r requirements.txt

JWT 토큰 생성

이 예시에서 Bookstore는 호출 서비스가 서비스 계정으로만 인증되는 서비스 간 인증을 사용하므로 요청과 함께 전송할 적절한 토큰을 간편하게 만들 수 있습니다. 생성된 토큰을 Google에서 추가로 인증해야 하는 경우(Google ID 토큰을 사용하여) 더 엄격한 서비스 간 인증을 요구할 수도 있습니다.

이 예시에 나와 있는 Python 스크립트는 더미 사용자 ID와 이메일을 사용하여 앞에서 다운로드한 JSON 키 파일로 토큰을 생성할 수 있습니다.

#!/usr/bin/env python

# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of generating a JWT signed from a service account file."""

import argparse
import json
import time

import google.auth.crypt
import google.auth.jwt

"""Max lifetime of the token (one hour, in seconds)."""
MAX_TOKEN_LIFETIME_SECS = 3600

def generate_jwt(service_account_file, issuer, audiences):
    """Generates a signed JSON Web Token using a Google API Service Account."""
    with open(service_account_file, 'r') as fh:
        service_account_info = json.load(fh)

    signer = google.auth.crypt.RSASigner.from_string(
        service_account_info['private_key'],
        service_account_info['private_key_id'])

    now = int(time.time())

    payload = {
        'iat': now,
        'exp': now + MAX_TOKEN_LIFETIME_SECS,
        # aud must match 'audience' in the security configuration in your
        # swagger spec. It can be any string.
        'aud': audiences,
        # iss must match 'issuer' in the security configuration in your
        # swagger spec. It can be any string.
        'iss': issuer,
        # sub and email are mapped to the user id and email respectively.
        'sub': issuer,
        'email': 'user@example.com'
    }

    signed_jwt = google.auth.jwt.encode(signer, payload)
    return signed_jwt

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('--file',
                        help='The path to your service account json file.')
    parser.add_argument('--issuer', default='', help='issuer')
    parser.add_argument('--audiences', default='', help='audiences')

    args = parser.parse_args()

    signed_jwt = generate_jwt(args.file, args.issuer, args.audiences)
    print(signed_jwt.decode('utf-8'))

스크립트를 사용하여 토큰을 생성하려면 다음 안내를 따르세요.

  • JWT 토큰을 생성하여 $JWT_TOKEN 변수에 할당합니다.

    JWT_TOKEN=$(python jwt_token_gen.py \
        --file=[SERVICE_ACCOUNT_FILE] \
        --audiences=[SERVICE_NAME] \
        --issuer=[SERVICE-ACCOUNT-ID])
    

    각 항목의 의미는 다음과 같습니다.

    • [SERVICE_ACCOUNT_FILE]는 다운로드한 소비자 서비스 계정 JSON 키 파일입니다.
    • [SERVICE_NAME]는 업데이트된 서비스 구성을 Endpoints에 배포했을 때 반환된 Bookstore 서비스의 이름입니다.
    • [SERVICE-ACCOUNT-ID]는 서비스 계정을 생성했을 때의 전체 소비자 서비스 계정 ID입니다.

인증된 gRPC 호출 실행

이 마지막 단계에서는 가이드에서 사용한 것과 동일한 클라이언트인 bookstore_client.py를 사용합니다. 인증된 호출을 실행하기 위해 클라이언트가 JWT를 메타데이터로 메서드 호출과 함께 전달합니다.

def run(host, port, api_key, auth_token, timeout, use_tls):
    """Makes a basic ListShelves call against a gRPC Bookstore server."""

    if use_tls:
        with open('../roots.pem', 'rb') as f:
            creds = grpc.ssl_channel_credentials(f.read())
        channel = grpc.secure_channel('{}:{}'.format(host, port), creds)
    else:
        channel = grpc.insecure_channel('{}:{}'.format(host, port))

    stub = bookstore_pb2_grpc.BookstoreStub(channel)
    metadata = []
    if api_key:
        metadata.append(('x-api-key', api_key))
    if auth_token:
        metadata.append(('authorization', 'Bearer ' + auth_token))
    shelves = stub.ListShelves(empty_pb2.Empty(), timeout, metadata=metadata)
    print('ListShelves: {}'.format(shelves))

이 예시를 실행하려면 다음 안내를 따르세요.

  1. kubectl get services를 사용하여 배포된 Bookstore의 외부 IP 주소를 가져옵니다.

    #kubectl get services
    NAME                 CLUSTER-IP      EXTERNAL-IP      PORT(S)           AGE
    echo                 10.11.246.240   104.196.186.92   80/TCP            10d
    endpoints            10.11.243.168   104.196.210.50   80/TCP,8090/TCP   10d
    esp-grpc-bookstore   10.11.254.34    104.196.60.37    80/TCP            1d
    kubernetes           10.11.240.1     <none>           443/TCP           10d
    

    이 경우에는 esp-grpc-bookstore 서비스이고 외부 IP는 104.196.60.37입니다.

  2. IP 주소를 EXTERNAL_IP 변수에 할당합니다.

    EXTERNAL_IP=104.196.60.37
    
  3. Bookstore 서비스의 모든 실행기를 나열합니다.

    python bookstore_client.py --port=80 --host=$EXTERNAL_IP --auth_token=$JWT_TOKEN
    

    이 서비스는 현재 Bookstore에 있는 모든 실행기를 반환합니다. 토큰을 제공하지 않거나 JWT를 생성할 때 엉뚱한 서비스 계정 ID를 지정하여 다시 한 번 확인할 수 있습니다. 명령어가 실패합니다.

다음 단계