Help us understand the problem. What is going on with this article?

DjangoのDBをSQLite3からMySQL on Dockerに移行したメモ on Raspberry Pi 4B

今回の目的

ちょっとごちゃごちゃしてます。

  1. Raspberry Pi 3B+で(直接)動いていた自分用DjangoプロジェクトをRaspberry Pi 4Bに移行
  2. そのSQLite3 DBが肥大化してきた(vacuumして120MBくらい)のと並行性確保のためMySQLに移行
  3. せっかくなのでDjangoプロジェクトをdocker-composeで動かすようにする
  4. MySQLもdocker-composeで動かそう
  5. DBを自動で更新する定期実行スクリプトもdocker-composeで動かそう

1. Raspberry Pi 4Bの初期セットアップ

まずはRaspberry Pi 4Bをセットアップする。

3B+で動いていたStretchは4Bでは動かないというような話を聞いたので、Download Raspbian for Raspberry Piから新しいRaspbian Busterを持ってきてMicro SDカードに焼く。メモリ節約(増えたけど)できそうなのでBusterはDesktopではなくLiteにした(どうせサーバ用途)。

mini HDMIケーブル/アダプタの持ち合わせがなかった(つらい、Microならあるのに...)ので、(20/03/07追記:間違い。Micro HDMIです。Miniはうすっぺらいやつ?)モニタレスセットアップする。100均で売ってるので必要になったら調達。
Micro SDカードを本体に挿し、LANケーブルでルータに接続。5V 3A Type-C電源(つらい、Microから買い替え)を接続して電源を入れる(Micro USBとType-Cの変換も100均にありそうなので電源を買い替えたくなければ... ちょっとこわいけど)。

ルータからDHCPで割り当てられたIPを調べて、LAN内でSSH接続(初期パスワード)。

Too many authentication failuresと言われたり、publickeyで弾かれるときは公開鍵で認証しようとして失敗してるので、-o PreferredAuthentications=password-o PubkeyAuthentication=noをsshコマンドのオプションに加えるか、~/.ssh/configPreferredAuthentications passwordPubkeyAuthentication noを加える。

パスワード変更・ユーザ名変更

パスワード変更、それから同時にユーザ名を変更する。piユーザにログインしたままだとユーザ名を変更できないので、新しいsudoerなユーザを作成してログインし直す(今回はLAN内なので一時的にrootパスワードを設定してもいいかもだけど)。

piユーザで以下のコマンドを実行してtmpuserを作成。

# useraddの場合ホームディレクトリは作られない
sudo useradd tmpuser
sudo passwd tmpuser

次にtmpuserをsudoersに追加する。

sudo adduser tmpuser sudo

ちょっと回り道をしたい場合、/etc/sudoersを編集してtmpuserを追加する。なんか/etc/sudoers他がreadonlyになってるので(chmodしてもいいけど)、/etc/sudoers.d/011_tmpuserを作る(sudoグループに追加でもいける)。

# /etc/sudoers.d/011_tmpuser
tmpuser ALL=(ALL:ALL) ALL

一度ログアウト、tmpuserユーザでログインし直して、以下のコマンドでpiユーザの名前を変更、piグループの名前も変更、最後にホームディレクトリを移動する。

sudo usermod -l NEW_NAME pi
sudo groupmod -n NEW_NAME pi
sudo usermod -m -d /home/NEW_NAME NEW_NAME

tmpuserユーザからログアウトして、NEW_NAMEユーザで再ログイン、tmpuserユーザを削除する。回り道をした場合、/etc/sudoers.d/011_tmpuserも削除する。なお、デフォルトでpiユーザがsudoグループに属しているので、NEW_NAMEユーザをあらためてsudoersに加える必要はない(はず)。

sudo userdel tmpuser
# sudo rm /etc/sudoers.d/011_tmpuser

ホスト名変更

# /etc/hostname
NEW_HOSTNAME

# /etc/hosts
...
127.0.1.1 NEW_HOSTNAME

