カテゴリー: ガジェット Page 2 of 111

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

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

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

再びの充電機能付きストラップ

以前持ってた充電機能付きのストラップを紛失したので買い直しました。

見た目は上部などパラコート風。で、見て分かるようにコネクタ付き。

今回はカバーの覆いがストラップのコネクタに。返しとスプリングがあるので多少の衝撃では問題ない作りになってます。

これで、機動性と「ケーブル忘れてどうしよう」を解決できます。

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

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

さっくりとしたまとめ

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

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

Thinkpad、ステッカー更新。

何かと使っているポモドーロタイマーソフト

Chill with You Lo-Fi Story。そのオリジナルグッズ群を手に入れました。

ステッカー群やアクキー、万年カレンダー。

手頃なサイズ感と世界観が好きなので、ThinkPadとというキャンパスを埋めていきます。

ただでさえ情報過多な天板が

隙間が更になくなりました。

キーボードレストもこの通り。これで、作業中でもポモドーロタイマーを意識する必要が出てきます。

コントローラー追加とポーチの差し替え。

ノートPCでゲームをする機会が増えたので、小型軽量のコントローラーを購入。

中身は箱とほぼ同じ

これだけコンパクトなのはありがたいのですが、

ユーティリティバッグに入らなくなりました。そこで、

母手作りのポーチに差し替えです。これなら

  • ACアダプター
  • マウス
  • イヤホン
  • ケーブル
  • コントローラー

をいれてもスッキリ収まります。スリザリン要素が少なくなったのはストラップで担保。後の拡張性にも備えられるものになりました。

X Server切り替え1年後の雑感。

こちらの記事から約一年。

8/03 : 契約、初期設定(UFW / fail2ban)

とあることから、実質丸一年が経過。その1年での振り返りを行ってみたいです。

2026年8月時点でのリソース使用量

--- 💻 サーバーのCPU状態 ---
  稼働時間 (Uptime): 8 days
  ロードアベレージ (Load Avg): 0.22, 0.22, 0.19
  CPU名 (Model): AMD EPYC-Milan Processor
  スペック (Speed): 2.00 GHz (1996.250 MHz)
  コア数 (Cores): 4 threads

--- メモリとスワップ ---
[物理メモリ(Physical Memory)]
                             total        used        free      shared  buff/cache   available
Mem:           5.8Gi       3.7Gi       447Mi       163Mi       2.1Gi       2.1Gi
Swap:          4.9Gi       1.1Gi       3.8Gi

[zRAM の展開状況]
NAME       DISKSIZE  DATA  COMPR ALGORITHM STREAMS ZERO-PAGES  TOTAL MEM-LIMIT MEM-USED MIGRATED MOUNTPOINT
/dev/zram0     2.9G  1.1G 254.4M lzo-rle         4      50834 261.6M        0B   948.3M     199K [SWAP]

--- ストレージ(Storage Status) ---
Filesystem      Size  Used Avail Use% Mounted on
  /dev/vda1       145G   62G   83G  43% /
  /dev/vda16      881M  222M  598M  28% /boot
  /dev/vda15      105M  6.2M   99M   6% /boot/efi
  s3fs            4.0G     0  4.0G   0% /mnt/wasabi2
  s3fs            4.0G     0  4.0G   0% /mnt/wasabi

--- CPUを利用しているプロセス: CPU Consumers (Top 10) ---
%CPU   %MEM   PID      USER         UNIT                 COMMAND
------------------------------------------------------------------------------------------
8.0    4.3    796409   www-data     apache2.service      Passenger RubyApp: 
4.4    6.8    621372   mysql        mysql.service        /usr/sbin/mysqld                                  
2.8    3.6    796362   www-data     apache2.service      Passenger AppPreloader:     
1.1    2.0    789277   www-data     apache2.service      /usr/sbin/apache2 -k start                        
0.7    2.1    784299   www-data     apache2.service      /usr/sbin/apache2 -k start                        
0.6    2.0    784297   www-data     apache2.service      /usr/sbin/apache2 -k start                        
0.6    3.9    622142   root         growi.service        node --import 
0.6    1.8    620879   mongodb      mongod.service       /usr/bin/mongod --config /etc/mongod.conf         
0.5    0.4    784275   root         apache2.service      Passenger core                                    
0.5    0.2    622750   www-data     apache2.service      /usr/sbin/apache2 -k start                        

