踏み台サーバ不要?EC2 Run Commandだけでインスタンス管理するためのシェルスクリプトを書いてみた

2016.09.28

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

西澤です。EC2 Run Commandはとても便利なのですが、AWS Management Consoleからコマンド実行するのはイマイチだし、CUIでやるには覚えないといけないコマンドが多くて辛いなと思っていたので、シェルスクリプトで一発で実行できるようにしてみました。その内容について簡単にご紹介したいと思います。

EC2 Run Commandとは?

あまり有名なサービスでは無いので、SSMRun Commandを聞いたことが無い、という方は、まずこちらをご覧ください。概念的なところは、GUIも利用しながら操作した方が理解しやすいと思います。

事前準備

EC2 Run Commandを利用するに当たって必要な設定は、下記公式ドキュメントにも記載されていますのでご確認ください。

IAMロール準備

そのまま利用可能なAWS管理ポリシーが用意されていますので、こちらを利用しましょう。Run Command実行対象のEC2側には、AmazonEC2RoleforSSMポリシーを付与したIAMロールを割り当てておきましょう。

$ POLICY_ARN=arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
$ DEFUALT_POLICY_VERSION=$(aws iam list-policy-versions \
  --policy-arn ${POLICY_ARN} \
  --query "Versions[?IsDefaultVersion==\`true\`].VersionId" \
  --output text)
$ aws iam get-policy-version \
  --policy-arn ${POLICY_ARN} \
  --version-id ${DEFUALT_POLICY_VERSION}
{
    "PolicyVersion": {
        "CreateDate": "2015-10-23T22:12:37Z",
        "VersionId": "v2",
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": [
                        "ssm:DescribeAssociation",
                        "ssm:GetDocument",
                        "ssm:ListAssociations",
                        "ssm:UpdateAssociationStatus",
                        "ssm:UpdateInstanceInformation"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                },
                {
                    "Action": [
                        "ec2messages:AcknowledgeMessage",
                        "ec2messages:DeleteMessage",
                        "ec2messages:FailMessage",
                        "ec2messages:GetEndpoint",
                        "ec2messages:GetMessages",
                        "ec2messages:SendReply"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                },
                {
                    "Action": [
                        "cloudwatch:PutMetricData"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                },
                {
                    "Action": [
                        "ec2:DescribeInstanceStatus"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                },
                {
                    "Action": [
                        "ds:CreateComputer",
                        "ds:DescribeDirectories"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                },
                {
                    "Action": [
                        "logs:CreateLogGroup",
                        "logs:CreateLogStream",
                        "logs:DescribeLogGroups",
                        "logs:DescribeLogStreams",
                        "logs:PutLogEvents"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                },
                {
                    "Action": [
                        "s3:PutObject",
                        "s3:GetObject",
                        "s3:AbortMultipartUpload",
                        "s3:ListMultipartUploadParts",
                        "s3:ListBucketMultipartUploads"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                }
            ]
        },
        "IsDefaultVersion": true
    }
}

一方で、EC2 Run Commandを実行する担当者用としては、AmazonSSMFullAccessを利用すれば良いでしょう。実質的にEC2インスタンスの管理者権限を有していることになりますので、取扱については十分に注意してください。

$ POLICY_ARN=arn:aws:iam::aws:policy/AmazonSSMFullAccess
$ DEFUALT_POLICY_VERSION=$(aws iam list-policy-versions \
  --policy-arn ${POLICY_ARN} \
  --query "Versions[?IsDefaultVersion==\`true\`].VersionId" \
  --output text)
$ aws iam get-policy-version \
  --policy-arn ${POLICY_ARN} \
  --version-id ${DEFUALT_POLICY_VERSION}
{
    "PolicyVersion": {
        "CreateDate": "2016-03-07T21:09:12Z",
        "VersionId": "v2",
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": [
                        "cloudwatch:PutMetricData",
                        "ds:CreateComputer",
                        "ds:DescribeDirectories",
                        "ec2:DescribeInstanceStatus",
                        "logs:*",
                        "ssm:*",
                        "ec2messages:*"
                    ],
                    "Resource": "*",
                    "Effect": "Allow"
                }
            ]
        },
        "IsDefaultVersion": true
    }
}

