Mastodon を FreeBSD で動かす

Mastodon (マストドン)とは

Mastodon とは、分散型のオープンソースなソーシャルネットワークです。
基本的な機能としては Twitter と似たようなものなのですが、画期的なのは複数のインスタンス(サーバ)が緩く連携するように出来ており、これによりインスタンスの違いを意識することなくフォローしたり発言したりすることが可能になります。
中央管理的ではないので、あるインスタンスが独裁的にユーザを閉め出したとしても、他のインスタンスに移動すれば解決します。

またこの連携により、ログインしているインスタンスのユーザの発言が見える「ローカルタイムライン (Local timeline) 」と、他のインスタンスのユーザの発言も見える「連合タイムライン (Federated timeline) 」という二つのユニークなタイムラインを備えています。

技術面で興味があったので、導入し挙動を確認するまでをまとめたいと思います。
ちなみに技術的に Mastodon を説明すると、Atom, Activity Streams(WebSocket), WebFinger, PubSubHubbub 及び Salmon といったプロトコルによって構成された OStatus を用いた分散型ミニブログで、つまるところ RSS のおばけです。
実際にユーザのタイムラインは RSS で購読可能です。

PubSubHubbub は気象庁からの気象データを受け取るのに実装したので分かりましたが、なんだかいろいろ新しいものの集合体でわくわくしますね。

Mastodon との出会い

海外でポスト Twitter として騒がれ始め、11日に ASCII.jp で記事を見かけました。
仕組みがあまり分からないなーと思っているうちに13日に Kirakiratter 誕生の報を受け即登録。
Kirakiratter はアイカツ!に登場する Twitter のようなサービスで個人的に作ってみたら面白そうだなーと思っていたので、こんな形で出会えるとは思いもよらず。
しかしこのスピード感とある程度リテラシーの高いユーザだけで構成されている世界、本当に Twitter の誕生を思い出して懐かしい気持ちになりました。

いろいろ使ってみても仕組みが分からないところがあり、これは自分で入れてみるしかないかなと思い立ちインストールしてみることにしました。
先日 IRC サーバを停止させたところなので、時代が入れ替わるのを感じます。

FreeBSD にインストール

FreeBSD の Docker は ZFS で実装されているという噂を聞き、流石に AWS EC2 で ZFS を使うのは重いなと思ったので Docker を使わずにインストールします。
ちなみに Mastodon では WebSocket を使いますが AWS CloudFront が WebSocket 非対応なため、直接アクセスする前提で構築します。
途中まで気づかずしばらく悩みました。
CloudFlare は対応しているので、ある程度の規模になるなら CloudFlare を使うのが良さそうです。

AWS EC2 の t2.micro インスタンス(後述しますが最終的には t2.small インスタンス)を登録し、以下の ansible playbook でさくっとインストールします。
しかし試行錯誤したのでちょっと怪しいです。
特に Ruby のバージョンがあやふやで、何かのパッケージが ruby24 に依存していたので ruby24 をインストールしていますが、ruby24-gems が pkg ではインストールできず。
またそのままいろんなものをインストールしていくと、依存の関係で最終的に入っている Ruby は 2.3 になります……。
とりあえず動作には問題がないようですが……。

# Ansible YAML file for Mastodon

---
- hosts: ec2
  become: yes
  become_method: su
  gather_facts: False

  tasks:
          - name: Install of git
            pkgng: name=git

          - name: Install of Ruby
            pkgng: name=ruby24

#          - name: Install of Ruby gem
#            pkgng: name=ruby24-gems

          - name: Install of rake
            pkgng: name=rubygem-rake

          - name: Install of ffmpeg
            pkgng: name=ffmpeg

          - name: Install of ImageMagick
            pkgng: name=ImageMagick7-nox11

          - name: Checking existence of Mastodon
            stat: path=/opt/mastodon/.env.production
            register: reg_mastodon

          - name: Install of Mastodon
            git: repo=https://github.com/tootsuite/mastodon.git dest=/opt/mastodon
            when: not reg_mastodon.stat.exists

          - name: Install of bundler with gem
            gem: name=bundler

          - name: Install of redis
            pkgng: name=redis

          - name: Setting of redis
            lineinfile: dest=/etc/rc.conf line='redis_enable="YES"' backup=yes

          - name: Install of gmake
            pkgng: name=gmake

          - name: Install of npm
            pkgng: name=npm

          - name: Install of yarn (npm)
            npm: name=yarn global=yes

          - name: Install of rack
            pkgng: name=rubygem-rack

