AWS CLIからSSM Run Commandからコマンドを実行するときのコマンド定義を楽する方法を考えてみた

ヒアドキュメントを使いこなせば簡単にAWS CLIからSSM Run Commandを実行できる
2023.11.04

AWS CLIからSSM Run Commandを実行する時に渡すコマンドを定義するのがつらい

こんにちは、のんピ(@non____97)です。

皆さんはAWS CLIからSSM Run Commandを実行する時に渡すコマンドを定義するのがつらいと思ったことはありますか? 私はあります。

AWS CLIからSSM Run Commandを実行する際はsend-commandを叩きます。

この時、SSM Run CommandでAWS-RunShellScriptなどコマンド実行するドキュメントを選択する場合、実行するコマンドは--parametersで定義します。

--parameters (map)
The required and optional parameters specified in the document being run.
key -> (string)
value -> (list)
(string)

--parameters | send-command — AWS CLI 1.29.71 Command Reference

ここで、コマンド一行一行をリストで定義するというのが中々に大変です。

例えば、何かしら3つのコマンドを叩く場合は以下のようにcommandsのリストを渡してあげます。

$ aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters commands=["echo helloWorld","echo helloWorld2","echo helloWorld3"]

複数のコマンドを実行したいときに横に長くなるので、実際には以下のように叩くと思います。

aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters commands=["echo helloWorld",\
"echo helloWorld2",\
"echo helloWorld3"]

これを一行一行定義するのは中々骨が折れますし、変数を展開させる/させないでエスケープ処理をするのも大変です。また、コメント行も入れたとしても見づらいですね。

それらのお悩みを解決したい時は、ヒアドキュメントと配列を組み合わせて使ってみてください。

もう少し細かく分解すると以下のことを行います。

  • 空白やタブで配列の要素として区切られないように、デリミタを改行のみに変更
  • ヒアドキュメントで実行したいコマンドを定義
  • ヒアドキュメントの内容を配列として変数に代入
  • ヒアドキュメントの配列をJSON形式の配列に変換
  • JSON形式の配列を使用してSSM Run Commandを実行

実際に動かしながら紹介します。

いきなりまとめ

  • ヒアドキュメントの配列を活用することで見やすく定義できる
  • 変数を展開したいかどうかで、ヒアドキュメントを分割して定義
  • デリミタを一時的に改行のみにしよう
  • SendCommand APIのparametersで指定できる文字列の最大は100KB
    • ファイル連携をする場合はS3バケットを活用しよう
  • 複雑なシェルスクリプトを書きたい場合は、そのシェルスクリプトファイルをEC2インスタンス上に置いてSSM Run Commandからスクリプトファイルを実行する方が楽

やってみる

基本の形

実際に動かしながら確認をします。

まず、以下のようにcdpwdなど複数行のコマンドをSSM Run Commandで叩きます。実行したいコマンドはヒアドキュメントとして定義します。そして、ヒアドキュメントの内容を改行区切りの配列として読み込ませ、それを整形するといった形です。

(
# デリミタを改行のみに変更
IFS=$'\n';

# 実行したいコマンドを定義
commands=($(cat << 'EOF'
# ディレクトリ移動
cd /home/ec2-user

# ディレクトリ移動ができたことを確認
pwd

echo test
EOF
))

# 配列の要素が改行で区切られていることを確認
echo commands : "${commands[@]}"

# ヒアドキュメントの配列をJSONの配列に変換
json_commands=$(python -c \
  'import json,sys; print(json.dumps(sys.argv[1:]))' \
  "${commands[@]}"
)

# 変換後の配列の確認
echo json_commands : "${json_commands}"

# SSM Run Command を実行
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters "{\"commands\":${json_commands}}"
)

実行結果は以下のとおりです。正常に実行の受付が行われました。エスケープ周りを気にする必要がないので、非常に楽ですね。