SSM Agentの準備

Linux環境はデフォルトでは、SSM Agentが導入されていません。ちょうど本日のリリースでAmazon Linuxではyumによる導入が可能になりましたので、こちらを利用すれば簡単ですね。Windowsはデフォルトで導入されているEC2Configサービスを利用して動作する為、エージェントの導入等は不要です。

利用するドキュメントの確認

Run Commandから利用可能なコマンド群は、Documentという名前で管理されています。今回は、任意のコマンドを実行できる環境を作りたかったので、下記の2つのドキュメントを利用することにしました。

  • Windows用
    • AWS-RunPowerShellScript
  • Linux用
    • AWS-RunShellScript
$ aws ssm list-documents \
  --query "DocumentIdentifiers[?contains(Name,\`ShellScript\`)]"
[
    {
        "Owner": "Amazon",
        "Name": "AWS-RunPowerShellScript",
        "PlatformTypes": [
            "Windows"
        ]
    },
    {
        "Owner": "Amazon",
        "Name": "AWS-RunShellScript",
        "PlatformTypes": [
            "Linux"
        ]
    }
]
$ DOCNAME=AWS-RunPowerShellScript
$ aws ssm get-document \
  --name ${DOCNAME} \
  --query "Content" \
  --output text
{
    "schemaVersion":"1.2",
    "description":"Run a PowerShell script or specify the paths to scripts to run.",
    "parameters":{
        "commands":{
            "type":"StringList",
            "description":"(Required) Specify the commands to run or the paths to existing scripts on the instance.",
            "minItems":1,
            "displayType":"textarea"
        },
        "workingDirectory":{
            "type":"String",
            "default":"",
            "description":"(Optional) The path to the working directory on your instance.",
            "maxChars":4096
        },
        "executionTimeout":{
            "type":"String",
            "default":"3600",
            "description":"(Optional) The time in seconds for a command to be completed before it is considered to have failed. Default is 3600 (1 hour). Maximum is 28800 (8 hours).",
            "allowedPattern":"([1-9][0-9]{0,3})|(1[0-9]{1,4})|(2[0-7][0-9]{1,3})|(28[0-7][0-9]{1,2})|(28800)"
        }
    },
    "runtimeConfig":{
        "aws:runPowerShellScript":{
            "properties":[
                {
                    "id":"0.aws:runPowerShellScript",
                    "runCommand":"{{ commands }}",
                    "workingDirectory":"{{ workingDirectory }}",
                    "timeoutSeconds":"{{ executionTimeout }}"
                }
            ]
        }
    }
}
$ DOCNAME=AWS-RunShellScript
$ aws ssm get-document \
  --name ${DOCNAME} \
  --query "Content" \
  --output text
{
    "schemaVersion":"1.2",
    "description":"Run a shell script or specify the commands to run.",
    "parameters":{
        "commands":{
            "type":"StringList",
            "description":"(Required) Specify a shell script or a command to run.",
            "minItems":1,
            "displayType":"textarea"
        },
        "workingDirectory":{
            "type":"String",
            "default":"",
            "description":"(Optional) The path to the working directory on your instance.",
            "maxChars":4096
        },
        "executionTimeout":{
            "type":"String",
            "default":"3600",
            "description":"(Optional) The time in seconds for a command to complete before it is considered to have failed. Default is 3600 (1 hour). Maximum is 28800 (8 hours).",
            "allowedPattern":"([1-9][0-9]{0,3})|(1[0-9]{1,4})|(2[0-7][0-9]{1,3})|(28[0-7][0-9]{1,2})|(28800)"
        }
    },
    "runtimeConfig":{
        "aws:runShellScript":{
            "properties":[
                {
                    "id":"0.aws:runShellScript",
                    "runCommand":"{{ commands }}",
                    "workingDirectory":"{{ workingDirectory }}",
                    "timeoutSeconds":"{{ executionTimeout }}"
                }
            ]
        }
    }
}

