Vultr + Arch Linux で Mastodon を構築する

以前から自分専用のマイクロブログを立ち上げたいと思っていた。

X や Facebook、Instagram といったみんながよく使っている、いわゆる中央集権型 SNS はどうも投稿し続けていくには気乗りがしない。 その原因を考えるに、全てのデータが運営会社に管理される分が気に入らないのだと思う。

買収や破綻したときに全てのデータが失われてしまうリスクがあるので、ライフログとしては向かない。 自由な意見を書き込めば場合によっては、アカウントが凍結されるかもしれない。

そういった問題を解決するための仕組みとして、分散型 SNS の Fediverseいう仕組みがある。

Fediverse は、共通のプロトコルを使用して、異なるソフトウェアやサービスが互いに通信できるように設計されている。 最も一般的なプロトコルは Mastodon や Misskey が採用している ActivityPubである。

ActivityPub を使用することで、異なるサーバやプラットフォームに所属するユーザ同士がフォロー、メッセージ送信、投稿の共有などを行うことが可能になる。

これがまさに私の求めていたもので、自由に発信し、ライフログとしての役割を果たすための最適なプラットフォームとなり得ると考えた。

Fediverse を使えば、自分専用のマイクロブログを立ち上げ、それを自分で管理できる。 これにより、コンテンツの所有権を完全に保持し、外部の制約やリスクから解放される。 また、他の Fediverse ユーザーと繋がりながらも、自分のペースで投稿し続けることができるのも素晴らしい。

かくして、私は自分専用のマイクロブログを立ち上げる決意を固めた。

※ ↓ が今回立ち上げたインスタンス。

技術選定

どのソフトウェアを使うか

Fediverse の構成要素の 1 つとなるソフトウェアを選定するにあたって、FLOSSであることは前提条件である。

その上で、プロトコルを選んでいくわけだが、現在 Fediverse で最も広く採用されているプロトコルである ActivityPub を選ぶのが自然の流れだと思う。 最近では Threads も ActivityPub に対応したし(ベータ版だが)、Tumblr も今後対応する予定なので将来性もある。

ActivityPub を採用しているソフトウェアはたくさんあるが、消去法で Mastodon にした。 Misskey も選択肢の 1 つだったが、破壊的変更が多いのと、独特の文化が合わないと感じたので止めた。

他にもバックエンドが Elixir で書かれた Pleromaいった技術的興味を惹かれるソフトウェアもある。 今後、移行する可能性もあるが、ひとまず Mastodon を使って構築していく。

どのサーバを使うか

Mastodon のホスティングには、以前から興味があった Vultr選択した。 Vultr は基本的な料金が安いのに加え、東京にデータセンターがあることが大きな魅力だ。 特に、低価格なプランが豊富で、プロジェクトの規模に応じた柔軟な選択が可能な点が気に入っている。

肝心のメモリは 2GB にした。 Mastodon はメモリ消費が多いことがデメリットだが、おひとり様インスタンスであればメモリ使用量を監視しながら適切にチューニングすることで、問題なく運用できると考えている。

Vultr はプランの多様性も魅力の一つで、必要に応じてリソースを増やせる柔軟性がある。 今回選択したのは High Frequency プランである。 ストレージは 64GB の NVMe で、高速なデータ転送が可能だ。 CPU は 3GHz 以上の Intel Xeon の 1 コアを選んだ。 AMD の CPU も選べるが、シングルスレッドの性能では Intel が依然として強いイメージがあり、その信頼性に基づいて Intel を選択した。

Mastodon にアップロードした画像は Cloudflare R2保存する。 AWS の S3 が推奨される方法であるが、できるだけ料金を抑えたいので R2 にした。

どの OS を使うか

Mastodon の公式ドキュメントでは、主に Ubuntu や Debian といったディストリビューションが推奨されている。 これらは広く使われており、サポートやドキュメントが充実しているため、特にサーバ環境においては非常に安定している選択肢だ。 しかし、個人的に慣れている軽量かつシンプルな Arch Linux使うことに決めた。

本来、Arch Linux はサーバ用途としてはあまり向いていないとされている。 その理由は、主にローリングリリースモデルの特性によるもので、頻繁な更新によって予期せぬトラブルや互換性の問題が発生するリスクがあるからだ。 例えば、システムの更新が原因で依存関係が崩れたり、サービスが一時的に停止したりする可能性がある。 これについては、以下の動画で詳しく解説されている。

それにもかかわらず、Arch Linux を使う理由は、単純にArch は最高」と思っているからに他ならない。 Arch のミニマルなアプローチ、シンプルな構成、そして何よりもカスタマイズの自由度は、他のディストリビューションでは得られないものだ。

とはいえ、基本的な考え方操作は、どのディストリビューションでも共通している部分が多い。 Mastodon のようなオープンソースのソフトウェアは、様々な環境で動作するよう設計されているため、特定のディストリビューションに依存することなく自由に選ぶことができる。 プライベートなインスタンスであれば、好きなのを使えば良いと思う。

肝心の料金

Vultr でサーバを 1 ヶ月間、24 時間フル稼働させた場合、基本料金として $12 かかる。 現在の為替レートを考慮すると、円安の影響で $1 はおよそ ¥145 となり、月額の費用は約 1,750 円になる。

画像等のアップロード先である R2 では従量課金制が採用されているが、特筆すべきはエグレス料金が無料であることだ。 エグレス料金とは、データをストレージから外部に出す際に発生する費用のことで、他のクラウドサービスプロバイダでは一般的に発生する料金である。 しかし、R2 ではこのエグレス料金がかからないため、コストを大幅に抑えることができる。

特に、個人で運用する Mastodon のインスタンスにおいては、ユーザが大量に画像を投稿しない限り、R2 の無料利用枠内で十分にやりくりすることが可能だ。 ドメイン代を除いて、実質 Vultr の料金しかかからない。 毎月払うにはまあまあ高い金額だが、これより低い金額のサーバだと満足できるスペックがないから仕方ない。

Mastodon の構築方

早速 Mastodon を構築していく。 実行環境は macOS。

まずは SSH を使ってリモートサーバに接続する。

Terminal window
ssh root@123.456.78.90

Vultr から発行された root パスワードを念の為、変更しておく。

Terminal window
passwd

次に、リモートサーバとの接続を切って、ローカルシェルに戻る。 そして、新しい SSH キーを生成する。 最後に、作成した SSH キーの公開鍵をリモートサーバに追加して、公開鍵認証を追加する。

Terminal window
exit
ssh-keygen -t ed25519 -C "your_email@example.com"
ssh-copy-id -i ~/.ssh/id_ed25519_vultr.pub root@123.456.78.90

これで、今後リモートサーバにログインする際に、パスワードを入力する代わりに公開鍵認証を使えるようになる。 これは、セキュリティの向上や利便性のために必ず行わなければならないルーティンである。

必要に応じて、以下のように .ssh/config変更しておくと後々の作業が楽になる。

.ssh/config
Host vultr
Hostname 123.456.78.90
User root # 後で mastodon に変更する
Port 22
IdentityFile ~/.ssh/id_ed25519_vultr

