aws-cli EC2 Run Command (Linux)

EC2 Run Commandとは
======
EC2インスタンスにssm-agentをインストールしておくことで、サーバへログインすることなく外部から任意のコマンドを実行出来ます。
以前はMS Windowsのみの対応でしたが、Linuxにも対応。みんなこれを待っていた！（はず）
外部から内部への通信は無くエージェント側からAWSの管理サーバへ通信を行うので内部から外部の通信しか発生しませんから、インスタンスが外部と通信可能な環境であれば動作します。
今回はサンプルとしてインストールしたnginx Webサーバを外部から停止させ、ログインせずともサーバ上で任意のコマンドを実行できることを確認します。


前提条件
========

デフォルトVPCの存在
--------------

デフォルトVPCが存在すること。
存在しなくても動作しますが、各種コマンド実行時のオプション指定は適宜行ってください。


実行権限
--------------

EC2インスタンスを起動する権限があること。
IAMを作成する権限があること。
これらを満たすアカウント情報を aws configure で登録しておいてください。


AWS CLIのバージョン
-------------------

以下のバージョンで動作確認済

* AWS CLI 1.9.14

```bash:コマンド:
aws --version
```
```text:結果(例):
      aws-cli/1.9.14 Python/2.6.6 Linux/2.6.32 botocore/1.3.14
```


0. 事前作業
===========


0.1. リージョンの決定
---------------------

現在一部のリージョンのみ対応していて東京はまだです。
今回は us-west-2 (オレゴン)で実行します。
(カレントユーザが利用するカレントリージョンも切り変わります。)

```bash:変数の設定:
export AWS_DEFAULT_REGION='us-west-2'
```

0.2. 変数の確認
---------------

プロファイルとリージョンが想定のものになっていることを確認します。

```bash:コマンド
aws configure list
```
```text:結果(例):
            Name                    Value             Type    Location
            ----                    -----             ----    --------
         profile                <not set>             None    None
      access_key     ****************M73Q shared-credentials-file
      secret_key     ****************YWg/ shared-credentials-file
          region                us-west-2              env    AWS_DEFAULT_REGION
```


1. IAMの準備
===========

1.1. IAMロールの作成
-----------------

```bash:コマンド:
ROLE_NAME="ec2-handson-role"
aws iam create-role --role-name ${ROLE_NAME} --assume-role-policy-document '{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole" } ] }'
```
```text:結果(例):
{
    "Role": {
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "sts:AssumeRole",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "ec2.amazonaws.com"
                    }
                }
            ]
        },
        "RoleId": "AROAIXFYSOEMNKZQ6532G",
        "CreateDate": "2015-12-21T06:00:03.792Z",
        "RoleName": "ec2-handson-role",
        "Path": "/",
        "Arn": "arn:aws:iam::454200CLI555:role/ec2-handson-role"
    }
}
```

1.2. IAMロールへPolicyを追加
-----------------

EC2 Run Commandに必要なポリシーである AmazonEC2RoleforSSM をIAMロールへ付与します。

Policy AmazonEC2RoleforSSMのARNを取得。

```bash:コマンド:
aws iam list-policies | grep AmazonEC2RoleforSSM
```
```text:結果:
            "PolicyName": "AmazonEC2RoleforSSM",
            "Arn": "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM",
```

"Arn"欄の arn: で始まる文字列をコピーして、次のコマンド１行目 POLICY_ARN に入れます。（たぶんjqを使うと自動で入る気がするけどjqはわからない）

```bash:コマンド:
POLICY_ARN="arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
aws iam attach-role-policy --role-name ${ROLE_NAME} --policy-arn ${POLICY_ARN}
```
```text:結果:
返り値なし
```

1.3. インスタンスプロファイルの作成
-----------------