commands : # ディレクトリ移動 cd /home/ec2-user # ディレクトリ移動ができたことを確認 pwd echo test
json_commands : ["# \u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u79fb\u52d5", "cd /home/ec2-user", "# \u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u79fb\u52d5\u304c\u3067\u304d\u305f\u3053\u3068\u3092\u78ba\u8a8d", "pwd", "echo test"]
{
    "Command": {
        "CommandId": "cb7f10ca-2cdc-48de-81ac-85b2f91300c2",
        "DocumentName": "AWS-RunShellScript",
        "DocumentVersion": "$DEFAULT",
        "Comment": "",
        "ExpiresAfter": "2023-11-03T13:13:45.875000+00:00",
        "Parameters": {
            "commands": [
                "# ディレクトリ移動",
                "cd /home/ec2-user",
                "# ディレクトリ移動ができたことを確認",
                "pwd",
                "echo test"
            ]
        },
        "InstanceIds": [
            "i-0a2ce926164e897c6"
        ],
        "Targets": [],
        "RequestedDateTime": "2023-11-03T11:13:45.875000+00:00",
        "Status": "Pending",
        "StatusDetails": "Pending",
        "OutputS3Region": "us-east-1",
        "OutputS3BucketName": "",
        "OutputS3KeyPrefix": "",
        "MaxConcurrency": "50",
        "MaxErrors": "0",
        "TargetCount": 1,
        "CompletedCount": 0,
        "ErrorCount": 0,
        "DeliveryTimedOutCount": 0,
        "ServiceRole": "",
        "NotificationConfig": {
            "NotificationArn": "",
            "NotificationEvents": [],
            "NotificationType": ""
        },
        "CloudWatchOutputConfig": {
            "CloudWatchLogGroupName": "",
            "CloudWatchOutputEnabled": false
        },
        "TimeoutSeconds": 3600,
        "AlarmConfiguration": {
            "IgnorePollAlarmFailure": false,
            "Alarms": []
        },
        "TriggeredAlarms": []
    }
}

SSM Run Commandの実行結果を確認すると、意図したような出力がされていました。

/home/ec2-user
test

ヒアドキュメント内の書きっぷりは普段と同じで問題ありません。バックラッシュでコマンドを複数行に渡って記述しても問題ありません。

(
# デリミタを改行のみに変更
IFS=$'\n';

# 実行したいコマンドを定義
commands=($(cat << 'EOF'
# SSM Run CommandのPIDを取得
run_command_pid="$PPID"

# PIDの確認
echo PPID : "$run_command_pid"

# Command IDをSSM Agentのログから抽出できるまでループ
while true; do
  command_id=$(grep "\"Pid\":$run_command_pid" /var/log/amazon/ssm/amazon-ssm-agent.log \
    | awk '{print $5}' \
    | tr -d []
  )

  if [[ -n "$command_id" ]]; then
    break
  else
    sleep 1
  fi
done

# Command IDの出力
echo Command ID : "$command_id"
EOF
))

# 配列の要素が改行で区切られていることを確認
echo commands : "${commands[@]}"

# ヒアドキュメントの配列をJSONの配列に変換
json_commands=$(python -c \
  'import json,sys; print(json.dumps(sys.argv[1:]))' \
  "${commands[@]}"
)

# 変換後の配列の確認
echo json_commands : "${json_commands}"

# SSM Run Command を実行
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters "{\"commands\":${json_commands}}"
)

# 以降はコマンド実行時の出力