上記の設定をした上で再度リモートサーバに接続する。

Terminal window
ssh vultr

Vultr の Arch Linux の状態を確認しておく。

まずは、メモリ使用状況から。

Terminal window
free
total used free shared buff/cache available
Mem: 2013236 258740 1732296 828 162000 1754496
Swap: 6451196 0 6451196

ありがたいことに、初めからスワップが 6GB 設定されている。

次に、カーネルバージョンを確認する。

Terminal window
uname -r
6.6.42-1-lts
pacman -Syu
reboot
uname -r
6.6.45-1-lts

インスタンスの起動直後はカーネルが古くなっていることがあるため、初めからインストールされているパッケージと併せてアップグレードしておく。

インストール済みのパッケージ一覧を出力。

Terminal window
pacman -Q
acl 2.3.2-1
archlinux-keyring 20240709-1
argon2 20190702-6
attr 2.5.2-1
audit 4.0.1-3
autoconf 2.72-1
automake 1.17-1
base 3-2
base-devel 1-1
bash 5.2.032-1
bind 9.20.0-1
binutils 2.43+r4+g7999dae6961-1
bison 3.8.2-6
brotli 1.1.0-2
bzip2 1.0.8-6
ca-certificates 20240618-1
ca-certificates-mozilla 3.103-1
ca-certificates-utils 20240618-1
cdrtools 3.02a09-6
cloud-guest-utils 0.33-2
cloud-image-utils 0.33-2
cloud-init 24.1-2
cloud-utils 0.33-2
coreutils 9.5-1
cracklib 2.10.2-1
cronie 1.7.2-1
cryptsetup 2.7.4-1
curl 8.9.1-2
db5.3 5.3.28-5
dbus 1.14.10-2
dbus-broker 36-4
dbus-broker-units 36-4
dbus-units 36-4
debugedit 5.0-6
device-mapper 2.03.25-2
dhclient 4.4.3.P1-3
diffutils 3.10-1
ding-libs 0.6.2-2
dnssec-anchors 20190629-4
duktape 2.7.0-7
e2fsprogs 1.47.1-4
efibootmgr 18-3
efivar 39-1
ethtool 1:6.9-1
expat 2.6.2-1
expect 5.45.4-5
fakeroot 1.35-1
file 5.45-1
filesystem 2024.04.07-1
findutils 4.10.0-1
flex 2.6.4-5
fuse-common 3.16.2-1
fuse3 3.16.2-1
gawk 5.3.0-1
gc 8.2.6-1
gcc 14.2.1+r32+geccf707e5ce-1
gcc-libs 14.2.1+r32+geccf707e5ce-1
gdbm 1.24-1
gettext 0.22.5-1
git 2.46.0-1
glib2 2.80.4-1
glibc 2.40+r16+gaa533d58ff-2
gmp 6.3.0-2
gnu-netcat 0.7.1-10
gnupg 2.4.5-4
gnutls 3.8.6-1
gpgme 1.23.2-6
gpm 1.20.7.r38.ge82d1a6-6
gptfdisk 1.0.10-1
grep 3.11-1
groff 1.23.0-6
grub 2:2.12-2
gssproxy 0.9.2-1
guile 3.0.10-1
gzip 1.13-4
hwdata 0.385-1
iana-etc 20240612-1
icu 75.1-1
iotop 0.6-11
iproute2 6.10.0-2
iptables-nft 1:1.8.10-2
iputils 20240117-1
jansson 2.14-4
jemalloc 1:5.3.0-4
jq 1.7.1-2
json-c 0.17-2
kbd 2.6.4-1
keyutils 1.6.3-3
kmod 32-1
krb5 1.21.3-1
less 1:661-1
libaio 0.3.113-3
libarchive 3.7.4-1
libassuan 3.0.0-1
libbpf 1.4.3-1
libcap 2.70-1
libcap-ng 0.8.5-2
libdaemon 0.14-6
libedit 20240517_3.1-1
libelf 0.191-4
libevent 2.1.12-4
libffi 3.4.6-1
libgcrypt 1.11.0-2
libgpg-error 1.50-1
libidn2 2.3.7-1
libisl 0.26-2
libksba 1.6.7-1
libldap 2.6.8-1
libmaxminddb 1.10.0-1
libmm-glib 1.22.0-1
libmnl 1.0.5-2
libmpc 1.3.1-2
libndp 1.9-1
libnetfilter_conntrack 1.0.9-2
libnewt 0.52.24-2
libnfnetlink 1.0.2-2
libnftnl 1.2.7-1
libnghttp2 1.62.1-1
libnghttp3 1.4.0-1
libnl 3.10.0-1
libnm 1.48.8-1
libnsl 2.0.1-1
libp11-kit 0.25.5-1
libpcap 1.10.4-1
libpgm 5.3.128-3
libpipeline 1.5.7-2
libpsl 0.21.5-2
libpwquality 1.4.5-5
libsasl 2.1.28-4
libseccomp 2.5.5-3
libsecret 0.21.4-1
libsodium 1.0.20-1
libssh2 1.11.0-1
libsysprof-capture 46.0-4
libtasn1 4.19.0-2
libteam 1.32-2
libtirpc 1.3.5-1
libtool 2.5.1-2
libunistring 1.2-1
liburcu 0.14.0-2
liburing 2.6-2
libusb 1.0.27-1
libutempter 1.2.1-4
libuv 1.48.0-2
libverto 0.3.2-5
libxcrypt 4.4.36-2
libxml2 2.13.3-1
libyaml 0.2.5-3
licenses 20240728-1
linux-api-headers 6.10-1
linux-firmware 20240703.e94a2a3b-1
linux-firmware-whence 20240703.e94a2a3b-1
linux-lts 6.6.45-1
linux-lts-headers 6.6.45-1
lmdb 0.9.32-1
lsof 4.99.3-2
lvm2 2.03.25-2
lz4 1:1.10.0-2
m4 1.4.19-3
make 4.4.1-2
man-db 2.12.1-1
mkinitcpio 39.2-2
mkinitcpio-busybox 1.36.1-1
mobile-broadband-provider-info 20240407-1
mpdecimal 4.0.0-2
mpfr 4.2.1-4
nano 8.1-1
ncurses 6.5-3
net-tools 2.10-2
nettle 3.10-1
networkmanager 1.48.8-1
nfs-utils 2.6.4-1
nfsidmap 2.6.4-1
nftables 1:1.1.0-2
npth 1.7-1
nspr 4.35-3
nss 3.103-1
numactl 2.0.18-1
oniguruma 6.9.9-1
openssh 9.8p1-1
openssl 3.3.1-1
p11-kit 0.25.5-1
pacman 6.1.0-3
pacman-mirrorlist 20240717-1
pacutils 0.14.0-1
pahole 1:1.27-2
pam 1.6.1-2
pambase 20230918-1
parted 3.6-2
patch 2.7.6-10
pciutils 3.13.0-1
pcre 8.45-4
pcre2 10.44-1
pcsclite 2.3.0-1
perl 5.38.2-2
perl-clone 0.46-3
perl-data-dump 1.25-5
perl-encode-locale 1.05-12
perl-error 0.17029-6
perl-file-listing 6.16-3
perl-html-parser 3.82-1
perl-html-tagset 3.24-1
perl-http-cookiejar 0.014-2
perl-http-cookies 6.11-1
perl-http-daemon 6.16-3
perl-http-date 6.06-2
perl-http-message 6.46-1
perl-http-negotiate 6.01-13
perl-io-html 1.004-5
perl-io-socket-ssl 2.085-1
perl-json 4.10-2
perl-libwww 6.77-1
perl-log-message 0.08-10
perl-log-message-simple 0.10-10
perl-lwp-mediatypes 6.04-5
perl-lwp-protocol-https 6.14-1
perl-mailtools 2.21-8
perl-net-http 6.23-3
perl-net-ssleay 1.94-1
perl-term-readline-gnu 1.46-3
perl-term-ui 0.50-4
perl-timedate 2.33-6
perl-try-tiny 0.31-4
perl-uri 5.28-1
perl-www-robotrules 6.02-13
pinentry 1.3.1-5
pkgconf 2.1.1-1
polkit 125-1
popt 1.19-1
procps-ng 4.0.4-3
psmisc 23.7-1
python 3.12.4-1
python-attrs 23.2.0-3
python-cffi 1.16.0-2
python-charset-normalizer 3.3.2-2
python-configobj 5.0.8-5
python-cryptography 42.0.6-1
python-filelock 3.13.3-2
python-idna 3.7-1
python-jinja 1:3.1.4-1
python-jsonpatch 1.33-2
python-jsonpointer 3.0.0-1
python-jsonschema 4.23.0-1
python-jsonschema-specifications 2023.12.1-2
python-markupsafe 2.1.5-2
python-netifaces 0.11.0-5
python-oauthlib 3.2.2-3
python-packaging 24.1-1
python-pip 24.2-1
python-pycparser 2.22-2
python-pyrsistent 0.19.3-4
python-pyserial 3.5-6
python-referencing 0.35.1-1
python-requests 2.32.3-1
python-rpds-py 0.19.0-1
python-six 1.16.0-9
python-typing_extensions 4.12.2-1
python-urllib3 1.26.19-1
python-wheel 0.44.0-1
python-yaml 6.0.1-4
qemu-img 9.0.2-1
readline 8.2.013-1
reflector 2023-2
rpcbind 1.2.7-1
run-parts 5.17-1
screen 4.9.1-2
sed 4.9-3
shadow 4.16.0-1
slang 2.3.3-3
sqlite 3.46.0-1
sudo 1.9.15.p5-2
systemd 256.4-1
systemd-libs 256.4-1
systemd-sysvcompat 256.4-1
tar 1.35-2
tcl 8.6.14-4
texinfo 7.1-2
thin-provisioning-tools 1.1.0-1
tpm2-tss 4.0.1-1
trizen 1:1.68-1
tzdata 2024a-2
ufw 0.36.2-4
unzip 6.0-21
util-linux 2.40.2-1
util-linux-libs 2.40.2-1
vim 9.1.0672-1
vim-runtime 9.1.0672-1
watchdog 5.16-2
wget 1.24.5-3
which 2.21-6
wpa_supplicant 2:2.11-2
xz 5.6.2-1
zeromq 4.3.5-2
zip 3.0-11
zlib 1:1.3.1-2
zstd 1.5.6-1

