WSL2のUbuntuでAzure Cosmos DBに接続するpythonプログラムを開発する時のTIPS

## この記事は

[求ム！Pythonを使ってAzureで開発する時のTips！【PR】日本マイクロソフト Advent Calendar 2020](https://qiita.com/advent-calendar/2020/azure-python)の13日目の記事です。

Cosmos DBに接続するpythonのプログラムをWSL2 + VisualStudio Codeで開発する際のTipsを書いてみようと思います。WSL2とは書いていますが、Macや他のLinuxでも活用できる内容だと思います。

## VisualStudio Codeでpythonの開発を行う

VisualStudio Code(以下VSCode)はpythonの開発環境(IDE)としても大変優秀ですが、Windowsで開発を行う場合、pythonの実行環境としては主に3つの選択肢があります。

<dl>
<dt>ローカルにpythonをインストールする</dt>
<dd>現在は<a href="https://github.com/pyenv-win/pyenv-win">pyenv</a>も動くようになっていますので一緒にインストールすると大変便利です。</dd>
<dt>WSL2上のpythonを使う</dt>
<dd>何らかの事情でpythonをWindows環境にインストールできない、またはLinuxのネイティブライブラリやコマンドに依存したパッケージを使うといった場合にはWSL2でpythonを実行し、開発とデバッグのインターフェースとしてWindowsのVSCodeを使うという手があります。
こちらについては<a href="https://aadojo.alterbooth.com/entry/2020/08/19/095654">弊社の同僚の松村が記事を書いています</a>ので、是非参考にしていただければと思います。
</dd>
<dt>コンテナを使う</dt>
<dd>同じくLinuxでpythonを動かしつつインターフェースとしてVSCodeを使いたい場合、コンテナでpythonを実行し、WindowsのVSCodeで拡張機能のRemote-Containerを使うという手段があります。
こちらは<a href="https://aadojo.alterbooth.com/entry/2020/08/20/102600">私が別記事を書いています</a>ので、そちらを参考にしていただければと思います。
</dd>
</dl>

さて、[Azure Cosmos DB](https://docs.microsoft.com/ja-jp/azure/cosmos-db/introduction)に接続するpythonアプリケーションを開発する場合、どの環境を選択すると良いでしょうか。ローカルにインストールした場合は特に難しい事はないのですが、それ以外の場合はちょっと一工夫が必要になります。
ここではWSL2を選択した場合のTIPSを書こうと思います。

## Azure Cosmosエミュレータ

Cosmos DBに接続するプログラムを開発する場合、[Azure Cosmosエミュレータ](https://docs.microsoft.com/ja-jp/azure/cosmos-db/local-emulator?tabs=cli%2Cssl-netstd2)を開発環境にインストールして行うケースは多いと思いますが、現在提供されているCosmosエミュレータはWindows版のみとなります。このため、MacやLinux、Windows上でもWSL2やコンテナを使って開発している人はそのままではエミュレータを使って開発することが出来ません。

Cosmosエミュレータを外部からのネットワーク接続を許可して起動することは可能ですが、その場合は接続時の証明書の問題がついて回ります。

特にpythonのCosmos DB SDK(接続はurllib3)では接続時にlocalhost(ないしはIPアドレス`127.0.0.1`など)での接続であれば証明書の検証をバイパス出来ますが、localhost以外へ接続する際は、自己署名の証明書はシステムに事前にインポートしていたとしても検証が通らないようです[^1]。

[^1]: 2020年12月現在で私が検証した範囲でのことなので、もし解決方法ご存じの方いらっしゃいましたら是非教えてください

では、WSL2や非Windows環境ではエミュレータの利用は諦めるしかないのでしょうか。

## トンネル

タイトルで出落ち感はありますが、こういうときはsshでトンネル掘ってやると解決できます。

まずはWSL2でsshdを起動し、sshで接続できるようにします。WSL2のUbuntsu 20はデフォルトではsshdは起動してないので手動で起動します。以下のコマンドをWSL2のターミナルで実行します。

```bash:bash
$sudo /sbin/service ssh start
```

デフォルトだとパスワード認証はコメントアウトされているので、公開鍵を準備するか、`/etc/ssh/sshd_config`に`PasswordAuthentication yes`の行を足しましょう。

そして、WSL2のIPアドレスを確認します。同じくWSL2のターミナルで実行します。

```bash:bash
$/sbin/ip addr list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: bond0: <BROADCAST,MULTICAST,MASTER> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
3: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
4: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
    inet 172.21.***.***/20 brd 172.21.**.*** scope global eth0
       valid_lft forever preferred_lft forever
    inet6 ****::***:****:****:****/64 scope link
       valid_lft forever preferred_lft forever
5: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0
```

172.21.\*\*\*.\*\*\*の辺りですね。ここに向けてWindowsからsshでWSL2にトンネル掘りながらログインしてみましょう。以下をWindowsのターミナルで実行します。

```powershell:powershell
ssh -R 8081:localhost:8081 172.21.***.*** -l WSL2でのユーザ名
```

ちなみにここで使っているsshコマンドは`%SYSTEMROOT%\System32\OpenSSH\ssh.exe`で、バージョンはこんな感じです。

```powershell:powershell
>ssh -V
OpenSSH_for_Windows_7.7p1, LibreSSL 2.6.5
```

さて、これでいけるかな・・・ということでWSL2のターミナルでcurlで確認してみましょう。証明書の検証をバイパスするために`-k`オプションをつけておきます。

```bash:bash
$curl -k https://localhost:8081/
curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8081
```

・・・おや、つながりませんね。何故でしょうか？

## WSL2とlocalhost問題

WSL2には、「WSL2内で起動したサーバに対して、Windows側から https://localhost:ポート番号/ といった形で接続できる」という便利機能があります。これのおかげで例えばWSL2でFlaskアプリを作ってgunicornで起動して、そこにWindowsのChromeからアクセスしてデバッグする・・といったことが簡単にできるわけです。

ということは、上記sshのトンネルで指定した「localhost:8081」ってどこに向いてるのか・・というと、何とWSL2の8081番に向いてるわけです。トンネルの入り口と出口が一緒になってしまってます。これでは想定した動作はしません。ちなみに、ポートを変えても動作しません。

WSL2の設定で`localhostForwarding=False`としてみたのですが、駄目でした。恐らくsshのバイナリ(というかリンクされたリゾルバ？)の問題なのかと思われます。

## puttyでやってみる

とりあえず他のsshアプリケーションで試してみましょう。ここでは[putty](https://www.putty.org/)を使ってみます。

起動したら接続先に先ほどのIPアドレスを入力し、左メニューの「Connection/SSH」の「＋」を押して展開し、「Tunnels」を選択します。

![Items.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59415/f1c5f915-7c5c-f797-bfda-4a316d0bf7ff.png)

「Source port」に「8081」、「Destination」に「localhost:8081」と入れ、「Remote」を選択し、「Add」ボタンを押します。

![2020-11-11_17h07_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59415/8ad4f033-454b-b645-709d-c6777fe15f57.png)


以下のような状態になったらOKです。

![2020-11-11_17h09_31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59415/5c0d2dbc-66f5-235e-b08f-3c8b6dba9e33.png)

設定を次回のために保存しておきましょう。左メニューから「Session」を選択、「Saved Sessions」の枠に適当な名前を入れ、「Save」を押します。
次回は起動後に「Saved Sessions」の中から保存したものを選んで「Load」を押し、IPアドレスが変わってるならそこだけ入力してやればOKです。

![2020-11-11_17h04_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59415/836d5c19-b889-45ce-3e7a-5e38f779309b.png)

これで設定は完了です。「Open」を押してssh接続して、接続したputtyのターミナルでもWSL2のターミナルでも良いので再度curlで実行してみます。

```bash:bash
$curl -k https://localhost:8081/
{"code":"Unauthorized","message":"Required Header authorization is missing. Ensure a valid Authorization token is passed.\r\nActivityId: 748cfa2a-94d2-482e-a495-592f8c77d13c, Microsoft.Azure.Documents.Common/2.11.0"}
```

接続できましたね！

## pythonから接続する

あとはWLS2内で開発するpythonプログラムでCosmosDBのURIを`https://localhost:8081`とし、Cosmosエミュレータで表示されているキーをmasterKeyに渡してやればOKです。接続するまでの所はこんな感じですね。

```python:python3
import azure.cosmos.cosmos_client as cosmos_client
import azure.cosmos.exceptions as exceptions

url = 'https://localhost:8081/'
key  = '***********'

client = cosmos_client.CosmosClient(url,{'masterKey': key})
```

こうするとpythonプログラムからはlocalhostに接続しているように見えるので証明書の検証がスキップされ(warningは出ますが)、通信はputtyで作ったトンネルを通ってホストOSであるWindowsで動いているCosmosエミュレータに接続されます。

なお、MacやWSL2以外のLinuxなどで開発をしている場合はlocalhost問題は発生しないはずなので、puttyを使わずに最初のssh.exeでのトンネル構築だけでうまく行くと思います(WSL2を起動してなければ)。

## まとめ

sshでのトンネルはこういうときには大変便利ですが、WSL2だとlocalhost問題でちょっとハマることがあります。そんなときはputtyなどWindows標準のssh.exe以外を使うとうまく行くようです。
この記事を参考にして、みなさまのCosmos DBを利用するpython(に限らず)アプリケーションの開発時実行環境の選択肢が増えれば幸いです。

とはいえ、Linuxネイティブのライブラリが必要と言った場合を除けば、Windowsでpython環境作るのが一番楽ちんではありますね。どっちにしてもエミュレータ自身を動かすためのWindowsは必要ですし(身も蓋もないというか台無し)