🗂

conoha VPSでDjangoをデプロイする

2023/06/19に公開

はじめに

このセクションは全てインストールした後に書いています。
結果的には以下のバージョンになりました。
以降に登場するアプリのバージョンは全て下記を参照してください。

Ubuntu 22.04.2 LTS
nginx version: nginx/1.18.0 (Ubuntu)
gunicorn (version 20.1.0)
Python 3.10.6
django 4.2.1

以下の構成になるように進めていく

[Client]
         ↓ (HTTPS/HTTP)
[Nginx]
         ↓ (HTTP)
[Gunicorn]
         ↓ (WSGI)
[Django Application]

データベースは使用していません。

他にはgitの導入、SSL化までを行います。
目的は既存のDjangoプロジェクトをデプロイして安全に運用することです。

conohaを選んだ理由はイメージバックアップ機能があることと既に契約していたからです。

サーバー追加

minicondaでPythonに纏わる物は管理したいのと、既にあるアプリの稼働が目的なので
conohaのDjangoアプリケーションは敢えて使わない。

本当はCentOSに慣れているのですが、なんだかもうオワコンな風潮なのでubuntuを選択


sshkeyは「+追加」を押すとダウンロードできる。

ダウンロードした鍵でssh接続する

公式に案内がある
https://support.conoha.jp/v/vps_sshkey/?btn_id=v-vps_ssh-sidebar_v-vps_sshkey#02

私はmacユーザーなので、~/.ssh(/User/ユーザー名/.ssh)にダウンロードした****.pemを移動

# やらないと権限ゆるゆるだよと怒られる
chmod 600 ~/.ssh/[公開鍵名].pem

# まずはrootで接続
ssh -i ~/.ssh/[公開鍵名].pem root@[ipアドレス] -p 22

Ubuntuのユーザー管理

初期設定

osライブラリのアップデート

sudo apt-get update
sudo apt-get upgrade

この時色々聞かれるが全部デフォルトにした。

一般ユーザーを作成

rootでログインは危険なので作業ユーザーを作成する。

sudo adduser 任意のユーザー名

# 管理者権限付与
gpasswd -a ユーザー名 sudo

# ユーザーを確認
cat /etc/passwd

一般ユーザー用のSSHキーの生成と設置

せっかくログインしたのに、rootログインは禁止にするのでまたsshキーを作ることになる。めんd

macなので以下を参考に公開鍵を作成した。
ただ作成するディレクトリは注意しないと上書きされちゃうかも知れないので注意
https://qiita.com/wakahara3/items/52094d476774f3a2f619

公式にもやり方説明されているが慣れている人はこっちのほうが早いと思う
この項目はrootで作業している

mkdir /home/[ユーザー名]/.ssh
chmod 700 /home/[ユーザー名]/.ssh

# アップロードがめんどいのでローカルで作成したid_rsa.pubの中身をコピペ
vi /home/[ユーザー名]/.ssh/authorized_keys

# これやらないとパーミッションエラーになる
chmod 600 /home/[ユーザー名]/.ssh/authorized_keys
chown -R [ユーザー名]:[ユーザー名] /home/[ユーザー名]/.ssh/

作成した[ユーザー名]と公開鍵でログインを確認できればおk

ssh -i ~/.ssh/id_rsa [ユーザー名]@[ipアドレス] -p 22

https://support.conoha.jp/v/addusersshkey/?btn_id=v-addusersshkey-sidebar_v-addusersshkey

一応以下も試しておく。
Ubuntuでスーパーユーザー(rootユーザー)になるには、以下のコマンドを使用します:

sudo su

このコマンドを実行すると、パスワードを求められることがあります。これは現在のユーザーのパスワードです。

一方、スーパーユーザーから元のユーザーに戻るには、以下のコマンドを使用します:

exit

または

logout

これらのコマンドは、現在のシェル(この場合は、rootユーザーとして開始されたシェル)を終了し、元のシェルに戻ります。

rootログインを禁止

作成した[ユーザー名]と公開鍵でログインを確認してからやること

SSHデーモンの設定ファイルを編集するためにviエディタを使用するには、以下のコマンドを実行します:

sudo vi /etc/ssh/sshd_config

そして、PermitRootLogin という行を探し、その値を no に変更します:

PermitRootLogin no

その後、変更を保存して終了するためには以下の手順を実行します:

  1. Esc キーを押すことでコマンドモードに切り替えます。
  2. :wq を入力し、エンターキーを押すことでファイルを保存し終了します。

最後に、新しい設定を反映するためにSSHデーモンを再起動します:

sudo systemctl restart sshd

これでrootユーザーの直接ログインが禁止されました。

sshポートの変更

デフォルトのsshポートは危ないので変更する。

SSHのポートを変更するには、先ほどと同じく /etc/ssh/sshd_config ファイルを編集します。

以下のコマンドで vi エディタを使用してファイルを開きます:

sudo vi /etc/ssh/sshd_config

そして、Port という行を探します。この行はSSHサーバーがリッスンするポート番号を設定するためのものです。デフォルトではこの値は 22 です。

Port 22

この値を変更したいポート番号に変えます。例えば、ポート番号を 2222 に変更する場合は、次のように設定します:

Port 2222

その後、変更を保存して終了します:

  1. Esc キーを押すことでコマンドモードに切り替えます。
  2. :wq を入力し、エンターキーを押すことでファイルを保存し終了します。

最後に、新しい設定を反映するためにSSHデーモンを再起動します:

sudo systemctl restart sshd

