AWS Systems Manager ランコマンドの出力を AWS CLI で一括で取得する

--details で詳細を出力するパターンと、S3 に出力した内容を取得してくるパターンがあります。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

コンバンハ、千葉(幸)です。

今回やりたいのはこういったことです。

  • マネジメントコンソール上で複数台を対象に AWS Systems Manager ランコマンドを実行した
  • 当該コマンドについて、それぞれのターゲットインスタンスからの出力を AWS CLI で一括で取得したい

そもそもの発端は以下エントリを書いたことでした。

ここではAWS-RunPowerShellScriptドキュメントをランコマンドで実行し、複数台の Windows インスタンスにインストールされた CloudWatch エージェントの番号を出力しています。この時は実行結果をマネジメントコンソール上で 1 台ずつ確認したので、AWS CLI で一括で確認したいと考えました。

で、上記のケースでいえば以下のようなコマンドで実現できました。

$ COMMAND_ID=xxxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxxxx
$ aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output text | awk 'BEGIN {print "InstanceId, InstanceName, Status, Output"} {
    print $1", "$2", "$3", "$4
    }' | column -t -s ',' | sed '/^ *$/d'
InstanceId            InstanceName   Status    Output
i-0xxxxxxxxxxxxxxxx   HOSTNAME-A     Success   1.247349.0b251399
i-0xxxxxxxxxxxxxxxx   HOSTNAME-B     Success   1.247348.0b251302
i-0xxxxxxxxxxxxxxxx   HOSTNAME-C     Success   1.247347.4b250525
i-0xxxxxxxxxxxxxxxx   HOSTNAME-D     Success   1.247347.6b250880
i-0xxxxxxxxxxxxxxxx   HOSTNAME-E     Success   1.247349.0b251399
i-0xxxxxxxxxxxxxxxx   HOSTNAME-F     Success   1.231221.0
...

aws ssm list-command-invocationsをうまく使うことで大体のケースに対応できたのですが、少し注意点もあったので何パターンかご紹介します。

前提知識

  • AWS Systems Manager ランコマンドの出力には「標準出力」と「標準エラー出力」がある
  • ランコマンドの出力を AWS CLI で取得する際には以下のコマンドが候補となる
  • list-command-invocationsで出力を確認するためには--detailsオプションを付与する必要がある
  • list-command-invocationsでは標準出力と標準エラー出力が一つの項目として出力される
  • list-command-invocationsではプラグインごとに出力が分かれることがある

このあたりの詳細は以下エントリをご参照ください。

今回はすべてのパターンでlist-command-invocationsを用います。また、取得したいのは標準出力のみであるとします。(細部をカスタマイズすれば標準エラー出力も取得できます。)

実行環境

今回のコマンドは手元の Mac 端末と AWS CloudShell で動作確認をしています。

主要なコマンドのバージョンをそれぞれ記しておきます。

  • AWS CLI
  • awk
  • sed

Mac 端末の場合。

$ aws --version
aws-cli/2.9.1 Python/3.9.11 Darwin/22.1.0 exe/x86_64 prompt/off

$ awk --version
awk version 20200816

$ sed --version #BSD版であることを表すらしい
sed: illegal option -- -
usage: sed script [-Ealnru] [-i extension] [file ...]
	sed [-Ealnu] [-i extension] [-e script] ... [-f script_file] ... [file ...]

AWS CloudShell の場合。

$ aws --version
aws-cli/2.9.10 Python/3.9.11 Linux/4.14.301-224.520.amzn2.x86_64 exec-env/CloudShell exe/x86_64.amzn.2 prompt/off

$ awk --version
GNU Awk 4.0.2
Copyright (C) 1989, 1991-2012 Free Software Foundation.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/.

$ sed --version
sed (GNU sed) 4.2.2
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Jay Fenlason, Tom Lord, Ken Pizzini,
and Paolo Bonzini.
GNU sed home page: <http://www.gnu.org/software/sed/>.
General help using GNU software: <http://www.gnu.org/gethelp/>.
E-mail bug reports to: <bug-sed@gnu.org>.
Be sure to include the word ``sed'' somewhere in the ``Subject:'' field.