commands : # SSM Run CommandのPIDを取得 run_command_pid="$PPID" # PIDの確認 echo PPID : "$run_command_pid" # Command IDをSSM Agentのログから抽出できるまでループ while true; do   command_id=$(grep "\"Pid\":$run_command_pid" /var/log/amazon/ssm/amazon-ssm-agent.log     | awk '{print $5}'     | tr -d []   )   if [[ -n "$command_id" ]]; then     break   else     sleep 1   fi done # Command IDの出力 echo Command ID : "$command_id"
json_commands : ["# SSM Run Command\u306ePID\u3092\u53d6\u5f97", "run_command_pid=\"$PPID\"", "# PID\u306e\u78ba\u8a8d", "echo PPID : \"$run_command_pid\"", "# Command ID\u3092SSM Agent\u306e\u30ed\u30b0\u304b\u3089\u62bd\u51fa\u3067\u304d\u308b\u307e\u3067\u30eb\u30fc\u30d7", "while true; do", "  command_id=$(grep \"\\\"Pid\\\":$run_command_pid\" /var/log/amazon/ssm/amazon-ssm-agent.log     | awk '{print $5}'     | tr -d []", "  )", "  if [[ -n \"$command_id\" ]]; then", "    break", "  else", "    sleep 1", "  fi", "done", "# Command ID\u306e\u51fa\u529b", "echo Command ID : \"$command_id\""]
{
    "Command": {
        "CommandId": "89af29ed-90c6-4af5-b618-d12439245959",
        "DocumentName": "AWS-RunShellScript",
        "DocumentVersion": "$DEFAULT",
        "Comment": "",
        "ExpiresAfter": "2023-11-03T13:31:19.068000+00:00",
        "Parameters": {
            "commands": [
                "# SSM Run CommandのPIDを取得",
                "run_command_pid=\"$PPID\"",
                "# PIDの確認",
                "echo PPID : \"$run_command_pid\"",
                "# Command IDをSSM Agentのログから抽出できるまでループ",
                "while true; do",
                "  command_id=$(grep \"\\\"Pid\\\":$run_command_pid\" /var/log/amazon/ssm/amazon-ssm-agent.log     | awk '{print $5}'     | tr -d []",
                "  )",
                "  if [[ -n \"$command_id\" ]]; then",
                "    break",
                "  else",
                "    sleep 1",
                "  fi",
                "done",
                "# Command IDの出力",
                "echo Command ID : \"$command_id\""
            ]
        },
        "InstanceIds": [
            "i-0a2ce926164e897c6"
        ],
        "Targets": [],
        "RequestedDateTime": "2023-11-03T11:31:19.068000+00:00",
        "Status": "Pending",
        "StatusDetails": "Pending",
        "OutputS3Region": "us-east-1",
        "OutputS3BucketName": "",
        "OutputS3KeyPrefix": "",
        "MaxConcurrency": "50",
        "MaxErrors": "0",
        "TargetCount": 1,
        "CompletedCount": 0,
        "ErrorCount": 0,
        "DeliveryTimedOutCount": 0,
        "ServiceRole": "",
        "NotificationConfig": {
            "NotificationArn": "",
            "NotificationEvents": [],
            "NotificationType": ""
        },
        "CloudWatchOutputConfig": {
            "CloudWatchLogGroupName": "",
            "CloudWatchOutputEnabled": false
        },
        "TimeoutSeconds": 3600,
        "AlarmConfiguration": {
            "IgnorePollAlarmFailure": false,
            "Alarms": []
        },
        "TriggeredAlarms": []
    }
}

実行結果は以下のとおりです。正しく動作していますね。

PPID : 1628
Command ID : 183b2284-ff94-4423-9e90-9d1d2a6528d3

AWS CLIで複数行のコマンドを定義するのはかなり大変だったので、非常に楽になりました。

ちなみに、ヒアドキュメントの配列をJSONの配列に変換しているときにPythonを使っているのは好みです。「jqはインストールされていないけどPythonはインストールされている」という場面が多いのでPythonを使っているというだけです。

変数展開

ローカルマシン内の変数を展開した上で、コマンド実行したい場面もあると思います。

その場合は、変数展開の有無でヒアドキュメントを分割することで対応可能です。

EOFなど終了文字列をシングルクォートまたはダブルクォートで囲まなければ変数展開されます。また、ヒアドキュメントに記載した一部のみ変数展開したくない場合は\$と変数の先頭にバックラスラッシュを付与してあげれば大丈夫です。

# テキストファイルを作成
tee word-list.txt << 'EOF' > /dev/null
hogehoge
fugafuga
foo
bar
EOF

# ファイルの内容を読み込み
word_list=$(cat word-list.txt)