これでSSHのポートが変更されました。ただし、新しいポート番号を使ってSSH接続を行うには、接続コマンドに -p オプションで新しいポート番号を指定する必要があります。例えば、新しいポート番号が 2222 の場合、接続コマンドは次のようになります:

ssh -p 2222 user@hostname

また、新しいポートがファイアウォールやセキュリティグループのルールによってブロックされていないことを確認してください。

新しいSSHポートがファイアウォールやセキュリティグループのルールによってブロックされていないかを確認する方法は、環境(特にホスティングサービス)によります。

  1. ローカルのファイアウォールの確認(UFWなど)
    UbuntuではUFW (Uncomplicated Firewall) というファイアウォールが広く使われています。UFWの設定を表示するには以下のコマンドを実行します:

    sudo ufw status
    

    これにより、どのポートが開いていてどのポートが閉じているかを確認できます。新しいSSHポートがリストに含まれていない場合、以下のコマンドでそのポートを開くことができます:

    sudo ufw allow 2222/tcp
    

    ここで、2222は新しいSSHポート番号です。

  2. クラウドサービスのセキュリティグループの確認
    クラウドサービス(AWS, GCP, Azureなど)を使用している場合、各インスタンス(またはVM)には通常セキュリティグループまたはネットワークポリシーというものが関連付けられています。これはそのインスタンスに対する接続を制御します。各クラウドサービスの管理コンソールからセキュリティグループの設定を確認・編集できます。新しいSSHポートが許可されているかどうかを確認し、必要なら許可のルールを追加します。

  3. 外部からの接続テスト
    ポートが開いているかどうかを確認するために、外部のマシンから新しいSSHポートに接続を試みることもできます。成功すればポートは開いていると言えます。ただし、これはファイアウォールやセキュリティグループの設定が正しいことを完全に保証するものではありません。

これらの手順を経て新しいSSHポートが開いていることを確認できれば、そのポートでSSH接続を受け付けることができます。

表示されたUFW(Uncomplicated Firewall)のステータスによると、現在「OpenSSH」への全ての接続が許可されています。ただし、これはデフォルトのSSHポート(22番)のみを指している可能性があります。

新しく設定したSSHポート(例えば、先ほどの例での2222番)を開放するには、以下のコマンドを実行します:

sudo ufw allow 2222/tcp

このコマンドを実行した後、再度 sudo ufw status を実行して新しいポートが開放されたことを確認してください。新しいポートがリストに含まれていれば、そのポートは外部からの接続を受け付ける設定になっています。

minicondaをインストール

minicondaとは?

MinicondaはPythonやそのパッケージを管理するためのツールで、Anacondaのより軽量なバージョンです。AnacondaはPythonのディストリビューションであり、科学計算やデータ分析、機械学習などを行うために必要な多くのパッケージを含んでいますが、その全てが必要とは限らないため、必要なパッケージだけを選択してインストールできるMinicondaが提供されています。

Minicondaの主な特徴:

  • 軽量: Anacondaと比較して、必要最低限のパッケージのみを含むため、ディスクスペースの節約やインストール時間の短縮が可能です。
  • カスタマイズ可能: 必要なパッケージを個別に追加インストールできるため、自分だけのPython環境を構築することが可能です。

Minicondaを使用するメリット:

  1. 環境管理: Condaというパッケージ管理ツールが含まれているため、異なるプロジェクト間でPythonのバージョンやパッケージのバージョンを個別に管理することが可能です。これにより、プロジェクト間の依存性の衝突を防ぐことができます。
  2. 多種多様なパッケージの利用: CondaはPythonのパッケージだけでなく、RやRubyなど他の言語のパッケージも管理できます。また、AnacondaのパッケージリポジトリであるAnaconda Cloudには、科学計算やデータ分析に必要な多くのパッケージが提供されています。
  3. マルチプラットフォーム対応: Windows、Mac、Linuxなど、異なるオペレーティングシステムで使用できます。

これらの特性により、Minicondaはデータサイエンスの分野だけでなく、一般的なソフトウェア開発においてもPython環境の管理を効率的に行うためのツールとして広く利用されています。

インストール

# ダウンロードディレクトリを作成
[ユーザー名]@123-45-678-910:~$ mkdir downloads
[ユーザー名]@123-45-678-910:~$ cd downloads/

# Linux用latestをダウンロード
[ユーザー名]@123-45-678-910:~/downloads$ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
--2023-06-15 11:19:32--  https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
Resolving repo.anaconda.com (repo.anaconda.com)... 104.16.131.3, 104.16.130.3, 2606:4700::6810:8203, ...
Connecting to repo.anaconda.com (repo.anaconda.com)|104.16.131.3|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 73134376 (70M) [application/x-sh]
Saving to: ‘Miniconda3-latest-Linux-x86_64.sh’

Miniconda3-latest-Linux-x8 100%[======================================>]  69.75M  24.2MB/s    in 2.9s

2023-06-15 11:19:35 (24.2 MB/s) - ‘Miniconda3-latest-Linux-x86_64.sh’ saved [73134376/73134376]

# インストール
[ユーザー名]@123-45-678-910:~/downloads$ bash Miniconda3-latest-Linux-x86_64.sh

# 色々聞かれるが指定がなければyesとEnterで進める
Thank you for installing Miniconda3!

# 反映させる
[ユーザー名]@123-45-678-910:~/downloads$ source ~/.bashrc

# 先頭の(base)は現在アクティブなconda環境が base であることを示しています。
(base) [ユーザー名]@123-45-678-910:~/downloads$ which conda
/home/[ユーザー名]/miniconda3/bin/conda

