FlaskアプリケーションをGoogle App Engineにデプロイし、Cloud SQLに接続する方法

この記事では、Flaskを使用したウェブアプリケーションをGoogle App Engineにデプロイし、Cloud SQL（PostgreSQL）データベースに接続する手順を詳しく説明します。ウェブアプリの詳細については、別の記事で解説する予定です。

GCP（Google Cloud Platform）は初回でアカウントを作成すると、90日間で300ドル分貰えるのでおすすめです。また他にも、無料枠が多いのも特徴です。詳細は[こちら](https://cloud.google.com/free/docs/free-cloud-features?hl=ja#free-tier-usage-limits)

[こちら](https://janken-game-438508.dt.r.appspot.com/)が今回作成した簡単なwebアプリになります。UI,UXがこだわりきれてないのですが、カメラを起動して、ユーザーの手を認識して、ユーザーがコンピュータの指示に従うとスコアが増えていくようになっています。ぜひ、遊んで、スコアをリーダーボードにあげてみてください。

## 前提条件
以下は他の記事でも詳しく解説されているので、そちらをみてください。
・Google Cloud Platform（GCP）アカウントを持っていること
・**Cloud SDK**がインストールされていること
・Python 3.9 以上がインストールされていること

## ステップ1: プロジェクトのセットアップ

### 1.1 プロジェクトディレクトリの作成

まず、新しいプロジェクトディレクトリを作成してここにつくってWEBアプリのコードをかいていきます。

```
mkdir janken-game
cd janken-game
```

### 1.2 必要なパッケージのインストール

requirements.txt ファイルを作成し、以下の内容を追加します。
以下は例です。
```
Flask==2.3.3
SQLAlchemy==1.4.36
gunicorn==20.1.0
cloud-sql-python-connector[pg8000]==1.2.2
```

## ステップ2: Flaskアプリケーションの作成

**ウェブアプリの詳細な実装については、別の記事で解説する予定です。**

### 2.1 アプリケーションコードの作成

app.py ファイルを作成し、以下のコードを追加します。

```python
import os
from flask import Flask, render_template, request, jsonify, redirect, url_for
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base, LeaderboardEntrySingle, LeaderboardEntryBoth

# Cloud SQL Python Connector のインポート
from google.cloud.sql.connector import Connector
import pg8000

app = Flask(__name__)

# 環境変数からデータベース接続情報を取得
db_user = os.environ.get("DB_USER")
db_password = os.environ.get("DB_PASS")
db_name = os.environ.get("DB_NAME")
cloud_sql_connection_name = os.environ.get("CLOUD_SQL_CONNECTION_NAME")  # app.yaml で設定

# Cloud SQL Python Connector を初期化
connector = Connector()

def getconn():
    conn = connector.connect(
        cloud_sql_connection_name,
        "pg8000",
        user=db_user,
        password=db_password,
        db=db_name,
    )
    return conn

# SQLAlchemy エンジンを作成
engine = create_engine(
    "postgresql+pg8000://",
    creator=getconn,
)

Base.metadata.create_all(engine)
DBSession = sessionmaker(bind=engine)
session = DBSession()


# ルートエンドポイント
@app.route('/')
def index():
    return render_template('index.html')

# 片手モードのゲーム開始エンドポイント
@app.route('/start_game_single_hand')
def start_game_single_hand():
    return render_template('game_single_hand.html')

# 両手モードのゲーム開始エンドポイント
@app.route('/start_game_both_hands')
def start_game_both_hands():
    return render_template('game_both_hands.html')

# スコアの受け取りエンドポイント（片手モード）
@app.route('/submit_score_single', methods=['POST'])
def submit_score_single():
    data = request.get_json()
    username = data.get('username')
    score = data.get('score')

    if username and score is not None:
        entry = LeaderboardEntrySingle(username=username, score=score)
        session.add(entry)
        session.commit()
        return jsonify({'status': 'success'})
    else:
        return jsonify({'status': 'error'}), 400

# リーダーボード表示エンドポイント（片手モード）
@app.route('/leaderboard_single')
def leaderboard_single_hand():
    leaderboard = session.query(LeaderboardEntrySingle).order_by(LeaderboardEntrySingle.score.desc()).all()
    return render_template('leaderboard_single.html', leaderboard=leaderboard)

# スコアの受け取りエンドポイント（両手モード）
@app.route('/submit_score_both', methods=['POST'])
def submit_score_both():
    data = request.get_json()
    username = data.get('username')
    score = data.get('score')

    if username and score is not None:
        entry = LeaderboardEntryBoth(username=username, score=score)
        session.add(entry)
        session.commit()
        return jsonify({'status': 'success'})
    else:
        return jsonify({'status': 'error'}), 400

# リーダーボード表示エンドポイント（両手モード）
@app.route('/leaderboard_both')
def leaderboard_both_hand():
    leaderboard = session.query(LeaderboardEntryBoth).order_by(LeaderboardEntryBoth.score.desc()).all()
    return render_template('leaderboard_both.html', leaderboard=leaderboard)

if __name__ == '__main__':
    app.run(debug=True)
```


### 2.2 モデルの定義

models.py ファイルを作成し、データベースのモデルを定義します。
(以下は例です）

```python# models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class LeaderboardEntrySingle(Base):
    __tablename__ = 'leaderboard_single'
    id = Column(Integer, primary_key=True)
    username = Column(String)
    score = Column(Integer)

class LeaderboardEntryBoth(Base):
    __tablename__ = 'leaderboard_both'
    id = Column(Integer, primary_key=True)
    username = Column(String)
    score = Column(Integer)
```

## ステップ3: Cloud SQL（PostgreSQL）インスタンスの作成


## 3.1 インスタンスの作成
1. Google Cloud Console にアクセスし、左側のメニューから「SQL」を選択します。
2. インスタンスを作成」をクリックし、「PostgreSQL」を選択します。
3. インスタンスID、パスワード、リージョン（例: asia-northeast2）を設定します。
4. 「作成」をクリックしてインスタンスを作成します。


### 3.2 データベースとユーザーの作成

1. インスタンスが作成されたら、その詳細ページに移動します。
2. 「データベース」タブで、新しいデータベース（例: janken_db）を作成します。
3. 「ユーザー」タブで、新しいユーザー（例: flask_user）を作成し、パスワードを設定します。

### 3.3 接続情報の取得

1. インスタンス接続名（例: your-project-id:region:instance-id）をメモします。
2. Private IP を使用する場合は、「接続」タブでPrivate IPを有効にします。

## ステップ4: app.yaml の作成

App Engineの設定ファイル app.yaml を作成し、以下の内容を追加します。

```yaml
runtime: python39
entrypoint: gunicorn -b :$PORT app:app

env_variables:
  DB_USER: "flask_user"  # データベースユーザー名
  DB_PASS: "your-password"  # データベースパスワード
  DB_NAME: "janken_db"  # データベース名
  CLOUD_SQL_CONNECTION_NAME: "your-project-id:region:instance-id"  # インスタンス接続名

vpc_access_connector:
  name: "projects/your-project-id/locations/asia-northeast2/connectors/serverless-connector"

automatic_scaling:
  target_cpu_utilization: 0.65
  max_instances: 2

handlers:
- url: /static
  static_dir: static

- url: /.*
  script: auto
```

::: note warn
注意: your-password や your-project-id:region:instance-id などは、あなたの環境に合わせて置き換えてください。
:::

## ステップ5: サービスアカウントの権限設定

### 5.1 Cloud SQL Admin APIの有効化
1. Google Cloud Console で左上のメニューから「APIとサービス」 > 「ライブラリ」を選択します。
2. 「Cloud SQL Admin API」を検索し、選択します。
3. 「有効にする」をクリックします。

### 5.2 サービスアカウントに役割を付与
1. 左側のメニューから「IAMと管理」 > 「IAM」を選択します。
2. サービスアカウント your-project-id@appspot.gserviceaccount.com を探します。
3. 「編集」をクリックし、「+役割を追加」を選択します。
4. 「Cloud SQL クライアント」の役割を選択し、追加します。
5. 「保存」をクリックします。

### ステップ6: アプリケーションのデプロイ

自分の作成したフォルダで、以下のコマンドを実行して、アプリケーションをデプロイします。

```
gcloud app deploy
```

デプロイが成功したら、次のコマンドでアプリケーションをブラウザで開きます。

```
gcloud app browse
```

## ステップ7: トラブルシューティング

### 7.1 ログの確認

問題が発生した場合は、以下のコマンドでログを確認できます。

```
gcloud app logs tail -s default
```

### 7.2 よくある問題と解決策

・**権限エラー**: サービスアカウントに必要な役割（「Cloud SQL クライアント」など）が付与されていることを確認します。

・**APIが有効化されていない**: 「Cloud SQL Admin API」が有効になっていることを確認します。

・**接続エラー**: CLOUD_SQL_CONNECTION_NAME が正しく設定されていることを確認します。