今まで、「食べたリンゴの割合をグラフ化するアプリ」をHerokuで動かしていました。
Python + Django + Highcharts + Herokuで食べたリンゴの割合をグラフ化してみた - メモ的な思考的な
そんな中、個人的に使ってみたかったGoogle Cloudで動かせるか気になったので、必要な機能を検証してみました。
- Djangoアプリを、Coogle Cloud の Cloud Run + Cloud Storage + Litestream な環境で動かしてみた - メモ的な思考的な
- Django + google-authで、Google Cloud SchedulerからのHTTPリクエストのみ受け付けるAPIエンドポイントを作成する - メモ的な思考的な
- Google Cloud Secret Manager にあるシークレットの値を、ローカル環境のPythonスクリプトで取得してみた - メモ的な思考的な
検証してみても特に気になるところはなかったため、本格的にGoogle Cloud へ移行しようと考えました。
そこで、上記の記事に加えて追加の移行作業を行ったため、内容をメモしておきます。
目次
- 環境
- どのリージョンを使うか検討し、us-west1 (オレゴン) を使う
- Djangoの静的ファイル用の設定を追加
- Secret ManagerにTwitter APIの情報などの秘匿情報を置く
- 秘匿するまでもないが環境変数として設定したいものはデプロイ時に設定する
- Cloud Loggingへログを出力できるようにする
- Cloud Schedulerを設定する
- Google Domainsで購入したドメインを設定
- 専用のサービスアカウントを作成
- Djangoのsettings.pyを修正
- Cloud Runへのデプロイコマンド
- 動作確認
- 不要なリソースの削除
- その他
- ソースコード
環境
- 開発環境
- WSL2 の Ubuntu 22.04.1
- Djangoアプリ
- Litestream 0.3.9
- Google Cloud
- Cloud Run
- Cloud Storage
- Cloud Scheduler
- Google Domains
- Cloud Run用のドメインを設定
なお、以下の記事の作業は実施済とします。
Djangoアプリを、Coogle Cloud の Cloud Run + Cloud Storage + Litestream な環境で動かしてみた - メモ的な思考的な
また、移行前後で使っている機能は以下の通りです。
Herokuだと | 役割 | Google Cloudでは |
---|---|---|
Dyno | Djangoアプリをホスト | Cloud Run |
Dyno | 静的ファイルの配信 | Cloud Storage |
Heroku Postgres | Djangoアプリのデータベース | SQLite + Litestream + Cloud Storage |
Heroku Scheduler | ツイートを定期的に収集 | Cloud Scheduler |
どのリージョンを使うか検討し、us-west1 (オレゴン) を使う
高頻度で使うようなDjangoアプリではないため、一番安価かつ多機能なリージョンを選択しようと考えました。
Cloud RunやCloud Storageの無料枠を見ると、北米のリージョンに適用されそうでした。
- https://cloud.google.com/free/docs/free-cloud-features?hl=ja#cloud-run
- https://cloud.google.com/free/docs/free-cloud-features?hl=ja#storage
また、Cloud Runにカスタムドメインを利用する場合は、特定のリージョンのみ可能そうでした。
Cloud Run のドメイン マッピングの制限事項 | カスタム ドメインのマッピング | Cloud Run のドキュメント | Google Cloud
そこで、
- 北米のリージョン
- そのうち、一番日本に近そうなリージョン
- カスタムドメインが使える
を考慮して、 us-west1
(オレゴン) に各リソースを用意することにしました。
Djangoの静的ファイル用の設定を追加
今回、Djangoの静的ファイル(JavaScriptやCSS)はCloud Storageから配信するため、設定を追加します。
Cloud Storageに、Djangoの静的ファイル用のバケットを作成
Litestream用のバケットの設定に加え、 allUsers
に Storage オブジェクト閲覧者
の権限を付与します。
もし、静的ファイル用のバケットを公開しない場合、デプロイして動作させると、ログに以下のエラーが出ます。
AttributeError: you need a private key to sign credentials.the credentials you are currently using <class 'google.auth.compute_engine.credentials.Credentials'> just contains a token. see https://googleapis.dev/python/google-api-core/latest/auth.html#setting-up-a-service-account for more details.
django-storageを使って、静的ファイルをCloud Storageから配信するよう設定
Google Cloudのチュートリアルの通り、 django-storages
を使って静的ファイルをCloud Storageから配信できるよう設定します。
Cloud Run 環境での Django の実行 | Python | Google Cloud
django-storages
の公式ドキュメントに従い、 pip install django-storages[google]
でインストールします。
Google Cloud Storage — django-storages 1.13.1 documentation
続いて、本番環境の settings.py に設定を行います。
# staticファイルの設定 DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage' GS_BUCKET_NAME = os.environ['GS_BUCKET_NAME'] STATICFILES_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage' # 公開バケットを使用 GS_QUERYSTRING_AUTH = False GS_DEFAULT_ACL = None
Cloud Runの起動時にcollectstaticを実行するように設定を追加
Cloud Storageから配信できるよう、Cloud Runの起動時に collectstatic を実行し、静的ファイルをCloud Storageに置くようにします。
起動時に実行される run.sh
に追記します。
python manage.py collectstatic --noinput --settings dj_ringo_tabetter.settings.production
なお、 --noinput
オプションを追加していますが、これは「すでに静的ファイル置き場にファイルが存在する場合、以下のようなメッセージが出て入力待ちになる」のを回避するためです。
You have requested to collect static files at the destination location as specified in your settings. This will overwrite existing files! Are you sure you want to do this? Type 'yes' to continue, or 'no' to cancel
Secret ManagerにTwitter APIの情報などの秘匿情報を置く
外部公開を防ぎたいものを記載します。
なお、Cloud RunではSecret Managerの内容を環境変数に読み込むこともできるようです。
シークレットを使用する | Cloud Run のドキュメント | Google Cloud
ただ、ベストプラクティスを読むと、環境変数に読み込むのではなく、ライブラリを使って動的に取得したほうが良さそうです。
Secret Manager のベスト プラクティス | Secret Manager のドキュメント | Google Cloud
そこで、前回の記事を参考に、 google-cloud-secret-manager
経由でSecret Managerから値を取得するようにします。
Google Cloud Secret Manager にあるシークレットの値を、ローカル環境のPythonスクリプトで取得してみた - メモ的な思考的な
# Secret Managerから値を取得する関数 def fetch_secret_manager(self): if settings.DEBUG: client = secretmanager.SecretManagerServiceClient.from_service_account_json('gcp_credential.json') else: # Cloud Run上であれば、認証情報は取得できている client = secretmanager.SecretManagerServiceClient() path = client.secret_version_path(os.environ['GCP_PROJECT_ID'], 'twitter_tokens', 'latest') response = client.access_secret_version(name=path) value = response.payload.data.decode('UTF-8') # strからdictにする return json.loads(value)
秘匿するまでもないが環境変数として設定したいものはデプロイ時に設定する
Litestremaの REPLICA_URL
など、秘匿するまでもないが環境変数として設定したいものはデプロイ時に設定するようにします。
そのため、デプロイ時に
gcloud beta run deploy ringo-tabetter \ --set-env-vars REPLICA_URL=gcs://*** \
として環境変数を設定し、ソースコードでは os.environ
で環境変数から値を取得できるようにします。
Cloud Loggingへログを出力できるようにする
デフォルトの設定では、Cloud Loggingにログを出力できません。
そのままではトラブルシューティングがやりづらいので、以下を参考に google-cloud-logging
を使ってCloud Loggingにログ出力できるようにします。
- Cloud RunでDjangoの快適なlogging設定 - みーのぺーじ
- Python Client for Cloud Logging | Python client library | Google Cloud
本番のsettings.pyに以下を追加します。
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{module} {message}', 'style': '{', }, }, 'handlers': { 'cloud_logging': { 'class': 'google.cloud.logging.handlers.CloudLoggingHandler', 'client': google.cloud.logging.Client(), 'formatter': 'verbose' } }, 'loggers': { 'django': { 'handlers': ['cloud_logging'], 'level': 'INFO' }, } }
Cloud Schedulerを設定する
Cloud Schedulerを使い、自分のツイートを定期的に収集します。
ただ、Heroku Schedulerとは異なり、Cloud SchedulerではDjangoのカスタムコマンドを実行できないことから
としました。
また、
- Cloud Schedulerの設定
google-auth
を使って、Cloud Schedulerのリクエストを受け付けるエンドポイント作成
については、以前の記事通りです。
Django + google-authで、Google Cloud SchedulerからのHTTPリクエストのみ受け付けるAPIエンドポイントを作成する - メモ的な思考的な
以前の記事と異なるのは、Djangoでリクエストを受け付けたら、 call_command
でカスタムコマンドを実行する必要があることです。
Running management commands from your code | django-admin と manage.py | Django ドキュメント | Django
ソースコードはこんな感じです。
class GatherTweetView(View): """ Cloud Schedulerからのリクエストを受け付ける View """ http_method_names = ['get'] client_id = os.environ['SCHEDULER_CLIENT_ID'] def get(self, request, *args, **kwargs) -> RingoJsonResponse: authz_header = request.headers.get('Authorization') received_id_token = authz_header.replace('Bearer', '').lstrip() try: # 認証 id_token.verify_oauth2_token(received_id_token, requests.Request(), self.client_id) # tweetの収集 call_command('gather_tweets') return RingoJsonResponse({ 'status': 'success' }) except ValueError as e: print(e) return RingoJsonResponse({ 'status': 'unauthorized' })
Google Domainsで購入したドメインを設定
Herokuと異なり、Cloud RunではURLが ringo-tabetter-***-**.a.run.app
と、ランダムな文字列が含まれてしまいます。
そこで、 Google Domains で購入したドメインのサブドメインにDjangoアプリを割り当ててみます。
Cloud Runをカスタムドメインで動かすため、Cloud Runの カスタムドメインの管理
にて マッピング
を追加します。
以下の内容で設定します。
項目 | 値 |
---|---|
マッピングするサービス | Cloud Runで作成したringo-tabetterアプリを選択 |
確認済ドメイン | thinkami.dev |
サブドメインを指定 | ringo-tabetter |
設定するとGoogle DomainsのDNSに追加するDNSレコードの内容が表示されます。
そのDNSレコードをGoogle DomainsのDNSに設定し、しばらく待つとCloud Runへのマッピングが終わります。
専用のサービスアカウントを作成
デフォルトのサービスアカウントでは権限が広めなため、使うものだけに制限したサービスアカウントを新規作成します。
今回は、Cloud Run用とCloud Scheduler用の2つを用意し、それぞれのサービスに割り当てます。
なお、サービスアカウント作成後にサービスアカウントのロールを変更したい場合は、サービスアカウントのメニューではなく、IAMから行うようです。
Cloud Run用
以下のロールを持つサービスアカウントを作成します。
ロール名 | 目的 | 公式ドキュメント |
---|---|---|
Cloud Run管理者 | Cloud Run実行 | Cloud Run IAM roles | Cloud Run Documentation | Google Cloud |
Secret Managerのシークレットアクセサー | Secret Managerの読み取り | IAM を使用したアクセス制御 | Secret Manager のドキュメント | Google Cloud |
ストレージ管理者 | Litestreamなどで、バケット・オブジェクトの読み書き | Cloud Storage に適用される IAM のロール | Google Cloud |
ログ書き込み | Cloud Loggingへの書き込み | IAM によるアクセス制御 | Cloud Logging | Google Cloud |
Cloud Scheduler用
Google Cloud のリソースにアクセスするわけではないので、ロールの割当はせず、サービスアカウントのみ作成します。
Djangoのsettings.pyを修正
SECRET_KEY
Google Cloudへデプロイするにあたり、 SECRET_KEY
を再生成することにしました。
そこで、以下のstackoverflowを参考に、Django shellにて SECRET_KEY
を再生成しました。
Effects of changing Django's SECRET_KEY - Stack Overflow
再生成した SECRET_KEY
は、デプロイ時に環境変数へ設定してDjangoアプリ起動時に読み込めるようにします。
Cloud Runへのデプロイコマンド
gcloud beta run deploy
コマンドでデプロイします。
gcloud beta run deploy ringo-tabetter \ --source . \ --set-env-vars REPLICA_URL=gcs://<Cloud Storageのバケットパス> \ --set-env-vars SCHEDULER_CLIENT_ID=<Cloud SchedulerのクライアントID> \ --set-env-vars GCP_PROJECT_ID=<Google CloudのprojectId> \ --set-env-vars GS_BUCKET_NAME=<静的ファイルのバケット> \ --set-env-vars TWITTER_USER_ID=<ツイート収集対象のTwitter User ID> \ --set-env-vars DJANGO_SECRET_KEY=<シークレットキー> \ --max-instances 1 \ --execution-environment gen2 \ --no-cpu-throttling \ --allow-unauthenticated \ --region us-west1 \ --service-account <Cloud Runのサービスアカウント> \ --project <Google CloudのプロジェクトID>
動作確認
https://ringo-tabetter.thinkami.dev にアクセスしたところ、グラフが表示されました*1。
なお、「カスタムドメインだけアクセス可」とはしていないので、Cloud Runで払い出されたURLでもアクセスできます。
https://ringo-tabetter-syqtxyot6q-uw.a.run.app
Cloud Runから払い出されたURLをアクセス不可にする場合は、以下が参考になるかもしれません。
- GCP Cloud Run: Disable default URL and use Custom Domain only? - Stack Overflow
- Cloud Run での上り(内向き)の制限 | Cloud Run のドキュメント | Google Cloud
不要なリソースの削除
(2023/1/4 追記)
Cloud Runへデプロイした場合、Cloud StorageやArtifact Registryに都度オブジェクトが生成されます。
ただ、そのまま放置しておいてもデータは削除されず課金されてしまいます。
そこで、過去のデプロイデータなど不要なものを削除します。
Cloud Storage
Cloud Storageには Cloud Runへデプロイする際の Cloud Build データが生成されます。
ただ、Artifact Registryへの生成が済んでしまえばCloud Buildデータは不要になるようです。
Cloud Functions for Firebaseの利用で、異様にGCPのStorageが消費されると思ったら..
そのため、Cloud Build用のバケット (***_cloudbuild
) のオブジェクトを削除するようにします。
ただ、手動で削除するのは手間なので、簡単に設定できるCloud Storageのライフサイクルルールでオブジェクトを削除するようにします。
上記参考記事に従い、以下を設定します。
項目 | 内容 |
---|---|
アクション | オブジェクトの削除 |
オブジェクトの条件 | オブジェクトが生成されてから1日以降 |
Artifact Registry
こちらもビルドされたデータが残り続けるため、定期的に削除します。
ただ、Cloud Storageとは異なり、簡単に設定できるものがないことから、公式ドキュメントでは gcr-cleaner
が案内されています。
イメージを管理 | Artifact Registry のドキュメント | Google Cloud
gcr-cleaner
については、以下の記事で詳しく解説されていました。
Cloud Build+Cloud Runでできた不要なContainer Registryを自動削除する(gcr-cleaner) - くらげになりたい。
ただ、現時点ではアプリのデプロイ回数が少ないことから、まだ gcr-clerner
の設定はせず、手動で削除するようにしています。
その他
今回やったことの範囲外かもしれませんが、以下の記事も参考になりました。
- ahmetb/cloud-run-faq: Unofficial FAQ and everything you've been wondering about Google Cloud Run.
- はじめてみよう Cloud Run ハンズオン | gcp-getting-started-cloudrun/tutorial.md at main · google-cloud-japan/gcp-getting-started-cloudrun
最後になりましたが、長い間Herokuにお世話になりました。ありがとうございました。
ソースコード
Githubに上げました。
https://github.com/thinkAmi/dj_ringo_tabetter
今回のプルリクはこちらです。
https://github.com/thinkAmi/dj_ringo_tabetter/pull/20
*1:リダイレクトされるので、実際には https://ringo-tabetter.thinkami.dev/hc/total が表示されます