タグ: Ubuntu Page 1 of 30

Apache RewriteRuleを書き直して気付いたこと。「正規表現」は短く書くためではなく、保守しやすくするために使う

先日、Redmine の Googlebot 対策として Apache の mod_rewrite を使った RewriteRule を追加しました。

まずは CPU 使用率を下げることが最優先だったため、とにかく確実に動くことを重視して場当たり的に正規表現を追加しました。

なので、今回は、RewriteRule を書き直しながら改めて感じた、「正規表現は短く書くためではなく、保守しやすくするための道具」という話です。

動けばいい。だが、そんな状態では後でメンテしづらい

最初に書いた設定はこんな感じでした。

RewriteRule ^/projects/.*/knowledgebase - [G,L]
RewriteRule ^.*/knowledgebase - [G,L]

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.pdf$ - [G,L]

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.csv$ - [G,L]

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.atom$ - [G,L]

もちろん、これでも動きます。むしろトラブル対応中であれば、このくらい分かりやすく書いた方が安全です。

ただ、冷静になって眺めてみると、

「同じことを何回も書いている」

ことに気付きました。

正規表現とは何か

そこで、さらに正規表現で直していきます。「正規表現」という言葉を聞くと、難しそうな印象を持たれる方も多いかもしれませんが、思ったよりも単純なルールでできます。

例えば、

issues.pdf
issues.csv
issues.atom

この3つは末尾だけが違います。

これを

issues\.(pdf|csv|atom)

と書けば、

.pdf でも .csv でも .atom でも一致する」

という意味になります。MtGで言うなれば、

  • 日没を遅らせる者
  • 時を解す者
  • ドミナリアの英雄

につく「テフェリー」は

(日没を遅らせる者|時を解す者|ドミナリアの英雄)、テフェリー

と書けます。このように、共通点があるものを一つのルールで表現する。それが正規表現です。

書き直した結果

最終的には次のような形に整理しました。

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteCond %{REQUEST_URI} ^/(projects/[^/]+/)?issues\.(pdf|csv|atom)$ [NC,OR]
RewriteCond %{QUERY_STRING} (^|&)(sort|set_filter|per_page)= [NC]
RewriteRule ^/(projects/[^/]+/)?issues - [G,L]

PDF、CSV、Atom を一つの正規表現へまとめ、さらに「動的クエリ」と「エクスポート要求」を一つの RewriteRule で処理できるようにしています。

行数だけを見ると少し減った程度です。

ですが、保守性はかなり向上しました。

保守性を求めた正規表現

今回の目的は「短く書くこと」ではありません。

例えば将来、

issues.json

も遮断対象にしたくなったとします。以前の書き方なら RewriteRule を一本追加します。この書き方であれば

(pdf|csv|atom|json)

と一か所を書き換えるだけで済みます。変更箇所が一つだけになる。これは保守する上でかなり大きな違いです。

Knowledgebaseも同じ考え方で整理。

Knowledgebase の RewriteRule も整理しました。

以前は

RewriteRule ^/projects/.*/knowledgebase - [G,L]
RewriteRule ^.*/knowledgebase - [G,L]

のように複数行で書いていました。これを

RewriteRule ^/(home/www-data/|(.*/)?knowledgebase) - [G,L]

という一つのルールへまとめています。さらに、このルールでは以前問題になった

/home/www-data/...

のような物理パスへの誤アクセスも同時に処理しています。

「Knowledgebaseだけ」ではなく、「Rails に渡したくないもの」という視点で整理し直した結果です。

「同じ意味のもの」をまとめる

今回 RewriteRule を整理していて感じたのは、正規表現を書くことが目的ではないということです。目的は、「同じ意味を持つものを一つのルールで表現する」ことでした。

  • Knowledgebase は「存在しない旧機能」です。
  • PDF、CSV、Atom は「重いエクスポート」です。
  • sort や set_filter は「重い動的クエリ」です。

そう考えると、設定ファイルも「何を止めたいのか」が分かる構成になっていきます。

未来の自分が読める設定にする

サーバー設定は、一度書いたら終わりではありません。半年後、一年後あるいは障害対応中の深夜に、また自分が読むことになります。

その時に

「この RewriteRule は何を止めているんだっけ?」

と悩むようでは、あまり良い設定とは言えません。もちろん、正規表現は凝ろうと思えばいくらでも複雑にできます。ですが、複雑さは必ずしも保守性につながりません。

今回の書き直しで目指したのは、「短い設定」ではなく、 「意図がまとまっていて、後から見ても理解しやすい設定」 でした。
正規表現は、そのための道具なのだと改めて感じた次第です。

Redmine 6.x移行記(後編)犯人はGooglebotだった。Apacheの410 GoneでPassengerを守るまで

前回の記事では、Redmine 6.1への移行後に Passenger の CPU 使用率が異常に高止まりし、production.log と netstat を追い始めたところまでを書きました。

調べていく中で、

  • 削除した Knowledgebase へのアクセス
  • sort=set_filter= を含むチケット一覧
  • PDF や CSV、Atom のエクスポート要求

といった、一見すると関連性のないリクエストが大量に飛んできていることが分かってきます。そして最後に、

「もしかしてクローラーなのでは?」

という疑問が残りました。今回は、その続きを書いていこうと思います。

アクセス元を確認してみる

まずは production.log とアクセスログを突き合わせながら、アクセス元を確認してみました。

すると、見えてきたのは見慣れた IP アドレスです。

66.249.xx.xx

Googlebot でした。しかも一度や二度ではありません。HTTPS のセッションを何十本も張りながら、次々とリクエストを投げています。

しかもアクセスされていたのは、削除した Knowledgebase だけではありません。チケット一覧についても、

sort=
page=
set_filter=

といったパラメータ違いを延々と巡回しています。さらに、

issues.pdf
issues.csv
issues.atom

まで取得しようとしていました。普段ブラウザで Redmine を使っていると、これらは「便利な機能」という認識です。

ですが、サーバー側から見ると話は変わります。例えば PDF の生成であれば、データベースからチケットを取得し、テンプレートを組み立て、PDF をレンダリングし、レスポンスを返す。

HTML を返すだけとは比較にならないくらい重い処理になります。

つまり Googlebot は、リンクがあるから辿っているだけなのですが、Passenger からすると重い仕事ばかり持ち込まれている状態でした。

404でもPassengerは起きてしまう

Knowledgebase のアクセスについても同じです。

「存在しないページなんだから404で終わりでは?」

最初はそう考えていました。しかし実際には、Rails が起動し、ルーティングを行い、存在しないことを確認し、404 を返します。404 を返すこと自体は正しい動作です。ですが、その404を返すためにも Passenger は仕事をしています。

これでは、存在しないページへのアクセスであっても CPU を使い続けることになります。

ならばRailsまで届かせなければいい

ここで考え方を変えました。Rails が重いのではありません。Rails に仕事を渡してしまっていることが問題です。

であれば、Rails が起きる前にApacheで止めればいい。Apache を使っている以上、一番軽いのは Apache の段階で処理してしまうことです。

そこで mod_rewrite を使うことにしました。バーチャルホストの.confを以下のように変えていきます。

RewriteRule ^/projects/.*/knowledgebase - [G,L]
RewriteRule ^.*/knowledgebase - [G,L]

まずは削除済みの Knowledgebase。続いて、

RewriteCond %{HTTP_USER_AGENT} (Googlebot|GoogleOther|bingbot|Baiduspider) [NC]
RewriteRule ^/(projects/[^/]+/)?issues\.(pdf|csv|atom)$ - [G,L]