SSM Agentとの疎通確認

下記コマンドでSSM Agentとの疎通確認を行うことができます。IAMロールが適切に割り当てられ、SSM Agentがインターネットと通信できる状態となっていれば、PingStatusOnlineとなっているはずです。

$ aws ssm describe-instance-information
{
    "InstanceInformationList": [
        {
            "IsLatestVersion": false,
            "ComputerName": "ip-10-200-1-103.us-west-2.compute.internal\n",
            "PingStatus": "Online",
            "InstanceId": "i-6445c27c",
            "IPAddress": "10.200.1.103",
            "ResourceType": "EC2Instance",
            "AgentVersion": "1.2.290.0",
            "PlatformVersion": "2016.09",
            "PlatformName": "Amazon Linux AMI",
            "PlatformType": "Linux",
            "LastPingDateTime": 1475043780.582
        },
        {
            "IsLatestVersion": false,
            "ComputerName": "ip-10-200-1-36.us-west-2.compute.internal\n",
            "PingStatus": "ConnectionLost",
            "InstanceId": "i-8e46c196",
            "IPAddress": "10.200.1.36",
            "ResourceType": "EC2Instance",
            "AgentVersion": "1.2.290.0",
            "PlatformVersion": "2016.09",
            "PlatformName": "Amazon Linux AMI",
            "PlatformType": "Linux",
            "LastPingDateTime": 1475039485.941
        },
        {
            "IsLatestVersion": true,
            "ComputerName": "WIN-HVTPS1N9GQA.WORKGROUP",
            "PingStatus": "Online",
            "InstanceId": "i-e20b8cfa",
            "ResourceType": "EC2Instance",
            "AgentVersion": "3.19.1153",
            "PlatformVersion": "6.3.9600",
            "PlatformName": "Windows Server 2012 R2 Standard",
            "PlatformType": "Windows",
            "LastPingDateTime": 1475043638.601
        }
    ]
}

EC2 Run Commandだけでインスタンス管理するシェルスクリプト

細かいところは割愛しますが、下記のような仕様としました。自己責任でご利用ください。おかしな点があればご指摘いただけると嬉しいです。

  • 実行対象インスタンスのNameタグと実行したいコマンドを引数で指定
  • 実行対象は1台のみ(send-commandコマンド自体は複数台に同時実行が可能)
  • 正常動作時はなるべく余計な情報を出力しない
  • タイムアウトは30秒(時間のかかる処理は想定しない)
  • 実行するコマンド内に記号がある場合の動作は全く考慮していません
#!/bin/bash