shかbashか

shbash の主な違いは、bashsh を拡張したシェルであるという点です。bash は "Bourne-Again SHell" の略で、オリジナルの sh(Bourne shell)に対して多くの改善と追加機能を提供しています。

以下に、bashsh に比べて持っているいくつかの追加機能を挙げておきます:

  • コマンド行編集
  • コマンド行補完
  • ジョブコントロール
  • シェル関数とエイリアス
  • より高度なスクリプト機能(配列など)

一方、shはPOSIX準拠のシェルであり、ほとんどのUnix系システムで互換性があります。つまり、shで書かれたスクリプトは、bashや他のシェルでも実行できます。

ただし、Ubuntuにおいては、shdashというシェルを指すシンボリックリンクになっています。dashbashよりもシンプルで高速なシェルであり、システムの起動スクリプトなどで使用されています。

したがって、特定の機能(配列や関数など)を必要とする場合、またはスクリプトがbash特有の構文を使用している場合(例えばMinicondaのインストーラのような場合)は、bashコマンドを使用するべきです。

minicondaインストール後にsource ~/.bashrcをやる理由は?

まず、~/.bashrcとは何かから始めましょう。

~/.bashrcは、Bashシェル(LinuxやMacのようなUnix系OSのコマンドラインインターフェース)が読み込むスクリプトの一つです。このスクリプトは、新しいシェルセッションが開始されるたび(新しいターミナルウィンドウを開いたり、新しいシェルを起動したりするとき)に自動的に実行されます。

~/.bashrcファイルには以下のような設定が含まれます:

  • 環境変数の設定:これらはプログラムが自身の動作をカスタマイズするために参照する変数です。たとえば、PATH環境変数はシェルが実行可能プログラムを探すディレクトリのリストを保持しています。
  • エイリアス:これらはコマンドのショートカットを作成します。たとえば、alias ll='ls -l'と設定すると、llと入力するだけでls -lコマンドを実行できます。
  • シェル関数:これらはコマンドのグループをカプセル化し、その結果を再利用できるようにします。
  • シェルのプロンプトのカスタマイズ:これはコマンドラインの見た目を変更するためのもので、例えば、現在のディレクトリや現在のユーザ名を表示することができます。

それでは、Minicondaのインストールが終わった後にsource ~/.bashrcを実行する理由は何でしょうか?

Minicondaのインストーラは、~/.bashrcファイルに新たな設定を追加します。具体的には、新しくインストールしたMinicondaの実行可能ファイルを見つけるためにPATH環境変数を更新します。しかし、これらの変更は、新たなシェルセッションを開始するか、source ~/.bashrcを実行するまで反映されません。

したがって、source ~/.bashrcを実行すると、新たなMinicondaの設定が現在のシェルセッションにすぐに反映されます。これにより、MinicondaのPythonインタープリターやcondaコマンドなどがすぐに使えるようになります。

bashの先頭の(base)って?

この (base) は、現在アクティブなconda環境が base であることを示しています。AnacondaやMinicondaをインストールすると、base という名前のデフォルトの環境が作成されます。

AnacondaやMinicondaはPythonと一緒に多数の科学計算パッケージをインストールするためのディストリビューションで、これらのパッケージは仮想環境として管理されます。これにより、異なるプロジェクトで異なるパッケージのバージョンを使用することが可能になります。

コマンドラインプロンプトの前に表示される (base) は、現在 base 環境がアクティブであることを示しています。別の環境に切り替えるには、conda activate [environment_name] コマンドを使用します。ここで、[environment_name] は切り替えたい環境の名前に置き換えます。

conda env list コマンドを実行すると、現在インストールされているすべてのconda環境のリストを表示できます。

https://tomono.tokyo/2020/04/19/8278/
https://applingo.tokyo/article/10280

プロジェクト用仮想環境の作成

仮想環境内で使えるPythonのバージョンの調べ方

conda search "^python$"

仮想環境の新規作成

conda create -n 仮想環境名 python=3.10.6

# 作成が終わると有効化と無効化のコマンドを教えてくれる

# To activate this environment, use
#
#     $ conda activate 仮想環境名
#
# To deactivate an active environment, use
#
#     $ conda deactivate

現在の仮想環境のリストを確認

(base) ユーザー名@123-45-678-910:~$ conda env list
# conda environments:
#
base                  *  /home/ユーザー名/miniconda3
仮想環境名          /home/ユーザー名/miniconda3/envs/仮想環境名

作成した仮想環境を有効化

conda activate 仮想環境名

requirements.txtからDjango環境を再現

pipのfreezeコマンドを使って、現在の環境にインストールされている全てのパッケージとそのバージョンを一覧化し、その一覧をrequirements.txtという名前のファイルに保存する方法です。

以下のコマンドを実行します:(ローカルだったり開発中のプロジェクトで)

pip freeze > requirements.txt

これにより、requirements.txtという名前のファイルが生成され、その中には現在のPython環境にインストールされている全てのパッケージとそのバージョンが一覧化されます。

そして、このrequirements.txtファイルを本番環境(今作成中のUbuntuサーバー)にコピーし、次のコマンドを実行することで、同じパッケージをインストールできます:

(仮想環境名) ユーザー名@123-45-678-910:~$ pip install -r requirements.txt

