カテゴリー: Linux Page 2 of 66

【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移行】テーマが出ない・画面が崩壊する? Propshaft移行に伴うテーマディレクトリ変更とCSSパス修正の記録

Redmine 5.1からRedmine 6.1(Rails 7.2)への移行検証を進めていると、プラグイン以外にもいくつか「以前と同じやり方では動かない」箇所が出てきます。

その中でも、意外と厄介だったのがカスタムテーマの移行でした。

今回、長年使ってきた kodomo テーマをRedmine 6.1環境へ移そうとしたところ、

  • 管理画面のテーマ一覧にカスタムテーマが出てこない
  • 何とか設定を通してみると、今度はCSSが適用されず画面が崩壊する

という、二段構えの問題に遭遇しました。

最初は「テーマをコピーすれば終わりだろう」と考えていたのですが、Redmine 6ではその前提自体が変わっています。

調べていくと、Redmine 6.0で行われたアセットパイプラインの変更、つまりSprocketsからPropshaftへの移行が、この問題の根っこにありました。

今回は、このテーマ移行で実際に引っかかった箇所と、Redmine 6.1側で動く状態まで戻した手順を記録しておきます。


1. まず何が起きたのか

今回の移行では、kodomo テーマをRedmine 5.1環境からRedmine 6.1環境へ持ってきました。

ところが、Redmine 6.1を起動して管理画面の

設定 > 表示 > テーマ

を確認してみても、表示されているのは標準テーマの Alternate と Classic だけ。

カスタムテーマの Kodomo がありません。

テーマそのものは、

public/themes/kodomo

に存在しています。

ディレクトリやファイルの所有者・権限にも問題はありません。Passengerを再起動しても出てこない。

そこでRedmine内部のテーマスキャンを確認すると、どうもそもそもテーマとして認識されていないようでした。

さらに、DB側の設定を直接変更してテーマを適用させてみると、今度は別の問題が発生しました。

テーマ名は設定できた。しかしCSSが適用されない。

ブラウザに表示されるのは、文字とリンクとリストが並んだだけの、いわゆる「素のHTML」。見事に画面が崩壊しました。

つまり今回の問題は、

  1. テーマそのものをRedmineが見つけられない
  2. 見つけさせてもCSSの読み込み方が古い

という二つの問題が重なっていたわけです。

2. 原因はRedmine 6.0からのアセットパイプライン変更

ここで重要になるのが、Redmine 6.0から導入されたPropshaftです。

Redmine 5.xまでの環境では、Railsのアセット管理に主として Sprockets が使われていました。

SprocketsはCSSやJavaScriptなどのアセットを処理・結合・コンパイルする仕組みで、Redmineでも長らく使われてきたものです。

一方、Redmine 6.0ではこの部分がPropshaftへ移行しました。

PropshaftもRailsのアセットパイプラインを担う仕組みですが、Sprocketsとはアセットの探索や扱い方が異なります。

Redmine 5.x時代に作られたテーマは、

「以前のRedmineでは、この場所にテーマがあり、この相対パスなら標準CSSを読み込める」

という前提で作られています。ところがRedmine 6.xでは、その前提が崩れています。今回のテーマ移行で問題になったのも、まさにそこでした。

3. テーマの置き場所が変わっている

Redmine 5.xまで使っていたテーマは、基本的に

public/themes/

以下へ配置していました。ところがRedmine 6.xでは、テーマの標準配置場所が変わっています。

Redmine本体のルートディレクトリから見て、

themes/

です。今回のRedmine 6.1環境は、

/home/www-data/redmine_v6

なので、テーマは最終的に

/home/www-data/redmine_v6/themes/kodomo

となる形です。つまり、Redmine 5.1からそのまま

public/themes/kodomo

をコピーしただけでは、Redmine 6.1側のテーマローダーから見つけてもらえません。これが、最初の

「テーマを置いたのに管理画面に出てこない」

という問題の原因でした。

4. テーマを themes/ へ移す

ということで、まずはRedmine 6.1側にテーマを配置します。

sudo -u www-data cp -pir public/themes/kodomo themes/
sudo chown -R www-data:www-data themes
sudo chmod -R 755 themes

これで、

/home/www-data/redmine_v6/themes/kodomo

という構造になります。テーマの中には、少なくとも今回のケースでは、

themes/
└── kodomo/
    └── stylesheets/
        └── application.css