(
# デリミタを改行のみに変更
IFS=$'\n';

# 実行したいコマンドを定義
commands=($(cat << 'EOF'
# SSM Run CommandのPIDを取得
run_command_pid="$PPID"

# PIDの確認
echo PPID : "$run_command_pid"

# Command IDをSSM Agentのログから抽出できるまでループ
while true; do
  command_id=$(grep "\"Pid\":$run_command_pid" /var/log/amazon/ssm/amazon-ssm-agent.log \
    | awk '{print $5}' \
    | tr -d []
  )

  if [[ -n "$command_id" ]]; then
    break
  else
    sleep 1
  fi
done

# Command IDの出力
echo Command ID : "$command_id"
EOF
))

commands+=($(cat << EOF
# ローカルで読み込んだファイルの内容を展開して、ターゲット内でファイルを作成
echo '$(echo "$word_list")' > word_list_\$run_command_pid.txt
EOF
))

commands+=($(cat << 'EOF'
# 作成したファイルの内容を読み込んで echo 
cat word_list_$run_command_pid.txt \
  | xargs -I {word} -P 4 -n 1 \
    echo {word}
EOF
))

# 配列の要素が改行で区切られていることを確認
echo commands : "${commands[@]}"

# ヒアドキュメントの配列をJSONの配列に変換
json_commands=$(python -c \
  'import json,sys; print(json.dumps(sys.argv[1:]))' \
  "${commands[@]}"
)

# 変換後の配列の確認
echo json_commands : "${json_commands}"

# SSM Run Command を実行
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters "{\"commands\":${json_commands}}"
)


# 以降はコマンド実行時の出力

commands : # SSM Run CommandのPIDを取得 run_command_pid="$PPID" # PIDの確認 echo PPID : "$run_command_pid" # Command IDをSSM Agentのログから抽出できるまでループ while true; do   command_id=$(grep "\"Pid\":$run_command_pid" /var/log/amazon/ssm/amazon-ssm-agent.log     | awk '{print $5}'     | tr -d []   )   if [[ -n "$command_id" ]]; then     break   else     sleep 1   fi done # Command IDの出力 echo Command ID : "$command_id" # ローカルで読み込んだファイルの内容を展開して、ターゲット内でファイルを作成 echo 'hogehoge fugafuga foo bar' > word_list_$run_command_pid.txt # 作成したファイルの内容を読み込んで echo  cat word_list_$run_command_pid.txt   | xargs -I {word} -P 4 -n 1     echo {word}
json_commands : ["# SSM Run Command\u306ePID\u3092\u53d6\u5f97", "run_command_pid=\"$PPID\"", "# PID\u306e\u78ba\u8a8d", "echo PPID : \"$run_command_pid\"", "# Command ID\u3092SSM Agent\u306e\u30ed\u30b0\u304b\u3089\u62bd\u51fa\u3067\u304d\u308b\u307e\u3067\u30eb\u30fc\u30d7", "while true; do", "  command_id=$(grep \"\\\"Pid\\\":$run_command_pid\" /var/log/amazon/ssm/amazon-ssm-agent.log     | awk '{print $5}'     | tr -d []", "  )", "  if [[ -n \"$command_id\" ]]; then", "    break", "  else", "    sleep 1", "  fi", "done", "# Command ID\u306e\u51fa\u529b", "echo Command ID : \"$command_id\"", "# \u30ed\u30fc\u30ab\u30eb\u3067\u8aad\u307f\u8fbc\u3093\u3060\u30d5\u30a1\u30a4\u30eb\u306e\u5185\u5bb9\u3092\u5c55\u958b\u3057\u3066\u3001\u30bf\u30fc\u30b2\u30c3\u30c8\u5185\u3067\u30d5\u30a1\u30a4\u30eb\u3092\u4f5c\u6210", "echo 'hogehoge", "fugafuga", "foo", "bar' > word_list_$run_command_pid.txt", "# \u4f5c\u6210\u3057\u305f\u30d5\u30a1\u30a4\u30eb\u306e\u5185\u5bb9\u3092\u8aad\u307f\u8fbc\u3093\u3067 echo ", "cat word_list_$run_command_pid.txt   | xargs -I {word} -P 4 -n 1     echo {word}"]
{
    "Command": {
        "CommandId": "3c16e6e8-2bd5-4b35-9994-aebe6bb51144",
        "DocumentName": "AWS-RunShellScript",
        "DocumentVersion": "$DEFAULT",
        "Comment": "",
        "ExpiresAfter": "2023-11-03T14:02:39.433000+00:00",
        "Parameters": {
            "commands": [
                "# SSM Run CommandのPIDを取得",
                "run_command_pid=\"$PPID\"",
                "# PIDの確認",
                "echo PPID : \"$run_command_pid\"",
                "# Command IDをSSM Agentのログから抽出できるまでループ",
                "while true; do",
                "  command_id=$(grep \"\\\"Pid\\\":$run_command_pid\" /var/log/amazon/ssm/amazon-ssm-agent.log     | awk '{print $5}'     | tr -d []",
                "  )",
                "  if [[ -n \"$command_id\" ]]; then",
                "    break",
                "  else",
                "    sleep 1",
                "  fi",
                "done",
                "# Command IDの出力",
                "echo Command ID : \"$command_id\"",
                "# ローカルで読み込んだファイルの内容を展開して、ターゲット内でファイルを作成",
                "echo 'hogehoge",
                "fugafuga",
                "foo",
                "bar' > word_list_$run_command_pid.txt",
                "# 作成したファイルの内容を読み込んで echo ",
                "cat word_list_$run_command_pid.txt   | xargs -I {word} -P 4 -n 1     echo {word}"
            ]
        },
        "InstanceIds": [
            "i-0a2ce926164e897c6"
        ],
        "Targets": [],
        "RequestedDateTime": "2023-11-03T12:02:39.433000+00:00",
        "Status": "Pending",
        "StatusDetails": "Pending",
        "OutputS3Region": "us-east-1",
        "OutputS3BucketName": "",
        "OutputS3KeyPrefix": "",
        "MaxConcurrency": "50",
        "MaxErrors": "0",
        "TargetCount": 1,
        "CompletedCount": 0,
        "ErrorCount": 0,
        "DeliveryTimedOutCount": 0,
        "ServiceRole": "",
        "NotificationConfig": {
            "NotificationArn": "",
            "NotificationEvents": [],
            "NotificationType": ""
        },
        "CloudWatchOutputConfig": {
            "CloudWatchLogGroupName": "",
            "CloudWatchOutputEnabled": false
        },
        "TimeoutSeconds": 3600,
        "AlarmConfiguration": {
            "IgnorePollAlarmFailure": false,
            "Alarms": []
        },
        "TriggeredAlarms": []
    }
}