# 確認
(仮想環境名) ユーザー名@123-45-678-910:~$ pip freeze
asgiref==3.7.2
contourpy==1.0.7
cycler==0.11.0
Django==4.2.1
django-environ==0.10.0
fonttools==4.39.4
kiwisolver==1.4.4
matplotlib==3.7.1
numpy==1.24.3
packaging==23.1
pandas==2.0.2
Pillow==9.5.0
pyparsing==3.0.9
python-dateutil==2.8.2
pytz==2023.3
scipy==1.10.1
six==1.16.0
sqlparse==0.4.4
typing_extensions==4.6.3
tzdata==2023.3

デプロイ前にdjango側でやっておいた方が良いこと

git ignore

いきなりgit cloneしたい所だがローカルファイルとは同期したくないファイルを除外しておく。

これは何気に結構重要だと思っていてローカルのキャッシュで思わぬ動作をしたりデバッグでも苦労する羽目になりかねないので必ずやっておこう。

# .gitignore file for Django projects

# Django
*.log
*.pot
*.pic
__pycache__/
logs/
db.sqlite3
db.sqlite3-journal
media

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Docker
db-volumes/

このサイトで自動的に作ることもできるが想定外の物まで除外されるとめんどうなのでそのままコピペはやめた
https://www.toptal.com/developers/gitignore/

もうコミットしちゃってる場合
例えばリポジトリ全体から pycache フォルダを削除するには、リポジトリのルートから以下のコマンドを実行する。

find . -name '__pycache__' -type d -exec git rm -r --cached {} \;

 # logファイルの場合
find . -name '*.log' -type f -exec git rm --cached {} \;

この上でコミットすればリモート側も削除されているはず。

settings.pyを開発用と本番環境で分ける

以下の記事を参考にsettings.pyは分割している

your_project/
└── your_project/
    ├── settings/
    │   ├── __init__.py
    │   ├── base.py
    │   ├── local.py
    │   └── production.py
    ├── __init__.py
    ├── urls.py
    └── wsgi.py

https://self-methods.com/django-settings-split/

django-environを使用して環境変数を設定する

機能の差で言うとこんな感じらしい。
django-environ > django-dotenv
単純に検索ボリュームでもdjango-environの方が人気そうなので採用する。

django-environを使ってDjangoプロジェクトで環境変数を管理するための手順を以下に示します。

  1. ローカル環境でdjango-environをインストールします。ターミナルで次のコマンドを実行してください。

    pip install django-environ
    
  2. これを他の環境にも適用するため、依存関係をrequirements.txtに追加します。ターミナルで次のコマンドを実行してください。

    pip freeze > requirements.txt
    
  3. Djangoの設定ファイル(通常はsettings.py)を開き、django-environを使用するようにします。ファイルの先頭に次のコードを追加してください。

    import environ
    import os
    
    # 環境変数の読み込み
    env = environ.Env(
    	# デフォルト設定しないとエラーになるので
    	DEBUG=(bool, False)
    )
    # .env fileの読み込み
    environ.Env.read_env(env_file=os.path.join(BASE_DIR, '.env'))
    

    このコードはdjango-environをインポートし、.envファイルを読み込むためのenvオブジェクトを作成します。DEBUGのような環境変数には、キャスティング(型変換)とデフォルト値を設定することができます。

  4. .envファイルを作成し、そこに環境変数を記述します。例えば:

    DEBUG=True
    SECRET_KEY=your-secret-key
    DATABASE_URL=postgres://user:password@localhost:5432/mydatabase
    
  5. settings.py内で環境変数を参照するときは、envオブジェクトを使います。例えば:

    DEBUG = env('DEBUG')
    SECRET_KEY = env('SECRET_KEY')
    DATABASES = {
        'default': env.db(),  # Reads the DATABASE_URL environment variable and convert it to Django's DATABASES setting format
    }
    

これで、django-environを使ってDjangoプロジェクトで環境変数を管理することができるようになります。本番環境でも同じ手順を行い、.envファイルに本番環境用の設定を記述してください。ただし、.envファイルはセキュリティ上の理由からリモートリポジトリには含めないでください。

gitの設定

セキュリティ上、各リポジトリに対して異なるSSHキーを設定することがベストプラクティスです。それにより、もし一つのキーが何らかの理由で漏洩した場合でも、その影響を最小限に抑えることができます。

以下に、新たにSSHキーを作成し、それをGitHubに登録する手順を示します。ここでは、新たなキーの名前として.ssh/github_yourprojectを使用しますが、適宜自身の環境に合わせて読み替えてください。

  1. サーバー上で新たなSSHキーを生成します。キーの名前には特定のリポジトリを識別できるものを設定します。以下のコマンドを実行します:

ホームディレクトリに移動するには、以下のコマンドを使います:

cd
ssh-keygen -t ed25519 -C "your_email@example.com" -f .ssh/github_yourproject

ここで、-Cオプションにはあなたのメールアドレスを設定します。これはキーのラベルとして機能します。また、-fオプションでキーのファイル名を指定します。

  1. 新たに生成した公開鍵(ここでは.ssh/github_yourproject.pub)を表示します。以下のコマンドを実行します:
cat .ssh/github_yourproject.pub
  1. 表示された公開鍵をコピーします。

  2. GitHubのウェブサイトに移動し、yourprojectリポジトリの設定画面を開きます。

  3. 左側のメニューから"Deploy keys"を選択します。

  4. 右上の"Add deploy key"ボタンをクリックします。

  5. "Title"フィールドにはキーの名前を、"Key"フィールドには先ほどコピーした公開鍵を貼り付けます。

  6. 必要に応じて"Allow write access"のチェックボックスを選択し、"Add key"ボタンをクリックします。

