Google Cloud Functions (Python3) を使ってみた

Google Cloud Functions で Python3 が使えるということだったので使ってみました。実際のプロダクトで使うことも考えて、環境変数の扱い、ローカルでの開発についても説明しています。

----

なお、この記事はタスク管理にGitHubを使う一連の記事の中の一つです。

- [情シスのタスクをGitHubのissueで管理するようにしたら捗った](https://qiita.com/itkr/items/1ae5578c1ca761dacf88)
- [プロジェクト管理ツールとしてGitHubを"普通に"使う](https://qiita.com/itkr/items/0d4c0015da28827b2bb7)
- **Google Cloud Functions (Python) を使ってみた** ←ここ
- [GitHub App を作ってPythonからAPIを叩いてみた](https://qiita.com/itkr/items/db5ed965bf8cac1cdd4e)
- [Google Apps Script から Google Cloud Functions のHTTPトリガーを実行してみた](https://qiita.com/itkr/items/6a515c5cf8c9a18f1880) 

## 前提

以下のことが完了している前提で説明します。

- [Google Cloud Platform のアカウント作成](https://console.cloud.google.com/getting-started?hl=ja)
- [gcloudコマンドのインストール](https://cloud.google.com/sdk/downloads?hl=JA)
    - ログイン ( `gcloud auth login` )

## クイックスタート

まずは簡単に関数を作ってみます。

### 関数の作成

https://console.cloud.google.com/functions

<img width="832" alt="スクリーンショット 2019-07-31 21.31.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14918/d07f2dce-bd1d-707a-eb6c-f49f5101d5f0.png">

### 設定

今回指定した設定は以下の表のとおりです。

| 項目         |                     |
|--------------|---------------------|
| メモリ       | 256 MB              |
| トリガー     | HTTP                |
| ランタイム   | Python 3.7          |
| ソースコード | インライン エディタ |
| 実行する関数 | hello_world         |

![スクリーンショット-2019-07-31-21.32.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14918/8564fab7-30c3-bbed-6a4c-cfd0d09ed4b5.png)

### 確認

Google Cloud Platform のコンソール画面からも確認できますが、 gcloud でログインしていればローカルからのコマンドでも確認できます。

```sh
$ gcloud functions list
```

結果

```txt
NAME        STATUS  TRIGGER       REGION
function-1  ACTIVE  HTTP Trigger  us-central1
```

### 実行

実行のためのトリガーは以下のように複数の選択肢があります。

- HTTP
- Cloud Storage
- Cloud Pub/Sub
- Firebase

今回はHTTPのトリガーを作成したので、ブラウザや `curl` コマンドで直接 `GET` リクエストを送って確認できます。URLはコンソール画面でに表示されています。

![スクリーンショット-2019-07-31-21.41.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14918/a4b07fa0-5d00-f416-ce80-81105360dd48.png)

![スクリーンショット-2019-07-31-21.41.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14918/fd440f93-2ea5-0674-3264-550923b17a8f.png)

## Python3 関連ファイル

Python3で Cloud Functions を作成すると、２つのファイルができています。

- main.py
- requirements.txt

実行したい関数は main.py の中に適当な名前で実装し、「実行する関数」でその名前を指定すると実行できます。サードパーティ製の必要なライブラリがあったら requirements.txt に書いておけばデプロイしたときに自動的にインストールしてくれます。また、Pythonファイルは一つだけではなくPythonパッケージを作成して main.py から `import` することもできます。

![スクリーンショット-2019-08-02-21.32.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14918/177315d4-e45a-2f11-2aca-9013da3d00e8.png)

## ローカルからのデプロイ

できるならコードを毎回ブラウザ上のエディタではなくローカルで開発してGit管理するほうが望ましいと思われます。Cloud Source Repositories を使うと Google Cloud Platform 上での管理もできますが、ここではGitHubを想定します。

### gcloudコマンドによるデプロイ

とりあえずコピーしてくるなり何なりして同様の環境をローカルに作り、ローカルからデプロイコマンドを実行すればデプロイできます。

```sh
$ gcloud functions deploy function-1 --trigger-http --runtime=python37
```

少し変更を加えて先程のエンドポイントにアクセスするとローカルからのデプロイができていることがわかると思います。

### 不要なファイルをアップロードしないようにする

ローカルに適当なファイルを置いておけばコマンド一つでデプロイしてくれるのですが、何もしなければGit管理用のファイルなど、関係ないファイルまでアップロードしてしまいます。

そこで .gcloudignore というファイルを作成しておきます。.gitignore のように、ここにアップロードしたくないファイルやディレクトリを記載しておけば、デプロイの際にアップロードされることがありません。

なお、 .gcloudignore から他のファイルを参照することもできるので、 .gitignore を参照すると良いかもしれません。

```.gcloudignore
.gcloudignore
.gitignore
Makefile
README.md
requirements_local.txt
test/
venv/

#!include:.gitignore
```

## ローカルでの開発

簡単にデプロイして確認ができる Cloud Functions ですが、毎回アップロードしなければ確認できないのは不便です。そのためにローカルでエミュレートする仕組みを作ります。

### 仮想環境の作成 (venv)

必須ではありませんが、必要に応じて仮想環境を用意しておくと便利です。

```sh
$ python3 venv venv
$ source venv/bin/activate
```

### Flaskによるエミュレート

Google Cloud Function で始めに実行される関数は、Flaskの `flask.Request` オブジェクトを受け取ります。ローカルで開発する際にはlocalhostにFlaskの開発用サーバーが立ち上がるようにすることでエミュレートできます。

#### ライブラリのインストール

```sh
$ pip install flask
```

#### 実装

```main.py
from flask import Flask, request


def hello_world(request):
    # ...
    return 'HelloWorld!'


if __name__ == "__main__":
    app = Flask(__name__)

    @app.route('/')
    def index():
        return hello_world(request)

    app.run('127.0.0.1', 8000, debug=True)
```

このように、 main.py が直接呼ばれた場合は開発用サーバーが立ち上がり、 localhost:8000 にアクセスすると `hello_world` が実行されます。

## 環境変数

作成するアプリケーションによっては、他のサービスのAPIキーなど、ソースコードに書きたくない情報を含んでいる場合があります。そういった場合には環境変数を設定することができます。

### 設定方法

#### コマンドの引数を使う方法

一つの方法は、デプロイする際にコマンドの引数で指定する方法です。

```sh
$ gcloud functions deploy function-1 --trigger-http --runtime=python37 --set-env-vars FOO=bar,BAZ=boo
```

![スクリーンショット-2019-08-03-13.31.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14918/884a6953-80fc-935e-f1f5-fac190a81176.png)


#### Yamlを使う方法

```env.yaml
FOO: bar
BAZ: boo
```

```sh
$ gcloud functions deploy function-1 --trigger-http --runtime=python37 --env-vars-file=./env.yaml
```

### 読み込み

Google Cloud Function に設定した環境変数を使う方法は自分でサーバーを立ち上げたときなど、他の環境と同じです。

```python
import os


FOO = os.getenv('FOO')  # 'bar'
BAZ = os.getenv('BAZ')  # 'boo'
```

### Yamlを使うときの運用方法

コマンドの引数を使う方法とYamlを使う方法がありますが、どちらかといえばファイルで管理できるYamlを使うほうが好みです。しかし、Git管理はしたくないので、そのために少し工夫を加えます。

#### ローカルでの確認

Yamlで環境変数を設定する場合は、ローカル環境でもそのYamlを読み込むようにすると便利です。そこで以下のようにYamlから環境変数を設定するように手を加えます。

##### ライブラリのインストール

```sh
$ pip install PyYAML
```

※ ImportError にならないようにするために requirements.txt にもPyYAMLを書き加えておきます。

##### 実装

```python
import os
import yaml


try:
    # local
    with open('./env.yaml') as f:
        os.environ.update(yaml.load(f))
except FileNotFoundError as e:
    # Google Cloud Functions
    pass


FOO = os.getenv('FOO')  # 'bar'
BAZ = os.getenv('BAZ')  # 'boo'
```

#### Google Cloud Storage を使う

Git管理をしたくないので、適当な Google Cloud Storage のバケットを作って、そこに env.yaml を保存しておき、デプロイする際にコピーしてくるようにします。 .gitignore に env.yaml を書き加えるのも忘れずに。

##### アップロード

```sh
$ gsutil cp ./env.yaml gs://[bucket_name]/env.yaml
```

##### ダウンロード

```sh
$ gsutil cp gs://[bucket_name]/env.yaml ./env.yaml
```

ちなみに `gsutil` コマンドは Google Cloud SDK をインストールしたときにインストールされているかと思います。

## 環境変数を使わない運用

公開したくない情報をGit管理しないために環境変数を使う方法もありますが、 Google Cloud Storage を直接 Google Cloud Function から読み込んで環境変数を使わない方法もあります。

### IAMの設定

Google Cloud Functions から Cloud Storage の情報を取得するために、必要に応じてサービスアカウントに対して権限を付与します。ちなみにサービスアカウントとは Google Cloud Platform の各サービスが、疑似ユーザーのように振る舞う仕組みです。

#### Google Cloud Functions のサービスアカウント

サービスアカウントはコンソール画面から確認できます。

![スクリーンショット-2019-08-03-14.58.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14918/ce842f9b-82ba-54e1-a5bc-1435fe6b182e.png)

#### サービスアカウントに権限を付与

おそらくデフォルトのままでも大丈夫ですが、権限が足りない場合は「[IAM と管理](https://console.cloud.google.com/iam-admin/iam)」から対象のサービスアカウントを編集して↓の権限を付与します。

```
ストレージ→ストレージ オブジェクトの閲覧者
```

※ なお、これは「バケットレベルで権限を一様に設定（バケットポリシーのみ）」のアクセス制御モデルで作成したバケットを使用しています。

### ライブラリのインストール

ここでは `google-cloud-storage` を利用してみます。

```sh
$ pip install google-cloud-storage
```

### 実装

`google-cloud-storage` を利用したときの実装例です。

```python
import os
import yaml

from google.cloud import storage


def get_data():
    client = storage.Client()
    bucket = client.get_bucket('[bucket_name]')
    blob = storage.Blob('env.yaml', bucket)
    return yaml.load(blob.download_as_string())


os.environ.update(get_data())

FOO = os.getenv('FOO')  # 'bar'
BAZ = os.getenv('BAZ')  # 'boo'
 ```

### ローカルでの開発

なお、ローカルで開発する場合は↓のようにログインする必要があります。

```sh
$ gcloud auth application-default login
```

## サンプルコード

https://github.com/itkr/my-gcf-tutorial