@mizumotokのブログ

テクノロジー、投資、書評、映画、筋トレなどについて

Django+SPA(Nuxt.js)の環境をHerokuにデプロイしてみた

バックエンドはDjangoでフロントエンドはNuxt.jsのようなSPAで環境をheroku上で構築したいのですが、課題がいくつか出てきました。

  • Herokuデプロイ時にNuxt.jsのビルドはどうするのか(gitレポジトリにビルド生成物を置きたくない!)
  • Nuxt.jsで生成された静的ファイルをDjangoからどうハンドリングするのか(nginxには頼れない!)
  • Nuxt.jsのページルーティングをDjangoからどうハンドリングするのか

一つ一つ解決していきましょう。

f:id:mizumotok:20201030120610p:plain

Django + SPAの環境をHerokuにデプロイするときの課題

バックエンドはDjangoでフロントエンドはNuxt.jsのようなSPAで環境を構築したいケースはよくあります。
Nuxt.jsサーバを立てる場合は、DjangoとNuxt.jsそれぞれのレポジトリを作って、別々の2つのHeroku環境にデプロイすればいいです。
今回はNuxt.jsを使いますが、Nuxt.jsのサーバは立てずに、静的なフロントエンド環境を前提とします。そうするとデプロイ時にいくつかの課題点が出てきます。

ビルドの課題

HerokuではBuildpacksというものを使って、デプロイ時にビルドしてくれます。
Buildpacksは標準で多くの言語が用意されています。
devcenter.heroku.com

Herokuが便利なのはレポジトリのルートディレクトリを見て自動的に言語を判定してくれます。言語だけでなくフレームワークまで判定してくれるのです。
例えば、nodejsだったらyarnを使うのかnpmを使うのか、pythonだったらpipかpipenvか、rubyだったらrailsのバージョンやその他のrackアプリケーションか。
自動判定のおかげで何もせずともいい感じにビルドが入ってデプロイしてくれます。
今回のケースのように2つのbuildpack、python(Django)とnodejs(Nuxt.js)を走らせたいときは少し工夫が必要です。

Djangoでの静的ファイルについての課題

Djangoは静的ファイルを扱うのに少し癖があります。
基本的には /static というパス以下を静的ファイルとして扱います。
Nuxt.jsや多くのSPAフレームワークではindex.htmlを生成してくれますが、その中で生成されたjavascript等のアセットのパスは決まっています。Nuxt.jsの場合は/_nuxt/xxxxxx.jsのようなパスになり、それがindex.htmlの中に直書きされます。
RailsやLaravelだったらpublicディレクトリに放り込んでおけば、そのままのパスで静的ファイルとして認識してくれるのですけどね。Djangoだと細工が必要です。

DjangoでのSPAページルーティングについての課題

Nuxt.jsのようなSPAフレームワークではページルーティングの仕組みを持っていて、URLにあわせてページを表示してくれます。
Djangoでもルーティング機能は当然持っていますので、SPAでのルーティングとDjangoでのルーティングを交通整理する必要があります。

nginxを使っていれば、静的ファイルもルーティングの振り分けもnginxの設定で簡単にできるのですが、Herokuではnginx相当のリバースプロキシの設定はいじれません。
Djangoですべて解決する必要があります。

まずはプロジェクトの作成(Django & Nuxt.js)

DjangoもNuxt.jsもコマンドをいくつかたたくだけで、プロジェクトができてしまいます。
まずはそれぞれのプロジェクトを作っておきます。

Djangoプロジェクトの作成

herokuではDjangoのサーバを動かしますので、まずはプロジェクトのルートディレクトリにDjangoプロジェクトを作ります。
まずdjangoやその他のライブラリをインストールしましょう。
最低限必要なのは4つです。

python仮想環境/パッケージ管理ツールとしてpipenvを使っていきます。

$ pipenv install django gunicorn whitenoise django-heroku

とりあえずhelloというプロジェクトを作ってみます。

$ pipenv shell
$ django-admin startproject hello .

Djangoを起動すると無事にブラウザでアクセスできるようになりました。
http://127.0.0.1:8000/

$ python manage.py runserver

Djangoのプロジェクトはここでおいておきます。

現場で使える Django の教科書《基礎編》

現場で使える Django の教科書《基礎編》

  • 作者:横瀬 明仁
  • 発売日: 2018/08/26
  • メディア: オンデマンド (ペーパーバック)

Nuxt.jsプロジェクトの作成

ルートディレクトリにそのままつくると少し煩雑になるので、clientディレクトリをつくってその下にNuxt.jsプロジェクトを作りましょう。

$ mkdir client
$ cd client
$ npx create-nuxt-app .

いくつかの質問にポチポチ答えるとあっという間にプロジェクトができます。
今回は以下のような感じで答えました。

create-nuxt-app v3.4.0
✨  Generating Nuxt.js project in .
? Project name: client
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert se
lection)
? Linting tools: ESLint
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert 
selection)
? Continuous integration: None
? Version control system: None

パッケージマネージャはyarnを選びましたが、これは後で使います。
Git(Version control system)は上の階層(プロジェクルート)でつくるので、ここではいらないですね。
Nuxt.jsサーバを立ち上げてみましょう。
http://127.0.0.1:3000/

$ yarn dev

Vue.js&Nuxt.js超入門

Vue.js&Nuxt.js超入門

Nuxt.jsで生成されるアセットをDjangoで静的ファイルとして扱えるようにする

Nuxt.jsの設定

Djangoでは基本的には/staticパス以下を静的ファイルとして扱います。
Nuxt.jsから静的ファイルを生成してみましょう。