ランコマンドの出力を AWS CLI で一括で取得するパターン

大まかな考え方は変わらないのですが、細部を微妙に変えたパターンを用意しました

  • A. 単純に table 出力するパターン
  • B. 単純に json 出力するパターン
  • C. awk をかませるパターン
  • D. awk をかませて S3 からストリーミングダウンロードするパターン

以下の要素によってどのパターンを選ぶべきかが変わります。

  • 標準出力に改行が含まれるかどうか
  • 標準エラー出力があるかどうか

当てはめてみるとこのようになります。(:できるけど他のパターンがおすすめ、:おすすめ)

# 標準出力 標準エラー出力 A B C D
1 改行なし あり
2 改行なし なし
3 改行あり あり
4 改行あり なし

B の json 出力パターンは大抵の場合で使えるので汎用性があります。個人的なお気に入りはパターン D ですが、使い所は少し限られます。また、D パターンはランコマンド実行時の「S3 バケットへの書き込み」を有効にしていることが前提となります。

ランコマンドの実行例

今回は以下のようなランコマンドを実行したケースを想定します。

aws ssm send-command\
    --document-name "AWS-RunShellScript"\
    --document-version "1"\
    --targets '[{"Key":"InstanceIds","Values":["i-02148b21728321265","i-04f6c32b1d9c46916","i-0b14c40e0479d0009"]}]'\
    --parameters '{"workingDirectory":[""],"executionTimeout":["3600"],"commands":["#!/bin/bash","curl http://169.254.169.254/latest/meta-data/local-ipv4"]}'\
    --timeout-seconds 600 --max-concurrency "50" --max-errors "0"\
    --output-s3-bucket-name "chiba-ssm-log" --output-s3-key-prefix "hogehoge"\
    --region ap-northeast-1

ドキュメントAWS-RunShellScriptで以下のコマンドを実行する内容です。

#!/bin/bash
curl http://169.254.169.254/latest/meta-data/local-ipv4

これにより標準出力には当該インスタンスの IP アドレスが、標準エラー出力には curl のプログレスバーが出力されます。

このランコマンドの結果をaws ssm list-command-invocationsで確認すると以下のような結果が得られます。

% COMMAND_ID=2ca57e61-f070-4803-b008-3050f8c15a5c
% aws ssm list-command-invocations --command-id $COMMAND_ID\
 --details
{
    "CommandInvocations": [
        {
            "CommandId": "2ca57e61-f070-4803-b008-3050f8c15a5c",
            "InstanceId": "i-0b14c40e0479d0009",
            "InstanceName": "ip-172-31-38-78.ap-northeast-1.compute.internal",
            "Comment": "",
            "DocumentName": "AWS-RunShellScript",
            "DocumentVersion": "1",
            "RequestedDateTime": "2022-12-28T03:18:14.983000+09:00",
            "Status": "Success",
            "StatusDetails": "Success",
            "StandardOutputUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/2ca57e61-f070-4803-b008-3050f8c15a5c/i-0b14c40e0479d0009/awsrunShellScript/0.awsrunShellScript/stdout",
            "StandardErrorUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/2ca57e61-f070-4803-b008-3050f8c15a5c/i-0b14c40e0479d0009/awsrunShellScript/0.awsrunShellScript/stderr",
            "CommandPlugins": [
                {
                    "Name": "aws:runShellScript",
                    "Status": "Success",
                    "StatusDetails": "Success",
                    "ResponseCode": 0,
                    "ResponseStartDateTime": "2022-12-28T03:18:15.183000+09:00",
                    "ResponseFinishDateTime": "2022-12-28T03:18:15.499000+09:00",
                    "Output": "172.31.38.78\n----------ERROR-------\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r100    12  100    12    0     0   8510      0 --:--:-- --:--:-- --:--:-- 12000\n",
                    "StandardOutputUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/2ca57e61-f070-4803-b008-3050f8c15a5c/i-0b14c40e0479d0009/awsrunShellScript/0.awsrunShellScript/stdout",
                    "StandardErrorUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/2ca57e61-f070-4803-b008-3050f8c15a5c/i-0b14c40e0479d0009/awsrunShellScript/0.awsrunShellScript/stderr",
                    "OutputS3Region": "ap-northeast-1",
                    "OutputS3BucketName": "chiba-ssm-log",
                    "OutputS3KeyPrefix": "hogehoge/2ca57e61-f070-4803-b008-3050f8c15a5c/i-0b14c40e0479d0009/awsrunShellScript"
                }
            ],
            "ServiceRole": "",
            "NotificationConfig": {
                "NotificationArn": "",
                "NotificationEvents": [],
                "NotificationType": ""
            },
            "CloudWatchOutputConfig": {
                "CloudWatchLogGroupName": "",
                "CloudWatchOutputEnabled": false
            }
        },
        {
        ...2台目の実行結果...
        },
        {
        ...3台目の実行結果...
        }
    ]
}