# Check Arguments
if [ $# -lt 2 ]; then
  echo "usage: $0 [EC2NAME] \"[COMMAND]\""
  exit 1
fi

# Set Variables
EC2NAME="$1"
COMMAND="$2"

# Check EC2 Instance ID
EC2ID=$(aws ec2 describe-instances \
  --query "Reservations[].Instances[?Tags[?Key==\`Name\`].Value|[0]==\`${EC2NAME}\`].[InstanceId]" \
  --output text)

NUMBER=$(echo ${EC2ID} | wc -w)
if [ ${NUMBER} -ge 2 ]; then
  echo "Two or more EC2 Instances with the name \"${EC2NAME}\" are found."
  exit 1
fi

if [ "${EC2ID}" = "" ]; then
  echo "EC2:\"${EC2NAME}\" is not found."
  exit 1
fi

echo "### InstanceID: ${EC2ID}"

# Check SSM Agent Status
PINGSTATUS=$(aws ssm describe-instance-information \
  --query "InstanceInformationList[?InstanceId==\`${EC2ID}\`].PingStatus" \
  --output text)

if [ "${PINGSTATUS}" != "Online" ]; then
  echo "SSM Agent on EC2:\"${EC2NAME}\" is not Online."
  exit 1
fi

# Check Platform Type
PLATFORM_TYPE=$(aws ssm describe-instance-information \
  --query "InstanceInformationList[?InstanceId==\`${EC2ID}\`].PlatformType" \
  --output text)
echo "### PlatformType: ${PLATFORM_TYPE}"

# Exec Command
TIMEOUT=30
if [ "${PLATFORM_TYPE}" = "Linux" ]; then
  DOCNAME=AWS-RunShellScript
elif [ "${PLATFORM_TYPE}" = "Windows" ]; then
  DOCNAME=AWS-RunPowerShellScript
else
  echo "PlatformType:\"${PLATFORM_TYPE}\" is not supported."
  exit 1
fi
echo "### DocumentName: ${DOCNAME}"
echo "### ExecCommand: \"${COMMAND}\""

COMMAND_ID=$(aws ssm send-command \
  --instance-ids ${EC2ID} \
  --document-name ${DOCNAME} \
  --timeout-seconds ${TIMEOUT} \
  --parameters commands="${COMMAND}",executionTimeout="${TIMEOUT}" \
  --query "Command.CommandId" \
  --output text)

# Get Command Result
while :
do
  RESULT=$(aws ssm list-command-invocations \
    --command-id ${COMMAND_ID} \
    --detail \
    --query "CommandInvocations[].[Status]" \
    --output text)
  if [ "${RESULT}" = "Pending" -o "${RESULT}" = "InProgress" ]; then
    echo "(Exec Status is now \"${RESULT}\". Waiting...)"
    sleep 5
  else
    echo "====================================================================="
    break
  fi
done

# Get Command Result
if [ "${RESULT}" = "Success" ]; then
  aws ssm list-command-invocations \
    --command-id ${COMMAND_ID} \
    --detail \
    --query "CommandInvocations[].CommandPlugins[].Output" \
    --output text
else
  echo "Command Failed. Check the output below."
    aws ssm list-command-invocations \
    --command-id ${COMMAND_ID} \
    --detail \
    --query "CommandInvocations[]"
fi

exit 0

下記は実行例です。敢えて出力は少なめにしているので、より詳細な情報が必要であればAWS Management Consoleからも確認してみてください。

$ ./ssm_run_command.sh
usage: ./ssm_run_command.sh "[EC2NAME]" "[COMMAND]"

$ ./ssm_run_command.sh ssmtest4 "uname -n"
EC2:"ssmtest4" is not found.

$ ./ssm_run_command.sh ssmtest2 "pwd"
### InstanceID: i-8e46c196
SSM Agent on EC2:"ssmtest2" is not Online.

$ ./ssm_run_command.sh ssmtest1 "hostname;date"
### InstanceID: i-6445c27c
### PlatformType: Linux
### DocumentName: AWS-RunShellScript
### ExecCommand: "hostname;date"
=====================================================================
ip-10-200-1-103
Wed Sep 28 05:42:15 UTC 2016

$ ./ssm_run_command.sh ssmtest3 "hostname;Get-Date"
### InstanceID: i-e20b8cfa
### PlatformType: Windows
### DocumentName: AWS-RunPowerShellScript
### ExecCommand: "hostname;Get-Date"
(Exec Status is now "Pending". Waiting...)
(Exec Status is now "InProgress". Waiting...)
=====================================================================
WIN-HVTPS1N9GQA
2016年9月28日 5:41:50

まとめ

今回ご紹介したスクリプトをベースにして、用途に合わせた形で作り変えてご活用いただけたら嬉しいです。繰り返しですが、自己責任でのご利用をお願いします。