--- メモリを利用しているプロセス: Memory Consumers (Top 10) ---
%CPU   %MEM   PID      USER         UNIT                 COMMAND
------------------------------------------------------------------------------------------
0.4    11.2   479114   elastic+     elasticsearch.service /usr/share/elasticsearch/jdk/bin/java -Des.network
4.4    6.8    621372   mysql        mysql.service        /usr/sbin/mysqld                                  
8.0    4.3    796409   www-data     apache2.service      Passenger RubyApp: 
0.0    4.1    786930   www-data     apache2.service      Passenger RubyApp: 
0.6    3.9    622142   root         growi.service        node --import ./bin/runtime/env-preload.mjs dist/s
0.0    3.8    796066   www-data     apache2.service      Passenger RubyApp: 
2.8    3.6    796362   www-data     apache2.service      Passenger AppPreloader:     
0.0    3.2    990      root         mnt-wasabi.mount     
0.2    2.9    789814   www-data     php8.3-fpm.service   php-fpm: pool www                                 
0.4    2.8    794701   www-data     php8.3-fpm.service   php-fpm: pool www                                 

何というか

  • Growi
  • Nextcloud
  • Redmine

というメモリを食いまくるサイト(そのほかLaravel Stack)を運営していてOoM Killerが発生しなかったものだと思っています。

なぜメモリが枯渇しなかったか

アクセスを絞っている

これが一番のポイントです。筆者は広告が嫌いなので、自分のサイトにも作っていません。そのため、SEOや広告料収入によるアクセスアップを狙っていません。

「これは俺のメモ帳だ。節度を持ってアクセスする分にはかまわないがそれ以外は容赦しない」

というスタンスを貫いています。

クローラーや迷惑ボットの排除

  • 一度でも不審な攻撃/その兆候を見せたら次のアクセスを禁じる「ONE OUTS」システム
  • 雑なクローラーを別サイトへと転送。その推論モデルを破綻させるような文言を随所に仕込み、Webアプリの重い処理に到達させない「Jailhouse Lock」システム

更に、tcpレベルでの嫌がらせをやらかす相手にはipsetで防御。

https://barrel.reisalin.com/books/one-outs/page/ipsetddos

zRamによるメモリ圧縮

先にあった、Swapにブロックデバイスを追加することにより、スワップアウトが更に効率化しています。

https://barrel.reisalin.com/books/linux/page/ubuntuzram

AIとの対話によるセキュリティ案強化

これが一番です。Mod_Securityを「偽陽性を踏んだから単にそれをRemoveしよう」という雑な運用から

「偽陽性を踏みがちなアクション(記事の投稿、ファイルのアップロード)だけを無視する」というリアルタイム外科手術的な技法へと変換。

これにより

「攻撃は防ぐ 偽陽性は排除する おまえごときに『両方』やるのは そうそう難しいことじゃあないな」

と言えるようになったのが極めて大きいです。

結局のところ

「4GB→6GB」にスペックアップしたとしても普通に運用したらOoM Killerやアクセス過多に陥ったであろうシステムを

  • 自分のサイト運営方針に則って数を絞り
  • 不穏なアクセスを防ぐことが第一歩と考えた

AIの力を借りての運用という形でした。

Growiを7.5.7→8.0.0へアップデートしたときの作業メモ

概要

筆者にとっての必須エディタ、Growi。そのv8.0.0が出たので、以下の要件に基づきアップデートを行いました。

リリース情報

実施日: 2026年7月29日
対象環境: Ubuntu 24.04 (非Docker環境 / systemd運用)
その他要件:

itemversion
OSUbuntu 24.04
GROWI7.5.7
node.js24.14.1
npm11.13.0
pnpm11.1.1

必要要件の確認

GROWIの最新リリースにおける変更点(ES7の完全廃止、jemalloc対応、メモリ最適化、ESM移行)に対する自環境の適合状況を確認。

Elasticsearchバージョン確認

ES8/ES9のみに対応しているため、以下を確認。

curl -X GET "http://localhost:9200/" |grep number

9.4.4となっていたので、要件を満たすことは確認しました。

OSメモリ最適化モジュールの確認

ldconfig -p | grep jemalloc

libjemalloc.so.2 (libc6,x86-64) => /lib/x86_64-linux-gnu/libjemalloc.so.2と出たのでモジュールも確認済みです。

ここまでやれば、覚悟を決めてアップデートを行います。

