Cloud Runに環境変数を設定しながらデプロイする

## はじめに
Google Cloudの[Cloud Run](https://cloud.google.com/run/docs/overview/what-is-cloud-run?hl=ja)では、開発したアプリケーションをgcloudコマンドで簡単にデプロイすることができます。
今回はDockerfileを作成せず、[ソースコードから直接デプロイする](https://cloud.google.com/run/docs/deploying-source-code?hl=ja)際に環境変数を設定しながらデプロイする方法を紹介します。アプリケーションはPythonのフレームワーク（Django,Flask,FastAPI）で試してみたいと思います。

## 前提
Google Cloud,gcloud CLIの準備として以下の「始める前に」を実行済みとします。

https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-python-service?hl=ja#before-you-begin

Pythonのバージョンは全て3.11.9で試しています。

また、環境変数を設定してデプロイする際、.envファイルから環境変数を取得してデプロイに使用する前提としています。その際に使用できるシェルスクリプトを作成しました。[gcloud run deploy](https://cloud.google.com/run/docs/deploying-source-code?hl=ja#deploying)コマンドを使用しています。
全環境共通で、以下のファイル（deploy.sh）を使用します。今回はPythonで試していますが、それ以外でも以下のファイルは使い回せると思います。
```sh:deploy.sh
#!/bin/bash

# アプリケーション名とリージョン、その他パラメータを設定
APP_NAME="sample-app-name" # 任意のアプリケーション名を設定してください
REGION="asia-northeast1" # 今回は東京リージョンに設定します
MEMORY="512Mi"
CPU="1"
MIN_INSTANCES="0"
MAX_INSTANCES="1"

# 環境変数をCloud Run用のフォーマットに変換する関数
format_env_vars() {
    local result=""
    while IFS='=' read -r key value; do
        # コメントや空行をスキップ
        [[ $key =~ ^#.*$ ]] && continue
        [[ -z $key ]] && continue
        
        # 値をそのまま使用
        result+="$key=$value,"
    done < .env
    echo "${result%,}" # 最後のカンマを削除
}

# 環境変数を整形
ENV_VARS=$(format_env_vars)

# デプロイコマンドを構築
DEPLOY_CMD="gcloud run deploy $APP_NAME \
  --source . \
  --region $REGION \
  --set-env-vars $ENV_VARS \
  --memory $MEMORY \
  --cpu $CPU \
  --min-instances $MIN_INSTANCES \
  --max-instances $MAX_INSTANCES \
  --allow-unauthenticated" # 今回は認証なしでアクセスできる前提とします

# コマンドを表示（デバッグ用）
echo "実行するコマンド:"
echo "$DEPLOY_CMD"

# デプロイを実行
eval "$DEPLOY_CMD"

echo "デプロイが完了しました。"
```

## Djangoのサンプル
### ライブラリ・ディレクトリ構成
まずはDjangoから紹介します。ライブラリは以下を使用します。
```text:requirements.txt
Django==5.1.1
gunicorn==23.0.0
```

ディレクトリ構成は以下の通りです。
```sh
.
├── Procfile
├── deploy.sh # デプロイ時に使用するシェルスクリプトです
├── manage.py # デフォルトのまま
├── requirements.txt
├── sample_app
│   ├── admin.py # デフォルトのまま
│   ├── apps.py # デフォルトのまま
│   ├── migrations # デフォルトのまま
│   ├── models.py # デフォルトのまま
│   ├── templates
│   │   └── index.html
│   ├── tests.py # デフォルトのまま
│   └── views.py
└── sample_django_cloud_run
    ├── asgi.py # デフォルトのまま
    ├── settings.py
    ├── urls.py
    └── wsgi.py # デフォルトのまま
```

### サンプルコード
今回は環境変数が読み込めるかの確認のため、html上で表示してみます。
```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>django-sample</title>
</head>

<body>
    <h1>Hello Django</h1>
    <p>TEST_VARIABLE1 の値: {{ test_var1 }}</p>
    <p>TEST_VARIABLE2 の値: {{ test_var2 }}</p>
</body>

</html>
```
```py:views.py
import os
from django.shortcuts import render

# Create your views here.
def index(request):
    test_var1 = os.environ.get('TEST_VARIABLE1', 'Not set')
    test_var2 = os.environ.get('TEST_VARIABLE2', 'Not set')
    context = {
        'test_var1': test_var1,
        'test_var2': test_var2,
    }
    return render(request, 'index.html', context)
```

実装自体はこれで終わりです。Djangoで必要なアプリ設定・ルーティング設定だけしておきます。
```py:urls.py
from django.contrib import admin
from django.urls import path
from sample_app.views import index

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', index, name='index'), # 追加
]
```
```py:settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'sample_app', # 追加
]
```

また、Djangoにおいては本番環境の運用において、settings.pyでALLOWED_HOSTSを適切に設定しなければなりません。
今回は環境変数でホストを設定しておくこととします。
```py:settings.py
import os

ALLOWED_HOSTS = []
if os.environ.get("CLOUD_RUN_URL"):
    ALLOWED_HOSTS.append(os.environ["CLOUD_RUN_URL"])
```

:::note info 
Cloud Runのデフォルトドメインを使用する場合、以下のようなURLとなります。
https://< APP_NAME >-< Google Cloud プロジェクト番号 >.<リージョン名>.run.app
:::

では、実際にデプロイしてみます。.envファイルには以下のような記載をしておきます。
```text:.env
TEST_VARIABLE1=Hello-Django
TEST_VARIABLE2=Cloud-Run
CLOUD_RUN_URL=<ホストを設定>
```
Procfileを作成し、以下のように記述しておきます。
```text:Procfile
web: gunicorn sample_django_cloud_run.wsgi --bind :$PORT --workers 1 --threads 8 --timeout 0
```

:::note info
[こちら](https://cloud.google.com/docs/buildpacks/overview?hl=ja#use_to_containerize_your_code)にてPythonではProcfileが必須とのことなので、Djangoに限らず作成しています。
:::

以下のコマンドを実行し、デプロイを実施します。処理は数分〜10分程度かかるかと思います。
※deploy.shのAPP_NAME等、設定は必要に応じて修正してください。
```sh
bash deploy.sh # zshでもOK
```

正常に完了するとデプロイ先のURLがターミナル上に表示されているかと思いますので、アクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/5c6f2a1c-7751-59df-5ab9-969af6896e26.png)

ちゃんと環境変数も合わせてデプロイできているようです！

## Flaskのサンプル
### ライブラリ・ディレクトリ構成
続いてFlaskです。ライブラリは以下を使用します。
```text:requirements.txt
Flask==3.0.3
gunicorn==20.1.0
```

ディレクトリ構成は以下の通りです。
```sh
.
├── Procfile
├── deploy.sh
├── main.py
├── requirements.txt
└── templates
    └── index.html
```

### サンプルコード
Djangoと同様、環境変数を読み込んでhtml上で表示してみます。
```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>flask-sample</title>
</head>

<body>
    <h1>Hello Flask</h1>
    <p>TEST_VARIABLE1 の値: {{ test_var1 }}</p>
    <p>TEST_VARIABLE2 の値: {{ test_var2 }}</p>
</body>

</html>
```

```py:main.py
from flask import Flask, render_template
import os

app = Flask(__name__)


@app.get("/")
def index():
    # 環境変数から値を取得
    test_var1 = os.getenv('TEST_VARIABLE1', 'Not set')
    test_var2 = os.getenv('TEST_VARIABLE2', 'Not set')
    return render_template('index.html', test_var1=test_var1, test_var2=test_var2)


if __name__ == "__main__":
    app.run(host="localhost", port=8080, debug=True)
```

では、デプロイを行なってみます。Django同様、.envとProcfileを作成します。
```text:.env
TEST_VARIABLE1=Hello-Flask
TEST_VARIABLE2=Cloud-Run
```

```text:Procfile
web: gunicorn main:app --bind :$PORT --workers 1 --threads 8 --timeout 0
```

以下のコマンドを実行します。
※deploy.shのAPP_NAME等、設定は必要に応じて修正してください。
```sh
bash deploy.sh # zshでもOK
```

完了後、デプロイ先のURLにアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/74d6d60b-4021-8cf1-c6e8-02c2388ee798.png)

Flaskも環境変数と合わせてデプロイできました！

## FastAPIのサンプル
### ライブラリ・ディレクトリ構成
続いてはFastAPIで試してみます。ライブラリは以下を使用します。Django,Flaskとは異なり、FastAPIで推奨されているuvicornを使用します。
```text:requirements.txt
fastapi==0.115.0
uvicorn==0.30.6
```

ディレクトリ構成は以下の通りです。FastAPIはAPI開発に特化したフレームワークですので、これまでとは異なりHTMLに表示はしません。
```sh
.
├── Procfile
├── deploy.sh
├── main.py
└── requirements.txt
```

### サンプルコード
FastAPIでは、環境変数を読み込んでそれを返すAPIで試してみます。
```py:main.py
from fastapi import FastAPI
import os

app = FastAPI()

@app.get("/")
async def index():
    # 環境変数から値を取得
    test_var1 = os.getenv('TEST_VARIABLE1', 'Not set')
    test_var2 = os.getenv('TEST_VARIABLE2', 'Not set')
    return {'test_var1': test_var1, 'test_var2': test_var2}
```

これまでと同じ流れでデプロイを行います。.envとProcfileを作成します。
```text:.env
TEST_VARIABLE1=Hello-FastAPI
TEST_VARIABLE2=Cloud-Run
```

```text:Procfile
web: uvicorn main:app --host 0.0.0.0 --port $PORT
```

以下のコマンドを実行します。
※deploy.shのAPP_NAME等、設定は必要に応じて修正してください。
```sh
bash deploy.sh # zshでもOK
```

デプロイ完了後、デプロイ先のURLにアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637890/91204461-6369-0dbe-071a-67ad8c88901a.png)

こちらもうまくデプロイできました！

## まとめ
今回はCloud RunにPythonで開発したアプリケーションを簡単にデプロイする手順をまとめてみました。環境変数をいくつか設定したいことはよくあると思いますので、そのような時に参考としてもらえると幸いです。
Cloud Runはコンテナ実行のプラットフォームですが、Dockerfileを作成しなくてもソースコードだけで簡単にデプロイできるのは嬉しいですね。実際にはCI/CDで運用することが多いと思いますので、その辺りも試してみたいです。