```bash:コマンド:
INSTANCE_PROFILE_NAME="ec2-handson-profile"
aws iam create-instance-profile --instance-profile-name ${INSTANCE_PROFILE_NAME}
```
```text:結果(例):
{
    "InstanceProfile": {
        "InstanceProfileId": "AIPAIWWQNUCXKCYHEZ46O",
        "Roles": [],
        "CreateDate": "2015-12-21T05:54:47.445Z",
        "InstanceProfileName": "ec2-handson-profile",
        "Path": "/",
        "Arn": "arn:aws:iam::454200CLI555:instance-profile/ec2-handson-profile"
    }
}
```

1.4. インスタンスプロファイルへIAMロールを追加
-----------------

```bash:コマンド:
aws iam add-role-to-instance-profile --instance-profile-name ${INSTANCE_PROFILE_NAME} --role-name ${ROLE_NAME}
```
```text:結果:
返り値なし
```



2. EC2インスタンスの準備
===========

2.1. 鍵ペアの作成（任意）
---------------

鍵ペアを既に作成済みの場合、この作業は不要です。
鍵ペア oregon を作成します。
秘密鍵ファイルは ~/.ssh/oregon.pem に保存されます。

```bash:コマンド
EC2_KEY_NAME="oregon"
FILE_SSH_KEY="${HOME}/.ssh/${EC2_KEY_NAME}.pem"
aws ec2 create-key-pair --key-name ${EC2_KEY_NAME} --query 'KeyMaterial' --output text > ${FILE_SSH_KEY} && cat ${FILE_SSH_KEY} && chmod 600 ${FILE_SSH_KEY}
```
```text:結果(例):
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAg4AFnT934UdMmuSADElYSWzOQ/XpXUc0570zKwtDB3RXS9Fi8YP8J5jv6QRl
（本当はもっと長い）
MfRletfQDp2Hbth3SnNu2MaFQs8C+ODyx7XTx/13LW247AGVsSfq63jovntOL7PRuFA=
-----END RSA PRIVATE KEY-----
```


2.2. セキュリティグループの作成（任意）
-----------------------

セキュリティグループを既に作成済みの場合、この作業は不要です。
セキュリティグループ ec2-handson-sg を作成します。

```bash:コマンド:
SG_NAME="ec2-handson-sg"
aws ec2 create-security-group --group-name ${SG_NAME} --description ${SG_NAME}
SG_ID=`aws ec2 describe-security-groups --filter Name=group-name,Values=${SG_NAME} --query 'SecurityGroups[].GroupId' --output text` && echo ${SG_ID}
```
```text:結果(例):
      {
          "GroupId": "sg-59c3e63d"
      }

      sg-59c3e63d
```

外部からのHTTP接続をセキュリティグループに追加。
あえてSSHは追加せずログイン不能な状態にしておきます。

```bash:コマンド:
aws ec2 authorize-security-group-ingress --group-id ${SG_ID} --protocol 'tcp' --port 80 --cidr 0.0.0.0/0
```
```text:結果:
      (戻り値なし)
```


2.3. Userdataファイルの作成
-----------------

UserdataはEC2インスタンス作成時に実行されるShellScriptです。
yum updateを行い、nginxとamazon-ssm-agentをインストールします。

```bash:コマンド:
cat << EOF > ssm.sh
#!/bin/bash
yum -y update
curl https://amazon-ssm-us-west-2.s3.amazonaws.com/latest/linux_amd64/amazon-ssm-agent.rpm -o amazon-ssm-agent.rpm
yum install -y amazon-ssm-agent.rpm nginx
chkconfig nginx on
service nginx start
EOF
chmod 755 ssm.sh
```
```text:結果:
返り値なし
```

Userdataは作成したローカルファイルが転送され、権限が引き継がれます。
ファイルに実行権限が無いと書いてもEC2上で実行されないので注意が必要です。
このためファイル作成後 chmod にて権限を設定しています。


2.4. インスタンスの起動
-----------------
Amazon Linux(2015-09)をインスタンスタイプt2.microで起動します。

AMI IDの取得:

