Menu

Mobile navigation

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 を使ってリモートサーバに接続する。

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

passwd

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

exit
 
ssh-keygen -t ed25519 -C "[email protected]"
 
ssh-copy-id -i ~/.ssh/id_ed25519_vultr.pub [email protected]

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

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

.ssh/config

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

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

ssh vultr

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

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

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

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

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

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

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

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

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(日本標準時)に変更する。

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 に追加が必要。

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 // [!code ++]

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

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

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

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

vim /etc/locale.conf

/etc/locale.conf

# LANG=en_US.UTF-8
LANG=ja_JP.UTF-8 // [!code ++]

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

reboot

一応、確認。

echo $LANG
ja_JP.UTF-8

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

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

useradd -mg wheel mastodon
 
passwd mastodon

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

EDITOR=vim visudo

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

/etc/sudoers

# %wheel ALL=(ALL:ALL) NOPASSWD: ALL
%wheel ALL=(ALL:ALL) NOPASSWD: ALL

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

vim /etc/ssh/sshd_config

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

/etc/ssh/sshd_config

PermitRootLogin yes // [!code --]
PermitRootLogin no // [!code ++]
 
PasswordAuthentication yes // [!code --]
PasswordAuthentication no // [!code ++]

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

systemctl restart sshd

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

pacman -S fail2ban

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

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

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

/etc/fail2ban/jail.local

[sshd]
enabled = true
mode = aggressive

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

systemctl enable --now fail2ban

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

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

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

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

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 がインストールされていた。 以下のコマンドで現在定義されているすべてのテーブルをリストアップする。

nft list tables
 
table ip filter
table ip6 filter

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

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 サービスを即座に起動し、さらにシステム起動時に自動的に開始するように設定する。

systemctl enable --now nftables

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

nft -c -f /etc/nftables.conf

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

nft list ruleset

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

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

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 の設定は完了である。

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

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

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

su - mastodon

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

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

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 からインストールする。

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 ユーザとして新しいシェルを開く。

sudo -iu postgres

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

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

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

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

https://pgtune.leopard.in.ua

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

# 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 にペーストする。

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

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

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

exit
 
systemctl enable --now redis

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

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

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

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

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

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 依存パッケージをインストールするための設定と実行を行う。

# 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。

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: [email protected]
SMTP password: xxx
SMTP authentication: plain
SMTP OpenSSL verify mode: none
Enable STARTTLS: always
E-mail address to send e-mails "from": Mastodon <[email protected]m>
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 に関する環境変数を変更する。

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

以下の環境変数を追加。

/home/mastodon/live/.env.production

S3_ENDPOINT=https://xxx.r2.cloudflarestorage.com/ // [!code ++]
S3_PERMISSION=private // [!code ++]

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

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

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

# 管理者ユーザに切り替え
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 向けに編集していく。

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 設定ファイルを読み込ませるための処理を行う。

# 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; // [!code --]
  server_name mastodon.kkhys.me; // [!code ++]
  root /home/mastodon/live/public;
  location /.well-known/acme-challenge/ { allow all; }
  location / { return 301 https://$host$request_uri; }
}
 
server {
  listen 443 ssl http2; // [!code --]
  listen 443 ssl; // [!code ++]
  listen [::]:443 ssl http2; // [!code --]
  listen [::]:443 ssl; // [!code ++]
  http2 on; // [!code ++]
 
  server_name example.com; // [!code --]
  server_name mastodon.kkhys.me; // [!code ++]
 
  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; // [!code ++]
  # ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_certificate_key /etc/letsencrypt/live/mastodon.kkhys.me/privkey.pem; // [!code ++]
 
  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 の設定は完了したので、以下のコマンドを実行して起動させる。

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

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

# 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 をインストールする。

sudo pacman -S libvips

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

sudo pacman -S libheif imagemagick openslide poppler-glib

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

.env/production

MASTODON_USE_LIBVIPS=true // [!code ++]

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

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

全文検索機能の追加

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

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

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

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

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

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

/home/mastodon/live/.env.production

ES_ENABLED=true // [!code ++]
ES_HOST=localhost // [!code ++]
ES_PORT=9200 // [!code ++]
ES_PRESET=single_node_cluster // [!code ++]

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

vim /etc/elasticsearch/elasticsearch.yml

/etc/elasticsearch/elasticsearch.yml

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

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

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

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

# 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 専用のユーザとロールを作成する。

# 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 アプリの環境変数にユーザ名とパスワードを設定する。

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

/home/mastodon/live/.env.production

ES_USER=mastodon // [!code ++]
ES_PASS=xxx // [!code ++]

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

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 が適切なサイズということになる。

vim /etc/elasticsearch/jvm.options.d/ram.options

/etc/elasticsearch/jvm.options.d/ram.options

-Xms512m
-Xmx512m

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

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 プラグインをインストールする。

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

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

sudo systemctl restart elasticsearch
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 では以下のインデックスが存在する。

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 をアンインストールする。

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

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

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

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

ログローテーション

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

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

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 をインストールし、設定ファイルを編集してログのローテーションを設定する。

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 の設定に従って即座にローテーションを行う。 ここで指定したとおりにファイルサイズが小さくなっているかを確認しておくこと。

logrotate -f /etc/logrotate.conf

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

vim /etc/systemd/journald.conf

/etc/systemd/journald.conf

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

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

systemctl restart systemd-journald

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

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 証明書を更新するためのサービスの動作を定義していく。

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 というタイマーを定義する。

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 を自動的に起動するように設定する。

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

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

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 証明書の有効期限を確認しておくと安心。

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 の行方はゆるく観測していく。

Footnotes

  1. Ruby × jemalloc のすすめ

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