公開鍵認証

NEW_NAMEユーザに公開鍵を登録、/etc/ssh/sshd_configを編集してSSHの認証を公開鍵のみにする。

pi側

mkdir ~/.ssh
chmod 700 ~/.ssh

ホスト側

cd ~/.ssh
ssh-keygen -f KEY_NAME
scp KEY_NAME.pub RPI4_HOST:.ssh/

pi側

cd ~/.ssh
cat KEY_NAME.pub >> authorized_keys
chmod 600 authorized_keys

あとは~/.ssh/configでIdentityFileを指定すればよい。やはりToo many authentication failuresといわれるときは、IdentitiesOnly yesを追加する。

必要に応じて/etc/ssh/sshd_configを編集してPasswordAuthentication noを設定するなどする。

2. Raspberry Pi 4BにDocker/docker-composeを導入

sudo curl -fsSL https://get.docker.com/ | sh
sudo apt install python3-pip
sudo apt install libffi-dev
sudo pip3 install docker-compose

3. DjangoプロジェクトのDocker/docker-compose移行

Djangoプロジェクトはgitで管理してたので、git cloneで新サーバにプログラムを移行。DB(SQLite3)はscpで移行。

旧サーバではvirtualenvで環境管理してたので、ここからrequirements.txtを生成。

pip3 freeze > requirements.txt

せっかくなので新サーバの環境構築はDocker/docker-composeで行う。django:wsgi-gunicorn-nginxの構成だったが、まずは単体で動作テスト。

# Dockerfile
FROM python:3
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/
# docker-compose.yml
version: '3'

services:
    web:
        build: .
        command: python manage.py runserver 0.0.0.0:8000
        volumes:
            - .:/code
        ports:
            - "127.0.0.1:8000:8000"
        environment:
            - ENVIRONMENT=production
sudo docker-compose up

4. MySQLをDocker上で動かす(docker-compose/Raspberry Pi 4)

Raspberry Pi上のdocker-composeでMySQL(MariaDB)を動かす。jsurf/rpi-mariadbを使う。

...
    db:
        # image: mariadb
        image: jsurf/rpi-mariadb
        command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
        volumes:
            - DATABASE_DIRECTORY:/var/lib/mysql
        environment:
            - MYSQL_ROOT_PASSWORD=ROOT_PASSWORD
            - MYSQL_DATABASE=DATABASE_NAME
            - MYSQL_USER=USER
            - MYSQL_PASSWORD=PASSWORD
    web:
...

5. DjangoのDB設定をSQLite3からMySQLへ変更

あとで戻すので、簡単に戻せるようにしつつ、Djangoのsettings.pyを適当にいじって環境変数からDBを指定するようにする。

DATABASE_ENGINE = os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3')
DATABASE_OPTIONS = {}
if DATABASE_ENGINE == 'django.db.backends.mysql':
    DATABASE_OPTIONS = {
        'charset': os.environ.get('DATABASE_CHARSET'),
    }

DATABASES = {
    'default': {
        'ENGINE': DATABASE_ENGINE,
        'HOST': os.environ.get('DATABASE_HOST'),
        'PORT': os.environ.get('DATABASE_PORT'),
        'NAME': os.environ.get('DATABASE_NAME', os.path.join(BASE_DIR, 'db.sqlite3')),
        'USER': os.environ.get('DATABASE_USER'),
        'PASSWORD': os.environ.get('DATABASE_PASSWORD'),
        'OPTIONS': DATABASE_OPTIONS,
    },
}

docker-compose.ymlは以下のように編集。environmentのDATABASE部分をすべてコメントアウトするか、docker-compose.ymlを他に作ることでDBをSQLite3に戻せるようにしておく。

    web:
...
        environment:
            - ENVIRONMENT=production
            - DATABASE_ENGINE=django.db.backends.mysql
            - DATABASE_HOST=db
            - DATABASE_PORT=3306
            - DATABASE_NAME=DATABASE_NAME
            - DATABASE_USER=USER
            - DATABASE_PASSWORD=PASSWORD
            - DATABASE_CHARSET=utf8mb4
        depends_on:
            - db