↑Output には標準出力と標準エラー出力が連結されて記録されています。

標準エラー出力が含まれないパターン

実行するコマンドを以下のように変更し、標準エラー出力が含まれないパターンのランコマンドも実行しておきます。

#!/bin/bash
curl http://169.254.169.254/latest/meta-data/local-ipv4 2> /dev/null

実行結果のイメージはこちらです。標準出力のみになっていることが分かります。

% COMMAND_ID=5431f2a8-1fc2-47b5-aca0-3abc55cac49d
% aws ssm list-command-invocations --command-id $COMMAND_ID\
 --details
{
    "CommandInvocations": [
        {
            "CommandId": "5431f2a8-1fc2-47b5-aca0-3abc55cac49d",
            "InstanceId": "i-0a9960e6e67d81a58",
            "InstanceName": "ip-172-31-32-131.ap-northeast-1.compute.internal",
            "Comment": "",
            "DocumentName": "AWS-RunShellScript",
            "DocumentVersion": "1",
            "RequestedDateTime": "2022-12-28T04:06:59.740000+09:00",
            "Status": "Success",
            "StatusDetails": "Success",
            "StandardOutputUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/5431f2a8-1fc2-47b5-aca0-3abc55cac49d/i-0a9960e6e67d81a58/awsrunShellScript/0.awsrunShellScript/stdout",
            "StandardErrorUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/5431f2a8-1fc2-47b5-aca0-3abc55cac49d/i-0a9960e6e67d81a58/awsrunShellScript/0.awsrunShellScript/stderr",
            "CommandPlugins": [
                {
                    "Name": "aws:runShellScript",
                    "Status": "Success",
                    "StatusDetails": "Success",
                    "ResponseCode": 0,
                    "ResponseStartDateTime": "2022-12-28T04:06:59.872000+09:00",
                    "ResponseFinishDateTime": "2022-12-28T04:07:00.004000+09:00",
                    "Output": "172.31.32.131",
                    "StandardOutputUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/5431f2a8-1fc2-47b5-aca0-3abc55cac49d/i-0a996
0e6e67d81a58/awsrunShellScript/0.awsrunShellScript/stdout",
                    "StandardErrorUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/hogehoge/5431f2a8-1fc2-47b5-aca0-3abc55cac49d/i-0a9960
e6e67d81a58/awsrunShellScript/0.awsrunShellScript/stderr",
                    "OutputS3Region": "ap-northeast-1",
                    "OutputS3BucketName": "chiba-ssm-log",
                    "OutputS3KeyPrefix": "hogehoge/5431f2a8-1fc2-47b5-aca0-3abc55cac49d/i-0a9960e6e67d81a58/awsrunShellScript"
                }
            ],
            "ServiceRole": "",
            "NotificationConfig": {
                "NotificationArn": "",
                "NotificationEvents": [],
                "NotificationType": ""
            },
            "CloudWatchOutputConfig": {
                "CloudWatchLogGroupName": "",
                "CloudWatchOutputEnabled": false
            }
        },
        {
        ...略...
        }
    ]
}

↑また、ここでの標準出力には末尾も含めて改行コードが含まれていないことが分かります。

A. 単純に table 出力するパターン

