52
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Docker+Django+Nginx+MySQL+Gunicornで環境構築~M1Mac対応

Last updated at Posted at 2022-08-17

前提

  • フレームワークはDjango
  • DBはMySQL
  • アプリケーションサーバはGunicorn
  • WebサーバはNginx
  • DB側のコンテナ名はmysql、django側のコンテナ名はapp、nginx側はwebにします
  • 開発用はdocker-compose.yml(Django+MySQL)として作成します
  • 本番用はdocker-compose.prod.yml(Django+MySQL+Nginx)として作成します
  • 作成するプロジェクト名はdjangopjにしていますが別のプロジェクト名で作成する際はdjangopjと置き換えて作成してください

概要

タイトルに記載の通り、

  • Django
  • MySQL
  • Nginx
    • Gunicorn

を使ったコンテナ開発環境の作成方法について解説していきます
その際にパッケージ管理にrequirements.txtを使用します

また、記事の後半に応用編として

  • Poetryを使った開発環境の構築方法
  • Pydanticを使った環境変数の管理方法

について解説していきます
はじめてDockerを使う方はまずはrequirements.txtを使った基本的な構築方法を理解してから挑戦していただければと思います

ディレクトリ構成

初回作成時のディレクトリ構成は以下の通りです
containersフォルダを作成し、その中にDjango、MySQL、Nginxのフォルダを作成してください
また、Nginxのフォルダの中にconf.dフォルダも作成します

tree
.
├── containers
│   ├── django
│   │   └── Dockerfile
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── init.sql
│   │   └── my.cnf
│   └── nginx
│       ├── Dockerfile
│       └── conf.d
│           └── default.conf
├── .gitignore
├── .env
├── .env.prod
├── entrypoint.sh
├── docker-compose.prod.yml
├── docker-compose.yml
└── requirements.txt

作成するファイル

  • DjangoのDockerfile
  • MySQLのDockerfile
  • NginxのDockerfile
  • my.cnf(MySQL用の設定ファイル)
  • init.sql(MySQLのユーザに権限を付与)
  • default.conf(Nginx用の設定ファイル)
  • docker-compose.yml(開発用)
  • docker-compose.prod.yml(本番用)
  • requirements.txt(ざっくり言うとRailsでいうGemfileにあたる)
  • .env(開発用の環境変数の設定ファイル)
  • .env.prod(本番用の環境変数の設定ファイル)
  • entrypoint.sh(Djangoのコマンドを実行する用のシェルスクリプト)
  • .gitignore

の作成方法について順に説明します

そもそもなんで開発用と本番用に分けるの?

Django+MySQL+Nignxの構成で開発する場合、Nginxは静的ファイルを表示させる機能しかないため、ViewやModelの変更を反映させるには都度コンテナを再起動させる必要があります(要するにホットリロードができないため、開発効率が悪い)
そのため、開発はDjango+MySQLのコンテナで行い、本番環境ではNginxのポートから画面を確認する運用になるかと思います
実際は本番環境ではAWSのECSやFargateなどを使用しますが本記事ではローカル上で使用する開発用、本番用のDockerfileとdocker-compose.ymlの書き方もあわせて解説します

各ファイルに必要なコードを記入しよう

Dockerfile(Django)

containers/django/Dockerfile
# Pythonのイメージを指定
FROM python:3
# PYTHONDONTWRITEBYTECODEとPYTHONUNBUFFEREDはオプション
# pycファイル(および__pycache__)の生成を行わないようにする
ENV PYTHONDONTWRITEBYTECODE=1
# 標準出力・標準エラーのストリームのバッファリングを行わない
ENV PYTHONUNBUFFERED=1
# コンテナのワークディレクトリを/codeに指定
WORKDIR /code
# ローカルのrequirements.txtをコンテナの/codeフォルダ直下に置く
COPY requirements.txt /code/
# コンテナ内でpipをアップグレード
RUN pip install --upgrade pip
# pip install -r requirements.txtを実行
RUN pip install -r requirements.txt
# ソースコードをコンテナにコピー
COPY . /code/
# entrypoint.shに実行権限を付与
RUN chmod 755 entrypoint.sh

