AWS API を叩くためだけに Python を使っていたけどもうやめた

## AWS CLI でがんばるのは辛いと思っていた時期もあった

ここ１年ぐらい AWS であれやこれやすることが多くなってきたのですが、インスタンスを上げたり下げたりをいちいちブラウザでマネジメントコンソールを開いてぽちぽちするのは辛いです。コンソールからさくっと立ち上げたいです。

[AWS コマンドラインインターフェイス（AWS CLI）](https://aws.amazon.com/jp/cli/) というものがあるので、コンソールからさくっとできることはできるのですが、単にインスタンスを立ち上げてそれっぽいタグを付与するだけでも・・・

インスタンスを起動する。

```bash
aws ec2 run-instances \
  --image-id "ami-12345678" \
  --key-name "ore-no-key" \
  --instance-type "t2.micro" \
  --iam-instance-profile "Name=areare" \
  --subnet-id "subnet-12345678" \
  --security-group-ids "sg-12345678" \
  --no-associate-public-ip-address
```

次のような JSON が結果として得られる。

```js
{
    "OwnerId": "012345678901",
    "ReservationId": "r-12345678",
    "Groups": [],
    "Instances": [
        {
            "Monitoring": {
                "State": "disabled"
            },
            "PublicDnsName": "",
            "RootDeviceType": "ebs",
            "State": {
                "Code": 0,
                "Name": "pending"
            },
            "EbsOptimized": false,
            "LaunchTime": "2015-12-07T10:52:52.000Z",
            "PrivateIpAddress": "192.0.2.123",
            "ProductCodes": [],
            "VpcId": "vpc-12345678",
            "StateTransitionReason": "",
            "InstanceId": "i-12345678",
            "ImageId": "ami-12345678",
            "PrivateDnsName": "ip-192-0-2-123.ap-northeast-1.compute.internal",
            "KeyName": "ore-no-key",
            "SecurityGroups": [
                {
                    "GroupName": "default",
                    "GroupId": "sg-12345678"
                }
            ],
            "ClientToken": "",
            "SubnetId": "subnet-12345678",
            "InstanceType": "t2.micro",

            // 以下略
        }
    ]
}
```

この JSON から InstanceId を取り出してインスタンスにタグを付ける。

```bash
aws ec2 create-tags \
  --resources "i-12345678" \
  --tags "Key=Name,Value=ore-no-instance"
```

などと、コマンドが２発に分かれるうえ、１回目のコマンドの結果のJSONから必要な項目を取り出して次のコマンドに使う、みたいなことをする必要があってちょっと辛いです。

さらに、↑の例だとサブネットとかセキュリティグループの ID をベタ書きしているので２発で済んでいますが「名前で検索したものを指定したい」となるとその分実行するコマンドも増えるし、また、それらのコマンドも JSON が返るのでシェルで扱うのはちょっと辛いです。

## AWS SDK for Python の方が簡単だと思っていた時期もあった

AWS SDK というものがあって、各種プログラミング言語で AWS を操作することができます。ので、ちょっと複雑な処理なら適当なプログラミング言語でやれば楽そうです。

幾つかの言語やプラットフォームの SDK がありますが・・・

- https://aws.amazon.com/jp/tools/

RHEL系のディストリなら Python がほぼ間違いなく使えるので Python を使うことにしました。

yum で epel からさくっとインストールできます（python-boto というパッケージ）。

```
yum install epel-release
yum install python-boto
```

次のコードは、サブネットやセキュリティグループを名前で検索して、インスタンスを起動して、タグを設定しています。

```python
import boto.vpc
import boto.ec2
from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType
from boto.ec2.networkinterface import NetworkInterfaceCollection, NetworkInterfaceSpecification

vpc = boto.vpc.connect_to_region("ap-northeast-1")
ec2 = boto.ec2.connect_to_region("ap-northeast-1")

subnet, = vpc.get_all_subnets(filters = {"tag:Name": "nat-a"})
groups = ec2.get_all_security_groups(filters = {"group-name" : ["sore", "dore"] })

device_map = BlockDeviceMapping()
device_map['/dev/sda1'] = BlockDeviceType(delete_on_termination = True, volume_type = "gp2")

interfaces = NetworkInterfaceCollection(NetworkInterfaceSpecification(
  subnet_id = subnet.id,
  groups = [x.id for x in groups],
  associate_public_ip_address = False
))

instance, = ec2.run_instances(
  image_id = "ami-12345678",
  key_name = "ore-no-key",
  instance_type = "t2.micro",
  block_device_map = device_map,
  instance_profile_name = "areare",
  network_interfaces = interfaces
).instances

instance.add_tags({
  "Name": "ore-no-instance"
})
```

同じことを AWS CLI とシェルでやろうとすると結構辛いです。

## AWS CLI でがんばるほうがやっぱり楽だった

しばらく Python でどうにかしていたのですが、やっぱり AWS CLI がんばることにしました。

↑と同じことを AWS CLI でやると次のようになります。

```bash
subnet_id=$(aws ec2 describe-subnets \
  --filters "Name=tag:Name,Values=nat-a" \
  --query "Subnets[].SubnetId" --output text)

group_ids=($(aws ec2 describe-security-groups \
  --filters "Name=group-name,Values=sore,dore" \
  --query "SecurityGroups[].GroupId" --output text))

image_id="ami-12345678"

instance_id=$(aws ec2 run-instances \
  --image-id "$image_id" \
  --key-name "ore-no-key" \
  --instance-type "t2.micro" \
  --iam-instance-profile "Name=areare" \
  --block-device-mappings \
      "DeviceName=/dev/sda1,Ebs={VolumeSize=8,DeleteOnTermination=true,VolumeType=gp2}" \
  --subnet-id "$subnet_id" \
  --security-group-ids "${group_ids[@]}" \
  --no-associate-public-ip-address \
  --query 'Instances[].InstanceId' \
  --output text
)

aws ec2 create-tags --resources "$instance_id" --tags "Key=Name,Value=ore-no-instance"
```

`awscli` の `--filters` や `--query` を駆使すれば AWS CLI とシェルだけでもなんとかなりました。

## AWS CLI のメリット

Python とくらべて次のようなメリットがあると思います。

### さっとヘルプ見れる

`aws ec2 run-instances help` とかですぐにヘルプが見られます。

AWS SDK for Python のリファレンスも下記にありますが・・

 - http://boto.readthedocs.org/en/latest/

いちいちこのサイトを開いて調べなければならないのはめんどくさすぎます。

もしかしたら Python でもうまい方法があるのかもしれませんが・・・わたしの Python 力はほぼゼロなのでわからないです。

### set -eux が便利

実際に作業するときには１行１行コマンドを貼り付けたりはせずに複数行をペタッと貼り付けて実行しています。なので、途中でコケたときのことも考える必要があります。が、AWS CLI、というかシェルなら、次のようにサブシェルで囲っておけば途中で失敗したらそこで止まるし、実行されたコマンドも表示されるので、どこでコケたのかが一目瞭然です。

```bash
(
  set -eux

  # 略
)
```

途中でコケたときのリトライがやりやすいです。

## filter や query を使いこなす

AWS CLI だけでどうにかしようとすると `--filter` オプションや `--query` オプションを使わざるを得ません。

### filter

`--filter` オプションは describe 系のコマンドで検索条件を指定するためのものです。どういう値が指定できるかは `aws ec2 describe-subnets help` のようなヘルプコマンドで調べることができます。

このオプションの値は下記のように変な指定のしかたをします。

```bash
--filters "Name=tag:Name,Values=nat-a"
```

この例だと `tag:Name` という項目が `nat-a` という値であるものが検索されます。

下記のように `Values` に複数記述すれば、いずれかに一致するものが検索されます。

```bash
--filters "Name=tag:Name,Values=nat-a,net-c"
```

`Name` と `Values` の組を複数指定すれば AND 条件になります。

```bash
--filters "Name=tag:Name,Values=nat-a,net-c" "Name=tag:Role,Values=oreore"
```

### query

`--query` オプションは結果の JSON を [JMESPath](http://jmespath.org) でフィルタします。

JMESPath というのは XPath の JSON 版みたいなもので、要するに [jq](https://stedolan.github.io/jq/) みたいなやつです。

単体でインストールすることもできます。

```
yum install epel-release
yum install python-pip
pip install jmespath
```

次のように使います。

```js
$ echo '{"a": {"b": {"c": 123}}}' | jp.py "a.b.c"
123
```

例えば、インスタンス作成時の JSON から InstanceId を取りたければ次のようにします。

```js
echo '{
    "OwnerId": "012345678901",
    "ReservationId": "r-12345678",
    "Groups": [],
    "Instances": [
        {
            "VpcId": "vpc-12345678",
            "StateTransitionReason": "",
            "InstanceId": "i-12345678",
            "ImageId": "ami-12345678"
        }
    ]
}' | jp.py "Instances[0].InstanceId"
```

詳細は [JMESPath のチュートリアル](http://jmespath.org/tutorial.html) を見るといいでしょう。

### output

AWS CLI はデフォでは結果が JSON で返ってきますが、シェルで扱うなら生のテキストのほうが良いので、`--output` オプションで `text` を指定します。

```bash
--output text
```

## さいごに

Python でやってたころはなにかやろうとするたびにググっていたのですが、AWS CLI なら `help` でさくっと使い方を見ることができるのでググる必要がなく、とても捗ります。

## （おまけ）AWS CLI に JSON を食わす

AWS CLI はコマンドラインでいろいろ指定する以外に JSON を指定して実行することができます。例えば次のようにです。

```bash
aws ec2 run-instances --cli-input-json '{
    "ImageId": "ami-12345678",
    "KeyName": "ore-no-key",
    "InstanceType": "t2.micro",
    "BlockDeviceMappings": [{
        "DeviceName": "/dev/sda1",
        "Ebs": { "VolumeSize": 8, "DeleteOnTermination": true, "VolumeType": "gp2" }
    }],
    "NetworkInterfaces": [{
        "DeviceIndex": 0,
        "SubnetId": "subnet-12345678",
        "DeleteOnTermination": true,
        "AssociatePublicIpAddress": false,
        "Groups": [
          "sg-12345678",
          "sg-12345678"
        ]
    }],
    "IamInstanceProfile": {
        "Name": "areare"
    }
}'
```

この JSON のひな形は `--generate-cli-skeleton` オプションで得られます。

```bash
aws ec2 run-instances --generate-cli-skeleton
```

コマンドライン引数でバラで指定するのと比べてあまりメリットは感じませんが・・・たぶん CloudFormation のテンプレートファイルと同じ形式だと思うので、CloudFormation をバリバリ使っているならこの方法もありかもしれません。