#          - name: Setting of sidekiq
#            lineinfile:
#                    dest=/opt/mastodon/config/sidekiq.yml
#                    line={{ item }}
#                    backup=yes
#            with_items:
#                    - ':daemon: true'
#                    - ':pidfile: ./tmp/pids/sidekiq.pid'
#                    - ':logfile: ./log/sidekiq.log'

          - name: Install of unicorn
            pkgng: name=rubygem-unicorn

          - name: Copy unicorn.rb file of Mastodon
            copy: src=dist/unicorn.rb dest=/opt/mastodon/config backup=yes

          - name: Copy config of unicorn (Mastodon)
            copy: src=dist/unicorn dest=/usr/local/etc/rc.d mode=0755 backup=yes

          - name: Setting of unicorn
            lineinfile: dest=/etc/rc.conf line='unicorn_enable="YES"' backup=yes

          - name: Install of nginx
            pkgng: name=www/nginx

          - name: Setting of nginx
            lineinfile: dest=/etc/rc.conf line='nginx_enable="YES"' backup=yes

          - name: Setting log directory of nginx
            file: path=/var/log/nginx state=directory

          - name: Copy config of nginx.conf
            copy: src=dist/nginx/nginx.conf dest=/usr/local/etc/nginx backup=yes

          - name: Install of certbot
            pkgng: name=py27-certbot

          - name: Creating DH param
            shell: openssl dhparam 2048 -out /usr/local/etc/dhparam.pem
            args:
                    creates: /usr/local/etc/dhparam.pem

          - name: Copy Mastodon starting script (sidekiq)
            copy: src=dist/start_sidekiq.sh dest=/opt/mastodon backup=yes

          - name: Copy Mastodon starting script (streaming)
            copy: src=dist/start_streaming.sh dest=/opt/mastodon backup=yes

          - name: Copy Mastodon config file
            copy: src=dist/.env.production dest=/opt/mastodon backup=yes

上記でいくつかコピーしているファイルは以下です。

unicorn.rb
unicorn で起動するためのファイルです。

@dir = "/opt/mastodon"
worker_processes 1
working_directory @dir

listen "/var/run/unicorn.sock"
pid "/var/run/unicorn.pid"

preload_app true

stdout_path File.expand_path("log/unicorn.stdout.log", @dir)
stderr_path File.expand_path("log/unicorn.stderr.log", @dir)

unicorn
unicorn 自動起動スクリプトですが、再起動時はこれでは起動出来ないので調査が必要。
ただし、後述の sidekiq 等は今のところ手動起動なのであまり困ってはいません。

追記: unicorn の自動起動スクリプトを更新しました。

#!/bin/sh
#
# unicorn (mastodon)
#

# PROVIDE: unicorn
# REQUIRE: LOGIN cleanvar
# KEYWORD: shutdown

. /etc/rc.subr

name="unicorn"
rcvar=unicorn_enable

load_rc_config $name

extra_commands="status"
start_cmd=unicorn_start
stop_cmd=unicorn_stop
status_cmd=unicorn_status

unicorn_enable=${unicorn_enable:-"NO"}
if [ -r /var/run/unicorn.pid ]; then
        unicorn_pid=`cat /var/run/unicorn.pid`
fi

PATH=${PATH}:/usr/local/bin

unicorn_start()
{
        cd /opt/mastodon && unicorn_rails -c config/unicorn.rb -E production -D
        echo "${name} started"
}

unicorn_stop()
{
        if [ "x" == "x${unicorn_pid}" ]; then
                echo "${name} not running?"
        else
                kill -9 ${unicorn_pid}
                rm /var/run/unicorn.pid
                echo "${name} stoped"
        fi
}

unicorn_status()
{
        if [ "x" == "x${unicorn_pid}" ]; then
                echo "${name} is not running."
        else
                echo "${name} is running as pid ${unicorn_pid}."
        fi
}

run_rc_command "$1"

start_sidekiq.sh
sidekiq 手動起動用。
ansible で sidekiq.yml に :daemon: true を追加しているのでデーモンとして動きます。
追記: 1.3.3 で sidekiq.yml に変更がありました。
マージに失敗することがあるため、オプションを指定する方法に変更しました。

#!/bin/sh