という構造が必要です。ここまでやってから、もう一度Redmine側からテーマを認識できるか確認します。

5. しかし、これだけではまだ画面は直らない

ここが今回のもう一つの罠でした。テーマを正しい場所へ移したことで、Redmineからテーマとして認識されるようになります。しかし、実際にテーマを適用してみると、

CSSが読み込まれません。

結果として、画面はこうなります。

文字
リンク
箇条書き
フォーム
表

……以上。

ブラウザとしては正しくHTMLを表示しているのですが、Redmineとして見ると完全に「何かがおかしい」。これはテーマ側の application.css が、Redmine本体のCSSを正しく参照できていないためでした。

6. application.css の相対パスも変更する

Redmineのカスタムテーマでは、標準のCSSを読み込んだ上で、自分のテーマ固有のCSSを追加する構造になっているものがあります。今回の kodomo もそのタイプでした。

Redmine 5.x時代のテーマでは、

@import url(../../../stylesheets/application.css);

という指定になっていました。ところが、Redmine 6.xではテーマの配置場所そのものが変わっています。そのため、この相対パスでは標準CSSへ到達できません。

Redmine 6.x側では、

@import url(../../application.css);

へ変更します。今回の場合は、

themes/kodomo/stylesheets/application.css

から見て、Redmine側のCSSを参照するためのパスが変わった、と考えると分かりやすいです。

7. application.css を修正する

まずは念のためバックアップを取得します。

cd /home/www-data/redmine_v6/themes/kodomo/stylesheets
sudo -u www-data cp -p application.css application.css.bak

そして、旧パスを新しいパスへ置き換えます。

sudo -u www-data sed -i \
's|@import url(../../../stylesheets/application.css);|@import url(../../application.css);|g' \
application.css

念のため、修正後の内容も確認しておきます。

grep '@import' application.css

ここで、

@import url(../../application.css);

となっていればOKです。

8. Railsからテーマを認識できるか確認する

ここまで来たら、Webブラウザで確認する前にRails側からテーマを認識できるか確認しておきます。

cd /home/www-data/redmine_v6
sudo -u www-data RAILS_ENV=production bundle exec rails runner "
  Redmine::Themes.rescan
  Redmine::Themes.themes.each do |t|
    puts 'Loaded Theme -> ID: ' + t.id + ' | Name: ' + t.name
  end
"

正常に読み込まれていれば、例えば次のように表示されます。

Loaded Theme -> ID: alternate | Name: Alternate
Loaded Theme -> ID: classic | Name: Classic
Loaded Theme -> ID: kodomo | Name: Kodomo

ここで kodomo が出てくれば、少なくともRedmineのテーマローダーからは認識されています。

9. 最後にPassengerを再起動する

テーマを修正したら、Webアプリケーション側も再起動しておきます。Passenger環境では、

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

としておきます。その後、Redmineへアクセスして、

管理 > 設定 > 表示 > テーマ

を確認します。ここに、

Alternate
Classic
Kodomo

と表示されれば、テーマ自体は認識されています。あとは Kodomo を選択して画面を確認します。

CSSが正常に読み込まれていれば、今度はRedmine 5.1時代の見慣れた画面が戻ってきます。

10. 今回の問題を整理すると

今回のトラブルは、最初は「Redmine 6.1で古いテーマが使えなくなった」と見えました。

しかし、実際には二つの問題がありました。

問題1:テーマを置く場所が変わった

Redmine 5.xでは、

public/themes/kodomo

でした。Redmine 6.xでは、

themes/kodomo

です。そのため、旧環境からテーマをそのままコピーしただけでは、Redmineから認識されません。

問題2:テーマ内部のCSS参照も古い

さらに、テーマがRedmineから認識されたとしても、古い application.css のままでは標準CSSを読み込めません。

Redmine 5.xでは、

@import url(../../../stylesheets/application.css);

だったものを、Redmine 6.xでは、

@import url(../../application.css);

へ変更する必要がありました。つまり、

「テーマを移動する」だけではなく、「テーマからRedmine本体を参照しているパスも見直す」必要がある

ということです。

11. Redmine 6.xへテーマを移行するときのチェックリスト