これで新しいSSHキーがGitHubのリポジトリに追加されました。これにより、サーバーからGitHubのリポジトリに対する認証にこのキーを使用することができます。

最後に、新しいキーを使用してリポジトリをクローンするための設定を行います。~/.ssh/configファイルを開き、以下の設定を追加します:

Host github.com-yourproject
  HostName github.com
  User git
  IdentityFile ~/.ssh/github_yourproject

この設定により、github.com-yourprojectというホスト名で接続することができます。

接続テスト

ssh -T git@github.com-yourproject

Hi ユーザー名/yourproject! You've successfully authenticated, but GitHub does not provide shell access.

任意のディレクトリに移動してclone

git clone git@github.com-yourproject:<username>/yourproject.git

開発用サーバーが動くか確認

このまま本番環境で公開するのはもちろん駄目だけどwebサーバー入れる前に動くかどうか確認しないと
問題の切り分けが難しいのでcloneしたコードが動くか確認したいよね。

コマンドを打つ前にFWのポートを開放しておこう

$ sudo ufw allow 8000
Rule added
Rule added (v6)

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: reject (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
8000                       ALLOW IN    Anywhere
8000 (v6)                  ALLOW IN    Anywhere (v6)

開発サーバーの起動

python manage.py runserver 0.0.0.0:8000

これで問題なければ http://ipアドレス:8000 でブラウザから確認できる。
エラーがあればコマンド実行した時点で色々でるので対処すると良い。

ローカルと本番環境でsettings.pyを切り分けている場合はlocal用の設定は以下に書き変えれば良い。
本番環境のはだめだよ。

ALLOWED_HOSTS = ['*']

大体、ignore対象の.envファイルかlogsディレクトリが無いみたいなエラーかも知れない。
停止は ctrl + c ですぐにできる。

webサーバーの構成

今は Nginx、gunicorn、Django の組み合わせがモダンな開発手法とのこと。
関係性はざっくりこんな感じ。

[Client]
         ↓ (HTTPS/HTTP)
[Nginx]
         ↓ (HTTP)
[Gunicorn]
         ↓ (WSGI)
[Django Application]
  1. Client(クライアント):クライアントは通常、ユーザーが使用するウェブブラウザやモバイルアプリなどのエンドユーザーのデバイス上で動作するアプリケーションです。クライアントはNginxにリクエストを送信し、レスポンスを受け取ります。

  2. Nginx(リバースプロキシ):Nginxは、クライアントからのリクエストを受け取り、適切なバックエンドサーバー(この場合、Gunicorn)にそのリクエストを転送します。また、バックエンドサーバーからのレスポンスをクライアントに戻します。これにより、一つのクライアントリクエストが複数のバックエンドサーバーに分散されることでサーバーの負荷を軽減したり、バックエンドサーバーが直接的な外部からの攻撃に晒されるのを防いだりします。

  3. Gunicorn(アプリケーションサーバー):GunicornはWSGIサーバーとして動作し、Django ApplicationとHTTPベースの通信を行います。つまり、NginxからのリクエストをDjango Applicationが理解できる形式に変換し、Django ApplicationからのレスポンスをHTTPレスポンスに変換してNginxに返します。

  4. Django Application(Webアプリケーション):これは具体的なWebアプリケーションで、ビジネスロジックやデータベースとのインタラクションなど、アプリケーション固有の作業を処理します。Django ApplicationはGunicornを通じてリクエストを受け取り、レスポンスを生成します。

これら全体がバックエンドシステムを形成し、最終的にクライアントに対してWebサービスを提供します。

こちらの記事が非常に参考になりました
https://snowtree-injune.com/2020/10/28/about-nginx-dj013/
https://snowtree-injune.com/2020/10/30/nginx-static-dj014/
https://snowtree-injune.com/2020/10/30/nginx-static-dj015/

Nginxのインストール

# ファイアウォールの設定をする
$ sudo ufw status    # 一覧を表示
$ sudo ufw allow 80  # httpを開放
$ sudo ufw allow 443 # httpsを開放

# Nginxのインストール
$ sudo apt install nginx

http://ipアドレス/ にアクセスで
Welcome to nginx!が表示されるはず。

バージョン確認

$ nginx -v
nginx version: nginx/1.18.0 (Ubuntu)

ドキュメントルートの確認をしてみる

grep "root /" -r /etc/nginx/

/etc/nginx/sites-available/default:	root /var/www/html;
/etc/nginx/sites-available/default:#	root /var/www/example.com;

/var/www/htmlを覗くとindex.nginx-debian.htmlがあるこれがWelcome to nginx!を表示している。

$ ls -al /var/www/html
total 12
drwxr-xr-x 2 root root 4096 Jun 16 17:35 .
drwxr-xr-x 3 root root 4096 Jun 16 17:35 ..
-rw-r--r-- 1 root root  612 Jun 16 17:35 index.nginx-debian.html

Nginxの設定ファイルは主に二つのディレクトリに存在します:/etc/nginx/nginx.conf/etc/nginx/sites-available/です。これら二つの役割は次のとおりです:

  • /etc/nginx/nginx.conf: これはNginxのメイン設定ファイルです。全体の設定、モジュール設定、http、mail、stream各ブロックの設定など、Nginxサーバーの全般的な設定が含まれています。

  • /etc/nginx/sites-available/: このディレクトリは、ウェブサイトごとのサーバー設定を含んでいます。各ウェブサイトごとに個別の設定ファイルを作成することで、それぞれのサイトに対して異なる設定を適用することが可能になります。また、これらの設定ファイルは通常、/etc/nginx/sites-enabled/ディレクトリにシンボリックリンクとして配置され、その設定が有効化されます。

したがって、nginx.confはNginx全体の設定を行い、sites-availableは個々のウェブサイトに特化した設定を行うために使用します。


この違いを理解した上で設定していこう。

歴史的な背景なのかはよくわからないが、Apacheの場合一般的なドキュメントルートは/var/www/html/だった。
一方、Nginxの場合は、/usr/share/nginx/htmlを一般的なドキュメントルートらしい。

その名残で /etc/nginx/sites-available/default のデフォルト設定は /var/www/html/ なのかな?
最初から /usr/share/nginx/html にしてくれた方がわかりやすいと思うんだが。
/var/www/html/にしちゃうとApache使ってんだかNginx使ってんだかわかりづらいのでドキュメントルートは/usr/share/nginx/htmlにしておきたいと思う。

静的ファイル、メディアファイルの配置ディレクトリ

$ cd /usr/share/nginx/html
$ sudo mkdir static
$ sudo mkdir media

# 所有者を変える
$ sudo chown ユーザー名 static
$ sudo chown ユーザー名 media

バーチャルホストの設定

バーチャルホスト名は自身が分かりやすければなんでも良い。

server0 →  想定外のアクセスの設定(不正アクセス対策でもある)
app  →  アプリのホストの設定

$ cd /etc/nginx/sites-available
$ sudo touch server0
$ sudo touch app
$ sudo chown ユーザー名 server0
$ sudo chown ユーザー名 app

# 確認
$ ls -al
total 12
drwxr-xr-x 2 root        root 4096 Jun 17 15:08 .
drwxr-xr-x 8 root        root 4096 Jun 16 17:35 ..
-rw-r--r-- 1 root        root 2412 Jul 27  2022 default
-rw-r--r-- 1 ユーザー名 root    0 Jun 17 15:05 server0
-rw-r--r-- 1 ユーザー名 root    0 Jun 17 15:08 app

/etc/nginx/sites-available/server0 のファイルを設定

# ファイル編集
vi /etc/nginx/sites-available server0

#設定ファイル名:server0
server {
    listen 80 default_server;
    server_name _;  #すべてのリクエストに対応する。
    return 444;  #ステータスコード444を返す。
}

/etc/nginx/sites-available/app のファイルを設定

# 設定ファイル名:app
server {
	listen 80;  #ポート番号の指定http
	server_name ***.***.***.***;  #IPアドレス(ドメイン)の指定
	root /usr/share/nginx/html;  #ドキュメントルートの指定

	# index.html等はファイル名の指定なしで実行
	index index.html index.htm index.nginx-debian.html;
    
	location /static {
                #.htmlは省略できる
		try_files $uri $uri/ $uri.html =404;
	}

	location /media {

	}

    # djangoにデータを引き渡す設定
    # この時点のテストでトップページを表示する場合コメントアウトしないと502エラーになる
    location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X_Forwarded-Proto $scheme;

            proxy_pass http://127.0.0.1:8000;
        }
}