Arch Linux といえば、最初は何も入っていないイメージだったが、親切にも必要最低限のパッケージがインストール済みである。 正直なところ、ありがた迷惑。

次に日付の設定を行っていく。

デフォルトのタイムゾーンは UTC なので、これを JST(日本標準時)に変更する。

Terminal window
date
Tue Aug 13 06:11:30 AM UTC 2024
ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
date
Tue Aug 13 03:12:01 PM JST 2024

次にロケールを変更する。 デフォルトでは en_US設定されている。 また、システムで利用可能なロケールには ja_JPないため、/etc/locale.gen追加が必要。

Terminal window
echo $LANG
en_US.UTF-8
locale -a
C
C.utf8
en_US.utf8
POSIX
vim /etc/locale.gen
/etc/locale.gen
ja_JP.UTF-8 UTF-8

locale-genシステムで利用できるロケールを作成する。

Terminal window
locale-gen
locale -a
C
C.utf8
en_US.utf8
ja_JP.utf8
POSIX

locale -a実行すると ja_JP追加されていることを確認できる。

次に、デフォルトのロケールを変更するために /etc/locale.conf修正する。

Terminal window
vim /etc/locale.conf
/etc/locale.conf
# LANG=en_US.UTF-8
LANG=ja_JP.UTF-8

最後にインスタンスを再起動させることで変更が反映される。

Terminal window
reboot

一応、確認。

Terminal window
echo $LANG
ja_JP.UTF-8

root に直接ログインするのは好ましくないので mastodon というユーザを作成して、パスワードを設定する。 作成したユーザは管理者グループ wheel に所属させる。

systemd-homedいう新しいユーザ管理方法もあるが、ここでは慣れている古典的な方法でユーザを作成した。

Terminal window
useradd -mg wheel mastodon
passwd mastodon

wheel グループのユーザが sudo コマンドを使用して、一時的に特権を得られるようにするために、/etc/sudoers ファイルの編集を行う。

Terminal window
EDITOR=vim visudo

sudo利用するときに、パスワード入力を要求されないほうが使いやすいので、セキュリティ的には弱くなるが、パスワード入力なしで sudo コマンドを使用できるように設定しておく。

/etc/sudoers
# %wheel ALL=(ALL:ALL) NOPASSWD: ALL
%wheel ALL=(ALL:ALL) NOPASSWD: ALL

次は SSH 接続時の設定を変更する。

Terminal window
vim /etc/ssh/sshd_config

PermitRootLogin no設定することで、root ユーザが直接リモートログインすることを禁止している。 また、PasswordAuthentication noパスワードを使ったログインを無効にし、公開鍵などの認証方式を用いることを強制してセキュリティを強化した。 これは公開鍵を紛失したら二度とサーバにログインできないことを意味する。

/etc/ssh/sshd_config
PermitRootLogin yes
PermitRootLogin no
PasswordAuthentication yes
PasswordAuthentication no

最後に SSH サーバを有効化して上記の設定を反映させる。

Terminal window
systemctl restart sshd

次に不正アクセスや攻撃からサーバを保護するためのセキュリティツールである fail2banインストールする。

Terminal window
pacman -S fail2ban

fail2ban の設定ファイルは、一般的に jail.confデフォルトの設定が記述されており、ユーザが変更を加える場合は jail.localいうファイルに設定をコピーし、そのコピーを編集するのが推奨されているのでそれに倣う。

Terminal window
cp /etc/fail2ban/jail.{conf,local}
vim /etc/fail2ban/jail.local

以下の編集をして、SSH サーバの監視を有効にしている。

/etc/fail2ban/jail.local
[sshd]
enabled = true
mode = aggressive

