【Django × Apache】「Internal Server Error」の原因はsettings.pyに！ EC2サーバーでのアプリ反映に失敗した場合の原因と解決策

# 概要
* 稼働中のDjangoアプリケーションをバージョンアップしました。ローカル環境で動作確認し問題なかったので、EC2サーバーでも`git pull`して反映...しようとしたら、なぜか画面が`Internal Server Error`になってしまいました。
* 結論、パーミッションの問題だったので、同様の悩みを抱える方の一助になればと思い、まとめておきます。

## 環境
* サーバー（AWS EC2インスタンス）
    * OS：Ubuntu 18.04.3 LTS
    * Web Server：Apache/2.4.29
    * Django version: 3.0.5

# エラー内容
* EC2サーバーで`git pull`してアプリを更新したら以下の画面になってしまいました。
* こんな画面です。Djangoの黄色いエラー画面ならともかく...これめちゃ焦るやん💦

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/ac06b850-1db2-e282-f0d0-1113ba6d34a0.png)

# 原因の追求
* Apacheの`error.log`やDjangoのlogファイルを見ても、原因がわかりません。Apacheはちゃんと起動していました。
* ローカル環境ではちゃんと動いていたので、やっぱりApache周りの問題だろうか...
* ちなみにApacheの設定ファイルなどは一切いじっていません。Djangoのソースコードを更新しただけです。
* `$ free -h`で確認しましたが、メモリ不足でもありませんでした。
* 原因は意外なところにありました。djangoの`settings.py`でした...

## 前提
* 構造は以下の通り。

```
/var/www/html/sample_homepage/
└── homepage
    ├── homepage
    │   └── settings.py
    └── loggings
```

* `sample_homepage`パーミッションを確認したところ、以下のようになっていました。

```terminal
$ ls -l /var/www/html
drwxrwxr-x 10 root     www-data      4096 Apr  6 11:55 sample_homepage
```

# 解決方法
* `settings.py`を以下のように変更してあげる必要がありました。
* 以下が問題だった箇所

```py
#(省略)
log_filename = 'loggings/log_file_' + datetime.now().strftime("%Y-%m-%d") + '.log'

if not os.path.exists("loggings"):
    os.makedirs("loggings")
    #(省略)
```

↓
* 以下のように変更してあげます

```py
#(省略)
log_filename = '/var/www/html/sample_homepage/homepage/loggings/log_file_' + datetime.now().strftime("%Y-%m-%d") + '.log'
if not os.path.exists("/var/www/html/sample_homepage/homepage/loggings"):
    os.makedirs("/var/www/html/sample_homepage/homepage/loggings")
```

* 具体的には、相対パスから絶対パスにしています。

# 原因
* Internal Server Errorになる理由は、Pythonスクリプトがloggingsディレクトリを作成しようとした際に、Apacheがアクセス権限の問題を引き起こしているからでした。そのため、以下が解決策になります。
    * (1) 相対パス(`"loggings"`)を使わずに絶対パス(`/var/www/html/...`)を使う（=上述の解決策）
    * (2) 適切なアクセス権限を設定する（=この場合は以下3通りの方法がある）
        * ①`"www-data"`ユーザーにログファイルの保存先を変更する
        * ②`"www-data"`ユーザーに適切なアクセス権を与える
        * ③`"root"`ユーザーにも書き込み権限を与える

## (1) 「相対パスを使わずに絶対パスを使う」をもう少し詳しく補足
* まず、大前提、以下の違いがあります。
    * `"/var/www/html/sample_homepage/homepage/loggings"`：絶対パス。ファイルシステムのルートからの完全なパス。
    * `"loggings"`：相対パス。現在の作業ディレクトリからの相対的なパス。
* `os.makedirs()`関数を使用して新しいディレクトリを作成する場合
    * 相対パスを使用する場合：ディレクトリが作成される場所は現在の作業ディレクトリに依存
    * 絶対パスを使用する場合：ディレクトリが作成される場所は絶対パスに依存
* つまり、
    * 絶対パスを使用→`"www-data"`ユーザーがアクセス権限を持っている場所にディレクトリを作成できる→成功
    * 相対パスを使用→現在の作業ディレクトリがアプリケーションのディレクトリである場合でも、`"www-data"`ユーザーがアクセス権限を持っている場所にディレクトリを作成することはできない（`"www-data"`ユーザーがアクセス権限を持っている場所≠アプリケーションのディレクトリとは異なる場合がある）→失敗

### 相対パス失敗パターンの例
* 「`"www-data"`ユーザーがアクセス権限を持っている場所≠アプリケーションのディレクトリとは異なる場合がある」と言うのは、例えば以下のような形です。
    * アプリケーションが`"/var/www/html"`に配置されている
    * `"www-data"`ユーザーが`"/var/www/html/sample_homepage"`に書き込み権限を持っている
    * `settings.py`で相対パス`"loggings"`と指定
    * 実際には`"/var/www/html/sample_homepage/loggings"`というパスが解決されてしまう
    * `"www-data"`ユーザーが書き込み権限を持っているのは`"/var/www/html/sample_homepage"`であり、`"/var/www/html/sample_homepage/loggings"`には書き込み権限を持っていない(可能性がある)ため、`os.makedirs()`関数が失敗