作成したファイルにシンボリックリンクを貼って反映させていく

# defailtのシンボリックリンクを削除
$ sudo unlink /etc/nginx/sites-enabled/default

# シンボリックリンクを貼る
$ sudo ln -s /etc/nginx/sites-available/server0 /etc/nginx/sites-enabled/server0
$ sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app

# シンボリックリンクの確認
$ ls -l /etc/nginx/sites-enabled/server0
lrwxrwxrwx 1 root root 34 Jun 17 15:17 /etc/nginx/sites-enabled/server0 -> /etc/nginx/sites-available/server0

Nginxをリロードして設定を反映させる

# 状態確認
$ systemctl status nginx.service

# リロード
$ sudo systemctl reload nginx.service

/usr/share/nginx/html配下にtest.htmlを置いていると
URLが不正ですhttp://123.456.78/static/test.html
で確認できるはず。

参考リンク
https://snowtree-injune.com/2020/10/30/nginx-static-dj014/
https://flat-kids.net/2020/12/24/【トラブルメモ】nginxで不正なドメインでのアクセ/

静的なファイルを集める

集めるって言うとアレだけど色んなとこにあるstaticディレクトリ配下のファイルを一箇所にコピーしてるみたい。
settings.pyに設定する静的ファイルディレクトリ

STATIC_ROOT = '/usr/share/nginx/html/static'
MEDIA_ROOT = '/usr/share/nginx/html/media'

djangoプロジェクト内にある静的ファイルをNginxが静的ファイルを見れるところに集めてくれる

# 仮想環境へ切り替え
$ conda activate 仮想環境名
$ cd /home/ユーザ名/プロジェクト名(manage.pyがあるところ)
$ python manage.py collectstatic

You have requested to collect static files at the destination
location as specified in your settings:

    /usr/share/nginx/html/static

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel: yes

127 static files copied to '/usr/share/nginx/html/static'.

gunicornのインストールと設定

# pipのアップデート
$ python -m pip install -U pip

# gunicornのインストール
$ python -m pip install gunicorn

$ cd /home/ユーザ名/プロジェクト名(manage.pyがあるところ)

# バックグラウンドでgunicornを起動
$ gunicorn --bind 127.0.0.1:8000 プロジェクト名.wsgi -D

おかしい時は以下で確認

# gunicornの停止
$ pkill gunicorn