実行結果は以下のとおりです。

PPID : 12636
Command ID : 3c16e6e8-2bd5-4b35-9994-aebe6bb51144
hogehoge
fugafuga
foo
bar

SSM Run Commandのパラメーターのペイロードの上限を確認してみた

64KiB

頑張ればSSM Run Commandで複雑なシェルスクリプトを実行することが可能であることを確認しました。

ここで気になるのはSSM Run Commandのパラメーターのペイロードの上限です。

SendCommand APIが出力する例外としてMaxDocumentSizeExceededがあり、64KBのドキュメントだとエラーとなるようです。

MaxDocumentSizeExceeded

The size limit of a document is 64 KB.

HTTP Status Code: 400

SendCommand - AWS Systems Manager

試しに64KiBのファイルを作成して、そのファイルの中身をコマンドのペイロードとして渡すことができるか確認してみます。

# テキストファイルを作成
dd if=/dev/urandom of=/dev/stdout bs=1k count=47 2>/dev/null \
  | base64 \
  | tr -d '\n' \
  > test_file.txt 

# ファイルの内容を読み込み
test_file=$(cat test_file.txt)

(
# デリミタを改行のみに変更
IFS=$'\n';

# 実行したいコマンドを定義
commands+=($(cat << EOF
# ローカルで読み込んだファイルの内容を展開して、ターゲット内でファイルを作成
echo '$(echo "$test_file")' > test_file.txt
EOF
))

commands+=($(cat << 'EOF'
# 作成したファイルの内容を読み込んで echo 
cat test_file.txt | wc -c
EOF
))

# ヒアドキュメントの配列をJSONの配列に変換
json_commands=$(python -c \
  'import json,sys; print(json.dumps(sys.argv[1:]))' \
  "${commands[@]}"
)

# SSM Run Command を実行
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters "{\"commands\":${json_commands}}"
)

