【Python】リモート環境への接続情報をJSONと環境変数で管理する方法

# 概要
Pythonスクリプトを利用してリモート環境への接続する際、**接続先の選択肢が複数**あるのであれば、JSONファイルで管理することをおすすめします。

また、接続先によってパスワード管理の機密性が変わってくると思うので、環境変数に登録することも選択肢として考慮します。

# 実施環境
Windows11（Windows10でも可）
Python 3.12.4

# 実装準備

### JSONファイルの作成
```conf.json
{
    "remote": {
        "wsl": {
            "host": "127.0.0.1",
            "user": "pecorimaru",
            "password": "Cnetuser"
        },
        "aws": {
            "host": "XXX.XXX.XXX.1",
            "user": "ubuntu",
            "password": "環境変数"
        }
    }
}
```

こちらがJSONファイルを作成した内容です。

**[host][user][password]**
上記の定義はSSHによるリモート接続を行うことを想定した情報です。
"aws"への接続パスワードはマスクするため、"環境変数"と入れています。


### 環境変数の定義
環境変数を用いたパスワード管理は、**スクリプトは共有フォルダに配置したいもののパスワード情報までシェアしたくない**場合に有用です。
利用するPCに同じ環境変数を設定しておくことで、スクリプトを一様に動かせます。

:::note warn
攻撃者からの侵入という観点において安全とは言えないので、本番データの更新ができる強力な権限は慎重に検討した上で設定すると良いでしょう。
:::

**設定方法**
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626788/54a53ecc-2db8-c41f-583b-53d613d59175.png">

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626788/e9b16515-bf2d-bd1c-9655-8c05c9efd65a.png)

**変数名／変数値**
* AWS_PASS／testpwd

# 実装方法
ファイルは下記の４ファイルで構成します。
※conf.jsonは**実装準備**の項で記載したものを利用します。

```.作成ファイル
src/
　├ conf.json         -- SSH接続の認証情報を記載
　├ fileutils.py      -- JSON設定情報の取得処理を記載
　├ remoteutils.py    -- SSHクライアントを取得する処理を記載
　└ main.py           -- メイン処理を記載
```

```fileutils.py
import json

def get_json_data(path: str):
 
    # r:読み取り専用 -1:バッファリング無し, utf-8:文字コード
    with open(path, "r", -1, "utf-8") as file:
 
        file_data = file.read()
 
    json_data = json.loads(file_data)
 
    return json_data
```

```remoteutils.py
import paramiko
from paramiko import SSHClient

import os

import fileutils

# conf.jsonのremote設定を取得
conf = fileutils.get_json_data("conf.json")
remote_conf = conf["remote"]

def get_ssh_client(con_nm: str) -> SSHClient:

    coninf = remote_conf[con_nm]

    # パスワードが"環境変数"である場合は環境変数：AWS_PASSを参照
    password = os.environ.get("AWS_PASS") if "環境変数" == coninf["password"] else coninf["password"]

    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(coninf["host"], username=coninf["user"], password=password)

    return ssh

```

```main.py
import remoteutils

def main():

    ssh = remoteutils.get_ssh_client("wsl")

    # コマンドを実行し、結果を取得
    stdin, stdout, stderr = ssh.exec_command('pwd')
    print(stdout.read().decode().strip())

    ssh.close()

if __name__ == "__main__":

    main()
```

```.実行結果
PS 省略..\src>Python main.py
/home/pecorimaru
```
上記は"wsl"にSSH接続し、pwdコマンドを実行した戻り値をプリントした結果です。接続先に"aws"が指定された場合は環境変数からパスワードを取得します。
このように設定情報を設定ファイルに保存し、SSH接続する処理を汎用部品に保存することで、メイン処理の記述を簡単にすることができます。

# 【応用】マスクのパターンごとの実装

上記の実装はパスワードをマスクする接続先が"aws"に限定されていて汎用性に欠けるので、もう少し現場環境に合わせた実装を考えてみます。

### パスワードをマスクしたい環境が複数ある場合

| remote | host | user | password |
|:-:|:-:|:-:|:-:|
| wsl  |  127.0.0.1 | pecorimaru  | Cnetuser |
| wsl2  |  127.0.0.2 | pecorimaru  | 環境変数 |
| aws  |XXX.XXX.XXX.XXX | ubuntu  | 環境変数  |
| aws2 |XXX.XXX.XXX.XXX | ubuntu2 | 環境変数  |
| aws3 |XXX.XXX.XXX.XXX | ubuntu3 | 環境変数  |

```conf.json
{
    "remote": {
        "wsl": {
            "host": "127.0.0.1",
            "user": "pecorimaru",
            "password": "Cnetuser"
        },
        "wsl2": {
            "host": "127.0.0.2",
            "user": "pecorimaru",
            "password": "環境変数"
        },
        "aws": {
            "host": "XXX.XXX.XXX.1",
            "user": "ubuntu",
            "password": "環境変数"
        },
        "aws2": {
            "host": "XXX.XXX.XXX.2",
            "user": "ubuntu2",
            "password": "環境変数"
        },
        "aws3": {
            "host": "XXX.XXX.XXX.3",
            "user": "ubuntu2",
            "password": "環境変数"
        }
    }
}
```

上記は"aws"以外の環境もマスクするため、環境変数の変数名／変数値を少し変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626788/c71c7088-7b30-9021-398d-2f218c426589.png)


**変数名／変数値**
* REMOTE_ACCESS／wsl2@Cnetuser2;aws@testpwd;aws2@testpwd2;aws3@testpwd3;

**[環境名1@パスワード1;環境名2@パスワード2;環境名3@パスワード3;...]**
の規則で設定するため、環境名やパスワードに"@"や";"が含まれると正しく機能しません。ちなみに区切り文字自体は何でも良いです。

# 実装方法

実装内容を修正するのはremoteutils.pyのみで、他はそのままでOKです。

```remoteutils.py
import paramiko
from paramiko import SSHClient

import os

import src.utils.fileutils as fileutils

conf = fileutils.get_json_data("conf/conf.json")
remote_conf = conf["remote"]

def get_ssh_client(con_nm: str) -> SSHClient:

    coninf = remote_conf[con_nm]

    # パスワードが"環境変数"である場合は環境変数：AWS_PASSを参照
    password = get_remote_access(con_nm) if "環境変数" == coninf["password"] else coninf["password"]

    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(coninf["host"], username=coninf["user"], password=password)

    return ssh


def get_remote_access(con_nm: str) -> str:

    remote_access = os.environ.get("REMOTE_ACCESS")

    # 環境変数の変数値を";"で分割し、ブランクを除いた要素をリストに格納
    conf_list = [elem for elem in remote_access.split(";") if elem]

    for conf in conf_list:
        conf_con_nm, password = conf.split("@")
        if con_nm == conf_con_nm:
            return password
```

パスワードだけでなく、ユーザーIDもマスクしたい場合はもう少し工夫が必要ですが、上記と同様に区切り文字を入れてsplit関数で整形すると対応できます。