以下のコマンドで fail2ban サービスを即座に起動し、さらにシステム起動時に自動的に開始するように設定する。

Terminal window
systemctl enable --now fail2ban

現在 fail2ban がシステム上でどのように動作しているかを確認する。

Terminal window
fail2ban-client status
Status
|- Number of jail: 1
`- Jail list: sshd

SSH デーモンに対して監視と保護を行っていることを確認できる。

インスタンス作成から 1 週間ぐらい経ったので、どれくらい効果があったのか確かめてみる。

Terminal window
fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 4
| |- Total failed: 8110
| - Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
- Actions
|- Currently banned: 1
|- Total banned: 811
- Banned IP list: xxx.xxx.xxx.xxx

要点だけ確認すると、fail2ban が稼働している期間中に、SSH 接続が失敗した回数の合計が 8,110 回、811 の IP アドレスがブロックされたということである。 思っていたよりも多い。

IP アドレスがブロックされたときに、自分宛に通知メールを送信することもできるが、あまりに件数が多いため有効にしていない。

fail2ban では一度ブロックされた IP アドレスは、設定された時間が経過すると自動的に解除される。 この時間は細かく変更可能。 永久にブロックすることも可能なので、頻繁に同じ IP アドレスが攻撃を試みている場合は、対応した方が良いかもしれない。

次に nftables を使って、パケットフィルタリングを行なっていく。

Vultr で立ち上げた Arch Linux には、あらかじめ nftables と iptables-nft がインストールされていた。 以下のコマンドで現在定義されているすべてのテーブルをリストアップする。

Terminal window
nft list tables
table ip filter
table ip6 filter

iptables-nft は使わないため、nftables の設定ファイルである /etc/nftables.conf編集して、自分でルールを設定していく。

Terminal window
vim /etc/nftables.conf

設定の意味はコード内にコメントとして記載した。

/etc/nftables.conf
#!/usr/sbin/nft -f
# vim:set ts=2 sw=2 et:
# 現在の全ルールセット(テーブル、チェーン、ルール)を削除
flush ruleset
table inet filter {
# 受信パケットに対して適用される設定
chain input {
# 優先度は 0 であり、デフォルトのポリシーは drop
# これは、受信パケットがこのチェーンに到達した場合、ルールに一致しない限り、全てのパケットを拒否することを意味する
type filter hook input priority 0; policy drop;
# ループバックアドレスへのアクセスをローカルのループバックインターフェースからのものだけに制限し、それ以外のインターフェースからのアクセスを防ぐ
iif "lo" accept
ip daddr 127.0.0.0/8 iif != "lo" reject
# すでに確立されたコネクションや既存のコネクションに関連するパケットを許可する
# これにより、正常な通信の流れが維持され、セッションが途切れたり中断されたりすることが防ぐ
ct state established,related accept
# ウェブサーバが HTTP および HTTPS のリクエストを受け入れられるようにする
tcp dport 80 accept
tcp dport 443 accept
# 特定のアプリケーションやプロトコルが UDP ポート 443 を使用する場合にそのトラフィックを許可する
udp dport 443 accept
# リモートからの SSH 接続要求を受け入れる
tcp dport 22 ct state new accept
# ネットワーク接続の確認やトラブルシューティングのために、外部からの ping 要求を受け入れる
icmp type echo-request accept
# ネットワーク上の障害やエラーを診断するのに役立つ情報を保存する
# Allow destination unreachable messages, especially code 4 (fragmentation required) is required or PMTUD breaks
icmp type destination-unreachable accept
# nftables でパケットがこのルールに一致した場合、nftables denied: というプレフィックス付きでログが記録され、1 分間に最大 5 回のログを生成する
limit rate 5/minute log prefix "nftables denied: " level debug
# 上記で設定したルールに沿わない場合は接続を拒否する
reject
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}

以下のコマンドで nftables サービスを即座に起動し、さらにシステム起動時に自動的に開始するように設定する。

Terminal window
systemctl enable --now nftables

nftables の設定が正しく行われているかテストしていく。 まずは構文チェックから。

Terminal window
nft -c -f /etc/nftables.conf

問題なければ、特に何も表示されない。 念の為、現在の設定を確認する。

Terminal window
nft list ruleset

先ほど設定した内容が反映されていれば OK。

次にログが記録されているか確認する。

Terminal window
dmesg | grep "nftables denied:"
[631369.913820] nftables denied: IN=xxx OUT=xxx MAC=xxx SRC=xxx DST=xxx LEN=xxx TOS=xxx PREC=xxx TTL=xxx ID=xxx PROTO=xxx SPT=xxx DPT=xxx WINDOW=xxx RES=xxx SYN URGP=xxx

上記のような形式のログが出力されていれば正常に設定がされている。

これで nftables の設定は完了である。

次に必要なパッケージをインストールしていく。

Terminal window
pacman -S imagemagick ffmpeg postgresql postgresql-libs redis yaml-cpp libxml2 libxslt protobuf pkg-config jemalloc nginx certbot certbot-nginx libidn

以降の処理は mastodon ユーザにログインしてから行う。

Terminal window
su - mastodon

Arch Linux に標準で付属しているパッケージマネージャである pacman は公式リポジトリのみ対応している。 公式リポジトリに存在しないパッケージを扱うために AUR(Arch User Repository)というユーザコミュニティによって提供される非公式リポジトリが存在する。

AUR のパッケージを効率的に管理するため、paruいう Rust 製の AUR ヘルパーを使用していく。 以下はそのインストール方法。

Terminal window
git clone https://aur.archlinux.org/paru.git
cd paru
less PKGBUILD
makepkg -si
cd ..
rm -rf paru
sudo pacman -Rs rust

paru のインストールとビルドが完了したら、ビルド用ファイルは不要なので削除する。 Rust は今後使うことはないため、アンインストールしておく。

続いて、Ruby のパッケージマネージャである rbenv AUR からインストールする。

Terminal window
paru -S rbenv ruby-build
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install 3.2.3
rbenv init
echo 'eval "$(rbenv init - bash)"' >> ~/.bash_profile
source ~/.bash_profile
rbenv global 3.2.3
gem install bundler --no-document

Ruby のインストール時は高性能なメモリアロケータである jemalloc指定しておく。 jemalloc が使用されることで実行時のメモリ管理が改善され、特に高負荷の環境でのパフォーマンスが向上する1

次にデータベースの設定を行うので、postgres ユーザとして新しいシェルを開く。

Terminal window
sudo -iu postgres

postgres ユーザに切り替えたら、PostgreSQL データベースクラスタを初期化していく。 環境変数 $LANG には先ほど設定した ja_JP.UTF-8使用される。

Terminal window
initdb --locale $LANG -E UTF8 -D '/var/lib/postgres/data'

続いて、PostgreSQL のデータベースサーバのパフォーマンスをチューニングしていく。 具体的には、サーバのハードウェアリソースと使用ケースに基づいて、最適なパラメータを調整することでデータベースの動作を最適化する。

以下のサイトにてサーバの構成に基づいた入力を行うと設定ファイルが出力される。

以下は今回出力された設定ファイル。

# DB Version: 16
# OS Type: linux
# DB Type: web
# Total Memory (RAM): 2 GB
# CPUs num: 1
# Connections num: 100
# Data Storage: ssd
max_connections = 100
shared_buffers = 512MB
effective_cache_size = 1536MB
maintenance_work_mem = 128MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 2621kB
huge_pages = off
min_wal_size = 1GB
max_wal_size = 4GB

これを PostgreSQL の設定ファイルである postgresql.confペーストする。

Terminal window
vim /var/lib/postgres/data/postgresql.conf
systemctl enable --now postgresql
su - postgres
psql
CREATE USER mastodon CREATEDB;
\q

PostgreSQL では、データベースユーザを作成する際にクライアント側のユーザ名と同じ名前にするのがおすすめ。

これにより、ident 認証活用することができ、ユーザ名が一致していればパスワードの入力が不要になるため接続がよりスムーズになる。

Terminal window
exit
systemctl enable --now redis

次に、一旦 postgres ユーザからログアウトして、admin ユーザに戻る。 そして、 先ほどインストールした Redis を起動する。

再度 mastodon ユーザに切り替えて、JavaScript のツールマネージャである Volta使って Node.js や Yarn をインストールし、開発環境を整える。

もちろん、直接 curl で Node.js をダウンロードしてビルドしても良いのだが、長期運用していく上でバージョンを上げなければならないときは必ず来るわけで、そのときのために Volta を入れておいた方が良いnodenvでも良いけど)。

Terminal window
su - mastodon
curl https://get.volta.sh | bash
source ~/.bashrc
volta install node
# 現在は yarn のバージョン 4 をインストールする必要がある点に注意
volta install yarn@1

そして、ようやく Mastodon 本体のソースコードをクローンする。

Terminal window
git clone https://github.com/mastodon/mastodon.git live && cd live
# 最新の安定版タグにチェックアウト
git checkout $(git tag -l | grep '^v[0-9.]*$' | sort -V | tail -n 1)

次に Mastodon の Ruby および JavaScript 依存パッケージをインストールするための設定と実行を行う。

Terminal window
# Gemfile に指定されている依存関係が読み取り専用でインストールされ、開発環境用のオプションは適用されない
bundle config deployment 'true'
# development と test グループの依存関係を除外する
bundle config without 'development test'
# Gemfile に記述された必要な Gem をインストールする
# このとき、システムの利用可能な CPU コア数に基づいて並列インストールを行う
bundle install -j$(getconf _NPROCESSORS_ONLN)
# JavaScript 依存パッケージをインストールする
# yarn.lock ファイルは変更しない
yarn install --pure-lockfile

Mastodon のセットアップコマンドを実行する前に R2 で CORS ポリシーを設定しておく。

[
{
// すべてのヘッダーを許可する
"AllowedHeaders": [
"*"
],
// HTTP ヘッダのうち、GET と HEAD のみ許可する
"AllowedMethods": [
"GET",
"HEAD"
],
// どのオリジンからのリクエストも許可する
"AllowedOrigins": [
"*"
],
// ETag ヘッダのみクライアント側に公開する
"ExposeHeaders": [
"ETag"
],
// プリフライトリクエストの結果を 3,000 秒キャッシュする
"MaxAgeSeconds": 3000
}
]

次に Mastodon を本番環境で設定するためのコマンドを実行する。 このコマンドを実行すると、データベース、Redis、およびファイルストレージの設定を対話形式で行える。 直接、環境変数ファイルを変更しても良いけど、特に理由がなければこのコマンドで OK。

Terminal window
RAILS_ENV=production bundle exec rake mastodon:setup
# ドメイン名を指定する
Domain name: mastodon.kkhys.me
# シングルユーザーモードにするかどうか
# 今回はおひとり様インスタンスなので Yes
Do you want to enable single user mode? (y/N) y
# Docker を使うかどうか
Are you using Docker to run Mastodon? (Y/n) n
# PostgreSQL の設定
PostgreSQL host: /var/run/postgresql
PostgreSQL port: 5432
Name of PostgreSQL database: mastodon_production
Name of PostgreSQL user: mastodon
Password of PostgreSQL user:
# Redis の設定
Redis host: localhost
Redis port: 6379
Redis password:
# ファイルストレージの設定
# 選択肢に R2 がないので、後から変更する
Provider Amazon S3
S3 bucket name: files-mastodon-kkhys-me
S3 region: auto
S3 hostname: files.mastodon.kkhys.me
S3 access key: xxx
S3 secret key: xxx
Do you want to access the uploaded files from your own domain? Yes
Domain for uploaded files: files.mastodon.kkhys.me
# SMTP サーバの設定
# 今回は Gmail を使用した
Do you want to send e-mails from localhost? No
SMTP server: smtp.gmail.com
SMTP port: 587
SMTP username: xxx@gmail.com
SMTP password: xxx
SMTP authentication: plain
SMTP OpenSSL verify mode: none
Enable STARTTLS: always
E-mail address to send e-mails "from": Mastodon <xxx@gmail.com>
Send a test e-mail with this configuration right now? Yes
Send test e-mail to: xxx
# アップデート情報などをメール送信しても良いかどうか
# Yes がおすすめ
Do you want Mastodon to periodically check for important updates and notify you? (Recommended) Yes
# .env.production に保存しても良いかどうか
Save configuration? Yes
# データベースのセットアップを初期化しても良いかどうか
Prepare the database now? Yes
# アセットのコンパイルをしても良いかどうか
Compile the assets now? (Y/n) Yes
# 管理者ユーザを作成するかどうか
# ここで作成しておいた方が楽
Do you want to create an admin user straight away? Yes
Username: xxx
E-mail: xxx
# 管理者ユーザのパスワードが出力されるのでメモしておく
You can login with the password: xxx

先ほど設定できなかった R2 に関する環境変数を変更する。

Terminal window
vim /home/mastodon/live/.env.production

以下の環境変数を追加。

/home/mastodon/live/.env.production
S3_ENDPOINT=https://xxx.r2.cloudflarestorage.com/
S3_PERMISSION=private

Mastodon の最低限の設定は以上で終わり。

続いて、Nginx 関連の設定を行なっていく。

SSL/TLS 証明書は Let's Encryptから入手するが、その際に Let's Encryptの公式クライアントである certbot使用する。

Terminal window
# 管理者ユーザに切り替え
sudo su -
# Certbot を使用して、指定されたドメインの SSL/TLS 証明書を取得する
# --nginx オプションは、Nginx サーバの設定を自動的に検出し、証明書の取得を簡素化する
# ここでは、Nginx が既にインストールされていることが前提
certbot certonly --nginx -d mastodon.kkhys.me
# Nginx の設定ディレクトリを作成する
# sites-available ディレクトリは、サーバの設定ファイルを保存するための場所
# sites-enabled ディレクトリは、現在有効なサイトの設定ファイルがリンクされている場所
mkdir -p /etc/nginx/sites-{available,enabled} /etc/nginx/conf.d
# 初期状態を確認するために出力する
cat /etc/nginx/nginx.conf

初期状態の Nginx 設定ファイルは以下のようになっている。

/etc/nginx/nginx.conf
#user http;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}

これを Mastodon 向けに編集していく。

Terminal window
vim /etc/nginx/nginx.conf
/etc/nginx/nginx.conf
user http;
worker_processes auto;
worker_cpu_affinity auto;
events {
multi_accept on;
worker_connections 1024;
}
http {
charset utf-8;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
log_not_found off;
types_hash_max_size 4096;
client_max_body_size 16M;
# MIME
include mime.types;
default_type application/octet-stream;
# logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
# load configs
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

次に Mastodon 側で用意された Nginx 設定ファイルを読み込ませるための処理を行う。

Terminal window
# Mastodon のための Nginx 設定ファイルを、Nginx の設定用ディレクトリにコピーする
cp /home/mastodon/live/dist/nginx.conf /etc/nginx/sites-available/mastodon
# sites-available にある Mastodon の設定ファイルへのシンボリックリンクを作成する
ln -s /etc/nginx/sites-available/mastodon /etc/nginx/sites-enabled/
vim /etc/nginx/sites-available/mastodon

デフォルトでは証明書との紐付けがうまくいかないため、以下の変更を行う。

/etc/nginx/sites-available/mastodon
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream backend {
server 127.0.0.1:3000 fail_timeout=0;
}
upstream streaming {
# Instruct nginx to send connections to the server with the least number of connections
# to ensure load is distributed evenly.
least_conn;
server 127.0.0.1:4000 fail_timeout=0;
# Uncomment these lines for load-balancing multiple instances of streaming for scaling,
# this assumes your running the streaming server on ports 4000, 4001, and 4002:
# server 127.0.0.1:4001 fail_timeout=0;
# server 127.0.0.1:4002 fail_timeout=0;
}
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=CACHE:10m inactive=7d max_size=1g;
server {
listen 80;
listen [::]:80;
server_name example.com;
server_name mastodon.kkhys.me;
root /home/mastodon/live/public;
location /.well-known/acme-challenge/ { allow all; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl http2;
listen 443 ssl;
listen [::]:443 ssl http2;
listen [::]:443 ssl;
http2 on;
server_name example.com;
server_name mastodon.kkhys.me;
ssl_protocols TLSv1.2 TLSv1.3;
# You can use https://ssl-config.mozilla.org/ to generate your cipher set.
# We recommend their "Intermediate" level.
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Uncomment these lines once you acquire a certificate:
# ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate /etc/letsencrypt/live/mastodon.kkhys.me/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_certificate_key /etc/letsencrypt/live/mastodon.kkhys.me/privkey.pem;
keepalive_timeout 70;
sendfile on;
client_max_body_size 99m;
root /home/mastodon/live/public;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon;
location / {
try_files $uri @proxy;
}
# If Docker is used for deployment and Rails serves static files,
# then needed must replace line `try_files $uri =404;` with `try_files $uri @proxy;`.
location = /sw.js {
add_header Cache-Control "public, max-age=604800, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/assets/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/avatars/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/emoji/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/headers/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/packs/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/shortcuts/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/sounds/ {
add_header Cache-Control "public, max-age=2419200, must-revalidate";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
try_files $uri =404;
}
location ~ ^/system/ {
add_header Cache-Control "public, max-age=2419200, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "default-src 'none'; form-action 'none'";
try_files $uri =404;
}
location ^~ /api/v1/streaming {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Proxy "";
proxy_pass http://streaming;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
tcp_nodelay on;
}
location @proxy {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Proxy "";
proxy_pass_header Server;
proxy_pass http://backend;
proxy_buffering on;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache CACHE;
proxy_cache_valid 200 7d;
proxy_cache_valid 410 24h;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
add_header X-Cached $upstream_cache_status;
tcp_nodelay on;
}
error_page 404 500 501 502 503 504 /500.html;
}

Nginx の設定は完了したので、以下のコマンドを実行して起動させる。

Terminal window
chmod 701 /home/mastodon
# Nginx の設定ファイルにエラーがないかをテストする
nginx -t
killall nginx
systemctl enable --now nginx

最後に Mastodon サービスを Systemd に設定して起動する。

Terminal window
# Mastodon の Systemd サービスファイルをコピーする
# これにより、Mastodon がシステムのサービスとして管理されるようになる
cp /home/mastodon/live/dist/mastodon-*.service /etc/systemd/system/
# Systemd に新しく追加されたサービスファイルを認識させる
systemctl daemon-reload
# Mastodon 関連の 3 つのサービスをシステム起動時に自動的に開始するように設定し、即座にこれらのサービスを起動する
systemctl enable --now mastodon-{web,sidekiq,streaming}

これで Mastodon インスタンスの構築は完了。 URL にアクセスすると正常にページが表示されるはず。

忙しい人はここまででも十分 Mastodon ライフを楽しめるので離脱しても問題ない。 ただし、より良い機能をユーザに提供したい人や今後発生するであろう問題をあらかじめ片付けておきたい人は以下の作業も併せて行っておくと良いです。

そのほかにやっておいた方が良いこと

ImageMagick から libvips に切り替える

v4.3.0 ImageMagick サポートが廃止され、libvips が採用されたのでその対応を行う。

本来なら上述した説明に組み込むべきであるが、あくまで記事には履歴として残しておきたいのでこのような形式をとっている(記事を書いている途中で v4.3.0 がリリースされた)。

まずは pacman で libvips をインストールする。

Terminal window
sudo pacman -S libvips

libvips の提案パッケージもついでにインストールしておく(後々 warning が出るので)。

Terminal window
sudo pacman -S libheif imagemagick openslide poppler-glib

次に .env.production編集して以下の環境変数を追加する。

.env/production
MASTODON_USE_LIBVIPS=true

最後に Mastodon サービスを再起動すれば完了。

Terminal window
sudo systemctl restart mastodon-{web,sidekiq,streaming}

全文検索機能の追加

Mastodon では、ユーザが投稿を効率的に検索できるように全文検索機能を追加できる。 この機能により、トゥートやユーザ、ハッシュタグなどを容易に検索でき、より快適な利用体験の提供が可能となる。

以下の手順に従って、全文検索機能を追加するための設定を行う。

Terminal window
# admin ユーザで実行する
pacman -S jdk17-openjdk
# ログアウトして mastodon ユーザに戻る
exit
paru -S elasticsearch7

Mastodon では全文検索エンジンに Elasticsearch使うことが推奨されているので、それに従う。 Mastodon は Elasticsearch のバージョン 7 でテストされているため、ここでは 7 をインストールしている(最新版はバージョン 8)。

Elasticsearch を有効化するために環境変数を追加する。

Terminal window
vim /home/mastodon/live/.env.production
/home/mastodon/live/.env.production
ES_ENABLED=true
ES_HOST=localhost
ES_PORT=9200
ES_PRESET=single_node_cluster

次に Elasticsearch の設定ファイルにセキュリティとノード構成のオプションを追加する。

Terminal window
vim /etc/elasticsearch/elasticsearch.yml
/etc/elasticsearch/elasticsearch.yml
# Elasticsearch のセキュリティ機能(認証とアクセス制御)を有効化する
xpack.security.enabled: true
# Elasticsearch を単一ノード構成で動作させる
discovery.type: single-node

続いて、Elasticsearch のセキュリティ設定とユーザパスワードの初期設定を行う。

パスワードを設定していない Elasticsearch に外部ユーザがアクセスする場合、認証なしで HTTP リクエストを直接送信するだけでアクセスが可能である。 この場合、Elasticsearch のデータに対して完全に無防備な状態となり、誰でもデータを閲覧・操作できるため、セキュリティ上の大きなリスクがある。

ただ、iptables でポートを制限しているため、ポート 9200 にアクセスしたとしても弾かれる。 そのため、必ずしもパスワードを設定しないといけないわけではないが、万が一のために設定しておいた方が良い。

Terminal window
# Elasticsearch サービスを起動し、サーバ再起動後も自動で起動するようにする
systemctl enable --now elasticsearch
# Elasticsearch用のキーストアを作成する
/usr/share/elasticsearch/bin/elasticsearch-keystore create
# Elasticsearch の内蔵ユーザ(システムユーザ)のパスワードを自動生成する
/usr/share/elasticsearch/bin/elasticsearch-setup-passwords auto
Changed password for user apm_system
PASSWORD apm_system = xxx
Changed password for user kibana_system
PASSWORD kibana_system = xxx
Changed password for user kibana
PASSWORD kibana = xxx
Changed password for user logstash_system
PASSWORD logstash_system = xxx
Changed password for user beats_system
PASSWORD beats_system = xxx
Changed password for user remote_monitoring_user
PASSWORD remote_monitoring_user = xxx
Changed password for user elastic
PASSWORD elastic = xxx

次に Mastodon 専用のユーザとロールを作成する。

Terminal window
# mastodon_full_access という名前のロールを作成する
curl -X POST -u elastic:xxx "localhost:9200/_security/role/mastodon_full_access?pretty" -H 'Content-Type: application/json' -d'
{
// Elasticsearch クラスタの監視権限を付与する
"cluster": ["monitor"],
"indices": [{
"names": ["*"],
"privileges": ["read", "monitor", "write", "manage"]
}]
}
'
# mastodon というユーザを作成する
curl -X POST -u elastic:xxx "localhost:9200/_security/user/mastodon?pretty" -H 'Content-Type: application/json' -d'
{
"password" : "xxx",
// 先ほど作成した mastodon_full_access ロールをこのユーザに割り当てる
"roles" : ["mastodon_full_access"]
}
'

お、curl のオプションに -u指定することで、Basic 認証を使ったリクエストを行える。

Elasticsearch に mastodon ユーザを作成したので、Mastodon アプリの環境変数にユーザ名とパスワードを設定する。

Terminal window
vim /home/mastodon/live/.env.production
/home/mastodon/live/.env.production
ES_USER=mastodon
ES_PASS=xxx

Elasticsearch の設定は完了したため、次に Mastodon 関連のサービスを再起動させてから全文検索のインデックスを作成する。

Terminal window
systemctl restart mastodon-sidekiq
systemctl reload mastodon-web
su - mastodon
cd live
RAILS_ENV=production bin/tootctl search deploy

tootctl search deployインデックスを作成した後は、Mastodon が自動的にインデックスを更新してくれるので手動で更新する必要はない。

ここまでの作業で問題なく検索できることを確認できるはず。 だが、このままでは負荷が大きい検索処理を行うと、2GB の小さいメモリでは Elasticsearch が稀に落ちてしまう。

それを防ぐために初期ヒープサイズと最大ヒープサイズを制限しておく。

適切なヒープサイズは以下のドキュメントを参考にした。

2GB の場合は 512MB が適切なサイズということになる。

Terminal window
vim /etc/elasticsearch/jvm.options.d/ram.options
/etc/elasticsearch/jvm.options.d/ram.options
-Xms512m
-Xmx512m

Elasticsearch サービスを再起動して、適切にヒープサイズが設定されているかを確認してみる。

Terminal window
systemctl restart elasticsearch
curl -X GET -u elastic:xxx "localhost:9200/_nodes?pretty"
{
// ...
"jvm" : {
"mem" : {
"heap_init_in_bytes" : 536870912,
"heap_max_in_bytes" : 536870912
}
}
// ...
}

上記のようになっていれば問題なし。

次に、日本語検索を最適化するために Elasticsearch の日本語形態素解析用プラグイン Kuromojiインストールする。 後述する理由により結局 Kuromoji のインストールは止めるけど、メモとして作業内容は残しておく。

Kuromoji の代わりに、後発の Sudachi使用する選択肢もある。 ただし、検索対象に特殊な業務ドメインがない場合は、どちらを選んでも大差はない2 導入が簡単な Kuromoji の方がおすすめ。

Kuromoji を使うとどのような利点があるかは以下の記事が参考になる。

まずは Elasticsearch に付属するプラグイン管理ツールを使って Kuromoji プラグインをインストールする。

Terminal window
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji

Elasticsearch を再起動して、プラグインがちゃんとインストールされたか確認する。

Terminal window
sudo systemctl restart elasticsearch
Terminal window
curl -X GET -u elastic:xxx "localhost:9200/_cat/plugins?v"
name component version
vultr analysis-kuromoji 7.17.18

問題なくインストールされている。

この状態で Elasticsearch を再起動して、検索のテストをしてみたが思った結果が返ってこない。 調べると、Ruby では Elasticsearch クライアントに chewyいうライブラリが使用されており、Kuromoji を有効化するためには chewy の設定ファイルを直接編集する必要がある。

Mastodon では以下のインデックスが存在する。

Terminal window
curl -X GET -u elastic:xxx "localhost:9200/_cat/indices?v"
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .geoip_databases xxx 1 0 38 0 36.7mb 36.7mb
green open .security-7 xxx 1 0 9 0 35.1kb 35.1kb
green open chewy_specifications xxx 1 0 5 0 19.3kb 19.3kb
green open instances xxx 1 0 12 0 5.4kb 5.4kb
green open statuses xxx 5 0 298 0 297.4kb 297.4kb
green open accounts xxx 1 0 22 0 18.4kb 18.4kb
green open public_statuses xxx 5 0 295 0 287.1kb 287.1kb
green open tags xxx 1 0 22 0 7.2kb 7.2kb

Mastodon は主に以下のインデックスを使用する。

  • statuses: 投稿の検索に使用
  • accounts: ユーザアカウントの検索に使用
  • public_statuses: 公開トゥートの検索に使用
  • tags: ハッシュタグの検索に使用
  • instances: 他の Mastodon インスタンス情報の検索や管理に使用

chewy に Kuromoji のマッピングを定義すれば Kuromoji は使えるようになるけど、今回の Mastodon インスタンス建設の前提として、持続的に楽をして運営していきたいと思っているので、コンフリクトが起こりそうな変更はなるべくしたくない。 その結果、迷ったけど Kuromoji の導入は見送った。

以下のコマンドで Kuromoji をアンインストールする。

Terminal window
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin remove analysis-kuromoji

あと、全文検索機能を追加してから気づいたけど、まさかのログインしてないと検索機能が使えないいう。。 おひとり様インスタンスであれば、自分しか検索機能を使えないということになるから、なんだかなという残念な気分。

ログインせずに検索機能を使える法も必ずあるとはわかっているけど、ソースコードはできるだけ弄りたくないからどうするか考えている。

Mastodon は検索機能を重視せずにハッシュタグ偏重の思想というのはなんとなくわかった。 逆にいえば、昔の投稿を掘り返されたりしないから良いのか?

ログローテーション

ログの容量を制限しないと無限に増え続けて、ディスク容量を圧迫してしまうため対策をしておく。

以下がログを 1 ヶ月放置し続けた結果である。

Terminal window
du -h -d1 /var/log
30M /var/log/nginx
44K /var/log/letsencrypt
4.0K /var/log/old
1.5M /var/log/elasticsearch
4.0K /var/log/gssproxy
4.0K /var/log/private
4.0K /var/log/audit
1.2G /var/log/journal
1.2G /var/log

ログファイルの合計サイズが 1.2GB とかなり大きくなっている。

システムのログ管理とサイズ制御のために、logrotate をインストールし、設定ファイルを編集してログのローテーションを設定する。

Terminal window
pacman -S logrotate
vim /etc/logrotate.conf

/etc/logrotate.confベースとなる設定を追加する。

/etc/logrotate.conf
# ログファイルを毎日ローテーションする
daily
# 指定されたログファイルが存在しなくてもエラーを出さずに処理を続行する
missingok
# ローテーションによって保存される古いログファイルの数を指定する
rotate 4
# ログファイルが 20MB 以上の場合にのみローテーションを実行する
size 20M
# ローテーション後に新しい(空の)ログファイルを作成する
create
# ローテーションされたログファイルを圧縮する
compress
# ログファイルを圧縮するのを次回のローテーションまで遅延させる
delaycompress
# ログファイルが空の場合はローテーションを実行しない
notifempty
# ログローテーションで無視される拡張子のリスト
tabooext + .pacorig .pacnew .pacsave
# 追加設定ファイルを読み込んで使用する
include /etc/logrotate.d

/etc/logrotate.d ディレクトリ以下にログファイルごとの個別設定を追加する。 ここではファイルサイズが大きくなりがちな nginx のみ設定することとする。

/etc/logrotate.d/nginx
/var/log/nginx/*log {
missingok
notifempty
create 640 http root
sharedscripts
compress
postrotate
test ! -r /run/nginx.pid || kill -USR1 `cat /run/nginx.pid`
endscript
}

logrotate コマンドを手動で強制実行し、/etc/logrotate.conf設定に従って即座にローテーションを行う。 ここで指定したとおりにファイルサイズが小さくなっているかを確認しておくこと。

Terminal window
logrotate -f /etc/logrotate.conf

journald のログファイルは、logrotate で管理するのではなく、journald 自体の設定ファイル/etc/systemd/journald.conf)で設定していく。

Terminal window
vim /etc/systemd/journald.conf
/etc/systemd/journald.conf
[Journal]
# ログを永続的に保存する
Storage=persistent
# 最大ディスクスペースを 200MB に制限する
SystemMaxUse=200M
# ログエントリの最大保持期間を 2 週間に設定する
MaxRetentionSec=2weeks

システムログを収集・管理する systemd-journald デーモンを再起動して、設定変更を反映させる。

Terminal window
systemctl restart systemd-journald

最後にログサイズを確認。

Terminal window
du -h -d1 /var/log
4.2M /var/log/nginx
44K /var/log/letsencrypt
4.0K /var/log/old
996K /var/log/elasticsearch
4.0K /var/log/gssproxy
4.0K /var/log/private
4.0K /var/log/audit
121M /var/log/journal
152M /var/log

ログファイルの合計サイズが 152MB になっていることを確認できた。

SSL 証明書の自動更新

SSL 証明書には期限があるため、定期的に更新が必要である。 毎回手動で実行するのは面倒くさいし、必ず忘れてしまうので自動で更新されるように設定していく。

まずは SSL 証明書を更新するためのサービスの動作を定義していく。

Terminal window
systemctl edit --full --force certbot-renewal
[Unit]
Description=Certbot Renewal Service
[Service]
# サービスは一度実行されると完了する
Type=oneshot
# 証明書の更新を行う Certbot コマンドを実行する
ExecStart=/usr/bin/certbot renew
# サービスごとに一時ディレクトリを分離し、セキュリティを向上させる
PrivateTmp=true

次に certbot-renewal.timer というタイマーを定義する。

Terminal window
sudoedit /etc/systemd/system/certbot-renewal.timer
[Unit]
Description=Run Certbot Renewal Twice Daily
[Timer]
# 毎日午前 0 時と午後 12 時に Certbot 更新サービスを実行する
OnCalendar=*-*-* 00,12:00:00
# タイマーがシステムが停止中の間に実行されなかった場合、次回起動時に直ちに実行する
Persistent=true
[Install]
# timers.target によってタイマーが起動されるようにする
WantedBy=timers.target

最後に certbot-renewal.timer自動的に起動するように設定する。

Terminal window
systemctl daemon-reload
systemctl enable --now certbot-renewal.timer

正しく登録されたか確認する。

Terminal window
systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2024-10-27 00:00:00 JST 51min Sat 2024-10-26 12:00:00 JST 11h ago certbot-renewal.timer certbot-renewal.service

問題なし。

念の為、定期的に SSL 証明書の有効期限を確認しておくと安心。

Terminal window
certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: mastodon.kkhys.me
Serial Number: xxx
Key Type: xxx
Domains: mastodon.kkhys.me
Expiry Date: 2025-01-17 14:27:26+00:00 (VALID: 83 days)
Certificate Path: /etc/letsencrypt/live/mastodon.kkhys.me/fullchain.pem
Private Key Path: /etc/letsencrypt/live/mastodon.kkhys.me/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

他にやり残したこと

時間と金銭的な余裕があれば以下の作業をしたいと思っている。

  • Tor を通じてオニオンサービスとして提供する
  • データベースを Neon などの Saas に分離させる

さいごに

Mastodon は思っていたよりもドキュメントが充実しているし、機能も豊富で、善意で成り立ったソフトウェアとは思えない完成度だ。 良い意味で枯れているから、突然メンテナンスされなくなったりする危険性がないのは安心できる。

周りの知り合いにアカウントを持っている人が少ないのが難点、というよりも一番の問題だけど、逆に誰にも見られていないから気楽につぶやけて心地よい。 今後、Mastodon の行方はゆる観測していく。


  1. Ruby × jemalloc のすすめ

  2. 検索基盤チームのElasticsearch×Sudachi移行戦略と実践この記事のようなドメインを扱うのであれば Sudachi の方が向いているかもしれない

最後までお読みいただき、ありがとうございます

コーヒー代をサポートしていただけると励みになります!

開発者用メニュー