GCP上に無料でWEBバックエンドサーバーを作る【永久無料枠only】

# 概要
この記事では、GCPの永久無料枠を最大限利用してWEBバックエンドサーバーを作る方法を解説します。

### 目標
jsonを入れるとjsonが帰ってくる、簡単なAPIサーバーを作ります。

# はじめに
まずはGCPの永久無料枠を見てみましょう。

<img width="1121" alt="スクリーンショット 2019-08-16 23.23.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163480/e48dc3d0-53aa-aa6c-96a6-5244728a935c.png">
<img width="1120" alt="スクリーンショット 2019-08-16 23.24.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163480/97d33b7e-a40e-2189-da7d-2b5819e4b434.png">
<img width="1128" alt="スクリーンショット 2019-08-16 23.24.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163480/e8378b15-d8d5-fe32-9e35-a84185c4a213.png">

Googleさんは太っ腹なので、かなりのサービスが無料で使えます。
その他約3万円の無料枠がありますが、永久無料ではないので、使いすぎには注意が必要です。

今回は完全無料枠で使える、Google Compute Engine と Google Cloud Firestore, Google Cloud Storage を使ってみます。

# Hello, World!を出力(GCE)
まずは定番、Hello, Worldの出力です。pythonのWEB用フレームワークであるFlaskを使います。

### インスタンスの構築
Compute Engine>VMインスタンス　から、新しいインスタンスを作成します。マシンタイプはf1-microで、ゾーンはus-から始まる場所にしましょう。ファイアウォールは、「HTTPトラフィックを許可する」を選択してください。

### 接続
インスタンスが起動できたら、SSHと書いてあるボタンを押して接続してください。別窓でシェルが確認できると思います。自分のアカウント名のフォルダが、ホームディレクトリです。

### セットアップ
ファイル構成は以下の通りです。

```
<ホームディレクトリ>/
　├ app/
　│　└ app.py
　├ setup.sh
　└ requirements.txt
```

メインの実行ファイルです。```host='0.0.0.0', port=80```となっている理由はhttpプロトコルのデフォルトのポート番号が80番で、外部に公開するアドレスを示すのが```0.0.0.0```だからです。

```python:app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return "Hello world!!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

```

セットアップのコマンドです。毎回実行するのはめんどくさいのでシェルスクリプトにすると楽です。pipを使って、必要なライブラリを入れています。最後のnohupは、ssh接続が切れてもサーバーがきちんと動くようにするコマンドです。

```shell:setup.sh
sudo apt install python3-pip
sudo apt-get update
sudo python3 -m pip uninstall pip && sudo apt install python3-pip --reinstall
sudo pip3 install -r requirements.txt
sudo nohup python3 app/app.py < /dev/null &
```

必要なライブラリは、```requirements.txt```に記入すると勝手に入れてくれます。

```requirements.txt
Flask
google-cloud-core
```

では早速実行しましょう。

```
sh setup.sh
```

何も出てこないですが、裏できちんと動いています。GCPコンソールに戻り、VMインスタンスから、「外部IP」をクリックしてみましょう。きちんとHello, World!が表示されていると思います。

ログの確認は、

```sh
$ tail nohup.out 
```
でできます。最新のログを10行分出力します。10行では足りない場合は```-n```オプションで行数を変更する事が出来ます。

```sh
$ tail -n 20 nohup.out 
```

次にプロセスを確認してみましょう。psコマンドでは、実行中のプロセスを見ることができます。grepコマンドを使って、```app.py```が含まれるプロセスを表示させています。

```
$ ps aux | grep app.py
root     13820  0.0  0.5  44828  3576 ?        S    13:37   0:00 sudo nohup python3 app/app.py
root     13821  0.0  8.8 569784 53904 ?        Sl   13:37   0:02 python3 app/app.py
```

終了させたいときは、以下のコマンドを実行します。PIDは、プロセスを表示させた時の左から二番目の数字です。上と下のどちらかを切ればOKです。

```
sudo kill <PID>
```

# ユーザー認証（GCE, Firestore)

続いて、ユーザー認証ができるようにしてみましょう。
Firestoreに保存したユーザー情報を利用してログインを実装します。

### FireStoreとは
NoSQLデータベースです。RDBと違って、使い方が独特なのですが、詳しい説明は今回は省略します。

