django + nginx + kubernetesでstatic fileをnginxから見せる方法

2021年8月14日追記

もっと簡単にできることに気が付いたので以下の記事を参照してください。
https://qiita.com/legacyworld/items/cee23b2ee1530bcb7f55

# 何をやりたいか

Djangoを`python manage.py runserver`ではなくちゃんとnginxから見せようとすると、staticファイルをnginxから見せる必要がある。そうしないとcss等が読み込めないためこういう残念なことになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629456/2447c7cc-ec7c-4ef4-28d0-7a1fe9e86188.png)

hostpathを使えば何とかならなくもないが、結局ホストにファイルをコピーするなど必要なのであまりやりたくない。
もしくは手作業でstaticフォルダを作るか、kubernetesに展開した後にコピーしても良いのかもしれないが、出来るだけ自動でやりたい。

git cloneしてソースをいじってコンテナをビルドして`kubectl apply -f manifest.yml`で終わりたいのだ。

ソースコードはこちら
https://github.com/legacyworld/django_k8s

# 環境
CentOS7
minikube v1.21.0

Djangoのソースコードとして以下を借りました。
https://github.com/kakky/mybook30
https://qiita.com/kaki_k/items/511611cadac1d0c69c54
少しだけ変更してます。

# 全体の流れ
ホストにゴミは残したくないので全部コンテナにぶち込む。
要するに`STATIC_ROOT`で指定したフォルダの中身をどうやってnginxに見せるか、である。

1. DjangoのDockerfile内でpipによるDjangoインストールからmigrateやcollectstaticまでやってしまってDocker HubにPush
1. nginxは/staticに来た時だけ返しそれ以外はuwsgiで飛ばすようにnginx.confを書いてビルドする
1. Kubernetesに展開する際にnginxとdjangoのコンテナ間で共有するPersistent volumeを作成する
1. collectstaticで作ったstaticフォルダの中身をこのpersistent volumeにコピーする。この時postStartを使ってコマンドを流す
1. 上記の結果nginxからstaticファイルが見えるようになる

もっと良い方法があると思うのだが、とりあえずはこれで行けた
DBのバックアップとかloaddataとかは考慮してません。

# 全体構造

```
.
|-- docker-compose.yml
|-- manifest.yml
|-- mybook30
|   |-- api
|   |   |-- admin.py
|   |   |-- apps.py
|   |   |-- __init__.py
|   |   |-- migrations
|   |   |   `-- __init__.py
|   |   |-- models.py
|   |   |-- tests.py
|   |   |-- urls.py
|   |   `-- views.py
|   |-- cms
|   |   |-- admin.py
|   |   |-- apps.py
|   |   |-- forms.py
|   |   |-- __init__.py
|   |   |-- migrations
|   |   |   |-- 0001_initial.py
|   |   |   `-- __init__.py
|   |   |-- models.py
|   |   |-- static
|   |   |   `-- cms
|   |   |       |-- css
|   |   |       |   |-- bootstrap.min.css
|   |   |       |   `-- bootstrap.min.css.map
|   |   |       `-- js
|   |   |           |-- bootstrap.bundle.min.js
|   |   |           |-- bootstrap.bundle.min.js.map
|   |   |           |-- jquery-3.4.1.min.js
|   |   |           `-- jquery-3.4.1.min.map
|   |   |-- templates
|   |   |   `-- cms
|   |   |       |-- base.html
|   |   |       |-- book_edit.html
|   |   |       |-- book_list.html
|   |   |       |-- impression_edit.html
|   |   |       `-- impression_list.html
|   |   |-- tests.py
|   |   |-- urls.py
|   |   `-- views.py
|   |-- Dockerfile
|   |-- manage.py
|   |-- mybook
|   |   |-- asgi.py
|   |   |-- __init__.py
|   |   |-- settings.py
|   |   |-- urls.py
|   |   `-- wsgi.py
|   |-- requirements.txt
|   `-- uwsgi.ini
`-- nginx
    |-- Dockerfile
    `-- nginx_conf
        `-- nginx.tpl
```

# docker-compose
nginxとdjango用に2つ用意するだけ。

```docker-compose.yml
version: '3'
services:

  django:
    image: legacyworld/django_k8s_django
    build:
      context: ./mybook30
      dockerfile: Dockerfile
    container_name: django
    ports:
      - 8000:8000
    stdin_open: true
    tty: true

  nginx:
    image: legacyworld/django_k8s_nginx
    build:
      context: ./nginx
      dockerfile: Dockerfile
    container_name: nginx
    ports:
      - 80:80
  ```
#Django設定
オリジナルから以下を変更している
## staticフォルダ設定

```settings.py
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [
    '/src/cms/static/cms',
    '/usr/local/lib/python3.8/site-packages/django/contrib/admin/static',
]
```

`cms/static/cms`とadmin用のstaticファイルが必要になる。コンテナを立ち上げた後に以下のコマンドで場所はわかる

```
root@ec7157d343ce:/src# python manage.py findstatic .
Found '.' here:
  /src/cms/static/cms
  /usr/local/lib/python3.8/site-packages/django/contrib/admin/static
  /usr/local/lib/python3.8/site-packages/django/contrib/admin/static
  /src/cms/static
