シェルに入るだけでEC2インスタンスが見れるようにPythonとxonshでよしなにした

# はじめに

みなさんはTerraformの勉強は進んでますか？（威圧）

僕は始めたばかりで今必死に[この本](https://www.amazon.co.jp/実践Terraform-AWSにおけるシステム設計とベストプラクティス-技術の泉シリーズ（NextPublishing）-野村-友規-ebook/dp/B07XT7LJLC)を読んでいます
結構楽しいし、AWS環境が自由に触れるとインフラの勉強が捗るのでいいなぁと思っている今日この頃ですが、勉強環境に一つだけ不満がありました

- [実践Terraform　AWSにおけるシステム設計とベストプラクティス (技術の泉シリーズ（NextPublishing）) | 野村 友規 | 工学 | Kindleストア | Amazon](https://www.amazon.co.jp/実践Terraform-AWSにおけるシステム設計とベストプラクティス-技術の泉シリーズ（NextPublishing）-野村-友規-ebook/dp/B07XT7LJLC)

### いちいちAWSのコンソール見るのめんどくさすぎ！！！

書籍でも書かれていますが（詳細は著作権が怖いので明言は避けます）基本的にAWSのインスタンスを確認するにはWeb UIによるコンソールから確認していくと思います

普段はそれで全然いいんですが、なにせ
「Terraformでインスタンス作って。。。んで？次は削除？はいはい。。。んで？またコンソールかぁ。。。」
というのをKindle見ながらやるのは結構苦痛でした

しかも知らないインスタンスに興味もないし、触りたくもないのでただ表示する機能だけほしい
awc cliは使ってもいいけど、フィルタを噛まさずにWeb UIっぽいくなってほしい

とりあえず現状はEC2インスタンスしか立てていないので、それくらいなら一覧表示するのは楽勝だから作っちゃお！という感じです

# 準備

## リポジトリ作成

まずは準備として、Terraform用リポジトリを作成します
リポジトリ作成はTerraformの性質上、ソースコード管理が基本になるので当たり前かもしれません
それならばそのリポジトリにPythonインスタンス表示スクリプトを埋め込めば楽かと思います

僕の場合は、練習用として作ったリポジトリなので、スクリプトをリポジトリに埋め込んじゃいました
ただし、書籍の著作権に関わるのでリポジトリは公開できません
ご了承お願いいたします


## direnv、dotenvを使ってAWSのトークンを管理しよう！

### direnv

[direnv](https://direnv.net)は「ディレクトリに入るだけでaliasや環境が勝手に読み込まれる！」いいやつです

- [direnvを使おう - Qiita](https://qiita.com/kompiro/items/5fc46089247a56243a62)
- [direnvにプロジェクト別開発環境構築する · lifeisegg2k/pub---rails-gem---study Wiki](https://github.com/lifeisegg2k/pub---rails-gem---study/wiki/direnvにプロジェクト別開発環境構築する)

### dotenv

僕はいつもdirenvと一緒に[dotenv](https://www.npmjs.com/package/dotenv)も使います
npmから入れた気がするので、ちょっと面倒ですがbrewでnpmを入れる方法が揃っているのでMacユーザは楽に入るかと思います

- [MacにNode.jsをインストール - Qiita](https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09)
- [環境変数の代わりに .env ファイルを使用する (dotenv) | まくまくNode.jsノート](https://maku77.github.io/nodejs/env/dotenv.html)


### dotenvでAWSのIAM情報を保持する

ここまで読まれた方は恐らくAWSのIAM情報に触れたことがある方かと思います
またはTerraform本を読んでる方かもしれません
ですからIAM情報の

- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_DEFAULT_REGION

が何を意味するかは説明を割愛します
これらのトークンの意味はを詳しく知りたい方は公式ページをご確認ください

- [AWS CLI の設定 - AWS Command Line Interface](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html)


トークン情報が取得できた、または冒頭に書いた実践Terraformの本で言えば、2章に入った方から次に進んでもらえるとスムーズかと思います

実際にdotenvを使ってトークン情報を記述します
リポジトリrepoの直下に.envファイルを作成します

注意点として、このままではgit上に.envファイルが上がってしまうので、.gitignoreにちゃんと反映してください

```text:repo/.gitignore
.env
```

```shell:repo/.env
AWS_ACCESS_KEY_ID=AAAAAAAAAAAAAAAAAAAA
AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
AWS_DEFAULT_REGION=ap-northeast-1
```
- [AWS CLI を設定する環境変数 - AWS Command Line Interface](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-envvars.html)



ちゃんと動くかの確認方法

- [AWS CLIで使える認証情報とオプションの設定方法まとめ | Developers.IO](https://dev.classmethod.jp/articles/how-to-configure-aws-cli)


### direnvでdotenvを使う

次はdirenvでdotenvを読み込ませ、作業ディレクトリ下で自動に設定を反映させる方法です
direnvでは.envrcで情報を読み取ります

```shell:repo/.envrc
dotenv
xonsh --rc xonshrc.xsh
```

一行目のdotenvはdirenvで.envを自動に読み込む設定になります

説明は後述するので省きますが、xonshというshellを利用します
こちらはPythonスクリプトがターミナル上で簡単に動かせるため、PythonでAWSのスクリプトを書いたら結構便利にshellで走らせられるといった具合です

### まとめ

以上でディレクトリに入るだけでAWSへ簡単に繋げられるような設定です
普段遣いしても不便はないし、Terraformの開発も大分楽になります


## PythonでEC2インスタンス一覧表示スクリプトを使おう！

direnv、dotenvの設定が完了したら、早速Pythonでインスタンスを表示するスクリプトを動かしてみましょう

```python:repo/describe_instances.py
import os
import boto3
import pandas as pd


def create_session():
    client = boto3.session.Session(
        aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
        aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
        region_name=os.environ["AWS_DEFAULT_REGION"]
    ).client("ec2")

    return client


def to_pandas():
    response = create_session().describe_instances()
    df = pd.io.json.json_normalize(
        pd.DataFrame(
            pd.DataFrame(
                response["Reservations"]
            ) \
                .apply(lambda r: r.str[0])
                .Instances \
                .values
        )[0]
    )
    df.loc[df["Tags"].isnull(), "Tags"] = df.loc[df["Tags"].isnull(), "Tags"].apply(lambda x: [])
    df.Tags = df.Tags.apply(lambda r: {t["Key"]: t["Value"] for t in r} if len(r) > 0 else {})
    df = pd.concat([df.drop(["Tags"], axis=1), df["Tags"].apply(pd.Series)], axis=1)
    df["SecurityGroups"] = df["SecurityGroups"].apply(lambda r: ", ".join([i["GroupName"] for i in r]))

    return df[["Name",
               "ImageId",
               "InstanceType",
               "Placement.AvailabilityZone",
               "State.Name",
               "PublicDnsName",
               "PublicIpAddress",
               "KeyName",
               "Monitoring.State",
               "LaunchTime",
               "SecurityGroups",
               "Owner",
               ]].sort_values(["LaunchTime",
                               "State.Name"],
                              ascending=[False,
                                         True])


if __name__ == "__main__":
    print(to_pandas())
```

Pythonスクリプトを保存し

```shell
python describe_instances.py
```

を実行してみてください
インスタンスがDataframe表示されて一覧表示が可能になります

|                            Name                            |       ImageId       |InstanceType|Placement.AvailabilityZone|State.Name|                     PublicDnsName                     |PublicIpAddress|               KeyName                |Monitoring.State|       LaunchTime       |                                        SecurityGroups                                         |   Owner   |
|------------------------------------------------------------|---------------------|------------|--------------------------|----------|-------------------------------------------------------|---------------|--------------------------------------|----------------|------------------------|-----------------------------------------------------------------------------------------------|-----------|
|linux-adfdsf                                      |ami-2dfdfs         |t2.medium   |ap-northeast-1a           |running   |ec2-25-25-25-25.ap-northeast-1.compute.amazonaws.com|111.111.111.1 |dfdfdest                    |disabled        |2020-01-10T01:01:32+0000|mdfsfsest                                                                            |NaN        |



## xonshをdirenvで一時的に使おう！

[xonsh](https://xon.sh/sidebar.html)はPythonで設定をカスタマイズできるshellです
Pythonユーザが使っていることが多く、標準設定でもかなり便利なのでおすすめできます
（ちなみに僕は普段遣いはしていません。なんとなくです。）

導入方法は簡単で、Pythonで利用されるパッケージマネージャpipを使うだけです

```shell
pip install xonsh
```

先程の.envrcにxonshの記述がありましたが、あれは一時的にxonshにshellを乗り換えて、xonsh上でインスタンス一覧表示スクリプトを動かしちゃおう！という試みです

- [Documentation Index — xonsh 0.9.18 documentation](https://xon.sh/sidebar.html)


### なぜxonshを一時利用するのか

xonshを一時利用する理由ですが、単刀直入に言うとshell上でのPython呼び出しのオーバーヘッドが大きすぎることが主な理由です
AWSへの接続、一覧の加工、表示をいちいちshell呼び出しからするのは非常に非効率なので、変数すらshellに保存しておけるxonshを選びました


### xonshの設定

xonshはxonshrcファイルを作ることで便利にカスタマイズできます
ここでは公式のxonshrcに変更を加えて、自作のインスタンス一覧表示スクリプトが簡単に動かせるようにします

```shell:repo/xonshrc.xsh
# adjust some paths
$PATH.append('/home/scopatz/sandbox/bin')
$LD_LIBRARY_PATH = ['/home/scopatz/.local/lib', '/home/scopatz/miniconda3/lib', '']

# alias to quit AwesomeWM from the terminal
def _quit_awesome(args, stdin=None):
    lines = $(ps ux | grep "gnome-session --session=awesome").splitlines()
    pids = [l.split()[1] for l in lines]
    for pid in pids:
        kill @(pid)

aliases['qa'] = _quit_awesome

# some customization options, see https://xon.sh/envvars.html for details
$MULTILINE_PROMPT = '`·.,¸,.·*¯`·.,¸,.·*¯'
$XONSH_SHOW_TRACEBACK = True
$XONSH_STORE_STDOUT = True
$XONSH_HISTORY_MATCH_ANYWHERE = True
$COMPLETIONS_CONFIRM = True
$XONSH_AUTOPAIR = True


# ここからオジリナルのxonshrc

import os
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

# 自作関数をインポート
import describe_instances
_df = describe_instances.to_pandas()
# 0から5列目までを表示
print(_df[_df.columns[0:5]])
```


- [Run Control File — xonsh 0.9.18 documentation](https://xon.sh/xonshrc.html)


## Tips

これで、ディレクトリに移動するだけでインスタンス一覧表示が自動に行われ、さらには

```shell
describe_instances.to_pandas()
```

をxonshに流し込むだけでDataframeになった一覧が取得できます

便利なので是非使ってみてください〜〜〜