Mastodonのアップデートの手順書（k8sで動いてるよ編）

みなさん Mastodon インスタンス立ててますか？:elephant:　ぼくは [ichiji.social](https//ichiji.social/) というインスタンスの保守を担当しています。ichiji.social は登録ユーザー数が先日2000人を超え、おかげさまでなかなかの規模のインスタンスに成長しています:pray:

Mastodon は、定期的に新しいバージョンがリリースされています。それらのリリースには機能追加やバグフィックスなどが含まれるため、なるべく新しいリリースに追従していくのがベターです。ichiji.social でも、特に決まった周期ではありませんが、最新バージョンへのアップデートを適時実施しています。

一方で、既に本番運用されている Mastodon インスタンスにアップデートを適用するのはなかなか難儀な仕事です。しかも、アップデートの手順の詳細な情報はあまり多くなく、公式ドキュメントにも、基本的な流れが書いてあるだけです。
https://docs.joinmastodon.org/administration/upgrading/

また、コードに一部カスタマイズを加えていると、そのマージ作業も必要となるため、上記の記事の手順だけでは不十分です。

この記事では、本番運用されている Mastodon インスタンスに、アップデートを適用するときにやっていることや気をつけていることを手順書としてまとめました。みなさんの運営する Mastodon インスタンスのアップデート作業の助けになれば幸いです🤗　あくまで一インスタンスでのやり方なので、ここはこうしたほうがいいよ、うちではこうやっているよ、という知見があればぜひ教えてください:pray:

なお、本記事の手順を実行する場合はご自身の責任にてお願いします。筆者は責任を負いかねますのでご了承ください:bow:


# 前提

この記事では以下のような Mastodon インスタンスを前提としています:

* 一部のページにカスタマイズを加えています（バナーの表示など）。DBには手を加えていません。
* GemやNPMパッケージもオリジナルのMastodonのままです。
* Kubernetes(k8s)クラスタで運用しているので、具体的なアップデートの適用作業は一部Kubernetesに依存した話になります。それ以外の部分は運用方法にかかわらず一般に使えると思います。


# アップデート作業概観

Mastodon のアップデートは、DB のマイグレーションなどを含むため、いきなり本番環境に適用するより、事前に検証や準備を済ませておくのがオススメです。ここでは大きく4つのパートに分けて説明します。

1. **予備調査**: 必要になる作業を調べておきます。基本的には必要な作業は毎回同じですが、バージョンによっては特別な作業が必要なこともある（詳細は後述）ため、ここでそれを調べておきます。
2. **コードの更新と動作確認**: 対象のバージョンのコードに更新して、手元の環境で動作確認をします。また、カスタマイズをしていると、新しいバージョンのコードをマージしたときに大抵コンフリクトするので、その解消も行います。コード的な準備はここで整います。
3. **デプロイ準備**: デプロイの準備を行います。必要な作業は運用方法によって異なりますし、運用方法によっては必要ないこともありそうです。この記事ではk8sクラスタにデプロイする場合にやっておくと良さそうなことを紹介します。
4. **アップデートメンテナンス**: いよいよアップデートを本番環境に適用します。この手順も必要な作業は運用方法によって異なります。この記事ではk8sでの一例を紹介します。

1, 2の手順は、運用方法によらず共通で使える手順です。一方3, 4は運用方法によって具体的な手順は異なるでしょう。また、3や4の手順は CI サービスなどで自動化しても良いですね（ichiji.social でも自動化を検討しています）。


# 手順書

## 1. 予備調査

### 1-1. 現行のバージョンを確認する

自インスタンスの現行の Mastodon のバージョン（以下(A)とする）を確認します（例: `2.6.1`）。

簡単には、自インスタンスの `/about` ページの一番下あたりを見るとわかります。正式にはコードの `lib/mastodon/version.rb` を見ます。

### 1-2. バージョンアップ対象のバージョンを決める

今回アップデートする Mastodon のバージョン（以下(B)とする）を決めます（例: `2.7.3`）。利用可能なバージョンは、Mastodon の GitHub リポジトリから確認できます。
https://github.com/tootsuite/mastodon/releases

特に特定のバージョンにこだわりがなければ、その時点で出ている最新安定バージョン[^1]で良いでしょう。


### 1-3. 必要な作業を洗い出す

(A)と(B)のバージョン間での変更に一通り目を通し、必要な作業を洗い出します。

変更点はすべて https://github.com/tootsuite/mastodon/releases に書いてあるので、これを読みましょう。

**注意点:**

* **`Upgrade notes`は必ず、注意して読みましょう。**アップデートに必要な作業はすべてここに書かれています。たまに特殊な対応が必要なことがあります（後述するpre-deployment migrationなど）。
* (B)の`Upgrade notes`だけでなく、その**中間にあるバージョンの`Upgrade notes`もすべて読みましょう**。もし中間のバージョンで特殊な対応が必要な場合は、それも合わせてやる必要があります。
* Dockerを使っている場合とそうでない場合とで分けて手順が書かれていることがあります。`Both Docker and non-Docker` と書かれている場合は、そのどちらでも必要な手順です。

概ね、以下のような作業が必要なことが多いです。このうちどれかが必要なかったり、逆に付加的な作業がある場合もあります。

* `bundle install`, `yarn install`
* DBのマイグレーション
* アセットのプリコンパイル
* Mastodonプロセスの再起動

(A)〜(B)の間に同じ作業が複数回出てくる場合、基本的には1回だけやればOKです。

## 2. コードの更新と動作確認

Mastodon の開発環境が手元にセットアップされている前提です。

**:warning:このセクションの作業は必ず手元の開発環境で行うこと。本番環境では行わない。**

### 2-1. 開発環境DBのバックアップを取る

以下の手順の途中で、万が一手元の DB が壊れても検証をやり直せるように、DB のバックアップを取っておくと便利です。バックアップは `pg_dump` コマンドでできます。

例: macOS で Homebrew 経由で PostgreSQL をインストールしている場合

```bash
# pg_dumpにPATHが通っていないのでフルパスで実行する
$ /usr/local/Cellar/postgresql@9.6/9.6.6/bin/pg_dump mastodon_development > db-backup-20190305.sql
```

なお、リストアは `psql` コマンドでできます。

例:

```bash
$ psql mastodon_development < db-backup-20190305.sql
```

### 2-2. 対象バージョンのコードを取得する

まず、手元の Mastodon リポジトリに `cd` しておきます。

```bash
$ cd /path/to/mastodon # 例
```

もしオリジナルの Mastodon リポジトリ (https://github.com/tootsuite/mastodon) を、手元のリポジトリの remote に追加していない場合は、以下の手順で追加しておきます。この作業は1回だけでOKです。

```bash
$ git remote add upstream https://github.com/tootsuite/mastodon.git
```

upstream のコードを最新にします。

```bash
$ git fetch upstream
```

### 2-3. 作業ブランチを切る

※以降、そのインスタンスの本番リリース用のコードが置かれているブランチを `production` としますが、ブランチ名は一例です。みなさんの環境によって適宜読み替えてください。

まず、`production` ブランチに移動し、最新の状態にしておきます。

```bash
$ git checkout production
$ git pull origin production
```

そこから今回のアップデート作業のための作業ブランチを切ります。ブランチ名は何でも良いですが、`feature/upgrading-<バージョン名>` のようにするとわかりやすいでしょう。

```bash
$ git checkout -b feature/upgrading-v2.7.3 # 例
```

### 2-4. 対象バージョンをマージし、必要に応じてコンフリクトを解消する

2-2 で取得した対象バージョンのコードを、タグ指定でマージします。

```bash
$ git merge v2.7.3 # 例
```

もし何事もなくマージできたら、運が良いです🎯　2-5へ進みましょう。

コンフリクトしたら、適宜解決します。このとき、**`Gemfile.lock` または `yarn.lock` がコンフリクトしたら、必ず upstream のものに checkout してください**（例: `git checkout --theirs Gemfile.lock yarn.lock`）。そうしないと、インストールするライブラリのバージョンがずれてしまいます。

完了したら、ここで一度コミットすると良いでしょう。

```bash
$ git add -u # コンフリクト解消したファイルも追加
$ git commit
```

### 2-5. 依存ライブラリをインストールする

新しい Mastodon のバージョンになって、必要になっているライブラリが変わっていたり、元々使っていたライブラリでもバージョンが変わっていることがあります。これらの依存ライブラリを改めてインストールします。

**ライブラリのインストールには必ず `bundle install` および `yarn install --frozen-lockfile`[^2] を使います。**`bundle update` や `--frozen-lockfile` 無しの `yarn install` では、ライブラリのバージョンが変わることがあります。マイナーバージョンしか変わっていなくても、挙動に大きく影響がある場合がある（過去に事例あり:sob:[^3]）ため、ライブラリのバージョンは lockfile のものを維持するように注意しましょう。

```bash
$ bundle install
$ yarn install --frozen-lockfile
```

ここで、基本的には `Gemfile.lock` および `yarn.lock` は**変わっていない**はずなので確認します[^4]。

```bash
$ git status
# 変更点が無いことを確認する
```

### 2-6. DBのマイグレーションを実行する

必要に応じて DB のマイグレーションを実行します。1-3の調査の結果必要ないこともあるので、その場合は2-7へ進みます。

#### 2-6-1. pre-deploymentマイグレーション

最近（v2.5.0、v2.7.0）の Mastodon では、通常のマイグレーションの他に、pre-deployment マイグレーションと呼ばれる作業が必要になることがあります。これは通常のマイグレーションの前に実行が必要なもので、さらに新しいバージョンのプロセスが走り始める前に完了させておく必要があります。必要がない場合は2-6-2へ進みます。

pre-deployment マイグレーションの手順は Rails の枠から外れるので、今後変わる可能性がありますが、参考のために v2.5.0 と v2.7.0 での手順を記載しておきます。

```bash
$ SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate
```

このとき、`db/schema.rb` が書き換わることがあります[^5]が、その場合は次の通常のマイグレーションで変更が元に戻るはずなので心配いりません:relieved:

#### 2-6-2. マイグレーション

普通の Rails と同じ手順でマイグレーションします。

```bash
$ bin/rails db:migrate
```

念のため、`db/schema.rb` などに変化がないことを確かめておきます。

```bash
$ git status
# 変更点が無いことを確認する
```

### 2-7. 新しいバージョンで動作確認する

開発環境用のサーバを立ち上げ、動作確認をします。サーバは `foreman` コマンドで立ち上げます。

```bash
$ foreman start
```

問題なく起動すれば、http://localhost:3000/ からアクセスできます。

各種機能を触ってみて、問題が無いか確認します。特に以下の点を確認します。

* トゥートが問題なくできること。また、そのトゥートがタイムラインに反映されること。リロードも試す。
	* Webサーバ、ジョブワーカーに問題がないか調べるため
* タイムラインが問題なく読み込めること。できれば無限スクロールの挙動も試す（そのためにはある程度の量のトゥートが必要）
	* ストリーミングサーバに問題がないか調べるため
* カスタマイズしている部分がある場合は、そこに不具合や表示崩れなどがないこと。

問題があれば適宜修正します。

これで、コード的なバージョンアップ作業は完了です👌　ここまでの変更を `production` ブランチにマージしましょう。複数人で作業している場合は、Pull Request などを通したほうが良いかもしれませんね。

```
$ git checkout production
$ git merge --no-ff feature/upgrading-v2.7.3 # 例
```


あとはこれを本番環境に適用していきます。


## 3. デプロイ準備

必要な準備は運用方法によって異なるため、このセクションは概要だけに留めます。当インスタンスでは、k8s へのデプロイに備えて以下のことを行います。

1. Dockerコンテナのビルド（pushはまだしない）
2. 必要に応じてk8s設定ファイルの更新（適用はまだしない）
3. メンテナンスの計画と利用者へのアナウンス

このセクションの作業は、アップデートメンテナンス当日でなくてもできるので、メンテナンスの前日くらいまでにやっておくとベターです。

### 3-1. Dockerコンテナをビルドする

新しいバージョンのコードで Docker コンテナをビルドしておきます。また、ビルドに問題が無いことも確認します。たまにベースイメージの変更などによりビルドがうまくいかないことがあります（ありました）。

例:

```bash
# --tagオプションは後ほどGCR(Google Container Registry)にイメージをpushするためのもの
$ docker build --tag=gcr.io/somegcpproject/mastodon .
```

ところで、v2.5.0 から[イメージビルド時に `assets:precompile` が走るようになった](https://github.com/tootsuite/mastodon/pull/7780)ので、メンテナンス時の手順が1つ減って楽になりました👌　一方でその分イメージビルドにしばらく時間がかかる（手元の環境では15分程度）ようになったので、時間に余裕を持ってやるのが吉です（メンテナンス当日にあわててやってはいけないぞ！:pensive:）

### 3-2. k8s設定ファイルを更新する

k8s の場合、Pod というリソースを更新することで、新しいバージョンを適用します。当インスタンスでは、Webサーバ用、ジョブワーカー用などの Pod をそれぞれ Deployment というリソース経由で管理しているので、その Deployment を更新することで新しい Pod に置き換えます。

Deployment の更新では、[設定ファイルの Pod テンプレート内に変更がないと、Pod を更新しません](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#updating-a-deployment)。そこで当インスタンスでは、独自のリビジョン番号（`20190319001` のようなもの）を Pod のラベルに設定して、それをデプロイごとに書き換えることで Deployment の更新を行っています（もっとよい方法があればぜひ教えてください🤔）。この書き換えも済ませておきます。

イメージ:

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 4
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: frontend
        revision: "20190319001" # ←ここ

# ...
```


### 3-3. メンテナンス時間枠を決め、利用者にアナウンスする

ここまでですべて前準備は整うので、ここまでの情報をもとに作業時間を見積もり、メンテナンス時間枠を決めます。また、利用者にも事前にアナウンスするのが良いでしょう:mega:


## 4. アップデートメンテナンス

3-3で決めた時間枠で実施します。

このセクションも、具体的な手順は運用方法によって異なるため、概要のみ紹介します。k8s 環境では、大まかには以下のことをやります。

1. データベースをバックアップ
2. Dockerイメージをアップロード
3. pre-deploymentマイグレーション、通常のマイグレーションなどのジョブを実行
4. Podを更新

### 4-1. データベースのバックアップを作成する

アップデート作業では、マイグレーションなどによりデータベースの破損の危険性があるため、アップデート作業の直前の時点でのデータのバックアップを**必ず**取っておきましょう（もちろん、それとは別に定期的にバックアップを取るようにしておくのが良いでしょう）。

### 4-2. Dockerイメージをアップロードする

3-1で作成した Docker イメージを k8s クラスタから見えるリポジトリにアップロードします。

例:

```bash
$ gcloud docker -- push gcr.io/somegcpproject/mastodon
```

なお、これ以降は、何らかの原因で Pod が再起動すると、新しいバージョンのコンテナが走り始めるため注意が必要です（だからこのタイミングで実施します）。

TODO: [`gcloud docker` は DEPRECATED](https://cloud.google.com/sdk/gcloud/reference/docker) なので `gcloud auth configure-docker` に移行する

### 4-3. pre-deploymentマイグレーションを実行する（必要な場合）

2-6-1で pre-deployment マイグレーションを行っている場合は、このタイミングで本番環境に対しても同じ作業が必要となります。

k8s で one-off の処理を走らせるには Job リソースが便利です。当インスタンスでは pre-deployment マイグレーションを実行するための Job を以下のような設定で作ってあります。`env` に `SKIP_POST_DEPLOYMENT_MIGRATIONS` が書いてあるのがミソです。

```pre-deployment-migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: pre-deployment-migration-job
spec:
  template:
    spec:
      containers:
      - name: web
        image: gcr.io/somegcpproject/mastodon
        imagePullPolicy: Always
        env:
        - name: SKIP_POST_DEPLOYMENT_MIGRATIONS
          value: "true"
        envFrom:
        - configMapRef:
            name: env-production # ここは環境に応じて
        volumeMounts:
        - name: mastodon-disk-system
          mountPath: /mastodon/public/system
        command: ["bundle", "exec", "rails", "db:migrate"]
      volumes:
      - name: mastodon-disk-system
        emptyDir: {}
      restartPolicy: Never
  backoffLimit: 1
```

これを `kubectl create` すると、pre-deployment マイグレーションが走り始めます。

```bash
$ kubectl create -f pre-deployment-migration-job.yaml
```

pre-deployment マイグレーションは、時間がかかることがあります（v2.5.0 のときは確か30分ほどかかりました）。心臓に悪いです:heartbeat:

### 4-4. 各種Podを更新する

3-2で調整した Deployment の設定ファイルを `kubectl apply` で適用し、Pod を置き換えます。また、当インスタンスでは Pod の `initContainers` の設定で DB のマイグレーション (`rails db:migrate`) を実行するようにしています[^6]。

```yaml
apiVersion: extensions/v1beta1
kind: Deployment

# ...

spec:
  template:
    spec:
      initContainers:
      - name: db-migration
        image: gcr.io/somegcpproject/mastodon
        imagePullPolicy: Always
        envFrom:
        - configMapRef:
            name: env-production
        command: ["sh", "-c", "bundle exec rails db:migrate"]

# ...
```

### 4-5. 動作確認する

作業としてはここですべて完了です。動作に問題が無いか本番環境にアクセスして簡単に確かめます。

### 4-6. 完了

お疲れ様でした:tada:


# さいごに

まとめてみて改めて感じましたが、Mastodon インスタンス、特にカスタマイズをしているインスタンスのアップデートは意外とやることや注意することが多くて大変です。

こうして一度手順をまとめてしまえば、次回以降のアップデート作業が格段に楽になります。また、自動化する際の足がかりにもなりますね。

みなさんもこの手順書を参考にバシバシ Mastodon をアップデートしてください。よい Mastodon 運営ライフを！:wave:


[^1]: バージョン番号に`rc`とつくリリースは避けたほうが無難です。rcはRelease Candidateの略で、いわゆる「先行リリース版」なので安定していないことがあります。
[^2]: https://github.com/yarnpkg/yarn/issues/4379
[^3]: 以前、誤ってPaperclipのバージョンを6.0.0→6.1.0に上げてしまい、それが原因でリモートフォローに失敗するなどの不具合が出たことがあります（[同様の事例](https://github.com/tootsuite/mastodon/issues/8983)）。気をつけましょう:pensive:
[^4]: upstreamに無いライブラリを独自に追加している場合は、この限りではありません。当インスタンスでは特にライブラリは追加していないため、そのケースは未検証です。
[^5]: v2.7.0のpre-deploymentマイグレーションで起こったことがあります。
[^6]: pre-deploymentマイグレーションも`initContainers`に入れてしまおうかと考えています🤔　そうするとJobを作らなくて良くなります。ただ、pre-deploymentマイグレーションのやり方が今後も変わらないのか懸念です。