Tuesday, September 5, 2023

Ubuntu 20.04とsystemdで実現するJavaアプリケーションの自動起動と監視

クラウド環境、特にAWS EC2のような仮想サーバー上でアプリケーションを運用する際、予期せぬサーバーの再起動やプロセスの停止は避けて通れない課題です。特に、長時間稼働を前提とするJavaアプリケーションの場合、手動での再起動はサービスの可用性を著しく低下させます。この記事では、Ubuntu 20.04 LTS環境で標準的に採用されている初期化システム「systemd」を利用して、Javaアプリケーション(.jarファイル)を安定的に自動起動させ、万が一の停止時にも自動で復旧させるための実践的な手法を、基本的な概念から実運用レベルの設定まで詳細に解説します。

なぜsystemdなのか? 現代的サービス管理の核心

かつてのLinuxディストリビューションでは、SysVinitやUpstartといった初期化システムが使われていました。しかし、これらはスクリプトベースの逐次処理が基本であり、起動の遅さや複雑な依存関係の管理に課題を抱えていました。systemdは、これらの問題を解決するために設計された、よりモダンで強力なシステムおよびサービスマネージャーです。

systemdの主な特徴は以下の通りです:

  • 並列処理による高速な起動: 依存関係のないサービスを同時に起動することで、システムの起動時間を大幅に短縮します。
  • 宣言的なユニットファイル: サービスの起動方法、依存関係、実行ユーザー、再起動ポリシーなどを、シェルスクリプトの複雑なロジックではなく、`.service`という拡張子を持つシンプルな設定ファイル(ユニットファイル)で宣言的に定義します。
  • 強力な依存関係管理: 「サービスAはサービスBの後に起動する」「サービスCはネットワークがオンラインになってから起動する」といった複雑な依存関係を`After`, `Wants`, `Requires`などのディレクティブで直感的に管理できます。
  • 統合されたログ管理 (Journald): `journalctl`コマンド一つで、全サービスのログを横断的かつ構造的に確認できます。特定のサービスのログだけを追跡したり、特定の時間帯のログを抽出したりすることが容易です。
  • プロセスの監視と自動再起動: サービスのプロセスを常に監視し、クラッシュや予期せぬ終了が発生した場合に、定義されたポリシー(例:常に再起動、失敗時のみ再起動)に従って自動的にサービスを再起動させることができます。これが、本記事の核心的なテーマです。

Ubuntu 20.04において、systemdはOSの根幹をなす存在です。これを理解し活用することは、安定したサーバー運用に不可欠なスキルと言えるでしょう。

systemdユニットファイル(.service)の構造を解剖する

systemdでサービスを管理するには、「ユニットファイル」を作成します。ここでは、Javaアプリケーションを起動するための基本的なユニットファイルを例に、その構造を詳しく見ていきましょう。ユニットファイルは通常、/etc/systemd/system/ディレクトリに配置します。

例としてmy-app.serviceという名前のファイルを作成する場合を考えます。

[Unit]
Description=My Java Application Service
After=network-online.target mysql.service
Wants=mysql.service

[Service]
ExecStart=/usr/bin/java -jar /home/ubuntu/my-app-0.0.1-SNAPSHOT.jar
User=myappuser
Group=myappuser
WorkingDirectory=/home/ubuntu/
Restart=on-failure
RestartSec=10s

[Install]
WantedBy=multi-user.target

このファイルは3つのセクション([Unit], [Service], [Install])から構成されています。それぞれのセクションとディレクティブ(設定項目)が持つ意味を深く理解することが重要です。

[Unit] セクション:サービスのメタデータと依存関係