MySQL

containers/MySQL/Dockerfile
# MySQL8系のイメージを指定
FROM mysql:8.0

# MySQLのローカルの設定ファイルをコンテナにコピー
COPY containers/mysql/my.cnf /etc/mysql/conf.d/my.cnf
# init.sqlをコンテナの/docker-entrypoint-init.db.dと共有
COPY containers/mysql/init.sql /docker-entrypoint-initdb.d

Nginx

Dockerfile
FROM nginx:1.21-alpine

# ローカルのdefault.confをコンテナにコピー
COPY containers/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf

my.cnf(MySQL用の設定ファイル)

containers/MySQL/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character_set_server=utf8mb4
collation_server=utf8mb4_bin
# タイムゾーンの設定
default_time_zone=SYSTEM
log_timestamps=SYSTEM
# MySQL8.0以上用のデフォルト認証プラグインの設定
default_authentication_plugin=caching_sha2_password
# mysqlオプションの設定
[mysql]
# 文字コードの設定
default_character_set=utf8mb4
# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default_character_set=utf8mb4

init.sql

-- MYSQL_USERに権限を付与
-- 今回はdjangoというユーザを指定
GRANT ALL PRIVILEGES ON *.* TO 'django'@'%';
FLUSH PRIVILEGES;

default.conf(Nginx用の設定ファイル)

default.conf
# Django(Gunicorn)の8000番ポートとつなぐ
upstream django {
    # サーバにDjangoのコンテナ名を指定。今回はapp
    # ポートはDjangoのコンテナの8000番ポート
    server app:8000;
}

server {
    # HTTPの80番ポートを指定
    listen 80;
    server_name 0.0.0.0;

    # プロキシ設定
    # 実際はNginxのコンテナにアクセスしてるのをDjangoにアクセスしてるかのようにみせる
    location / {
        proxy_pass http://django;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_redirect off;
    }
    
    # djangoの静的ファイル(HTML、CSS、Javascriptなど)を管理
    location /static/ {
		alias /static/;
	}
}

docker-compose.yml(開発用)

docker-compose.yml
# docker-composeのバージョンを指定
version: "3.9"

# db(MySQL),app(Django)のコンテナを作成
services:
  db:
    # コンテナ名をmysqlに設定
    container_name: mysql
    # MySQLのDockerfileをビルドする
    build:
      # ビルドコンテキストはカレントディレクトリ
      context: .
      dockerfile: containers/mysql/Dockerfile
    # M1チップでも動くように
    # Intel Macの場合あってもなくても動く
    platform: linux/x86_64
    # DBのボリュームを指定
    # ローカルの/data/dbをコンテナの/var/lib/mysqlにマウントする
    volumes:
      - db_data:/var/lib/mysql
    # 環境変数を.envを使って設定
    env_file:
      - .env
    # DBのコンテナのヘルスチェックを行う
    # mysqladmin(MySQLサーバーの管理を行うクライアントを使ってDBコンテナ自身(127.0.0.1)にpingを送ってヘルスチェックを行う
    healthcheck:
      test: mysqladmin ping -h 127.0.0.1 -u$$MYSQL_USER -p$$MYSQL_PASSWORD
      # ヘルスチェックのインターバルは10秒
      interval: 10s
      # タイムアウト時間は10秒
      timeout: 10s
      # リトライ回数は3回
      retries: 3
      # ヘルスチェックが失敗しても無視する時間は30秒
      start_period: 30s
  app:
    # コンテナ名をappに設定
    container_name: app
    # DjangoのDockerfileをビルドする
    build:
      # ビルドコンテキストはカレントディレクトリ
      context: .
      dockerfile: containers/django/Dockerfile
    # ボリュームを指定
    # ローカルのカレントディレクトリをコンテナの/codeにマウントする
    # ローカルの/staticをコンテナの/staticにマウントする
    volumes:
      - .:/code
      - ./static:/static
    # ローカルの8000番ポートとコンテナの8000番ポートをつなぐ
    ports:
      - "8000:8000"
    # シェルスクリプトを実行
    command: sh -c "/code/entrypoint.sh"
    # 環境変数を.envを使って設定
    env_file:
      - .env
    # 先にdbを起動してからappを起動する
    depends_on:
      db:
        # dbのヘルスチェックが終わってからappを起動させる
        condition: service_healthy
