目標
将来的にDocker 環境下で Django と React を連携させて Google Kubernetes Engine にデプロイしてポートフォリオを作りたい。
完全に独学で進めていますので間違いがあれば何卒ご指摘を。。
環境
Windows10 home で Docker Toolbox を使用しています。
DjangoRestFrameworkとReactで超簡単なTodoアプリケーションを作成します。
アプリケーションのコードはDJANGO for APIsのChapter3を参考にしています。
まずはローカルで始める
まずはローカルで Django と React の連携を ToDO アプリを作成しながら考えてみます。
ディレクトリを作成する
Docker Toolbox は Virtualbox を使って docker ホストを立てています。
コンテナのボリュームのマウントはC:Users/
環境下がデフォルトで設定されているので、
Docker toolbox を使用する場合は User ディレクトリ下をおススメします。
# プロジェクトフォルダの作成
mkdir gke-django
cd gke-djagno
# ディレクトリを作成する
mkdir backend
mkdir frontend
Backend の開発を進める
backend は Django-rest-framework で API を作成します。
まずは backend から環境を作成してみます。
settings.py
cd backend
# 仮想環境の作成
python -m venv venv
# 仮想環境の有効化
venv\Scripts\activate
# Pythonパッケージのインストール
python -m pip install --upgrade pip setuptools
python -m pip install django djangorestframework python-dotenv
# Djangoのプロジェクトを始める。
django-admin startproject config .
backend ディレクトリ下でdjango-admin startproject config .
とすることで、
backend
下にconfig
というプロジェクトフォルダが作成されました。
config/settings.py を編集していきます。
まずは基本的なことだけ編集します。
SECRET_KEY は.env に追記するのでコピーしておきましょう。
# config/settings.py
"""
Django settings for config project.
Generated by 'django-admin startproject' using Django 3.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
from dotenv import load_dotenv # 追加
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.basename(BASE_DIR)
# .envの読み込み
load_dotenv(os.path.join(BASE_DIR, '.env')) # 追加
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 変更
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
# 開発環境下で静的ファイルを参照する先
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
# 本番環境で静的ファイルを参照する先
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# メディアファイルpath
MEDIA_URL = '/media/''
settings.py 内で参照している.env
を作成します。
# .envファイルの作成
type nul > .env
# .envにコピペしておいたSECRET_KEYを追加する
SECRET_KEY = '+_f1u^*rb8+%cn-4o*kjn_(15*wspz0*!c)@=ll08odexo88a4'
todo アプリを始める
python manage.py startapp todos
settings.py にアプリケーションを追加します。
# conig/settings.py
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'todos.app.TodosConfig' # 追加
]
model をつくって migration して admin に登録します。
# todos/models.py
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
def __str__(self):
return self.title
$ python manage.py makemigrations todos
Migrations for 'todos':
todos\migrations\0001_initial.py
- Create model Todo
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, todos
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK
Applying todos.0001_initial... OK
# todos/admin.py
from django.contrib import admin
from .models import Todo
admin.site.register(Todo)
管理ユーザーを作成して admin にログインして todo を 3 つほど登録します。
$ python manage.py createsuperuser
ユーザー名 (leave blank to use 'yourname'): yourname
メールアドレス: youraddress@mail.com
Password:
Password (again):
Superuser created successfully.
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
March 10, 2020 - 23:41:26
Django version 3.0.4, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
http://127.0.0.1:8000/admin
にアクセスすると django-admin のログインページが開かれるので
createsuperuser で登録した内容でログインしてみましょう。
Todos を 3 つほど登録しておきましょう。
djangorestframework をはじめる
最初にpip
でインストールしたrestframework
を使用できるように config/settings.py を更新します。
# config/settings.py
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party
'rest_framework',
# Local
'todos.apps.TodosConfig',
]
# 追加
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
}
rest_framework.permissions.AllowAny
は django-rest-framework が暗黙的に決めているデフォルトの設定'DEFAULT_PERMISSION_CLASSES'
を解除するためのものです。
この設定はまだよくわかってないのですがとりあえず前に進みます。
todos/urls.py
, todos/views.py
, todos/serializers.py
を作成します。
URLs
config/urls.py
から各アプリケーションのurls.py
を追加します。
# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('todos.urls'))
]
todos/urls.py
を追加します。
$ type nul > todos\urls.py
# todos/urls.py
from django.urls import path
from .views import ListTodo, DetailTodo
urlpatterns = [
path('<int:pk>/', DetailTodo.as_view()),
path('', ListTodo.as_view())
]
Selializers
モデルインスタンスを json 形式へ変換するためのserializers.py
を追加します。
type nul > todos\serializers.py
# todos/serializers.py
from rest_framework import serializers
from .models import Todo
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('id', 'title', 'body')
fields = ('id', 'title', 'text')
でのid
は PrimaryKey を指定しない場合、
Django によって自動的に追加されます。
Views
Django Rest Framework でviews.py
を作成する場合はrest_framework.generics
の APIView を継承します。
# todos/views.py
from django.shortcuts import render
from rest_framework import generics
from .models import Todo
from .serializers import TodoSerializer
class ListTodo(generics.ListAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
class DetailTodo(generics.RetrieveAPIView):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
router など設定できていませんが、とりあえずは Todo アイテムを API として使用できるようになりました。
開発サーバーでhttp://127.0.0.1:8000/api/
にアクセスすると APIview を確認することができます。
ここまでは Django でよくあるローカル環境での開発です。
CORS
CORS(Cross-Origin Resource Sharing)は React と Django を連携させる場合、React を起動したlocalhost:3000
は Django の API サーバーlocalhost:8000
と
json のやり取りを行わせる必要があります。
django-cors-headers
をインストールしましょう。
python -m pip install django-cors-headers
config/settings.py
を更新します。
# config/settings.py
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party
'rest_framework',
'corsheaders',
# Local
'todos.apps.TodosConfig',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMidddleware', # 追加
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
##################
# rest_framework #
##################
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
}
CORS_ORIGIN_WHITELIST = (
'http://localhost:3000',
)
Tests
テストを書きます。
# todos/test.py
from django.test import TestCase
from .models import Todo
class TodoModelTest(TestCase):
@classmethod
def setUpTestData(cls):
Todo.objects.create(title="first todo", body="a body here")
def test_title_content(self):
todo = Todo.objects.get(id=1)
excepted_object_name = f'{todo.title}'
self.assertEqual(excepted_object_name, 'first todo')
def test_body_content(self):
todo = Todo.objects.get(id=1)
excepted_object_name = f'{todo.body}'
self.assertEqual(excepted_object_name, 'a body here')
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.007s
OK
Destroying test database for alias 'default'...
うまくいったようです。
Frontend の開発を進める
Nodejs は予めインストールしておきましょう。
$ cd frontend
$ npx create-react-app .
$ yarn start
yarn run v1.22.0
$ react-scripts start
i 「wds」: Project is running at http://192.168.56.1/
i 「wds」: webpack output is served from
i 「wds」: Content not from webpack is served from
C:\--you-path--\gke-django-tutorial\frontend\public
i 「wds」: 404s will fallback to /
Starting the development server...
Compiled successfully!
You can now view frontend in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.56.1:3000
Note that the development build is not optimized.
To create a production build, use yarn build.
フロントエンドのプロジェクトを React で開始することができました。
ブウラウザーでhttp://localhost:3000
にアクセスすると React の Welcome ページが確認できます。
App.js
api のエンドポイントは以下のような形で api を返してくるので、これを意識しておきましょう。
まずは mock となるデータで試してみます。
[
{
"id": 1,
"title": "test_title",
"body": "body of test_title"
},
{
"id": 2,
"title": "test_title2",
"body": "body of test_title2"
},
{
"id": 3,
"title": "test_title3",
"body": "body of test_title3"
}
]
// src/App.js
import React, { Component } from "react";
import axios from "axiso";
import "./App.css";
const list = [
{
id: 1,
title: "test_title",
body: "body of test_title"
},
{
id: 2,
title: "test_title2",
body: "body of test_title2"
},
{
id: 3,
title: "test_title3",
body: "body of test_title3"
}
];
class App extends Component {
constructor(props) {
super(props);
this.state = { list };
}
render() {
return (
<div>
{this.state.list.map(item => (
<div key={item.id}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</div>
))}
</div>
);
}
}
export default App;
http://localhost:3000
にアクセスするとモックデータが表示されました。
これを backend
から取得したデータで表示させたいです。
axios
frontend でリクエストを叩くには build-in の Fetch API
か axios
を使う方法がありますが、
今回は axios
を使うことにします。
npm install axios --save
yarn start
App.js を書き換えます。
// src/App.js
import React, { Component } from "react";
import axios from "axios";
import "./App.css";
class App extends Component {
state = {
todos: []
};
componentDidMount() {
this.getTodos();
}
getTodos() {
axios
.get("http://127.0.0.1:8000/api/")
.then(res => {
this.setState({ todos: res.data });
})
.catch(err => {
console.log(err);
});
}
render() {
return (
<div>
{this.state.todos.map(item => (
<div key={item.id}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</div>
))}
</div>
);
}
}
export default App;
これでローカル環境で frontend から backend へ api をたたいて todo リスト一覧を表示させることができました。
超超単純な形ですが一応は Django と React の連携が取れました。
次はこれを Docker 化していきたいと思います。
Docker 化を進める
frontend, backend それぞれに Dockerfile を作成して backend コンテナ、frontend コンテナを作成してみます。
まずは Docker-compose で立ち上げられるところまでを考えていきます。
Backend の Docker 化
Dockerfile を書く前に Django 側でやっておきたいことがいくつかあります。
# 静的ファイ用のディレクトリ
$ mkdir backend\static
# 静的ファイルを全部集めてstaticifilesディレクトリに集められ
$ python manage.py collectstatic
本来であればデータベースの内容や settings.py を local と production で分けたりしますが、
まずは現在の形をそのまま Docker 化できることを考えます。
Dockerfile を backend ディレクトリ内に作成します。
$ type nul > backend\Dockerfile
# backend/Dockerfile
# set base image
FROM python:3.7
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# set work directory
RUN mkdir /code
WORKDIR /code
# install dependencies
COPY requirements.txt /code/
RUN python3 -m pip install --upgrade pip setuptools
RUN pip install -r requirements.txt
# Copy project
COPY . /code/
EXPOSE 8000
次にプロジェクトディレクトリに docker-compose.yml を設置して
docker-compose up で backend コンテナを起動できるようにしてます。
# docker-compose.yml
version: "3.7"
services:
backend:
build: ./backend/.
command: python /code/manage.py runserver 0.0.0.0:8000
volumes:
- ./backend:/code
ports:
- "8000:8000"
$ docker-compose up
これでhttp://localhost:8000/api/
にアクセスすると backend コンテナの DRF ビューにアクセスすることができました。
DockerToolbox を使っている場合は docker ホストの IP アドレスでアクセスしてください。
Frontend の Docker 化
続いて frontend 側を Docker 化していきます。
参考ページ
[Dockerizing a React App]
(https://mherman.org/blog/dockerizing-a-react-app/)
[Creating an app with Docker Compose, Django, and Create React App]
(https://dev.to/englishcraig/creating-an-app-with-docker-compose-django-and-create-react-app-31lf)
[Using Docker for Node.js in Development and Production]
(https://dev.to/alex_barashkov/using-docker-for-nodejs-in-development-and-production-3cgp)
frontend は React で構築しています。これを Docker 化するにはどうしたら良いでしょうか。
backend と同じように frontend ディレクトリに Dockerfile を作成します。
type nul > frontend\Dockerfile
# frontend/Dockerfile
FROM node:12.2.0-alpine
RUN mkdir /code
WORKDIR /code
# Install dependencies
COPY package.json /code/
COPY package-lock.json /code/
RUN npm install
# Add rest of the client code
COPY . /code/
EXPOSE 3000
これで node コンテナ内に package.json を使って同じ環境を構築することができます。
docker-compose.yml に frontend サービスを追加します。
# docker-compose.yml
version: "3.7"
services:
backend:
build: ./backend/.
volumes:
- ./backend:/code
ports:
- "8000:8000"
stdin_open: true
tty: true
command: python /code/manage.py runserver 0.0.0.0:8000
environment:
- CHOKIDAR_USEPOLLING=true
frontend:
build: ./frontend/.
volumes:
- ./frontend:/code
- /code/node_modules
ports:
- "3000:3000"
command: npm start
stdin_open: true
tty: true
environment:
- CHOKIDAR_USEPOLLING=true
- NODE_ENV=development
depends_on:
- backend
environment にCHOKIDAR_USEPOLLING=true
を追加することでイメージを再ビルドすることなく
ホットリローディングしてくれるようになります。
frontend に関しては node_modules が巨大であるため、これをマウントしたりコピーしたりすると
かなりの時間を要します。
したがって、.dockerignore を追加して node_modules をイメージビルドに使用しないようにしておきます(あってる?)。
$ type nul > frontend\.dockerignore
/node_modules
docker-compose up する前に
これで docker-compose up する準備が整いました、が、docker-toolbox を使っている場合は
ポートフォワーディングしているホスト名がlocalhost
ではありません。これをホスト IP に書き換える必要があります。
docker-machine ls
コマンドを使って使用しているホスト IP を確認します。
backend/settings.py
手元のブラウザから frontend コンテナ ⇒backend コンテナにアクセスするため、
CORS_ORIGIN_WHITELIST
に docker ホスト IP を追加する必要があります。
# backend/settings.py
CORS_ORIGIN_WHITELIST = (
'http://localhost:3000',
'http://192.168.99.100:3000', # 追加
)
frontend/src/App.js
api のエンドポイントは docker ホスト IP になります。ここでは192.168.99.100:8000
としています。
// src/App.js
import React, { Component } from "react";
import axios from "axios";
import "./App.css";
class App extends Component {
state = {
todos: []
};
componentDidMount() {
this.getTodos();
}
getTodos() {
axios
.get("http://192.168.99.100:8000/api/") //変更
.then(res => {
this.setState({ todos: res.data });
})
.catch(err => {
console.log(err);
});
}
render() {
return (
<div>
<h1>mother fucker!!?? </h1>
{this.state.todos.map(item => (
<div key={item.id}>
<h1>{item.title}</h1>
<p>{item.body}</p>
</div>
))}
</div>
);
}
}
export default App;
docker-compose up
docker-compose.yml のあるディレクトリ下で docker-compose up します。
$ docker-compose up --build
React のコンパイル完了には時間がかかります。
起動できたらhttp://localhost:3000
にアクセスするとローカルで表示されていた内容が
再現されているはずです。
そしてGKEへ
できたら追加します。