今回の作業をチェックリストにすると、こんなところでしょうか。

  • public/themes/ に置きっぱなしになっていないか
  • Redmineルート直下の themes/ にテーマを配置したか
  • themes/<テーマ名>/stylesheets/application.css が存在するか
  • application.css の古い相対パスが残っていないか
  • @import url(../../application.css); になっているか
  • テーマディレクトリの所有者・権限を確認したか
  • Redmine::Themes.rescan でテーマが認識されているか
  • Passengerを再起動したか
  • ブラウザのキャッシュだけでなく、CSSの読み込み自体が成功しているか

まとめ

Redmine 5.1から6.1への移行というと、どうしてもデータベースやプラグインの互換性に目が行きます。

実際、そちらの方が重要ではあるのですが、今回のようにテーマもRedmine本体の変更に巻き込まれることがあります。

特にRedmine 6.0からは、RailsのアセットパイプラインがSprocketsからPropshaftへ移行しています。

その結果として、テーマの配置場所やCSSの参照方法について、Redmine 5.x時代のテーマをそのまま持ってきても動かないケースが出てきます。

今回の kodomo については、

public/themes/kodomo
        ↓
themes/kodomo

への移動と、

@import url(../../../stylesheets/application.css);

から

@import url(../../application.css);

への変更で復旧できました。

Redmine 6.xへの移行では、プラグインだけを確認して「よし、動いた」とするのではなく、テーマもまたバージョンアップの影響を受ける部品として確認しておくのが安全そうです。

今回のように、

「テーマが一覧に出ない」
↓
「何とか指定できた」
↓
「今度はCSSが全部消えた」

という二段コンボを食らうと、原因が同じ場所にあるとはなかなか気づきにくいところでした。

移行作業では、こういう「前のバージョンでは当たり前だった場所」を一つずつ疑っていく必要がありそうです。

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_questions が redmineup のメソッドを要求し、その時点で初期化が停止していました。

つまり、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の更新、不要プラグインの整理、設定変更なども心置きなく試せます。

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

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

NextcloudアップデートとDBバックアップを行うBashスクリプトの解説

Nextcloudのアップデートは、それほど頻繁に行う作業ではありません。しかし、だからこそ毎回「あれ、このコマンド実行したっけ?」となりがちです。

筆者自身も、メンテナンスモードの切り替えやデータベースのバックアップなど、手順を一つひとつ確認しながら作業していました。そこで「毎回やることはスクリプトに任せよう」と考え、更新前のバックアップから updater.phar の実行までを一通り補助する Bash スクリプトを作成しました。

今回は、そのスクリプトの仕組みを紹介します。

動作環境

動作を想定している環境は以下の通りです。

  • Ubuntu 20.04 LTS / 22.04 LTS / 24.04 LTS
  • MySQL または MariaDB
  • PHP / Bash
  • root 権限(sudo)
  • Apache実行ユーザ: www-data

Ubuntu系で一般的な構成であれば、そのまま利用できるようになっています。

スクリプト本体

最初に環境に合わせて、スクリプト冒頭の変数だけ変更します。

NC_DIR="/var/www/html/nextcloud"
NC_USER="www-data"

DB_NAME="nextcloud"
DB_USER="nextcloud"

BACKUP_DIR="/var/backups/nextcloud"

続いて実行権限を付与します。

chmod +x nextcloud_update.sh

最後に sudo で起動します。

sudo ./nextcloud_update.sh

途中では

  • データベースパスワード
  • 各ステップへ進むかどうか

を対話形式で確認します。

確認しながら進められるので、完全自動化というより「操作ミスを減らすための半自動化」という位置付けです。

Bashで自動化というと「全部ワンコマンドで終わらせる」方向へ行きがちですが、個人的にはサーバーの更新作業はそこまで割り切らない方が安心だと考えています。

特に Nextcloud のアップデートは、途中で状況を確認したくなる場面も少なくありません。

そのため、このスクリプトでは各工程ごとに確認を挟み、「人が判断するところ」と「機械に任せるところ」を分ける構成にしました。

毎回同じ手順を思い出しながら実行するより、確認すべきポイントだけに集中できるので、更新作業はかなり気楽になります。

以下がスクリプト全文です。

#!/bin/bash
# ==============================================================================
# Nextcloud Update & Database Backup Script
# ==============================================================================

set -euo pipefail

# --- 変数定義(環境に合わせて変更してください) ---
NC_DIR="/var/www/html/nextcloud"
NC_USER="www-data"
PHP_BIN="php"

# DB設定
DB_NAME="nextcloud"
DB_USER="nextcloud"
DB_HOST="localhost"
BACKUP_DIR="/var/backups/nextcloud"