### 相対パスを使う場合は気をつけよう
* 上記の例のように、相対パスの場合は、`os.makedirs()`関数が実行される場所からloggingsディレクトリにアクセスしようとします。しかし、この場合、`os.makedirs()`関数が実行されるのはApacheの実行時であり、通常は`/var/www/html`ディレクトリになります。つまり、os.makedirs()関数が実行される場所もこのディレクトリ内になるため、loggingsディレクトリもこのディレクトリ内に作成されます。しかし、`os.makedirs()`関数がディレクトリを作成するとき、そのディレクトリの所有者は実行したプロセスのユーザーになります。つまり、`root`ユーザーが`os.makedirs()`関数を実行したため、loggingsディレクトリの所有者も`root`ユーザーになってしまいます。しかし、Apacheの実行ユーザーは`www-data`グループに所属しています。つまり、`www-data`グループがアクセスできない`root`ユーザー所有のディレクトリやファイルは、Apacheからアクセスすることができないため、内部サーバーエラーが発生したのです。

* 絶対パスの場合は、ファイルやディレクトリにアクセスする場所が確実に指定されます。したがって、`/var/www/html/sample_homepage/homepage/loggings`ディレクトリが存在しない場合でも、`os.makedirs()`関数によって作成されたディレクトリの所有者は`root`ではなく、`www-data`グループになります。このため、Apacheの実行ユーザーである`www-data`グループがアクセスできるようになります。


* 相対パスを使用する場合には、アプリケーションが配置されているディレクトリよりも上位のディレクトリに対して`"www-data"`ユーザーが書き込み権限を持っていることを確認し、適切なパーミッションを設定する必要があります。
* 絶対パスを使用することで、`"www-data"`ユーザーがアクセス権限を持っている場所にディレクトリを作成することができます。


## (2) 「適切なアクセス権限を設定する」をもう少し詳しく補足
* `os.makedirs()`関数は、指定されたパスに新しいディレクトリを再帰的に作成する関数です。この関数を使用すると、指定されたパスにディレクトリが存在しない場合、新しいディレクトリを作成することができます。
* Apacheは、ユーザー`"www-data"`で動作しており、`"www-data"`ユーザーがディレクトリに書き込み権限を持っていない場合、`os.makedirs()`関数が失敗し、Internal Server Errorが発生します。ディレクトリに書き込み権限がないため、新しいディレクトリを作成できないためです。
* 上述の通り、`ls -l`で確認したところ、以下のようになっていました。

```terminal
drwxrwxr-x 10 root     www-data      4096 Apr  6 11:55 sample_homepage
```

* 所有者が`"root"`ユーザーで、グループ所有者が`"www-data"`になっています。つまり、`"www-data"`ユーザーはこのディレクトリに対して書き込み権限を持っていますが、`"root"`ユーザーは書き込み権限を持っていません。ログファイルの保存先としてこのディレクトリを使用する場合、`"root"`ユーザーがログファイルを作成することができないため、エラーが発生するわけですね。なので、以下のどれかを行う必要があります。
    * ①`"www-data"`ユーザーにログファイルの保存先を変更する
    * ②`"www-data"`ユーザーに適切なアクセス権を与える
    * ③`"root"`ユーザーにも書き込み権限を与える

### ①`"www-data"`ユーザーにログファイルの保存先を変更するコマンド
* ログファイルの保存先を変更するには、例えば、以下のコマンドで可能です。

```terminal
sudo mkdir /var/www/html/logs
sudo chown www-data:www-data /var/www/html/logs
```

* この場合、アプリケーションのログファイルの保存先を`"/var/www/html/logs"`に変更すればOKです

### ②`"www-data"`ユーザーに適切なアクセス権を与えるコマンド
* 以下のコマンドで、指定したディレクトリ以下のすべてのファイルとディレクトリの所有者を`"www-data"`ユーザーとグループに変更することができます。

```terminal
sudo chown -R www-data:www-data /var/www/html/sample_homepage
```

### ③`"root"`ユーザーにも書き込み権限を与えるコマンド(最終手段)
* 以下のコマンドにより、他のユーザーに対して書き込み権限が与えられます。ただし、一般的にはセキュリティ上の理由から、Webアプリケーションを実行するためのディレクトリやファイルに対してrootユーザーに書き込み権限を与えることは推奨されていません。あくまで一つの方法として記載します。これは最終手段です。

```terminal
sudo chmod o+w /var/www/html/sample_homepage
```

## その他：アプリケーションの実行ユーザーを確認する（Ubuntuの場合）
* アプリケーションの実行ユーザーが期待しているユーザーではない可能性もあります。その場合は、以下を確認してください。

```terminal
cd /etc/apache2
cat envvars
```

* 以下のように設定されていればOKです。

```terminal
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
```

* パーミッションまわりは奥が深いですね。。