このセクションでは、サービスそのものの説明や、他のユニットとの関係性を定義します。

  • Description: このサービスが何であるかを人間が理解しやすくするための短い説明です。systemctl statusコマンドなどで表示され、管理の助けになります。
  • After: 起動順序を定義します。ここに指定されたユニットが起動完了してから、このユニットが起動を開始します。上記の例では、ネットワークが完全に利用可能になり(network-online.target)、かつMySQLサービス(mysql.service)が起動した後にmy-app.serviceが起動します。これは、アプリケーションが起動時にデータベース接続を必要とする場合に非常に重要です。
  • Wants: 弱い依存関係を定義します。ここに指定されたユニットも一緒に起動しようと試みますが、もし指定されたユニットの起動に失敗しても、このユニット自体の起動は続行されます。
  • Requires: 強い依存関係を定義します。ここに指定されたユニットが起動に失敗した場合、このユニットも起動しません。また、Requiresで指定したユニットが停止すると、このユニットも停止します。より厳密な依存関係が必要な場合に使用します。

[Service] セクション:サービスの実行動作の核心

このセクションは、サービスの具体的な起動方法、実行ユーザー、再起動ポリシーなど、動作の核心部分を定義します。

  • ExecStart: サービスを起動するために実行されるコマンドを絶対パスで指定します。これが最も重要なディレクティブです。
    • 元の例では /bin/bash -c "exec java -jar ..." のようにシェルを介していましたが、多くの場合、直接実行可能ファイルを指定する方がシンプルで効率的です。/usr/bin/java のようにフルパスで指定することが推奨されます。
    • Spring Bootアプリケーションなどでプロファイルを指定したい場合は、ExecStart=/usr/bin/java -jar -Dspring.profiles.active=prod /home/ubuntu/my-app.jar のように引数を追加します。
  • User, Group: サービスを実行するユーザーとグループを指定します。セキュリティのベストプラクティスとして、rootユーザーでアプリケーションを実行することは絶対に避けるべきです。専用の非特権ユーザー(例:myappuser)を作成し、そのユーザーで実行するように設定しましょう。これにより、万が一アプリケーションに脆弱性があった場合でも、被害を最小限に抑えることができます。
  • WorkingDirectory: コマンドを実行する際の作業ディレクトリを指定します。アプリケーションが設定ファイルやログファイルを相対パスで参照する場合、この設定は不可欠です。
  • Restart: プロセスが停止した際の再起動ポリシーを定義します。
    • no: (デフォルト) 再起動しません。
    • on-success: 正常終了した場合(終了コード0)にのみ再起動します。
    • on-failure: 異常終了した場合(終了コードが0以外)に再起動します。サービスのクラッシュからの自動復旧に最も一般的に使われます。
    • on-abnormal: シグナルによってプロセスが kill された場合など、クリーンでない形で終了した場合に再起動します。
    • always: 終了理由を問わず、常に再起動します。
    EC2サーバーの再起動後だけでなく、アプリケーション自体のエラーでプロセスが落ちた場合にも自動復旧させたいので、on-failureまたはalwaysが強く推奨されます。
  • RestartSec: 再起動を試みるまでの待機時間を指定します。データベースの再接続待ちや、短時間での再起動ループを防ぐために、5s10sのように適切な値を設定することが推奨されます。
  • Environment: サービスプロセスに渡す環境変数を設定します。Environment="DB_HOST=localhost" "DB_USER=produser"のように複数設定できます。パスワードなどの機密情報は、EnvironmentFileディレクティブで別ファイルから読み込む方が安全です。

[Install] セクション:サービスの有効化設定

このセクションは、systemctl enableコマンドが実行されたときに、このサービスをどのターゲットに組み込むかを定義します。

  • WantedBy: サービスを有効化するターゲットを指定します。multi-user.targetは、複数のユーザーがログインでき、ネットワークも利用可能な、サーバー環境における標準的なランレベル(動作モード)です。ここに設定することで、systemctl enable my-app.service を実行すると、システム起動時にmulti-user.targetが起動する過程で、このサービスも自動的に起動されるようになります。具体的には、/etc/systemd/system/multi-user.target.wants/ディレクトリに、このサービスファイルへのシンボリックリンクが作成されます。

実践ガイド:Javaアプリケーションのサービス化手順