volumes:
  db_data:
  static:

docker-compose.prod.yml(本番用)

docker-compose.prod.yml
version: "3.9"

# db(MySQL),app(Django),web(Nginx)のコンテナを作成
services:
  db:
    container_name: mysql
    build:
      context: .
      dockerfile: containers/mysql/Dockerfile
    platform: linux/x86_64
    volumes:
      - db_data:/var/lib/mysql
    # コンテナ内の環境変数を.env.prodを使って設定
    env_file:
      - .env.prod
    healthcheck:
      test: mysqladmin ping -h 127.0.0.1 -u$$MYSQL_USER -p$$MYSQL_PASSWORD
      interval: 10s
      timeout: 10s
      retries: 3
      start_period: 30s
  app:
    container_name: app
    build:
      context: .
      dockerfile: containers/django/Dockerfile
    volumes:
      - .:/code
      - ./static:/static
    # 8000番ポートをNginx側が接続できるよう開く
    expose:
      - "8000"
    command: sh -c "/code/entrypoint.sh"
    # コンテナ内の環境変数を.env.prodを使って設定
    env_file:
      - .env.prod
    depends_on:
      db:
        # dbのヘルスチェックが終わってからappを起動させる
        condition: service_healthy
  web:
    # コンテナ名をwebに指定
    container_name: web
    # NginxのDockerfileをビルドする
    build:
      # ビルドコンテキストはカレントディレクトリ
      context: .
      dockerfile: containers/nginx/Dockerfile
    # ボリュームを指定
    # ローカルの/staticをコンテナの/staticにマウントする
    volumes:
      - ./static:/static
    # ローカルの80番ボートをコンテナの80番ポートとつなぐ
    ports:
      - "80:80"
    # 先にappを起動してからwebを起動する
    depends_on:
      - app
volumes:
  db_data:
  static:

requirements.txt

  • Django
  • mysqlclient
  • Gunicorn

をDjangoのコンテナにインストールするので記載します

requirements.txt
Django>=3.0,<4.0
mysqlclient>=1.3.6,<2.0
gunicorn>=19.9.0,<20.0

.env(開発用)

MySQLのrootユーザのパスワードなどをdocker-compose.ymlやDjangoのsettings.pyに書くのは危険なので.envファイルを使います
今回は開発用のためDEBUG=Trueにする必要があります
.gitignore(後述)があることで.envファイルはGitHubに上がることはありません
今回は以下のような内容にします

.env
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=django-db
MYSQL_USER=django
MYSQL_PASSWORD=django
# SECRET_KEYに任意の値を入力
SECRET_KEY=django
ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
# 開発環境のためTrue
DEBUG=True

.env.prod(本番用)

続いて本番環境用の環境変数を設定します
実際に本番環境の環境変数はAWSのパラメータストアなどに保存しますが今回は.env.prodを使います
本番用のためDEBUG=Falseにする必要があります

.env.prod
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=django-db
MYSQL_USER=django
MYSQL_PASSWORD=django-prod
# SECRET_KEYについては本番環境では推測されない値に変更しておきましょう
SECRET_KEY=xdmjx=9l@x)-jitznpb^%yjn6h=7g)$%e8_+1s)o+8o79csa4d
ALLOWED_HOSTS=localhost 127.0.0.1 [::1]
# 開発環境のためFalse
DEBUG=False

