아웃바운드 요청 마이그레이션

기본적으로 Python 2.7 런타임은 URL Fetch 서비스를 사용하여 아웃바운드 HTTP(S) 요청을 처리합니다. urllib, urllib2, httplib Python 라이브러리를 사용하여 이러한 요청을 실행하는 경우에도 마찬가지입니다. URL Fetch는 사용자가 명시적으로 사용 설정하지 않는 한 requests 라이브러리의 요청을 처리하지 않습니다.

Python 3 런타임은 아웃바운드 요청을 처리하는 데 중개 서비스가 필요하지 않습니다. URL Fetch API 사용을 중지하되 유사한 기능이 여전히 필요한 경우 requests 라이브러리와 같은 표준 Python 라이브러리를 사용하도록 해당 요청을 마이그레이션해야 합니다.

URL Fetch와 표준 Python 라이브러리 간의 주요 차이점

  • URL Fetch에서 처리되는 요청의 크기 제한할당량은 URL Fetch에서 처리하지 않는 요청의 크기 제한할당량과 다릅니다.

  • URL Fetch를 사용하면 앱이 다른 App Engine 앱에 요청을 전송할 때 URL Fetch가 X-Appengine-Inbound-Appid 요청 헤더를 추가하여 앱의 ID를 어설션합니다. 요청을 수신하는 앱은 이 ID를 사용하여 요청을 처리할지 여부를 결정할 수 있습니다.

    이 헤더는 URL 인출을 사용하는 경우 앱에서 전송된 요청에서만 사용할 수 있습니다. 사용자 또는 제3자가 요청에 추가하면 App Engine이 헤더를 삭제합니다.

    URL Fetch를 사용하지 않고 ID를 어설션하고 확인하는 방법에 대한 자세한 내용은 앱 ID를 OIDC ID 토큰으로 마이그레이션을 참조하세요.

    App Engine 앱 간에 요청이 전송될 때 요청 헤더를 사용해서 호출 앱의 ID를 확인하는 방법의 예시를 보려면 App Engine 간 요청 샘플을 참조하세요.

  • URL Fetch를 사용하여 모든 요청의 기본 시간 제한을 설정할 수 있습니다. requestsurllib와 같은 대부분의 Python 3 라이브러리는 기본 시간 제한을 None으로 설정하므로, 제한 시간을 지정하기 위해 코드에서 만드는 각 요청을 업데이트해야 합니다.

마이그레이션 프로세스 개요

  1. 앱에서 URL Fetch API를 사용하여 요청을 실행하는 경우 표준 Python 라이브러리를 대신 사용하도록 코드를 업데이트합니다. 각 요청에 제한 시간을 지정하는 것이 좋습니다.

  2. 로컬 개발 서버에서 아웃바운드 요청을 테스트합니다.

  3. App Engine에서 실행할 때 앱을 URL Fetch를 우회하도록 구성합니다.

  4. 배포

URL Fetch API를 Python 라이브러리로 바꾸기

  1. 아직 표준 Python 라이브러리를 사용하여 발신 요청을 실행하지 않는 경우 라이브러리를 선택하고 앱의 종속 항목에 추가합니다.

    예를 들어 요청 라이브러리를 사용하려면 app.yaml 파일과 동일한 폴더에 requirements.txt 파일을 만들고 다음 행을 추가합니다.

    requests==2.24.0
    

    Python 2와 호환성을 위해서는 requests 라이브러리를 버전 2.24.0으로 고정하는 것이 좋습니다. 앱을 배포하면 App Engine이 requirements.txt 파일에 정의된 모든 종속 항목을 다운로드합니다.

    로컬 개발의 경우 venv와 같은 가상 환경에 종속 항목을 설치하는 것이 좋습니다.

  2. google.appengine.api.urlfetch 모듈을 사용할 수 있도록 코드를 검색하고 Python 라이브러리를 사용하도록 코드를 업데이트합니다.

단순 HTTPS 요청 만들기

다음 예시에서는 requests 라이브러리를 사용하여 표준 HTTPS 요청을 실행하는 방법을 보여줍니다.

# Copyright 2020 Google LLC
#
# 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.

import logging

from flask import Flask

import requests

app = Flask(__name__)

@app.route('/')
def index():
    url = 'http://www.google.com/humans.txt'
    response = requests.get(url)
    response.raise_for_status()
    return response.text