```

これを`STATICFILES_DIR`に記載しておけば`collectstatic`で集めてくれる。
`STATIC_ROOT`はcollectstaticした時に集められる場所。今回だと`mybook30`の下に出来る
これをnginxに見せる必要がある

## DBの設定
dq.sqlite3をroot以外で書き込めるようにしないといけないのだが、ディレクトリがそうなっていないとエラーが出てしまうのでdbというディレクトリを作っている。
後ほどDokcerfileでchmodしている。
詳しくはこちらを参照
https://qiita.com/juniskw/items/7f948843d571ca2dd0f4

```settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db/db.sqlite3'),
    }
}
```

#nginxの設定
forwardingの部分だけ抜き出し

```nginx.conf
  server{
    listen 80;
    server_name nginx;
    location / {
      proxy_request_buffering off;
      include uwsgi_params;
      uwsgi_pass django:8000;
    }
    location /static {
      proxy_request_buffering off;
      root /usr/share/nginx/html;
    }
  }
```

`STATIC_URL`で`'/static/'`と指定しているので、`location /static`でstaticファイルへのアクセスを指定すればよい。
というわけで、`usr/share/nginx/html`に`STATIC_ROOT`の中身を見せる設定をすればよい。
これはKubernetesのDeploy時に行う。

# DjangoのDockerfile
python 3.8-busterから一気にstaticフォルダ作成とDBのmigrateまで行う

```Dockerfile
FROM python:3.8-buster

WORKDIR /src

ADD ./ /src/
RUN pip3 install -r requirements.txt

RUN mkdir db
RUN chmod 777 db
RUN python manage.py collectstatic
RUN python manage.py migrate
RUN chmod 777 ./db/db.sqlite3

RUN useradd -r -s /bin/false uwsgiusr
USER uwsgiusr
ENTRYPOINT ["uwsgi","--ini","/src/uwsgi.ini"]
```

`ADD`でソースコードを全部コピーして、requirements.txtに記載のあるものを全部インストールする。
dbディレクトリ作成とchmodは先ほどsettings.pyのところで説明した通り。

# kubernetesのmanifest
全部は長いので一部だけ。

##PersistentVolumeClaim
まずはPersistentVolumeClaimを作る。

```manifest.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
    name: static-pvc
spec:
    accessModes:
      - ReadWriteOnce
    resources:
        requests:
            storage: 1Gi
```

## DjangoのDeployment
上記のvolumeをdjangoのコンテナにマウントしてファイルをコピーする。
/mntにこのvolumeをマウントしている。

```manifest.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: django-deploy
spec:
  selector:
    matchLabels:
      run: django
  replicas: 1
  template:
    metadata:
      labels:
        run: django
    spec:
      containers:
      - name: django
        image: legacyworld/django_k8s_django:latest
        volumeMounts:
        - name: static
          mountPath: /mnt
        lifecycle:
          postStart:
            exec:
              command: ["cp", "-r", "/src/static", "/mnt"]
      securityContext:
        fsGroup: 999
      volumes:
      - name: static
        persistentVolumeClaim:
          claimName: static-pvc
```

`lifecycle`で`postStart`を指定するとDockerfileで記載しているENTRYPOINTに加えて、追加でコマンドを実行できる。
`securityContext`で`fsGroup`を指定しているのは、何もしないとrootの644でマウントされてしまってコピーできなくなるからである。
`999`は一回コンテナを立ち上げて`id uwsgiusr`で確認している。
一度確認すれば何回イメージを作っても変わらないようだ。

## nginxのDeployment

```manifest.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deploy
spec:
  selector:
    matchLabels:
      run: web
  replicas: 1
  template:
    metadata:
      labels:
        run: web
    spec:
      containers:
      - name: nginx
        image: legacyworld/django_k8s_nginx:latest
        env:
        - name: PORT
          value: "80"
        ports:
        - containerPort: 80
        volumeMounts:
        - name: static
          mountPath: /usr/share/nginx/html
      volumes:
      - name: static
        persistentVolumeClaim:
          claimName: static-pvc
```

こちらは`volumeMounts`するだけ。コピーが終われば自動的に見えて来るようになる。

# 確認
NodePortでアクセスする必要があるので、`kubectl`でポート番号を確認する。

```
[nutanix@localhost django_k8s]$ kubectl get services
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
django          NodePort    10.98.62.97      <none>        8000:32746/TCP   86m
kubernetes      ClusterIP   10.96.0.1        <none>        443/TCP          10d
nginx-service   NodePort    10.111.177.213   <none>        80:30199/TCP     86m
```

nginxの30199にアクセスをすればよい。

# 通常ページ
今回のケースだとport number = 30199
http://\<minikube hostのIP>:\<port number>/cms/book
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629456/6468e3b4-070d-dd97-1a48-2754ebecd305.png)

# adminページ
http://\<minikube hostのIP>:\<port number>/admin
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629456/7e97fa58-c08b-905b-5fc7-d2dc4e66dd95.png)

お疲れ様でした。