AWS EC2 インスタンスのメタデータを取得する

# 初めに

インスタンス内部からインスタンスIDを知りたいことがあったのでPythonで取得するコードを書いた。

# インスタンスのメタデータとは

インスタンスIDやAMIのIDなどのインスタンス自身の情報のこと。インスタンスから`curl http://169.254.169.254/latest/meta-data/` を実行することで取得できる。

最初にシェルで取得しようとして以下のコードを書いたが、うまくいかないことがわかった。理由は実行結果を見るとわかるように、パスが深いところがあるため。例えば、`identity-credentials/: ec2/` は、さらに`curl http://169.254.169.254/latest/meta-data/identity-credentials/ec2`を実行しなければ`identity-credentials`（トークンなどの認証情報）の値は返ってこない。


```sh
data=`curl -s http://169.254.169.254/latest/meta-data/`

echo $data | sed 's/ /\n/g' | while read line
do
  echo "$line:" `curl -s http://169.254.169.254/latest/meta-data/$line`
done
```

実行結果（値は少し変えている）

```
ami-id: ami-06098fd00463352b6
ami-launch-index: 0
ami-manifest-path: (unknown)
block-device-mapping/: ami root
events/: maintenance/
hibernation/: configured
hostname: ip-172-31-38-1.ap-northeast-1.compute.internal
identity-credentials/: ec2/
instance-action: none
instance-id: i-0d25432b11k20tc94
instance-life-cycle: on-demand
instance-type: t2.micro
local-hostname: ip-172-31-38-1.ap-northeast-1.compute.internal
local-ipv4: 172.31.38.1
mac: 04:3r:66:3d:76:18
metrics/: vhostmd
network/: interfaces/
placement/: availability-zone availability-zone-id region
profile: default-hvm
public-hostname: ec2-13-125-99-211.ap-northeast-1.compute.amazonaws.com
public-ipv4: 13.125.99.211
public-keys/: 0=key
reservation-id: r-0e1bbhj32u77445y7
security-groups: default
services/: domain partition
```

この問題に対処するため、Pythonで再帰的に`curl`を実行することにした。

つまり、以下の処理を再帰的に行う。

認証情報がほしい
↓
**`curl http://169.254.169.254/latest/meta-data/`**
↓
**`curl http://169.254.169.254/latest/meta-data/identity-credentials/`**
↓
**`curl http://169.254.169.254/latest/meta-data/identity-credentials/ec2`**
↓
認証情報取得

# コード

実際に書いたコードがこちら。

```py
import subprocess


def run_curl(url):
    args = ['curl', '-s', url]
    res = subprocess.run(args, encoding='utf-8', stdout=subprocess.PIPE)
    return res


def res_analysis(res):
    for path in res.stdout.split('\n'):
        if '/' in path:
            res_analysis(run_curl(res.args[2] + path))
        else:
            print('{}: {}'.format(path, run_curl(res.args[2] + path).stdout))


if __name__ == "__main__":
    url = 'http://169.254.169.254/latest/meta-data/'
    res_analysis(run_curl(url))
```

# 解説

Pythonで`curl`コマンドを実行するため、subprocessをインポート。`encoding`を指定して、バイナリ文字をutf-8に変換する。標準出力を変数に格納するために`stdout=subprocess.PIPE`を指定する。

対話モードで実行した場合

```python
>>> subprocess.run(args, encoding='utf-8', stdout=subprocess.PIPE)
CompletedProcess(args=['curl', '-s', 'http://169.254.169.254/latest/meta-data/'], returncode=0, stdout='ami-id\nami-launch-index\nami-manifest-path\nblock-device-mapping/\nevents/\nhibernation/\nhostname\nidentity-credentials/\ninstance-action\ninstance-id\ninstance-life-cycle\ninstance-type\nlocal-hostname\nlocal-ipv4\nmac\nmetrics/\nnetwork/\nplacement/\nprofile\npublic-hostname\npublic-ipv4\npublic-keys/\nreservation-id\nsecurity-groups\nservices/')
```

以下の条件分岐だが、`curl`でパスが返ってきた場合とIDなどの値が返ってきた場合で処理を分けるために使用している。パスが返ってきた場合、URLにそのパスを追加して再度`curl`を実行する。値が返ってきた場合出力する。


```python
if '/' in path:
    res_analysis(run_curl(res.args[2] + path))
else:
    print('{}: {}'.format(path, run_curl(res.args[2] + path).stdout))
```

書いていて一番気持ち悪いと思ったが、現状これが一番簡潔なのかなと思っているのが以下のコード。再利用のため`subprocess.run`は1つのメソッドとして分けたかった。その結果、合成関数みたいになってしまった。

```python
res_analysis(run_curl(url))
```

実行結果（値は少し変えている。...の部分は省略していることを表す。）

```
ami-id: ami-06098fd00463352b6
ami-launch-index: 0
ami-manifest-path: (unknown)
ami: /dev/xvda
root: /dev/xvda
history: []
scheduled: []
configured: false
hostname: ip-172-31-38-1.ap-northeast-1.compute.internal
info: {
  "Code" : "Success",
  "LastUpdated" : "2021-04-17T07:01:28Z",
  "AccountId" : "012..."
}
ec2-instance: {
  "Code" : "Success",
  "LastUpdated" : "2021-04-17T07:02:09Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIA...",
  "SecretAccessKey" : "OL...",
  "Token" : "IQ...",
  "Expiration" : "2021-04-17T13:07:15Z"
}
instance-action: none
instance-id: i-0d25432b11k20tc94
instance-life-cycle: on-demand
instance-type: t2.micro
local-hostname: ip-172-31-38-1.ap-northeast-1.compute.internal
local-ipv4: 172.31.38.1
mac: 06:2f:79:4c:69:19
vhostmd: <?xml version="1.0" encoding="UTF-8"?>
device-number: 0
interface-id: eni-6y01019922ooo176528
13.125.99.211: 172.31.38.1
local-hostname: ip-172-31-38-1.ap-northeast-1.compute.internal
local-ipv4s: 172.31.38.1
mac: 04:3r:66:3d:76:18
owner-id: 012...
public-hostname: ec2-13-125-99-211.ap-northeast-1.compute.amazonaws.com
public-ipv4s: 13.125.99.211
security-group-ids: sg-b78p90r2
security-groups: default
subnet-id: subnet-e6g098l4
subnet-ipv4-cidr-block: 172.31.32.0/20
vpc-id: vpc-c4b1weee
vpc-ipv4-cidr-block: 172.31.0.0/16
vpc-ipv4-cidr-blocks: 172.31.0.0/16
availability-zone: ap-northeast-1a
availability-zone-id: apne1-az4
region: ap-northeast-1
profile: default-hvm
public-hostname: ec2-13-125-99-211.ap-northeast-1.compute.amazonaws.com
public-ipv4: 13.125.99.211
0=key: <?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "..."
        "http://www...">
<html xmlns="http://www..." xml:lang="en" lang="en">
 <head>
  <title>404 - Not Found</title>
 </head>
 <body>
  <h1>404 - Not Found</h1>
 </body>
</html>

reservation-id: r-0e1bbhj32u77445y7
security-groups: default
domain: amazonaws.com
partition: aws
```

# 参考記事

https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html

https://qiita.com/caprest/items/0245a16825789b0263ad