requirements.txtPyMySQLを追記して、manage.pyの上部に以下を追記する。

if os.environ.get('DATABASE_ENGINE') == 'django.db.backends.mysql':
    import pymysql
    pymysql.install_as_MySQLdb()

なお、MySQL側の初期化が終わる前にDjango側がアクセスするとDjangoがエラー落ちするので、初回実行時はdocker-compose upを再実行する。2回目移行もDjangoが先に起動してしまう場合は、適当なsleepをはさむか待機スクリプトを作るなどして対処する。9番のセクションでgunicornをはさんだあとだと、Django(gunicorn)が先に起動してもエラーは出なくなった(みたいな)ので、あんまり気にしなくてもいいかもしれない。

        command: bash -c "sleep 5 && python manage.py runserver 0.0.0.0:8000"

6. Migration Errorの解消

DBモデルの定義によっては、sudo docker-compose up -dしてsudo docker-compose exec web python3 manage.py migrateするとエラーが出る。例えばunique制約付きのTextFieldがあって、max_lengthを指定していない場合。今回はURLをURLFieldではなくTextFieldに入れていたのをURLFieldに直し、また短い文字列とわかっているTextFieldにmax_length(255以下)を指定して解消した(ただし、Raspberry Pi上ではこれだけでは動かなかったので、あとで結局unique制約を外した)。

このあたりは高速化のためメイン機にプロジェクトとDBを移して実験しつつ行った。今回はMySQLのimageを変える必要があるが、(だいたい)同じ環境を自動で整えてくれて、しかもホストの環境を汚さない/影響を受けないのがDockerのいいところ(公式imageがないと互換性に悩まされるのが...?)。

7. DjangoのDBデータをjsonに書き出す

Migration Errorを解消したらDBをSQLite3に戻し、migrationしたあとでデータをjsonにダンプする。

sudo docker-compose run web bash
python3 manage.py makemigrations
python3 manage.py migrate
# python3 manage.py dumpdata > dump.json
python3 manage.py dumpdata --natural-foreign --natural-primary -e contenttypes -e auth.Permission > dump.json

8. DjangoのDBデータをjsonから書き戻す

python3 manage.py migrate
python3 manage.py loaddata dump.json
django.db.utils.IntegrityError: Problem installing fixture '/code/dump.json': Could not load APP.MODELNAME(pk=PK_NUM): (1062, "Duplicate entry 'ONE_FIELD_NUM' for key 'ONE_FIELD'")

OneToOneFieldにunique_together制約をかけていたのがよくなかった(OneToOneは多対1に使えない)みたいなので、ForeignKeyに変えた。また、この時点でmariadbでは動いたものの、jsurf/rpi-mariadbではutf8mb4にしているせいかKey lengthまわりのエラーが消えずに動かなかったので、すべての文字列のunique制約を外してしまうことにしたほか、こちらではmigrateが途中で止まってしまうのでmigrations以下のファイルを直接書き変えなければならなかった。別のPCで処理したDBを直接送りつけても動かなかったので、互換性にも不安が残るが...。何度も試行錯誤してようやくloaddataすることができた。

9. ホストのWebサーバ(リバースプロキシ)とDjangoの間にgunicornをはさむ

requirements.txtgunicornを追加する。

docker-compose.ymlを編集する。メモリを食う(と思う)のでワーカー数-wは必要に応じて調整する。

        # command: /usr/local/bin/gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300
        command: bash -c "sleep 5 && gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300"

manage.pyと同様に、wsgi.pyの上部に以下を追加する。

if os.environ.get('DATABASE_ENGINE') == 'django.db.backends.mysql':
    import pymysql
    pymysql.install_as_MySQLdb()

10. 定期実行スクリプトをDocker上で動かす

(20/02/15 追記)