パターン A のコマンド例は以下です。

% aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output table

標準エラー出力なしパターンのランコマンドの結果を当てはめて実行したイメージは以下です。

% COMMAND_ID=5431f2a8-1fc2-47b5-aca0-3abc55cac49d
% aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output table
---------------------------------------------------------------------------------------------------------
|                                        ListCommandInvocations                                         |
+---------------------+----------------------------------------------------+----------+-----------------+
|  i-097c5c842f42b067a|  ip-172-31-38-156.ap-northeast-1.compute.internal  |  Success |  172.31.38.156  |
|  i-0a9960e6e67d81a58|  ip-172-31-32-131.ap-northeast-1.compute.internal  |  Success |  172.31.32.131  |
+---------------------+----------------------------------------------------+----------+-----------------+

AWS CLI だけで完結しているので分かりやすいと言えば分かりやすいですね。

B. 単純に json 出力するパターン

コマンド例は以下です。

% aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output json

標準エラー出力ありのランコマンドの実行結果を当てはめた例が以下です。

% COMMAND_ID=2ca57e61-f070-4803-b008-3050f8c15a5c
% aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output json
[
    [
        "i-02148b21728321265",
        "ip-172-31-40-136.ap-northeast-1.compute.internal",
        "Success",
        "172.31.40.136\n----------ERROR-------\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r100    13  100    13    0     0  10400      0 --:--:-- --:--:-- --:--:-- 13000\n"
    ],
    [
        "i-04f6c32b1d9c46916",
        "ip-172-31-42-204.ap-northeast-1.compute.internal",
        "Success",
        "172.31.42.204\n----------ERROR-------\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r100    13  100    13    0     0   9863      0 --:--:-- --:--:-- --:--:-- 13000\n"
    ],
    [
        "i-0b14c40e0479d0009",
        "ip-172-31-38-78.ap-northeast-1.compute.internal",
        "Success",
        "172.31.38.78\n----------ERROR-------\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n\r  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\r100    12  100    12    0     0   8510      0 --:--:-- --:--:-- --:--:-- 12000\n"
    ]
]

お世辞にも見やすいとは言えませんが、改行コードが含まれる場合は他のパターンが対応していない(表示が崩れる)ため、このパターンが一番安牌かと思います。

CommandPlugins の中身が複数ある場合

ここまではCommandPlugins[0].Outputのようにプラグインの1番目のみを表示する前提でコマンドを組んできました。ただ、中には複数中身が存在するものもあります。

ドキュメントAWS-ConfigureAWSPackageの実行例をみるとこのように 2 つ存在することが分かります。