# --- ユーティリティ関数 ---
log_info() {
    echo -e "\n\e[34m[INFO]\e[0m $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log_warn() {
    echo -e "\e[33m[WARN]\e[0m $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log_error() {
    echo -e "\e[31m[ERROR]\e[0m $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2
}

ask_proceed() {
    local prompt_msg="${1:-次のステップに進みますか?}"
    while true; do
        read -rp "👉 ${prompt_msg} [y/N]: " answer
        case "${answer}" in
            [yY]|[yY][eE][sS])
                return 0
                ;;
            [nN]|[nN][oS]|"")
                log_warn "ユーザーにより処理が中断されました。"
                exit 1
                ;;
            *)
                echo "'y' または 'n' を入力してください。"
                ;;
        esac
    done
}

# --- 前提チェック ---
# root 権限チェック(sudo 経由を含む root 以外は即時終了)
if [ "${EUID}" -ne 0 ]; then
    log_error "このスクリプトは root 権限で実行する必要があります。(例: sudo $0)"
    exit 1
fi

log_info "前提条件と実行権限を確認しています..."

if [ ! -d "${NC_DIR}" ]; then
    log_error "Nextcloudディレクトリが見つかりません: ${NC_DIR}"
    exit 1
fi

if ! command -v mysqldump &> /dev/null && ! command -v mariadb-dump &> /dev/null; then
    log_error "mysqldump または mariadb-dump コマンドが見つかりません。"
    exit 1
fi

mkdir -p "${BACKUP_DIR}"

# ==============================================================================
# Step 1: データベースパスワードの取得と認証確認
# ==============================================================================
log_info "【Step 1】データベース情報の入力"
read -rsp "🔑 DBユーザー (${DB_USER}) のパスワードを入力してください: " DB_PASS
echo

# パスワードの疎通確認
if ! MYSQL_PWD="${DB_PASS}" mysqladmin -h "${DB_HOST}" -u "${DB_USER}" ping &>/dev/null; then
    log_error "データベースへの接続に失敗しました。パスワードまたはホスト設定を確認してください。"
    exit 1
fi
log_info "データベースへの接続を確認しました。"

# ==============================================================================
# Step 2: メンテナンスモードの有効化
# ==============================================================================
log_info "【Step 2】メンテナンスモードの有効化"
ask_proceed "メンテナンスモードをONにしますか?"

sudo -u "${NC_USER}" "${PHP_BIN}" "${NC_DIR}/occ" maintenance:mode --on
log_info "メンテナンスモードを有効化しました。"

# スクリプト異常終了時にメンテナンスモード解除を促すトラップを設定
cleanup() {
    if [ $? -ne 0 ]; then
        log_warn "エラーが発生したため処理を停止しました。"
        log_warn "必要に応じてメンテナンスモードを解除してください:"
        log_warn "sudo -u ${NC_USER} ${PHP_BIN} ${NC_DIR}/occ maintenance:mode --off"
    fi
}
trap cleanup EXIT

# ==============================================================================
# Step 3: DBバックアップの実行(インラインパスワード処理)
# ==============================================================================
log_info "【Step 3】DBバックアップの実行"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_backup_${TIMESTAMP}.sql.gz"

ask_proceed "DBバックアップを開始しますか? (保存先: ${BACKUP_FILE})"

# dumpコマンドの判定(MariaDB/MySQL互換)
DUMP_CMD="mysqldump"
command -v mariadb-dump &> /dev/null && DUMP_CMD="mariadb-dump"

# MYSQL_PWD環境変数を使用することで、psコマンド等へのパスワード露出を防止
if MYSQL_PWD="${DB_PASS}" ${DUMP_CMD} -h "${DB_HOST}" -u "${DB_USER}" --single-transaction --quick "${DB_NAME}" | gzip > "${BACKUP_FILE}"; then
    log_info "バックアップが正常に完了しました: ${BACKUP_FILE}"
else
    log_error "バックアップ処理中にエラーが発生しました。"
    exit 1
fi

# ==============================================================================
# Step 4: updater.phar によるアップデート実行
# ==============================================================================
log_info "【Step 4】updater.phar の実行"
log_warn "updater自体の対話プロンプトが表示されます。画面の指示に従って操作してください。"
ask_proceed "updater.phar を起動しますか?"

cd "${NC_DIR}"
sudo -u "${NC_USER}" "${PHP_BIN}" "${NC_DIR}/updater/updater.phar"