export RAILS_ENV=production
export NODE_ENV=production
export STREAMING_CLUSTER_NUM=1

# bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
bundle exec sidekiq -d -L ./log/sidekiq.log -P ./tmp/pids/sidekiq.pid

start_streaming.sh
streaming 手動起動用。
STREAMING_CLUSTER_NUM を明示的に指定しています。

#!/bin/sh

export RAILS_ENV=production
export NODE_ENV=production
export STREAMING_CLUSTER_NUM=1

nohup npm run start > log/streaming.log 2>&1 &

気をつけたいポイント

nginx

私は unicorn で動かしたので、upstream に unicorn 用の設定を書きます。
また設定例に書いてありますが、WebSocket 用に $connection_upgrade を設定するところがありまして、最初抜けていて悩んでいました。

http {
    upstream unicorn {
        server unix:/var/run/unicorn.sock;
    }

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

   server {
        listen 80;
        listen [::]:80;
        listen 443 ssl http2;
        listen [::]:443 ssl http2;

        # (snip)

        client_max_body_size 10M;

        location / {
            try_files $uri @proxy;
        }

        location @proxy {
            proxy_set_header Host $http_host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-CSRF-Token $http_x_csrf_token;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_pass http://unicorn;
        }

        location /api/v1/streaming {
            proxy_set_header Host $http_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://localhost:4000;

            proxy_buffering off;
            proxy_redirect off;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }
}

cron

ドキュメントにも記載されていますが、必ず以下の mastodon:daily を1日1回以上実行する必要があります。

### Mastodon
RAILS_ENV=production
PATH=${PATH}:/usr/local/bin
0 */6 * * * root cd /opt/mastodon && /usr/local/bin/bundle exec rake mastodon:daily > /dev/null

mail

AWS の場合、逆引き (rDNS) の設定と、SMTP の制限解除を申請する必要があります。
今時だと直接送信せずに Mailgun などのサービスを使うのでしょうけれど……。
申請については以下が詳しいです。

AWS EC2 Eメール上限緩和 / 逆引き(rDNS)設定 申請手順

ローカルのメール配送に任せる場合の設定は以下です。
ローカルなので STARTSSL は使用していません。

SMTP_SERVER=127.0.0.1
SMTP_PORT=25
[email protected]
SMTP_DELIVERY_METHOD=smtp
SMTP_OPENSSL_VERIFY_MODE=none
SMTP_ENABLE_STARTTLS_AUTO=false

swap

メモリ 1GB の環境だと、負荷に耐えられないという話があります。
私の場合は FreeBSD11R で t2.micro だと Mastodon に限らず高負荷時に無反応になってしまう現象にぶちあたり、最終的に t2.small で運用しています。

作り直しやホスト名変更などは難しそう

レアケースだと思いますが、連携部分で問題が発生しそうです。

テストしたり CloudFront が WebSocket 非対応なのに後から気づいたりなどで、ホスト名を変更したり一旦消したりしました。
PubSubHubbub で相手側に情報が残りますが、恐らく一旦消した関係で配信はされるもののフォロー通知など Salmon を通じて行われるものが出来なくなりました。
7日経って PubSubHubbub の購読情報が更新されたら直ったので、待つしかないようです。

ちなみにローカルディスクから S3 への引越はコピーで対応可能でした。

Federated timeline に流れてくるもの

どのくらいトラフィックがあるのか気になっていましたが、基本的にはそのインスタンスのユーザがフォローしている人が流れてくるようです。
なので個人インスタンスでは Home とあまり変わらない感じでした。
そんなわけでユーザ数が少ないインスタンスではある程度流量が想定できます。
Federated timeline がガンガン流れるのを眺めていたい方は、ユーザの多いインスタンスに入ることをお勧めします。

ここがずっと気になっていたので試すことができて良かったです。

まとめ

ちょっと怪しいところがありますが、インスタンスの導入の参考になれば。

Mastodon は可能性を感じますがまだまだ走り出したばかりというあたりでしょうか。
企業が自ドメインで登録して情報を発信していくみたいなのには向いていると思いますので、今後もインスタンスを運用しつつ Mastodon を見ていきたいと思います。

私の Mastodon アカウントはこちらですので宜しければフォローして下さい。
自インスタンス
[email protected]
Kirakiratter (アイカツ!)
[email protected]

FreeBSD,technical

Posted by yokky