「Update-motd」を使おう(ユースケース:カスタマイズスクリプトによる遊び心と有用性の追加)

はじめに

筆者が大好きな言葉かつ「何かあったときの道しるべ」としている言葉があります。

In every job that must be done, there is an element of fun. You find the fun and—snap! The job's a game.

映画『メリー・ポピンズ』の『お砂糖ひとさじで / A Spoonful of Sugar』の歌い出しとなっている言葉。

この、

  • どんな仕事にも遊びの要素がある
  • その遊びの要素を見つければ仕事はゲームになる

の2点を『自分自身のLinuxサーバの運用』に組み込んだという話です。

サーバー運用というのは、本来、堅牢性と安定性が求められる、どちらかというと無味乾燥な作業になりがちです。しかし、この単調な作業のどこかに「遊びの要素」を見出し、毎日ログインするのが楽しくなる**ような仕掛けを作れないかと考えました。

その答えが、「ログインコンソールに遊び心を持たせつつ、重要な情報を一瞥できるようにする」という試みです。これにより、単調な作業の始まりが、「今日の情報ブリーフィング」と「ちょっとした楽しみ」に変わります。

いつものようにとりとめない話を強引に組み立てていますが、与太話の延長として読んでいただければ幸いです。

Update-motdとは?

さて、サーバーの「入り口」をゲームのように変えるための舞台装置こそが、Linuxにおける MotD (Message of the Day) の仕組みです。

これは、/etc/update-motd.d/ ディレクトリ配下に配置されたシェルスクリプトやプログラムを順番に実行し、その出力結果を統合してユーザーに表示する仕組みです。

ポイント:

従来の MotD (/etc/motd) が再起動しない限り変わらない「貼り紙」だとすれば、Update-motd はログインのたびに最新の情報に更新される「ニュースフィード」のようなものです。

この動的な特性を利用することで、先述の「遊び心と有用性の追加」というテーマを実現しました。

筆者の例

といっても「なんのこっちゃ」になると思いますので、以下に該当しない方はこれ以降は読まなくて結構です。

  • そもそもLinuxサーバと言われても分からない
  • 仕事は仕事であり遊び心も実用性も不要

では、

/etc/update-motd.d/99-custom に組み込んだ例がこちらです。

#!/bin/bash
# Show weather information. Change the city name to fit your location
ansiweather -l City1 -s true -d true -a false
ansiweather -l City2 -s true -d true -a false

echo "CAST IN NAME OF GOD, YE NOT GUILTY."

# 現在の言語ロケールを保存します。
original_locale=$(locale | grep "LANG=" | cut -d= -f2)

# ロケールを英語に修正します。
export LANG="en_US.UTF-8"

# ロサンゼルス(カリフォルニア)の曜日を調べます。
day_of_week=$(TZ="America/Los_Angeles" date +"%A")

# 金曜日だった場合のみメッセージを表示します。
if [ "$day_of_week" == "Friday" ]; then
    echo "Today is Friday in California."
fi

# 元の言語ロケールに戻します。
export LANG="$original_locale"

ruby /home/user/script/ruby/ssl_checker.rb domain1.example.com domain2.example.com domain3.example.com

bash /home/user/script/bbc_headline.sh

スクリプトの構成と設計思想

このカスタムスクリプトは、

  • ログインコンソールに遊び心を持たせる
  • 重要な情報を一瞥できるようにする

という目的に沿って、大きく四つの機能ブロックで構成されています。

パーソナルな情報の表示と遊び心の追加

コマンド/機能目的と意義
ansiweather -l City1 ...関心のある地域(例:City1, City2)の天気を、カラフルで分かりやすい形式で表示します。作業開始前に個人的な関心事を満たします。
echo "CAST IN NAME OF GOD, YE NOT GUILTY."固定の引用句やメッセージを表示し、ログイン体験にユーモアや個人的なタッチを加えます。
Friday in California Check実行環境のタイムゾーンではなく、特定のタイムゾーン(LA)の曜日をチェックしてメッセージを表示する、技術的な遊び心です。ロケールを一時的に変更し、環境を汚染しないよう元に戻す配慮も含まれています。

サーバー運用上の重要情報チェック

コマンド/機能目的と意義
ruby /home/user/script/...自作のSSL証明書チェッカーを実行し、管理対象のウェブサイトの証明書期限(残日数)をチェックします。期限が近い場合は色を変えて警告するため、サービスの維持に必要な最重要情報を瞬時に把握できます。

外部のニュース情報取得

コマンド/機能目的と意義
bash /home/user/script/...自作のBBCヘッドライン取得スクリプトを実行し、世界のニュースの要約(ヘッドライン)を表示します。作業に入る前にグローバルな視点を持てるようにします。

さっくり言うと

この /etc/update-motd.d/99-custom スクリプトは、ログイン直後の数秒間を、

  1. 個人的な関心事の確認
  2. サーバーの緊急運用リスクのチェック
  3. 世界の主要情報のブリーフィングに使うため

多機能なパーソナルダッシュボードとしての役割を果たしています。

では、各スクリプトを見ていきましょう。

個人的な関心事の確認

  • ansiweather -l City1 ...
    • これは天気をコマンドラインで知るためのコマンド。(ビルトインコマンドではないので sudo apt install ansiweatherでインストールします。
    • これによって、自分が住む町や興味のある都市、行きたい場所などの情報をつかみます。
  • echo "CAST IN NAME OF GOD, YE NOT GUILTY."
    • 単に標準出力にメッセージを流すためのコマンド。サーバ接続を「ショータイム!」とするために仕込んでいます。
  • Friday in California Check
    • 『忍者戦隊カクレンジャー』発の有名なインターネットミーム『Today is Friday in California』をカリフォルニア(ロスアンゼルス)時間の金曜日にのみ表示するというスクリプト。
    • 筆者にとっては金曜日カレーのような意味合いを持ちます。

サーバ運用上のスクリプト

これはAIの力を借りながらも極めて丁寧で重要なスクリプトにしました。なので、スクリプトの意味に関してはChatGPTなりGoogle Geminiなりに聞きましょう。無料アカウントでも親切に教えてくれます。

#!/usr/bin/env ruby

require 'openssl'
require 'socket'
require 'date'
require 'uri'
require 'timeout'

# 色付け用の定数
COLOR_RED = "\e[31m"
COLOR_YELLOW = "\e[33m"
COLOR_GREEN = "\e[32m"
COLOR_RESET = "\e[0m"

# URLの最終的な到達先を取得するメソッド
def get_effective_url(url)
  # curlを使ってリダイレクトを追いかけ、最終的なURLを取得する
  # -s: サイレント, -L: リダイレクト追従, -I: ヘッダのみ, -o /dev/null: ボディ破棄, -w '%{url_effective}': 最終URLを出力
  effective_url = `curl -sLI -o /dev/null -w '%{url_effective}' "#{url}"`
  effective_url.empty? ? nil : effective_url
end

# 証明書の有効期限を取得するメソッド
def get_certificate_expiry_date(url)
  uri = URI.parse(url)
  hostname = uri.host
  port = uri.port || 443 # ポートがなければ443を使う
  ssl_socket = nil
  tcp_client = nil

  begin
    Timeout.timeout(5) do
      tcp_client = TCPSocket.new(hostname, port)
      ssl_context = OpenSSL::SSL::SSLContext.new
      ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, ssl_context)
      ssl_socket.hostname = hostname
      ssl_socket.connect

      cert = ssl_socket.peer_cert
      expiration_date = DateTime.parse(cert.not_after.to_s)
      days_remaining = (expiration_date - DateTime.now).to_i

      return expiration_date, days_remaining
    end
  rescue Timeout::Error
    return nil, "サーバーへの接続がタイムアウトしました。"
  rescue => e
    return nil, e.to_s
  ensure
    ssl_socket&.close
    tcp_client&.close
  end
end

# 結果を表示するメソッド
def print_result(url, expiration_date, days_remaining)
  if expiration_date
    formatted_date = expiration_date.strftime("%Y/%m/%d")
    
    # 残り日数に応じて色を選択
    color = if days_remaining < 14
              COLOR_RED
            elsif days_remaining < 30
              COLOR_YELLOW
            else
              COLOR_GREEN
            end

    puts "サイト #{url} の有効期限は #{formatted_date} です。#{color}残り #{days_remaining} 日です。#{COLOR_RESET}"
  else
    puts "#{COLOR_RED}サイト #{url} の証明書取得に失敗しました: #{days_remaining}#{COLOR_RESET}"
  end
end

