MastodonのメディアファイルをOpenStack Swift互換オブジェクトストレージに移行する

# はじめに

Mastodonを動かしていると、メディアファイル（写真や動画などの添付ファイル）により、次第にストレージの空きを消費していく。
ストレージの空きがなくなってしまうと、新たなメディアファイルのアップロードが出来なくなるだけでなく、サーバそのものの停止を招く可能性がある。

対策として「定期的に外部のメディアキャッシュを消す(`tootctl media remove`等)」「サーバにストレージを追加する」などの選択肢があるが、
前者はローカルのメディアファイルの増加に対応できず、後者は大幅なコスト増になってしまう。

そこで、第三の選択肢として、従量課金制でファイルを保存しておける「オブジェクトストレージ」にメディアファイルを逃がすことで、その不安を解消することができる。

オブジェクトストレージのサービスを行っている業者は数多くあるが、今回はそのうちのひとつ「[ConoHaオブジェクトストレージ](https://www.conoha.jp/objectstorage/?btn_id=lineup_objectstorage)」に移行する流れを検証してみたいと思う。

ちょうどConoHa VPS上にMastodonを構築し運用しているので、課金とサポートを一本化できることもあり選定した。
ConoHaオブジェクトストレージは`OpenStack Swift `互換のAPIを持ち、転送量に応じた課金がなく、あらかじめ確保した保存領域のサイズによって課金されるのが特徴。


# 検証環境

* ConoHa VPS (Fedora 32)
* ConoHa Object Storage


# ConoHa側の設定

## （持っていなければ）ConoHaにアカウントを作る

ConoHaアカウントおよび有効な課金情報がないとオブジェクトストレージ設定ができない。


## ConoHaのAPIパスワードを設定する

管理画面上の「API」メニューより、「APIユーザー」のパスワードを設定する。
パスワードを忘れてしまった場合、再設定するほかないので注意。


## ConoHaのAPI情報を取得する

以下の情報はこれ以降の設定で使用するので、どこかに控えておく。

**テナント情報**

|項目名|説明|
|---|---|
|テナント名|オブジェクト操作ユーザー名|

**APIエンドポイント**

|項目名|説明|
|---|---|
|Object Storage Service|オブジェクトストレージのエンドポイント|
|Identity Service|認証サーバのエンドポイント|


**API ユーザー**

|項目名|説明|
|---|---|
|ユーザー名|APIユーザー名|
|パスワード|APIパスワード|



# サーバ側の設定

## コマンドラインクライアント`swift`の導入

cURLを使用し、APIに対して直接操作を行うこともできるが、
利便性を考慮し、コマンドラインからファイル等の操作することができる`swift`コマンドを導入する。

`swift`コマンドはPythonによって書かれているため、先に開発パッケージとパッケージマネージャを導入しておく。

```bash:一般ユーザ作業
$ sudo dnf install python-devel gcc
$ sudo dnf install python-pip
```

`swift`コマンドはMastodonユーザが使えるようにする。
（`--user`オプションを外し、`sudo`を使うことで、全ユーザに対して使えるようにすることも可能）


```bash:Mastodonユーザ作業
$ pip install oslo.config==6.11.2 oslo.utils==3.41.5 oslo.serialization==2.29.2 --user
$ pip install python-keystoneclient==3.21.0 python-swiftclient==3.8.1 --user
```

`swift`コマンドの動作確認を行う。

```bash:Mastodonユーザ作業
$ swift stat
Auth version 1.0 requires ST_AUTH, ST_USER, and ST_KEY environment variables
to be set or overridden with -A, -U, or -K.

Auth version 2.0 requires OS_AUTH_URL, OS_USERNAME, OS_PASSWORD, and
OS_TENANT_NAME OS_TENANT_ID to be set or overridden with --os-auth-url,
--os-username, --os-password, --os-tenant-name or os-tenant-id. Note:
adding "-V 2" is necessary for this.
```

認証情報を設定せよとのメッセージが出力されれば、コマンドそのものの導入は成功している。

認証情報はコマンドに対して都度オプションで渡すこともできるが、利便性を考慮して環境変数での設定を行うこととする（このセッション中のみ有効）。
必要に応じて `~/.bashrc` に書き込む。

```bash:Mastodonユーザ作業
$ export OS_AUTH_URL=<認証サーバのエンドポイント>
$ export OS_TENANT_NAME=<テナント名>
$ export OS_USERNAME=<APIユーザー名>
$ export OS_PASSWORD=<APIパスワード>
```

再度、`swift`コマンドの動作確認を行う。

```bash:Mastodonユーザ作業
$ swift stat
                        Account: XXXXXXXXXXXX
                     Containers: 0
                        Objects: 0
                          Bytes: 0
Containers in policy "policy-0": 0
   Objects in policy "policy-0": 0
     Bytes in policy "policy-0": 0
              Meta Temp-Url-Key: XXXX
               Meta Quota-Bytes: 107374182400
                    X-Timestamp: 0000000000.00000
                   Content-Type: text/plain; charset=utf-8
                  Accept-Ranges: bytes
                     X-Trans-Id: XXXXXXXXXXX
```

このような表示がされれば、接続についても正常に行われている。

## コンテナの作成

オブジェクトの保存領域であるコンテナを作成する。

```bash:Mastodonユーザ作業
$ swift post <コンテナ名>
```

コンテナが作成されたか確認する。

```bash:Mastodonユーザ作業
$ swift stat <コンテナ名>
         Account: XXXXXXXXXXXX
       Container: <コンテナ名>
         Objects: 0
           Bytes: 0
        Read ACL:
       Write ACL:
         Sync To:
        Sync Key:
   Accept-Ranges: bytes
X-Storage-Policy: Policy-0
     X-Timestamp: XXXX
    Content-Type: text/plain; charset=utf-8
      X-Trans-Id: XXXXXXXXXXX
```

## コンテナのACL設定

`ACL: Access Control List`。アクセス制限を行うルールのことである。

誰でもコンテナの中身を参照できるよう、アクセス制限を設定する。

```bash
$ swift post <コンテナ名> --read-acl ".r:*"
```

コンテナ作成直後はACL設定が空白になっており、おそらく作成したユーザしか読み書きできないようになっている。
上記例では、誰でもコンテナ内のオブジェクトにアクセスできるようにする。(ただしリスティングはできない)

詳しいACL設定については、OpenStackのドキュメントを参照されたし。

[openstack Documentation - Access Control Lists (ACLs)](https://docs.openstack.org/swift/latest/overview_acl.html)




# サーバ側の設定

メディアファイルの配信方法（どのURLでアクセスさせるか）にはいくつか種類ある。
それぞれメリット・デメリットがあり、設定方法も微妙に違ってくる。

1. **オブジェクトストレージのURLをそのまま使う**
**URI例:** `https://object-storage.tyo1.example.jp/v1/XX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/containter/media_attachments/files/105/591/175/343/825/178/original/1d8ef060e3ebacfa.png`
**Pros:** Mastodonの設定ファイルを触るだけで設定できる。サーバ側のリバースプロキシに特別な設定をしなくてもOK。
**Cons:** URLが長くなる。都度オブジェクトストレージ上のファイルをリクエストしに行くので、リバースプロキシのキャッシュが効かず、表示時のパフォーマンスが落ちる可能性がある。

1. **既存の配信用ディレクトリを仮想ディレクトリに置き換え、あたかもその中にファイルがあるように振舞う。**
**URI例:** `https://mstdn.example.jp/system/media_attachments/files/105/591/175/343/825/178/original/1d8ef060e3ebacfa.png`
**Pros:** 既存のURLを再利用することができるため、メディアファイルへのリンクが切れることを防ぐことができる（メディアファイルを添付したとき、投稿内容にメディアファイルのURLを添付するタイプのクライアントが存在する。また、外部から参照している場合にも効果的）、リバースプロキシのキャッシュが効く。
**Cons:** リバースプロキシ(Nginx)の設定が2か所必要。ファイルシステムを触る必要がある。

1. **配信用の仮想ディレクトリを新設し、あたかもその中にファイルがあるように振舞う。**
**URI例:** `https://mstdn.example.jp/storage/media_attachments/files/105/591/175/343/825/178/original/1d8ef060e3ebacfa.png`
**Pros:** リバースプロキシのキャッシュが効く。
**Cons:** リバースプロキシ(Nginx)の設定が2か所必要。

1. **配信用のサブドメインを切って、あたかもそのサブドメインから配信されているように振舞う。**
**URI例:** `https://media.mstdn.example.jp/media_attachments/files/105/591/175/343/825/178/original/1d8ef060e3ebacfa.png`
**Pros:** リバースプロキシのキャッシュが効く。スケールアウトする際に便利かもしれない。[Mastodon公式ドキュメント](https://docs.joinmastodon.org/admin/optional/object-storage-proxy/)でも紹介されている手法。
**Cons:** リバースプロキシ(Nginx)の設定が2か所必要。サブドメイン用の証明書を取得する必要がある。





## サービス停止

サービスを止めることによって、これ以降メディアファイルが生成されることを防ぐ。

```bash:sudoer作業
$ sudo systemctl stop mastodon-{web,sidekiq,streaming}
```



## Mastodonのメディアファイルの保存先をオブジェクトストレージに切り替える

```bash:Mastodonユーザ作業
$ vim /home/mastodon/live/.env.production
```

```ini:.env.production
# 共通設定内容。この内容をどこかに追記する
SWIFT_ENABLED=true
SWIFT_USERNAME=<APIユーザ名>
SWIFT_PASSWORD=<APIパスワード>
SWIFT_TENANT=<テナント名>
SWIFT_AUTH_URL=<認証サーバのエンドポイント>
SWIFT_CONTAINER=<コンテナ名>
```

### サーバ側設定2: 配信ディレクトリ置き換えの場合

上記の共通設定内容に加え、次の一文を追記する。

```ini:.env.production
# サーバ側設定2、既存の配信用ディレクトリを置き換える
SWIFT_OBJECT_URL=https://mstdn.example.jp/system # ドメイン部を環境に合わせて書き換える
```

### サーバ側設定3: 配信ディレクトリ新設の場合

ここでは例として、`https://mstdn.example.jp/storage` 配下にメディアファイルが存在するかのように振舞う設定を行うものとする。環境に合わせて適宜読み替えること。

上記の共通設定内容に加え、次の一文を追記する。

```ini:.env.production
# サーバ側設定2、既存の配信用ディレクトリを置き換える
SWIFT_OBJECT_URL=https://mstdn.example.jp/storage # ドメイン部、ディレクトリ名を環境に合わせて書き換える
```

### サーバ側設定4: 別ドメインで配信する場合

ここでは例として、`https://media.mstdn.example.jp/` ドメインから配信が行われているように振舞う設定を行うものとする。環境に合わせて適宜読み替えること。

上記の共通設定内容に加え、次の一文を追記する。

```ini:.env.production
# サーバ側設定2、既存の配信用ディレクトリを置き換える
SWIFT_OBJECT_URL=https://media.mstdn.example.jp/ # ドメイン部を環境に合わせて書き換える
```




## メディアファイルへのアクセスをリバースプロキシ(Nginx)経由にする

それぞれの項目が長くなるため、混乱を防ぐために折り畳んであります。
選択した設定項目を展開して確認してください。

### サーバ側設定1: オブジェクトストレージのURLをそのまま使う場合

特別な設定は必要ない。


### サーバ側設定2: 配信ディレクトリ置き換えの場合

`/system/` 以下へのアクセスをリバースプロキシする設定を付加する。

<details><summary>設定項目</summary><div>

```bash:sudoer作業
$ sudo vim /etc/nginx/conf.d/mastodon.conf
```

```nginx:/etc/nginx/conf.d/mastodon.conf

# 前略

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name mstdn.example.jp;

  # 中略

  # location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
  #                  ↓ system/配下を指定している部分を外す
  location ~ ^/(emoji|packs) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Strict-Transport-Security "max-age=31536000";
    try_files $uri @proxy;
  }

  # 中略

  # ↓ここから追記
  # Object Storage
  location /system/ {
    limit_except GET {
      deny all;
    }
    resolver <任意のDNSサーバ>; # (任意) エンドポイントの名前解決用。複数指定する場合はスペース区切りで記入（ラウンドロビン）。
    proxy_pass <オブジェクトストレージのエンドポイント>/<テナント名>/;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    tcp_nodelay on;

    expires 7d; # (任意) ユーザ側でキャッシュしてもらう期間を設定
  }
  # ↑ここまで追記

  # 中略
}

# 後略
```
</div></details>


### サーバ側設定3: 配信ディレクトリ新設の場合

`/storage/` 以下へのアクセスをリバースプロキシする設定を付加する。

<details><summary>設定項目</summary><div>

```bash:sudoer作業
$ sudo vim /etc/nginx/conf.d/mastodon.conf
```

```nginx:/etc/nginx/conf.d/mastodon.conf

# 前略

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name mstdn.example.jp;

  # 中略

  # ↓ここから追記
  # Object Storage
  location /storage/ {
    limit_except GET {
      deny all;
    }

    resolver <任意のDNSサーバ>; # (任意) エンドポイントの名前解決用。複数指定する場合はスペース区切りで記入（ラウンドロビン）。

    rewrite /system/(.*) <オブジェクトストレージのエンドポイントのパス>/<テナント名>/$1 break;
    # /v1/yy_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/mstdn.example.jp/ など

    proxy_pass <オブジェクトストレージのエンドポイントのスキーム>;
    # https://object-storage.tyo2.conoha.io/ など

    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Strict-Transport-Security "max-age=31536000";
    tcp_nodelay on;

  }
  # ↑ここまで追記

  # 中略
}

# 後略
```
</div></details>

### サーバ側設定4: 別ドメインで配信する場合

`https://media.mstdn.example.jp/` 以下へのアクセスをリバースプロキシする設定を付加する。

(検証中)



# ファイル転送

Amazon S3互換オブジェクトストレージであれば、`tootctl upgrade storage-schema`コマンドを使って自動的に切り替えを行ってくれるようだが、OpenStack Swift向けの対応は現時点（Mastodon v3.3.0）ではされていない。
そのため、手動でデータを移行する必要がある。

どれから始めてもよいが、見た目の影響度が大きいものから始めるとよいかもしれない。

```bash:Mastodonユーザ作業
$ cd ~/live/public/system

$ swift upload <コンテナ名> accounts
$ swift upload <コンテナ名> custom_emojis
$ swift upload <コンテナ名> media_attachments
$ swift upload <コンテナ名> preview_cards
$ swift upload <コンテナ名> cache
```

## サーバ側設定2: 配信ディレクトリ置き換えの場合

ファイル転送終了後、ローカルの`public/system`をリネームし、コンフリクトが起こらないようにしておく。

```bash:Mastodonユーザ作業
$ cd ~/live/public
$ mv system system_old
```






# サービス再起動

```bash:一般ユーザ作業
$ sudo systemctl restart nginx
$ sudo systemctl start mastodon-{web,sidekiq,streaming}
```



# 動作確認

* メディアファイルが問題なく表示されていること
* メディアファイルのアップロードがエラーなくできること

を確認する。


# 後処理

切り替え後、しばらく様子を見て問題なさそうであれば、ローカルのメディアファイル（`public/system`）を削除しても差し支えない。


# あとがき

* 自分用にメモした手順です。必要に応じて読み替えてください。
* こうしたほうがいいよ的なアドバイスを頂けると大変助かります
* わたしはこのあたりに生息しています： https://nagoyadon.jp/@kumasun

以上