$ cd client
$ yarn generate

標準ではclient/distディレクトリ以下にindex.htmlやその他の静的ファイルが生成されます。
javasscript関連はclient/dist/_nuxtディレクトリです。index.htmlの中をのぞいてみても、当然のような文字列が書かれています。
Nuxt.jsではアセットのパスを変更することができます。nuxt.config.jsファイルの build.publicPath を次のようにしてみましょう。

build: {
  publicPath: '/static/_nuxt/'
}

この状態で再度generateしてみると、javasscript関連はclient/dist/static/_nuxtディレクトリに書き出されます。
index.html内ものようになります。
これでDjangoでも静的ファイルとして認識してくれそうです。

Djangoの設定

さて、Django側でも設定が必要です。
herokuのドキュメントでは静的ファイルを使うにはwhitenoiseを使うといいよと書いてあるので使ってみます。
devcenter.heroku.com

settings.py

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

MIDDLEWARE = [
   (その他のミドルウェア)
   'whitenoise.middleware.WhiteNoiseMiddleware',
]

STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'client/dist/static'),
)

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

ポイントはSTATICFILES_DIRSにNuxt.jsが書き出すjavascriptへのパスを指定していることです。
これでDjangoでの静的ファイルの設定は完了です。

ちなみにherokuのpython buildpackではビルド時にcollectstaticが走って、静的ファイルを一箇所に集めてくれます。今回の例ではSTATIC_ROOTで指しているstaticfilesディレクトリです。
herokuはきめ細かいですね。

DjangoでSPAのページルーティングに対応させる

DjangoでのルーティングをSPAのページにも適用させましょう。
SPAのURLになったらindex.htmlを表示させればいいのですが、SPAのURLを管理するのは面倒なので、Djangoが管理するURL以外だったらindex.htmlを表示させます。

urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', views.api),
    re_path('', TemplateView.as_view(template_name='index.html')),
]

こんな感じで、/admin//api/以外のパスはindex.htmlを表示させます。

index.htmlの場所をDjangoに教えておきましょうね。

settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'client/dist')],

TEMPLATES.DIRSindex.htmlのパスを書いておけばOKです。

views.apiは適当につくっておきましょう。

views.py

from django.http import JsonResponse


def api(request):
    return JsonResponse({'messsage': 'hello'})

単純なjsonを返すだけにしておきます。

ローカル環境で確認

ここまでをローカル環境で確認しておきましょう。

$ cd client
$ yarn generate
$ cd ..
$ pipenv shell
$ python manage.py runserver

http://127.0.0.1:8000/
Nuxt.jsのページが表示されます。

http://127.0.0.1:8000/admin/
Djangoの管理ページが表示されます。

http://127.0.0.1:8000/api/
先程作ったjsonが表示されます。

http://127.0.0.1:8000/xxxxxx
Nuxt.jsのエラーページ(not found)を表示されます。

静的ファイルの読み込みやルーティングはうまくいっていますね。

Heroku対応

Heroku環境にあわせる

django_herokuを使って、settings.pyに以下のように書いておけば、いい感じに対応してくれます。ALLOWED_HOSTSとかを。

settings.py

django_heroku.settings(locals())

Buildpacks適用

Buildpacksはheroku/pythonとheroku/nodejsを使います。
まずheroku/nodejsでNuxt.jsのビルドをしてから、heroku/pythonDjangoのビルドをします。

$ heroku login
$ heroku create
$ heroku buildpacks:set heroku/python
$ heroku buildpacks:add --index 1 heroku/nodejs

heroku/nodejsを先に起動させるように--index 1をつけてあります。

Herokuではプロジェクトルートディレクトリを見てBuildpacksを適用します。
Djangoはルートディレクトリに作ったので問題ないのですが、Nuxt.jsはサブディレクトリ(client)に作ったので、細工をしましょう。
heroku/nodejsがやっていることはpackage.jsonを見て、パッケージインストールとビルドをしているだけです。
そこで、ルートディレクトリにフェイクのpackage.jsonをおいておきます。

package.json

"scripts": {
  "postinstall": "cd client; yarn install --production=false",
  "build": "cd client; yarn nuxt-ts generate"
}

--production=falseは、Heroku上ではNODE_ENV=productionで動くのですが、Nuxt.jsのビルドにはdevDependencyを使うので、devDependencyもインストールさせるためのオプションです。

heroku/nodejs buidpackにyarnを使っていることを教えるために、yarn.lockを作っておきます。

$ yarn install

Procfile

最後にデプロイ後に起動させるコマンドをProcfileに書いておきます。
Node.jsやRailsプロジェクトでは不要ですが、Djangoでは必要になります。
Procfile

web: gunicorn hello.wsgi

デプロイ

ここまでの内容をgit commitしたら、いよいよHerokuにデプロイです。

$ git push heroku master
$ heroku open


https://xxxx.herokuapp.com/
Nuxt.jsのページが表示されます。

https://xxxx.herokuapp.com/admin/
Djangoの管理ページが表示されます。

https://xxxx.herokuapp.com/api/
jsonが表示されます。

https://xxxx.herokuapp.com/xxxxxx
Nuxt.jsのエラーページ(not found)を表示されます。

オールOKです!

ここまでのソースコードはこちらです。
 ↓ ↓ ↓
github.com

現場で使える Django の教科書《基礎編》

現場で使える Django の教科書《基礎編》

  • 作者:横瀬 明仁
  • 発売日: 2018/08/26
  • メディア: オンデマンド (ペーパーバック)
Vue.js&Nuxt.js超入門

Vue.js&Nuxt.js超入門