# メイン処理
def main
  # コマンドライン引数があるかどうかで処理を分岐
  domains_to_check = if ARGV.empty?
                       # 引数がない場合は、対話式で入力を受け付ける
                       print "チェックしたいサイトのドメインを入力してください(例: example.com): "
                       [gets.chomp]
                     else
                       # 引数がある場合は、それらを全てチェック対象とする
                       ARGV
                     end

  # 各ドメインをチェック
  domains_to_check.each do |domain|
    # 対話モードで空エンターされた場合などをスキップ
    next if domain.nil? || domain.strip.empty?
    
    # http/httpsから始まらない場合はhttpsを付与
    initial_url = domain.start_with?('http') ? domain : "https://#{domain}"
    
    puts "Checking: #{initial_url} ..."
    final_url = get_effective_url(initial_url)

    if final_url.nil?
      puts "#{COLOR_RED}サイト #{initial_url} にアクセスできませんでした。#{COLOR_RESET}"
      next
    end
    
    expiration_date, days_remaining = get_certificate_expiry_date(final_url)
    print_result(final_url, expiration_date, days_remaining)
  end
end

# メイン処理を呼び出し
main

このスクリプトはWebサーバ管理上、とても重要です。

筆者はLet's Encryptのワイルドカード証明書を用いているため、この適切な時期でのチェック(Let's Encryptは90日と短いです)が

  • 「そろそろ準備をしないと」
  • 「まだゆっくりできる」

の判断が可能になります。

BBC Headline

これも、ほぼビルトインコマンドで完結。必要な外部モジュールもxmlintのみ。

#!/bin/bash

# デフォルト値の設定
default_section="world"
default_count=3

# メインセクションのリスト
main_sections=("world" "uk" "business" "politics" "health" "education" "science_and_environment" "technology" "entertainment_and_arts")

# グローバルセクションのリスト
global_sections=("africa" "asia" "europe" "latin_america" "middle_east" "us_and_canada")

# 全セクションのリストを統合
all_sections=("${main_sections[@]}" "${global_sections[@]}")

# 引数の処理
if [[ "$1" =~ ^[0-9]+$ ]]; then
    section=$default_section
    count=$1
else
    section=${1:-$default_section} # 引数1が指定されていない場合はデフォルト値を使用
    count=${2:-$default_count}     # 引数2が指定されていない場合はデフォルト値を使用
fi

# 引数の短縮形を対応する正式名に変換
case "$section" in
    "usa" | "n-usa") section="us_and_canada" ;;
    "me") section="middle_east" ;;
    "latam" | "la") section="latin_america" ;;
    "eu") section="europe" ;;
    "science") section="science_and_environment" ;;
    "entertainment") section="entertainment_and_arts" ;;
    *) section=$section ;; # その他はそのまま
esac

# セクションの検証
if [[ ! " ${all_sections[@]} " =~ " ${section} " ]]; then
    echo "Error: Invalid section '${section}'. Valid sections are: ${all_sections[*]}"
    exit 1
fi

# URLの構築
if [[ " ${main_sections[@]} " =~ " ${section} " ]]; then
    url="https://feeds.bbci.co.uk/news/${section}/rss.xml"
else
    url="https://feeds.bbci.co.uk/news/world/${section}/rss.xml"
fi

# 最初に一度だけRSSフィードをダウンロードし、変数に格納する
xml_content=$(curl -s "$url")

# コンテンツが取得できなかった場合はエラー終了
if [ -z "$xml_content" ]; then
    echo "Error: No headlines found for section '${section}'. Please check the section name or try again later."
    exit 1
fi

# フィードの最終更新日時を取得し、フォーマットする
# 2>/dev/null は、xmllintが出す軽微なエラーを非表示にするため
feed_date_raw=$(echo "$xml_content" | xmllint --xpath "string(//channel/lastBuildDate)" - 2>/dev/null)
if [ -n "$feed_date_raw" ]; then
    # JSTに変換して表示フォーマットを整える
    feed_date_formatted=$(date -d "$feed_date_raw" '+%Y/%m/%d %H:%M:%S %Z')
fi

# 見出しを取得
headlines=$(echo "$xml_content" | xmllint --xpath "//item/title/text()" - 2>/dev/null | sed -e 's/<!\[CDATA\[//g' -e 's/\]\]>//g' | head -n "$count")

# 見出しの表示
echo "BBC News - ${section} section (${count} headlines)"
# 取得した日付を表示
if [ -n "$feed_date_formatted" ]; then
    echo "As of: ${feed_date_formatted}"
fi
echo "--------------------------------------------------" #区切り線
echo "$headlines"

これは

./bbc_headline.sh

とすることで

BBC News - asia section (7 headlines)
As of: 2025/11/02 20:04:03 JST
--------------------------------------------------
300 million tourists just visited China's stunning Xinjiang region. There's a side they didn't see
Devastation on repeat: How climate change is worsening Pakistan's deadly floods
Shein accused of selling childlike sex dolls in France
Cruise cancelled following death of woman left behind on island
From the fringe to the final - India's phenomenon
Why the Indian passport is falling in global ranking
China to loosen chip export ban to Europe after Netherlands row

等の表示が可能になります。

この、日付とニュースの同時表示というのは

「このときにこんなニュースがあった」と、日付と行動の紐付け並びに重要なフックが可能になります。

やや強引なまとめ

と、スクリプトはちょっとした知識とAIの助けがあれば構築可能なものばかりではありますが、

冒頭に掲げた

Spoonful sugar helps medicine goes down(スプーン一杯の砂糖で苦い薬も平気で飲める)

という工夫は何にでも応用可能という言葉で本記事を締めます。

横須賀とランチタイム。

平日休み、横須賀を訪れました。

記念艦三笠は公園含めて改修中。

悪天候が続く中での隙間を縫うかのような晴天に恵まれて、一通り公園を回った後、お昼の時間です。

ご当地サイダーで一息ついた後、出てきたのは

フライドチキン乗せのクリームパスタでした。

ただの濃厚なクリームパスタにあらず。クラムチャウダーの野菜やピクルスを加えることで、味に刺激感とフライドチキンとの調和を図っています。

「ボリューム軽め」のものとして提案いただいたのがこれでしたが、確かにこの味の工夫であれば全体的に軽くなるでしょう。

そして、食べ終わった後に驚きの要素がありました。

「ん? このパスタ皿のロゴは VSOE……?」

恐る恐る皿の裏を見ると「Richard Ginori」の文字。

ビンゴでした。『オリエント急行/Venice Simplon-Orient-Express』で出される皿!

こぢんまりとした店で、これが見られるとは思わず。この、大衆的なご当地グルメの店で出会えたささやかな感動でした。

状況に応じた解決策の提示:スニーカーネットの思考実験を添えて

ここ最近、ゲームの記事やLinuxサーバに関するもの以外の雑多なことを書きたいと思う機会がより高まりました。そこで、ブログのカテゴリに雑多な記事を書く『IDEA SPHERE』を新設し、とりとめのない記事を書いていこうと思います。

このカテゴリの第一弾は思考実験とユースケースです。

はじめに

「大容量のデータを東京→大阪へ転送する場合は、NWを増強したりサーバを高性能にするよりも、データディスクを新幹線(のぞみ)で運んだ方が早い」

という真理に近いジョークがあります。これは、TCP/IPを用いたネットワークよりも物理的な手段(スニーカーネットワーク)の方が早いというもの。

このジョークは本当なのか? ということで思考実験を行いました。

思考実験の定義

対決:NW転送 vs スニーカーネット

写真やテキスト、ビジネス文書などが入った大容量データを対象として、これらのデータを「東京オフィス → 大阪オフィス」へ転送した場合、NW転送とスニーカーネットワーク、どちらが早く完了するかを計算します。この複雑な試算に関してはAIの力を用いています。

前提条件

データ群の条件はこちらです。

  • データ: 4TB (4,194,304 MB)
    • ファイル構造: 超多重構造のファイル群(数万単位)
  • ストレージ: SSD
    • 冗長化されていて、ホットスペアとしてすぐに取り出せる。
  • サーバ: 4コア / 16GB メモリ
  • 回線: 1Gbps (理論最大 125 MB/s)
  • オフィスの位置:東京駅および新大阪駅からタクシーで15分程度。

スタートとゴール

  • NW転送側のスタートとゴール:東京側のファイルの転送指示 → 大阪側の転送完了まで
  • スニーカーネットワークのスタートとゴール:東京側のディスクをホットスペアから取り外し、大阪側のサーバに取り付けてRAIDリビルドが完了するまで。

NW転送の試算

A. 理論上の最速値(ベストケース)

回線速度を一切のロスなく維持し、オーバーヘッドが「ゼロ」と仮定した理想値です。

  • 計算:
    4,194,304 MB ÷ 125 MB/s = 33,554 秒
  • 換算:
    33,554 秒 ÷ 60 ÷ 60 ≒ 約 9.3 時間

B. 現実的な試算(オーバーヘッド考慮)

WAN経由での超多重構造ファイルの転送は、ファイルごとのプロトコル処理、メタデータ転送、TCP/IPのACK遅延により効率が大きく低下します。実効速度が理論値の約24%に低下すると仮定します。

  • 理論値:125 MB/s
  • 現実的な実効速度:30 MB/s(理論値の約24%)
  • 計算:
    4,194,304 MB ÷ 30 MB/s = 139,810 秒
  • 所要時間:
    139,810 秒 ÷ 60 ÷ 60 ≒ 約 38.8 時間(約 1.6 日)

