マネージド ID 対応ライブラリを使わないでマネージド ID による認証をしてみる話

# マネージド ID

Azure の各サービスは Azure Active Directory (AAD) による認証に対応しています。AAD アカウントに対して Azure リソースへの所定の動作を許可する権限を振る等して、アクセス・利用可能な範囲を制御しています。

ところで、サービスを使うのは人間だけではありません。例えばデータベースの場合、アプリケーションや他のサービスからのアクセスの方が人間によるアクセスよりも恐らく多いわけで、アプリケーションが毎回人間が用いるようなアカウントを使うように構築するというのは問題があります。具体的なところでいうと、人によって振られている権限が異なるためアプリケーションの動作に一貫性を持たせることができません。

そういうとき、サービスプリンシパルという AAD によって認証される無人アカウントを作成して対応します。しかしこのサービスプリンシパルはシークレットの管理が必要だったり作成が手間だったりと色々面倒です。

そこで使用されるものがマネージド ID です。

マネージド ID は各 Azure リソースが持つ無人の AAD アカウントのようなもので、サービスプリンシパルと異なり Azure でしか使用できない代わりにシークレットのやり取りが必要なく（というかそもそもできず）、圧倒的にセキュアかつ簡単に設定することができます。

# マネージド ID による認証

そんなマネージド ID ですが、マネージド ID に対応している接続元から繋ぐか、対応ライブラリを使用して認証を通す必要がある…………そんな風に考えていた時期が俺にもありました。

マネージド ID の根本はトークンによる AAD 認証です。

マネージド ID が振られたリソースにはローカルにトークン払い出し用のエンドポイントが用意され、ローカルからそのエンドポイントにアクセスしてトークンを貰い、そのトークンを対象リソースに提示するとリソースは AAD でそのトークンを確認し、認証するという流れです。

![data-flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359618/0f6eae10-a7b7-9c8b-f87e-50bed9c283c9.png)

上の図を含めてドキュメント[^1]に書いてあるので、お前ちゃんとドキュメント読めよという感じです。

# トークンによる認証

マネージド ID 対応ライブラリを使用すると AuthenticationMsi 的な名称の関数が用意されていますが、そういうライブラリを使えば接続できることは分かっているので、今回はトークンを使用して汎用的な接続手順を使って接続できるか試みます。

SQL Database と VM を用意し、VM でシステム割り当てマネージド ID を有効にし、SQL Database 側で マネージド IDによるアクセスを認めました。

## 事前準備

VM におけるマネージド ID の有効化手順はドキュメント[^2]を参照してください

SQL Database では AAD 管理者を用意[^3]した後、下記 SQL 文を該当のデータベースで実行します。ポータルに統合されているクエリエディタで十分です。

```SQL
CREATE USER [test-vm] FROM EXTERNAL PROVIDER;
ALTER ROLE db_owner ADD MEMBER [test-vm];
```
test-vm というのは今回検証用に作成した VM の名称です。マネージド ID で SQL Database にアクセスを試みる場合、ユーザー名は（システム割り当てマネージド ID を使っていれば) VM の名称が使用されますので、このユーザー名は VM と一致している必要があります。

さらに作成したユーザーに db_owner のロールに基づく権限を付与しています。本来は最小の権限だけを与えるべきですが、検証したらサクッと削除してしまうのでよしとします。

VM 環境に Python の実行環境を用意します。今回は Data Science VM を使用することで Jupyter 環境をデプロイ即用意しています。

使用するカーネルに SQL Database に接続する上で必要なライブラリである pyodbc を入れます。これを使用することで Python で SQL Database への接続が可能となります。

```
conda install pyodbc
```

conda を使うと必要な依存関係もまとめて解決してくれて便利でした。 (pip でインストールしようとしたら失敗しました)

## コード

必要ライブラリのインポートから始めます。

```Python
import os
import pyodbc
import requests 
import struct
```

pyodbc の他、トークンを発行するエンドポイントに対してアクセスを行うために requests を、pyodbc が少々トークンを使った認証で問題を抱えていてその対応のために struct を使用します。

続いてローカルのエンドポイントからトークンを取得します。

なお、下記のコードは github の pyodbc リポジトリの issue で投稿された michaelcapizzi 氏のコード[^4]を参考にしています。

```Python
identity_endpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
resource_uri="https://database.windows.net/"
token_auth_uri = f"{identity_endpoint}?resource={resource_uri}&api-version=2019-08-01"
head_msi = {"Metadata": "true"}
resp = requests.get(token_auth_uri, headers=head_msi)
access_token = resp.json()['access_token']
print(access_token)
```

169.254.169.254 はリンクローカルアドレスで、ローカルからのアクセスによってのみ接続できます。

続いて取得したトークンを少々加工します。

```Python
SQL_COPT_SS_ACCESS_TOKEN = 1256

byte_access_token = bytes(access_token, 'utf-8')
exptoken = b''
for i in byte_access_token:
        exptoken += bytes({i})
        exptoken += bytes(1)
tokenstruct = struct.pack("=i", len(exptoken)) + exptoken

token_dict = {SQL_COPT_SS_ACCESS_TOKEN: tokenstruct}
```

トークンをそのまま投げることができたら楽だったのですが pyodbc の場合ドライバーに値を渡すときにバイト列に変換した形で渡す必要があります。

いよいよ接続します。基本的な接続文字列と共に、作成したトークンを渡します。

```Python
conn = pyodbc.connect("Driver={ODBC Driver 17 for SQL Server};Server=tcp:<server-name>.database.windows.net,1433;Database=<db-name>;", attrs_before = token_dict);
```

エラーが出なければ、何でもいいので適当な SQL クエリを実行してます。

```Python
cursor = conn.cursor()
cursor.execute("select * from test1.table1")
row = cursor.fetchall()
```

データが取れたようなので、接続できているようです。

# まとめ

マネージド ID による認証を対応ライブラリを使わずローカルエンドポイントからトークンを取得して試してみました。

他の言語であってもとりあえず http リクエストができてトークンの取得さえできれば対応ライブラリを使わずにマネージド ID による接続ができそうです。

調べていくと App Service 等別のコード実行環境であっても多少違いはあれど概ね似たやり方でトークンを取得できるので、対応ライブラリが存在しない場合や事情により使えない場合にはこのやり方が応用できそうです。

例えばアプリケーションのホスト環境を VM ではなく Web App に変更した場合、上記コードの

```Python
head_msi = {"Metadata": "true"}
```

の部分を変えなければならない[^5]ようですが、あまり大きな違いはないように見受けられます。


[^1]: https://docs.microsoft.com/ja-jp/azure/active-directory/managed-identities-azure-resources/how-managed-identities-work-vm
[^2]: https://docs.microsoft.com/ja-jp/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm
[^3]: https://docs.microsoft.com/ja-jp/azure/azure-sql/database/authentication-aad-configure?tabs=azure-powershell#azure-ad-admin-with-a-server-in-sql-database
[^4]: https://github.com/mkleehammer/pyodbc/issues/228#issuecomment-506437261
[^5]: https://docs.microsoft.com/ja-jp/azure/app-service/overview-managed-identity?tabs=dotnet#obtain-tokens-for-azure-resources