さっくりとしているが面倒な手順

  1. Growi実行スクリプトの修正を行います。
  2. 修正結果が反映されたことを確認します。
  3. 関連サービスの停止を行います。
  4. (推奨)mongodbをまるごとバックアップしておきます。
  5. Growiのアップデートを行います。
  6. アップデートの確認を行います。

Growi実行スクリプトの修正

※筆者は

systemdに登録したサービス起動プログラムがGrowiのルートディレクトリ/home/www-data/growi配下にあるgrowi-start.shを参照して各種環境をキックしています。

※ご自身の環境に合わせ修正を行ってください※

  • 修正前ファイル
#!/bin/bash

# nvmのdefaultエイリアスが指すディレクトリを動的に取得してPATHに追加
# これにより、nvm installでバージョンを上げてもこのスクリプトの書き換えが不要になります
DEFAULT_NODE_VER=$(cat "$NVM_DIR/alias/default")
export PATH="$NVM_DIR/versions/node/$DEFAULT_NODE_VER/bin:$PATH"
GROWI_DIR="/home/www-data/growi"

cd $GROWI_DIR

# 環境変数の設定
export NODE_ENV=production
export AUDIT_LOG_ENABLED=true
export FORCE_WIKI_MODE=private
export MONGO_URI=mongodb://localhost:27017/growi
export ELASTICSEARCH_URI=http://localhost:9200/growi
export REDIS_URI=redis://localhost:6379
export PASSWORD_SEED=(設定済みのパスワード)

# npm run app:server を使用する(npmが内部で正しいパスを見つけてくれます)
# execを使うことでsystemdとの親和性を維持します
exec npm run app:server

このファイルを修正していきますが:バックアップを確実に行います。

  • ファイルバックアップ
sudo cp -pi /home/www-data/growi/growi-start.sh /path/to/backup/directory/growi-start.sh.$(date +%Y%m%d)
  • ファイルバックアップ確認
diff -u /path/to/backup/directory/growi-start.sh.$(date +%Y%m%d) /home/www-data/growi/growi-start.sh

エラー(差分)がなければ両者ともファイルがあり、バックアップが取れています。

バックアップ後、/home/www-data/growi/growi-start.shの編集を行います。(自分の環境に合わせます)

主な変更内容
  • JEMALLOC の有効化: export JEMALLOC_ENABLED=true を追加
  • LD_PRELOAD の明示: export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 を追加
  • MongoDB 接続プールの最適化: MONGO_URI?maxPoolSize=10 パラメータを付与

具体的には以下のような差分が取れていればOKです。

diff -u /path/to/backup/directory/growi-start.sh.$(date +%Y%m%d) /home/www-data/growi/growi-start.sh
# 環境変数の設定
 export NODE_ENV=production
+export JEMALLOC_ENABLED=true
+export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
 export AUDIT_LOG_ENABLED=true
 export FORCE_WIKI_MODE=private
-export MONGO_URI=mongodb://localhost:27017/growi
+export MONGO_URI=mongodb://localhost:27017/growi?maxPoolSize=10
 export ELASTICSEARCH_URI=http://localhost:9200/growi
 export REDIS_URI=redis://localhost:6379

設定ファイルの修正確認

  • Growiサービスの再起動
sudo systemctl restart growi.service

指定した環境変数および jemalloc ライブラリが GROWI プロセス(Node.js)へ正常にロードされていることを確認しました。

  • プロセスの環境変数確認:
    JEMALLOC_ENABLED=true および LD_PRELOAD が正常に適用されていることを確認。
  • メモリマップ / ファイルハンドラ確認:
    lsof および /proc/{PID}/maps コマンドにより、libjemalloc.so.2 が Node.js プロセスに読み込まれていることを確認。

メンテナンスモード有効化

  1. Growiに管理者権限でログインします。
  2. 管理トップ>アプリ設定に進み、「メンテナンスモードを開始する」をクリックします。
  3. トップページに戻り「メンテナンスモード」が表示されていることを確認します。

バックアップ

以下をバックアップします。

  • mongodbの格納データ
cat /etc/mongod.conf |grep dbPath

として、ここのディレクトリ一式を控えます。(筆者環境 /home/mongodb)

このディレクトリを任意の方法でバックアップします。

  • Growiの添付ファイル一式が納められているディレクトリ(ファイルアップロード先をlocalにしている場合のみ)
/growi/root/directory/apps/app/public