試算2:スニーカーネットワーク

反面、スニーカーネットワーク側の速度を試します。こちらはNWの遅延よりも確実な物理変数が加わります。

A. 移動・準備時間

SSDをホットスワップで取り外し、大阪で接続するまでの時間です。

作業内訳所要時間
SSD取り外し・梱包東京側30 分
東京側移動オフィス → 東京駅ホーム30 分
新幹線移動待機・乗車・ホーム移動3.0 時間 (180 分)
大阪側移動新大阪駅 → 大阪オフィス30 分
開梱・サーバ接続大阪側30 分
小計 (物理移動)4.5 時間

B. データ復旧(リビルド)時間

データ転送が完了する(リビルドが完了する)までをゴールとします。ローカルSSD間のリビルド実効速度を 300MB/s と仮定します。

  • 計算式:
    4,194,304 MB ÷ 300 MB/s = 13,981 秒
  • 所要時間:
    13,981 秒 ÷ 60 ÷ 60 ≒ 約 3.88 時間(余裕を見て 4.0 時間とする)

スニーカーネットワーク全体の所要時間

  • 移動時間:4.5 時間
  • リビルド時間:4.0 時間
合計4.5 時間 + 4.0 時間 = 8.5 時間

整理と勝敗

比較対象ゴール所要時間(試算)
NW転送 (理論値)転送完了まで約 9.3 時間
NW転送 (現実値)転送完了まで約 38.8 時間
スニーカーネットワークリビルド完了まで約 8.5 時間

整理した結果、スニーカーネットワーク(約8.5時間)は、ネットワーク転送の理論上の最速値(約9.3時間)よりも速い可能性が高いことがわかりました。

「超多重構造のファイル群」という条件がある限り、ネットワーク転送の現実的な所要時間は著しく悪化するため、スニーカーネットワークが圧倒的に優位となります。

更なる遅延要素: ウィルス対策システムによるスキャン

ただし、上記の計算値は現実的な要素とは言えません。なぜなら、東京と大阪に拠点を持つオフィスであれば導入されていない方がおかしいシステム「ウィルス対策/EPP/EDRの存在」です。

○NW転送時(38.8時間)の影響:

  • エンドポイント
    • 東京側からのPC/Serverファイル読み出し自、及び大阪側PC\Serverの書き込み時に、EPP/EDRなどのセキュリティソフトがファイルI/Oをフックし、リアルタイムスキャンを実行。
  • ゲートウェイ
    • 東京・大阪間の境界セキュリティ(次世代FW)などを持っている場合:
      • このゲートウェイがトラフィックのペイロード(中身)をスキャン。特にZIPファイルなどの圧縮形式は再帰的な解凍・検査が必要となり、GWのCPUに極大的な負荷をかけます。

これは、ネットワーク転送による実効速度の低下に加え、さらに致命的な処理時間の追加を意味し、38.8時間という試算は最低限の時間であり、実際には遥かに長くなる可能性が高いです。

○ スニーカーネット (約3.88時間) への影響
大阪側のサーバーが、SSDからローカルストレージへコピー(300MB/s程度)する際にも、同様に書き込みスキャンが発生します。

速度低下: 300MB/sという高速コピーも、スキャン処理によって低下します。

とはいえ、ボトルネックはネットワークではなくローカルI/Oです。たとえスキャンによってコピー時間が2倍(約7.76時間)になったとしても、ネットワーク転送の現実値(38.8時間)とは比較にすらなりません。

アナログな知恵:個人のデジタルライフに適用する

この前置きは「アナログな/あるいはアナクロな手段がはるかに効果的である」という主張を補強するためのものです。ここからが本番。

母より以下の相談を受けました。

「10年にも及ぶ数万単位の写真ファイルと、極めて重要な住所録ファイルがある。これを新しいPCに移行したい」

この写真には

  • 家族の思い出(筆者や亡き父、祖父母との写真)
  • 推し活(趣味含む)
  • 友人との旅行の記録

など亡くしてはならないデータ群。

これらを確実にバックアップし、新しいPCにコピーする手段として筆者が選んだのは、1TBのUSB-SSDです。

自室に設置しているNASや筆者が用いるクラウドストレージではなく、あえてUSB-SSDという「物理媒体による移動」を選んだのは、前述の思考実験と同じ真理に基づきます。

1. ネットワークボトルネックの完全回避による実効速度の最大化

思考実験で証明された通り、「数万単位の多重ファイル」の転送は、ファイル処理のオーバーヘッドとレイテンシがボトルネックになります。USB接続では、それがほぼゼロになります。

転送方法ボトルネックとなる要素
NAS 経由LAN 速度、プロトコル処理、多重ファイルのメタデータ処理
USB-SSD旧 HDD の読み込み速度、USB接続のSSD書き込み速度

旧PCのHDDの読み込み速度が最大のボトルネックとなりますが、USB-SSDはそれを最大限に引き出すことができ、NAS経由より遥かに高速でデータを吸い出すことができます。

2. データ移行と一次バックアップの同時実現

作業自体がバックアップ生成となります。

  1. 旧PCのHDDからSSDにデータをコピーする。
    • → この時点でSSDが完全なバックアップとなります。
  2. 新PCにSSDを接続し、データをコピーする。
    • → 移行が完了し、SSDはそのまま一次バックアップ媒体として保管できます。

クラウドやNASにアップロード、ダウンロードする二重の手間と転送時間が不要です。

3. コストパフォーマンスの最適化

  • USB-SSD:筆者が購入した1TBのスティック型SSDは1万円以下。
  • NAS:同容量のNASは最低でも2万円以上の初期投資が必要です。

移行が一回限りのタスクである場合、NASの過剰な機能に高額を支払うよりも、安価で高速な「ツール」であるSSDを選ぶ方が、最もコスト効率が高い選択となります。

4. データの独立性と安全性(世代管理)

移行専用のSSDであるため、筆者のNASの既存データと母の「亡くしてはならないデータ群」が混在するリスクが完全に排除されます。また、「このSSDは、旧PCからデータを吸い出したときの原本である」という形で、データ移行の起点となる世代を物理的に隔離でき、高い安全性をもたらします。

5. ITリテラシーへの配慮と安心感

ITリテラシーが低い人にとって、「ネットワーク」や「クラウド」といった実体のない場所にあるデータよりも、「この手のひらサイズの箱に、すべての思い出が入っている」という方が、圧倒的な安心感につながります。操作も「新PCに挿す」だけで完結するため、サポートの手間もかかりません。


まとめ

USB-SSDによる物理移動は、

  1. 実効転送速度
  2. バックアップの確実性
  3. データ管理のシンプルさ
  4. コスト効率

の四点において、高価で複雑なネットワークソリューションを凌駕しました。これは、東京〜大阪間の思考実験で導かれた結論が、そのまま個人のデジタルライフにも適用できることを示しています。

結局のところ、データ移行において最も重要なのは、最新の技術やスペックではなく、「状況とデータ特性を見極め、最も確実で効率的な手段を選ぶ知恵」である、という話。

  • 目的のためなら手段は正当化される:『君主論』
  • 世の中には手段のためなら目的を選ばないというどうしようもない人間がいる:『HELLSING』

の二律背反がありますが、ここに私は

「利用者の目的が達成されるならばシンプルな手段が効果的な場合がある」

の第三軸を加えた上で、本記事を締めくくります。

Windows11、アプリウィンドウをドラッグしたときのスナップ/画面分割を無効化。

Windows 11の動作にいらついたので、根本的に解決する手段をメモします。

アプリウィンドウを上に持ってきたとき、

このような分割を指示するウィンドウが出てきます。

この機能は私は使わないため、それを抑制するための手順です。

手順

-「設定」を開く
- スタートボタンを右クリックし、「設定」を選択します。
- または、Windowsキー + I を押します。

  • 「システム」→ マルチタスク」へ移動
    • 設定画面の左側メニューから「システム」を選択し、右側の項目から「マルチタスク」をクリックします。
  • 「ウィンドウのスナップ」をカスタマイズ
    • 「ウィンドウのスナップ」の項目にある下向きの矢印>のようなアイコン)をクリックして、詳細設定を展開します。
    • 展開されたオプションの中から、以下のチェックボックスのチェックを外します。
      • 「ウィンドウを画面の上部にドラッグしたときにスナップ レイアウトを表示する」
  • 設定を閉じる
    • これで、ウィンドウを画面上部にドラッグしてもスナップレイアウトが表示されなくなります。

ポイント

上記のチェックを外すことで、画面上部にドラッグしたときの動作のみを抑制できます。
「ウィンドウのスナップ」のメインスイッチ自体をオフにすると、マウスによる画面分割機能(ウィンドウを画面端にドラッグしたときの自動整列など、スナップ機能全般)がすべて無効になります。

