Amazon EC2 の外からOSコマンドを実行させるには、AWS Systems Manager (SSM) の SendCommand を用いるのが常道だと思いますが、環境上の制約で使用できない場合もあるかと思います。そこでSSMを使用することなく、OSコマンドを実行させるシンプルな仕組みを作ってみます。
(sshでのコマンド実行は自明ですし、AWSに関係ないのでここでは触れません。)
Systems Manager(SSM)とSendCommandについて
先ずはSSMとSendCommandについて軽く触れます。
SSMのSendCommandとは、SSMのマネージドノードとなっているEC2に対して、EC2の外からOSコマンドを送って実行できます。
「外から送って」と書くと、EC2に外からコマンドが入ってくる形のように思えますが、実際にはEC2で稼働しているssm-agent がSSMのエンドポイントに対してポーリングしており、コマンドを取ってくる形になっていると思われます。
(そうでなければEC2へのインバウンド通信許可を必要とするはずですが、実際にはそんなことはしません。)
この構成を真似することにします。
構成の概要
上図の構成とします。SSMの位置にSQSを配置し、そこにOSコマンドを入れる。EC2はQueueを定期的に監視するシェルスクリプトを常駐させて、キューにコマンドが入っていればそれをフェッチして実行します。要するにssm-agentの簡易版で置き換えた形だというだけです。(がっかりした人はすみませんm(_ _)m )
以下、EC2とSQSの内容について簡単に述べておきます。
SQSについて
キューは以下の構成とします。
標準キューであるということと、メッセージ保持を1分にしています。あまり長くするとagent役のshellの障害時などにキューにコマンドがたまり、思わぬ昔のコマンドが実行されてしまうことを防いでいます。1分以上フェッチされなければそのコマンドは実行されませんが、そこは割り切っています。また、キューポリシーは適当に設定します。
EC2について
シェルが動けば何でもいいですが、上記SQSキューに対して、sqs:ReceiveMessage および sqs:DeleteMessage の権限が必要ですので、EC2にアタッチするIAMロールに付与しておきます。
ポーリングスクリプトとサービス化
ポーリングスクリプト
ポーリングシェルは以下のように作成してみました。シェルの詳細についてはこの話題の主題ではありませんので細かくは触れません。
#!/bin/bash # SQSキューURL (適宜変更) QUEUE_URL="https://sqs.ap-northeast-1.amazonaws.com/xxxxyyyyzzzz/TestQueue" # ログディレクトリとファイル設定 LOG_DIR="/var/log/command-execution" LOG_FILE="$LOG_DIR/command_execution.log" # ログディレクトリの作成 if [ ! -d "$LOG_DIR" ]; then sudo mkdir -p "$LOG_DIR" sudo chown ec2-user:ec2-user "$LOG_DIR" sudo chmod 755 "$LOG_DIR" fi # ログファイル作成 touch "$LOG_FILE" chmod 644 "$LOG_FILE" echo "[$(date)] SQS polling service started." >> "$LOG_FILE" while true; do # SQSからメッセージ取得 RESPONSE=$(aws sqs receive-message \ --queue-url "$QUEUE_URL" \ --max-number-of-messages 10 \ --wait-time-seconds 10 \ --output json 2>/dev/null ) # RESPONSEが空ならデフォルト値を設定 if [ -z "$RESPONSE" ]; then MESSAGE_COUNT=0 else MESSAGE_COUNT=$(echo "$RESPONSE" | jq '.Messages | length' 2>/dev/null) if [ -z "$MESSAGE_COUNT" ]; then MESSAGE_COUNT=0 fi fi if [ "$MESSAGE_COUNT" -gt 0 ]; then echo "$RESPONSE" | jq -c '.Messages[]' | while IFS= read -r MESSAGE; do # ReceiptHandleとBodyを安全に取得 RECEIPT_HANDLE=$(echo "$MESSAGE" | jq -r '.ReceiptHandle' 2>/dev/null) COMMAND=$(echo "$MESSAGE" | jq -r '.Body | @sh' 2>/dev/null | sed "s/^'//;s/'$//") # 無効メッセージ判定 if [ -z "$RECEIPT_HANDLE" ] || [ -z "$COMMAND" ]; then echo "[$(date)] Warning: Invalid message detected. Skipping..." | tee -a "$LOG_FILE" continue fi # コマンド実行 TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") echo "[$TIMESTAMP] Executing command: $COMMAND" | tee -a "$LOG_FILE" { echo "----- START: $COMMAND -----" eval "$COMMAND" EXIT_CODE=$? echo "Exit Code: $EXIT_CODE" echo "----- END: $COMMAND -----" } >> "$LOG_FILE" 2>&1 # メッセージ削除 DELETE_RESPONSE=$(aws sqs delete-message --queue-url "$QUEUE_URL" --receipt-handle "$RECEIPT_HANDLE" 2>&1) if [ $? -eq 0 ]; then echo "[$(date)] Command executed and message deleted." | tee -a "$LOG_FILE" else echo "[$(date)] Error deleting message: $DELETE_RESPONSE" | tee -a "$LOG_FILE" fi done fi sleep 5 done
5秒(sleep 5)ごとに指定のキューからメッセージの取得を試みます。この値はSQSの料金に影響します。
コマンド実行後はメッセージを削除しています。
また、受け取ったコマンドと実行結果をログに出力するようにしています。オプションでこのログをCloudWatch Logs に連携してもいいでしょう。配置場所は/usr/local/bin/sqs-polling.sh とします。パーミッションは適当につけておきます。
サービス化
上記スクリプトをサービス化するため、systemdのユニットファイルを作成します。
[Unit] Description=SQS Polling Service After=network.target [Service] ExecStart=/usr/local/bin/sqs-polling.sh Restart=no User=ec2-user Environment="PATH=/usr/bin:/usr/local/bin" [Install] WantedBy=multi-user.target
上記を/etc/systemd/system/sqs-polling.service として保存して、以下コマンドでサービス起動・確認します。active(running)を確認してください。
sudo systemctl daemon-reload sudo systemctl start sqs-polling.service sudo systemctl status sqs-polling.service
動作確認
ではキューにコマンドを置いてみます。簡単にSQSのマネコンから実行します。loggerコマンドで/var/log/messagesに文字列を吐き出してみます。
Feb 28 14:25:59 ip-10-0-0-210 sqs-polling.sh[5414]: [2025-02-28 14:25:59] Executing command: logger "hello . Tech Harmony!!!!!!!!!!!!!!!!!!!!!!!!!!" Feb 28 14:25:59 ip-10-0-0-210 ec2-user[5415]: hello . Tech Harmony!!!!!!!!!!!!!!!!!!!!!!!!!! Feb 28 14:26:01 ip-10-0-0-210 sqs-polling.sh[5422]: [Fri Feb 28 02:26:01 PM JST 2025] Command executed and message deleted.
シェルのログファイル
[2025-02-28 14:25:59] Executing command: logger "hello . Tech Harmony!!!!!!!!!!!!!!!!!!!!!!!!!!" ----- START: logger "hello . Tech Harmony!!!!!!!!!!!!!!!!!!!!!!!!!!" ----- Exit Code: 0 ----- END: logger "hello . Tech Harmony!!!!!!!!!!!!!!!!!!!!!!!!!!" ----- [Fri Feb 28 02:26:01 PM JST 2025] Command executed and message deleted.
となって動作が確認できました。
キューにコマンドを置くことができれば何でもいいので、例えばLambda関数が該当キューにOSコマンドをputすれば、Lambdaが間接的にEC2のOSコマンドを打ったことになります。
最後に
EC2から他のAWSサービスへの連携はAWS CLIなどを使えば容易なのですが、AWSサービスからEC2(の中)への連携は手段が限られているように感じましたので、少し考えて作成してみました。
要点は、EC2から外部の状態を取りに行く、という点です。ここではSQSを例にしましたが、S3などを考えても面白いでしょう。
(定期的にS3のあるオブジェクトの存在を確認し、存在すればダウンロードして(以下略))
やったことはssm-agentの超簡易版を作成したに過ぎませんが、そこまで複雑でないことはわかっていただけたと思います。
コマンドの結果をサービス側で知るにはもうちょっと考える必要はありそうです。
SSMが使えるならそちらを使うべきだとは思いますが、色々な制限でそれが叶わない場合の選択肢として、皆様の参考になれば幸いです。
コードの使用は自己責任でお願いいたします。