PDF、CSV、Atom のエクスポート。さらに、

RewriteCond %{QUERY_STRING} (sort=|set_filter=|per_page=) [NC]
RewriteRule ^/(projects/[^/]+/)?issues - [G,L]

検索エンジンが総当たりしている動的クエリも Apache 側で受け止めることにしました。

404ではなく410を選んだ理由

404でも目的は達成できます。ですが、今回は410 Goneを選びました。

404エラーは、「今は見つからない」という意味です。検索エンジンから見ると、

「また後で来れば復活しているかもしれない」

という扱いになります。一方、410 Gone は、

「もう永久にありません。」

という宣言です。今回削除した Knowledgebase は、将来復活させる予定はありません。だったら、その意思を HTTP ステータスとして返した方が正確です。

robots.txtも追加した

もちろん、Apache の設定だけではありません。今後の巡回そのものを減らすため、robots.txt にも、

Disallow: /projects/*/knowledgebase*
Disallow: /*?*sort=
Disallow: /*?*set_filter=

などを追加しました。ただし、ここは誤解しやすいところです。robots.txt は即効性のある仕組みではありません。

既に巡回予定へ入っているリクエストは、そのまま飛んできます。まず Apache で止める。その上で robots.txt により今後の巡回を減らす。この二段構えにしました。

効果はすぐに現れた

設定を反映し、Passenger(mod_passengerなのでapacheそのもの)を再起動します。

その後、再び CPU 使用率を確認すると、先ほどまで70〜90%を推移していた Passenger が数%程度で落ち着いていました。

production.log からも

  • Knowledgebase の404、
  • IssuesController の大量アクセス、
  • 500.html

の RoutingError は姿を消しています。試しに curl から Googlebot の User-Agent を付けてアクセスしてもApache が Rails を起動することなく、即座に

HTTP/1.1 410 Gone

を返しました。狙い通りです。Passengerに到達すること無くは何も仕事をしていません。

今回学んだこと

今回改めて感じたのは、「プラグインを削除すること」と、「削除したことを検索エンジンへ伝えること」は全く別の話だということでした。

Redmine 側では綺麗にプラグインをアンインストールできていても、Google は昔の URL を覚えています。そして真面目に巡回を続けます。そのリクエストを Rails まで届けてしまえば、存在しないページであっても Passenger は起き、CPU を使います。

だからこそ、410エラーにより「もうありません。」という判断を Apache が先に行う。

今回のケースでは、この水際防衛が一番効果的でした。

Redmine に限らず、長く運用してきた Web アプリケーションでは、大きな整理やプラグインの削除を行った後、一度アクセスログを眺めてみることをおすすめします。

自分ではとっくに忘れていた URL を、検索エンジンは驚くほど律儀に覚えているものです。そして、それが思わぬサーバー負荷につながっていることも、決して珍しくありません。

余談「Apacheは実は柔軟」

これは私の持論なのですが、超大手プラットフォームや大手配信サイトではapacheは重い。nginxこそ主流だ的な記事を見かけますが筆者のように

多数のサーバを立てられない
それでもWebアプリを多数動かしたい
セキュリティも譲れない
場合はApacheの方が驚くほど柔軟なパターンがあります。その辺の話はまた改めて。

【WAF検知ログ解析】難読化された JavaScript 偵察ツールを ModSecurity はどう見抜いたのか

はじめに

Redmine 6.1への移行作業がようやく一段落したので、久しぶりにサーバーログを眺めていました。

すると、トップページ (hoge.example.com) に対して、見慣れない長大なペイロードが飛んできていました。

最近の攻撃は「脆弱性を突く」ことだけが目的ではありません。まずは対象がどんな環境なのかを調べ、その結果によって次の手を変えてきます。

今回は、その偵察段階で送られてきたJavaScriptペイロードがなかなか興味深い内容だったので、ModSecurityのログを追いながら見ていきます。

検出された ModSecurity ログ

まずは実際のログです。

例によってIPアドレスはダミーに置き換えています。テロリストに名前を与えていません。

[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Warning. Pattern match "(?i)(?:b[\\"'\\\\)\\\\[\\\\x5c]..." at ARGS:0. [file "REQUEST-932-APPLICATION-ATTACK-RCE.conf"] [line "205"] [id "932235"] [msg "Remote Command Execution: Unix Command Injection (command without evasion)"] [data "Matched Data: eval)(global[String['from'+'CharCode'](66,117,102,102,101,114)].from('KGFzeW5jIGZ1bmN0aW9uKCl7Ci8vIGZhc3RfcmVjb25fdjY..."] [hostname "hoge.example.com"] [uri "/"]
[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Rule [id "932250"] - Execution error - PCRE limits exceeded (-47): (null). [hostname "hoge.example.com"] [uri "/"]
[Wed Sep 02 02:04:07 2026] [security2:error] [client 198.51.100.24:56250] ModSecurity: Warning. Pattern match "(?i)\\\\b\\\\(?[\\"']*(?:assert|eval|exec|passthru)..." at ARGS:0. [file "REQUEST-933-APPLICATION-ATTACK-PHP.conf"] [line "406"] [id "933160"] [msg "PHP Injection Attack: High-Risk PHP Function Call Found"] [hostname "hoge.example.com"] [uri "/"]

最初に目を引くのは Rule 932235 が反応していることです。

Remote Command Execution:
Unix Command Injection

さらに途中では

PCRE limits exceeded (-47)

というログも出ています。

Base64の中身を見てみる

ログ中に埋め込まれていたBase64文字列をデコードすると、次のようなヘッダーが現れました。

(async function(){
// fast_recon_v6 — signature-rotated recon payload
// Changes from v5:
//   - Randomized top-level JSON keys (no fixed schema to fingerprint)
//   - Variable output structure per invocation
//   - IMDS calls use randomized User-Agent + jittered timeouts
//   - File reads are shuffled order (no deterministic sequence)
...

名前からも分かるように、これは偵察を目的としたJavaScriptです。面白いのは「何を調べるか」よりも、「どうやってWAFの検知を避けようとしているか」の方でした。

難読化の目的はコードを隠すことではない

例えば、

global[String['from'+'CharCode'](...)]

という書き方。普通なら

Buffer

と書けば済むものを、わざわざ文字コードから組み立てています。さらに

(0,eval)(...)

という間接呼び出しも使われています。どちらもJavaScriptとしては動作は同じですが、単純な文字列検索やシグネチャ検知を避けるための定番手法です。

コードを読めなくすることより、「機械に見つかりにくくすること」の方が目的と言った方が近いでしょう。

偵察対象も今どきらしい

コードを見ると、次のような情報収集も行おうとしていました。

  • クラウドメタデータ(IMDS)へのアクセス
  • IAMロールなど認証情報の取得
  • ファイル探索順序のランダム化
  • User-Agentやタイムアウト値のランダム化

固定パターンをできるだけ作らないよう工夫されており、「同じ攻撃を毎回少しずつ変える」という最近の攻撃らしい作りになっています。

PCRE が限界を迎えても、防御は止まらない

途中で

PCRE limits exceeded (-47)

が記録されています。これは巨大な入力に対して、ある正規表現がバックトラック上限へ達したことを示しています。

しかし、OWASP CRSは一つの巨大なルールだけで防御しているわけではありません。このリクエストでは、

  • 932235
  • 932260
  • 933160
  • 933210

と、別方向から検査するルールが続いてヒットしました。

つまり、一つの正規表現が処理を諦めても、他のルールが引き継ぐ設計になっています。

多層防御という言葉はよく聞きますが、今回のログはその動きを非常に分かりやすく見せてくれました。

おわりに

最近は攻撃そのものより、「検知をどう回避するか」の工夫を見る機会が増えてきました。

とはいえ、こうして落ち着いて中身を眺められるのは、防御基盤が先に仕事をしてくれているからです。

「サーバー運用はサファリパークに似ている」ようなものです。猛獣を間近で観察できるのは面白いですが、それは檻や装甲車があるからこそです。

今回も同じでした。難読化されたJavaScriptをじっくり読めたのは、攻撃がアプリケーションまで届かなかったからです。

運用者としては、その「観察できる余裕」こそが、一番ありがたい成果だったのかもしれません。

WasabiをRedmineの添付ファイル保存先にしたら、画像の貼り付けだけ「早期削除」が気になり始めた話

Redmine の添付ファイル保存先として、Wasabi オブジェクトストレージを s3fs でマウントして利用しています。

運用自体は安定していましたが、ある日 Wasabi のダッシュボードを眺めていて違和感を覚えました。

Timed Object Storage(早期削除)の対象が、思ったより増えている。

以前、Nextcloud や Growi とオブジェクトストレージの組み合わせで、Timed Object Storage に長期間悩まされた経験(150日の亡霊)があります。

そのとき学んだのは、「動いている」ことと、「そのストレージに適した運用」であることは別だということでした。

その経験があったからこそ、今回も「また何か起きているのではないか」と考え、一つずつ確認してみることにしました。

添付方法によって違いがあるのでは?

まず比較したのは、画像の添付方法です。

確認した結果、少なくとも現時点では次のような傾向が見えました。

添付方法Timed Object Storage の発生
ファイル選択からアップロード確認できず
Ctrl+Vでクリップボード貼り付け確認
Enhanced UIなどの貼り付け機能確認

もちろん、これだけで原因が断定できるわけではありません。「画像を添付する」という同じ操作でも、添付方法によって内部処理が違う可能性は十分考えられます。

クリップボード貼り付けでは一時ファイルを生成・削除しているのかもしれませんし、Enhanced UI 側の実装が影響しているのかもしれません。

現時点では、そこまで踏み込んだ検証はできていません。

今回試してみる対策

以前の経験から、オブジェクトストレージへ直接細かなアクセスを繰り返す構成は、あまり相性が良くないと感じています。

そこで今回は、s3fs のキャッシュ機能を利用して、ローカル SSD をワンクッション挟む構成へ変更してみることにしました。

追加した主なオプションは次の3つです。

  • use_cache
  • stat_cache_expire=60
  • enable_content_md5

まずキャッシュディレクトリを作成します。

sudo mkdir -p /var/cache/s3fs_bucket_a /var/cache/s3fs_bucket_b
sudo chown -R www-data:www-data /var/cache/s3fs_bucket_a /var/cache/s3fs_bucket_b
sudo chmod 750 /var/cache/s3fs_bucket_a /var/cache/s3fs_bucket_b

続いて /etc/fstab を更新します。

cd /etc
sudo cp -pi fstab /etc/conf_backup/fstab.20260830
sudo nano fstab
--- /etc/conf_backup/fstab.20260830     2025-08-07 11:01:54.000000000 +0900
+++ fstab                               2026-08-30 19:58:37.000000000 +0900
@@ -2,8 +2,9 @@
 LABEL=BOOT      /boot   ext4    defaults        0 2
 LABEL=UEFI      /boot/efi       vfat    umask=0077      0 1
 /swapfile       none    swap    sw      0 0
-# Wasabi Bucket A (storage.example.com)
-s3fs#storage.example.com /mnt/wasabi fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33 0 0
 
-# Wasabi Bucket B (counter.example.org)
-s3fs#counter.example.org /mnt/wasabi2 fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33 0 0
+# Wasabi Bucket A (storage.example.com - ap-northeast-1)
+s3fs#storage.example.com /mnt/wasabi fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33,use_cache=/var/cache/s3fs_bucket_a,stat_cache_expire=60,enable_content_md5 0 0
+
+# Wasabi Bucket B (counter.example.org - ap-northeast-1)
+s3fs#counter.example.org /mnt/wasabi2 fuse _netdev,allow_other,passwd_file=/home/sampleuser/.passwd-s3fs,url=https://s3.ap-northeast-1.wasabisys.com,use_path_request_style,uid=33,gid=33,use_cache=/var/cache/s3fs_bucket_b,stat_cache_expire=60,enable_content_md5 0 0

変更後は再マウントします。(daemon-reloadしないと怒られました)

sudo umount /mnt/wasabi
sudo umount /mnt/wasabi2
sudo systemctl daemon-reload
sudo mount /mnt/wasabi
sudo mount /mnt/wasabi2

これで本当に改善するのか?

正直なところ、この記事を書いている時点ではまだ分かりません。今回の変更は、「クリップボード貼り付け時の一時ファイルが原因ではないか」という仮説に基づく対策です。

実際に Timed Object Storage の発生が止まるのか、それとも別の要因があるのかは、しばらく Wasabi のダッシュボードを見ながら経過観察する必要があります。

もし改善が確認できれば追記しますし、変化がなければ別の原因を探ることになります。

まとめ

今回の目的は、「原因を突き止めた」という報告ではありません。

Wasabi のダッシュボードで小さな違和感を見つけ、その原因として添付方法の違いに着目し、対策を試し始めたという記録です。

以前、オブジェクトストレージとの組み合わせで大きく痛い目を見た経験があるからこそ、「いつもと違う」を見逃さずに済みました。

サーバー運用では、エラーが出てから対応するよりも、違和感の段階で調べ始める方が結果として被害は小さく済みます。

今回の変更が正解かどうかは、これからの経過観察で判断したいと思います。

【Redmine 6.1移行】DBマイグレーションで「Table already exists」連発? 本体統合された機能との競合を解消して移行を完走した記録

Redmine 5.1からRedmine 6.1への移行では、できるだけ本番環境へ影響を出さないよう、検証環境で一つずつ作業を進めてきました。

今回は以前のように「一気にアップグレードして問題を追う」のではなく、

  • 検証環境を作る
  • 問題が起きたら原因を調べる
  • 記録として残す
  • 次の工程へ進む

という流れで進められたため、最終的にはかなり安心して6.1環境を完成させることができました。

その途中で遭遇したのが、データベースマイグレーション時の Table already exists エラーです。

最初は単純なテーブル重複かと思いましたが、原因を追っていくとRedmine 6.0で行われた「プラグイン機能の本体統合」が関係していました。

今回は、このマイグレーションエラーの内容と対処手順を記録しておきます。

実データを流し込んだ直後にマイグレーションが止まる

今回の手順では、Redmine 5.1で整理・純化しておいたデータベースをMySQLダンプから復元し、そのままRedmine 6.1側でマイグレーションを実行しました。

cd /home/www-data/redmine_v6
sudo -u www-data RAILS_ENV=production bundle exec rake db:migrate

ところが途中で処理が停止します。

最初に止まったのは、リアクション機能です。

== 20250423065135 CreateReactions: migrating ==================================
-- create_table(:reactions)

Mysql2::Error:
Table 'reactions' already exists

この時点では、

「どこかでマイグレーションを実行し忘れたかな?」

程度に考えていました。ところが、修正して再実行すると、今度はこちら。

== 20250611092155 CreateDoorkeeperTables: migrating ===========================

Mysql2::Error:
Table 'oauth_applications' already exists

また別のテーブルが既に存在すると言われます。つまり偶然ではなく、何か共通した原因がありそうでした。

原因は「昔はプラグイン、今は標準機能」

調べてみると、どちらもRedmine 6.0で本体へ取り込まれた機能でした。

今回衝突したのは、

  • リアクション機能
  • OAuth認証(Doorkeeper)

の二つです。どちらも以前はプラグインとして利用していましたが、Redmine 6ではコア機能になっています。つまり、

Redmine 5.1時代

プラグイン
    ↓
DBにテーブル作成

だったものが、Redmine 6.1では、

Redmine本体
    ↓
同じ名前のテーブルを作成

という流れに変わっています。そのため、旧環境からDBを持ってくると、

既に存在するテーブルを、Redmine本体がもう一度作ろうとする

という状態になっていました。

なぜDROPしてよいのか

ここで少し悩みました。

「既存テーブルを削除してしまって本当に大丈夫なのか?」

しかし今回は、本体側へ正式に統合された機能です。つまり最終的に利用するのはRedmine本体が管理するスキーマになります。古いプラグイン時代のテーブルを残していても、最終的には使われません。

そこで今回は、競合しているテーブルだけを削除し、本体マイグレーションに改めて生成してもらうことにしました。

競合しているテーブルを削除する

MySQLから競合しているテーブルを削除します。

mysql -u redmine_v6 -p redmine_v6 -e "
DROP TABLE IF EXISTS reactions;
DROP TABLE IF EXISTS oauth_access_tokens;
DROP TABLE IF EXISTS oauth_access_grants;
DROP TABLE IF EXISTS oauth_applications;
"

これで、本体側が新しくテーブルを作れる状態になります。

改めてマイグレーションを実行する

続いて再度マイグレーションを実行します。

cd /home/www-data/redmine_v6
sudo -u www-data RAILS_ENV=production bundle exec rake db:migrate

今度は問題なく進みます。

== CreateReactions: migrated
== EnsureWikiTablesortSettingIsStoredInDb: migrated
== CreateDoorkeeperTables: migrated

最後まで完走し、正常終了しました。

プラグイン側も忘れずに更新

本体が終わったら、続いてプラグイン側のマイグレーションも実行します。

sudo -u www-data RAILS_ENV=production bundle exec rake redmine:plugins:migrate

本体だけ更新して安心しがちですが、ここまで実行して初めてプラグイン側も新しい環境へ追従できます。

configuration.ymlも忘れずに引き継ぐ

今回の環境ではSMTP設定も引き継ぐ必要がありました。

既存環境から configuration.yml をコピーします。

sudo -u www-data cp -p \
/home/www-data/redmine/config/configuration.yml \
/home/www-data/redmine_v6/config/configuration.yml

その後、Passengerを再起動します。

sudo touch /home/www-data/redmine_v6/tmp/restart.txt
sudo systemctl reload apache2

最後に管理画面からテストメールを送信し、Zoho Mail経由で正常に届くことまで確認しました。

移行後の確認

今回の検証では、最終的に以下を確認できました。

  • 過去のチケット・Wiki・添付ファイルを正常に閲覧できる
  • ガントチャートも問題なく表示される
  • kodomo テーマもRedmine 6.1環境で正常動作
  • テストメールを送信し、Zoho Mailで受信できることを確認
  • DMSFを利用しなくても、標準添付機能で画像を配置できることを確認

ここまで確認できれば、検証環境としては十分安心できる状態になりました。

今回の移行を振り返って

今回のRedmine 6.1移行では、「作業そのもの」よりも「途中で何が起きたか」を残しながら進められたことが大きかったように思います。

以前であれば、エラーを解消して先へ進むことを優先していた場面でも、

  • なぜ起きたのか
  • Redmine側の仕様変更なのか
  • プラグイン由来なのか
  • 次に同じ作業をするとき、何を確認すればよいのか

という視点で整理しながら進められました。

結果として、今回遭遇した Table already exists も「たまたま起きたエラー」ではなく、Redmine 6でプラグイン機能が本体へ統合されたことによる仕様変更だと理解できました。

移行作業では、どうしてもエラーそのものへ目が向きがちですが、「なぜそのエラーが起きたのか」まで追っておくと、次回以降の作業はずっと楽になります。

今回の6.1環境は、そうした記録を積み重ねながら構築できたこともあり、これまでで一番安心して仕上げられたバージョンアップだったように感じています。

Redmine 6.1環境に Additionals と Additional Tags を導入する ― 3つの落とし穴と解決の記録

はじめに

Redmine 6.1(Rails 7.2系)の検証環境構築に続き、今回は各種UI拡張やマクロ機能の土台となる Additionals および Additional Tags プラグインを導入しました。

依存関係の親玉とも言えるこの2つのプラグインですが、最新版(main ブランチ)をそのままクローンしてマイグレーションを実行したところ、複数の予期せぬエラーに遭遇しました。

今回は、遭遇した3つのトラブルと、それを解決して Redmine 6.1 上で正常認識させるまでのメモです。

今回の前提条件

  • Redmine: 6.1.4.stable (Rails 7.2.3.2)
  • Ruby: 3.2.x
  • 対象プラグイン:
  • additionals (AlphaNodes製 / 共通ライブラリ)
  • additional_tags (AlphaNodes製 / タグ機能拡張)

遭遇した3つの罠と対処法

罠1: 最新版が「Redmine 7.0以上」を要求して abort

GitHubから main ブランチをそのままクローンしてマイグレーションを実行したところ、以下のエラーが発生しました。

Redmine::PluginRequirementError: additional_tags plugin requires Redmine 7.0 or higher but current is 6.1.4.stable.25001
  • 原因: リポジトリ先端(main)の開発が既に次世代の Redmine 7.0 / Rails 8系 向けに進んでおり、6.1環境が弾かれていました。
    • 対処: Redmine 6.x に対応している安定版タグ(v3.4.0 など)へチェックアウトを切り替えます。

罠2: Gitの「dubious ownership」セキュリティエラー

sudo -u www-data git checkout を実行した際、Git のセキュリティ機構(所有権チェック)に阻まれました。

fatal: detected dubious ownership in repository at '/home/www-data/redmine_v6/plugins/additionals'
  • 原因: 実行ユーザーとディレクトリ所有者(www-data)の不一致による安全装置の作動。
    • 対処: www-data ユーザーの Git グローバル設定にセーフディレクトリを登録して回避しました。
sudo -u www-data git config --global --add safe.directory '*'

罠3: プラグイン依存 Gem の未調合(Could not find compatible versions)

タグを切り替えて rake redmine:plugins:migrate を実行したところ、今度は Bundler が停止しました。

Because every version of additionals depends on render_async >= 0
  and render_async >= 0 could not be found in locally installed gems,
  additionals cannot be used.
  • 原因: プラグインが要求する Gem(render_async 等)が、ローカルの vendor/bundle にインストールされていませんでした。
    • 対処: マイグレーション前に必ず bundle install を通す必要があります。

成功した導入手順まとめ

1. リポジトリのクローンと安全設定

  • Redmine(検証用)ルートディレクトリに移動
cd /home/www-data/redmine_v6/plugins
  • プラグインのgit clone
sudo -u www-data git clone https://github.com/alphanodes/additionals.git
sudo -u www-data git clone https://github.com/alphanodes/additional_tags.git
  • Git 所有権警告の解除
sudo -u www-data git config --global --add safe.directory '*'

2. Redmine 6.x 対応タグへのチェックアウト

  • additionalsでチェックアウト
cd /home/www-data/redmine_v6/plugins/additionals
sudo -u www-data git checkout $(sudo -u www-data git tag -l | grep -E '^v?3\.' | tail -n 1)
  • additional_tagsでチェックアウト
cd /home/www-data/redmine_v6/plugins/additional_tags

```bash
sudo -u www-data git checkout $(sudo -u www-data git tag -l | grep -E '^v?3\.' | tail -n 1)

3. 依存 Gem のローカルインストール(最重要)

cd /home/www-data/redmine_v6
sudo -u www-data bundle install

4. マイグレーションと再起動

sudo -u www-data bundle exec rake redmine:plugins:migrate RAILS_ENV=production
sudo -u www-data bundle exec rake assets:precompile RAILS_ENV=production

おわりに

構築中のRedmine 6.1サイトにアクセスし、管理画面(/admin/plugins)を開くと、Additional Tags (3.4.0)Additionals (3.4.0) が無事に認識され、エラーなく設定画面を開くことができました。

プラグイン導入時は、「最新ブランチを追うのではなく適切なリリースバージョン(タグ)を選ぶこと」と「DBマイグレーションの前に bundle install で Gem 依存を揃えること」が鉄則であると再認識させられた検証でした。

Redmine 5.1を壊さずに、同じサーバーへRedmine 6.1検証環境を構築する

Redmine 6.xへの移行を見据え、現在運用しているRedmine 5.1とは別に、同一サーバー上へRedmine 6.1の検証環境(redmine-v6)を構築しました。

「同じサーバーなら、そのまま新しいRedmineを入れればいい」と考えがちですが、不用意にシステム全体のRuby環境やWebサーバー設定を書き換えると、既存の稼働中サービスを巻き込んで停止(500エラー等)させるリスクがあります。

今回の構築では、「Redmine 6.1を動かすこと」以上に「現在動いている既存環境(5.1)に一切影響を与えないこと」を最優先に設計しました。そのための設計思想と具体的な構築手順をまとめます。

既存環境を壊さないための3つの原則

RedmineはRubyやGem、Webサーバー(Apache)など多くの基盤を利用するRailsアプリケーションです。既存環境との共存にあたり、特に以下の3点を徹底しました。

  1. sudo gem install を安易に使わず、ライブラリをプロジェクト内に隔離する
    システム全体にGemをインストールすると、既存Redmineが依存するGemバージョンと競合する恐れがあります。Bundlerを利用し、依存ライブラリはすべて検証環境のディレクトリ配下(vendor/bundle)へ閉じ込めます。
  2. Bundlerの設定をローカル(プロジェクト単位)に限定する
    グローバル設定(~/.bundle/config)への書き込みを避け、bundle config set --local を使用して対象ディレクトリ内でのみ設定を完結させます。
  3. Apacheの既存設定には触れず、新しいVirtualHostを追加するのみにとどめる
    既存のサイト設定(000-default.conf や既存Redmine用設定)の無効化(a2dissite)は行わず、新規VirtualHostの作成と有効化(a2ensite)だけで対応します。

検証環境・前提条件

  • OS: Ubuntu 24.04 LTS
  • Ruby: 3.2.x系(Ubuntu標準パッケージ / 5.1系と6.1系の両方に対応)
  • DB: MySQL 8.x
  • Web: Apache 2.4 + Passenger
  • Redmine: 6.1-stable(新規検証用)
  • パッケージ管理: aptitude(※aptでも同様に実行可能です)
  • 名前解決: 検証用ドメイン/サブドメインのDNS名前解決が完了していること

多分さっくりとはしていない手順

  1. Redmine-v6用のDBを作成します。
  2. Redmine 6.1を配置します。
  3. データベース接続設定を行います。
  4. Gemの初期化と設定を行います。 (※既存環境を壊さないように注意!!!)
  5. Apacheのvirtualhostを設定します。
  6. 設定を反映します。
  7. 動作確認を行います。

1. データベースと専用ユーザーの作成

既存のデータベースと完全に分離するため、redmine_v6 専用のDBおよびユーザーを作成します。

sudo mysql -u root -p
-- データベース作成
CREATE DATABASE redmine_v6 CHARACTER SET utf8mb4;

-- 専用ユーザー作成(パスワードは任意のものに変更してください)
CREATE USER 'redmine_v6'@'localhost' IDENTIFIED BY 'your_secure_password';

-- 権限付与
GRANT ALL ON redmine_v6.* TO 'redmine_v6'@'localhost';
FLUSH PRIVILEGES;
EXIT;

作成したユーザーで接続できるか確認します。

mysql -u redmine_v6 -p -e "SHOW DATABASES;"

2. Redmine 6.1 の配置

配置ディレクトリを既存環境と分けて作成し、ソースコードを取得します。

  • ディレクトリ作成
sudo mkdir -p /home/www-data/redmine-v6

※自分の環境に合わせます。

  • ディレクトリの所有者変更
sudo chown -R www-data:www-data /home/www-data/redmine-v6
  • Redmine 6.1-stable の取得
sudo -u www-data svn co https://svn.redmine.org/redmine/branches/6.1-stable /home/www-data/redmine-v6

3. データベース接続設定

設定ファイルのサンプルをコピーし、先ほど作成した redmine_v6 の情報を設定します。

sudo -u www-data cp -pi /home/www-data/redmine-v6/config/database.yml.example /home/www-data/redmine-v6/config/database.yml

/home/www-data/redmine-v6/config/database.yml を編集します。

production:
  adapter: mysql2
  database: redmine_v6
  host: localhost
  username: redmine_v6
  password: "your_secure_password"
  encoding: utf8mb4

4. Gemのローカルインストールと初期化

Gemの競合を防ぐため、必ずローカル設定で vendor/bundle 配下にインストールします。

  • Redmine(v6)のルートディレクトリに移動
cd /home/www-data/redmine-v6 && pwd

旧環境に移動すると偉い目に遭います。(遭いました

  • Bundlerの設定をローカルに限定
sudo -u www-data bundle config set --local path 'vendor/bundle'
sudo -u www-data bundle config set --local without 'development test'
  • 依存関係の調整とインストール
sudo -u www-data bundle update stringio
sudo -u www-data bundle install
  • シークレットトークンの生成
sudo -u www-data bundle exec rake generate_secret_token RAILS_ENV=production
  • DBマイグレーション
sudo -u www-data bundle exec rake db:migrate RAILS_ENV=production
  • デフォルトデータのロード(日本語)
sudo -u www-data RAILS_ENV=production REDMINE_LANG=ja bundle exec rake redmine:load_default_data
  • アセットのプリコンパイル
sudo -u www-data bundle exec rake assets:precompile RAILS_ENV=production

5. Apache VirtualHost の設定

既存の稼働中サイトには一切触れず、Redmine 6.1用の設定ファイルのみを新規作成・有効化します。

cat <<- __EOF__ | sudo tee /etc/apache2/sites-available/redmine-v6.conf
<VirtualHost *:80>
    ServerName 【v6.example.com】
    DocumentRoot /home/www-data/redmine-v6/public
    <Directory /home/www-data/redmine-v6/public>
        Options -MultiViews
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
__EOF__

【v6.example.com】 はご自身の検証用ドメインに書き換えてください。
※ これはあくまでもhttpd専用であり、ログもありません。適宜、セキュリティポリシーに沿って運用します。

6. 設定の反映

  • 新規サイト設定のみ有効化
sudo a2ensite redmine-v6.conf
  • 構文チェック(Syntax OK を確認)
sudo apache2ctl configtest
  • Apacheの再読み込み
sudo systemctl restart apache2.service

7. 動作確認

ブラウザから http://設定した検証用ドメイン にアクセスし、Redmine 6.1 のトップページが表示されれば構築完了です。

重要: 初期管理者アカウント(admin / admin)で即座にログインし、パスワードを強固なものへ変更してください。

おわりに

今回の検証環境構築では、Redmine 5.1(Rails 6.1系)とRedmine 6.1(Rails 7.2系)が共にRuby 3.2系をサポートしていたため、Ruby本体を共用しながらディレクトリ・Gem・DB・VirtualHostのレベルで環境を安全に分離できました。

サーバー運用においては、「新しい環境を動かすこと」以上に「稼働中の資産を壊さない境界線を引くこと」が重要になります。

Redmine 5.1環境で Knowledgebase を削除したら500エラーが消えない ― 真犯人は別のプラグインだった

はじめに

前回の記事では、Redmine 6.xへの移行準備として、redmine_dmsf を完全に切除しました。

その流れで、「それならKnowledgebaseも同じ手順で片付くだろう」と考えたのが、今回の始まりです。

長年ナレッジ管理として活躍してくれた redmine_knowledgebase ですが、現在はMarkdownネイティブな BookStack を主に利用しており、今後も利用予定はありません。

Redmine 6.xへのメジャーバージョンアップを見据え、依存関係を少しでも整理するため、完全アンインストールを行うことにしました。

前回の教訓もあります。

「マイグレーションを一つずつ戻す」のではなく、

  • データベース
  • マイグレーション履歴
  • プラグイン本体

をまとめて切除する外科手術方式です。

ところが今回は、それだけでは終わりませんでした。

Knowledgebaseを削除したはずなのに、ブラウザには何度アクセスしても

We're sorry, but something went wrong.

と表示され続けます。

今回の記事は、この500エラーの犯人を追い掛けた記録です。

今回の前提

今回の作業も、本番環境ではなく前回構築した Side-by-Side 構成の検証環境 (redmine-clone) で実施しています。

項目内容
対象環境検証環境(redmine-clone)
Redmine5.1系
Ruby3.2系
Rails6.1
DatabaseMySQL 8.x
対象プラグインredmine_knowledgebase

今回も、「失敗してもやり直せること」を前提に作業を進めています。

まずはバックアップ

前回と同様、テーブル削除を伴うため、最初にMySQLのダンプを取得します。

mysqldump \
  --single-transaction \
  --routines \
  --triggers \
  --default-character-set=utf8mb4 \
  -u redmine_clone \
  -p \
  redmine_clone \
  > redmine_clone_before_knowledgebase.sql

このバックアップが、最後の保険です。

Step1 Knowledgebaseを切除する

今回は最初から外科手術方式を採用しました。

まずは関連テーブルを削除します。

USE redmine_clone;

SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS
    kb_articles,
    kb_article_versions,
    kb_categories;

SET FOREIGN_KEY_CHECKS = 1;

続いて、マイグレーション履歴を削除します。

DELETE
FROM schema_migrations
WHERE version LIKE '%-redmine_knowledgebase%';

注意

この方法は「Knowledgebaseを今後利用しない」ことを前提としています。

将来的に再利用する予定がある場合は、通常の移行手順を検討してください。

最後にプラグイン本体を退避します。

cd /home/www-data/redmine-clone

sudo mv plugins/redmine_knowledgebase \
    /home/www-data/retired_plugins/

sudo touch tmp/restart.txt

ここまでは、前回のDMSFとほぼ同じ流れです。

そして私は、この時点で終わったと思っていました。

……終わらなかった

ブラウザを更新すると、

We're sorry, but something went wrong.

The issue has been logged for investigation.
Please try again later.

何度更新しても500エラー。

Knowledgebaseは削除したはずです。

それなのにRedmineは起動しません。

ここから、本当の原因調査が始まりました。

容疑者その1 ― ログが出ていない

まず最初にproduction.logを確認します。

……ところが、ログが増えません。おかしい。

シンボリックリンクを確認すると、

/home/www-data/redmine-clone/log
    ↓
/var/log/redmine

なんと、検証環境のログが本番環境を向いたままでした。

クローン作成時に張り替えたつもりだったシンボリックリンクが、そのまま残っていたのです。

修正します。

cd /home/www-data/redmine-clone

sudo rm log

sudo ln -sf /var/log/redmine-clone log

これでようやく検証環境のログが取得できるようになりました。

……しかし、500エラーは変わりません。容疑者その1、シロでした。

容疑者その2 ― Passenger

次に、Passengerを完全再起動します。

そこで、

passenger-config restart-app

を実行すると、

ArgumentError:
too long unix socket path
(116 bytes given but 108 bytes max)

というエラー。

UNIXドメインソケットの108バイト制限に引っ掛かっていました。

結局、

sudo touch tmp/restart.txt

sudo systemctl reload apache2

という昔ながらの方法で再起動することにしました。

……それでも500エラー。Passengerも違いました。

真犯人

ログが正常に取得できるようになったため、

rake コマンドで環境初期化を実行すると、ついに原因が現れました。

NoMethodError:

undefined method
`requires_redmineup'

plugins/redmine_questions/init.rb

Knowledgebaseではありませんでした。

真犯人は redmine_questions です。

Redmineは起動時に、すべてのプラグインの init.rb を読み込みます。

Knowledgebaseを削除しても、同居していた redmine_questionsredmineup のメソッドを要求し、その時点で初期化が停止していました。

つまり、Knowledgebaseを削除したことで壊れたのではなく、

以前から潜んでいた依存関係の問題が表面化しただけだったのです。

決断 ― 疑わしき依存はすべて切り離す

redmine_questions の利用状況を改めて確認した結果、今後利用する予定はありませんでした。依存関係を抱えたままRedmine 6.xへ進むリスクを考え、こちらもKnowledgebaseと同様に完全切除することにしました。

Questionsを切除する

まずは関連テーブルを削除します。

mysql -u redmine_clone -p
USE redmine_clone;

SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS
    questions,
    questions_answers,
    questions_sections,
    questions_statuses;

SET FOREIGN_KEY_CHECKS = 1;

続いてマイグレーション履歴を削除します。

DELETE
FROM schema_migrations
WHERE version LIKE '%-redmine_questions%';

最後にプラグイン本体を退避し、キャッシュを削除します。

sudo mv \
plugins/redmine_questions \
/home/www-data/retired_plugins/

sudo rm -rf tmp/cache/*

sudo touch tmp/restart.txt

sudo systemctl reload apache2

動作確認

ブラウザを更新すると……

500エラーは完全に解消しました。確認できた内容は次のとおりです。

  • Redmineが正常起動する
  • トップページが表示される
  • 管理画面へログインできる
  • Plugin一覧から
    • DMSF
    • Knowledgebase
    • Questions
      が消えている
  • エラーログに新たな例外が出ていない

ここまで確認できれば、今回の切除作業は完了です。

おわりに

今回の500エラーは、Knowledgebaseそのものが原因ではありませんでした。

長年運用してきたプラグイン同士の依存関係が、Knowledgebaseの切除をきっかけに姿を現しただけだったのです。

一つ外せば終わると思っていたものが、実際には別のプラグインを炙り出す。長期間運用してきたRedmineらしい現象だったと感じています。

結果として、

  • redmine_dmsf
  • redmine_knowledgebase
  • redmine_questions

という、今後利用予定のない大型プラグインを整理することができました。

Redmine本体はまだ5.1のままですが、不要な依存を抱えない、できるだけ素の状態へ近づけることができています。

そして今回も、問題の切り分けが落ち着いてできたのは、前回構築した redmine-clone の存在があったからです。

本番環境だったら、500エラーが出るたびに焦っていたかもしれません。しかし検証環境だからこそ、一つずつ仮説を立て、ログを確認し、原因を潰していくことができました。

いよいよ次は、Redmine 6.xへのメジャーバージョンアップの下準備です。

ここまで整理してきた環境が、どこまで素直に新しいバージョンを受け入れてくれるのか。それを確認する段階まで、ようやくたどり着きました。

Redmine 5.1環境で redmine_dmsf を完全に切除する ― Redmine 6.x移行へ向けたアンインストール記録

はじめに

前回、Redmine 6.xへの移行に備え、本番とは完全に切り離した Side-by-Side 構成の検証環境(redmine-clone)を構築しました。

今回取り組んだのは、長年利用してきたプラグイン、 redmine_dmsf を完全に取り除く作業です。

DMSFは非常に高機能なプラグインですが、現在の筆者の運用では役目を終えており、Redmine 6.xへの移行を見据えてアンインストールすることにしました。

ところが、標準的なアンインストール手順では途中で何度もエラーが発生し、作業はまったく進みません。

ロールバック
    ↓
エラー修正
    ↓
もう一度ロールバック
    ↓
別のエラー

まさにモグラ叩きです。

最終的には発想を切り替え、「過去のマイグレーションを修正しながら戻す」のではなく、「今後二度と利用しない」という前提で、プラグインを外科手術のように切除する方法を選択しました。

この記事は、その作業記録です。

今回の前提

今回の作業は、前回構築した 検証環境 (redmine-clone) を対象に実施しています。

項目内容
対象環境redmine-clone
Redmine5.1系
Ruby3.2系
Rails6.1
DatabaseMySQL 8.x
対象プラグインredmine_dmsf

本番環境では絶対に実施せず、必ず検証環境で確認してから行ってください。

今回の方針

通常であれば

sudo -u www-data bundle exec rake redmine:plugins:migrate VERSION=0

でロールバックするのが正攻法です。

しかし今回は、

  • Rails側の仕様変更
  • 古いマイグレーションコード
  • 運用期間中に蓄積したデータ

これらが複雑に絡み合い、途中でロールバックが停止しました。

そこで今回は

「DMSFを今後一切利用しない」

ことを前提に、

  • DMSFテーブルを削除
  • マイグレーション履歴を削除
  • プラグイン本体を撤去

という外科手術方式を採用しています。

作業前にMySQLバックアップを取得する

これから実施する操作は、テーブル削除やマイグレーション履歴の削除を伴います。

やり直しができるよう、必ずダンプを取得してから作業します。

※DB名、ユーザー名は自分の環境に合わせてください。

mysqldump \
  --single-transaction \
  --routines \
  --triggers \
  --default-character-set=utf8mb4 \
  -u redmine_clone \
  -p \
  redmine_clone \
  > redmine_clone_before_dmsf.sql

今回の作業では、このダンプが最後の保険になります。

最初に試した方法--NG

まずは標準手順どおりロールバックを試みました。

sudo -u www-data \
bundle exec rake \
redmine:plugins:migrate \
NAME=redmine_dmsf \
VERSION=0 \
RAILS_ENV=production

ところが、ここから長い戦いが始まります。

発生したエラー

1. Rails 6.1との仕様不整合

最初に発生したのは

ArgumentError: wrong number of arguments

というエラーでした。

原因は、古いマイグレーションで利用されている

t.index_exists?

の引数仕様が、現在のActiveRecordと一致していなかったためです。

2. 存在しないインデックス

修正すると、今度は

No indexes found...

で停止しました。

ロールバックコードは「存在するはず」と考えているインデックスが、現在のデータベースには存在しません。

過去十数年の運用の中でスキーマが変化していたことが原因でした。

3. nil参照によるクラッシュ

さらに進めると

undefined method 'id' for nil

で停止。

古いマイグレーションでは、存在する前提のデータが既になくなっており、ロールバック中にクラッシュしました。

今回の教訓

ここまでで感じたことは一つです。

標準手順だからといって、必ず最後まで実行できるとは限らない。

10年以上前に書かれたマイグレーションコードを、現在のRailsで逆方向へ実行する。

これは思っている以上に難しく、途中で止まる可能性があります。

一つ修正しても、次のマイグレーションで別のエラーが出る。今回の作業は、まさにモグラ叩きでした。

方針転換 ― 外科手術で切除する

今回はDMSFを再利用する予定はありません。

そこで、「きれいにロールバックする」ではなく、「安全に切除する」 という考え方へ切り替えました。

Step1 DMSFテーブルを削除する

まず外部キー制約を一時的に無効化します。

mysql -u redmine-clone -p

※本番環境のアカウントではログインしないこと。非常に悪い意味で眠気が吹っ飛びます。

USE redmine_clone;
SET FOREIGN_KEY_CHECKS=0;

続いて関連テーブルを削除します。

DROP TABLE IF EXISTS
    dmsf_file_revisions,
    dmsf_files,
    dmsf_folder_permissions,
    dmsf_folders,
    dmsf_links,
    dmsf_locks,
    dmsf_public_urls,
    dmsf_workflow_step_actions,
    dmsf_workflow_step_assignments,
    dmsf_workflow_steps,
    dmsf_workflows;

最後に外部キー制約を戻します。

SET FOREIGN_KEY_CHECKS=1;

Step2 マイグレーション履歴を削除する

続いて

schema_migrations

からDMSFの履歴を削除します。

DELETE
FROM schema_migrations
WHERE version LIKE '%-redmine_dmsf';

注意

この操作は「今後DMSFを利用しない」ことを前提としています。

データを保持したまま将来的に再利用する予定がある場合は、この方法は適しません。

Step3 状態確認

再度ロールバックを実行します。

sudo -u www-data \
bundle exec rake \
redmine:plugins:migrate \
NAME=redmine_dmsf \
VERSION=0 \
RAILS_ENV=production

正常終了していれば

echo $?
0

となります。

Step4 プラグイン本体を退避する

最後にプラグイン本体を退避します。

sudo mkdir -p \
/home/www-data/retired_plugins
sudo mv \
plugins/redmine_dmsf \
/home/www-data/retired_plugins/

Passengerを利用している場合は再起動します。

sudo touch tmp/restart.txt

apaceの再起動でもOKです。

sudo systemctrl reload apache2.service

動作確認

ここまで終わったら、

  • Redmineが起動する
  • Administration → Plugins にDMSFが表示されない
  • エラーログにDMSF関連エラーが出ていない
  • プロジェクトが正常に閲覧できる

ことを確認します。

問題がなければ、DMSFの切除は完了です。

おわりに

今回一番苦労したのは、アンインストールの方法そのものではありません。

**「標準手順が最後まで通らない」**ことでした。

最初はマイグレーションを一つずつ修正してロールバックしようと考えていましたが、進むたびに新しいエラーが現れ、終わりが見えませんでした。

そこで発想を変え、「もう利用しないものは、安全を確認したうえで切除する」という方針に切り替えたことで、ようやく前へ進むことができました。

もちろん、この方法は誰にでも勧められるものではありません。テーブルやマイグレーション履歴を直接操作するため、十分なバックアップと、失敗してもやり直せる検証環境があることが大前提です。

前回構築した redmine-clone は、まさにそのための環境でした。

標準手順が通らない場面ほど、「何度でもやり直せる環境」があることの価値を強く実感しています。

そして、この作業によってようやく、Redmine 6.xへの移行に向けた大きな障害を一つ取り除くことができました。

Redmine 5.1をSide-by-Side構成で複製する ― Redmine 6.x移行に向けた検証環境構築

はじめに

「既にサポート終了になっているRedmine 5.1。これをバージョンアップしたい」がそもそものきっかけ。

しかし、Redmineのメジャーバージョンアップは、RubyやRailsの更新だけではありません。利用しているプラグインやテーマとの互換性、添付ファイル、データベース、さらにはWebサーバーやWAFとの兼ね合いまで確認しなければならず、思いのほか確認項目は多くなります。

特に本番環境を運用している場合、「とりあえずアップデートしてみる」というわけにはいきません。

迂闊にアップデートするとRedmineそのものが動くならない事象が発生します。

そこで今回は、稼働中のRedmine 5.1環境をそのまま複製し、本番とは完全に独立した検証環境を同一サーバー上へ構築しました。

目的は単なるバックアップではなく、Redmine 6.xへの移行を何度でも試行錯誤できる環境を作ることです。

構築環境

今回の構成は次のとおりです。

項目内容
OSUbuntu 24.04 LTS
WebサーバーApache 2.4
WAFModSecurity + OWASP CRS
Ruby3.2系(aptビルトイン)
Redmine5.1系
データベースMySQL 8.x
SSLLet's Encrypt
添付ファイルS3互換ストレージ(Wasabi)

同一サーバー上へ検証環境を追加するため、Apache・Ruby・MySQLは共有しながら、Redmine本体・データベース・ログ・添付ファイルのみを完全に分離します。

本番環境と検証環境の構成

今回の構成では、以下のように役割を分離しています。

項目本番環境検証環境
URLredmine.example.comredmine-test.example.com
アプリケーション/home/www-data/redmine/home/www-data/redmine-clone
DocumentRoot/home/www-data/redmine/public/home/www-data/redmine-clone/public
データベースredmineredmine_clone
DBユーザーredmineredmine_clone
ログ/var/log/redmine/var/log/redmine-clone
添付ファイル/mnt/storage/redmine/files/mnt/storage/redmine-clone/files

このようにしておけば、本番環境へ一切影響を与えることなく、何度でもアップグレードの検証を繰り返せます。

さっくりしていそうでさっくりしない手順

あくまでも筆者の手順です。

  1. MySQLダンプを取得
  2. Redmine環境の(アプリの)クローン

1. まずはMySQLのダンプを取得する

アプリケーションをコピーする前に、現在のデータベースをバックアップしておきます。

※DB名、ユーザー名は自分の環境に合わせてください。

mysqldump \
  --single-transaction \
  --routines \
  --triggers \
  --default-character-set=utf8mb4 \
  -u redmine \
  -p \
  redmine \
  > redmine.sql

--single-transactionを利用しているため、InnoDB環境であればサービス停止なしで整合性の取れたバックアップを取得できます。

2. Redmine本体を複製する

アプリケーション本体は cp ではなく rsync を利用しました。

ACLや拡張属性も含めてそのまま複製できるため、権限周りで悩むことが少なくなります。

※ディレクトリ名も自分の環境に合わせます。

cd /home/www-data
sudo rsync -aX redmine/ redmine-clone/

ここではまだ添付ファイルやログは本番環境を参照しています。(エイリアスを切っているため)

次の手順で検証環境用へ切り替えます。

3. 添付ファイルとログを分離する

添付ファイルはS3互換ストレージをマウントしているため、検証用ディレクトリへ切り替えます。

※サーバ上にそのまま保存している場合でも容量に注意しましょう

cd /home/www-data/redmine-clone
sudo ln -sf /mnt/storage/redmine-clone/files files

続いてログディレクトリを作成します。

sudo mkdir -p /var/log/redmine-clone
sudo chown -R www-data:www-data /var/log/redmine-clone
sudo ln -sf /var/log/redmine-clone log

これで本番・検証それぞれのログを独立して取得できます。

4. 検証用データベースを作成する

MySQLへログインし、新しいデータベースとユーザーを作成します。

※DBの向き先は要注意です。パスワードもポリシーに則り、適切なものを指定します。

CREATE DATABASE redmine_clone CHARACTER SET utf8mb4;
CREATE USER 'redmine_clone'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON redmine_clone.* TO 'redmine_clone'@'localhost';
FLUSH PRIVILEGES;

5. ダンプをリストアする

取得したバックアップを検証用データベースへ投入します。

※※要注意※※

リストア先とリストア元は三回ぐらい深呼吸して、事前に確認しましょう。

「クローン先のDB名」が先で、「クローン元のダンプファイル」が後です。

mysql \
  -u redmine_clone \
  -p \
  redmine_clone \
  < redmine.sql

これでデータベースも本番環境と同じ状態になります。


6. database.ymlを書き換える

Redmineが参照するデータベースを検証環境へ向けます。

production:

- database: redmine
+ database: redmine_clone

- username: redmine
+ username: redmine_clone

password: ********

忘れがちですが、この変更をしないと検証環境が本番データベースへ接続してしまいます。死ぬほど笑えない事象は、往々にしてこういうところから始まります。(始まりました)

7. ModSecurityの除外設定を追加する (オプション)

今回はApacheでModSecurityとOWASP CRSを利用しています。

RedmineはPATCHやPUTなどのHTTPメソッドを使用するため、通常のCRSでは誤検知されるケースがあります。

本番環境ですでに除外ルールを適用している場合は、検証用ホスト名も同様に対象へ追加します。

- redmine.example.com
+ redmine.example.com
+ redmine-test.example.com

これを忘れると、画面上では保存ボタンを押しただけなのに403 Forbiddenになることがあります。

8. Apache VirtualHostを追加する

本番用VirtualHost/etc/apache2/sites-available/redmine.confをコピーし、/etc/apache2/sites-available/redmine-clone.conf

  • ServerName
  • DocumentRoot
  • ErrorLog
  • CustomLog

を検証環境用へ変更します。

例えば、

/home/www-data/redmine/public

/home/www-data/redmine-clone/public

へ変更し、

/var/log/redmine

/var/log/redmine-clone

へ変更します。

設定後は有効化してApacheを再読み込みします。

sudo a2ensite redmine-test.conf
sudo apache2ctl configtest
sudo systemctl reload apache2

動作確認

ここまで終わったら、検証環境へアクセスして以下を確認します。

  • ログインできる
  • チケット・Wiki・プロジェクトが一致している
  • 添付ファイルが表示される
  • ログが検証用ディレクトリへ出力される
  • ModSecurityで403にならない

ここまで確認できれば、Side-by-Side構成のクローン環境は完成です。

おわりに

サーバー運用では「バックアップを取ってから更新する」は半ば常識になっています。しかし、メジャーバージョンアップのような大きな変更では、それだけでは十分とは言えません。

実際に必要なのは、「失敗してもやり直せる環境」を用意しておくことです。

今回のように本番と完全に切り離した検証環境を用意しておけば、Redmine 6.xへのアップグレードだけでなく、Rubyやgemの更新、不要プラグインの整理、設定変更なども心置きなく試せます。

運用者であれば「更新前にバックアップを取る」は自然とできるようになります。しかし、その先の「何度でも検証できる環境を用意する」まで仕組みにしておくと、運用はさらに一段安定します。

私自身、今回の移行作業ではこの検証環境のおかげで、結果として、一度きりの「勝負」ではなく、納得いくまで検証を重ねた上で本番移行へ臨めます。

Page 1 of 30

Powered by WordPress & Theme by Anders Norén