entrypoint.sh

Djangoのマイグレーションや管理者画面、Django Rest Frameworkの静的ファイルを集めるコマンドを定義します
また、開発環境、本番環境では使うコマンドが違うので1つのシェルスクリプトに記載すると同じような記述をdocker-compose.ymlに書かなくてもいい上に可読性が上がります

#!/bin/sh

はshebangと言ってシェルスクリプトを実行するためのおまじないみたいなものなので忘れずに記載しましょう

entrypoint.sh
#!/bin/sh
python manage.py makemigrations --noinput
python manage.py migrate --noinput
python manage.py collectstatic --noinput
# 環境変数のDEBUGの値がTrueの時はrunserverを、Falseの時はgunicornを実行します
if [ $DEBUG = "True" ]
then
    python manage.py runserver 0.0.0.0:8000
else
    # gunicornを起動させる時はプロジェクト名を指定します
    # 今回はdjangopjにします
    gunicorn djangopj.wsgi:application --bind 0.0.0.0:8000
fi

.gitignore

GitHubの公式サイトからPythonの.gitignoreを作成します
.env.prodは.gitignoreにないため追記します
また、migrationとstaticは実際の開発ではGitの管理下に置く必要がないため、こちらも.gitignoreに追記します

# Environments
.env
# .env.prodを.gitignoreする
.env.prod
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

・・・

# ignore static files
static/
# ignore migration files
migrations/

imageのビルド、Djangoの画面表示まで行おう

今回はNginxのポートにアクセスしてDjangoの画面を表示させたいのでdocker-compose.prod.yml(本番用)を使います
開発する場合はコマンドで指定しているファイルをdocker-compose.ymlに置き換えて、8000番ポートにアクセスしてください

docker-composeでDocker imageを作成しよう(初回)

プロジェクトを新規作成する際はプロジェクト名と作成するディレクトリを指定して以下のコマンドを実行します
今回はdjangopjのプロジェクトをカレントディレクトリに作成します

terminal
docker-compose -f docker-compose.prod.yml run app django-admin startproject djangopj .

実行するとローカルのディレクトリ構成は以下のようになります
data/db内はファイルが非常に多いので省略します

tree
.
├── containers
│   ├── django
│   │   └── Dockerfile
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── init.sql
│   │   └── my.cnf
│   └── nginx
│       ├── Dockerfile
│       └── conf.d
│           └── default.conf
├── djangopj
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── .env
├── .env.prod
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── entrypoint.sh
├── manage.py
├── requirements.txt
└── static

すでにプロジェクトがある場合

GitHubにあるソースコードをcloneする場合など、プロジェクトが作成済みの時は以下のコマンドを実行します

docker-compose -f docker-compose.prod.yml build

settings.pyのDATABASESを変更

DjangoのデフォルトのDBはSQliteのため、MySQLに変更する必要がある

settings.py
from pathlib import Path
# osのモジュールをインポート
import os

# [・・・]

# SECRET_KEYを.envから取得
SECRET_KEY = os.environ.get("SECRET_KEY")

# DEBUGを.envから取得
# envファイルにTrue、Falseと書くとDjangoがString型と認識してしまいます
# os.environ.get("DEBUG") == "True"を満たすとboolean型のTrueになり、
# env内のDEBUGがTrue以外ならFalseになります
DEBUG = os.environ.get("DEBUG") == "True"

# ALLOWED_HOSTSを.envから取得
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(" ")

# [・・・]

# MySQLのパラメータを.envから取得
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        # コンテナ内の環境変数をDATABASESのパラメータに反映
        "NAME": os.environ.get("MYSQL_DATABASE"),
        "USER": os.environ.get("MYSQL_USER"),
        "PASSWORD": os.environ.get("MYSQL_PASSWORD"),
        "HOST": "db",
        "PORT": 3306,
        "OPTIONS": {
            "init_command": "SET sql_mode='STRICT_TRANS_TABLES'",
        },
    }
}


