Django 製 Web サービスを Cloud Run + PlanetScale に移行する
6年以上前から運営している Django 製のウェブサイトである srandom.com を VPS (CentOS, MySQL) から Cloud Run + PlanetScale に移行しました。
Cloud Run も PlanetScale も無料枠が大きいので、規模の小さいサービスであればほぼ無料で運用できる上、そのままスケールさせることもできるので便利そうです。
2024/03/09 追記: PlanetScale の無料プランが終了するみたいです。
以下、作業手順を雑に書いていきます。
1. Dockerize
Cloud Run で動かすのでまずは Dockerfile 単体で実行できるようにする。
- Uvicorn で動かす
- asgi.py を作成する
- 最近の Django プロジェクトであれば初めから生成されるらしいが、作った時期が昔すぎたため手動で作成した
- ref. https://github.com/django/django/blob/0f31d10c7cf94318395f51d2613297acf1970e69/django/conf/project_template/project_name/asgi.py-tpl
- main.py で起動させるようにする
- PORT は環境変数で指定したものを使いたいのでこういう感じで
- asgi.py を作成する
import os
import uvicorn
PORT = int(os.environ.get("PORT", 8000))
uvicorn.run(
"app.asgi:application",
host="0.0.0.0",
port=PORT,
log_level="info",
proxy_headers=True,
)
-
.env
を使うようにする-
local_settings.py
を使っていたがさすがに管理しにくいので django-environ を使った env ファイルでの管理に移行する
-
FROM python:3.10-slim as builder
RUN apt-get update && apt-get -y install --no-install-recommends gcc libmariadb-dev
WORKDIR /app
COPY Pipfile Pipfile.lock ./
RUN pip install -U pip && pip install pipenv
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install
FROM python:3.10-slim as runner
RUN apt-get update && apt-get -y install --no-install-recommends gcc libmariadb-dev ca-certificates
WORKDIR /app
COPY /app /app
COPY . /app
EXPOSE 8000
ENV PORT 8000
CMD .venv/bin/python main.py
2. Cloud Storage
恒久的に保存しておく必要があるファイル(今回だと静的アセットやユーザがダウンロードするための CSV)の生成処理には Cloud Storage を使うようにする。
-
collectstatic
- django-storages を使って、collectstatic を実行した時に Cloud Storage に static files がアップロードされるようにする。
-
Static URL
- static の URL が Cloud Storage に向くように設定する。
- static files 用のバケットは全員がアクセスできるように公開しておく
resource "google_storage_bucket_iam_member" "all_users_are_legacy_object_reader_of_your_bucket" {
bucket = "your-bucket"
role = "roles/storage.legacyObjectReader"
member = "allUsers"
}
if not DEBUG:
DEFAULT_FILE_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
GS_BUCKET_NAME = 'your-bucket'
GS_DEFAULT_ACL = None
GS_QUERYSTRING_AUTH = False
STATICFILES_STORAGE = 'storages.backends.gcloud.GoogleCloudStorage'
STATIC_URL = 'https://storage.googleapis.com/your-bucket/'
3. PlanetScale
- DB migration
- これまでの migration 履歴をリセットしてアプリごとに新しく
0001_initial.py
を作成する python manage.py makemigrations app
python manage.py migrate
- これで PlanetScale のスキーマができあがった
- これまでの migration 履歴をリセットしてアプリごとに新しく
- mysqldump
- 本番 DB のレコードを dump して PlanetScale に import する
- charset に utf8mb4 を使っていたが、PlanetScale でも問題なく使えそう
- database engine に django_psdb_engine を使う
- PlanetScale は外部キー制約をサポートしていないが、PlanetScale が用意してくれているデータベースラッパーを使えば自動的に外部キー構文を無効にしてくれる
- ref. https://planetscale.com/docs/tutorials/connect-django-app#optional-bring-in-planet-scale-custom-database-wrapper
- SSL 接続
- SSL CA path を設定する
- OS ごとにパスが違うのでローカルと Cloud Run での環境差に注意
- ref. https://planetscale.com/docs/concepts/secure-connections#ca-root-configuration
- SSL CA path を設定する
DATABASES = {
'default': {
...
'OPTIONS': {
'charset': 'utf8mb4',
'ssl': {
'ca': '/etc/ssl/certs/ca-certificates.crt',
}
},
}
}
4. Cloud Run
- Terraform で GCP リソースを諸々作成する
-
Cloud KMS で本番環境用の
.env
を encrypt するgcloud kms encrypt --location asia-northeast1 --keyring app --key env --project $PROJECT --plaintext-file .env.production --ciphertext-file path/to/.env.enc
- 生成した
.env.enc
はリポジトリに push する
- 次のジョブを実行する GitHub Actions Workflow を作成する
- PlanetScale 用に
django_psdb_engine
をダウンロードgit clone https://github.com/planetscale/django_psdb_engine.git
- Cloud KMS で
.env.enc
を decrypt して.env
を生成- 次のようなコマンドを実行する script を用意しておく
gcloud kms decrypt --location asia-northeast1 --keyring app --key env --project $PROJECT --plaintext-file .env --ciphertext-file path/to/.env.enc
- Artifact Registry に Docker image を push
- push した image を指定して Cloud Run にデプロイ
- 後述する Cloud Run Jobs の更新
- PlanetScale 用に
resource "google_cloud_run_service" "app" {
name = "app"
location = local.region
autogenerate_revision_name = true
template {
spec {
containers {
image = "gcr.io/cloudrun/hello" # 最初以外無視されるので何でも良い
resources {
limits = { "memory" : "512Mi", "cpu" : "2000m" }
}
}
service_account_name = google_service_account.cloud_run_app.email
}
metadata {
annotations = {
"autoscaling.knative.dev/maxScale" = "5"
}
}
}
lifecycle {
ignore_changes = [
metadata,
template.0.spec.0.containers.0.image,
template.0.metadata.0.annotations["client.knative.dev/user-image"],
template.0.metadata.0.annotations["run.googleapis.com/client-name"],
template.0.metadata.0.annotations["run.googleapis.com/client-version"],
template.0.metadata.0.labels["commit-sha"],
template.0.metadata.0.labels["managed-by"],
traffic,
]
}
}
name: app
on: push
jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
env:
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
SERVICE: app
REGION: asia-northeast1
IMAGE_APP: asia-northeast1-docker.pkg.dev/project/app/app:${{ github.sha }}
IMAGE_CMD: asia-northeast1-docker.pkg.dev/project/cmd/cmd:${{ github.sha }}
steps:
- uses: actions/checkout@v3
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v0
with:
credentials_json: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}
- name: Configure auth
run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev
- name: Build and push containers
run: |
git clone https://github.com/planetscale/django_psdb_engine.git
./scripts/decrypt_env.sh production
docker build . -f docker/Dockerfile -t ${{ env.IMAGE_APP }}
docker build . -f docker/Dockerfile_cmd -t ${{ env.IMAGE_CMD }}
docker push ${{ env.IMAGE_APP }}
docker push ${{ env.IMAGE_CMD }}
- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@main
with:
service: ${{ env.SERVICE }}
image: ${{ env.IMAGE_APP }}
region: ${{ env.REGION }}
- name: Update Cloud Run Jobs
run: |
gcloud beta run jobs update cmd --image ${{ env.IMAGE_CMD }} --region asia-northeast1
5. 定期実行ジョブの設定
元々 cron で Django のカスタムコマンドを実行してたので、バッチ用のエンドポイントを作るよりもコマンドをそのまま実行するようにしたい。
Cloud Run Jobs と Cloud Scheduler を使えばこれを実現できる。
- バッチコマンドごとに実行用の Dockerfile を作成する
- それらを Artifact Registry に push
- Cloud Run Job を作成する
- 作成した Cloud Run Job を定期的に実行する Cloud Scheduler ジョブを作成する
FROM python:3.10-slim as builder
RUN apt-get update && apt-get -y install --no-install-recommends gcc libmariadb-dev
WORKDIR /app
COPY Pipfile Pipfile.lock ./
RUN pip install -U pip && pip install pipenv
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install
FROM python:3.10-slim as runner
RUN apt-get update && apt-get -y install --no-install-recommends gcc libmariadb-dev ca-certificates
WORKDIR /app
COPY /app /app
COPY . /app
CMD .venv/bin/python manage.py command
resource "google_cloud_scheduler_job" "command" {
name = "command"
description = "Cloud Run Job: command"
schedule = "30 0 * * *" # At 0:30
time_zone = "Asia/Tokyo"
attempt_deadline = "600s" # 10m
retry_config {
retry_count = 1
}
http_target {
http_method = "POST"
uri = "https://asia-northeast1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/project/jobs/command:run"
oauth_token {
service_account_email = "xxx-compute@developer.gserviceaccount.com" # Default compute service account
}
}
}
6. サイト移行
ここまでの作業で一通り Cloud Run + PlanetScale での動作を確認できたので、サイト移行の作業をしていく。
- 現状の本番環境サイトをメンテナンスモードに設定する
- メンテナンスモード中であることが表示され、サイトの全ての機能が使えなくなる(既存の DB にレコードを追加してほしくないため)
- DB のデータ移行
- 本番 DB レコードを dump して PlanetScale の DB に import する
- ドメイン設定
- カスタムドメインでの接続が確認できたら Cloudflare を設定する
- Cloud Storage バケット用のドメインを設定する場合は Page Rules を使ってそのドメイン下のみ SSL の設定を
SSL (Flexible)
に設定する必要がある - 以前のサイトの設定と競合して(?)エッジ証明書が削除済みという表示になって発行されなかったのでここを参考に
certificate_authority
をlets_encrypt
に設定したらちゃんと発行された
- Cloud Storage バケット用のドメインを設定する場合は Page Rules を使ってそのドメイン下のみ SSL の設定を
curl -X PATCH "https://api.cloudflare.com/client/v4/zones/[zone_id]/ssl/universal/settings" -H "X-Auth-Email: [email]" -H "X-Auth-Key: [Global API Key]" -H "Content-Type: application/json" --data '{"certificate_authority":"lets_encrypt"}'
7. Cloud Monitoring
エラーログを監視して Slack にアラート通知を飛ばしたいので Cloud Monitoring でアラートポリシーを作成する。
WARNING レベルのログを一定以上検知したらアラートを飛ばすポリシーの例
resource "google_logging_metric" "cloud_run_app_warning" {
name = "cloud-run-app-warning"
description = "Warning log count of Cloud Run 'app'"
filter = "resource.type = \"cloud_run_revision\" AND resource.labels.service_name = \"app\" AND severity = \"WARNING\""
metric_descriptor {
metric_kind = "DELTA"
value_type = "INT64"
}
}
resource "google_monitoring_alert_policy" "cloud_run_app_warning_count" {
display_name = "[app] Warning log count is high"
combiner = "OR"
conditions {
display_name = "logging/user/cloud-run-app-warning [COUNT]"
condition_threshold {
threshold_value = 10
duration = "0s"
comparison = "COMPARISON_GT"
trigger {
count = 1
}
aggregations {
alignment_period = "600s"
per_series_aligner = "ALIGN_COUNT"
}
filter = "metric.type=\"logging.googleapis.com/user/cloud-run-app-warning\" resource.type=\"cloud_run_revision\""
}
}
notification_channels = [
# これを取得する方法がわからなかったので、一度手動で作成して import した
"projects/project/notificationChannels/xxx"
]
alert_strategy {
auto_close = "259200s"
}
}
指定した閾値を超えるとこんな感じの通知が来る。
所感
ここ数年 Go ばかり書いていたので久しぶりに Django を触れて楽しかった。(諸々手直しする必要があったので)
長い間惰性で VPS 運用をやってきたので、いい感じにクラウドに乗っけられてよかったです。
Cloud SQL や AlloyDB は高いし Heroku も無料プランが廃止されてしまうので、PlanetScale のようなサービスは個人開発者にとっては本当にありがたいですね。
Discussion