【Mac】Docker をインストールしてコンテナ内の MySQL を触ってみる

Docker を始めて使う際のプロセスをメモしておきます。

Macbook Pro に Docker Desktop をインストールし、Docker Hub から MySQL のコンテナイメージをダウンロードし起動する手順となります。

  1. Docker Desktop のインストール
  2. Docker Hub から Docker イメージをダウンロード
  3. コンテナの作成 & 起動
  4. コンテナに入ってみる
  5. コンテナから出る
  6. その他のコマンド
  7. コンテナに入らずにコンテナ内の MySQL にアクセスする
  8. データベースを作成する
  9. DBeaver で接続する

1. Docker Desktop のインストール

下記のリンクから Mac 用の Docker Desktop をダウンロードします。

https://docs.docker.com/desktop/install/mac-install/

「Docker.dmg」というファイルがダウンロードされるのでダブルクリックで実行し、画面表示に従ってインストールを進めます。

「docker –version」を実行してインストールが完了したか確認します。

% docker --version
Docker version 23.0.5, build bc4487a

無事インストールが出来た様です。

2. Docker Hub から Docker イメージをダウンロード

次に、「docker pull mysql」を実行して MySQL のコンテナイメージをダウンロードします。

ちなみに「実行場所にディレクトリが作られる」等はないので、ローカル環境のどこで実行しても大丈夫です。

% docker pull mysql
Using default tag: latest
latest: Pulling from library/mysql
328ba678bf27: Pull complete 
f3f5ff008d73: Pull complete 
dd7054d6d0c7: Pull complete 
70b5d4e8750e: Pull complete 
cdc4a7b43bdd: Pull complete 
a0608f8959e0: Pull complete 
5823e721608f: Pull complete 
a564ada930a9: Pull complete 
539565d00e89: Pull complete 
a11a06843fd5: Pull complete 
92f6d4aa041d: Pull complete 
Digest: sha256:a43f6e7e7f3a5e5b90f857fbed4e3103ece771b19f0f75880f767cf66bbb6577
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest

ダウンロードが完了しました。「docker images」コマンドを実行するとダウンロードされたコンテナイメージを確認することができます。

% docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
mysql        latest    8189e588b0e8   4 weeks ago   564MB

注意点

ちなみに、この時ローカルの Docker アプリケーションが起動していないと下記のエラーが出てしまいます。

% docker pull mysql
Using default tag: latest
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

この場合は Docker アプリケーションを起動して再度実行してください。

3. コンテナの作成 & 起動

Docker イメージのダウンロードの完了後、「docker run」コマンドでイメージからコンテナを作成、起動します。

Docker Hub のページに記載がある下記のフォーマットを元に適切な「docker run」コマンドを作成、実行します。

$ docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

主なオプション

上記のフォーマットに登場するオプションは下記の内容になります。

表記設定内容備考
–nameコンテナの識別子
-eコンテナ内の環境変数上記では MySQL のログインパスワードを設定
-dデタッチドモードバックグラウンドでコンテナを起動
mysql:tag使用する Docker イメージとそのバージョンの指定「イメージ:タグ」の形式で記述。タグのデフォルトは直近のもの。

加えて今回は -p オプションを設定し、ローカル PC からコンテナへのポート転送も設定しておきます。

表記設定内容備考
-pポートフォワーディング「ホストのポート:コンテナのポート」の形式で設定

今回使うコマンドは下記とします。

$ docker run --name sample-mysql -p 13306:3306 -e MYSQL_ROOT_PASSWORD=mysqlpassword -d mysql

「-p 13306:3306」の部分で、ホストの 13306 番ポートとコンテナの 3306 番ポートを紐づける設定にしています。

% docker run --name sample-mysql -p 13306:3306 -e MYSQL_ROOT_PASSWORD=mysqlpassword -d mysql
2b3b0efc8a7c7085e8e405dae4026a929a8d3cc76860f99dc9dde0b3f24474f1

「docker ps」コマンドを実行してコンテナが起動されているかを確認します。