```bash:コマンド:
AMI_NAME='amzn-ami-hvm-2015.09.0.x86_64-gp2'
AMI_ID=`aws ec2 describe-images --filters Name=name,Values=${AMI_NAME} --query 'Images[].ImageId' --output text` && echo ${AMI_ID}
```
```text:結果:
      ami-9ff7e8af
```

インスタンスの起動

```bash:コマンド:
aws ec2 run-instances --image-id ${AMI_ID} --associate-public-ip-address --instance-type t2.micro --key-name ${EC2_KEY_NAME} --region ${AWS_DEFAULT_REGION} --security-group-ids ${SG_ID} --user-data file://ssm.sh --iam-instance-profile Name=${INSTANCE_PROFILE_NAME}
```
```text:結果(例):
{
    "OwnerId": "454200CLI555",
    "ReservationId": "r-c38b2806",
    "Groups": [],
    "Instances": [
        {
            "Monitoring": {
                "State": "disabled"
            },
            "PublicDnsName": "",
            "RootDeviceType": "ebs",
            "State": {
                "Code": 0,
                "Name": "pending"
            },
            "EbsOptimized": false,
            "LaunchTime": "2015-12-21T06:13:03.000Z",
            "PrivateIpAddress": "172.31.20.212",
            "ProductCodes": [],
            "VpcId": "vpc-77777777",
            "StateTransitionReason": "",
            "InstanceId": "i-685357ac",
            "ImageId": "ami-f0091d91",
            "PrivateDnsName": "ip-172-31-20-212.us-west-2.compute.internal",
            "KeyName": "oregon",
            "SecurityGroups": [
                {
                    "GroupName": "handson",
                    "GroupId": "sg-77777777"
                }
            ],
            "ClientToken": "",
            "SubnetId": "subnet-77777777",
            "InstanceType": "t2.micro",
            "NetworkInterfaces": [
                {
                    "Status": "in-use",
                    "MacAddress": "02:37:b4:3f:70:a7",
                    "SourceDestCheck": true,
                    "VpcId": "vpc-77777777",
                    "Description": "",
                    "NetworkInterfaceId": "eni-87217bfe",
                    "PrivateIpAddresses": [
                        {
                            "PrivateDnsName": "ip-172-31-20-212.us-west-2.compute.internal",
                            "Primary": true,
                            "PrivateIpAddress": "172.31.20.212"
                        }
                    ],
                    "PrivateDnsName": "ip-172-31-20-212.us-west-2.compute.internal",
                    "Attachment": {
                        "Status": "attaching",
                        "DeviceIndex": 0,
                        "DeleteOnTermination": true,
                        "AttachmentId": "eni-attach-58f92752",
                        "AttachTime": "2015-12-21T06:13:03.000Z"
                    },
                    "Groups": [
                        {
                            "GroupName": "handson",
                            "GroupId": "sg-77777777"
                        }
                    ],
                    "SubnetId": "subnet-77777777",
                    "OwnerId": "454200CLI555",
                    "PrivateIpAddress": "172.31.20.212"
                }
            ],
            "SourceDestCheck": true,
            "Placement": {
                "Tenancy": "default",
                "GroupName": "",
                "AvailabilityZone": "us-west-2b"
            },
            "Hypervisor": "xen",
            "BlockDeviceMappings": [],
            "Architecture": "x86_64",
            "StateReason": {
                "Message": "pending",
                "Code": "pending"
            },
            "IamInstanceProfile": {
                "Id": "AIPAIVIS7IIM3UPFCZLYM",
                "Arn": "arn:aws:iam::454200CLI555:instance-profile/ec2-handson-profile"
            },
            "RootDeviceName": "/dev/xvda",
            "VirtualizationType": "hvm",
            "AmiLaunchIndex": 0
        }
    ]
}
```


2.5. インスタンスIDの取得
-----------------

```bash:コマンド:
EC2_INSTANCE_ID=`aws ec2 describe-instances --query 'Reservations[].Instances[].InstanceId' --output text` && echo ${EC2_INSTANCE_ID}
```
```text:結果(例):
i-6be2e6af
```