% COMMAND_ID=1b85a43b-65f8-4f98-94e9-0489c9092a69
% aws ssm list-command-invocations --command-id $COMMAND_ID\
 --details
{
    "CommandInvocations": [
        {
            "CommandId": "1b85a43b-65f8-4f98-94e9-0489c9092a69",
            "InstanceId": "i-0a9960e6e67d81a58",
            "InstanceName": "ip-172-31-32-131.ap-northeast-1.compute.internal",
            "Comment": "",
            "DocumentName": "AWS-ConfigureAWSPackage",
            "DocumentVersion": "1",
            "RequestedDateTime": "2022-12-28T04:37:46.562000+09:00",
            "Status": "Success",
            "StatusDetails": "Success",
            "StandardOutputUrl": "",
            "StandardErrorUrl": "",
            "CommandPlugins": [
                {
                    "Name": "configurePackage",
                    "Status": "Success",
                    "StatusDetails": "Success",
                    "ResponseCode": 0,
                    "ResponseStartDateTime": "2022-12-28T04:37:46.703000+09:00",
                    "ResponseFinishDateTime": "2022-12-28T04:37:54.796000+09:00",
                    "Output": "Initiating arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247357.0b252275 install\nPlugin aws:runShellScript ResultStatus Success\ninstall output: Running sh install.sh\ncreate group cwagent, result: 0\ncreate user cwagent, result: 0\n\nSuccessfully installed arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247357.0b252275\n",
                    "StandardOutputUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/cwagent/1b85a43b-65f8-4f98-94e9-0489c9092a69/i-0a9960e6e67d81a58/awsconfigurePackage/configurePackage/stdout",
                    "StandardErrorUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/cwagent/1b85a43b-65f8-4f98-94e9-0489c9092a69/i-0a9960e6e67d81a58/awsconfigurePackage/configurePackage/stderr",
                    "OutputS3Region": "ap-northeast-1",
                    "OutputS3BucketName": "chiba-ssm-log",
                    "OutputS3KeyPrefix": "cwagent/1b85a43b-65f8-4f98-94e9-0489c9092a69/i-0a9960e6e67d81a58/awsconfigurePackage"
                },
                {
                    "Name": "createDownloadFolder",
                    "Status": "Success",
                    "StatusDetails": "Success",
                    "ResponseCode": 0,
                    "ResponseStartDateTime": "2022-12-28T04:37:46.703000+09:00",
                    "ResponseFinishDateTime": "2022-12-28T04:37:46.703000+09:00",
                    "Output": "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [platformType, Windows]'. Step name: createDownloadFolder",
                    "StandardOutputUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/cwagent/1b85a43b-65f8-4f98-94e9-0489c9092a69/i-0a9960e6e67d81a58/awsrunPowerShellScript/0.createDownloadFolder/stdout",
                    "StandardErrorUrl": "https://s3.ap-northeast-1.amazonaws.com/chiba-ssm-log/cwagent/1b85a43b-65f8-4f98-94e9-0489c9092a69/i-0a9960e6e67d81a58/awsrunPowerShellScript/0.createDownloadFolder/stderr",
                    "OutputS3Region": "ap-northeast-1",
                    "OutputS3BucketName": "chiba-ssm-log",
                    "OutputS3KeyPrefix": "cwagent/1b85a43b-65f8-4f98-94e9-0489c9092a69/i-0a9960e6e67d81a58/awsrunPowerShellScript"
                }
            ],
            "ServiceRole": "",
            "NotificationConfig": {
                "NotificationArn": "",
                "NotificationEvents": [],
                "NotificationType": ""
            },
            "CloudWatchOutputConfig": {
                "CloudWatchLogGroupName": "",
                "CloudWatchOutputEnabled": false
            }
        },
        {
        ...略...
        }
    ]
}

それぞれの出力を取得したい場合は、CommandPlugins[0].Outputとしていた部分をCommandPlugins[].Outputに変更します。

% COMMAND_ID=1b85a43b-65f8-4f98-94e9-0489c9092a69
% aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[].Output]" \
    --output json
[
    [
        "i-097c5c842f42b067a",
        "ip-172-31-38-156.ap-northeast-1.compute.internal",
        "Success",
        [
            "Initiating arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247357.0b252275 install\nPlugin aws:runShellScript ResultStatus Success\ninstall output: Running sh install.sh\ncreate group cwagent, result: 0\ncreate user cwagent, result: 0\n\nSuccessfully installed arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247357.0b252275\n",
            "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [platformType, Windows]'. Step name: createDownloadFolder"
        ]
    ],
    [
        "i-0a9960e6e67d81a58",
        "ip-172-31-32-131.ap-northeast-1.compute.internal",
        "Success",
        [
            "Initiating arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247357.0b252275 install\nPlugin aws:runShellScript ResultStatus Success\ninstall output: Running sh install.sh\ncreate group cwagent, result: 0\ncreate user cwagent, result: 0\n\nSuccessfully installed arn:aws:ssm:::package/AmazonCloudWatchAgent 1.247357.0b252275\n",
            "Step execution skipped due to unsatisfied preconditions: '\"StringEquals\": [platformType, Windows]'. Step name: createDownloadFolder"
        ]
    ]
]

「CommandPlugins の中身が複数ある場合」に対応しているのもパターン B のみとなります。

C. awk をかませるパターン

コマンド例は以下です。

% aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output text | awk 'BEGIN {print "InstanceId, InstanceName, Status, Output"} {
    print $1", "$2", "$3", "$4
    }' | column -t -s ','