### サービスアカウントの権限設定
まずは、FireStore APIを有効化してください。その後、IAMと管理>IAM から、Compute Engine Service Accountに対して権限を与えます。FireStoreへの読み書きを許可してください。

続いてapp.pyに追記します。

```python:app.py
import logging

from flask import Flask, request, json, jsonify
from flask_httpauth import HTTPBasicAuth
from flask_bcrypt import Bcrypt
from flask_cors import CORS
from google.cloud import firestore

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False

# パスワードをハッシュ化します。データベースにはハッシュ化されたパスワードが保存されます。
bcrypt = Bcrypt(app)

auth = HTTPBasicAuth()

# 全てのドメインからのリクエストを許可しています。
CORS(app)

# Project ID is determined by the GCLOUD_PROJECT environment variable
db = firestore.Client()

@app.route('/')
def index():
    return "Hello world!!"

@app.route('/add-user', methods=['POST'])
def add_user():
    user_id = request.json["user_id"]
    user_name = request.json["user_name"]
    password = request.json["password"]
    pw_hash = bcrypt.generate_password_hash(password)

    doc_ref = db.collection('users').document(user_id)
    doc_ref.set({
        'user_id': user_id,
        'user_name': user_name,
        'password': pw_hash
    })

    return jsonify({"result": "OK"})


@auth.verify_password
def verify_password(user_id, password):
    doc_ref = db.collection('users').document(user_id)

    try:
        doc = doc_ref.get()
        doc_dict = doc.to_dict()
        print(doc.to_dict())
        if bcrypt.check_password_hash(doc_dict["password"], password):
            print('Login Succeeded!!')
            return True
        else:
            return False
    except:
        return False


@app.route('/authtest')
@auth.login_required
def authtest():
    return "Login Succeeded!!"

@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__':
    app.run(host='0.0.0.0', port=80)

```

```txt:requirements.txt
Flask
flask_httpauth
flask-bcrypt
flask_cors
google-cloud-firestore
google-cloud-core
```

### 解説

今回は、最もシンプルなBasic認証を使って実装しました。パスワードをそのまま保存するのは、万が一、データベースの中身が流出した時にパスワードがバレてしまうので好ましくありません。そこで、Bcryptを使ってパスワードをハッシュ化して暗号化した状態で保存します。

### ユーザー登録
/add-userに対して情報をPOSTします。
テストするときは、Chrome Appの「[Advanced REST client](https://install.advancedrestclient.com/install)」が便利です。

```json
{
    "user_id": "user@test.com",
    "user_name": "Test User",
    "password": "password"
}
```
このようにjsonリクエストをPOSTします。FireStoreから見ると登録できているはずです。


### 認証

/authtestにアクセスしてみましょう。ログイン画面が表示されるはずです。
```@auth.login_required``` アノテーションをつけたページはログインが必要になります。

# 画像のアップロード

WEBサービスで画像などの重いデータを保存したくなることもあると思います。GCPなら容量に制限はありますが、無料でデータを保存できます。

```python:app.py
@app.route('/uploader')
def uploader():
    return """
<form method="POST" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="submit">
</form>
"""


@app.route('/upload', methods=['POST'])
def upload():
    """Process the uploaded file and upload it to Google Cloud Storage."""
    uploaded_file = request.files.get('file')

    if not uploaded_file:
        return 'No file uploaded.', 400

    # Create a Cloud Storage client.
    gcs = storage.Client()

    # Get the bucket that the file will be uploaded to.
    bucket = gcs.get_bucket(<バケット名>)

    # Extract extension
    ext = uploaded_file.filename.split(".")[1]

    # Name the image
    image_id = str(uuid.uuid4())

    # Create a new blob and upload the file's content.
    blob = bucket.blob(image_id + '.' + ext)

    blob.upload_from_string(
        uploaded_file.read(),
        content_type=uploaded_file.content_type
    )

    # The public URL can be used to directly access the uploaded file via HTTP.
    return blob.public_url
```

# おまけ
AWSでは、EC2(GCPでいうCompute Engine)が有料なので、永久無料で使う事は出来ません。ただ、NoSQLのDynamoDB、ストレージのS3は永久無料枠があるので、EC2の代わりにLambdaを使えば似た事が出来ます。