# ==============================================================================
# Step 5: 状態の確認とメンテナンスモードの調整
# ==============================================================================
log_info "【Step 5】最終確認とメンテナンスモードの解除"

current_mode=$(sudo -u "${NC_USER}" "${PHP_BIN}" "${NC_DIR}/occ" maintenance:mode)
echo "現在の状態: ${current_mode}"

if echo "${current_mode}" | grep -q "enabled"; then
    read -rp "👉 メンテナンスモードをOFF(解除)にしますか? [y/N]: " unfreeze
    if [[ "${unfreeze}" =~ ^[yY]$ ]]; then
        sudo -u "${NC_USER}" "${PHP_BIN}" "${NC_DIR}/occ" maintenance:mode --off
        log_info "メンテナンスモードを解除しました。"
    else
        log_warn "メンテナンスモードを維持したまま終了します。"
    fi
fi

# ==============================================================================
# Step 6: バージョン確認
# ==============================================================================
log_info "【Step 6】バージョンの確認"
sudo -u "${NC_USER}" "${PHP_BIN}" "${NC_DIR}/occ" status

log_info "すべての更新手順が完了しました。"
trap - EXIT

このスクリプトで重視したこと

単にコマンドを並べるだけではなく、「途中で失敗しても致命傷にならないこと」を意識しています。

set -euo pipefail

Bashスクリプトでは定番ですが、非常に重要です。

  • コマンドが失敗した
  • 未定義変数を参照した
  • パイプラインの途中でエラーが発生した

このような場合は、その時点で処理を停止します。

「バックアップに失敗したのに、そのままアップデートが続く」といった事故を防ぐためです。

root権限の確認

EUID を利用して、root 権限以外では実行できないようにしています。

sudo ./nextcloud_update.sh

以外の実行方法では即座に終了します。

パスワードをコマンドラインへ残さない

昔ながらの

mysqldump -pPASSWORD

という書き方は、ps コマンドなどからパスワードが見えてしまいます。

そこで本スクリプトでは

MYSQL_PWD

環境変数を利用しています。

賛否のある方法ではありますが、少なくともコマンドライン引数へ平文パスワードを残すより安全です。(なお筆者は別スクリプトを利用していますが、それはまた別のお話)

trapによる復旧案内

一番怖いのは

  • メンテナンスモードON
  • エラー発生
  • そのまま終了

というパターンです。

そこで trap cleanup EXIT を利用し、異常終了した場合は

occ maintenance:mode --off

を案内するようにしています。

「解除方法を忘れた」という事態を防ぐための保険です。

実行の流れ

処理は次の順番で進みます。

1. 前提条件の確認

まずは

  • root権限
  • Nextcloudディレクトリ
  • mysqldump または mariadb-dump

の存在を確認します。

ここで問題があれば、それ以上処理は進みません。

2. データベース接続の確認

パスワードを入力したあと、

mysqladmin ping

で接続テストを行います。

アップデート直前になって認証エラーが発覚するより、最初に確認してしまった方が安心です。

3. メンテナンスモードを有効化

occ maintenance:mode --on

を実行します。この操作の前にも確認プロンプトを表示するため、誤操作を防げます。

4. データベースをバックアップ

バックアップには

  • mysqldump
  • mariadb-dump

のどちらか利用可能な方を自動で選択します。

取得したSQLは、そのまま gzip 圧縮して保存します。

nextcloud_backup_20260821_153000.sql.gz

のように日時付きファイルになるため、履歴も管理しやすくしています。

5. updater.phar を起動

Nextcloud標準の

updater.phar

を実行します。ここから先はNextcloud側の対話画面になりますので、画面の案内に従って更新を進めます。

6. 最終確認

更新後は

  • メンテナンスモードの状態確認
  • 必要ならOFFへ変更
  • occ status

まで実行します。最後に現在のバージョンが表示されれば、一通りの更新は完了です。

さっくりとしたまとめ

サーバー運用では「バックアップを取ってから更新する」は当たり前ですが、人間は当たり前ほど慣れてしまうものです。だから私は、忘れない仕組みを作るようにしています。

「運用者は呼吸するかのようにできる。だが、第三者に任せられるか? すっごく焦ってるときに緊急アップデートをした時は?」のためにも、確実に動く仕組みは大切というお話でした。

Page 2 of 66

Powered by WordPress & Theme by Anders Norén