使えるケースが「標準出力に改行なし」かつ「標準エラー出力なし」とパターン A と被るのですが、出力結果がおしゃれなのでこちらのパターンの方が好きです。

実行結果のイメージは以下の通り。

% COMMAND_ID=5431f2a8-1fc2-47b5-aca0-3abc55cac49d
% aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output text | awk 'BEGIN {print "InstanceId, InstanceName, Status, Output"} {
    print $1", "$2", "$3", "$4
    }' | column -t -s ','
InstanceId            InstanceName                                       Status    Output
i-097c5c842f42b067a   ip-172-31-38-156.ap-northeast-1.compute.internal   Success   172.31.38.156
i-0a9960e6e67d81a58   ip-172-31-32-131.ap-northeast-1.compute.internal   Success   172.31.32.131

大まかに以下の内容を行なっています。

  • テキスト形式で awk に値を渡す
  • awk の BEGIN でヘッダーを出力
  • awk で引数をカンマとスペース(", ")で区切って出力
  • column で 区切り文字にカンマを指定してテーブル形式に変換

標準出力の末尾に改行が含まれる場合

Cパターンを使えるケースとして「標準出力に改行なし」を挙げましたが、「改行が末尾にある」場合であれば一工夫すれば使えます。

例えば今回の出力は172.31.38.156などでしたが、これが172.31.38.156\nなどであったケースです。末尾に改行が含まれる場合、パターンCの出力結果に改行が挟まることになります。

以下のようにsed '/^ *$/d'で空白行を削除してあげれば綺麗に出力されます。

$ aws ssm list-command-invocations --command-id $COMMAND_ID --details\
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, CommandPlugins[0].Output]" \
    --output text | awk 'BEGIN {print "InstanceId, InstanceName, Status, Output"} {
    print $1", "$2", "$3", "$4
    }' | column -t -s ',' | sed '/^ *$/d'

冒頭で載せた「CloudWatch エージェントの番号を取得するランコマンド」の例でも、この末尾の改行を取り除くパターンを用いています。

D. awk をかませて S3 からストリーミングダウンロードするパターン

ここまでのパターンと異なり、ランコマンド実行時の「S3 バケットへの書き込み」が有効になっていることが前提です。

コマンドの例は以下です。

$ aws ssm list-command-invocations --command-id $COMMAND_ID \
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, StandardOutputUrl]" \
    --output text | awk 'BEGIN {print "InstanceId, InstanceName, Status, Output"} {
    "echo "$4" | sed -e \"s/.s3.*.com\//\" -e \"s/https/s3/\"" | getline s3uri
    "aws s3 cp "s3uri" -" | getline output
    print $1", "$2", "$3", "output
    }' | column -t -s ','

標準エラー出力が含まれるランコマンドの実行結果を当てはめてみるとこのような感じに。

% COMMAND_ID=2ca57e61-f070-4803-b008-3050f8c15a5c
% aws ssm list-command-invocations --command-id $COMMAND_ID \
    --query "sort_by(CommandInvocations, &InstanceId)[].[InstanceId, InstanceName, Status, StandardOutputUrl]" \
    --output text | awk 'BEGIN {print "InstanceId, InstanceName, Status, Output"} {
    "echo "$4" | sed -e \"s/.s3.*.com\//\" -e \"s/https/s3/\"" | getline s3uri
    "aws s3 cp "s3uri" -" | getline output
    print $1", "$2", "$3", "output
    }' | column -t -s ','
InstanceId            InstanceName                                       Status    Output
i-02148b21728321265   ip-172-31-40-136.ap-northeast-1.compute.internal   Success   172.31.40.136
i-04f6c32b1d9c46916   ip-172-31-42-204.ap-northeast-1.compute.internal   Success   172.31.42.204
i-0b14c40e0479d0009   ip-172-31-38-78.ap-northeast-1.compute.internal    Success   172.31.38.78

AWS CloudShell で実行した場合は以下のメッセージが出力されましたが、挙動としては問題ありませんでした。

awk: cmd. line:2: warning: escape sequence `\/' treated as plain `/'

