Pythonスクリプトをresponderを使ってさっとCloud Runで実行できるようにする

## サマリ

Cloud RunがGAされましたね。今までローカルで実行していたちょっと処理時間がかかるpythonスクリプトをお試しでCloud Runで実行したくなりました。
Cloud Runで実行するコンテナーはHTTPでリクエストを受けて実行できる必要があります。また、Cloud Runではリクエスト タイムアウトのことも考慮しないといけません。
リクエスト タイムアウトのことを考慮せずにとりあえずさっとスクリプトを動かすのにresponderが簡単だったので紹介します。

2020/09/05追記
[公式のホームページ](https://cloud.google.com/run/docs/tips?hl=ja#avoiding_background_activities
)でこのやり方について注意がありました。

> Cloud Run で実行中のアプリケーションがリクエストの処理を終了すると、コンテナ インスタンスの CPU へのアクセスが無効になるか、厳しく制限されます。したがって、リクエスト ハンドラの範囲外で実行されるバックグラウンド スレッドやルーティンを開始しないでください。

> バックグラウンド アクティビティは、HTTP レスポンスの送信後に発生します。コードを確認し、レスポンスの送信前にすべての非同期処理が完了するようにしてください。

この記事で紹介しているやり方を実施するときは上記のことを踏まえて実施してください。
@ChainSmokers さんコメントありがとうございました。

## 前提の話

### 試した環境

* OS: Mac OS 10.14.6
* docker desktop community: 2.1.0.5
* docker image: [python:3.7.5-buster](https://hub.docker.com/_/python/)

### [responder](https://responder.readthedocs.io/en/latest/index.html)

pythonのHTTPサービスフレームワークです。ASGI（Asynchronous Server Gateway Interface）を実装したものなので、Asynchronousと名前がついているとおり非同期処理もできます。
非同期処理自体は他のフレームワークでもできます。しかし、pythonのフレームワークで有名なFlaskやDjangoだとざっと調べたところパッケージを追加でインストールしたり、設定ファイルを変えたりする必要がありました。responderのサンプルコードを見るとインストールも実装も楽そうだったので使ってみました。

## やったこと

### ディレクトリ構成と各ファイルについて

```bash
vaivailx@MacBook-Pro-2 responder_sample_for_cloudrun % tree -L 3 .
.
├── Dockerfile
├── requirements.txt
└── src
    ├── sample.py
    └── webapp.py
```

pythonの各ファイルは以下の処理をします。

* sample.py: 実行したいメインの処理
* webapp.py: HTTPリクエストを受け付ける処理

requirements.txtはこれだけ。

```text
responder==2.0.4
```

Dockerfileではaptでパッケージの更新とrequirements.txtに書かれているパッケージをインストール。

```dockerfile
FROM python:3.7.5-buster

ENV APP_HOME /app
WORKDIR $APP_HOME
ADD ./requirements.txt /app
ADD ./src /app

# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
    && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
    # Install pylint
    && pip --disable-pip-version-check --no-cache-dir install pylint \
    # Update Python environment based on requirements.txt
    && pip --disable-pip-version-check --no-cache-dir install -r requirements.txt \
    && rm -rf requirements.txt \
    # Clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*

# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=

# run app
ENV PORT '8080'
CMD python3 webapp.py
EXPOSE 8080
```

responderを使ってHTTPリクエストを受け付ける処理。
`def`の前に`async`とつけるだけでその関数は非同期処理になります。

webapp.py

```python
import logging

import responder

from sample import process_sample

logging.basicConfig(filename=f'/var/log/webapp.log', level=logging.DEBUG)

api = responder.API()

@api.route("/async")
async def asyncsample(req, resp):

    @api.background.task
    def process_param(params):
        t = int(params.get('time', 10))
        process_sample(t)

    process_param(req.params)
    resp.media = {'async success': True}

@api.route("/sync")
def syncsample(req, resp):

    t = int(req.params.get('time', 10))
    process_sample(t)
    resp.media = {'sync success': True}

if __name__ == '__main__':
    api.run()

```

ログ出力をしているのは、Croud Runで実行時に確認できるためです。
ルート名通り、ルートパス/asyncにアクセスすると非同期処理が実行され、ルートパス/syncにアクセスすると動機処理が実行されます。
今回はクエリパラメーターの値分スリープをするようにしました。

sample.py

```python
import time
import logging

def process_sample(secs):
    logging.debug('process starts.')
    time.sleep(secs)
    logging.debug('process ends.')
```

### Cloud Runでの実行結果確認

では、Cloud Runのリクエスト タイムアウトを30秒に設定して実行してみます。（5分待つのが面倒なためデフォルト値から変更しています。）

**同期実行：実行時間10秒**

```bash
vaivailx@cloudshell:~ (cloudrun-test-259804)$ date;curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://responderasync-jpwo3ltkhq-an.a.run.app/sync?time=10
Fri Dec 13 09:52:59 +09 2019
{"sync success": true}vaivailx@cloudshell:~ (cloudrun-test-259804)$
```

<img width="891" alt="sync10_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18485/093952ab-c797-3037-afb9-628ee4b854b2.png">


→正常に終了

**同期実行：実行時間40秒**

```bash
vaivailx@cloudshell:~ (cloudrun-test-259804)$ date;curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://responderasync-jpwo3ltkhq-an.a.run.
app/sync?time=40
Fri Dec 13 09:55:45 +09 2019
upstream request timeoutvaivailx@cloudshell:~ (cloudrun-test-259804)$
```

<img width="905" alt="sync40_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18485/147343d6-a7da-32a3-934a-b6781b9c2ad0.png">

→タイムアウト

**非同期実行：実行時間10秒**

```bash
vaivailx@cloudshell:~ (cloudrun-test-259804)$ date;curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://responderasync-jpwo3ltkhq-an.a.run.app/async?time=10
Fri Dec 13 10:02:31 +09 2019
{"async success": true}vaivailx@cloudshell:~ (cloudrun-test-259804)$
```

<img width="974" alt="async10_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18485/91efe18d-2ea4-3657-09f4-ec37986537b6.png">

→
正常に終了

**非同期実行：実行時間40秒**

```bash
vaivailx@cloudshell:~ (cloudrun-test-259804)$ date;curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://responderasync-jpwo3ltkhq-an.a.run.app/async?time=40
Fri Dec 13 10:03:57 +09 2019
{"async success": true}vaivailx@cloudshell:~ (cloudrun-test-259804)$
```

<img width="916" alt="async40_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18485/d9b4a55c-e0ee-7484-5f4f-d345b5650c61.png">

→正常に終了

### ついでに確認したこと

ちなみにHTTPリクエスト タイムアウトは最大900秒と設定画面にありましたが、これは実行時間ではなく
あくまでリクエストを受け付けてレスポンスを返すまでの時間で、非同期にしておけば900秒以上できることは確認できました。
以下非同期で1200秒実行した結果です。

<img width="927" alt="async1200_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18485/27e39030-7648-e245-866e-e4e9db473120.png">

## 最後に

* responderを使うと非同期処理を簡単に実装できるのを紹介しました。
* 処理時間がかかるバッチのpythonスクリプトをCloud Runで動かしてみたいときに使うのがありかなと思います。
* Cloud Runは、[料金ページ](https://cloud.google.com/run/pricing?hl=ja)の通り、HTTPのレスポンスタイムではなく処理時間に応じて課金されます。非同期にしても処理を実行した時間はその分課金対象時間されるので注意してください。