riq0h.jp/content/post/かゆいところに手が届くインスタンス運用の初級テクニック集.md
Rikuoh 932a35747d
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix
2023-10-03 08:46:28 +09:00

239 lines
14 KiB
Markdown

---
title: "かゆいところに手が届くインスタンス運用の初級テクニック集"
date: 2023-07-22T20:47:25+09:00
draft: false
tags: ['tech']
---
ソロインスタンスを建ててそろそろ2週間が経とうとしている。おかげさまで絶好調だ。本稿では僕がMastodonインスタンスの運用を改善していく上で、手頃ながら日本語情報が乏しかった情報について取り上げる。
## SSLの対応をCloudflareに丸投げする
>**前提**
>・運用中のドメインをCloudflareで管理しているか、またはネームサーバを向けている。
>・CloudflareのSSL/TLS設定で暗号化モードを **フル(厳密)** に設定している。
各種の文献ではインスタンスを建てる過程でLet's Encryptを紹介しているものが多い。確かにこの証明書は様々な用途において気安い選択肢だが、反面、3ヶ月に1回の更新作業が地味に面倒だったり、cronで自動化してもなぜかcertbotがコケていたりと微妙な使い勝手の悪さは否めない。
そこで、僕はSSLの対応をCloudflareに丸投げすることを提案したい。多少の面倒を押してひとたび設定してしまえば以降は二度と証明書の顔を見なくて済む最高の選択肢がここにある。まずはCloudflareのページから運用中のドメインを指定して、**SSL/TLS → オリジンサーバ**へと進む。
![](/img/200.png)
次に「証明書を作成」をクリックして画面下の「作成」を押す。設定項目は特にいじらなくても差し支えない。証明書の有効期限はデフォルトで**15年間**となる。西暦2038年7月22日……。その頃には紙より薄いスマホの上にインスタンスが建っていそうだ。
暗号鍵が作成されると画面上に文字列が現れるので、速やかにコピペして指示通りの拡張子で保存する。この画面は再び開けないため、インスタンスを動かしているサーバ上だけでなく安全なローカル環境にも予備を保存しておくのが望ましい。サーバにそれぞれの鍵を保存したら、先にLet's Encryptで発行したSSL証明書を削除する。以降の操作はroot権限で行う。
```zsh
$ certbot revoke --cert-path /etc/letsencrypt/live/あんたのドメイン名/cert.pem
```
実行確認を承諾すると証明書は直ちに失効される。cronを設定している人は`crontab`を実行して自動更新も忘れずに解除しよう。続いて、nginxの設定に進む。手始めに必要なディレクトリを作成してCloudflareの暗号鍵を移動する。
```zsh
$ mkdir /etc/ssl/certs
$ mkdir /etc/ssl/private
$ mv あんたのドメイン名.pem /etc/ssl/certs/あんたのドメイン名.pem
$ mv あんたのドメイン名.key /etc/ssl/private/あんたのドメイン名.key
```
移動したら任意のエディタでnginxの設定ファイルを編集する。
```zsh
$ vim /etc/nginx/sites-available/あんたのドメイン名.conf
ssl_certificate /etc/ssl/certs/あんたのドメイン名.pem;
ssl_certificate_key /etc/ssl/private/あんたのドメイン名.key;
```
編集後、念のために`nginx -t`でエラーを確認して問題がなければ`systemctl restart nginx`でnginxを再起動する。Web UIに接続して証明書の有効性が確認できたら作業は完了だ。
## データベースのバックアップをCloudflareに丸投げする
>**前提**
>・Docker環境でインスタンスを動かしている。
誉れ高き丸投げシリーズその2。どんどん丸投げしていこう。我々はすでに巨人の肩に乗っているし、どうせ今さら降りることなどできない。Cloudflareのページで**R2 → 概要**と進んでバケットを作成する。バケットの名前はなんでも構わない。ついでに設定から自動削除をスケジュールすると容量の節約になる。
![](/img/201.png)
次にwranglerを導入する。wranglerはCloudflareのWorkerをCLIで動かすツールだが、**ほとんどのインスタンス運営者が使用しているUbuntuやDebianでは簡単にインストールが行えない。** aptコマンドでインストールされるNode.jsのバージョンが古すぎるためにエラーを起こしてしまうのだ。したがって、wranglerを導入する**前に**最新のNode.jsをインストールしなければならない。
```zsh
# すでに古いNode.jsが入っている場合はアンインストールする。
$ apt purge nodejs
$ curl -fsSL https://deb.nodesource.com/setup_current.x | bash -
$ apt install nodejs
```
dpkgに怒られが発生した時は`sudo dpkg -i --force-overwrite /var/cache/apt/archives/nodejs_20.5.0-deb-1nodesource1_amd64.deb`で強制的に上書きするとうまくいく。バージョン部分の`20.5.0`は本稿執筆時点での最新の数字なので適宜書き換えられたし。
最新のNode.jsを手に入れたところでようやくwranglerのインストールに入る。npmを導入していなければこれも`apt install npm`で予めインストールする。
```zsh
$ npm create cloudflare@latest
# この後、色々訊かれるが指定すべき選択肢は以下の通り。
・Ok to proceed?
→ Yes
・In which directory do you want to create your application?
→ プロジェクトディレクトリの命名を行う。適当な名前でいい。
・What type of application do you want to create?
type Scheduled Worker (Cron Trigger)
・Do you want to use TypeScript?
→ no
・Do you want to deploy your application?
→ no
```
作業が完了したら命名したディレクトリに移り、`npx wrangler login`を実行する。URLが表示されるのでコピペしてブラウザに貼り付けるとCloudflareの認証画面が現れる。**しかし、SSH越しに実行しているかぎりこの認証は絶対に失敗する。** 認証情報をlocalhostに渡しているせいで照合が成立しないからだ。今回はちょっとした荒業でこいつをくぐり抜けたい。
一旦、愚直にコピペしたURLで認証を行なって失敗してみると、ブラウザのアドレス欄からlocalhostの8976番ポートと通信を試みていた形跡がうかがえる。つまり、このURLをサーバの固定IPアドレスに書き換えれば期待通りの挙動に変化すると考えられる。そこでまずはサーバ側の8976番ポートを開けて準備を整える。ufwはとても簡便なファイアウォールフロントエンドなので、知らなくともぜひ導入してみてほしい。
```zsh
$ ufw allow 8976
$ ufw reload
```
ポートを開いた状態で例のURLの`localhost:8976`の部分を`あんたのサーバのIPアドレス:8976`に書き換えてエンターを押す。うまくいけばたちまち認証が完了してサーバ上でwranglerが使えるようになる。開いたポートはもう使わないので確実に閉じておく。
```zsh
$ ufw deny 8976
$ ufw reload
```
以降はインスタンスを動かしているユーザに切り替えて非root環境で作業を行う。ここまで来たところで、試しにバックアップ作業を手動で実行する。
```zsh
# pg_dumpでバックアップを取得してgzipで固める。userとdbの部分は各自の環境に合わせて変更すること。
$ sudo docker exec mastodon-db-1 pg_dump -Fc -U user db | gzip -c >> backup.gz
# バックアップファイルに権限を与える。
$ chmod 774 ./backup.gz
# wranglerのプロジェクトディレクトリに移動する。
$ cd /home/ユーザ/プロジェクトディレクトリ
# バックアップファイルをCloudflare R2にアップロードする。この際、ファイル名を現在時刻に書き換える。
$ sudo npx wrangler r2 object put "あんたのバケット名/$(date +\%Y\%m\%d_\%H-\%M-\%S).gz" --file=/home/ユーザ/インスタンスのディレクトリ/backup.gz
# 元のバックアップファイルを削除する。
$ rm /home/ユーザ/インスタンスのディレクトリ/backup.gz
```
一連の動作が間違いなく完了するのを確認した上で、同様の処理内容をシェルスクリプトにしたためる。これでいつでもワンタッチでバックアップを巨人の口にねじ込めるという寸法だ。作成したスクリプトは`chmod +x ファイル名.sh`で実行権限を与えてから`sudo ./ファイル名.sh`で発動する。
```zsh
#!/bin/bash
echo "Backup begin..."
cd /home/ユーザ/インスタンスのディレクトリ/
docker exec mastodon-db-1 pg_dump -Fc -U user db | gzip -c >> backup.gz
chmod 744 ./backup.gz
echo "Success!"
su - ユーザ << bash
echo "Uploading to Cloudflare R2..."
cd /home/ユーザ/プロジェクトディレクトリ/
npx wrangler r2 object put "あんたのバケット名/$(date +\%Y\%m\%d_\%H-\%M-\%S).gz" --file=/home/ユーザ/インスタンスのディレクトリ/backup.gz
rm /home/ユーザ/インスタンスのディレクトリ/backup.gz
bash
```
上記のスクリプトをcronに登録するとバックアップ作業の自動化が達成できる。
```zsh
# rootで実行する。
sudo crontab -u root -e
# 毎日午前5時に指定された場所のスクリプトを実行する。別に好きな時間でいい。
0 5 * * * sh /home/ユーザ/ファイル.sh
```
最高だね。面倒なことは全部機械にやらせよう。ただし、cronのやつは油断すると裏切るのでたまにCloudflareのバケットを見に行った方がいいかもしれない。
## Mastodonのリモートメディアを確認して削除する
>**前提**
>・Docker環境でMastodonインスタンスを動かしている。
Web UIのサーバ設定でもリモートメディアを自動削除するようにできるが、具体的に何GBのキャッシュが存在していて何GBぶん減らせたのか判らないところがちょっと物足りない。下記の平易なスクリプトでそれを補える。
```zsh
#!/bin/bash
cd /home/ユーザ/インスタンスのディレクトリ/
echo "Check media usage..."
docker-compose run web bundle exec bin/tootctl media usage
read -p "Enter to proceed..."
echo "Removing..."
docker-compose run web bundle exec bin/tootctl media remove -d 1
echo "Done."
```
この記述例ではメディアの使用量を照会した後に処理の続行を確認して、Enterキーを押すと24時間以前のリモートメディアが削除される。予期せぬ請求やストレージの圧迫を避けるためにもそれなりの頻度で実施しておきたい。
## Mastodonの投稿読み込み数上限を破壊する
>**前提**
>・Docker環境でMastodonインスタンスを動かしている。
Mastodonは投稿の読み込み数に制限がある。おそらく負荷対策だろう。過去の投稿は最大で800までしか読み込めない。いちユーザの立場では変えられないゆえ不便を被っている者も少なくないと思われるが、我々は圧倒的権力を誇る鯖缶だ。いくらでも好きな数字に書き換えられる。
```ruby
# mastodon/app/lib/feed_manager.rb
MAX_ITEMS = 2000
```
さしあたり僕は2000にした。編集後は`sudo docker-compose build`で再ビルドしなければ反映されない。これで深夜帯に蓄積された投稿の一部しか読めないなどという理不尽から解き放たれる。
## Mastodonの画像リサイズ制限を破壊する
>**前提**
>・Docker環境でMastodonインスタンスを動かしている。
Mastodonは画像の最大解像度が1080p相当に抑えられている。多くのユーザを限られたリソースで支える状況下ではやむをえないが、ソロインスタンスの支配者には意味のない制約だ。4K画質相当まで上げてしまおう。
```javascript
// resize_image.js
const MAX_IMAGE_PIXELS = 8847360;
```
```ruby
# media_attachment.rb
IMAGE_STYLES = {
original: {
pixels: 8_847_360,
file_geometry_parser: FastGeometryParser,
}.freeze,
```
こっちも再ビルドを忘れてはならない。もちろん4Kを越える解像度のカメラやディスプレイを持っている人は8Kなどにしてもよい。
## Mastodonの文字数上限を破壊する
>**前提**
>・Docker環境でMastodonインスタンスを動かしている。
500文字もあれば十分と思いきや、ここ一番の時に足りない場合が意外とあったりする。実装系にもよるがだいたいどれも8000文字くらいは受け取れるらしいので不要は制限は予め取り払っておいた方が楽だ。さしあたり僕は9999文字に設定した。ここでも2つのファイルを編集するが、当該のファイル内を「500」で検索すれば容易に修正箇所を見つけることができる。
```ruby
# mastodon/app/javascript/mastodon/features/compose/components/compose_from.javascript
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 9999 || (isOnlyWhitespace && !anyMedia));
};
<CharacterCounter max={9999} text={this.getFulltextForCharacterCounting()} />
```
```ruby
# mastodon/app/validators/status_length_validator.rb
class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 9999
```
余談だが、最近華々しいリニューアルを果たしたMisskeyフォークの[Firefish](https://joinfirefish.org)は一瞬だけ最大文字数を2億5000万文字に設定できたらしい。いい心意気だ。