Linuxにおけるrootの特別感

  • なんとなくrootを使っている
  • そもそもrootしかアカウントを使っていない

と言う方への注意喚起としても書いている記事です。

Linuxにおける「root」の特別性:システムを司る絶対的な権限

Linuxサーバーを運用する上で、誰もが耳にする最も重要なアカウント、それが「root」ユーザーです。このアカウントは、単なる管理者権限を持つユーザーという枠を超え、システム全体を司る絶対的な権限を持つ存在として位置づけられています。

なぜrootはそれほどまでに特別か、というその問題提起を行います。

システム全体を制御する「神」の権限

Linuxは、セキュリティと安定性のために「権限分離(Privilege Separation)」の原則に基づいて設計されています。一般ユーザーは、自分のホームディレクトリや許可されたファイルに対してのみ操作が可能で、システムの中核に関わる重要な設定ファイルやディレクトリには基本的に触れることができません。

一方でrootユーザーは、この権限の壁を完全に超越します。

  • 全ファイルの読み書き・実行権限: システム内のすべてのファイルとディレクトリに対して、制限なくアクセス、変更、削除が可能です。
  • カーネル操作: システムの心臓部であるLinuxカーネルの設定変更や、重要なモジュールのロード/アンロードが可能です。
  • 全ユーザーの管理: どのユーザーのパスワードでも変更でき、プロセスを強制終了させ、システムを再起動・シャットダウンできます。
  • ポートの利用: 予約された特別なポート(1024未満)を利用したサービスを起動できます。

絶大な力と隣り合わせの危険性

この絶大な力は、システム管理者にとっては不可欠なツールですが、一歩間違えれば致命的な破壊に繋がりかねません。