それでは、理論を元に、実際にEC2 Ubuntu 20.04サーバー上でJavaアプリケーションをサービスとして登録し、自動起動させる手順をステップバイステップで見ていきましょう。

ステップ1:事前準備

まず、サービス化したいJavaアプリケーション(.jarファイル)と、それを実行するためのJavaランタイム(JRE/JDK)がサーバーにインストールされていることを確認します。

1. Javaのインストール確認:


java -version

もしインストールされていない場合は、OpenJDKをインストールします。


sudo apt update
sudo apt install default-jre

2. アプリケーション(.jarファイル)の配置:

ビルドした.jarファイルをサーバーの適切な場所に配置します。例えば、/home/ubuntu/my-app-0.0.1-SNAPSHOT.jar のように配置します。

ステップ2:専用ユーザーの作成(強く推奨)

セキュリティのため、アプリケーション実行用の専用ユーザーを作成します。ここではmyappuserという名前のユーザーを作成します。


# -r: システムアカウントとして作成
# -m: ホームディレクトリを作成
# -s /bin/false: このユーザーでログインシェルを無効化
sudo useradd -r -m -s /bin/false myappuser

次に、アプリケーションファイルと作業ディレクトリの所有者を、この新しいユーザーに変更します。


sudo chown -R myappuser:myappuser /home/ubuntu/my-app-0.0.1-SNAPSHOT.jar
# もし作業ディレクトリが別にあるなら、そちらも変更
# sudo chown -R myappuser:myappuser /var/lib/myapp

ステップ3:systemdユニットファイルの作成

/etc/systemd/system/ディレクトリに、サービス用のユニットファイルを作成します。ファイル名は[サービス名].serviceとします。ここではmy-app.serviceとします。


sudo nano /etc/systemd/system/my-app.service

エディタが開いたら、先ほど解説した内容を元に、自身の環境に合わせて設定を記述します。


[Unit]
Description=My Spring Boot Application
# ネットワークとデータベースの後に起動
After=network-online.target mysql.service

[Service]
# セキュリティのため専用ユーザーで実行
User=myappuser
Group=myappuser

# アプリケーションがファイルを読み書きする場合、作業ディレクトリの指定が重要
WorkingDirectory=/home/ubuntu

# 実行コマンド
ExecStart=/usr/bin/java -jar /home/ubuntu/my-app-0.0.1-SNAPSHOT.jar

# 成功時も失敗時も常に再起動
Restart=always
# 異常終了時は10秒後に再起動
RestartSec=10s

# 標準出力をJournaldにリダイレクト
StandardOutput=journal
StandardError=journal
# ログに識別子を付与
SyslogIdentifier=my-app

[Install]
WantedBy=multi-user.target

nanoエディタでの保存は Ctrl + O、終了は Ctrl + X です。

ステップ4:サービスの管理と操作

ユニットファイルを作成・編集した後は、systemdにその変更を認識させる必要があります。

1. systemdのリロード:


sudo systemctl daemon-reload

このコマンドは、ユニットファイルに何らかの変更を加えた後、必ず実行する必要があります。

2. サービスの有効化:

次に、システム起動時にサービスが自動的に起動するように「有効化」します。


sudo systemctl enable my-app.service

これを実行すると、[Install]セクションのWantedByで指定したターゲットへのシンボリックリンクが作成されます。

3. サービスの起動:

手動でサービスを今すぐ起動します。


sudo systemctl start my-app.service

4. サービスの状態確認:

サービスが正しく起動したか、現在の状態を確認します。これが最も重要なトラブルシューティングの第一歩です。


sudo systemctl status my-app.service

成功している場合の出力例:

● my-app.service - My Spring Boot Application
     Loaded: loaded (/etc/systemd/system/my-app.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-27 10:30:00 UTC; 5s ago
   Main PID: 12345 (java)
      Tasks: 42 (limit: 4662)
     Memory: 512.8M
     CGroup: /system.slice/my-app.service
             └─12345 /usr/bin/java -jar /home/ubuntu/my-app-0.0.1-SNAPSHOT.jar

Oct 27 10:30:00 ip-172-31-20-10 systemd[1]: Started My Spring Boot Application.
... (アプリケーションのログの一部が表示される)
  • Loaded: ユニットファイルが正しく読み込まれているか。enabledは自動起動が有効になっていることを示します。
  • Active: active (running)となっていれば成功です。もしinactive (dead)failedとなっている場合は、何らかの問題が発生しています。
  • Main PID: 実行されているJavaプロセスのIDです。
  • 下部には、サービスの最新ログが表示されます。エラーの原因を探る上で非常に有用です。

その他の主要なコマンド:

  • サービスを停止: sudo systemctl stop my-app.service
  • サービスを再起動: sudo systemctl restart my-app.service
  • 自動起動を無効化: sudo systemctl disable my-app.service

ステップ5:再起動テストとログ確認

最後に、設定が意図通りに機能するかを確認します。

1. サーバー再起動テスト:

EC2インスタンス自体を再起動して、ログイン後にサービスが自動的に立ち上がっているかを確認します。


sudo reboot

再起動後、再度SSHでログインし、systemctl status my-app.service を実行してactive (running)になっていることを確認します。

2. プロセス強制終了テスト:

Restart=on-failureRestart=always の設定が機能するかを確認するために、プロセスを意図的に停止させてみます。


# statusコマンドでPIDを確認 (例: 12345)
sudo kill -9 12345

その後、すぐにsystemctl status my-app.serviceを何度か実行してみてください。一瞬failed状態になった後、RestartSecで指定した秒数後に自動的に新しいプロセスIDでactive (running)状態に復帰するはずです。これにより、アプリケーションのクラッシュからの自己修復能力が確認できます。

トラブルシューティングとログ分析

サービスがうまく起動しない場合、その原因を特定するためにログを確認することが不可欠です。systemdは`journalctl`という強力なログツールを提供しています。

特定のサービスのログを表示:


journalctl -u my-app.service

このコマンドで、指定したサービスの起動から現在までのすべてのログを時系列で確認できます。エラーメッセージやスタックトレースが出力されていないか確認しましょう。

ログをリアルタイムで追跡 (tail -f のように):


journalctl -u my-app.service -f

直近100行のログを表示:


journalctl -u my-app.service -n 100

起動時のログに絞って表示:


journalctl -u my-app.service -b

よくあるエラーの原因:

  • パスの間違い: ExecStartで指定したJavaや.jarファイルのパスが間違っている。
  • 権限不足: Userで指定したユーザーが、.jarファイルやWorkingDirectoryへの読み取り・実行権限を持っていない。ls -lコマンドで権限を確認し、必要であればchmodchownで修正してください。
  • 設定ファイルエラー: アプリケーションが必要とする設定ファイルが見つからない、または内容が間違っている。WorkingDirectoryの指定が正しいか確認してください。
  • ポートの競合: アプリケーションが使用しようとしているポートが、既に別のプロセスによって使用されている。sudo netstat -tulpnコマンドでポートの使用状況を確認できます。

まとめ

本記事では、Ubuntu 20.04環境でsystemdを利用し、Javaアプリケーションを堅牢なサービスとして登録・管理する方法を網羅的に解説しました。単にサーバー起動時にコマンドを実行するだけでなく、専用ユーザーによるセキュアな実行、依存関係の明示、そして何よりもプロセスの常時監視と自動再起動という、本番環境に不可欠な要素をsystemdのユニットファイル一つで宣言的に実現できます。

複雑な監視スクリプトを自作することなく、OS標準の仕組みを最大限に活用することで、AWS EC2上でのアプリケーションの可用性と運用効率は飛躍的に向上します。ここに示したベストプラクティスを適用し、安定したサービス基盤を構築してください。


0 개의 댓글:

Post a Comment