# [・・・]

# 言語を日本語に設定
LANGUAGE_CODE = "ja"
# タイムゾーンをAsia/Tokyoに設定
TIME_ZONE = "Asia/Tokyo"

# [・・・]

# STATIC_ROOTを設定
# Djangoの管理者画面にHTML、CSS、Javascriptが適用されます
STATIC_ROOT = "/static/"
STATIC_URL = "/static/"

コンテナを起動

コンテナをデタッチモードで起動する
デタッチモード起動することでコンテナの中に入らずにバックグラウンドで起動させることができます

terminal
docker-compose -f docker-compose.prod.yml up -d

127.0.0.1:80にアクセスしてみよう

ホストからNginxのポートに接続します
ブラウザにアクセスし、NginxのポートからDjangoのポートへアクセスできます
以下のページが表示されます

このようにNot Foundと表示される場合はルーティングの設定をしてないからでコンテナ自体はうまく起動しています
スクリーンショット 2022-10-16 21.03.54.png

127.0.0.1:80/adminにアクセスし、以下の画面が出たら成功です

スクリーンショット 2022-10-16 21.06.04.png

DEBUG=Trueに設定した場合は下記の画像が表示されます

スクリーンショット 2022-08-17 21.10.44.png

画面が表示されない時は?

上記のような画面が表示されない場合は初回起動時にMySQL側のコンテナがうまく立ち上がってない可能性があるので

docker-compose -f docker-compose.prod.yml down

でコンテナを停止させた後に

docker compose -f docker-compose.prod.yml up -d

でコンテナをもう一度立ち上げてみてください

もう一度docker compose -f docker-compose.prod.yml up -dしても接続できない時

failed (111: Connection refused) while connecting to upstream

Nginxのdefault.confを見直す必要があります

containers/nginx/conf.d/default.conf
upstream django {
    server app:8000;
}

serverの後はDjangoではなく、Nginxのコンテナ名を間違えて指定してしまうことはよくあるのでdocker psコマンドでDjangoのコンテナ名を指定しているか確認してください

django.db.utils.OperationalError: FATAL: database does not exist

.envファイル内のMYSQL_DATABASEの名前が命名規則に即してない場合は上記のようなエラーが出る場合があります
下記のサイトからご自身が設定したデータベース名がMySQLの命名規則に沿っているか確認してください

Poetryを使ったコンテナ環境の構築

PoetryとはPythonのパッケージ管理ツールのことです
Poetryを使用することで

  • コマンド一つでパッケージをインストールできる
  • パッケージ間の依存関係を自動で解消してくれる
  • Blackなどの設定を共通のファイルにまとめることができる

などとても便利です
実際の開発現場でよく使われているので可能なのであれば習得しましょう
詳細は以下の記事を参考にしてください

ディレクトリ構成

ディレクトリ構成に関してはrequirements.txtがpyproject.tomlに置き換わります

tree
.
├── containers
│   ├── django
│   │   └── Dockerfile
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── init.sql
│   │   └── my.cnf
│   └── nginx
│       ├── Dockerfile
│       └── conf.d
│           └── default.conf
├── .gitignore
├── .env
├── .env.prod
├── entrypoint.sh
├── docker-compose.prod.yml
├── docker-compose.yml
└── pyproject.toml

pyproject.toml

requirements.txt同様

[tool.poetry.dependencies]

内に以下のパッケージをインストールします

