AWSとIBM CloudのVPC間をSite-to-Site VPNで接続してwatsonx.aiのAPIを叩いてみる

## はじめに
この記事は、AWSとIBM CloudのVPC間をSite-to-Site VPNで接続し、Private Endpoint経由でwatsonx.aiのAPIをAWS側から叩く検証をしてみたという内容となっています。
普通にBedrockでよいのでは？と思いながらも、社内政治的に（？）こういったユースケースもあるかもしれないな？と思ったため、冬休みの宿題としてお正月に検証してみました。

![自由研究CDM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134250/dae0ffca-18da-6089-3f7e-90841804ba06.png)

## 検証に使用したソースコード
今回の検証では、以下のソースコードをPrivate Subnet上にプロビジョニングしたEC2上で実行しました。

```python
import json
import requests

from ibm_cloud_sdk_core import IAMTokenManager
from ibm_watsonx_ai import APIClient
from ibm_watsonx_ai import Credentials
from ibm_watsonx_ai.foundation_models import ModelInference
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames as GenParams
from ibm_watsonx_ai.foundation_models.utils.enums import DecodingMethods

apikey = "Your API Key"
access_token = IAMTokenManager(
    apikey,
    url = "https://private.iam.cloud.ibm.com/identity/token"
).get_token()

my_credentials = Credentials(
  url = "https://private.jp-tok.ml.cloud.ibm.com",
  platform_url = "https://private.api.jp-tok.dataplatform.cloud.ibm.com",
  token = access_token
)
client = APIClient(my_credentials)

gen_parms = {
    GenParams.DECODING_METHOD: DecodingMethods.SAMPLE,
    GenParams.MAX_NEW_TOKENS: 100
}
model = ModelInference(
  model_id="ibm/granite-8b-japanese",
  credentials=my_credentials,
  params=gen_parms,
  project_id="Your Project ID",
  space_id=None,
  verify=False,
)

prompt_txt = "watsonx.aiの特徴を日本語で教えてください"
gen_parms_override = gen_parms = {
    GenParams.DECODING_METHOD: DecodingMethods.SAMPLE,
    GenParams.MAX_NEW_TOKENS: 50
}

generated_text_response = model.generate_text(prompt=prompt_txt, params=gen_parms_override)
print(generated_text_response)
```

## 実際に叩いてみた結果
無事にPrivate Endpoint経由でwatsonx.aiのAPIを実行し、応答を得られました（回答精度の良し悪しはさておき。。。）
```
[ec2-user@ip-xx-x-x-xx ~]$ python3.11 watsonx.py 
Watsonは、IBMにより開発された主要なコグニティブ・システムです。Watson X. AIは、AIコグニティブテクノロジーを企業が簡単に利用できるようにする、Watson for Businessの拡張製品です。
```

## VPNの接続方法
https://repost.aws/ja/knowledge-center/vpn-ibm-static-connection

基本的には上記手順と同様の手順で問題ありませんでしたが、いくつか手順通りではうまくいかなかった部分や少しわかりにくい部分もあったため、備忘録も兼ねて残しておきます。

### 暗号化アルゴリズムと整合性アルゴリズムについて
暗号化アルゴリズムはAES128、整合性アルゴリズムはSHA-256に設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134250/59188cec-32c9-2858-c296-db7b04c10f05.png)

手順書では、認証（整合性アルゴリズム）はSHA1に設定するよう記載されていますが、IBM CloudではSHA1はすでにサポートされていないようです。

https://cloud.ibm.com/docs/vpc?topic=vpc-using-vpn&locale=ja

### Diffie-Hellman Groupについて
Diffie-Hellman Groupは14に設定しました。
![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134250/5d4af91b-680a-ab37-2a78-13663382287e.png)

手順書では2に設定するよう記載されていますが、IBM Cloudではデフォルトが14になっていたため、AWS側もこちらに合わせた形です。

### IBM Cloud → AWSの経路について
手順書には記載されていませんが、IBM Cloud → AWSの経路もルーティングテーブルで作成しないと疎通ができなかったため、忘れず作成するようにしましょう。
![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134250/7eb2161b-d3d0-bc4a-3942-6f768676591f.png)

### 

## ハマったポイントなど

### ハマったポイント①: IAMトークンの取得
IBM CloudのAPIキーをSDKに渡すと、SDK内部で暗黙的にIAMトークンを取得する作りとなっていますが、このときに参照するエンドポイントがPublic Endpointとなっているため、Private Subnetからは接続ができずエラーになってしまいました。

```
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='iam.cloud.ibm.com', port=443): Max retries exceeded with url: /oidc/token (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f18f25da0a0>: Failed to establish a new connection: [Errno 110] Connection timed out'))
```

こちらのエラーを回避するため、Private Endpoint経由でIAMトークンを取得する処理を追加し、SDKにはAPIキーではなくIAMトークンを渡すよう実装しました。

```python
apikey = "Your API Key"
access_token = IAMTokenManager(
    apikey,
    url = "https://private.iam.cloud.ibm.com/identity/token"
).get_token()

my_credentials = Credentials(
  url = "https://private.jp-tok.ml.cloud.ibm.com",
  token = access_token
)
client = APIClient(my_credentials)
```

### ハマったポイント②: 謎アクセス
謎のアクセスでこけました。パスを見た感じ、SDK内部でDeployment Spaceあたりを参照する処理が実行されているのですかね？

```
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.jp-tok.dataplatform.cloud.ibm.com', port=443): Max retries exceeded with url: /v2/spaces?limit=1 (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x7f4b3a0f9430>: Failed to establish a new connection: [Errno 101] Network is unreachable'))
```

Credentialsを設定する際にurlとtokenに加えてplatform_urlも設定したところ、Private Endpointを参照するようになりました（結局ここの処理がなんのための処理なのかはよくわかっていない）

```python
my_credentials = Credentials(
  url = "https://private.jp-tok.ml.cloud.ibm.com",
  platform_url = "https://private.api.jp-tok.dataplatform.cloud.ibm.com",
  token = access_token
)
client = APIClient(my_credentials)
```

## 学び・知らなかったことなど
- IBM CloudのPrivate Endpointは、IBM Cloudのネットワーク内でしか名前解決できないわけではなく、名前解決自体はどこからでもできる（そりゃそうかという感じですが）
```shell-session
yukinocist:~$ nslookup private.api.jp-tok.dataplatform.cloud.ibm.com
;; Got recursion not available from 172.31.160.1
Server:         172.31.160.1
Address:        172.31.160.1#53

Non-authoritative answer:
private.api.jp-tok.dataplatform.cloud.ibm.com   canonical name = cpd-prod-tok.jp-tok.proxy.serviceendpoint.cloud.ibm.com.
Name:   cpd-prod-tok.jp-tok.proxy.serviceendpoint.cloud.ibm.com
Address: 166.9.249.179
Name:   cpd-prod-tok.jp-tok.proxy.serviceendpoint.cloud.ibm.com
Address: 166.9.249.143
Name:   cpd-prod-tok.jp-tok.proxy.serviceendpoint.cloud.ibm.com
Address: 166.9.249.114
```