% docker ps                                                                                 
CONTAINER ID   IMAGE     COMMAND                   CREATED          STATUS          PORTS                                NAMES
2b3b0efc8a7c   mysql     "docker-entrypoint.s…"   15 seconds ago   Up 14 seconds   33060/tcp, 0.0.0.0:13306->3306/tcp   sample-mysql

無事にコンテナが作成、起動されています。

ちなみに、先述した「docker pull」を実行しなくても、「docker run」実行時にローカル環境に対象のイメージがない場合、自動的に Docker Hub 上で探してくれます。

4. コンテナに入ってみる

「docker exec」コマンドを実行し、作成したコンテナの中に入ってみます。

% docker exec -it sample-mysql bash
bash-4.4# 
表記設定内容
-iti は STDIN(標準入力)の開放、t は擬似 TTY(標準入出力先デバイス)の割り当て。
sample-mysql対象のコンテナの名前
bashコンテナ内で使用するシェル
bash-4.4# mysql -u root -p   
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 10
Server version: 8.0.33 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)

とりあえずコンテナ内の MySQL を参照することはできました。

mysql> exit
Bye
bash-4.4# 

5. コンテナから出る

「exit」と入力すればコンテナから出られます。

bash-4.4# exit
exit
% 

6. その他のコマンド

取り急ぎいくつかのコマンドを記載しておきます。

コマンド表記用途
docker stop コンテナ名コンテナの停止
docker start コンテナ名コンテナの起動
docker ps起動中のコンテナの確認
docker ps -a作成されたコンテナの確認(起動中、停止中両方)

7. コンテナに入らずにコンテナ内の MySQL にアクセスする

「docker exec」を使わず、ターミナルからコンテナ内の MySQL にアクセスしてみます。

「mysql -h 127.0.0.1 -P 13306 -u root -p」のコマンドでポート 13306 番ポートの MySQL を指定してログインします。

% mysql -h 127.0.0.1 -P 13306 -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.33 MySQL Community Server - GPL

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.00 sec)

mysql> 

こんな感じでアクセスできました。

8. データベースを作成する

「db_in_container」というデータベースを作ってみます。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.01 sec)

mysql> create database db_in_container;
Query OK, 1 row affected (0.01 sec)

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| db_in_container    |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.01 sec)

無事作れました。

9. DBeaver で接続してみる

DBeaver でリモートサーバーの MySQL へ接続

DBeaver からリモートーサーバ上にある MySQL への接続を作成したので、メモとして残します。

今回は SSH Tunnel を使用し、SSH でサーバーへ接続→localhost として MySQL へアクセスする方法を選びました。

  1. 新しい接続
  2. SSH Tunnel の設定
  3. allowPublicKeyRetrieval
  4. MySQL ログイン情報の設定

新しい接続

まず、DBeaver の画面上でデータベースタブをクリックし、「新しい接続」をクリックします。

すると下記の様な画面が表示されるので今回の場合は「MySQL」を選択します。

SSH Tunnel の設定

今回、SSH Tunnel を使用して接続するので、まず「SSH」タブをクリックし、MySQL を設置しているサーバーへの SSH 接続情報を入力します。

MySQL へのログイン情報ではありません。

SSH 接続部分のテストをします。

無事成功です。

allowPublicKeyRetrieval

「ドライバのプロパティ」タブで「allowPublicKeyRetrieval」を TRUE にします。

MySQL ログイン情報の設定

最後にデータベース名やユーザー名、パスワードなど MySQL のログイン情報を入力します。

これもテストしてみます。

成功です。「終了」をクリックで接続の作成は完了です。

Ubuntu でのログ肥大化で Django が 502 エラー

Django アプリで 502 エラー

VPS でホストしている Django アプリの挙動がおかしくなりました。

トップページには問題なくアクセス出来るものの、ログインをしようとすると「502 Bad Gateway」のエラーが発生。

どれか一つのアプリではなく、同じサーバーでホストしている複数のアプリで同じ挙動(トップページは表示されるがログイン機能で 502 エラー)が出たので調べました。

サーバーの容量が限界に近い