以下でbusybox crondを使った設定をしたが、ログ周りが不便なのと結局本番用のスクリプトはうまく動いてくれていなかったみたいなので、Pythonで定期実行スクリプトを書いて、Djangoコンテナと同じ構成のコンテナをもう一個作って実行することにした。しかし冗長なので、Djangoコンテナ側に定期実行用のエンドポイントを作ってHTTPリクエストを飛ばすだけのコンテナにした方がいいかもしれない..。


(旧版)

今回はDjangoと同じコンテナ内で定期実行を走らせる。

定期実行スクリプトはこれまでだいたいpythonでループを書いちゃうか、systemdのtimerで動かしていた。今回のものはsystemd/timerだったので、これをDockerコンテナ内に移行しようとしたが、ホストからDockerコンテナ内のスクリプトを動かすのはexecでできるものの、Dockerコンテナ内でsystemd/timerを動かすのはよくわからない。

ともあれ、python:3のベースOSはDebianでsystemdがなさそう(init.d)なので、cronで定期実行することにする。

cronを使うのは初めてなのもあって、そうとう迷走した。

Dockerで指定した環境変数を引き継ぎたいので、busyboxに含まれるcrondを使う。

まず、実行ディレクトリに以下のようなファイルcrontabを作成する。

# * * * * * cd /code && echo `env` >> env.txt
0 */6 * * * cd /code && /usr/local/bin/python3 AUTORUN_SCRIPT.py

上は1分ごとに環境変数をファイルに書き出す設定(デバッグ用)で、下は6時間ごとに/code/AUTORUN_SCRIPT.pyをrootユーザ、ワーキングディレクトリ/codeで自動実行する設定。時刻はJSTでOK。

次に、Dockerfileにcrondのインストールと設定ファイルの追加を定義する。/var/spool/cron/crontabs/rootはディレクトリではなくファイルになるのが正解。

# Dockerfile
...
RUN apt update && apt install -y \
  busybox-static
ENV TZ Asia/Tokyo
COPY crontab /var/spool/cron/crontabs/root
...

それから、Dockerコンテナ起動時にcrondが起動するようにする。今回はdocker-composeを使っているので、Dockerfile内のCMDは実行されないのに注意。代わりに、docker-compose.yml内のcommandにcrondの起動コマンドを追加する。crondはバックグラウンド実行されるので、続けて自動的にgunicornが起動する。

# docker-compose.yml
...
        # command: bash -c "busybox crond && gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300"
        command: bash -c "sleep 5 && busybox crond && gunicorn -w 4 -b 0.0.0.0:8000 MY_PROJECT.wsgi -t 300"
...

DjangoのDBをいじるのでAUTORUN_SCRIPT.py中にpymysqlのinstallを追加する(manage.pywsgi.pyと同じ)などして、実験用のスクリプトが動作することが確認できた。デバッグ用のcron設定を消して設定完了。

11. 起動時自動実行とバックグラウンド実行

docker-compose.ymlrestart: alwaysを追加してホスト起動時に自動的に開始するようにして、sudo docker-compose up -dでバックグラウンドで起動。あとはホストをrebootしても大丈夫(sudo docker-compose pssudo docker ps)。

結果

無事にハードウェア(Raspberry Pi)の移行、DBの移行、DBエンジンの移行、Dockerへの移行&永続化に成功した。

ハードウェアのスペック向上、MySQLへの移行によってパフォーマンスが改善した(ように思われる)ほか、DB操作を並行して行うことができるようになった(みたいな)ので、同時にDBにアクセスしたときに発生していたDatabase is lockedエラーが見られなくなった。

個人用プロジェクトなのでログ設定端折ったりだとかunique切ったりだとかしてるけど...。ここはそうとう時間かけたのであきらめた。ただ、loaddataしたあとでならmigrationでunique戻したりできるかもしれない。

あとはcronを別のコンテナに分けた方がいいのかな、と思いつつ、Djangoプロジェクトとまったく同じ依存関係なので分けずにまとめてしまった。これどう分けるんだ...?

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした