AWS Systems Manager Run CommandでFargate上のSpring Bootコンテナのスレッドダンプを取得する

とある事情で、AWS Fargate上で稼働しているSpring Bootコンテナのスレッドダンプを取得する必要があった。いくつか詰まる所があったのでメモしておく。

 

 

[Fargate上のコンテナでコマンドを実行したい]

こちらのブログを参考に、Systems Managerを利用してFargate上のコンテナでコマンドを実行することに決めた。

tech.smartcamp.co.jp

 

同じSSM Agentで動かせるため、2021年3月に登場したばかりのAmazon ECS Execを利用することも検討した。

しかし今回は、対象のコンテナが10個以上あり、すべてのコンテナにそれぞれ手動で接続するのが面倒だったため、残念ながら今回はECS Execの採用を見送った。

 

[SSMエージェントのセットアップ]

先ほどのブログを参考にすすめる。

tech.smartcamp.co.jp

 

Dockerfileに以下の行を追加する。

#ssm-agentのインストール
RUN wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb && \
    dpkg -i amazon-ssm-agent.deb && \
    rm -f amazon-ssm-agent.deb && \
    cp /etc/amazon/ssm/seelog.xml.template /etc/amazon/ssm/seelog.xml

#AWS CLIのインストール
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    ./aws/install && \
    rm -rf ./aws && \
    rm -f ./awscliv2.zip

 

つぎに、コンテナ起動時に実行するentrypoint.shに以下の修正を入れる。

if [ "$SSM_ACTIVATE" = "true" ]; then
 # アクティベーションの作成
  ACTIVATE_PARAMETERS=$(aws ssm create-activation \
    --default-instance-name "${SSM_INSTANCE_NAME}" \
    --description "${SSM_INSTANCE_NAME}" \
    --iam-role "service-role/AmazonEC2RunCommandRoleForManagedInstances" \
    --region "ap-northeast-1")
   
  export ACTIVATE_CODE=$(echo $ACTIVATE_PARAMETERS | jq -r .ActivationCode)
  export ACTIVATE_ID=$(echo $ACTIVATE_PARAMETERS | jq -r .ActivationId)

  # コンテナのマネージドインスタンスへの登録
  amazon-ssm-agent -register -code "${ACTIVATE_CODE}" -id "${ACTIVATE_ID}" -region "ap-northeast-1" -y

  # ssm-userからrootユーザーにスイッチするための権限付与
  echo "ssm-user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ssm-agent-users

  # SSMエージェントの登録
  nohup amazon-ssm-agent > /dev/null &
fi

 

SSMエージェントのセットアップが上手くできたか確認するために、AWS Systems Manager Session Managerで接続しようとしたところ、以下のエラーが出た。

f:id:kidani_a:20210501200743p:plain

 

日本語がわかりにくいので、英語で確認すると以下のように表示された。SSMエージェントのバージョンは正しいが、インスタンスの設定が足りないらしい。

f:id:kidani_a:20210501201422p:plain

 

エラーメッセージ末尾のLearn moreのリンクは以下のページになっていた。

Troubleshooting Session Manager - AWS Systems Manager

読み進めていくと、タスクのIAMロールに権限が足りていないようだった。

docs.aws.amazon.com

こちらのドキュメントを参考に、以下のIAMポリシーを追加した。Systems Managerの設定を確認したところ、セッションデータの暗号化が無効になっていたため、ドキュメントで指定されていたkms:Decryptの権限は不要と判断して削除した。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:UpdateInstanceInformation",
                "ssmmessages:CreateControlChannel",
                "ssmmessages:CreateDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:OpenDataChannel"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetEncryptionConfiguration"
            ],
            "Resource": "*"
        }
    ]
}

 

これでタスクにSession Managerを利用して接続し、コマンドを実行できるようになった。

 

[Spring Bootアプリのスレッドダンプを取得する]

スレッドダンプの取得方法は下記の記事を参考にした。

qiita.com

 

JDKに付属しているjstackコマンドを利用するとスレッドダンプを取得できるらしい。

Session Managerで接続して確認したところ、現在利用している環境のUbuntuベースのコンテナ内にはJREしかインストールされていなかった。そのため、以下のコマンドで別途JDKをインストールする必要がある。Spring BootアプリがJava8で動いていたので、JDKのバージョンはそれに合わせてある。

apt-get install -y openjdk-8-jdk

 

これでフルパスを指定しなくとも、jstackコマンドを利用できるようになった。

 

最終的に、AWS Systems Manager Run Commandで全コンテナから一括でスレッドダンプを取得することを考えると、JavaのプロセスIDをpsコマンドの結果から取り出す必要がある。

今回はPID=$(ps aux | grep tomcat | grep java | cut -d ' ' -f 9)と無理やりcutコマンドでプロセスIDを取得した。

 

正しくはawkコマンドを使い以下のようにすると良さそう。

PID=$(ps aux | grep tomcat | grep java | awk '{ print $2 }')

myokoym.hatenadiary.org

 

あとはjstackコマンドを利用してスレッドダンプをファイルに書き出すだけ。

ファイル名から、どのコンテナのいつ時点のスレッドダンプか分かるようにしておきたかったので、コンテナ内で利用できたアプリ名の環境変数APP_NAMEをファイル名に組み込んでいる。

FILENAME=/tmp/ThreadDump.$APP_NAME.$(date "+%Y%m%d-%H%M%S").txt
jstack $PID > $FILENAME

 

このままだと、各コンテナにファイルを集めにいかなければいけない。それは面倒なので、S3バケットを事前に作成しておき、AWS CLIを利用してS3にファイルをアップロードする。

aws s3 cp $FILENAME s3://my_threaddump_bucket

 

以上のコマンドをまとめると、こうなる。

# 事前準備:jstackコマンドをインストール
apt-get install -y openjdk-8-jdk

# スレッドダンプを取得
PID=$(ps aux | grep tomcat | grep java | awk '{ print $2 }')
FILENAME=/tmp/ThreadDump.$APP_NAME.$(date "+%Y%m%d-%H%M%S").txt
jstack $PID > $FILENAME

# S3にアップロード
aws s3 cp $FILENAME s3://my_threaddump_bucket

 

[Run Commandでシェルスクリプトを(再)実行する]

あとは、Systems Managerの画面で、「Run Command」メニューを選択し、AWS-RunShellScriptドキュメントを選択して、先ほど作成したシェルスクリプトをコピペする。

 

「Targets」のところで「Choose instances manually」をクリックして、対象のコンテナをぽちぽち選択したら、「Run」ボタンを押して実行するだけ。

それぞれのコンテナでシェルスクリプトが実行されて、指定したS3バケットにファイルがアップロードされてくるのを待っていると、最も遅いコンテナでも10秒かからずにスレッドダンプを取得できた。

 

このあと、実際のスレッドダンプを確認してもらったところ、いくつかのコンテナにリクエストを送ってからスレッドダンプを再取得するようにとの追加の依頼があった。

もしSession Managerとかで各コンテナに接続してスレッドダンプを取得する方法を採用していたら、ものすごく面倒なことになるところだ。

今回はSystems Manager Run Commandを利用しているので、以前実行したコマンドを同じコンテナに対して再実行できた。おかげでかなりの時間を節約することができた。

 

[まとめ]

  • Fargateで動くDockerコンテナにSSM AgentをセットアップしてSession Manager / Run Command / ECS Execで接続できるようにした
  • 複数コンテナで同じコマンドを実行するためにRun Commandを採用した
  • Run Commandは同じコンテナ群に対して同一コマンドの再実行をサポートしていたのが便利だった