Web3層構造 on Docker  ~nginx, django, postgreSQL の連携~

# はじめに
### 今回作成するもの
![Group_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431877/02c07fdb-0993-276a-4449-e66ca782199d.png)
### 事前準備
・パブリックにアクセス可能なサーバ（本記事内の作業ではEC2インスタンスを使用。OSはAmazon Linux 2を選択。）
・Python3系のインストール（本記事内の作業では 3.8.9 のバージョンを使用。記事の後半に`pyenv`経由のインストールの手順あり。）

# docker-compose の動作確認 （nginx を起動するまで）
### dockerのインストール
EC2インスタンスにsshでログインし、下記コマンドを実行してdockerをインストールする。
また、docker の起動 (`start`) 及びインスタンス起動時に自動で起動 (`enable`) できるようにする。

```zsh
sudo yum install -y docker
sudo systemctl start docker
sudo systemctl enable docker
sudo docker info # 確認
```

### ec2-user を docker グループに追加する

これにより `sudo` なしで `docker` コマンドが打てるようになる。
また、[docker 公式ドキュメント](https://docs.docker.com/engine/install/linux-postinstall/)にあるように

>If testing on a virtual machine, it may be necessary to restart the virtual machine for changes to take effect.

グループ追加後にインスタンスを再起動する。

```
sudo usermod -aG docker $USER
cat /etc/group | grep docker # 確認
sudo reboot # 再起動
```

### docker-composeのインストール
[docker 公式ドキュメント](https://docs.docker.com/compose/install/#install-compose-on-linux-systems)を確認し、version を決定する（今回は`1.29.2`）。

```zsh
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose version # 確認
```

### docker-compose からコンテナの起動を確認する
docker-composeでnginxのイメージを取得してテストページを表示してみる。

```yml:docker-compose.yml
version: '3.7'
services:
    nginx:
        container_name: nginx
        image: nginx:latest
        ports:
            - 80:80
```

以下コマンドを実行しコンテナを立ち上げる。

```zsh
docker-compose up -d
```

EC2インスタンスのIPアドレスをブラウザのURLバーに入れて表示してみる。
![Screen Shot 2021-06-20 at 12.21.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431877/7afc6e94-4974-7843-ef90-031f4c9b1614.png)
上のようなnginxのテストページが見えていればOK。
下記コマンドで一旦終了させる。

```zsh
docker-compose down
```

# 3層アーキテクチャを構築する

### nginxコンテナの準備（Webサーバ）

ディレクトリ構造
>containers/
　├ django/
　│　└ ...
　├ nginx/
　 　└ conf/
 　　　　└ nginx.conf
 　　 └ uwsgi_params
　├ docker-compose.yml



volumeの割り当てと依存関係を追加する。


```yml:docker-compose.yml
version: '3.7'
services:
    nginx:
        container_name: nginx
        image: nginx:latest
        volumes: # add
            - ./nginx/conf:/etc/nginx/conf.d # add
            - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params # add
        ports:
            - 80:80
        depends_on: # add
            - django # add

```

nginx.conf を作成する。
今回は upstream ディレクティブの記述にあるように django コンテナの8000番ポートへ通信を流す設定を行う。

```conf:nginx.conf
upstream django {
    server django:8000;
}

server {
    listen 80;
    location / {
        uwsgi_pass django;
        include /etc/nginx/uwsgi_params;
    }
}
 ```

wsgiの通信で必要とされる uwsgi_params の作成を行う（[参考](https://uwsgi-docs.readthedocs.io/en/latest/Nginx.html)）。

```:uwsgi_params
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_ADDR $server_addr;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;
```

### djangoコンテナの準備（アプリケーションサーバ）

ディレクトリ構造
>containers/
　├ django/
　│　└ startup
　│　　　└ setuser.sh
　│　　　└ startup.sh
　│　└ uwsgi
　│　　　└ uwsgi.ini
　│　└ Dockerfile
　│　└ requirements.txt
　├ nginx/
　│　└ ...
　├ docker-compose.yml


djangoコンテナ側でnginxコンテナからの通信を受けられるように、uwsgi.ini の作成を行う。

```ini:uwsgi.ini
[uwsgi]
socket = :8000
module = djangoapp.wsgi
wsgi-file = /app/app/wsgi.py
logto = /wsgi/wsgi.log
py-autoreload = 1
```

djangoコンテナのイメージの元となるDockerfileを作成する。

```Dockerfile:Dockerfile
FROM python:3
WORKDIR /app
COPY requirements.txt /app
RUN pip install -r requirements.txt
COPY . /app
```

djangoコンテナで必要とするPythonのモジュールのリストを作成する。

```text:requirements.txt
django
psycopg2
uwsgi
```

djangoコンテナ内で実行されるシェルスクリプト（startup.sh, setuser.sh）を作成する。
userを作成し、uwsgi,ini を実行する。

```sh:startup.sh
source /startup/setuser.sh # setuser.sh を実行する
uwsgi --ini /wsgi/uwsgi.ini # uwsgi.ini の設定をもとに uwsgi を実行する
```

```sh:setuser.sh
#!/bin/bash -e
SHELL_NAME='setuser.sh'
echo "[$SHELL_NAME] START"

# setup group
if getent group "$GROUP_ID" > /dev/null 2>&1; then
    echo "[$SHELL_NAME] GROUP_ID '$GROUP_ID' already exists."
else
    echo "[$SHELL_NAME] GROUP_ID '$GROUP_ID' does NOT exist. So execute [groupadd -g \$GROUP_ID \$GROUP_NAME]."
    groupadd -g $GROUP_ID $GROUP_NAME
fi

# setup user
if getent passwd "$USER_ID" > /dev/null 2>&1; then
    echo "[$SHELL_NAME] USER_ID '$USER_ID' already exists."
else
    echo "[$SHELL_NAME] USER_ID '$USER_ID' does NOT exist. So execute [useradd -m -s /bin/bash -u \$USER_ID -g \$GROUP_ID \$USER_NAME]."
    useradd -m -s /bin/bash -u $USER_ID -g $GROUP_ID $USER_NAME
fi

echo "[$SHELL_NAME] FINISH"
```


startupディレクトリ配下の全てのファイルに実行権限を付与する。

```zsh
chmod +x -R ~/dev/3l_on_docker/django/startup/
```


ホスト側でdjangoプロジェクトを作成する。
そのためまずはホスト側でdjangoのインストールを行う。

```
pip install django
```

django ディレクトリ内でdjango projectの作成を行う。
今回はプロジェクト名を`djangoapp`とする。

```bash
django-admin startproject djangoapp
```

プロジェクトを作成するとdjangoディレクトリ内に`djangoapp`ディレクトリが作成される。
デフォルトでは外部からの通信は全て拒否されているため、setting.py の修正を行う。

```python:setting.py
...

ALLOWED_HOSTS = ['*']

...
```

今回は外部の全ての通信を許可する`'*'`を設定した。

`docker-compose up -d`でコンテナを立ち上げ、EC2のパブリックIPアドレスにアクセスする。

![Screen Shot 2021-06-24 at 9.54.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431877/78f93296-69e4-9037-3fbb-b8c554f8b6e2.png)

上のような django のテストページが見えていればOK。

#### サンプルアプリケーションの作成
テストページが見れたので、ここからは自身で作成したページを表示する。
まずはホスト側でアプリケーションを作る。

```
python manage.py startapp sampleapp
```

sampleapp ディレクトリ直下に urls.py を作成する。


```python:urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]
```

```python:views.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")
```

`http://[IP アドレス]/[app 名]`にアクセスし、`"Hello, world. You're at the polls index."`が確認できればOK。


#### データベースのレイアウト（models.py の編集）

```python:models.py
class User(models.Model):
    user_name = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

class FigurePaths(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    figure_url = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    like_point = models.IntegerField(default=0)
```

```python:setting.py
INSTALLED_APPS = [
    'instalikeapp.apps.InstalikeappConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
```

```bash
python manage.py makemigrations sampleapp
```

migration ディレクトリ内に`0001_initial.py`が作られる。

`docker exec -it bash`で django コンテナに入り `sqlmigrate` と `migrate` を実行する
これによってデータベースへデータを入力するための準備を完了する。

```bash
[ec2-user@ip-10-0-1-89 djangoapp]$ docker exec -it django bash
root@87e83722b47a:/app# python manage.py sqlmigrate sampleapp 0001
root@87e83722b47a:/app# python manage.py migrate
```

### postgreSQLコンテナの準備（DBサーバ）

postgresqlコンテナの設定を追加する。

```yml:docker-compose.yml
version: '3.7'
services:
    nginx:
        container_name: nginx
        image: nginx:latest
        volumes:
            - ./nginx/conf:/etc/nginx/conf.d
            - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params
        ports:
            - 80:80
        depends_on:
            - django
    
    django:
        container_name: django
        build: ./django
        command: bash -c /startup/startup.sh
        volumes:
            - ./django/uwsgi:/wsgi
            - ./django/djangoapp:/app
            - ./django/startup:/startup
        ports:
            - 8000:8000
        environment:
            - USER_ID=1000
            - GROUP_ID=1000
            - USER_NAME=ec2-user
            - GROUP_NAME=ec2-user
        depends_on: # add
            - postgresql # add

    postgresql: # add
        image: postgres:latest # add
        container_name: postgresql # add
        environment: # add
            - POSTGRES_DB=instalikeapp_db # add
            - POSTGRES_USER=user # add
            - POSTGRES_PASSWORD=password # add
        volumes: # add
            - ./pgdata:/var/lib/postgresql/data # add
        ports: # add
            - 5432:5432 # add

```

コンテナを立ち上げ直す。

```bash
docker-compose restart
```

# djangoコンテナ内のpython の インタプリタからデータベースにデータを入れてみる
詳細は[djangoのチュートリアル](https://docs.djangoproject.com/ja/3.2/intro/tutorial02/)を参考にすると良い。
djangoコンテナに入り、pythonのインタプリタからデータベースへの入力を行う。

```
docker exec -it django bash
root@87e83722b47a:/app# python manage.py shell
>>> from instalikeapp.models import User, FigurePaths
```

下記コマンドでpostgresqlにログインする。

```
psql -d myapp -U USERNAME -h localhost
```

djangoコンテナからデータベースへデータの入力を行い、postgresqlコンテナでデータベースの中身を確認する。
以下の図の左はdjangoコンテナ内でデータベースへの入力操作を行なったもの、 右側はpostgresqlコンテナ内でデータベースの中身を確認したもの。
![Screen Shot 2021-06-24 at 13.17.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431877/bbc7b2e1-986c-62f1-5bce-c5c7a587a055.png)




# おまけ
### Python 3系のインストール（pyenv経由）
```zsh
sudo yum install git
git clone https://github.com/yyuu/pyenv.git ~/.pyenv
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init --path)"' >> ~/.bashrc
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.bashrc
source ~/.bashrc
sudo yum install gcc zlib-devel bzip2 bzip2-devel readline readline-devel sqlite sqlite-devel openssl openssl-devel -y
pyenv install 3.8.9
pyenv global 3.8.9
```

### DjangoサイトにCSSを反映させる

```python:setting.py
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static') # add
```

プロジェクト直下のstaticディレクトリに静的ファイルをコピーする

```bash
python manage.py collectstatic
```


```conf:nginx.conf
server {
    listen 80;
    
    location / {
        uwsgi_pass django;
        include /etc/nginx/uwsgi_params;
    }

    location /static/ {
        root    /app/;
    }
}
```