このコマンドで実行している内容は大まかに以下です。

  • テキスト形式で awk に値を渡す
    • 出力そのものでなく出力の S3 オブジェクト URL を渡している
  • awk の BEGIN でヘッダーを出力
  • awk 内で 4 つ目の引数(オブジェクト URL)を sed で S3 URL に変換し、変数 s3uri に格納
  • awk 内で S3 URL を用いて aws s3 cp によるストリーミングダウンロードを実行し、変数 output に格納
  • awk で 1~3 の引数と変数 output をカンマとスペース(", ")で区切って出力
  • column で 区切り文字にカンマを指定してテーブル形式に変換

当初はaws ssm list-command-invocations--detailsオプションで Output の詳細が出力できることに気づかなかったので、このパターンが唯一の手法かと勘違いしていました。

awk でこんな風にゴニョゴニョできるんだなということに気づけたので、個人的にはお気に入りのパターンです。

sed で S3 のオブジェクト URL を S3 URL に変換する

aws s3 cp コマンドでは、aws s3 cp <S3 URI> -のように指定することでストリーミングダウンロード(オブジェクトそのものはダウンロードせず中身のみ取得する)ができます。

ここで指定する S3 URI はs3://から始まる S3 URL である必要があります。aws ssm list-command-invocationsで得られる URL はパス形式のオブジェクト URL であるため、sed で変換をかけています。

通常実行する際は以下のコマンドで実現できます。

% URL=https://s3.ap-northeast-1.amazonaws.com/{バケット名}/{プレフィックス}/{オブジェクト名}
% echo $URL | sed -e 's/s3.*.com\///' -e 's/https/s3/'
s3://{バケット名}/{プレフィックス}/{オブジェクト名}

awk の中で sed を用いる際には'でなく"を用いる必要があり、かつそれをエスケープしてあげる必要がありました。それに伴い少し構成を変えています。

# 普通にsedを行う場合
sed -e 's/s3.*.com\///' -e 's/https/s3/'
# awkの中で用いる場合
sed -e \"s/.s3.*.com\//\" -e \"s/https/s3/\"

ちなみに仮想ホスティング形式の URL を S3 URL に変換したい場合は以下のようにすればよさそうです。

% URL2=https://{バケット名}.s3.ap-northeast-1.amazonaws.com/{プレフィックス}/{オブジェクト名}
% echo $URL2 | sed -e 's/.s3.*.com//' -e 's/https/s3/'
s3://{バケット名}/{プレフィックス}/{オブジェクト名}

余談:コマンド実行も AWS CLI で行いたい場合

ここまでは「マネジメントコンソールで実行したランコマンドの結果を AWS CLI で確認する」前提で記載していましたが、ランコマンドの実行から AWS CLI で行いたいこともあるでしょう。

ランコマンドの実行はaws ssm send-commandで行います。その戻り値は以下のようなものなので……

{
    "Command": {
        "CommandId": "92853adf-ba41-4cd6-9a88-142d1EXAMPLE",
        "DocumentName": "AWS-RunShellScript",
        "DocumentVersion": "",
        "Comment": "echo HelloWorld",
        "ExpiresAfter": 1550181014.717,
        "Parameters": {
            "commands": [
                "echo HelloWorld"
            ]
        },
...

こんな感じでランコマンド ID が取得できそうです。

COMMAND_ID=$(aws ssm send-command {なんらかコマンドに応じたパラメータ} \
--query 'Command.CommandId' --output text)

終わりに

AWS Systems Manager ランコマンドの出力を AWS CLI で一括で取得する、という内容でした。

標準出力に改行が含まれているかどうか、標準エラー出力が存在しているかどうかによって有効なパターンが異なりますので、用途に合ったものを選択してください。もちろん細部はカスタマイズすることも可能です。

今回は見送りましたが、aws ssm get-command-invocationであれば標準出力と標準エラー出力が分かれて出力される、という違いもあります。そういった部分も含めてこのエントリが何らか参考になれば幸いです。

以上、 チバユキ (@batchicchi) がお送りしました。

参考

awk でゴニョゴニョする手法は以下エントリの内容で知り、参考にしました。