# 以降はコマンド実行時の出力

{
    "Command": {
        "CommandId": "fbc22f62-c0b4-40e3-bf88-b671917e66a4",
        "DocumentName": "AWS-RunShellScript",
        "DocumentVersion": "$DEFAULT",
        "Comment": "",
        "ExpiresAfter": "2023-11-03T21:48:43.339000+00:00",
        "Parameters": {
            "commands": [
                "# ローカルで読み込んだファイルの内容を展開して、ターゲット内でファイルを作成",
                "echo 'RZZkgOEDm3X+..(中略)..VnnN7vw=' > test_file.txt",
                "# 作成したファイルの内容を読み込んで echo ",
                "cat test_file.txt | wc -c"
            ]
        },
        "InstanceIds": [
            "i-0a2ce926164e897c6"
        ],
        "Targets": [],
        "RequestedDateTime": "2023-11-03T19:48:43.339000+00:00",
        "Status": "Pending",
        "StatusDetails": "Pending",
        "OutputS3Region": "us-east-1",
        "OutputS3BucketName": "",
        "OutputS3KeyPrefix": "",
        "MaxConcurrency": "50",
        "MaxErrors": "0",
        "TargetCount": 1,
        "CompletedCount": 0,
        "ErrorCount": 0,
        "DeliveryTimedOutCount": 0,
        "ServiceRole": "",
        "NotificationConfig": {
            "NotificationArn": "",
            "NotificationEvents": [],
            "NotificationType": ""
        },
        "CloudWatchOutputConfig": {
            "CloudWatchLogGroupName": "",
            "CloudWatchOutputEnabled": false
        },
        "TimeoutSeconds": 3600,
        "AlarmConfiguration": {
            "IgnorePollAlarmFailure": false,
            "Alarms": []
        },
        "TriggeredAlarms": []
    }
}

なんだか正常に実行できていそうに見えます。

ローカルで生成したファイルのサイズが64KiBであることを確認します。

# テキストファイルのサイズの確認
$ cat test_file.txt | wc -c
65536

$ echo $(($(cat test_file.txt | wc -c) / 1024))
64

SSM RunCommandの実行結果を確認すると65537 Byteでした。増えた1 Byteはechoで末尾の改行出力を無効にしていなかったためのものなので、特に問題ないですね。

65537

あくまでSSM Documentのサイズが64KB以上の時にMaxDocumentSizeExceededが投げられるのでしょうか。

96KiB

続いて96KiBのファイルでチャレンジです。

# テキストファイルを作成
dd if=/dev/urandom of=/dev/stdout bs=1k count=72 2>/dev/null \
  | base64 \
  | tr -d '\n' \
  > test_file.txt 

# ファイルの内容を読み込み
test_file=$(cat test_file.txt)

(
# デリミタを改行のみに変更
IFS=$'\n';

# 実行したいコマンドを定義
commands+=($(cat << EOF
# ローカルで読み込んだファイルの内容を展開して、ターゲット内でファイルを作成
echo '$(echo -n "$test_file")' > test_file.txt
EOF
))

commands+=($(cat << 'EOF'
# 作成したファイルの内容を読み込んで echo 
cat test_file.txt | wc -c
EOF
))

# ヒアドキュメントの配列をJSONの配列に変換
json_commands=$(python -c \
  'import json,sys; print(json.dumps(sys.argv[1:]))' \
  "${commands[@]}"
)

# SSM Run Command を実行
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters "{\"commands\":${json_commands}}" \
  > /dev/null
)

SSM Run Commandの実行結果は以下の通りで、確かに96KiBでした。

98305

126KiB

続いて、126KiBです。

# テキストファイルを作成
dd if=/dev/urandom of=/dev/stdout bs=1k count=95 2>/dev/null \
  | base64 \
  | tr -d '\n' \
  > test_file.txt 

# ファイルの内容を読み込み
test_file=$(cat test_file.txt)

