はじめに
はじめまして。フューチャーインスペースの平井です。
昨今の円安でAWS使用料が増加したことにより、構成見直し等で費用削減を図っている方も多いと思います。
私の現場でも費用削減の一環として、先日Jenkinsで使用しているエージェントノード(※1)をオンデマンドインスタンスからスポットインスタンスに移行しました。
※1:昨今のマスター
やスレーブ
といったプログラミング用語置き換えの流れにより、Jenkinsもマスターノードをコントローラーノード、スレーブノードをエージェントノードへと改名したようです。
今回は移行時に工夫した点を紹介いたします。
環境/構成
- コントローラーノード
- OS:CentOS Linux release 7.7.1908 (Core)
- Jenkins: 2.190
- EC2 Plugin: 1.50.3
- エージェントノード
- OS:CentOS Linux release 7.7.1908 (Core)
- openjdk:1.8.0_232
前提
まず、元々オンデマンドインスタンスで動かしていたジョブについて説明します。
対象のジョブではプロジェクト開発支援のため日々ソースファイル解析が行われていました。
解析対象ソースはSubversionで管理されており、以下のような課題がありました。
- 取得処理の課題
- svnリポジトリが100超あり、65万ファイル、容量70GBと巨大
- ソース解析の中でファイル間の依存関係解決をするため、常に全量取得する必要がある
- ジョブはソースタイプ毎に分かれているため、ジョブ単位で取得するとストレージが肥大化する
そこで、処理短縮とストレージ節約を図るため、対および実現案を考えました。
- 対策
- svn checkoutしたワーキングディレクトリを保持してsvn updateにより差分で最新ソース取得し時間を短縮化させる
- svn checkout/updateするジョブを1つに制限し、複数ジョブで1つのworkspaceを直接参照する
- 実現案
- Shared Workspace Plugin(※2)を使って各ジョブで共有
- ※2:共有元ジョブのworkspaceををzipに固めてmasterノードに転送、共有先ジョブではmasterから転送してzip解凍するJenkinsプラグイン
- Copy Artifact Plugin(※3)を使ってworkspaceを成果物保存して各ジョブでコピー
- ※3:ジョブで作られたファイルを成果物としてmasterノードに転送、共有先ジョブではmasterから転送するJenkinsプラグイン
- 1つのsvn checkout/updateジョブのworkspaceをs3に転送して永続化、次回実行時はs3から取得してsvn update実行
- エージェントノード(オンデマンドインスタンス)でsvn checkout状態をEBSに永続化
- オンデマンドインスタンスはジョブ実行時のみ起動・停止
- Shared Workspace Plugin(※2)を使って各ジョブで共有
1,2は大量ファイルの場合転送コストが大きく、ストレージもジョブごとに消費するためコストメリットがなく却下。
3も1,2と同様転送コストが大きいことに加えSCMポーリングとの相性が悪いため却下。
4のオンデマンドインスタンス方式を採用していました。
スポットインスタンスへの移行
その後もオンデマンドインスタンスで運用していましたが、冒頭の通り費用削減の一環でスポットインスタンス移行を検討することとなりました。
結果、費用削減の他にもメリットがあることを確認し、本格的に移行する流れとなりました。
主なメリット
費用削減
インスタンスクラスにもよりますが、オンデマンドインスタンスと比較して約70%の割引を受けることができます。
参考:AWSコスト削減のためのスポットインスタンス活用術
また、オンデマンド・スポット共通ですが、高負荷な処理にエージェントノードを使うことでマスターノードのインスタンスタイプを小さくして運用できます。
つまり、コスト削減と高パフォーマンスの両取りが可能です。
構成のシンプル化
現在の構成では起動/停止ジョブでエージェントノード(オンデマンドインスタンス)を制御する必要がありました。
しかし移行後の構成ではAmazon EC2 Plugin(※4)でエージェントノード(スポットインスタンス)を制御できるため、構成がシンプルになりました。
※4:ジョブ実行時に自動でEC2を作成し、エージェントノードとして使用できるようにするプラグイン。EC2はジョブ終了後に自動で終了される。
- 現在の構成(ジョブによるエージェントノード制御)
- 移行後の構成(Amazon EC2 Pluginによるエージェントノード制御)
リタイアメント対応が不要
スポットインスタンスはジョブ実行毎に作成/終了されるため、オンデマンドインスタンスで意識する必要のあったEC2リタイアメント対応が不要となりました。
参考:インスタンスのリタイア
実装のポイント
checkout状態をEBSで永続化する構成のため、スポットインスタンスの場合は起動時にEBSをアタッチさせる必要があります。なお起動時のEBSアタッチについては起動テンプレートを使う方法がAWS公式から案内されています。
参考:起動時に永続的なセカンダリ EBS ボリュームを新しい EC2 Linux スポットインスタンスに自動的に添付するにはどうすればよいですか。
当初、Amazon EC2 Pluginに起動テンプレートを指定してスポットインスタンスを動かせれば、と考えたのですが、プラグインが起動テンプレートに対応していませんでした。
そこで起動テンプレート以外の方法を探り、最終的に同プラグインのinit scriptにEBSアタッチ処理を組み込んで実現できるか確認することとしました。
init scriptについて
init scriptではスポットインスタンス起動時に実行したい処理をshellで書くことができます。
今回は以下の通りとしました。
|
順に解説します。
EBSアタッチ処理
http://169.254.169.254/latest/meta-data/instance-id
(※5)でインスタンスIDを取得し、後続のEBSアタッチ処理に渡しています。
<ボリュームID>にはアタッチしたいEBSを指定します。また、sleep 10によりEBSアタッチが未完了のまま次処理に進むことを防いでいます。
※5:169.254.169.254
はAWSインスタンスメタデータサービスの固定IPです。
参考:インスタンスメタデータの取得## EBSアタッチ
INSTANCE_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id)
if [ -n ${INSTANCE_ID} ] ; then
aws ec2 attach-volume --volume-id <ボリュームID> --device /dev/sdb --instance-id ${INSTANCE_ID} --region ap-northeast-1
sleep 10
else
exit 1
fiEBSをjenkins_homeのパスにマウント
EBSのデバイス名には規則があり、追加順に/dev/nvme[0-26]n1
で採番されていきます。
参考:Linux インスタンスの Amazon EBS および NVMe
ルートボリュームのデバイス名は/dev/nvme0n1
のため、今回のアタッチ分は/dev/nvme1n1
となります。また、今回はjenkins_homeのパスを/data/jenkins_home
としたいため、/data
配下をマウントしました。## /dataマウント
UUID=$(lsblk -f | grep nvme1n1 | sed -e 's/ \+/ /g' |cut -f 3 -d " ")
if [ -n ${UUID} ] ; then
sudo mount UUID=${UUID} /data
else
exit 1
fi
## マウント確認
if ! mountpoint /data; then
exit 1
fi
スポットインスタンスへの変更
実際にエージェントノードをスポットインスタンスに変更し、想定通りの動きとなるか確認しました。
変更に伴い発生した作業を記載します。
AWS
エージェントノード用のIAM Role作成
エージェントノード内でEBSのアタッチを行うため、ec2:AttachVolume権限を付与したIAM Roleを作成しました。
エージェントノード用のセキュリティグループ作成
エージェントノード立ち上げ時、コントローラーノードからエージェントノードへssh接続する処理があるため、エージェントノードにSSH(22)を許可したセキュリティグループが必要となります。
今回は元々使用していたセキュリティグループを流用しました。
エージェントノード用のAMI作成
以下1~4を実施してAMIを作成しました。
1. Javaのインストール
エージェントノードは起動時にslave.jarを実行するためインストールしました。
2. AWS CLIのインストール
AWS CLIコマンドでEBSをアタッチするためインストールしました。
3. ジョブ実行用ユーザ作成
デフォルトユーザ(例:AL2ならec2-user)が使えるため作成しなくても良いのですが、既存のエージェントノードでJenkinsという専用ユーザを用意していたため今回も踏襲して作成しました。
4. visudo編集
ジョブ実行ユーザがNOPASSWDでmountコマンドを実行できるようにするため設定しました。
jenkins ALL=(ALL) NOPASSWD: /usr/bin/mount |
Jenkins
EC2 Pluginインストール
JenkinsにAmazon EC2 Pluginをインストールしておきます。
エージェントノードの起動設定
「Jenkinsの管理 → システムの設定」のクラウド項目で「Amazon EC2」を選択すると表示されます。
今回は意識して設定した項目を抜粋します。
AMI ID
- 上述の手順で作成したエージェント用のAMIを設定しました。
Instance Type
- エージェントを起動するインスタンスタイプを設定します。
今回は変更前と同タイプにしました。
- エージェントを起動するインスタンスタイプを設定します。
Use Spot Instance
- スポットインスタンスで起動させるためチェックを入れました。
Spot Max Bid Price
- スポットインスタンス起動に許容できる最高価格を設定します。
今回はできるだけ中断のリスクを避けたかったためデフォルト(オンデマンドインスタンスの価格)としました。
- スポットインスタンス起動に許容できる最高価格を設定します。
Security group names
- エージェントノード用のセキュリティグループを設定しました。
Remote FS root
- エージェントノードのjenkins_homeとなるパスを設定します。
今回はアタッチするEBSを/data配下にマウントするため、/data/jenkins_home/
としました。
- エージェントノードのjenkins_homeとなるパスを設定します。
Remote user
- エージェントノードにログインするユーザを設定します。今回はJenkinsユーザとしました。
Labels
- 一意なラベルを設定します。ここで指定したラベルを後述のジョブで設定します。
Init script
- EBSアタッチ処理を記載します。
## 初期化
INSTANCE_ID=
UUID=
## EBSアタッチ
INSTANCE_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id)
if [ -n ${INSTANCE_ID} ] ; then
aws ec2 attach-volume --volume-id <ボリュームID> --device /dev/sdb --instance-id ${INSTANCE_ID} --region ap-northeast-1
sleep 10
else
exit 1
fi
## /dataマウント
UUID=$(lsblk -f | grep nvme1n1 | sed -e 's/ \+/ /g' |cut -f 3 -d " ")
if [ -n ${UUID} ] ; then
sudo mount UUID=${UUID} /data
else
exit 1
fi
## マウント確認
if ! mountpoint /data; then
exit 1
fiNumber of Executors
- ジョブの並列度を指定します。今回は変更前と同じとしています。
IAM Instance Profile
- エージェントノード用に作成したIAM Roleを設定しました。
ジョブの設定
ジョブの設定 → 実行するノードを制限からさきほど設定したラベルを指定します。
動作確認
スポットインスタンスがEBSアタッチされた状態でエージェントノードとして起動するかを確認します。
なお一部情報はマスクしています。
まずログからスポットインスタンスが立ち上がることを確認します。
2 21, 2023 2:48:02 午後 情報 hudson.plugins.ec2.EC2Cloud$1 call
SlaveTemplate{ami='<AMI ID>', labels='<ラベル名>'} Node EC2 (<エージェントノード名>) - <インスタンス名> (<インスタンスID>) moved to RUNNING state in 5 seconds and is ready to be connected by Jenkins
2 21, 2023 2:48:03 午後 情報 hudson.plugins.ec2.EC2RetentionStrategy start
Start requested for EC2 (<エージェントノード名>) - <インスタンス名> (<インスタンスID>)
2 21, 2023 2:48:03 午後 情報 hudson.plugins.ec2.EC2Cloud log
Launching instance: <インスタンスID>立ち上がった段階ではルートボリュームのみ存在しています。
サーバ上でもルートボリューム(
/dev/nvme0n1p1
)のみであることが確認できます。[jenkins@<インスタンス名> ~]$ df -h
ファイルシス サイズ 使用 残り 使用% マウント位置
devtmpfs 16G 0 16G 0% /dev
tmpfs 16G 0 16G 0% /dev/shm
tmpfs 16G 17M 16G 1% /run
tmpfs 16G 0 16G 0% /sys/fs/cgroup
/dev/nvme0n1p1 30G 24G 6.1G 80% /
tmpfs 3.1G 0 3.1G 0% /run/user/1101
SSH接続後、init scriptが実行されます。
2 21, 2023 2:48:18 午後 情報 hudson.plugins.ec2.EC2Cloud log
Connected via SSH.
2 21, 2023 2:48:18 午後 情報 hudson.plugins.ec2.EC2Cloud log
Creating tmp directory (/tmp) if it does not exist
2 21, 2023 2:48:18 午後 情報 hudson.plugins.ec2.EC2Cloud log
Executing init scriptこのタイミングで追加するEBSのアタッチを確認できます。
サーバ上でも追加したEBSがデバイス
/dev/nvme1n1
として/data
にマウントされました。[jenkins@<インスタンス名> ~]$ df
ファイルシス 1K-ブロック 使用 使用可 使用% マウント位置
devtmpfs 16128016 0 16128016 0% /dev
tmpfs 16152604 0 16152604 0% /dev/shm
tmpfs 16152604 16940 16135664 1% /run
tmpfs 16152604 0 16152604 0% /sys/fs/cgroup
/dev/nvme0n1p1 31445996 25533000 5912996 82% /
tmpfs 3230524 0 3230524 0% /run/user/1101
tmpfs 3230524 0 3230524 0% /run/user/1301
tmpfs 3230524 0 3230524 0% /run/user/0
/dev/nvme1n1 104832000 62866356 41965644 60% /data
最後に
java -jar /tmp/remoting.jar -workDir /data/jenkins_home/
が実行され、スポットインスタンスの起動を確認できました。2 21, 2023 2:48:32 午後 情報 hudson.plugins.ec2.EC2Cloud log
Copying remoting.jar to: /tmp
2 21, 2023 2:48:32 午後 情報 hudson.plugins.ec2.EC2Cloud log
Launching remoting agent (via SSH client process): ssh -o StrictHostKeyChecking=no -i /tmp/ec2_8484693589346546536.pem jenkins@<エージェントノードIP> -p 22 java -jar /tmp/remoting.jar -workDir /data/jenkins_home/
2 21, 2023 2:48:37 午後 情報 hudson.slaves.CommandLauncher launch
agent launched for EC2 (<エージェントノード名>) - <インスタンス名> (<インスタンスID>)
おわりに
Amazon EC2 Pluginはinit scriptで自由に構成を変えられるので、ベースとなるAMIが1つで済むのも強みだと思いました。
また、今回は処理を直接init scriptに書きましたが、以下の方式にすれば処理内容をgitで管理することもできそうです。
- git管理している実行ファイルをgit clone
- git cloneした実行ファイルを呼び出す
今回の検証を足掛かりにし、引き続き構成改善をしていこうと思います。