久方ぶりです。crowdworks.jpのSREチームに所属しています @ciloholic です。
この記事では、半年ほど行なっていた「RundeckのECS on Fargate化」の取り組みを紹介していきます。
まず、Rundeckとは
PagerDuty社が提供するOSSのジョブ管理ソフトウェアです。OSS版の「Rundeck Community」とEnterprise版の「PagerDuty Process Automation」が存在し、ジョブのスケジュール実行や処理失敗時の自動リトライ、処理の開始/終了/成功/失敗に対して通知フックが設定できます。SSH経由でもコマンドが実行できるため、複数サーバに渡ってジョブ実行が可能です。
なぜ、RundeckをFargate化したのか
crowdworks.jpでは、2017年頃からRundeck v2系を使用してきました。RundeckのマイナーバージョンアップやEOL対応などの各種メンテナンスは実施していましたが、外部からのアクセスが少ないバッチサーバだったこともあり、メジャーバージョンアップの優先度は低くなっていました。しばらくの間、Rundeck v2系のままで運用を続けていましたが、2022年3月にRundeck v4系がリリースされたこともあり、Rundeckをアップグレードすることになりました。
Rundeck本体のアップグレードと共に、インフラ構成やデプロイ周りの見直しも行うことにしました。現行のRundeckは、EC2内にdocker composeで構築されており、ジョブが起動していない時間帯を見計らって手動でデプロイを行なっていました。EC2自体もメンテナンスコストが高く、手順書を見ながら恐る恐るデプロイ作業していたため、RundeckのFargate化に踏み切りました。
また、RDS MySQL v5.7系のEOLが2023年10月までだったこともあり、同じタイミングでAurora MySQL v3系への切り替えも行いました。
インフラ構成とデプロイ方法
下記に新旧のインフラ構成図とデプロイ方法を記載します。
Before
EC2内にdocker composeで構築されており、Nginx/OAuth Proxy/Rundeckコンテナが起動しています。 デプロイは下記の手順を手動実行していました。
- RundeckのExecution ModeをPassiveに変更する
- Passiveに変更すると、実行中のジョブはそのまま、新規ジョブの起動が抑制されます
- 起動中のジョブが停止するまで待つ
- 起動中のジョブの停止が待てない場合、ジョブをキャンセルする(後でフォローする)
- 踏み台サーバを経由してRundeckサーバにSSHでログインする
- デプロイして再起動する
After
Fargate内にNginx/OAuth Proxy/Rundeckコンテナが起動しています。 デプロイは下記の手順がGitHub ActionsとECS Taskで実行されます。
- main/developブランチにマージする
- Dockerイメージをbuild、pushする
- RundeckのExecution ModeをPassiveに変更する
- 起動中のジョブが停止するまでポーリングする(約5分間)
- 起動中のジョブが全て停止したらデプロイする
- 起動中のジョブが全て停止しなかったら、起動中のジョブを諦めてデプロイする
- 諦めたジョブは、GitHub ActionsのJob Summariesへ記録する(後でフォローする)
- 起動中のRundeckコンテナを洗い替える(Execution ModeはActiveに戻っている)
RundeckのFargate化に向けたタスク
他にも色々と検討や対応を行ないましたが、主なタスクを紹介していきます。
- Rundeckの公式Dockerイメージの使用
- Rundeckプラグインの使用
- 保持期間が切れた実行ログの対応
- Rundeckのデプロイ方法
Rundeckの公式Dockerイメージの使用
現行のRundeckは、UbuntuにRundeckをインストールしたDockerイメージを自作していましたが、v3系から公式Dockerイメージが提供されていたため、初めはその公式Dockerイメージを使用しようと考えていました。
この頃、タイミングが悪いことにIntel MacをM1 Macに切り替えていました。ローカル環境でRundeckの公式Dockerイメージを起動してみましたが、ARM64上でAMD64用Dockerイメージをエミュレーションしている所為か、まともに起動できなかったため、追加でARM64対応も行う必要がありました。
RundeckのARM64対応に必要だったこと
RundeckのDockerfileを確認してみると、UbuntuにJavaとツール類をインストールしているだけのシンプル構成でした。Ubuntu、remco、tiniをARM64対応してベースイメージを作り直せば、Rundeck本体のコード類をコピーするだけで起動しそうでした。
Ubuntu
pullすれば自動的にARM64のDockerイメージが取得されるので対応不要。
remco
AMD64対応しかされていなかったが、Goのクロスコンパイルできるため、自前でARM64を生成した。
修正前
修正後
FROM golang:buster AS remco WORKDIR /src ENV REMCO_VERSION 0.12.3 RUN wget https://github.com/HeavyHorst/remco/archive/refs/tags/v$REMCO_VERSION.tar.gz && \ tar -zxvf v$REMCO_VERSION.tar.gz && \ cd remco-$REMCO_VERSION && \ make GOARCH=`go env GOARCH` && \ cp -pf bin/remco /src
COPY --from=remco /src/remco /usr/local/bin/remco
tini
既にARM64対応されていたので、成果物の取得先を切り替えた。
修正前
修正後
# Add Tini ENV TINI_VERSION v0.19.0 RUN ARCH=$(dpkg --print-architecture) && \ wget -q https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${ARCH} -O /tini && \ wget -q https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${ARCH}.asc -O /tini.asc RUN gpg --batch --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \ && gpg --batch --verify /tini.asc /tini RUN chmod +x /tini
Rundeckプラグインの使用
現行のRundeckと同様、v4系でも下記プラグインを使用しています。
- rundeck-ec2-nodes-plugin
- ノードでEC2を指定するために使用
- rundeck-s3-log-plugin
- 実行ログをS3に保存するために使用
EC2ではIAMインスタンスプロファイルを使用していましたが、Fargateではタスク定義で定義したIAMロールを使用する必要がありました。
rundeck-s3-log-plugin
はプラグイン内のJava SDKが自動的にタスク定義のIAMロールを使用してくれましたが、rundeck-ec2-nodes-plugin
はタスク定義のIAMロールを使用してくれなかったため、EC2ノードプラグイン用のIAMロールを作成し、Assume Role ARN
に直接指定しました。
保持期間が切れた実行ログの対応
Rundeck v4系では、新たにExecution History Cleanという機能が追加され、保持期間が切れた実行ログを削除できるようになりました。
当初、この機能を使用して保持期間が切れた実行ログを削除しようと考えていました。しかし、保持期限が切れた実行ログは、S3のライフサイクルでGlacierストレージクラスに移行するようにしており、ストレージクラスを復元しないと削除できなかったため、この機能での削除は諦めました。
最終的には、保持期間が切れた実行ログは削除せず、S3のライフサイクルでGlacierストレージクラスに移行するのみの対応となりました。
Rundeckのデプロイ方法
クラスタ構成の検討
Rundeckにもクラスタ構成ができるとドキュメントにあったので、デプロイに活かせると考えていたのですが、想像していたActive/Active構成
やActive/Standby構成
とは異なっていました。
クラスタ構成になったRundeckへのWebリクエストは、2つのノードに負荷分散するものの、ジョブ自体は最後にジョブ定義を編集したノードに所有され、ジョブのスケジューリングも同じノードで行われます。当然、ノード1に障害が発生すると、ノード1が所有しているジョブが実行されないことになります。この時、ノード1で実行されなかったジョブを自動でノード2へ移行し、再実行する仕組みが商用オプションで用意されています(Autotakeover)。
かなり癖が強いRundeckのクラスタ構成を活かしつつ、OSS版でデプロイの仕組みを作り込もうとすると、Rundeckのバージョンアップのタイミングで壊れそうなので、できるだけシンプルにRundeckを停止させた後にデプロイする方法を採用しました。
Rundeckのクラスタ構成については、下記URLを参考にさせていただきました。
https://h3poteto.hatenablog.com/entry/2019/07/25/165532
できるだけ安全にデプロイする
現行のRundeckをデプロイする際は、できるだけジョブが起動していない時間帯を見計らってExecution ModeをPassiveに変更し、全てのジョブが完了するのを待ってデプロイを行なっていました。デプロイを自動化するにあたり、この対応も自動化したいと考えていました。
下記がGitHub Actions上でのデプロイの流れです。
- [GitHub] main/developブランチにマージする
- [GitHub Actions] Dockerイメージをbuild、pushする
- [ECS Task] RundeckのExecution ModeをPassiveに変更する
rd system mode passive
- [ECS Task] 起動中のジョブが停止するまでポーリングする(約5分間)
- 起動中のジョブが全て停止したらデプロイする
rd executions list -p "*"
- 起動中のジョブが全て停止しなかったら、起動中のジョブを諦めてデプロイする
- 諦めたジョブは、GitHub ActionsのJob Summariesへ記録する(後でフォローする)
- 起動中のジョブが全て停止したらデプロイする
- [GitHub Actions] 起動中のRundeckコンテナを洗い替える(Execution ModeはActiveに戻っている)
運悪くロングランしているジョブがあると、後で再実行する等のフォローが必要になりますが、比較的ジョブ実行が少ない時間帯を狙ってデプロイするだけである程度は回避はできそうです。
ただし、デプロイするタイミングで本来発火すべきだったジョブを取りこぼしてしまう可能性があるため、注意が必要です。
ジョブ実行履歴の可視化
Rundeckの再起動やデプロイを行うため、その時間帯に重要なジョブが実行していないかを確認する必要があります。以前は、Rundeckのデータベースにあるジョブ実行履歴から、再起動やデプロイを行う時間帯のレコードを取得して確認していました。
毎回行われるジョブ実行履歴の確認が手間だったので、ジョブの実行履歴をGoogleカレンダーに登録するツールをGoで自作し、可視化するようにしました。
日次で前日分のジョブ実行履歴をGoogleカレンダーに登録するジョブを起動しています。全てのジョブ実行履歴をGoogleカレンダーに登録してしまうとイベントが密になって見づらいため、スケジュール実行の間隔が60分未満のジョブは登録しないようにしています。
まだ、Googleカレンダーに登録する機能しか作り込んでいませんが、何かの参考になれば幸いです。
誤デプロイ対策
mainにマージすると自動的に本番環境へデプロイされるため、間違ってジョブが大量に実行される時間帯や月次ジョブの実行中に誤デプロイしてしまうリスクがありました。そのリスクを軽減するため、PR作成時にGitHub Actionsがコメントを追加し、そのコメントをResolveしないとマージできないような仕組みを作成しました。
最後に
Rundeckの知見が無い状態からのバージョンアップとFargate化だったため、技術検証やジョブ定義の移行、動作確認を地道に進めていたら本番リリースまで半年もかかってしまいました。時間はかかってしまいましたが、Rundeck自体の技術的な負債を粗方解消できたり、デプロイが自動化できたおかげで運用コストを激減させることができました。
まだ、Rundeckのジョブ自体に負債が残っているので、これからも改善活動を続けていきたいと思います。
We're hiring!
crowdworks.jpでは、サービスが持続的に成長するために改善し続けるエンジニアを募集しています! ご興味ある方は、ぜひお気軽にご連絡ください。