例えば、一般ユーザーであれば誤って自分のファイルしか削除できませんが、rootユーザーが誤って / (ルートディレクトリ) で rm -rf のようなコマンドを実行すれば、システム全体が一瞬にして破壊されてしまいます。(システム権限に関する部分は維持されるでしょう。しかし、/homeディレクトリにあるユーザデータは消し飛びます。

そのため、システム管理者であっても、普段の作業は権限の制限された一般ユーザーとして行い、root権限は本当に必要な時だけ susudo コマンドで一時的に利用することが鉄則です。

ちょっとした具体例

例えば:

systemctl status apache2.service

は本当によく使うコマンドです。

同様に

exit

はコンソールから抜ける時にも使う初歩的なコマンドでしょう。

しかし、

systemctl 

を打った状態で電話があった、声をかけられ中座する必要があった場合。 (これは本当に頻発します)
「まぁいいや、そっちに集中したいからコンソールを抜けよう」と

systemctl exit

を実行すると

shutdown -h now

と全く同じ効果が得られます。これは笑い事ではない事実です。

systemctl exit一般権限のユーザで実行した場合は

==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
'exit.target'を開始するには認証が必要です。
Authenticating as:

と、「ん? なんか様子が違うぞ?」で思い直すことができますが、これがrootユーザーの場合は

システム停止を自然に、言葉通りに受け止め実行します。

話は戻しましょう。

「全能である」がゆえの危険性と不可欠な存在という側面が、rootユーザーを他のアカウントと一線を画す「特別」な存在にしているのです。

一般的にrootのプロンプトが「#」とされているのは、この 「システムを破壊する力」 を持っていることを常に意識し、コマンドの入力に細心の注意を払うための、管理者自身への視覚的な警告として機能しているのです。

それを防ぐためのちょっとしたTIPS

これを防ぐために私がLinuxサーバを設定時に真っ先に指定する記述がこれです。

/home/hoge/.bashrc

hogeは自分のログインユーザです。

ここの末尾に以下のように記します。

PS1="[\u@\H \W]\$ "

# 一般ユーザ向けのプロンプト設定
if [ "$PS1" ]; then
  if [ "$(id -u)" -eq 0 ]; then # rootユーザの場合
    PS1='\[\e[0;31m\][\u@\H \W]#\[\e[0m\] '
  else # 一般ユーザの場合
    PS1='\[\e[0;32m\][\u@\H \W]$\[\e[0m\] '
  fi
fi

プロンプト設定の意図解説

このPS1設定の主要な意図は、セキュリティの確保と作業効率の向上のために、ユーザーの種別(rootか一般ユーザーか)と現在のコンテキストを視覚的に明確化の措置です。

ユーザー種別による視覚的な区別

最も重要な意図は、誤操作を防ぐためにrootユーザー(管理者)と一般ユーザーの環境を瞬時に見分けられるようにすることです。

  • rootユーザーの場合 (id -u0)
    • 色: \e[0;31m は赤色を設定しています。絶大な権限を持つroot環境では、コマンド入力に細心の注意が必要であることを、視覚的な警告として管理者に促します。
    • プロンプト末尾: \#(通常は#に展開)は、伝統的にrootユーザーを示す記号です。
  • 一般ユーザーの場合:
    • 色: \e[0;32m は緑色を設定しています。通常作業を行う安全な環境であることを示します。
    • プロンプト末尾: $ は、伝統的に一般ユーザーを示す記号です。

コンテキストの明確化

ユーザーの種別に関わらず、プロンプトには以下の情報を含めることで、管理者が「誰として」「どのサーバーの」「どのディレクトリにいるか」を一目で把握できるようにしています。

記号意味
\u現在のユーザー名rootadmin
\Hホスト(サーバー)の完全な名称server.example.com
\W現在の作業ディレクトリのベース名/var/log/ の場合は log

設定スクリプトの技術的な解説

初期設定

PS1="[\u@\H \W]\$ "

これは、色の設定が機能しない場合や、条件分岐(if文)に入る前のデフォルトのプロンプトとして機能します。

条件分岐によるプロンプトの切り替え

if [ "$PS1" ]; then
    if [ "$(id -u)" -eq 0 ]; then # rootユーザの場合
        PS1='\[\e[0;31m\][\u@\H \W]#\[\e[0m\] '
    else # 一般ユーザの場合
        PS1='\[\e[0;32m\][\u@\H \W]$\[\e[0m\] '
    fi
fi
  • if [ "$(id -u)" -eq 0 ]; then: コマンドid -uを実行し、現在のユーザーIDを取得します。0はrootユーザーを意味します。このIDに基づいてプロンプトを切り替えます。
  • \[\]: これらは、エスケープシーケンス(色の設定部分)をbashに非表示文字として認識させるための重要な記号です。これがないと、bashがプロンプトの長さを誤認し、コマンド入力時の行がずれてしまう不具合が発生します。
  • \e[0m: これは色のリセットコードです。プロンプトの末尾にこれを記述することで、プロンプト以降に入力されるユーザーのコマンドや出力が色付きにならないように戻しています。

Ubuntu/Debian系での重要な留意点

このプロンプト設定は、ログインするユーザーの環境に適用される必要があります。

UbuntuやDebian系のシステムでは、rootユーザーとしてログイン(またはsu -で環境を切り替え)する際の.bashrcの扱いが、他のシステム(例:CentOS/RHEL)と異なる場合があります。

多くのシステムでは、rootユーザーのホームディレクトリは /root です。

  • 一般ユーザーのプロンプト設定は、/home/(ユーザー名)/.bashrc に記述します。
  • rootユーザーのプロンプト設定は、通常 /root/.bashrc に記述する必要があります。

この設定は、root環境にも適用。コンソールログインなどでrootでログインした場合にも備え、/root/.bashrcにも明記する必要があります。

補足:

/etc/bash.bashrc などのシステム全体の設定ファイルに記述することも可能ですが、特定のユーザーのみに設定を適用したい場合は、ユーザーごとの.bashrcファイルを使用するのが最も確実な方法です。

まとめ

RHEL系で最初にsudo su -をしたときに明示される

「大いなる力には大いなる責任が伴う」

をより明確にした言葉があります。『鉄人28号』でも高らかに歌われている

あるときは正義の味方
あるときは悪魔の手先
いいもわるいもリモコンしだい

この3行は、まさにLinuxのrootアカウントという「大いなる力」の二面性を表しています。

サーバ管理者は、この力が「悪魔の手先」とならないよう、使う一瞬一瞬に

  • 「自分は今、強大な力を得ているのか?」
  • 「この力は良き目的、必要な手段として用いるのか?」

を自問する必要があります。 その『意識のリモコン』を操作するために、筆者は明確にプロンプトで視覚化しているというお話でした。

MySQLのデータベースを暗号化してバックアップするスクリプト。(2025年10月版)

本記事では、筆者が用いるバックアップスクリプトを元にして、バックアップの大切さを述べます。以前に作成した記事のリファインという形です。

『賢者の石』としてのバックアップ

LinuxのみならずとってITにおけるバックアップは、言うなれば『賢者の石/Philosopher's Stone』です。

ここまで断言できる根拠があります。筆者が前に述べたとおり、Linuxサーバは設定一つの変更、チューニング、誤操作などで全てが壊れる世界です。

そんな中、適切なバックアップを取っておくことで、失敗全てを帳消しにできる確率が飛躍的に高まります。

また、バックアップを取ることにより、他のサーバへの移し替えや「問題が発生した状況の再現」も容易になります。

筆者が手順で

sudo cp -pi /something/very/very/massive/important/file.conf /extra_sensitive_backup.conf.$(date +%Y%m%d)

等でバックアップを取り、

diff -u sudo cp -pi /extra_sensitive_backup.conf.$(date +%Y%m%d) /something/very/very/massive/important/file.conf 

と差分を取るのも、「事前のちょっとした面倒な作業」は「後に発生する大災害」を救ってくれます。

特に重要なWebアプリでのDBバックアップの重要性

翻って、Webアプリケーションの運用となると、この「賢者の石」としてのバックアップの役割は、単なるシステム設定の保護を超え、Webサイトのみならず事業の存続に直結します。

なぜなら、Webアプリにおいて最も価値があり、最も頻繁に変化し続ける情報、すなわち『データ(データベース)』こそが、そのアプリケーションの「魂」だからです。

自分を含め

  • 長年蓄積した記事
  • 画像情報
  • (商用利用の場合は)決済情報、顧客情報

これらデータベースに格納された情報は、失われた瞬間、企業の信用も収益もゼロになります。法的な責任も発生すれば、これらは簡単にマイナスになるという、筆者が前にも述べている「理不尽な非対称性」が発生します。

ファイルシステム全体が健在であっても、データベースだけが破損すれば、サイトは事実上停止します。これは、設定ファイル一つを誤った際の「システム停止」とは次元の異なる、「世界の消滅」を意味します。

だからこそ、WebアプリのDBバックアップには、以下の3つの要素が絶対的に求められます。

  1. 完全なバックアップ(データの保護): mysqldumpなどを利用し、停止時やトランザクション中のデータも漏れなくダンプすること。
  2. 確実な安全性(暗号化): 個人情報を含む機密性の高いデータであるため、保管時には必ず強固なパスワードで暗号化し、不正アクセスからデータを守ること。
  3. 信頼できる世代管理: 毎日確実にバックアップを取り続け、かつ必要な日数分だけ保持し、古すぎるバックアップは自動で削除する仕組み(世代管理)が不可欠です。これにより、ディスク容量の圧迫を防ぎつつ、「昨日の正常な時点」や「1週間前の状態」へ確実に巻き戻せる保証を得るのです。

次に紹介するスクリプトは、このWebアプリの命綱たるデータベース (MySQL) を、安全性と世代管理を両立させながら、コマンド一つで確実に守り抜くために作成したものです。

事実、筆者はこの手のバックアップにより

  1. AWS
  2. WebArena
  3. XServer

と3つのVPSからのWebサイト移行を行いました。

その前に事前確認

Prevention is better than cure.(転ばぬ先の杖)というやつです。

これはUbuntu系での手順です。

MySQL での操作です。

  • mysqldump 確認
which mysqldump

/usr/bin/mysqldumpなどと返ってくることを確認。見つからない場合は

sudo aptitude install mysql-client
  • データベースユーザーのアクセス権確認・付与(重要)

データベースユーザー(例: your_db_user)が、バックアップ対象のデータベース($DATABASE_NAME)全体に対してデータを読み取る権限(SELECT)を持っている必要があります。

MySQL/MariaDBにrootでログインし、以下のコマンドで確認します。

SHOW GRANTS FOR 'your_db_user'@'localhost'; 
  • 権限の付与:

もし権限が不足している場合は、rootでしてログインし、必要な権限を付与します。

データベースのダンプ(バックアップ)には、最低限 SELECT 権限が必要です。

また、--single-transactionオプションを使う場合、ほとんどのストレージエンジンでRELOAD(またはPROCESS)権限は必須ではありません。

ここまで来たら、いよいよスクリプトの作成です。

DBというシステムの生命線のバックアップたる「賢者の石」。パスの間違いなどで簡単に「持ってかれる」等価交換以上の代償を伴う作業。慎重にやっていきましょう。

さっくりとは言い難い手順

  1. 事前準備。DBアカウントファイルを作成します。
  2. 事前準備。バックアップなどを格納するディレクトリを作成します。
  3. 自分の環境に合わせて スクリプトを作成します。
  4. 動作の確認を行い、cron設定を行います。

必要なファイルとディレクトリ群、注意

項目説明設定値 (スクリプト内変数)
MySQLアカウントファイルmysqldump実行に使用するデータベース接続情報ファイル。パーミッションは400必須。$CREDENTIALS_FILE (/home/hoge/script/config/db_account/db_account.txt)
バックアップ保管ルート圧縮後のバックアップファイルを保管するディレクトリ。$BACKUP_ROOT_DIR (/path/to/backup/directory)
パスワード保管ディレクトリバックアップZIPファイルの解凍パスワードを保管するディレクトリ。$PASSWORD_DIR (/home/hoge/dbrestore_password)
実行ユーザースクリプトを実行するOSユーザー。/home/hogeの所有者

事前準備(ファイル・ディレクトリの作成とパーミッション設定)

スクリプトを実行する前に、以下のファイルとディレクトリを準備してください。

  • データベースアカウントファイルの作成

$CREDENTIALS_FILEで指定されたパスに、MySQLの接続情報を記述したファイルを作成します。

  1. ディレクトリの作成: mkdir -p /home/hoge/script/config/db_account
  2. アカウントファイルの作成 (/home/hoge/script/config/db_account/db_account.txt):
[mysqldump]
# [mysqldump]セクションが必要ですuser=your_db_user # ダンプ操作が可能なユーザー名 password="your_db_password" # ユーザーのパスワード

パーミッションの設定(重要):
スクリプトの実行条件であり、セキュリティ上必須です。

chmod 400 /home/hoge/script/config/db_account/db_account.txt

※重ねての注意点このDBアカウント情報はrootアカウントと同等以上のリスク管理が必要です。すなわち

「No shown, No stolen, No matter what. (見せても盗まれてもならぬ、何があっても)」

の強い覚悟が必要です。(とはいえ自分の命と天秤にかけた状態で)

バックアップディレクトリの作成

バックアップファイルとパスワードファイルを保管するディレクトリを作成します。

  1. バックアップルートディレクトリの作成:
    bash mkdir -p /path/to/backup/directory
  2. パスワード保管ディレクトリの作成:
    bash mkdir -p /home/hoge/dbrestore_password
  3. パーミッションの設定:
    スクリプトを実行するOSユーザーが、これらのディレクトリに対して書き込み・読み取り・実行権限を持っていることを確認してください。通常はchownコマンドで所有者を設定します。

スクリプトの作成

backup_mysql.shを任意の方法で、或いは自身の教義・信仰に則ったエディタを用いて作成します。変数の部分は 自分の環境に合わせて 調整します。

#!/bin/bash

# コマンドの実行中にエラーが発生した場合、スクリプトを終了します
set -e

## 変数ここから ##
# $HOMEの変数を指定します。
HOME_DIR="/home/hoge"
# SQLをバックアップする**保管先**ディレクトリを指定します。運用に合わせて指定ください。
BACKUP_ROOT_DIR="/path/to/backup/directory"
# バックアップ処理用の一時ディレクトリ名を指定します。
TMP_SUBDIR="temp_dump"
# パスワードファイルを保管するディレクトリを指定します。
PASSWORD_DIR="${HOME_DIR}/dbrestore_password"

# 保持するバックアップの世代を日数で指定します。
KEEP_DAYS=7

# ファイルに付与する日付/作業ディレクトリ名/バックアップファイル名を指定します。
CURRENT_DATE=$(date +%Y%m%d%H%M%S) # 日付に時刻を加えて一意性を高める
BACKUP_BASE_NAME="database" # バックアップ対象のデータベース名などから命名
SQL_FILE_NAME="${BACKUP_BASE_NAME}.sql"
ZIP_FILE_NAME="${BACKUP_BASE_NAME}.${CURRENT_DATE}.zip"
PASSWORD_FILE_NAME="db-restore.${CURRENT_DATE}.txt"

# 一時的なバックアップディレクトリのフルパス
TMP_BACKUP_DIR="${BACKUP_ROOT_DIR}/${TMP_SUBDIR}"
# 圧縮後のバックアップファイルのフルパス
ZIP_FILE_PATH="${BACKUP_ROOT_DIR}/${ZIP_FILE_NAME}"
# パスワードファイルのフルパス
PASSWORD_FILE_PATH="${PASSWORD_DIR}/${PASSWORD_FILE_NAME}"

# アカウントファイルを指定します。運用に合わせて指定ください。
CREDENTIALS_FILE="${HOME_DIR}/script/config/db_account/db_account.txt"
# redmineのデータベース名を指定します。
DATABASE_NAME="database"
# バックアップ時に指定するオプションを指定します。
OPTIONS="--defaults-extra-file=$CREDENTIALS_FILE --no-tablespaces --single-transaction"

# 世代管理のためのファイルパターン
BACKUP_FILE_PATTERN="${BACKUP_BASE_NAME}.*.zip"
PASSWORD_FILE_PATTERN="db-restore.*.txt"
## 変数ここまで ##

## 処理ここから ##

# 1.アカウントファイルのパーミッションが400かどうかチェックします。
# 400以外は処理そのものを終了します。
permissions=$(stat -c "%a" "$CREDENTIALS_FILE")
if [ "$permissions" != "400" ]; then
    echo "🚨 エラー: アカウントファイル(${CREDENTIALS_FILE})のパーミッションは400である必要があります。(現在: ${permissions})" >&2
    exit 1
fi

# 2. 一時的なバックアップディレクトリを作成します。
# -pで存在しない親ディレクトリも作成し、既に存在してもエラーにしない
mkdir -p "${TMP_BACKUP_DIR}" || exit 1
mkdir -p "${PASSWORD_DIR}" || exit 1

# 3. mysqldumpを実行してデータベースのバックアップを取ります。
echo "⏳ ${DATABASE_NAME}のデータベースダンプを開始..."
mysqldump $OPTIONS -h localhost "$DATABASE_NAME" > "${TMP_BACKUP_DIR}/${SQL_FILE_NAME}" || {
    echo "🚨 エラー: mysqldumpが失敗しました。" >&2
    rm -rf "${TMP_BACKUP_DIR}" # 失敗したら一時ディレクトリをクリーンアップ
    exit 1
}
echo "✅ データベースダンプ完了。"

# 4. パスワードによる暗号化を実施します。
password=$(openssl rand -base64 12)

# 【ルートディレクトリ圧縮回避のため、一時ディレクトリに移動して圧縮対象ファイルを直接指定】
echo "⏳ バックアップファイルの圧縮・暗号化を開始..."
cd "${TMP_BACKUP_DIR}" || exit 1
# zipコマンドで、SQLファイルのみを対象とし、圧縮後のzipを一つ上の階層に出力
zip -P "$password" "${ZIP_FILE_PATH}" "${SQL_FILE_NAME}" || {
    echo "🚨 エラー: zip圧縮が失敗しました。" >&2
    cd - > /dev/null # 元のディレクトリに戻る
    rm -rf "${TMP_BACKUP_DIR}" # 失敗したら一時ディレクトリをクリーンアップ
    exit 1
}
cd - > /dev/null # 元のディレクトリに戻る
echo "✅ 圧縮・暗号化完了: ${ZIP_FILE_PATH}"

# 5. 一時的なバックアップディレクトリを削除します。
echo "🗑️ 一時ディレクトリを削除: ${TMP_BACKUP_DIR}"
rm -rf "${TMP_BACKUP_DIR}"

# 6. 解凍パスワードを指定ディレクトリに保存し、権限を設定します。
echo "🔐 解凍パスワードを保存: ${PASSWORD_FILE_PATH}"
echo "$password" > "$PASSWORD_FILE_PATH"
# 7.パスワードの読み取り権限を600に変更します。
chmod 600 "$PASSWORD_FILE_PATH"

# 8. 【確実にバックアップファイルを世代管理 (古いファイルの削除)】
echo "🧹 保持期間(${KEEP_DAYS}日)より古いバックアップを削除..."
# バックアップファイル本体の削除
find "$BACKUP_ROOT_DIR" -maxdepth 1 -name "$BACKUP_FILE_PATTERN" -type f -mtime +$KEEP_DAYS -print -delete
# 対応するパスワードファイルの削除
find "$PASSWORD_DIR" -maxdepth 1 -name "$PASSWORD_FILE_PATTERN" -type f -mtime +$KEEP_DAYS -print -delete
echo "✅ 世代管理完了。"

## 処理ここまで ##
  1. スクリプトファイルの作成と配置:
    このスクリプトをファイル(例:backup_mysql.sh)として保存します。
    bash # 例 cp (修正済みスクリプトの内容) /home/hoge/backup_mysql.sh
  2. 実行権限の付与:
    bash chmod +x /home/hoge/backup_mysql.sh
  3. テスト実行:
    bash /home/hoge/backup_mysql.sh
  4. 定期実行の設定(Cronなど):
    Cronを利用して、スクリプトを定期的に実行するように設定します。
    bash # crontab -e で開き、以下の行を追加 (例: 毎日午前2時に実行) 0 2 * * * /home/hoge/backup_mysql.sh > /dev/null 2>&1

スクリプトの動き

このスクリプトは、運用者(つまり私のようなサーバ管理者)望む機能を盛り込んだ機能を持たせています。

  • 予め配備されたアカウント情報に基づき、mysqldumpで、対象DBのみのバックアップを行います。
  • バックアップと同時に圧縮&暗号化を行います。
  • 復号に必要なパスワードは同時に別ディレクトリに保管。有り体に言えば、スクリプト自身も知らないランダムパスワードなので推測される可能性はほぼありません。
  • その後、スクリプトはDBバックアップディレクトリとパスワード格納ディレクトリを精査。変数に基づいて、特定日数以上が過ぎたファイルを自動的に削除して、ディスクの容量を安定化します。

では、どうやって運用するの?

パスワードによる複合化

スクリプト実行後、対象の.zipファイルを

unzip database.20251028113148.zip

等として、スクリプト実行日時が入ったファイルを展開します。

このとき、パスワードを尋ねられるので、対になる複合化のパスワードファイルdb-restore.20251028113148.txtで開きます。database.sqlが平文で出てきます。

DBの切り戻し

こうして、DBのバックアップさえできていれば、その後の安心感はまるで異なります。

  • サーバが吹っ飛んだ
  • 別サーバに移行したい

等の場合も、

mysql -h localhost -u your_db -p your_db < /path/to/backup/directory/database.sql

とすることで、切り戻しや移行のハードルが非常に楽になります。

最後に

「何かの事故」
「故意/未必の故意/ミス」

など、「障害」という奴は予測できないからこそ「障害」です。管理者にできることは

「障害が来ないことを祈る」

ではありません。

「障害が来ても大丈夫な備え」をシステム化することです。

「汝平和を欲さば、戦への備えをせよ」/"Si vis pacem, para bellum"

は、ローマより続く言葉であるからこそ、普遍性と不変性があるのです。

CPU/メモリの利用状況を確認するスクリプト

Webサーバのみならず、サーバ運用において「どのプロセスがCPU/メモリを喰っているか」というボトルネックの把握は重要です。

それを把握するためのスクリプトのご紹介です。

なぜボトルネックの把握が重要なのか

以下の3点が主な理由です:

  1. リソースの最適化と安定運用
     高負荷プロセスを特定することで、不要な消費を抑え、他のサービスへの影響を防げます。
  2. 障害予防と早期対応
     異常なリソース使用は障害の前兆であることが多く、早期発見によりダウンタイムを回避できます。
  3. 攻撃予兆への対応
     DDoS/執拗な攻撃などはリソース量にダイレクトに現れます。

把握するためのシェルスクリプト

といっても、topwコマンドなどでは煩雑な情報が多いため、シンプルに

  1. CPUを多く使っているプロセス
  2. メモリを多く使っているプロセス

に絞り込みを行います。というのも、プロセスの暴走は先に示したとおり、CPU/メモリを多く使うからです。

それをより分かりやすく視覚化するスクリプト例が以下の通り。

top-procs.sh等の名前で、任意の場所に作成します。

#!/bin/bash

# スクリプト名: top-procs.sh
# 説明: CPU使用率またはメモリ使用率が高い上位5つのプロセスを表示します。

# 表示するプロセス数の設定
TOP_N=5

# ヘルプ表示関数
show_help() {
    echo "--- プロセス監視スクリプト ---"
    echo "このスクリプトは、システムのCPU使用率またはメモリ使用率が高い上位${TOP_N}つのプロセスを表示します。"
    echo ""
    echo "使用方法: $0 [オプション]"
    echo ""
    echo "オプション:"
    echo "  -c          : CPU使用率 (%\$CPU) の高い上位${TOP_N}つのプロセスを表示します。"
    echo "  -m          : メモリ使用率 (%\$MEM) の高い上位${TOP_N}つのプロセスを表示します。"
    echo "  -a          : CPUとメモリの両方の上位${TOP_N}つのプロセスを表示します。(引数なしと同じ)"
    echo "  -h          : このヘルプを表示します。"
    echo ""
    echo "出力形式: 割合(%) PID COMMAND"
    echo "-----------------------------------------"
}

# プロセス情報表示関数
# 引数1: ソート対象 (CPU/MEM)
# 引数2: ソートフィールド番号 (ps auxの3番目か4番目)
# 引数3: タイトル
show_top_procs() {
    local type=$1
    local field=$2
    local title=$3

    echo ""
    echo "--- ${title} (上位 ${TOP_N} プロセス) ---"
    echo " %${type}   PID  COMMAND"
    echo "-----------------------------------------"

    ps aux |
        # ヘッダー行をスキップ
        tail -n +2 |
        # 指定フィールド (CPU:%3, MEM:%4) で降順ソート
        sort -k ${field} -r |
        # 上位N行を抽出
        head -n ${TOP_N} |
        # PID ($2)、割合 ($field)、COMMAND ($11以降) を整形して表示
        awk -v field="${field}" '{
            cmd="";
            for(i=11;i<=NF;i++){
                cmd=cmd" "$i
            };
            # $fieldには$3(%CPU)または$4(%MEM)の値が入る
            printf "%6.2f%% %6s %s\n", $field, $2, cmd
        }'
}

# メインロジック

if [ "$#" -eq 0 ] || [ "$1" == "-a" ]; then
    # 引数なし、または -a の場合 (全て表示)
    show_top_procs "CPU" 3 "CPU使用率"
    show_top_procs "MEM" 4 "メモリ使用率"
elif [ "$1" == "-c" ]; then
    # -c の場合 (CPUのみ)
    show_top_procs "CPU" 3 "CPU使用率"
elif [ "$1" == "-m" ]; then
    # -m の場合 (メモリのみ)
    show_top_procs "MEM" 4 "メモリ使用率"
elif [ "$1" == "-h" ]; then
    # -h の場合 (ヘルプ)
    show_help
else
    # 不正な引数の場合
    echo "不正なオプションです: $1" >&2
    show_help
    exit 1
fi

仕組み

メインロジックは非常に簡単。

ps , sort, 等のコマンドとawkを発展させたもの。

./top-procs.sh

を実行することで、

--- CPU使用率 (上位 5 プロセス) ---
 %CPU    PID  COMMAND
-----------------------------------------
 52.10%  12345  ruby_app_server: /var/www/webapp1 (production)
  9.40%   1086  /usr/sbin/database_server [...]
  3.80%  42162  /usr/sbin/web_server -k start
  1.50%  42161  /usr/sbin/web_server -k start
  0.90%   7978  nodejs_process /path/to/nodejs_app/server.js

--- メモリ使用率 (上位 5 プロセス) ---
 %MEM    PID  COMMAND
-----------------------------------------
 13.10%   1984  /usr/bin/java -Xms256m -Xmx256m [...] search_engine -Des.path.home=/usr/share/search_engine [...]
 10.00%   1086  /usr/sbin/database_server [...]
  7.50%  12345  ruby_app_server: /var/www/webapp1 (production)
  3.90%  78630  ruby_app_server: /var/www/webapp2 (production)
  3.80%  76583  ruby_app_server: /var/www/webapp3 (production)

が出てきます。

この例では、rubyアプリが圧倒的にCPUを消費し、ElasticSearchがメモリを食っているというのが分かります。

そして、

  • -a / 引数無し : CPUとメモリの両方を表示
  • -c : CPU情報のみを表示
  • -m : メモリ情報のみを表示
  • -h : これら引数やスクリプトの内容を表示

と、目的に合わせた柔軟な表示も可能にしています。

ついでにコマンド化

こういった障害発生時のボトルネック判定時、いちいちスクリプトの場所を探すという悠長なことはできません。

なので、余裕がある(つまりこのスクリプトを作成した直後です)状況で、

sudo ln -sf /path/to/script/top-procs.sh /usr/local/bin/top-procs

として、どこからでもコマンドを呼び出せるようにします。(スクリプトの場所は自分がこれを保存した絶対パスを指定してください)

which top-procs

/usr/local/bin/top-procs

と表示されればコマンド化は完了。こうすることにより、どのユーザーでもこのコマンド一発で上記のボトルネック判定が可能になります。

統率者デッキ、再修正。

本当は10月に回したかったデッキです。

統率者

  • F《起源の番人、ザーレル/Szarel, Genesis Shepherd(EOC)》

Szarel, Genesis Shepherd / 起源の番人、ザーレル (2)(黒)(赤)(緑)
伝説のクリーチャー — 昆虫(Insect) ドルイド(Druid)
飛行
あなたの墓地にある土地をプレイしてもよい。
あなたのターン中、あなたがトークンでもこれでもないパーマネント1つを生け贄に捧げるたび、これでないクリーチャー最大1体を対象とする。それの上にこれのパワーに等しい個数の+1/+1カウンターを置く。

2/5

クリーチャー

  • 《甦りし悪夢、ブレイズ/Braids, Arisen Nightmare(EOC)》
  • 《ユーミディアンの荒地起こし/Eumidian Wastewaker(EOC)》
  • 《エヴェンドの低木刈り/Evendo Brushrazer(EOC)》
  • 《アクームの怒り、モラウグ/Moraug, Fury of Akoum(ZNR)》
  • 《土地守/Groundskeeper(EOC)》
  • 《森を護る者/Sylvan Safekeeper(MH3)》
  • 《事件現場の分析者/Aftermath Analyst(EOC)》
  • 《サテュロスの道探し/Satyr Wayfinder(EOC)》
  • 《春花のドルイド/Springbloom Druid(EOC)》
  • 《機能不全ダニ/Haywire Mite(BLC)》
  • 《不屈の追跡者/Tireless Tracker(EOC)》
      1. 手掛かり
  • 《不屈の補給兵/Tireless Provisioner(BLC)》
      1. 宝物
      1. 食物
  • 《水平線の探検家/Horizon Explorer(EOC)》
    • 1. 着陸船
  • 《進化の証人/Evolution Witness(MH3)》
  • 《ムル・ダヤの巫女/Oracle of Mul Daya(EOC)》
  • 《最強のベイロス/Baloth Prime(EOC)》
    • 2. 4/4 ビースト
  • 《アルゴスの庇護者、ティタニア/Titania, Protector of Argoth(EOC)》
    • 3. 5/3 エレメンタル
  • 《猛り狂うベイロス/Rampaging Baloths(EOC)》
    • 2. 4/4 ビースト
  • 《波乱の悪魔/Mayhem Devil(EOC)》
  • 《ウィンドグレイスの魂/Soul of Windgrace(EOC)》
  • 《ギトラグの怪物/The Gitrog Monster(EOC)》
  • 《クロールの死の僧侶、マジレク/Mazirek, Kraul Death Priest(EOC)》
  • 《フェイに呪われた王、コルヴォルド/Korvold, Fae-Cursed King(EOC)》

インスタント

  • 《羅利骨灰/Tear Asunder(EOC)》
  • 《砕土/Harrow(EOC)》
  • 《コラガンの命令/Kolaghan's Command(DTK)》
  • 《ウィンドグレイスの裁き/Windgrace's Judgment(EOC)》

ソーサリー

  • 《信仰無き物あさり/Faithless Looting(UMA)》
  • 《惑星殲滅/Planetary Annihilation(EOC)》
  • 《冒涜の行動/Blasphemous Act(EOC)》
  • 《害獣の侵入/Pest Infestation(EOC)》
    • 4. 1/1 害獣
  • 《森の占術/Sylvan Scrying(BFZ)》
  • 《探検/Explore(WHO)》
  • 《壌土からの生命/Life from the Loam(UMA)》
  • 《自然の知識/Nature's Lore(EOC)》
  • 《遥か見/Farseek(EOC)》
  • 《耕作/Cultivate(EOC)》
  • 《耕作の閃光/Flare of Cultivation(MH3)》
  • 《形なき始まり/Formless Genesis(EOC)》
    • 5. X/X 多相の戦士
  • 《明日への探索/Search for Tomorrow(TSP)》
  • 《見事な再生/Splendid Reclamation(EOC)》
  • 《スカイシュラウドの要求/Skyshroud Claim(EOC)》
  • 《世界魂の憤怒/Worldsoul's Rage(EOC)》
  • 《大渦の脈動/Maelstrom Pulse(ARB)》
  • 《花崗岩の凝視/Gaze of Granite(EOC)》
  • 《僻境への脱出/Escape to the Wilds(EOC)》
  • 《蟲の収穫/Worm Harvest(C18)》
    • 6. 1/1 ワーム

エンチャント

  • 《発生の器/Vessel of Nascency(SOI)》
  • 《パーフォロスの槌/Hammer of Purphoros(THS)》
    • 7. 3/3 ゴーレム
  • 《鏡割りの寓話/Fable of the Mirror-Breaker(NEO)》
      1. 2/2 ゴブリン・シャーマン

アーティファクト

  • 《探検の地図/Expedition Map(2XM)》
  • 《太陽の指輪/Sol Ring(EOC)》
  • 《秘儀の印鑑/Arcane Signet(EOC)》
  • 《探査幼生艦/Exploration Broodship(EOC)》
  • F《世界播種、ハースハル/Hearthhull, the Worldseed(EOC)》

プレインズウォーカー

  • レンと次元壊し

土地

  • 4《沼/Swamp(EOE)》 フルアート
  • 4《山/Mountain(EOE)》 フルアート
  • 5《森/Forest(EOE)》 フルアート
  • 《土建組一家の監督所/Riveteers Overlook(EOC)》
  • 《硫黄泉/Sulfurous Springs(EOC)》
  • 《燻る湿地/Smoldering Marsh(EOC)》
  • 《ラノワールの荒原/Llanowar Wastes(EOC)》
  • 《カープルーザンの森/Karplusan Forest(EOC)》
  • 《燃えがらの林間地/Cinder Glade(EOC)》
  • 《春色の湿原/Vernal Fen(EOC)》
  • 《黄昏のぬかるみ/Twilight Mire(EOC)》
  • 《ボジューカの沼/Bojuka Bog(EOC)》
  • 《ユーミディアンの孵化場/Eumidian Hatchery(EOC)》
    • 8. 1/1 昆虫
  • 《寓話の小道/Fabled Passage(EOC)》
  • 《統率の塔/Command Tower(EOC)》
  • 《ウルザの物語/Urza's Saga(MH2)》
    • 9. 0/0 構築物
  • 《変容する森林/Shifting Woodland(MH3)》
  • 《モリアの坑道/Mines of Moria(LTR)》
  • 《演劇の舞台/Thespian's Stage(WHO)》
    • 宝物
  • 《暗黒の深部/Dark Depths(UMA)》
    • 20/20 マリット・レイジ
  • 《ダクムーアの回収場/Dakmor Salvage(EOC)》
  • 《見捨てられたぬかるみ、竹沼/Takenuma, Abandoned Mire(NEO)》
  • 《不毛の大地/Wasteland(TMP)》
  • 《魂の洞窟/Cavern of Souls(AVR)》
  • 《虹色の眺望/Prismatic Vista(MH1)》
  • 《樹木茂る山麓/Wooded Foothills(ONS)》
  • 《新緑の地下墓地/Verdant Catacombs(ZEN)》
  • 《血染めのぬかるみ/Bloodstained Mire(ONS)》
  • 《草むした墓/Overgrown Tomb(RAV)》
  • 《血の墓所/Blood Crypt(RTR)》
  • 《踏み鳴らされる地/Stomping Ground(GTC)》
  • 《耐え抜くもの、母聖樹/Boseiju, Who Endures(NEO)》
  • 《ケッシグの狼の地/Kessig Wolf Run(ISD)》

OUT

  1. 《苦い真理/Painful Truths(BFZ)》
  2. 《秋の占い師/Augur of Autumn(MID)》

IN

  1. 《不屈の補給兵/Tireless Provisioner(BLC)》
  2. 《鏡割りの寓話/Fable of the Mirror-Breaker(NEO)》

トークンも用意できないていたらくでした。ただ、回していて楽しいデッキです。

10月の食事ピックアップ。

連休時の話。

お世話になっている肩にご馳走になりました。

伊勢エビ

予想外にも程があった伊勢エビ。過去に数回食べたことがありましたが

この量が出るのは驚きでした。しかし、それ以上に驚きだったのが「これは半身です」と言われたこと。

残りの半身は天ぷら。確かに海老は天ぷらの種としての定番ですが、伊勢エビというのも予想外でした。上品な甘味と歯触り、いずれも素晴らしいものでした。

しかし、話はそれだけでは終わらず。

刺身で出てきた尾頭は具足煮。しかも、前身をこんがりあぶった鬼殻焼きスタイルです。

頭の味噌から出る出汁がたっぷり出ていて、残っていた頭の肉も食べ応え十分。

その他食事

これをその他と言っていいのかは不明ですが:

兜煮。全体は崩れず、味がしっかり染みている当最高の出来。目玉のゼラチン質も最高です。

ご飯は伝統的な塩むすびと

ネギトロ&トロタク。

ここまで贅沢でいいのかという形の、非常に充実した食事でした。

『Elasticsearchを利用しているGrowiでのアップデート注意点(Growi特有のプラグインに起因するサービス停止)』

概要

GrowiでElasticsearchによる全文検索を利用している中でのアップデートの注意点です。なお、Growiをオンプレ運用していてサーバリソースの関係上などでElasticsearchを用いていないという方はここから下は参考程度にとどめてください。

環境

  • Grwoi v7.3.3
  • Elasticsearch v9.2.0
  • Apacheによるリバースプロキシ

パッケージアップデート時の注意

sudo aptitude upgrade

(※筆者は好みでaptitudeを使っています)

とした中で、

以下のパッケージが更新されます:          
  elasticsearch 

と、elasticsearchを含むアップデートがあったときの注意点です。

注意1:設定ファイルの維持

(データベースを読み込んでいます ... 現在 170063 個のファイルとディレクトリがインストールされています。)
.../elasticsearch_9.2.0_amd64.deb を展開する準備をしています ...
elasticsearch (9.2.0) で (9.1.5 に) 上書き展開しています ...
elasticsearch (9.2.0) を設定しています ...

設定ファイル '/etc/elasticsearch/jvm.options'
 ==> これはインストールしてから (あなたかスクリプトによって) 変更されています。
 ==> パッケージ配布元が更新版を提供しています。
   どうしますか? 以下の選択肢があります:
    Y か I  : パッケージメンテナのバージョンをインストールする
    N か O  : 現在インストールされている自分のバージョンを残す
      D     : 両バージョンの差異を表示する
      Z     : 状況を調査するためにシェルを開始する
 デフォルトでは現在使っている自分のバージョンを残します。
*** jvm.options (Y/I/N/O/D/Z) [デフォルト=N] ? 

の質問が来た場合、N を確実に選んでください。手なりでのyは厳禁です。なぜなら、Growi設定時に

-Xms256m
-Xmx256m

の2行を追記しているからです。(※数値は環境によって異なりますが、Growi推奨値が256MBです)

この -Xms256m および -Xmx256m の2行は、Elasticsearch の JVMヒープメモリの初期サイズ(Xms)と最大サイズ(Xmx) を指定しています。これが削除される、つまりデフォルト設定に戻ると、Growi の安定性やパフォーマンスに以下のリスクがあります。

削除された場合のリスク

  1. メモリ不足によるクラッシュ
  • デフォルトではJVMが自動でヒープサイズを決定しますが、サーバのメモリ量や他プロセスの使用状況によっては、必要なメモリが確保されず、ElasticsearchがOutOfMemoryErrorで落ちる可能性があります。
  1. パフォーマンスの低下
  • 初期サイズが小さいと、ヒープの拡張が頻繁に発生し、GC(ガベージコレクション)による停止時間が増加します。これにより、Growiの検索やデータ取得が遅くなることがあります。

間違えてY/Iを選択してしまったら?

/etc/elasticsearch/jvm.options.dpkg-old

または

/etc/elasticsearch/jvm.options.dpkg-dist

にオリジナルが残されています。

sudo cp -pi /etc/elasticsearch/jvm.options.dpkg-old /etc/elasticsearch/jvm.options

または

sudo cp -pi /etc/elasticsearch/jvm.options.dpkg-dist /etc/elasticsearch/jvm.options

として復旧させましょう。

注意2:Growi依存プラグインによるサービス起動不可

そうして、アップデートが完了後、

Restarting services...
 systemctl restart elasticsearch.service
Warning: The unit file, source configuration file or drop-ins of elasticsearch.service changed on disk. Run 'systemctl daemon-reload' to reload units.


Job for elasticsearch.service failed because the control process exited with error code.
See "systemctl status elasticsearch.service" and "journalctl -xeu elasticsearch.service" for details.


systemctl status elasticsearch.service
failed (Result: exit-code)

と、failed (Result: exit-code)が出てサービスの再起動に失敗することがあります。この場合、Growiの全文検索は使うことができません。

原因:Growi固有のプラグイン

Growiインストール時、Elasticsearch用の

  • analysis-icu
  • analysis-kuromoji

のプラグインをインストールしています。これはパッケージと同時にアップグレードされません。Elasticsearchのコマンドで導入したパッケージのためです。

対処:再インストール

  • パッケージアンインストール
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin remove analysis-icu
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin remove analysis-kuromoji
  • パッケージ再インストール
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-icu
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-kuromoji

2つの注意点を対処後

まず、Run 'systemctl daemon-reload'と言われているので、

sudo systemctl daemon-reload

で更新されたデーモン設定ファイルを読み込みます。

次いで、

systemctl status elasticsearch.service

で、Elasticsearchが依然としてfailed (Result: exit-code)となっていることを確認。その後、

sudo systemctl restart elasticsearch.service

としてサービスを再起動。今度はエラーが起きないと思います。

systemctl status elasticsearch.service

でもactive/runningを確認しましょう。

Growi上での最終確認

  1. Growiに管理者権限でアクセスします。
  2. 設定>全文検索管理に進みます。

接続の状態 接続されていますと表示されていることを確認し、

トップページに戻って検索を開始。想定通りの検索結果が出れば対処完了です。

Page 3 of 274

Powered by WordPress & Theme by Anders Norén