Ubuntu にログインした所、下記の様に表示されました。

Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-137-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Thu Mar 30 17:12:11 JST 2023

  System load:  0.02               Processes:             142
  Usage of /:   95.0% of 94.40GB   Users logged in:       0
  Memory usage: 35%                IPv4 address for ens3: 133.125.39.81
  Swap usage:   28%

  => / is using 95.0% of 94.40GB

「Usage of /: 95.0% of 94.40GB」との表示で、どうやら容量の 95.0 % を使ってしまっているらしい。

原因はログファイルの肥大化

「sudo du -sh /* | sort -hr」でルートディレクトリ直下をチェック。

$ sudo du -sh /* | sort -hr
du: cannot access '/proc/905133/task/905133/fd/4': No such file or directory
du: cannot access '/proc/905133/task/905133/fdinfo/4': No such file or directory
du: cannot access '/proc/905133/fd/3': No such file or directory
du: cannot access '/proc/905133/fdinfo/3': No such file or directory
85G	/var
4.2G	/usr
701M	/home
306M	/boot
7.9M	/root
6.6M	/etc
728K	/run
56K	/tmp
16K	/lost+found
8.0K	/srv
4.0K	/opt
4.0K	/mnt
4.0K	/media
4.0K	/cdrom
0	/sys
0	/sbin
0	/proc
0	/libx32
0	/lib64
0	/lib32
0	/lib
0	/dev
0	/bin

なるほど「/var」で 85 GB も使っていると。

$ sudo du -sh /var/* | sort -hr
81G	/var/log
2.7G	/var/lib
1.3G	/var/www
103M	/var/cache
2.4M	/var/spool
2.1M	/var/backups
36K	/var/tmp
4.0K	/var/opt
4.0K	/var/mail
4.0K	/var/local
4.0K	/var/crash
0	/var/run
0	/var/lock

「/var/log」で 81 GB か。

$ sudo du -sh /var/log/* | sort -hr
45G	/var/log/syslog
31G	/var/log/syslog.1
4.1G	/var/log/journal
325M	/var/log/syslog.2.gz
193M	/var/log/syslog.5.gz
190M	/var/log/syslog.6.gz
188M	/var/log/syslog.7.gz
164M	/var/log/syslog.4.gz
159M	/var/log/syslog.3.gz
126M	/var/log/btmp
122M	/var/log/btmp.1
104M	/var/log/nginx
27M	/var/log/auth.log.1
21M	/var/log/auth.log
2.7M	/var/log/auth.log.4.gz
2.1M	/var/log/auth.log.3.gz
2.0M	/var/log/auth.log.2.gz
1.4M	/var/log/mail.log.1
1.3M	/var/log/mail.log
776K	/var/log/vsftpd.log

「/var/log/syslog」と「/var/log/syslog.1」が大きい。

けどこれって定期的に gz に圧縮されているのか?

ログファイルの容量を下げてみる

とりあえず「sudo logrotate -f /etc/logrotate.conf」を実行し、logrotate を強制的に実行してみました。

$ sudo logrotate -f /etc/logrotate.conf
error: destination /var/log/mysql/error.log.1.gz already exists, renaming to /var/log/mysql/error.log.1.gz-2023040500.backup

上記の様にエラーメッセージは出たものの、「syslog.1.gz-2023031800.backup」が作成され、サーバーにログインし直すと容量に空きができました。

「Usage of /: 30.9% of 94.40GB」となっています。

Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-137-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed Apr  5 07:34:52 JST 2023

  System load:  0.3                Processes:             140
  Usage of /:   30.9% of 94.40GB   Users logged in:       0
  Memory usage: 46%                IPv4 address for ens3: 133.125.39.81
  Swap usage:   22%

Django アプリ復活で解決

ログイン機能が 502 エラーを返していた状況はなくなり、問題なく動く様になりました。

原因は Django のコードに print 文がコメントアウトされずに残っていて、自然言語処理に関する冗長なログがそのまま Gunicorn 経由で出力されていたためでした。

Fetch API と Django のクロスオリジンで気をつける事

個人的に開発する中で、localhost:8080 でホストしている javascript ファイルから localhost:8000 でホストしてる Django に対し HTTP リクエストをかけたところ、CORS (Cross Origin Resource Sharing) 関連のエラーが色々出ました。

色々調べながら解決しましたが、自分なりに分かりやすく整理したかったのでこの記事にまとめました。

Javascript からの HTTP リクエスト には fetch API を使っています。

  1. CORS (Cross Origin Resource Sharing) とは
  2. Step 1: GET
  3. Step 2: request に Cookie を含める
  4. Step 3: POST や DELETE

CORS (Cross Origin Resource Sharing) とは

下記こちらからの抜粋です。

アプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンからの場合は正しい CORS ヘッダーを含んでいることが必要です。

オリジン間リソース共有の仕様は、ウェブブラウザーから情報を読み取ることを許可されているオリジンをサーバーが記述することができる、新たな HTTP ヘッダーを追加することで作用します。

Step 1: GET

まずは一番単純な GET から。Cookie も送らない想定です。

何もクロスオリジン対応をしていないと恐らく「No ‘Access-Control-Allow-Origin’ header is present on the requested resource.」という様なエラーが発生します。

クロスオリジンの request に対してサーバー側から Access-Control-Allow-Origin ヘッダーを適切に返す必要があります。

Fetch API での対応

Fetch API は普通に fetch() で request URL を叩けば大丈夫です。

let fetchResponse = await fetch(
    'http://localhost:8000/api/something'
)

メインはサーバー側(Django)での対応です。

Django での対応

django-cors-headers を pip install し、settings.py を下記のように追記して http://localhost:8080 からのリクエストを許可します。

pip install django-cors-headers
# settings.py

INSTALLED_APPS = [
   ...
   'corsheaders',
]

MIDDLEWARE = [
  ...
  'corsheaders.middleware.CorsMiddleware',
]

CORS_ALLOWED_ORIGINS = ['http://localhost:8080']

これでクロスオリジンの request を送れる様になります。(localhost:8080 → localhost:8000)

次に、request に cookie を含める設定を行います。

クロスオリジンでの fetch API は「デフォルトでは cookies を送信しない」仕様になっています。

これを解決するには fetch API の credentials パラメータを ‘include’ に設定し、サーバー側は Access-Control-Allow-Credentials ヘッダーを true で返す必要があります。

Fetch API での対応

fetch API のオプションに credentials: ‘include’ を含めます。

let fetchResponse = await fetch(
    'http://localhost:8000/api/something',
    {credentials: 'include'} // 追記分
)

Django での対応

settings.py に「CORS_ALLOW_CREDENTIALS = True」を追記します。

# settings.py
CORS_ALLOW_CREDENTIALS = True

これで Fetch API で Cookie が送れる様になり、Django 側もそれを受けられる様になりました。

Django でログイン済みのはずが AnonymousUser となる件

ちなみにログインユーザーを対象にした処理を Django 側で行う場合、上記の設定をしていないと、例えブラウザの cookie と Django の django_session テーブルで sessionid が一致していても 500 Internal Server Error や 403 Forbidden が発生すると思います。

理由は、クライアント側からの sessionid を request.COOKIES として受け取れていないため、request.user が非ログインユーザーを意味する AnonymousUser となるためです。

Step 3: POST や DELETE

さらに、クロスオリジンで POST や DELETE など unsafe な request を送る場合、クライアント側の fetch() に X-CSRFToken ヘッダーを設定し、Django 側では settings.py の CSRF_TRUSTED_ORIGINS のリストに request を許可するオリジンを記述する必要がります。

Fetch API での対応

let fetchResponse = await fetch(
    'http://localhost:8000/api/something',
    {credentials: 'include'},
    // ここから追記分
    method: 'POST',
    headers: {
        'X-CSRFToken': csrftoken, // Cookie から取得
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        'someKey1': someData1,
        'someKey2': someData2
    }
)

GET メソッドは fetch のデフォルト値でしたが、POST や他のメソッドを使用する場合は method を指定する必要があります。

また、headers に X-CSRFToken として cookie から取得した csrftoken をアサインし、Content-Type も指定する必要があります。

Django での対応

settings.py の CSRF_TRUSTED_ORIGINS のリストに request を許可するオリジンを記述します。

# settings.py
CSRF_TRUSTED_ORIGINS = ['http://localhost:8080']

【Django】Apple MusicKit JS で Apple Music サインインの問題発生

事象

まず、MusicKit JS でプレイヤーを設置し曲をフル再生しようとすると Apple Music へのサインインが必要となります。

その際の正常なフローとしては:

  1. 自動ポップアップでサインイン画面が表示される
  2. ユーザーID & パスワード、そして6 桁のワンタイムコードを入力してサインイン
    • Mac の Safari では指紋認証のみでサインイン可
  3. アプリケーション(オリジン毎)による Apple Music へのアクセス許可を求められる
  4. 「許可」ボタンのクリックするとポップアップ画面が閉じる
  5. アプリケーションで曲のフルバージョンが再生できるようになる

今回、上記 4 の「許可」ボタンをクリックしてもボタンがグレーアウトされるだけで何も変わらない事象が起きた。

ポップアップウィンドウは閉じず、曲も再生されない。

もう一度曲を再生しようとしても再度別のポップアップが開いてサインインを要求されるという状況でした。

ちなみにサインインされてない状態だとフルバージョンではなく 30 秒バージョンの再生になります。

問題 1:「許可」ボタンクリックから進まない

まず、上記ステップで「許可」ボタンをクリックしてもボタンがグレーアウトするだけでその先に進まなかった問題から。

原因:Referrer-Policy の設定漏れ

諸々調べた結果、原因としてボタンクリック時の HTTP request に Referer 情報が含まれていなかった事が挙げられます。

Django ではデフォルトの Referrer-Policy が same-origin となりますが、same-origin の挙動は「同一オリジンのリクエストではオリジン、パス、クエリー文字列を送信します。オリジン間リクエストでは Referer ヘッダーを送信しません。」となっています。

つまり Apple の認証システムへの request 時に Referer ヘッダーが送信されず、その後の処理が行われなかったと思われます。

Django での Referrer-Policy 変更方法

Django で Referrer-Policy を変更するには settings.py で SECURE_REFERRER_POLICY を設定する必要があります。

おそらく「SECURE_REFERRER_POLICY = “strict-origin-when-cross-origin”」で良いと思います。

残る問題

これで「許可」ボタンをクリックすると、ポップアップウィンドウの中でアプリが表示されました。

仕方なくその中で曲を再生してみようとするとまた別のポップアップウィンドウが開きサインインを求められるループに入りました。

一旦グレーアウトで止まる問題は解消されましたが、別の問題が発生しただけです。

問題 2:曲のフル再生ができない

「許可」ボタンクリック後ポップアップウィンドウが閉じない問題はさておき、曲が再生できないのはなぜなのかを調べました。

原因 1/2:musicUserToken が付与されない

デベロッパーツールで request/response の内容を確認した結果、「許可」ボタンをクリックした際の Apple 側からの response で musicUserToken を受け取っているにもかかわらず、それがこちら側の musicKitInstance に渡っていないことが判明。

正常時はブラウザの Local Storage に保存され、無事認証が完了した musicKitInstance をデベロッパーツールで覗くと下記のように musicUserToken が入っているはずですが、今回はこれが「undefined」となっていました。

ちなみにコード上で半ば無理やり下記のようにアサインすると曲の再生ができていました。(music は musicKitInstance)

music.musicUserToken = "AoSKv/b0ED6YZzVuAEIvki4eOgFgeQYrCPaU+KSFV7fFdEozGUawOuKXxrzGyISRlHPfJlOzkclA+Nk4I0SbLI/f0tiZ++a+QYOG3EP+d935PvL+udndhJjfG/xe+ctry69X/rTtqgdr2VRCbqMgt/xzocg7gg2w/QPuTcA7YSpevglys3/2AsC69ofZKl8fHKkp04dyLuhxVZOC2h4PGXc+6chmnSHIxo7tp/VTv+IWr8+fhQ=="

原因 2/2:ポップアップ起動直前の URL が「#」で終わっている

正直かなり特殊な例かもしれませんが、自分のアプリ上、曲のタブを表示するボタンに a タグを使用し href=”#” としていました。

つまり、曲を再生する前にこれをクリックするとメインウィンドウの URL が「http://localhost:8000/#」という形になりますが、これが良くなかったようです。

サインイン用のポップアップが開いて「許可」ボタンをクリックした後、ポップアップウィンドウの URL が更新されますが、その際メインウィンドウの URL と musicUserToken が「#」で繋がったものが入ります。

「http://localhost:8000/#ユーザートークン」の形で「#」につづけて musicUserToken が入ってくるんですが、上記で先に「#」が一つ入るため「http://localhost:8000/##ユーザートークン」という風に「#」が重複していました。

これが原因なのではと思い「#」を一つ消してブラウザ更新すると musicKitInstance に musicUserToken がアサインされ、URL からトークンの部分が消えました。(http://localhost:8000/ になる)

解決:URLに「#」を含まない様変更

アプリのコードを変更して「#」が付かないようにしたところ、「許可」ボタンクリックで musicUserToken は付与されるようになりました。

残る問題

ただ、メインウィンドウに戻らずポップアップ上にそのままアプリが表示される状況です。

その状態で曲はフル再生できますが、ポップアップが閉じてくれないのは問題です。

正直これに関しては解決できませんでした。

Django のテンプレートをそのまま Django の外に置いて python の http.server の Web サーバーで表示したところ無事にサインインできました。

なぜ内容が同じなのに Django で render されたものだとポップアップの挙動がおかしくなるのかは謎です。

と言うわけで、結局 html/css/js の部分は Django を通さず、API 部分だけ Django で書くことにしました。

それによってクロスオリジンのエラーを解決する必要がありましたが今のところ大丈夫そうです。

その他メモ書き

  • musicUserToken はオリジン毎の割り当て。
    • 127.0.0.1:8000 で取得した musicUserToken は localhost:8000 には共有されない。
    • 同じ http://127.0.0.1:8000 であれば http.server を使った html/js でも Django の runserver を使った Django テンプレートでも同じ musicUserToken で動く。

Javascript から Django REST Framework への POST/DELETE で 415 Unsupported Media Type

問題

Django REST Framework を使って API を作成中、Javascript から POST または DELETE リクエストを送ろうとした結果「POST(リクエスト先 URL) 415 (Unsupported Media Type)」のエラー表示。

*DELETE の場合も同じエラーが出ます。

原因

リクエストヘッダーに Content-Type がなかったのが原因。

解決方法

元々↓だったヘッダー部分に…

fetch('http://example.com/exampleapi',
    {
        method: 'POST',
        headers:{
            "X-CSRFToken": getCookie('csrftoken')
        body: JSON.stringify({ 'username': 'example'})
    }
)

↓の様に Content-Type を追加したら解決しました。

fetch('http://example.com/exampleapi',
    {
        method: 'POST',
        headers:{
            "X-CSRFToken": getCookie('csrftoken'),
            'Content-Type': 'application/json'} // 追記分
        body: JSON.stringify({ 'username': 'example'})
    }
)

DELETE の場合も同じ方法で解決しました。

Javascript から Django REST Framework への POST/DELETE で 403 Forbidden

問題

Django REST Framework を使って API を作成中、Javascript から POST または DELETE リクエストを送ろうとした結果「POST(リクエスト先 URL) 403 (Forbidden)」のエラー表示。

*DELETE の場合も同じエラーが出ます。

原因

CRSF (Cross Site Request Forgery) に対するセキュリティが働いて、リクエストが拒否されている様子。

解決法

ブラウザのクッキーから CSRF Token を取って、POST リクエストのヘッダーに含めたら解決しました。

元々↓だったところに…

const fetchOptions = {
    method: 'POST'
}
fetch('http://example.com/exampleapi',fetchOptions)

クッキーから csfrtoken を抽出して headers に含めて POST リクエストをする様にしました。

function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
                const cookie = cookies[i].trim();
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

const fetchOptions = {
    method: 'POST',
    headers:{"X-CSRFToken": getCookie('csrftoken')}
}
fetch('http://example.com/exampleapi',fetchOptions)

これで一応 POST リクエストが通る様になりました。(HTTP Status 200 が返ってきました。)

DELETE の場合も同じ方法で解決しました。

参考にした記事

【Django】カスタム User モデルの作成方法

Django プロジェクトを作る際、もしデフォルトの User モデルで機能が足りている場合でも、将来の保守性や変更の必要性が出た場合を考慮して、カスタムの User モデルを使用することが強く推奨されています。

また、この作業 Django プロジェクトを作成して最初の migrate 実行前に行う必要があります。

  1. ユーザー管理用アプリケーションの作成
  2. カスタム User モデルの作成
  3. カスタム User モデルの登録
  4. マイグレーション

参考資料

ユーザー管理用アプリケーションの作成

account という名前でアプリケーションを作ります。

% python manage.py startapp account
# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'account.apps.AccountConfig', # 追記
]

カスタム User モデルの作成

# account/models.py

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    pass

カスタム User モデルの登録

# settings.py

AUTH_USER_MODEL = 'account.User'

尚、この設定をした時の注意点として、django.contrib.auth.models の User を直接参照すると AUTH_USER_MODEL で設定した User モデルを参照できないそうです。

なので User モデルを参照する必要がある場合は django.contrib.auth.get_user_model() を使用する必要があります。

# account/admin.py

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User # models.py のカスタム User をインポート

admin.site.register(User, UserAdmin)

マイグレーション

まず、新たに作成した「account」アプリケーションのマイグレーションを作成します。

% python manage.py makemigrations account
Migrations for 'account':
  account/migrations/0001_initial.py
    - Create model User

マイグレーションを実行します。

% python manage.py migrate
Operations to perform:
  Apply all migrations: account, admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0001_initial... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying account.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying sessions.0001_initial... OK

無事マイグレーション完了です。

【Mac】Django アプリ作成 & MySQL 初回 migrate まで

  1. Python 仮想環境の作成
  2. Django プロジェクト&アプリケーションの作成
  3. MySQL へ初回 migrate
  4. 管理ユーザーの作成

Python 仮想環境の作成

任意の場所に Python 仮想環境を作成、起動します。

% python3 -m venv justjam
% cd justjam
% source bin/activate

Django プロジェクト&アプリケーションの作成

仮想環境内で django をインストール、そしてプロジェクトとアプリケーションを作成します。

% pip install --upgrade pip
% pip install django
% django-admin startproject justjam_proj
% cd justjam_proj
% python manage.py startapp api
# settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api.apps.ApiConfig', # 追記分
]

runserver を実行し、ブラウザで動作確認します。

% python manage.py runserver

MySQL へ初回 migrate

注)カスタムユーザーモデルを使用する場合は、以下のステップに進む前に設定を完了しておく必要があります。

settings.py の DATABASES を変更し MySQL 対応にします。

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'justjam',
    }
}

MySQL でデータベースを作成し、migrate を実行します。

% mysql
mysql> create database justjam;
mysql> exit
% pip install mysql
% python manage.py migrate

Django のテーブルが作成されていることを確認します。

% mysql
mysql> show tables
    -> ;
+----------------------------+
| Tables_in_justjam          |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+
10 rows in set (0.00 sec)

mysql> exit;

管理ユーザーの作成

% python manage.py createsuperuser
Username (leave blank to use 'username'): admin
Email address: admin@justjam.com
Password: 
Password (again): 
Superuser created successfully.

Apple MusicKit JS で簡単な音楽プレーヤーを作ってみる

Apple の MusicKit JS を使って、とりあえず簡単な音楽プレーヤーを作ってみます。

その前段階として Developer Token を作成してください。下記が参考になれば幸いです。

というわけで作ったのが下記の html ファイル。「Your Developer Token」となっている部分を自身で作成したものに置換すれば動くと思います。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />

<title>MusicKit JS</title>
</head>
<body>
<center>
    <p><img id="artwork"/></p>
    <p id="media-item-title">Display Media Item Title here</p>
    <p id="album-title">Display Album Title here</p>
    <p id="artist-name">Display Artist Name here</p>
    <p id="playback-time">Display Playback Time here</p>
    <p><button id="play">Play</button><button id="pause">Pause</button></p>
    <p><button id="previous-item">Previous</button><button id="next-item">Next</button></p>
</center>

<script src="https://js-cdn.music.apple.com/musickit/v1/musickit.js"></script>
<script type="text/javascript">
    // MusicKit JS の Promise を作成
    const setupMusicKit = new Promise((resolve) => {
        document.addEventListener('musickitloaded', function () {
            // MusicKit を定義
            MusicKit.configure({
                developerToken: 'Your Developer Token',
                app: {
                    name: 'My MusicKit JS App',
                    build: '2022.11.14'
                }
            })
            // MusicKit のインスタンスで Promise を resolve
            resolve(MusicKit.getInstance())
        })
    });

    // MusicKit.configure() が完了するのを待って残りを実行
    setupMusicKit.then(async (music) => {

        // 再生中の曲の情報を表示する HTML 要素を取得
        let currentSongName = document.getElementById('media-item-title');
        let currentAlbumName = document.getElementById('album-title');
        let currentArtistName = document.getElementById('artist-name');
        let playbackTime = document.getElementById('playback-time');

        // playbackTimeDidChange をトリガーにして再生時間の表示を更新
        music.addEventListener('playbackTimeDidChange', () => {
            playbackTime.textContent = music.player.currentPlaybackTime
        });

        // mediaItemDidChange をトリガーにして再生中の曲名の表示を更新
        music.addEventListener('mediaItemDidChange', () => {
            currentSongName.textContent = music.player.nowPlayingItem.title
        });

        // Play ボタンと player.play() の紐付け
        let playButton = document.getElementById('play');
        playButton.addEventListener('click', async () => {
            await music.player.play();
            // 曲を再生しつつ、紐づく情報の表示を更新
            currentAlbumName.textContent = music.player.nowPlayingItem.albumName
            currentArtistName.textContent = music.player.nowPlayingItem.artistName
        });

        // Pause ボタンと player.pause() の紐付け
        let pauseButton = document.getElementById('pause');
        pauseButton.addEventListener('click', () => {
            music.player.pause();
        });

        // Previous ボタンと player.skipToPreviousItem() の紐付け
        let previousButton = document.getElementById('previous-item');
        previousButton.addEventListener('click', () => {
            music.player.skipToPreviousItem();
        });

        // Next ボタンと player.skipToNextItem() の紐付け
        let nextButton = document.getElementById('next-item');
        nextButton.addEventListener('click', () => {
            music.player.skipToNextItem();
        });

        // アルバムを再生 Queue に登録
        await music.setQueue({
            album: '1542182291' // アルバム ID を渡す
        })

        // アルバム情報取得のプロミス
        let albumInfoPromise = music.api.album(1542182291)
        albumInfoPromise.then((albumData) => {
            let artworkImg = document.getElementById('artwork')
            // アルバムアートの URL を取得
            let artworkURL = MusicKit.formatArtworkURL(albumData.attributes.artwork, 100, 100)
            // アルバムアートを表示
            artworkImg.setAttribute('src', artworkURL)
        })
    })
</script>
</body>
</html>

大事なのは下記の記述で MusicKit JS を読み込んでいるところと。。。

<script src="https://js-cdn.music.apple.com/musickit/v1/musickit.js"></script>

下記の様な感じで MusicKit インスタンスの Promise を作っているところ。

    // MusicKit JS の Promise を作成
    const setupMusicKit = new Promise((resolve) => {
        document.addEventListener('musickitloaded', function () {
            // MusicKit を定義
            MusicKit.configure({
                developerToken: 'Your Developer Token',
                app: {
                    name: 'My MusicKit JS App',
                    build: '2022.11.14'
                }
            })
            // MusicKit のインスタンスで Promise を resolve
            resolve(MusicKit.getInstance())
        })
    });

この辺りは色々やり方があるんだと思いますが、とりあえず「musickitloaded」のイベントを待って MusicKit.configure() を実行する必要があります。

あとは公式ドキュメントを見ながら色々遊べればと。。。

ちなみに Apple Music API という、アルバムや楽曲などの情報の取得を主とした API もありますが、MusicKit API の MusicKit.API クラスがその役割を担っています。