# gunicornのプロセスの確認
$ ps ax | grep gunicorn

これでエラーがなければVPSのIPアドレスにアクセスすればあなたのプロジェクトが表示されるはずだ。

ちなみに筆者はwsgi.pyに環境変数が取得できていないバグで少しはまった。
一回プロジェクトをkillして起動し直したら反映した。

pkill gunicorn
gunicorn --bind 127.0.0.1:8000 プロジェクト名.wsgi -D

ドメインの設定

今回はエックスサーバーで運用中のドメインにサブドメインを切ってconohaのVPSに適用する。
別にIP運用でも問題ないのだがドメインを設定しないとLets encryptのsslが使えないらしいので。

エックスサーバー側でサブドメインを作成して、DNS設定からAレコードを追加

conoha VPSで「DNS」からそのサブドメインを追加

Nginxの/etc/nginx/sites-available配下の対象のバーチャルホスト設定にサブドメイン名を設定

server {
    ・・・
    server_name サブドメイン名;
    ・・・
    location / {
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X_Forwarded-Proto $scheme;

            proxy_pass http://127.0.0.1:8000;
    }
    ・・・
}

設定を再読み込み

# Nginx
sudo systemctl reload nginx.service

# gunicorn
pkill gunicorn
gunicorn --bind 127.0.0.1:8000 プロジェクト名.wsgi -D


Djangoプロジェクトのsettings.py(自分の場合はproduction.py)のALLOWED_HOSTSにサブドメインを追加

SSL化

色々な情報があるが2023年6月時点ではpython3-certbot-nginxを使用するのが最もシンプルみたいなので採用する。

基本的にはこのコマンドでできた。

sudo apt update
sudo apt install python3-certbot-nginx

# -m以降は途中で聞かれる色々をスキップするオプション
sudo certbot --nginx -d ドメイン名 -m メールアドレス  --agree-tos

指定したドメイン名が書かれている /etc/nginx/sites-available/バーチャルホスト名 に以下のコードが自動的に追加される。

このコードは後に記述する不正アクセス対策に使う。

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/ドメイン名/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/ドメイン名/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

これであっさりSSL化できたのだが、問題が発生した。
POSTのテストをすると

アクセス禁止 (403)
CSRF検証に失敗したため、リクエストは中断されました。

Help
Reason given for failure:

    Origin checking failed - https://ドメイン名 does not match any trusted origins.
    
In general, this can occur when there is a genuine Cross Site Request Forgery, or when Django’s CSRF mechanism has not been used correctly. For POST forms, you need to ensure:

Your browser is accepting cookies.
The view function passes a request to the template’s render method.
In the template, there is a {% csrf_token %} template tag inside each POST form that targets an internal URL.
If you are not using CsrfViewMiddleware, then you must use csrf_protect on any views that use the csrf_token template tag, as well as those that accept the POST data.
The form has a valid CSRF token. After logging in in another browser tab or hitting the back button after a login, you may need to reload the page with the form, because the token is rotated after a login.
You’re seeing the help section of this page because you have DEBUG = True in your Django settings file. Change that to False, and only the initial error message will be displayed.

You can customize this page using the CSRF_FAILURE_VIEW setting.

chatGPTに聞くと


Django 1.9以降では、CSRF保護メカニズムはリクエストのオリジンをチェックし、それがsettings.pyで設定されたCSRF_TRUSTED_ORIGINSリストに含まれているかどうかを確認します。リクエストのオリジンがこのリストに含まれていない場合、DjangoはCSRFエラーを引き起こします。

この問題を解決するためには、settings.pyのCSRF_TRUSTED_ORIGINSリストにドメイン名を追加すると良いでしょう。

例えば以下のように設定します:

CSRF_TRUSTED_ORIGINS = ['ドメイン名']

なるほどね、とそのままやると解決できなかった。
これには2つ罠があり記述間違いとDEBUG環境がTrueだと無視されると言うことだ。
いや、そんなんわかんねえよ。。

記述違いはこう訂正する。

CSRF_TRUSTED_ORIGINS = ['https://ドメイン名']

AIに間違いを指摘したが2021年9月まではそうだったし!ってキレ返されたのでそうだったんだろう。

settings.py

DEBUG = False

不正アクセス対策に追加

ssl化したので不正アクセス対策にもhttpsの対応を追加する
Nginxインストール(バーチャルホスト)のセクションで作成した /etc/nginx/sites-available/server0 を編集する。

$ vi /etc/nginx/sites-available/server0

httpsのブロックを追加
証明書のパスは環境によって異なると思うので自身の環境い合わせてください。

#設定ファイル名:server0

# http
server {
    listen 80 default_server;
    server_name _;  #すべてのリクエストに対応する。
    return 444;     #ステータスコード444を返す。
}

# https
server {
    listen 443 ssl default_server;
    server_name _;  #すべてのリクエストに対応する。

    ssl_certificate /etc/letsencrypt/live/ドメイン名/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/ドメイン名/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    return 444;     #ステータスコード444を返す。
}

参考リンク
https://snowtree-injune.com/2020/12/10/https-dj017/
https://book-reviews.blog/specific-domain-errors-on-Django/

ベーシック認証をかける

htpasswdというツールを使用します。
このツールはApacheのパッケージの一部として提供されていますが、Nginxのパスワードファイルの作成にも使用できます。

# インストール
sudo apt-get install apache2-utils

# .htpasswdと言うファイルを作成する
sudo htpasswd -c /etc/nginx/.htpasswd ベーシック認証のユーザー名
New password:[ベーシック認証のパスを入力]
Re-type new password:

