DjangoをCloud Runにデプロイ

DjangoをCloud Runにデプロイした時のメモ。

以下の記事を参考にしました。

Cloud Run での Django  |  Google Codelabs

今回はSQLiteを使用しているためCloud SQLは除外しています。
また、SQLiteは別で設定予定のため今回の作業には含まれません。

Cloud API を有効化

gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  secretmanager.googleapis.com \
  artifactregistry.googleapis.com

環境変数を設定

export PROJECT_ID=$(gcloud config get-value core/project)
export REGION=us-central1

サービスアカウントを作成

gcloud iam service-accounts create cloudrun-sa
export SERVICE_ACCOUNT=$(gcloud iam service-accounts list \
    --filter cloudrun-sa --format "value(email)")

Artifact Registry を作成

export ARTIFACT_REGISTRY={$REGION}-docker.pkg.dev/{$PROJECT_ID}/containers

Storage バケットを作成

export GS_BUCKET_NAME={$PROJECT_ID}-media
gcloud storage buckets create gs://{$GS_BUCKET_NAME} --location {$REGION} 

バケットを管理する権限をサービスアカウントに付与する。

gcloud storage buckets add-iam-policy-binding gs://{$GS_BUCKET_NAME} \
    --member serviceAccount:{$SERVICE_ACCOUNT} \
    --role roles/storage.admin

Secret Managerに保存

echo GS_BUCKET_NAME=\"{$GS_BUCKET_NAME}\" >> .env
echo SECRET_KEY=\"$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 50 | head -n 1)\" >> .env
echo DEBUG=True >> .env
gcloud secrets create application_settings --data-file .env

サービス アカウントにこのシークレットへのアクセスを許可する。

gcloud secrets add-iam-policy-binding application_settings \
  --member serviceAccount:{$SERVICE_ACCOUNT} --role roles/secretmanager.secretAccessor

作成されたことを確認。

gcloud secrets versions list application_settings
rm .env

設定ファイルを作成

記事を参考に設定ファイルを作成します。

mv myproject/settings.py myproject/basesettings.py
touch myproject/settings.py

myproject/settings.py

import io
import os
from urllib.parse import urlparse

import environ

# Import the original settings from each template
from .basesettings import *

# Load the settings from the environment variable
env = environ.Env()
env.read_env(io.StringIO(os.environ.get("APPLICATION_SETTINGS", None)))

# Setting this value from django-environ
SECRET_KEY = env("SECRET_KEY")

# Ensure myproject is added to the installed applications
if "myproject" not in INSTALLED_APPS:
    INSTALLED_APPS.append("myproject")

# If defined, add service URLs to Django security settings
CLOUDRUN_SERVICE_URLS = env("CLOUDRUN_SERVICE_URLS", default=None)
if CLOUDRUN_SERVICE_URLS:
    CSRF_TRUSTED_ORIGINS = env("CLOUDRUN_SERVICE_URLS").split(",")
    # Remove the scheme from URLs for ALLOWED_HOSTS
    ALLOWED_HOSTS = [urlparse(url).netloc for url in CSRF_TRUSTED_ORIGINS]
else:
    ALLOWED_HOSTS = ["*"]

# Default false. True allows default landing pages to be visible
DEBUG = env("DEBUG", default=False)

# Set this value from django-environ
DATABASES = {"default": env.db("DATABASE_URL", default="sqlite:///db.sqlite3")}

# Define static storage via django-storages[google]
GS_BUCKET_NAME = env("GS_BUCKET_NAME")
STATICFILES_DIRS = []
GS_DEFAULT_ACL = "publicRead"
STORAGES = {
    "default": {
        "BACKEND": "storages.backends.gcloud.GoogleCloudStorage",
    },
    "staticfiles": {
        "BACKEND": "storages.backends.gcloud.GoogleCloudStorage",
    },
}

requirements.txt(追加)

gunicorn
django-storages[google]
django-environ

Procfileを作成

touch Procfile

Procfile

web: gunicorn --bind 0.0.0.0:$PORT --workers 1 --threads 8 --timeout 0 myproject.wsgi:application

イメージをビルドする

gcloud builds submit --pack image={$ARTIFACT_REGISTRY}/myimage

イメージ名は適宜変更します。

Cloud Runにデプロイ

gcloud run deploy django-cloudrun \
  --region $REGION \
  --image {$ARTIFACT_REGISTRY}/myimage \
  --set-secrets APPLICATION_SETTINGS=application_settings:latest \
  --service-account $SERVICE_ACCOUNT \
  --allow-unauthenticated

デプロイが完了して表示されたURLを選択するとDjangoのスタートページが表示されます。

変更の適用

変更があった場合は、以下の操作を行います。

  1. 新しいイメージをビルド
  2. Cloud Run を更新
gcloud builds submit --pack image=${ARTIFACT_REGISTRY}/myimage
gcloud run services update django-cloudrun \
  --region $REGION \
  --image ${ARTIFACT_REGISTRY}/myimage

.envを読み込み

ローカル環境で開発する時は、環境変数.envから読み込めるほうが都合がいい場合もあったのでmyproject/settings.pyに以下を追加しました。

from pathlib import Path

# プロジェクトのルートディレクトリを取得
BASE_DIR = Path(__file__).resolve().parent.parent

# .envファイルを読み込む
env_path = os.path.join(BASE_DIR, '.env')
if Path(env_path).is_file():
  env.read_env(env_path)