AWS EC2(特にEC2に限定した話ではないですが)上で、常駐するJava(およびScalaなどのJVM上で動作する言語)アプリケーションをデーモンとして実行するのに苦労したので残しておきます。
「いまさらEC2??」ってのは置いといて...
要件
- EC2上でJavaアプリケーションを常駐させたい。
- EC2インスタンスの起動時に、アプリケーションを自動的に開始させたい。
- EC2インスタンスの終了/削除時に、アプリケーションを安全に(実行中の処理が終了するのを待ってから)停止させたい。
- JavaアプリケーションのJVMプロセスが落ちたら、自動的に再起動させたい。
Javaアプリケーションの実装
- シャットダウンフック(java.lang.Runtime#addShutdownHook())を使って、プロセスが終了されたときに、実行中の処理がないかを確認して処理が完了するのを待つようにします。
- JavaならSpring Boot を使えば簡単(と思う)が、Spring Boot を使わない場合などは自前で実装する必要あり。
サービスのrcスクリプト
Javaアプリケーションを、Linuxサービスとして起動するためのrcスクリプトを作成します。
rcスクリプトにはいろいろな流儀がありますが、最低限このような感じで良いと思います。
(Init.d and Start Scripts for Scala/Java Server Apps で紹介されている内容をベースに少しカスタマイズしました)
- プロセスIDをファイルに書き出して、多重起動を防止します。
- EC2インスタンスの起動/停止時にサービスも自動的に起動/停止させるために、ロックファイルの作成が必要です。(参考:CentOS で OS のシャットダウンや再起動時に何らかの処理をさせたい)
- EC2のデフォルトタイムゾーンは通常
UTC
ですが、予期せずに変わることがあるので明示的に-Duser.timezone=UTC
を指定しておいたほうが安全です。 -
stop
でサービスを停止するときは、JVMプロセスを強制的にkillしないようにします。(強制的にkillすると、シャットダウンフックが実行されません)
/etc/init.d/example
#!/bin/bash
### BEGIN INIT INFO
# Provides: example
# chkconfig: 2345 99 99
# Required-Start: $local_fs $network $named networking
# Required-Stop: $local_fs $network $named networking
# Short-Description: example
# Description: example
### END INIT INFO
DAEMON_NAME=example
DAEMON_DIR=/home/ec2-user/example
MODULE_NAME=example-server
START_SCRIPT="java -Duser.timezone=UTC -jar $DAEMON_DIR/$MODULE_NAME.jar"
# プロセスIDファイルは /var/run 下に作成する
PID_FILE=/var/run/$DAEMON_NAME.pid
# ロックファイルは /var/lock/subsys 下に作成する
# ロックファイルを作成しておかないと、マシンをシャットダウンするときにサービスの停止が行われない
LOCK_FILE=/var/lock/subsys/$DAEMON_NAME
# ***********************************************
# ***********************************************
ARGS="" # optional start script arguments
DAEMON=$START_SCRIPT
# colors
red='\e[0;31m'
green='\e[0;32m'
yellow='\e[0;33m'
reset='\e[0m'
echoRed() { echo -e "${red}$1${reset}"; }
echoGreen() { echo -e "${green}$1${reset}"; }
echoYellow() { echo -e "${yellow}$1${reset}"; }
start() {
# $! で起動したプロセスのプロセスIDを退避しておく(あとでプロセスIDファイルに書き出す)
PID=`$DAEMON $ARGS > /dev/null 2>&1 & echo $!`
}
case "$1" in
start)
if [ -f $PID_FILE ]; then
PID=`cat $PID_FILE`
echo $PID
if [ -z "`ps axf | grep -w ${PID} | grep -v grep`" ]; then
start
else
echoYellow "Already running [$PID]"
exit 0
fi
else
start
fi
if [ -z $PID ]; then
echoRed "Failed starting"
exit 3
else
# サービスが正常に起動できたら、プロセスIDファイル、ロックファイルを作成する
echo $PID > $PID_FILE
touch $LOCK_FILE
echoGreen "Started [$PID]"
exit 0
fi
;;
status)
if [ -f $PID_FILE ]; then
PID=`cat $PID_FILE`
if [ -z "`ps axf | grep -w ${PID} | grep -v grep`" ]; then
echoRed "Not running (process dead but pidfile exists)"
exit 1
else
echoGreen "Running [$PID]"
exit 0
fi
else
echoRed "Not running"
exit 3
fi
;;
stop)
if [ -f $PID_FILE ]; then
PID=`cat $PID_FILE`
if [ -z "`ps axf | grep -w ${PID} | grep -v grep`" ]; then
echoRed "Not running (process dead but pidfile exists)"
exit 1
else
PID=`cat $PID_FILE`
# プロセスを停止するときは -9 で強制終了してはいけない
# 強制終了すると、シャットダウンフックが実行されず、実行中の処理が途中で終わってしまう
kill -HUP $PID
echoGreen "Stopped [$PID]"
# サービスが正常に停止できたら、プロセスIDファイルとロックファイルを削除する
rm -f $PID_FILE
rm -f $LOCK_FILE
exit 0
fi
else
echoRed "Not running (pid not found)"
exit 3
fi
;;
restart)
$0 stop
$0 start
;;
*)
echo "Usage: $0 {status|start|stop|restart}"
exit 1
esac
サービスとして登録
実装したアプリケーションモジュール、rcスクリプトをEC2インスタンスにデプロイします。
ここでは Cloud Formation でEC2を構築するテンプレートを例示します。
- EC2インスタンス起動時に、自動的にS3からモジュールを取得してデプロイ、アプリケーションを実行します。
- rcスクリプトを
chkconfig
に登録することで、EC2インスタンスの起動/停止時にアプリケーションも自動的に起動/停止されます。(参考:EC2で起動時やterminate時にシェルを実行する
)
EC2AutoScalingConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Metadata:
# CloudFormation:Init で、S3からリソースを取得できるようにするための設定
AWS::CloudFormation::Authentication:
S3AccessCreds:
type: S3
roleName:
Ref: EC2Role
buckets:
- モジュールなどを保存しているS3バケット名
# EC2インスタンスの初期化処理。S3からモジュールを取得してデプロイします。
AWS::CloudFormation::Init:
configSets:
Setup:
- PreDeploy
- DeployExampleServer
- PostDeploy
- StartApplication
# 前処理。必要なパッケージのインストールなど。AMIにしておいたほうが望ましいです。
PreDeploy:
packages:
yum:
java-1.8.0-openjdk-devel: []
awslogs: []
files:
/home/ec2-user/awslogs-agent-setup.py:
source: https://s3.amazonaws.com/aws-cloudwatch/downloads/latest/awslogs-agent-setup.py
mode: "000755"
/home/ec2-user/awslogs.conf:
content: |+
[general]
state_file = /var/lib/awslogs/agent-state
[/var/log/messages]
log_group_name = /example/ec2
log_stream_name = {instance_id}-sysmsg
file = /var/log/messages
datetime_format = %b %d %H:%M:%S
buffer_duration = 5000
initial_position = start_of_file
commands:
SetDefaultJVM:
command: alternatives --set java /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java
# アプリケーションのデプロイ
DeployExampleServer:
files:
# S3からモジュールを取得
/home/ec2-user/example/example-server.jar:
source:
Fn::Sub: https://s3-${AWS::Region}.amazonaws.com/モジュールなどを保存しているS3バケット名/example-server-1.0.0.jar
owner: ec2-user
group: ec2-user
# S3からrcスクリプトを取得
/home/ec2-user/example/example:
source:
Fn::Sub: https://s3-${AWS::Region}.amazonaws.com/モジュールなどを保存しているS3バケット名/example
owner: ec2-user
group: ec2-user
mode: "000755"
# アプリケーションが出力したログを、awslogs でCloudWatchに転送
/home/ec2-user/example/conf/awslogs.conf:
content: |+
[example]
log_group_name = /example/example-server
log_stream_name = {instance_id}-{{name}}
file = /root/logs/example.log
encoding = utf_8
datetime_format = %Y/%m/%d %H:%M:%S.%f
time_zone = UTC
buffer_duration = 5000
initial_position = start_of_file
multi_line_start_pattern = {datetime_format}
owner: ec2-user
group: ec2-user
# アプリケーションのJVMプロセスが落ちたときに、自動的に再起動させるcronスケジュール
/home/ec2-user/example/conf/cron.conf:
content: |+
*/5 * * * * /sbin/service example start > /dev/null 2>&1
owner: ec2-user
group: ec2-user
# commandsに複数のコマンドを記述する場合は、アルファベット順に実行されることに注意
commands:
# rcスクリプトを配置(/etc/init.d 下にシンボリックリンク)
A_RegisterService:
command: ln -s /home/ec2-user/example /etc/init.d/example
# chkconfigで追加して、EC2インスタンスの起動/停止時にアプリケーションが自動的に起動/停止するように設定
B_AddChkconfig:
command: chkconfig --add example && chkconfig example on
C_AppendAwsLogsConf:
command: cat /home/ec2-user/example/conf/awslogs.conf >> /home/ec2-user/awslogs.conf
D_AppendCrontab:
command: cat /home/ec2-user/example/conf/cron.conf >> /home/ec2-user/cron.conf
PostDeploy:
commands:
A_PipUpgrade:
command:
pip install --upgrade pip
B_ConfigureAwsLogs:
command:
Fn::Sub: python /home/ec2-user/awslogs-agent-setup.py -n -r ${AWS::Region} -c /home/ec2-user/awslogs.conf
C_ConfigureCron:
command: crontab /home/ec2-user/cron.conf
services:
sysvinit:
awslogs:
enabled: true
ensureRunning: true
crond:
enabled: true
ensureRunning: true
StartApplication:
commands:
A_ManagedContentsSynchronizer:
command: service example start
Properties:
:
UserData:
Fn::Base64:
Fn::Sub: |+
#!/bin/bash
# 起動したEC2インスタンスにわかりやすい名前を付ける
INSTANCE_ID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`
START_TIME=`date '+%Y%m%d%H%M'`
aws ec2 create-tags --region ${AWS::Region} --resources ${!INSTANCE_ID} --tags Key=\"Name\",Value=example-${!START_TIME}
# ---- Run cfn-init
yum -y update
yum update -y aws-cfn-bootstrap
/opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource EC2AutoScalingConfig --configsets Setup
以上です。