# ベーシック認証を掛けるよーってnginxの設定に教えてあげる
vi /etc/nginx/sites-available/バーチャルホスト名

# 認証の行を追加
server {
        server_name ドメイン名;   # VPSサーバーのドメインを指定

        # ドキュメントルートの指定(静的ファイル、メディアファイルの保存先)
        root /usr/share/nginx/html/;

        # index.html等はファイル名の指定なしで実行
        index index.html index.htm index.nginx-debian.html;

        # 認証
        auth_basic "Restricted Content";
        auth_basic_user_file /etc/nginx/.htpasswd;

# 整合性を確認してリスタート
sudo nginx -t
sudo systemctl restart nginx

https://rin-ka.net/nginx-basic-auth/

デプロイ後のメンテナンス

phpと違うところはメンテンス性が結構面倒だなと感じた

ソースコードの変更

# 仮想環境を有効化
conda activate 仮想環境名

# .gitやrequirementsがあるディレクトリに移動
cd プロジェクト名

# gitubからrequirements取得
git pull

ローカルでライブラリを追加した場合

# ローカルマシンで書き出し
$ pip freeze > requirements.txt

サーバーにログインして反映させる作業が必要

# 仮想環境を有効化
conda activate 仮想環境名

# .gitやrequirementsがあるディレクトリに移動
cd プロジェクト名

# gitubからrements.txt取得
git pull

# 反映
$ pip install -r requirements.txt

静的ファイルの追加

# 仮想環境を有効化
$ conda activate 仮想環境名

# .gitやmanage.pyがあるディレクトリに移動
$ cd プロジェクト名

# gitubから静的ファイルの更新を取得
$ git pull

# 反映
$ python manage.py collectstatic

今回、github actionsでデプロイを自動化しなかったのは結局サーバー入って
あれこれしなきゃならなそうだったので意味なさそうだったのもある。

他のメンテナンスすべき事項はこんな感じらしい。
バックアップに関してはconohaのイメージバックアップに頼ろうと思うが、他の項目は随時更新する。


サーバー上でのデプロイ後のメンテナンスとしては以下のような作業が考えられます:

  1. データベースのマイグレーション:ローカルでデータベースのスキーマを変更した場合(新しいモデルを追加したり、既存のモデルを変更したりした場合)、それらの変更をサーバーのデータベースにも反映する必要があります。Djangoではこれを python manage.py makemigrationspython manage.py migrate コマンドで行います。

  2. サーバーソフトウェアのアップデート:セキュリティパッチや新機能がリリースされたとき、サーバー上のソフトウェア(Nginx、gunicorn、Pythonなど)をアップデートすることがあります。これは通常、パッケージマネージャ(apt、yumなど)を使用して行います。

  3. ログの確認:サーバー上で問題が発生した場合、ログファイルを確認して問題を診断することがあります。これにはサーバーのシステムログ、アプリケーションのログ(Djangoのログなど)、データベースのログなどがあります。

  4. バックアップ:データロスを防ぐため、定期的にデータベースや重要な設定ファイルのバックアップを取ることが重要です。

  5. モニタリングとアラート:サーバーのパフォーマンスを監視し、異常が発生したときに通知を受け取るためのシステムを設定することもあります。

これらのタスクは、システムの状況や要件により異なります。また、自動化ツール(AnsibleやTerraformなど)を使用してこれらのプロセスを自動化することも一般的です。


LAMP環境の開発は慣れているものの、PythonもDjangoもまだ始めて10日くらいかつ初めてのデプロイで知らないアプリばかりで苦労したがかなり勉強になった。

以上で目的は達成されました。
お疲れ様でした。

重すぎてタイムアウトで502エラー対応

めちゃめちゃ処理が重いアプリをデプロイした時に502 Bad Gatewayが発生して困った。
このエラーは.envに変数追加忘れとかいろんな要因で出た。

その時の対処が以下だ。

まずは cat /var/log/nginx/error.log でエラーを見よう。

Gunicornをコマンドライン引数でカスタマイズすることができます。具体的には、--timeoutオプションを使用してタイムアウトの時間を指定できます。

例えば、タイムアウトを120秒に設定したい場合、以下のようにGunicornを起動できます。

gunicorn --bind 127.0.0.1:8000 --timeout 120 プロジェクト名.wsgi

また、Gunicornをサービスとして実行している場合は、サービスの設定ファイル(通常は /etc/systemd/system/ ディレクトリ内にある)を編集して、Gunicornのコマンドライン引数を変更する必要があります。

これと同様に、Nginxのタイムアウト設定も確認する必要があります。Nginxの設定ファイルは通常、/etc/nginx/nginx.conf または /etc/nginx/sites-available/ ディレクトリ内にあります。このファイルを編集して、以下のように proxy_read_timeoutproxy_connect_timeout の値を調整できます。

http {
    ...
    
    server {
        ...
        
        location / {
            ...
            proxy_read_timeout 120s;
            proxy_connect_timeout 120s;
            ...
        }
    }
}

変更したら、NginxとGunicornを再起動してください。

その他の参考記事

https://baapuro.com/Django/four/
https://blowup-bbs.com/webserver-interface-summary/
https://it-biz.online/it-skills/vps-python/
https://qiita.com/narunblog/items/eb20a7f6e5a705a6f0b2
https://denno-sekai.com/conoha-vps-django/
https://snowtree-injune.com/django-series-dj000/

Discussion