作業中のリージョンに他のEC2インスタンスが存在する場合、複数表示されうまくいきません。
この場合は 2.4. での出力結果22行目あたりに
```"InstanceId": "i-6be2e6af",```
といった項目があるので、この値を手動で
```EC2_INSTANCE_ID="i-6be2e6af"```
のようにコマンドを打ち環境変数へ格納してください。


2.6. パブリックIPアドレスの取得
----------------------

```bash:コマンド:
EC2_PUBLIC_IP=`aws ec2 describe-instances --instance-id ${EC2_INSTANCE_ID} --query "Reservations[].Instances[].PublicIpAddress" --output text` && echo ${EC2_PUBLIC_IP}
```
```text:結果(例):
      54.77.77.777
```


2.7. インスタンスが動作するまで数分待ちます
-----------------

通常、数分でインスタンスが起動完了しますが、userdataでインストール処理をしているので通常より時間がかかります。
Webブラウザで 2.6. で表示されたIPアドレスへアクセスしてnginxの画面が表示されたら完了です。
```http://54.77.77.777/
```


3. EC2 Run Commandの実行
===========

3.1. EC2 Run Command実行の説明
-----------------

```bash:コマンド:
aws ssm send-command により実行します。
```
```オプション:
必須オプション
--document-name 実行するコマンドの種類を指定します。shell実行の場合 "AWS-RunShellScript"。現状Linuxでは他に "AWS-UpdateSSMAgent"のみサポートされています。
--instance-ids インスタンスIDを指定します。

任意オプション
--parameters　引き渡すパラメータ。ShellScriptの場合はこれも必須で、実行するコマンドそのものです。
--output-s3-bucket-name　実行結果を出力するS3バケット名
--output-s3-key-prefix 出力するディレクトリ名(指定しないとバケット直下に出力されます)
--cli-input-json JSONよくわかりません
```


3.2. 結果出力用S3バケットの作成（任意）
-----------------
AWSCLIでもマネジメントコンソールでも出力結果を確認できるので任意ですが、
S3バケットを指定するとコマンドの実行結果(標準出力)を保存することができます。
ここではS3バケット名を ec2-handson としていますが、自分で名前を考えてください。
これは全世界で一意である必要があり同じバケット名が使えないからです。

```bash:コマンド:
BUCKET_NAME="s3://ec2-handson/"
aws s3 mb ${BUCKET_NAME}
```
```text:結果(例):
make_bucket: s3://ec2-handson/
```


3.2. コマンドの実行
-----------------
今回はnginxサーバを停止させるべく service nginx stop コマンドでも実行してみましょう。
なお、インスタンスを複数指定して同時実行することもできます。

```bash:コマンド:
aws ssm send-command --document-name "AWS-RunShellScript" --instance-ids ${EC2_INSTANCE_ID} --parameters '{"commands":["service nginx stop"],"executionTimeout":["300"]}' --output-s3-bucket-name ${BUCKET_NAME}
```
```text:結果(例):
    "Command": {
        "Status": "Pending",
        "ExpiresAfter": 1450856459.898,
        "Parameters": {
            "commands": [
                "service nginx stop"
            ],
            "executionTimeout": [
                "300"
            ]
        },
        "DocumentName": "AWS-RunShellScript",
        "OutputS3BucketName": "ec2-handson",
        "OutputS3KeyPrefix": "hoge",
        "InstanceIds": [
            "i-2b191aef"
        ],
        "CommandId": "d6b7ae11-ff52-41e8-9578-e8ada35680b8",
        "RequestedDateTime": 1450855859.898
    }
}
```


3.3. コマンドが実行されたか確認
-----------------
Webブラウザをリロードしてみます。
```http://54.77.77.777/
```
サーバへ接続できなくなっていれば成功です。


3.4. コマンドが実行結果の表示
-----------------