@app.errorhandler(500)
def server_error(e):
    logging.exception('An error occurred during a request.')
    return """
    An internal error occurred: <pre>{}</pre>
    See logs for full stacktrace.
    """.format(e), 500

if __name__ == '__main__':
    # This is used when running locally.
    app.run(host='127.0.0.1', port=8080, debug=True)

비동기 HTTPS 요청 만들기

다음 예시에서는 requests 라이브러리를 사용하여 비동기 HTTPS 요청을 실행하는 방법을 보여줍니다.

# Copyright 2020 Google LLC
#
# 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.

import logging

from flask import Flask, make_response

from requests_futures.sessions import FuturesSession
from time import sleep

TIMEOUT = 10    # Wait this many seconds for background calls to finish
app = Flask(__name__)

@app.route('/')     # Fetch and return remote page asynchronously
def get_async():
    session = FuturesSession()
    url = 'http://www.google.com/humans.txt'

    rpc = session.get(url)

    # ... do other things ...

    resp = make_response(rpc.result().text)
    resp.headers['Content-type'] = 'text/plain'
    return resp

@app.route('/callback')     # Fetch and return remote pages using callback
def get_callback():
    global response_text
    global counter

    response_text = ''
    counter = 0

    def cb(resp, *args, **kwargs):
        global response_text
        global counter

        if 300 <= resp.status_code < 400:
            return  # ignore intermediate redirection responses

        counter += 1
        response_text += 'Response number {} is {} bytes from {}\n'.format(
            counter, len(resp.text), resp.url)

    session = FuturesSession()
    urls = [
        'https://google.com/',
        'https://www.google.com/humans.txt',
        'https://www.github.com',
        'https://www.travis-ci.org'
    ]

    futures = [session.get(url, hooks={'response': cb}) for url in urls]

    # No wait functionality in requests_futures, so check every second to
    # see if all callbacks are done, up to TIMEOUT seconds
    for elapsed_time in range(TIMEOUT+1):
        all_done = True
        for future in futures:
            if not future.done():
                all_done = False
                break
        if all_done:
            break
        sleep(1)

    resp = make_response(response_text)
    resp.headers['Content-type'] = 'text/plain'
    return resp

@app.errorhandler(500)
def server_error(e):
    logging.exception('An error occurred during a request.')
    return """
    An internal error occurred: <pre>{}</pre>
    See logs for full stacktrace.
    """.format(e), 500

로컬에서 테스트

아웃바운드 요청을 업데이트한 경우 로컬 개발 서버에서 앱을 실행하고 요청이 성공하는지 확인합니다.

URL Fetch 우회

App Engine에 앱을 배포할 때 URL Fetch 요청 처리를 중지하려면 다음 안내를 따르세요.

  1. app.yaml 파일에서 GAE_USE_SOCKETS_HTTPLIB 환경 변수를 원하는 값으로 설정합니다. 값은 빈 문자열을 포함하여 임의의 값일 수 있습니다. 예를 들면 다음과 같습니다.

    env_variables:
      GAE_USE_SOCKETS_HTTPLIB : ''
    
  2. URL Fetch에서 requests 라이브러리에서 보낸 요청을 처리하도록 사용 설정한 경우 앱에서 요청 AppEngineAdapter를 삭제할 수 있습니다.

    예를 들어 appengine_config.py 파일에서 requests_toolbelt.adapters.appengine을 삭제하고 Python 파일에서 requests_toolbelt.adapters.appengine.monkeypatch()를 삭제합니다.

이전 단계에서 설명한 대로 URL Fetch를 우회하더라도 앱은 여전히 URL Fetch API를 직접 사용할 수 있습니다.

앱 배포

앱을 배포할 준비가 되었으면 다음을 수행해야 합니다.

  1. App Engine에서 앱을 테스트합니다.

    Google Cloud Console의 App Engine 할당량 페이지에서 앱이 Url Fetch API를 호출하지 않는지 확인합니다.

    URL Fetch API 호출 보기

  2. 앱이 오류 없이 실행되면 트래픽 분할을 사용하여 업데이트된 앱의 트래픽을 천천히 늘립니다. 앱을 면밀히 모니터링하여 문제가 없는 것을 확인한 후 더 많은 트래픽을 업데이트된 앱으로 라우팅합니다.