(
# デリミタを改行のみに変更
IFS=$'\n';

# 実行したいコマンドを定義
commands+=($(cat << EOF
# ローカルで読み込んだファイルの内容を展開して、ターゲット内でファイルを作成
echo '$(echo -n "$test_file")' > test_file.txt
EOF
))

commands+=($(cat << 'EOF'
# 作成したファイルの内容を読み込んで echo 
cat test_file.txt | wc -c
EOF
))

# ヒアドキュメントの配列をJSONの配列に変換
json_commands=$(python -c \
  'import json,sys; print(json.dumps(sys.argv[1:]))' \
  "${commands[@]}"
)

# SSM Run Command を実行
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters "{\"commands\":${json_commands}}" \
  > /dev/null
)

# 以降はコマンド実行時の出力

An error occurred (MaxDocumentSizeExceeded) when calling the SendCommand operation: The total size of your parameter(s) and document exceeds the 100KB limit.

MaxDocumentSizeExceededとして、上限が100KBであるというエラーを出力してくれました。

ということで、長すぎるファイルを連携する場合は一工夫が必要そうです。

なお、128KiBで試そうとすると、以下のようにPythonの引数として渡すことができないようでした。

-bash: /usr/bin/python: Argument list too long

サイズの大きいファイルの連携をする際はS3バケットにアップロードしよう

SSM Run Commandで実行したいスクリプトのみで100KBに到達することは中々ないと思いますが、ファイル連携だと簡単に到達しそうです。

サイズの大きいファイルを連携したい場合はS3バケットにアップロードして、ターゲットからダウンロードしてあげると良いでしょう。

こうすることで、SendCommand APIの100KB上限に到達するのを防ぐことができます。

試しに128KiBのファイルでやってみます。

# テキストファイルを作成
dd if=/dev/urandom of=/dev/stdout bs=1k count=96 2>/dev/null \
  | base64 \
  | tr -d '\n' \
  > test_file.txt 

# ファイルをS3バケットにアップロード
aws s3 cp test_file.txt s3://<S3バケット名>/

(
# デリミタを改行のみに変更
IFS=$'\n';

# 実行したいコマンドを定義
commands+=($(cat << 'EOF'
cd /home/ec2-user

# ファイルのダウンロード
aws s3 cp s3://<S3バケット名>/test_file.txt .

# ファイルサイズの確認
echo file sieze : $(($(cat test_file.txt | wc -c) / 1024))
EOF
))


# ヒアドキュメントの配列をJSONの配列に変換
json_commands=$(python -c \
  'import json,sys; print(json.dumps(sys.argv[1:]))' \
  "${commands[@]}"
)

# SSM Run Command を実行
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --instance-ids "i-0a2ce926164e897c6" \
    --parameters "{\"commands\":${json_commands}}" \
  > /dev/null
)

SSM Run Commandの実行結果は以下の通りです。

Completed 128.0 KiB/128.0 KiB (2.3 MiB/s) with 1 file(s) remaining
download: s3://<S3バケット名>/test_file.txt to ./test_file.txt
file sieze : 128

SSM Run Commandのペイロードにはファイルの中身を載せていないので、問題なく実行できました。

連携に使用したS3バケット内のオブジェクトの削除はSSM Run Command内で行っても良いですし、S3のライフサイクルポリシーで行う形でも良いと思います。

ターゲットが複数ある場合、前者だと1つのターゲットが処理した時点でオブジェクトが削除されてしまうため、個人的には後者が良いと考えます。

ヒアドキュメントを使いこなせば簡単にAWS CLIからSSM Run Commandを実行できる

AWS CLIからSSM Run Commandからコマンドを実行するときのコマンド定義を楽する方法を考えてみました。

ヒアドキュメントを使いこなせば、簡単にAWS CLIからSSM Run Commandを実行できることを紹介しました。

なお、複雑なシェルスクリプトを実行する場合においても、そのシェルスクリプトファイルをEC2インスタンス上に置いてSSM Run Commandからスクリプトファイルを実行する方が楽だったりします。

特にSSM Run Commandを実行する環境と連携する場合は、そのような方法を採用した方がSSM Run Commandのコマンド定義もスッキリします。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!