```bash:コマンド:
aws ssm list-command-invocations --detail
```
```text:結果(例):
{
    "CommandInvocations": [
        {
            "Status": "Success",
            "CommandPlugins": [
                {
                    "Status": "Success",
                    "Name": "aws:runShellScript",
                    "OutputS3BucketName": "ec2-handson",
                    "OutputS3KeyPrefix": "d6b7ae11-ff52-41e8-9578-e8ada35680b8/i-2b191aef/awsrunShellScript",
                    "ResponseCode": 0,
                    "Output": "Stopping nginx: [  OK  ]\r\n",
                    "ResponseFinishDateTime": 1450855860.194,
                    "ResponseStartDateTime": 1450855860.0880001
                }
            ],
            "InstanceId": "i-2b191aef",
            "DocumentName": "AWS-RunShellScript",
            "CommandId": "d6b7ae11-ff52-41e8-9578-e8ada35680b8",
            "RequestedDateTime": 1450855859.898
        }
    ]
}
```

Status: Success となっていれば実行成功です。
Output: には標準出力の内容が表示されています。 "Stopping nginx: [  OK  ]\r\n" と成功していますね。

S3バケットを指定した場合
s3://バケット名/ディレクトリ/コマンドID/インスタンスID/ドキュメント名/x.ドキュメント名/stdout
ファイルに標準出力の内容が記録されています。


3.5. ハマるコマンド
-----------------

reboot を実行することもできますが、延々と再起動を繰り返してしまいます。
これはssm-agentがコマンド実行完了のステータスをAWSのサーバへ送るまではPending状態になっており、
rebootを実行した瞬間にインスタンスはシャットダウンするため全プロセスがkillしますからssm-agentも消滅するので永遠にPending状態。
そして再起動後に再びrebootが実行され、executionTimeoutで指定した時間まで延々と再起動が繰り返されます。
どうしても実行したい場合はタイムアウト時間を30秒ぐらいにします。これで一度だけ実行されタイムアウト終了となります。

halt や shutdown コマンドもタイムアウト終了となります。
これらはインスタンスが停止するので何度も繰り返されることはありませんが、ssm-agentがAWSのサーバへ結果を送出できず Status: TimedOut になります。
いずれも実害はありませんが、ログをとしてはエラーに分類されるので何らかの対策が必要です。



4. 後片付け
===========

4.1. インスタンスの削除
-----------------

```bash:コマンド:
aws ec2 terminate-instances --instance-ids ${EC2_INSTANCE_ID}
```
```text:結果(例):
{
    "TerminatingInstances": [
        {
            "InstanceId": "i-685357ac",
            "CurrentState": {
                "Code": 48,
                "Name": "terminated"
            },
            "PreviousState": {
                "Code": 80,
                "Name": "stopped"
            }
        }
    ]
}
```


4.2. ローカルファイルの削除
-----------------

```bash:コマンド:
rm ssm.sh
```
```text:結果:
返り値なし
```


4.3. IAMの削除
-----------------

```bash:コマンド:
aws iam remove-role-from-instance-profile --instance-profile-name ${INSTANCE_PROFILE_NAME} --role-name ${ROLE_NAME}
aws iam delete-instance-profile --instance-profile-name ${INSTANCE_PROFILE_NAME}
aws iam detach-role-policy --role-name ${ROLE_NAME} --policy-arn ${POLICY_ARN}
aws iam delete-role --role-name ${ROLE_NAME}
```
```text:結果:
返り値なし
```


5. まとめ
===========

これをどうやって使うのかいまいちわからないですね。
EC2 Run Commandによるコマンドはroot権限で実行されます。そして誰がやったのかという記録は残りません。（未検証）
エージェントのエンドポイントはインターネット上なのでVPC内で通信を完結させることができません。
なので（うちの）業務で使うのは難しそう。

今回やったwebサーバの停止・開始や、おかしくなったインスタンスをhaltさせオートスケーリングにより新しいインスタンスを起動させるぐらいしか用途が思いつきません。
ようするに非常用？