(筆者環境 /home/www-data/growi/apps/app/public)ここも念のためバックアップします。

※ 添付ファイルのアップロード先をAWSやAzureなどにしている場合は不要です

  • vpsや仮想ゲストの場合はシステム全体:推奨

スナップショット機能などでシステム全体をバックアップした方が確実で安心です。

ElasticsearchとGrowiの停止

  • Elasticsearchサービス停止
sudo systemctl stop elasticsearch.service
  • Growiサービス停止
sudo systemctl stop growi.service
  • サービス停止確認
systemctl status elasticsearch.service growi.service | grep Active

inactive(dead)を確認します。

作業前バックアップ

  • データディレクトリを丸ごとコピー (-aオプションでパーミッションを維持)
sudo cp -a /var/lib/elasticsearch/ /path/to/backup/dir/elastic_bk.$(date +%Y%m%d)

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

  • バックアップ確認
sudo ls -l /path/to/backup/dir/elastic_bk.$(date +%Y%m%d)

バックアップした内容があることを確認します。(※管理者権限でないとこのディレクトリを見ることはできません)

growiディレクトリに移動します

cd /home/www-data/growi && pwd

自分の環境に合わせます。(筆者環境/home/www-data/growi)

リリースタグを確認します。

  • リリースタグ取得
sudo git fetch --tags
  • リリースタグ確認
sudo git tag -l

スペースで確認していき、上記リリースサイトと同じバージョンがあることを確認します。

チェックアウトとインストールを行います。

  • 変更を一時的に退避
sudo git stash
  • チェックアウト
sudo git checkout 【バージョン】

リリースタグは再確認しましょう。今回は 2026/07/28にリリースされたv8.0.0を選択しました。

  • pnpm install
sudo pnpm i

※これができるのは、筆者のgrowi実行環境がrootだからです。このパーミッション設定は確実に確認しましょう。

  • ビルド
NODE_OPTIONS="--max-old-space-size=4096" sudo pnpm run app:build

→ ビルド時のメモリ大量使用を抑えるため上限を抑えています。余談ですが、ここは神に祈る作業です。

ElasticsearchとGrowiの再開

  • Elasticsearchサービス開始
sudo systemctl restart elasticsearch.service
  • Growiサービス開始
sudo systemctl restart growi.service
  • サービス開始確認
systemctl status elasticsearch.service growi.service | grep Active

active(running)を確認します。

メンテナンスモード無効化

  1. Growiに管理者権限でログインします。
  2. 管理トップ>アプリ設定に進み、「メンテナンスモードを終了する」をクリックします。
  3. トップページに戻り「メンテナンスモード」が表示されていないことを確認します。

バージョンアップを確認します。

  1. 画面下部にあるバージョンがチェックアウトしたバージョン(v8.0.0)であることを確認します。
  2. 各種機能(ページ閲覧や編集)などが正常に行えるかを確認します。

追加モジュールが動いていることを確認します。

  • 一時的に特権ユーザー昇格

※一般権限で見られないため

sudo su -
  • モジュール追加確認
 pgrep -f "node.*growi" | xargs -I {} lsof -p {} 2>/dev/null | grep jemalloc
  • 実行例
MainThrea 479521 root  mem       REG  253,1    830656    15385 /usr/lib/x86_64-linux-gnu/libjemalloc.so.2

バージョンアップ後の作業

必要に応じてバックアップしたファイル一式やスナップショットを削除します。

暑さ対策グッズ。

体温以上の暑さが続く中、これだけは外せないという暑さ対策グッズはこれに落ち着きました。

アルミ張りの日傘

ワークマンにて購入。

直射日光を遮ることで、体感温度は数度から10℃近く下がると言われています。また、輻射熱を遮る点でも重要です。

サングラス

強い紫外線、まぶしさは目から入って脳に疲労物質を出させる原因になります。

普段の眼鏡と同じ度にすることで、普段の視界のまましっかり減光できます。

携帯式氷嚢

血管が集まる首元、首の横、脇の下、手首などを直接冷やせます。

この対策グッズでもMVPです。ハンディファンで生暖かい風を浴びるより合理的です。

このシンデレラフィットするかのようなヒップバッグも見つけられました。これで鞄にもつけられるので満足。

この暑い夏は始まったばかりですが、なんとか死なないように抑えていきたいものです。

Page 2 of 111

Powered by WordPress & Theme by Anders Norén