pyproject.toml
[tool.poetry]
name = "api"
version = "0.1.0"
description = "api"
authors = ["shun198"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
Django = "^4.2.3"
djangorestframework = "^3.14.0"
mysqlclient = "^2.1.1"
gunicorn = "^20.1.0"

Dockerfile(Django)

containers/django/Dockerfile
FROM python:3.11

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /code
COPY pyproject.toml /code/
# Initialize python project with Poetry
RUN pip install --upgrade pip && pip install poetry
RUN poetry install
# entrypoint.shに実行権限を付与
RUN chmod 755 entrypoint.sh

新規プロジェクトの作成

新規プロジェクトを作成する際は以下のコマンドを入力します

docker-compose -f docker-compose.prod.yml run app poetry run django-admin startproject djangopj .

以下のようになれば成功です

tree
.
├── containers
│   ├── django
│   │   └── Dockerfile
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── init.sql
│   │   └── my.cnf
│   └── nginx
│       ├── Dockerfile
│       └── conf.d
│           └── default.conf
├── djangopj
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── .env
├── .env.prod
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── entrypoint.sh
├── manage.py
├── poetry.lock
├── pyproject.toml
└── static

Pydanticを使った環境変数の管理

PydanticはPythonの型アノテーションを使って型ヒントを提供したり、バリデーションエラーを簡単にしてくれるライブラリです
環境変数の設定に使うことで

  • 使用している環境変数の型が直感的にわかる
  • 環境変数関連でエラーが発生するとPydanticのエラーが出るのでデバッグが容易になる

などのメリットがあります

pyproject.toml

Pydanticをインストールします

pyproject.toml
[tool.poetry]
name = "djangopj"
version = "0.1.0"
description = "api"
authors = ["shun198"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
Django = "^4.2.3"
djangorestframework = "^3.14.0"
mysqlclient = "^2.1.1"
gunicorn = "^20.1.0"
pydantic = "^1.10.6"

環境変数の設定

今回はdjangopjの中にenvironment.pyを作成します

tree
.
├── containers
│   ├── django
│   │   └── Dockerfile
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── init.sql
│   │   └── my.cnf
│   └── nginx
│       ├── Dockerfile
│       └── conf.d
│           └── default.conf
├── djangopj
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── environment.py
│   ├── urls.py
│   └── wsgi.py
├── .env
├── .env.prod
├── .gitignore
├── docker-compose.prod.yml
├── docker-compose.yml
├── entrypoint.sh
├── manage.py
├── poetry.lock
├── pyproject.toml
└── static

environment.py

下記のように環境変数を設定します

environment.py
"""環境変数定義用のモジュール"""

from pydantic import BaseSettings


class DjangoSettings(BaseSettings):
    """Django関連の環境変数を設定するクラス"""

    SECRET_KEY: str = "secretkey"
    ALLOWED_HOSTS: str = "localhost 127.0.0.1 [::1] back web"
    MYSQL_DATABASE: str = "django"
    MYSQL_USER: str = "django"
    MYSQL_PASSWORD: str = "django"
    MYSQL_HOST: str = "db"
    MYSQL_PORT: int = 3306


django_settings = DjangoSettings()

以下のようにdjango_settingsをimportすると環境変数を設定できます
エディタの補完機能を使えば該当する環境変数をタイポなしで設定できるので開発が捗ります
また、上記のようにデフォルト値を設定できますが、
docker-compose.ymlでMySQLのコンテナを起動させるときにMySQLの環境変数が必要です

.env
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=django-db
MYSQL_USER=django
MYSQL_PASSWORD=django
settings.py
from .environment import django_settings

# SECRET_KEYを.envから取得
SECRET_KEY = django_settings.SECRET_KEY

# ALLOWED_HOSTSを.envから取得
ALLOWED_HOSTS = django_settings.ALLOWED_HOSTS.split()

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        # コンテナ内の環境変数をDATABASESのパラメータに反映
        "NAME": django_settings.MYSQL_DATABASE,
        "USER": django_settings.MYSQL_USER,
        "PASSWORD": django_settings.MYSQL_PASSWORD,
        "HOST": django_settings.MYSQL_HOST,
        "PORT": django_settings.MYSQL_PORT,
        "OPTIONS": {
            "init_command": "SET sql_mode='STRICT_TRANS_TABLES'",
        },
    }
}

以上です

記事の紹介

以下の記事も書いたので良かったら読んでいただけると幸いです

参考文献

52
35
15

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?