normalian blog

Let's talk about Microsoft Azure, ASP.NET and Java!

Bicep ファイル で Azure Firewall のルール上の IP Group を更新する

ちょっと人から聞かれて軽く書いておいたので供養致します。特に「IP Group が複数の場合にどうするの?」と聞かれたので、以下が bicep ファイルの内容です。

resource firewallPolicy 'Microsoft.Network/firewallPolicies@2023-04-01' existing = {
  name: 'StandardPolicy'
}

resource ipgroup1 'Microsoft.Network/ipGroups@2023-04-01' existing = {
    name: 'IPG-Test01-WestUS2'
}

resource ipgroup2 'Microsoft.Network/ipGroups@2023-04-01' existing = {
    name: 'IPG-Test02-WestUS2'
}

resource networkRuleCollectionGroup 'Microsoft.Network/firewallPolicies/ruleCollectionGroups@2022-01-01' = {
  parent: firewallPolicy
  name: 'DefaultNetworkRuleCollectionGroup'
  properties: {
    priority: 2000
    ruleCollections: [
      {
        ruleCollectionType: 'FirewallPolicyFilterRuleCollection'
        action: {
          type: 'Allow'
        }
        name: 'Net-RuleCollection-01'
        priority: 1250
        rules: [
          {
            ruleType: 'NetworkRule'
            name: 'time-windows'
            ipProtocols: [
              'ANY'
            ]
            destinationAddresses: [
              'AzureCloud'
            ]
            sourceIpGroups: [
                ipgroup1.id
                ipgroup2.id
            ]
            destinationPorts: [
              '80'
            ]
          }
        ]
      }
    ]
  }
}

それを以下のコマンドでデプロイしました。

az deployment group create --resource-group "your resource group" --template-file "your bicep file"--mode incremental

ここでのポイントは既存のリソースに対する update なので bicep 側で existing キーワードを忘れない様にすることです。IP Group 辺りでうっかり忘れると以下の様になるのでご注意を。

az deployment group create --resource-group RG-Firewall-Test-WestUS2 --template-file .\hello01.bicep --mode incremental
az : WARNING: C:\Users\daisami\OneDrive - Microsoft\Desktop\hello01.bicep(1,7) : Warning no-unused-params: Parameter "location" is declared but never used. 
[https://aka.ms/bicep/linter/no-unused-params]
At line:1 char:1
+ az deployment group create --resource-group RG-Firewall-Test-WestUS2  ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (WARNING: C:\Use...-unused-params]:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
ERROR: {"status":"Failed","error":{"code":"DeploymentFailed","target":"/subscriptions/423a72cd-b110-476f-8b8b-0569fe1773bd/resourceGroups/RG-Firewall-Test-West
US2/providers/Microsoft.Resources/deployments/hello01","message":"At least one resource deployment operation failed. Please list deployment operations for 
details. Please see https://aka.ms/arm-deployment-operations for usage details.","details":[{"code":"BadRequest","target":"/subscriptions/423a72cd-b110-476f-8b
8b-0569fe1773bd/resourceGroups/RG-Firewall-Test-WestUS2/providers/Microsoft.Resources/deployments/hello01","message":"{\r\n  \"Message\": \"The request is 
invalid.\",\r\n  \"ModelState\": {\r\n    \"resource\": [\r\n      
\"{\\\"Error\\\":{\\\"Code\\\":\\\"FirewallPolicyRuleCollectionGroupInvalidPriorityValue\\\",\\\"Message\\\":\\\"Invalid Rule Collection Group . Invalid 
priority value 0, must be between 100 and 65000\\\",\\\"Target\\\":null,\\\"Details\\\":null},\\\"Status\\\":\\\"Failed\\\"}\"\r\n    ]\r\n  }\r\n}"}]}}

Microsoft Entra アプリケーション プロキシで閉域なオンプレミスサーバにアクセスする+α な TIPS

皆様は Microsoft Entra アプリケーション プロキシ という機能をご存じでしょうか?Entra ID の認証・認可機能を利用して、オンプレミスのサービスに対してセキュアな接続を提供する社畜垂涎のサービスとなります。細かくは色々とありますが、以下の様に EntraID が認証した情報を用い、Microsoft Entra application proxy connectors をインストールした Windows Server を経由してセキュアかつ閉域的な Web サーバにアクセスすることが可能です。

learn.microsoft.com

Microsoft Entra application proxy connectors(実態はエージェントで Windows サービスとして稼働。以下はコネクタと呼びます)をインストールした端末が必要&当該サーバが Windows Server である必要がありますが、接続先のサーバは Windows, Linux 含めて OS は問わないのでかなり汎用性の高いサービスだと思っています。

昨今では Microsoft Entra Private Access というサービスが’発表されており、今後 Entra ID 的にはこちらのサービスを利用することが推奨されると思いますが、現在 Microsoft Entra Private Access はパブリックプレビューなので、まだまだ使いどころはあるサービスだと思っています。
www.microsoft.com

セットアップはどうやってやるの?

以下ふたつのセットアップが必要になります。ここでは既に HTTPS アクセスが可能な Web サーバ(今回は IIS を利用していますが、Linux でも構いません)がセットアップ済としています。Web サーバには認証局から発行された証明書を利用することを強く推奨します。オレオレ証明書を利用する場合、後述するようなエラーが発生します

  • Windows Server にコネクタのセットアップ
  • Entra ID に Enterprise Application を作成する

まず、アーキテクチャ図における「Windows Server ※要コネクタ」のセットアップを行います。当該サーバが Entra ID を介したユーザリクエストを中継処理するサーバとなります。上記で記載した Microsoft Entra Private Access だと本サーバのセットアップは不要となりますが、逆にネットワーク分離的にはこちらのコネクタ入りの Windows Server が中継する様にネットワーク接続を制限するうま味もあるので残るのではないかという気もしております。
セットアップ自体は簡単です、以下のスクリーンショットに従い Entra ID の Application Proxy メニューに移動します。そこに Download connector service があるので、こちらでコネクタのセットアップ用のファイル(単なる exe 形式です)をダウンロードします。

コネクタのインストール先 Windows Server で以下の様にセットアップファイルを実行し、ウィザードに従ってセットアップを実行してください( Uninstall になってるのは既にセットアップ済のサーバで実行したからなので、ここが Install になってるはずです)。

セットアップが完了すると以下の様に Windows サービスに Microsoft AAD Application Proxy Connector/Microsoft AAD Application Proxy Connector Updater の二つが表示されます。

最後に以下のスクリーンショットに従い Entra ID の Application Proxy メニューに移動します。こちらで当該 Windows Server の Status が Active になっていればセットアップ完了です。

オンプレミスのファイヤーウォールNSG 等でアクセスが制限されている場合、Windows Server のセットアップが上手くいっていても Active にならない場合があります。その場合は以下のネットワーク接続要件を確認してください。
learn.microsoft.com


次にEntra ID 上に Enterprise Application のリソースを作ります。Azure ポータルから Entra ID の画面に遷移して以下の Enterprise applications を選択してください。ここでMicrosoft Entra ID P1 ライセンスが確認できますので、もしライセンスが割り当てられてない場合はライセンスの購入・割り当てを検討ください。

以下の New application からアプリケーションプロキシを利用するリソースを作成します。

New application を選択後、以下の画面となりますがこの中から Add an on-premise application を選択していきます。

次に各種パラメータを入力します。以下に図示しましたが、この中で特に重要なのが Internal Url と External Url です。

  • Name:ここに入力した名前で Entra ID 上に表示されます
  • External Url:ユーザが直接アクセスする URL になります。オンプレミスのリソースにコネクタ経由でアクセスする際、ユーザはこちらをブラウザ等に入力することになります
  • Internal Url:コネクタがリクエストを送るオンプレミス内の URL となります(今回の例では Azure 内ですが)。こちらの名前解決はコネクタをインストールしたサーバに準拠するので、極論当該サーバの hosts ファイルを編集しても対応可能です。こちらで既にセットアップ済 HTTPS アクセス対象の Web サーバの URL を指定してください
  • Connector Group:こちらは後述しますが、ここでコネクタをセットアップした Windows Server を選択します。

リソース作成後、本 Enterprise Application のリソースを利用するユーザを割り当てます。

最後にブラウザに External Url を入力してアクセスを試します。この際 Enterprise Application を作成した Entra ID テナントでの認証を求められるので、認証完了後に以下の様にオンプレミス(今回は相当)へのリソースへアクセスが可能となります。

オレオレ証明書を利用する場合

デフォルト設定でコネクタが認証済の証明書以外を利用する場合、以下の SSL に関するエラーを表示します。

開発環境に限定した場合ですが、Enterprise Application リソースの Application proxy メニューから、以下の Advanced メニューにおける Validate Backend SSL certificate メニューのチェックを外します。こちらの有効化には体感で数分~10分程度かかったので、設定直後にエラーが続いても焦らなくて OK です。

こちらでオレオレ証明書でもアクセス可能ですが、あくまで開発環境用だという点に留意ください。

ネット―ワークの接続要件は?

皆様の一番関心のあるところ&アプリケーションプロキシの良い点だと考えています。許可する必要があるトラフィックはコネクタが利用する一部のアウトバウンド接続であり、インバウンドは原則開放する必要がありません。以下に接続要件が記載されています。
learn.microsoft.com
インバウンドはセキュア通信であれば 443 をコネクタがインストールされた Windows Server 向けにだけ許可すれば良く、それ以外のサーバやポートに対しては特に公開する必要が無いのは大きなメリットだと考えています。

ライセンスは必要?

本サービスの利用には Microsoft Entra ID P1 or P2 ライセンスが必要になる点に注意が必要です。以下を参照ください。
Microsoft Entra アプリケーション プロキシに関してよく寄せられる質問 | Microsoft Learn

接続先の Internal URL に IP アドレスは指定できるの?

以下の様に IP アドレスを直接指定する場合、ポータルでエラーが発生して指定できません。認証局が発行した証明書の利用を前提としているので、IP アドレスの利用は非推奨どころか不可となります。

冗長化はどう考えたらいいの?

今回は特に触れませんでしたが、コネクタグループを利用して冗長化を行います。詳細は以下の記事を参照して頂ければと思いますが、基本的には用途ごとにコネクタグループを作成し、各コネクタグループにコネクタをセットアップしたサーバを複数登録することで冗長化します。
learn.microsoft.com

Azure Batch を試す際の TIPS を簡単にまとめてみた

ちょっと人に聞かれた際に久々に触ったので、振り返りを含めてちょっとまとめてみました。チュートリアル的な hello world は既存の公式ドキュメントを読めば良いと思うので、簡単な概要部分だけ紹介した後は簡単な TIPS 系を羅列していきたいと思っています。

まず、Azure Batch の概要となります。Azure Batch は task という形式で様々な粒度の処理を実行可能であり、スケジューリング機能等々を持っています。以下に Azure Batch のアーキテクチャ図を記載しています。

最初に Azure Batch アカウントを作成しますが、簡単な利用をする場合に重要となる用語は Pool, Job, Application となります。

  1. Pool :最初に作成する必要があり、様々な処理を実行するためのコンピュートリソースです。こちらは Azure になじみのある方なら VMSS の様なものと言えばわかりやすいかもしれません。図でも記載がある通り、既存の template image から OS を選択することもできますが、自身で作成した custom image の利用も可能です。
  2. Job :処理行うまとまりの単位になります。こちらには Job/Job Schedule という二種類があり、on-demand 実行をする job と定期実行をするための job の二種類が存在します。
  3. Application :自身が作成したカスタムアプリケーションを指します。zip 形式でアーカイブする必要があり、Azure Storage アカウント上に配置されることを想定しています。事前に Azure Batch アカウントに Azure Storage を紐づけるのを忘れないようにしましょう。

こちらのアーキテクチャについては以下の動画開始 100 秒位のところで図示されているので、そちらも参照すると良いと思います。

最初に Quotas タブを見よう

上記に記載した通り、最初に Pool を作成する必要がありますが、こちらは VM の SKU(Dv3 Series 等)を選択する必要があります。以下の画像でも表示されていますが、必要となる VM SKU の Quota が 0 の場合、Pool を作成してもリソースが割り当てられずに処理が実行されません。

Azure Batch アカウントの作成直後、こちらの Quota を確認し、必要な場合は Quota increase request をしてください。多少の時間がかかるので、これを最初にやるのがお勧めです。

Pool を作成する際の tips

様々な処理を実行するために必須な Pool を作成します。こちらも作成時に幾つかポイントがありますが、OS イメージの選択に幾つか方法があります。まずは以下の様に Marketplace から標準で提供されているものを選ぶパターン。

次に、以下の様に作成したカスタム VM イメージを利用することも可能です。今回は利用しませんでしたが、Docker イメージの利用も可能なようです。

次に、VM SKU の指定、ノード数等を指定するので、ここで指定する VM SKU は事前に Quota increase request を作成してください。

当然閉域網で処理を行いたいニーズも出てくると思いますが、コンピュートリソース単位(プール単位)で VNET 等の指定をすることが分かります。

Job を作成する際の TIPS

Job は実際にコマンドやプログラムを実行する単位である Task をグループ化したものになります。Job の作成時は以下の様に利用する Pool を指定することに加え、必要な場合は環境変数を指定することも可能です。

Job の配下に Task を作成する画面は以下の様になります。今回は以下の様に hello world と出力するだけの Task となります。管理者権限で実行可能なことに加え、冒頭で説明した Application Package も利用可能なことが分かります。

こちらを実際に実行すると以下の様にすぐ実行が完了し、特にエラー等が発生しなければ Completed になります。

実行が完了した task01 をクリックする詳細画面となり、標準出力・標準エラー出力がそれぞれファイルとして格納されていることが分かります。stdout.txt を開くと hello world が出力されていることが確認できます。

Application を利用する際の TIPS

せっかく Azure Batch を利用するので、OS 標準コマンドだけでなく作成したアプリケーションを利用したいこともあるでしょう。その場合、zip 形式にパッケージングしたファイルを Azure Storage にアップロードします。しかし、その前に zip ファイルを上げる先の Azure Storage を Azure Batch アカウントとリンクさせましょう。以下の画面の様になっていれば設定完了です。

今回の場合、Azure Storage にアップロードする zip ファイルは以下のようにパッケージしました。自作の python ファイルを含んでいます。

test01.zip
        readme.md
        test01.py

ポータルのウィザードに従って正常にアップロード出来れば以下の様になります。

Job のセクションで紹介した通り、Job 作成時にアップロードした Application を指定します。加えて以下の様にスクリプトを指定します。

bash -c 'python3 $AZ_BATCH_APP_PACKAGE_test01_1_0_0/test01.py'

指定した Application の zip ファイルは特定の作業ディレクトリに展開されています。ディレクトリ名を含む環境変数は以下の記事が参考になります。
learn.microsoft.com

Azure Monitor を利用した場合

Diagnostic Setting を有効化し、Log Analytics ワークスペースにすべての情報を転送する設定をしました。しばらく操作をした後、試しに実行すると以下の様に AzureDiagnostics, AzureMetrics, Usage の三つのテーブルが作成されていました。試しに AzureMetrics にクエリを発行したところ、ノード数がどの程度使われているかを見ている様です。

次に Usage にクエリを投げてみましょう。データは出ていますが、deprecated になっており、別のテーブルを利用することが推奨されています。

最後に AzureDiagnostics へのクエリを実行すると ServiceLog と AuditLog が出力されています。AuditLog については以下の様にポータル操作のログが出力されています。ServiceLog 側はノードの実行数等が出力されていました。

Semantic Kernel/C# で Azure Cognitive Search にベクトル化データを保存する(成功編

今回は掲題のテーマを試してみたいと思います。実は以下の記事にて試した結果、上手く動きませんでした。
normalian.hatenablog.com

以前試した際は Semantic Kernel 側が Azure Cognitive Search のコネクタ対し未実装だったので結果が返ってこないという悲しい自体でした。しかし、以下の記事の様に無事実装がされたようなのでさっそく試してみました。
devblogs.microsoft.com

事前準備

以下のリソースを事前に作成してください。

  • Azure OpenAI リソース、text-davinci-003、text-embedding-ada-002 モデルのデプロイメント
  • Azure Cognitive Search(今回は Japan East を利用

次に Visual StudioC# のプロジェクトを作成作成します。作成したプロジェクトに NuGet でのライブラリインストールします。Visual Studio で NuGet の画面を開き、以下の Microsoft.SemanticKernel と Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch の二つをインストールします。この際に "Include prerelease" にチェックを入れるのを忘れないようにしてください。

ソースコードの作成

C#ソースコードを記載します。私が今回利用したものは以下となります。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Memory.AzureCognitiveSearch;
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Text;
using System.Reflection;
using System.Security.Cryptography;

namespace MyOpenAITest
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            // Azure OpenAI parameters
            const string deploymentName_chat = "text-davinci-003";
            const string endpoint = "https://your-azureopenai-endpoint.openai.azure.com/";
            const string openAiKey = "your-azureopenai-key";
            const string deploymentName_embedding = "text-embedding-ada-002";

            const string azuresearchEndpoint = "https://your-azuresearch-endpoint.search.windows.net";
            const string azuresearchKey = "your-azuresearch-key";
            const string azuresearchIndexname = "SKGitHub";

            Console.WriteLine("== Start Applicaiton ==");

            var builder = new KernelBuilder();
            builder.WithAzureTextCompletionService(
                     deploymentName_chat,
                     endpoint,
                     openAiKey);
            builder.WithAzureTextEmbeddingGenerationService(
                deploymentName_embedding,
                endpoint,
                openAiKey);
            builder.WithMemoryStorage(new AzureCognitiveSearchMemoryStore(azuresearchEndpoint, azuresearchKey));

            var githubFiles = new Dictionary<string, string>()
            {
                ["https://github.com/microsoft/semantic-kernel/blob/main/README.md"]
                    = "README: Installation, getting started, and how to contribute",
                ["https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/02-running-prompts-from-file.ipynb"]
                    = "Jupyter notebook describing how to pass prompts from a file to a semantic skill or function",
                ["https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/00-getting-started.ipynb"]
                    = "Jupyter notebook describing how to get started with the Semantic Kernel",
                ["https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT"]
                    = "Sample demonstrating how to create a chat skill interfacing with ChatGPT",
                ["https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs"]
                    = "C# class that defines a volatile embedding store",
                ["https://github.com/microsoft/semantic-kernel/tree/main/samples/dotnet/KernelHttpServer/README.md"]
                    = "README: How to set up a Semantic Kernel Service API using Azure Function Runtime v4",
                ["https://github.com/microsoft/semantic-kernel/tree/main/samples/apps/chat-summary-webapp-react/README.md"]
                    = "README: README associated with a sample starter react-based chat summary webapp",
            };

            var kernel = builder.Build();
            Console.WriteLine("Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.");
            var i = 0;
            foreach (var entry in githubFiles)
            {
                await kernel.Memory.SaveReferenceAsync(
                    collection: azuresearchIndexname,
                    description: entry.Value,
                    text: entry.Value,
                    externalId: entry.Key,
                    externalSourceName: "GitHub"
                );
                Console.WriteLine($"  URL {++i} saved");
            }

            string ask = "I love Jupyter notebooks, how should I get started?";
            Console.WriteLine("===========================\n" +
                                "Query: " + ask + "\n");

            //From here, search with vector data store
            var memories = kernel.Memory.SearchAsync(azuresearchIndexname, ask, limit: 5, minRelevanceScore: 0.77);

            i = 0;
            await foreach (MemoryQueryResult memory in memories)
            {
                Console.WriteLine($"Result {++i}:");
                Console.WriteLine("  URL:     : " + memory.Metadata.Id);
                Console.WriteLine("  Title    : " + memory.Metadata.Description);
                Console.WriteLine("  Relevance: " + memory.Relevance);
                Console.WriteLine();
            }
        }
    }
}

アプリケーションの実行

実行すると以下のメッセージが返ってきました。 "Result 1:" が返ってきて、ちゃんと結果も記載されています。

Azure ポータル側でインデックスを確認すると、以下の様にドキュメントデータが保存されていることが分かります。

どの様なフィールドが作成されるかについては、以下のソースコードを参照すると理解が早い認識です。
semantic-kernel/dotnet/src/Connectors/Connectors.Memory.AzureCognitiveSearch/AzureCognitiveSearchMemoryRecord.cs at main · microsoft/semantic-kernel · GitHub

何とか無事に検索できることは分かりましたが、以下のソースコードを眺めている限り Azure Cognitive Search で利用可能なはずの Semantic Search を有効化するポイントがなく、どうやら実装を見送ったことが分かります。2023年7月時点でようやく Semantic Kernel で Azure Cognitive Search ベクトルデータストアとして活用することができるようになりましたが、急激に発展を遂げるこの分野、まだまだ目が離せないようです。
github.com

Azure Files のプライベートエンドポイントの IP アドレスを固定にする

今回は掲題のテーマを備忘録程度に記載したいと思います。ご存じの方はいらっしゃると思いますが、プライベートエンドポイントで固定 IP を振ることができる機能が2022年10月時点で提供されております。
azure.microsoft.com

しかし、たまたまポータルで Azure Files のにプライベートエンドポイントを作成したら固定 IP アドレスが割り振れず(嘘でした。追記を参照下さい)、作成後は IP アドレスが変更できませんでした。その結果、コマンドで試したら固定 IP が割り振れたので、以下の私が利用したスクリプトを備忘録的に張り付けておきます。

$resourcegroupname="xxxxxxxxx"
$storageaccountname="xxxxxxxxx"
$vnetname="xxxxxxxxx"
$subnetname="xxxxxxxxx"

storageAccountId=$(az storage account show --name $storageaccountname --resource-group $resourcegroupname  --query "id" --output tsv)

az network private-endpoint create \
    --name "myendpoint" \
    --resource-group $resourcegroupname \
    --vnet-name $vnetname \
    --subnet $subnetname \
    --private-connection-resource-id $storageAccountId \
    --group-id file \
    --connection-name "myConnection" \
    --ip-config name=ipconfig-1 group-id=file member-name=file private-ip-address=10.0.1.10 \

上記のスクリプトを実行すると以下の様にポータル上で IP アドレスが固定になっていることが確認できます。ご参考まで。

追記:後で確認したら以下の様にポータルで設定できました(汗

Semantic Kernel/C# をフロントエンド、LangChain/Python をバックエンドとして Azure OpenAI を使えるか?

未だ Semantic Kernel を勉強中の身ですが、C# でもベクトルデータを利用できるコードを書けるのは非常にナイスです。Python 側も当然強力な言語ですが、Web でのフロントエンド構築を含むユーザインタフェースを作成する際は C# 等を利用することが多い認識です。以下の記事で C# での Azure OpenAI 利用を試しましたが、C# を利用することで様々な Azure 機能と連携しやすくなるのも大きな利点あり、個人的には特に Azure 上で Application Insights 等のロギングライブラリと連携が容易になる点が魅力的です。
normalian.hatenablog.com

今回は Azure PostgreSQL をデータストアとして Semantic Kernel/C# が フロントエンド、LangChain/Python をバックエンドとして Azure OpenAI を使えるかということを検証したいと思います。

LangChain/Python 側でドキュメントをベクトルデータ化して PosgreSQL に保存する

さっそく利用したソースコードの紹介となりますが、以下となります。もともと Azure Cognitive Search 側で利用していたソースコードを PosgreSQL に変えただけです。

import os
import re
from typing import List, Tuple
from dotenv import load_dotenv

from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores.pgvector import PGVector
from langchain.document_loaders import WebBaseLoader
from langchain.text_splitter import TokenTextSplitter, TextSplitter
import hashlib

deployment="text-embedding-ada-002"
chunk_size=1
openai_api_type="azure"
openai_api_key="your-azure-openai-key"
openai_api_base="https://your-azure-openai-endpoint.openai.azure.com"
openai_api_version="2023-03-15-preview"

embeddings = OpenAIEmbeddings(
    deployment=deployment, 
    chunk_size=chunk_size,
    openai_api_type=openai_api_type,
    openai_api_key=openai_api_key,
    openai_api_base=openai_api_base,
    openai_api_version=openai_api_version)

# このコレクション名がデータストア時に反映されない、なぜだ…
COLLECTION_NAME = "test02"

CONNECTION_STRING = PGVector.connection_string_from_db_params(
    driver=os.environ.get("PGVECTOR_DRIVER", "psycopg2"),
    host=os.environ.get("PGVECTOR_HOST", "your-severname.postgres.database.azure.com"),
    port=int(os.environ.get("PGVECTOR_PORT", "5432")),
    database=os.environ.get("PGVECTOR_DATABASE", "database-name"),
    user=os.environ.get("PGVECTOR_USER", "your-username"),
    password=os.environ.get("PGVECTOR_PASSWORD", "your-password"), # パスワードに @ を含めると文字列パースでエラーになった(汗
)

vector_store = PGVector(
    collection_name=COLLECTION_NAME,
    connection_string=CONNECTION_STRING,
    embedding_function=embeddings,
)

source_url = 'https://your-blobstorage-account.blob.core.windows.net/your-document'

print("############# start #1 ")
chunk_size = 400
chunk_overlap = 100
document_loaders = WebBaseLoader
text_splitter: TextSplitter = TokenTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
documents = document_loaders(source_url).load()
docs = text_splitter.split_documents(documents)

print("############# start #2 ")
# Remove half non-ascii character from start/end of doc content (langchain TokenTextSplitter may split a non-ascii character in half)
# Azure Cognitive Search だと要らなかったが、PosgreSQL だと必要だった
pattern = re.compile(r'[\x00-\x09\x0b\x0c\x0e-\x1f\x7f\u0080-\u00a0\u2000-\u3000\ufff0-\uffff]')  # do not remove \x0a (\n) nor \x0d (\r)
for(doc) in docs:
    doc.page_content = re.sub(pattern, '', doc.page_content)
    if doc.page_content == '':
        docs.remove(doc)
            
keys = []
for i, doc in enumerate(docs):
    # Create a unique key for the document
    # print(doc)
    source_url = source_url.split('?')[0]
    filename = "/".join(source_url.split('/')[4:])
    hash_key = hashlib.sha1(f"{source_url}_{i}".encode('utf-8')).hexdigest()
    hash_key = f"doc:{COLLECTION_NAME}:{hash_key}"
    keys.append(hash_key)
    doc.metadata = {"source": f"[{source_url}]({source_url}_SAS_TOKEN_PLACEHOLDER_)" , "chunk": i, "key": hash_key, "filename": filename}

print("############# start #3 ")
vector_store.add_documents(documents=docs)

print("############# end")

ソースコード内に色々と TIPS を記載しましたが、特殊文字処理が必要など、いくつかの対応が必要でした。上記のソースコードを実行すると以下の様に public."langchain_pg_collection" と public.langchain_pg_embedding という二つのスキーマが PosgreSQL 上に作成されます。スキーマの詳細については以下を参照下さい。

LangChain/Python の場合に作成されるスキーマ名を調べたところ、以下の様にハードコードされている箇所がありました。
github.com
なぜ collection 名を指定しても反映されなかったのかはやや腑に落ちませんが、本題ではないので話題を先に進めたいと思います。

ベクトル形式でドキュメントがデータストアに格納されたので、次に Sematic Kernel 側で C#ソースコードを書いてみました。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Memory.Postgres;
using Microsoft.SemanticKernel.Memory;
using Npgsql;
using Pgvector.Npgsql;

namespace MyOpenAITest
{
    class Program
    {
        public static async Task Main(string[] args) {
            // Azure OpenAI parameters
            const string deploymentName_chat = "text-davinci-003";
            const string endpoint = "https://your-azureopenai-endpoint.openai.azure.com/";
            const string openAiKey = "your-azureopenai-key";
            const string deploymentName_embedding = "text-embedding-ada-002";

            // posgresql connection string
            const string connectionString = "Host=your-posgresql-servername.postgres.database.azure.com;Port=5432;Database=your-dbname;User Id=your-usename;Password=your-password";

            Console.WriteLine("== Start Applicaiton ==");

            NpgsqlDataSourceBuilder dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
            dataSourceBuilder.UseVector();
            NpgsqlDataSource dataSource = dataSourceBuilder.Build();
            PostgresMemoryStore memoryStore = new PostgresMemoryStore(dataSource, vectorSize: 1536/*, schema: "public" */);

            IKernel kernel = Kernel.Builder
                // .WithLogger(ConsoleLogger.Log)
                .WithAzureTextCompletionService(deploymentName_chat, endpoint, openAiKey)
                .WithAzureTextEmbeddingGenerationService(deploymentName_embedding, endpoint, openAiKey)
                .WithMemoryStorage(memoryStore)
                .WithPostgresMemoryStore(dataSource, vectorSize: 1536, schema: "public")
                .Build();

            string ask = "How can I Associate EA and CSP subscriptions into same AAD tenant?";
            Console.WriteLine("===========================\n" +
                                "Query: " + ask + "\n");

            var memories = kernel.Memory.SearchAsync("langchain_pg_collection", ask, limit: 5, minRelevanceScore: 0.77);

            int j = 0;
            await foreach (MemoryQueryResult memory in memories)
            {
                Console.WriteLine($"Result {++j}:");
                Console.WriteLine("  URL:     : " + memory.Metadata.Id);
                Console.WriteLine("  Title    : " + memory.Metadata.Description);
                Console.WriteLine("  Relevance: " + memory.Relevance);
                Console.WriteLine();
            }
            Console.WriteLine("== End Application ==");
        }
    }
}

上記を実行すると以下の様に '42703: column "key" does not exist というエラーが発生します。どうやら key という column が無いようです。

この結果から、Semantic Kernel/C# と LangChain/Python ではベクトルストアへのデータ格納時にスキーマが異なるのではとの推察をしました。

Semantic Kernel/C# 側でベクトルデータを PosgreSQL に格納してスキーマを確認する

次に Semantic Kernel/C# 側でベクトルデータを PosgreSQL に格納してみます。以下の様に GitHub のドキュメント文字列をベクトル化し、ベクトルデータストア(この場合は PosgreSQL)に格納します。

using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.Memory.Postgres;
using Microsoft.SemanticKernel.Memory;
using Npgsql;
using Pgvector.Npgsql;

namespace MyOpenAITest
{
    class Program
    {
        public static async Task Main(string[] args)
        {
            // Azure OpenAI parameters
            const string deploymentName_chat = "text-davinci-003";
            const string endpoint = "https://your-azureopenai-endpoint.openai.azure.com/";
            const string openAiKey = "your-azureopenai-key";
            const string deploymentName_embedding = "text-embedding-ada-002";

            // posgresql connection string
            const string connectionString = "Host=your-posgresql-endpoint.postgres.database.azure.com;Port=5432;Database=sk_demo;User Id=your-posgresql-username;Password=your-posgresql-password";

            Console.WriteLine("== Start Applicaiton ==");

            NpgsqlDataSourceBuilder dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
            dataSourceBuilder.UseVector();
            NpgsqlDataSource dataSource = dataSourceBuilder.Build();
            PostgresMemoryStore memoryStore = new PostgresMemoryStore(dataSource, vectorSize: 1536/*, schema: "public" */);

            IKernel kernel = Kernel.Builder
                // .WithLogger(ConsoleLogger.Log)
                .WithAzureTextCompletionService(deploymentName_chat, endpoint, openAiKey)
                .WithAzureTextEmbeddingGenerationService(deploymentName_embedding, endpoint, openAiKey)
                .WithMemoryStorage(memoryStore)
                .WithPostgresMemoryStore(dataSource, vectorSize: 1536, schema: "public")
                .Build();

            const string memoryCollectionName = "SKGitHub";

            var githubFiles = new Dictionary<string, string>()
            {
                ["https://github.com/microsoft/semantic-kernel/blob/main/README.md"]
                    = "README: Installation, getting started, and how to contribute",
                ["https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/02-running-prompts-from-file.ipynb"]
                    = "Jupyter notebook describing how to pass prompts from a file to a semantic skill or function",
                ["https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/00-getting-started.ipynb"]
                    = "Jupyter notebook describing how to get started with the Semantic Kernel",
                ["https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT"]
                    = "Sample demonstrating how to create a chat skill interfacing with ChatGPT",
                ["https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs"]
                    = "C# class that defines a volatile embedding store",
                ["https://github.com/microsoft/semantic-kernel/tree/main/samples/dotnet/KernelHttpServer/README.md"]
                    = "README: How to set up a Semantic Kernel Service API using Azure Function Runtime v4",
                ["https://github.com/microsoft/semantic-kernel/tree/main/samples/apps/chat-summary-webapp-react/README.md"]
                    = "README: README associated with a sample starter react-based chat summary webapp",
            };

            Console.WriteLine("Adding some GitHub file URLs and their descriptions to a volatile Semantic Memory.");
            int i = 0;
            foreach (var entry in githubFiles)
            {
                await kernel.Memory.SaveReferenceAsync(
                    collection: memoryCollectionName,
                    description: entry.Value,
                    text: entry.Value,
                    externalId: entry.Key,
                    externalSourceName: "GitHub"
                );
                Console.WriteLine($"  URL {++i} saved");
            }

            string ask = "How can I Associate EA and CSP subscriptions into same AAD tenant?";
            Console.WriteLine("===========================\n" +
                                "Query: " + ask + "\n");

            var memories = kernel.Memory.SearchAsync(memoryCollectionName, ask, limit: 5, minRelevanceScore: 0.77);
            //var memories = kernel.Memory.SearchAsync("langchain_pg_collection", ask, limit: 5, minRelevanceScore: 0.77);

            int j = 0;
            await foreach (MemoryQueryResult memory in memories)
            {
                Console.WriteLine($"Result {++j}:");
                Console.WriteLine("  URL:     : " + memory.Metadata.Id);
                Console.WriteLine("  Title    : " + memory.Metadata.Description);
                Console.WriteLine("  Relevance: " + memory.Relevance);
                Console.WriteLine();
            }

            Console.WriteLine("== End Application ==");
        }
    }
}

上記を実行後、PosgreSQL に接続した結果、スキーマは以下の様になりました。御覧の通り、Semantic Kernel 側で作成したスキーマには text 型のデータを格納する key という名前のカラムが存在しますが、LangChain 側には存在しないカラムなことが分かります。

そのほかにも、embedding されたデータを格納するカラムのデータ型・カラム名は同じなことは分かりますが、その他は異なるカラムでのデータが格納されていることが分かると思います。

「Semantic Kernel/C# をフロントエンド、LangChain/Python をバックエンドとして Azure OpenAI を使えるか?」の結論

本ポストの掲題でもあったテーマについては、実現は難しいというのが結論になると思います。両ライブラリで格納されたベクトルデータだけは同じですが、他のカラム等々は異なるデータ形式で格納されているので、現実的にはこちらを上手くマッピングさせて相互運用というのは難しいのではと考えています。

上記に加え、「既に PosgreSQL に格納されているベクトル化データをそのまま別のデータストア(Azure Cognitive Search 等)に移行したい」というベクトルデータの格納コンポーネントを変えるという処理も、今回の結果を踏まえれば難しいという推察ができます。データのマッピングを考えるより、新しくベクトル化データを作り直す方が現実的でしょう。

結果的に期待を満たすのでなく、実現が難しいという知見を得るにとどまってしまいましたが、こちらの検証結果が皆様のお役に立てば幸いです。

Microsoft Defender for Cloud にオレオレコンプライアンスを登録する

このブログを読むような方々はコンプライアンスと聞くと身構えたりテンションが下がるフレンズだと思います(けものフレンズはもう5年前か…)。できれば関わりたくないですが、社畜業を営む我々にとって避けて通れないのもまた事実。可能であれば何とか楽に「コンプラ対応ばっちりっすわ~」と流したいものでしょう。Microsoft は以下の様なコンプライアンスオファリングと呼ばれるサイトがあり、膨大な情報が提供されているのでこれで大丈夫!
learn.microsoft.com
と言いたいところですが、実際のところ「そんな大量の情報読めない」「コンプライアンスのどの項目が Azure のどの機能に対応しているか分からん」というのが実態でしょう。その場合にお勧めしている機能が Microsoft Defender for Cloud の Regulatory compliance です。画面を眺めた方がわかりやすいので、まずは以下のスクリーンショットを参照下さい。

対応しているコンプライアンス一覧に関しては上記リンクを参照して頂ければと思いますが、この例では PCI-DSS での項目が表示されています。仮想マシンや仮想ネットワーク等の IaaS 機能だけでなく、Azure PaaS 機能に加えて AWSGCP にもアカウントリンクして情報の一元管理が可能です。
特に PCI-DSS については、Microsoft Defender for Cloud で分類されている項目がそのまま PCI-DSS 自体の項目に対応しているので、自身のサービスが PCI-DSS に順守する場合に何を確認したら良いか一元的に Azure 上で管理できるということがわかります。
learn.microsoft.com
learn.microsoft.com
後に詳細に解説しますが、上記二つ目のリンクで参照する通り Regulatory compliance の中身は Azure Policy であり、いくつかのコンプライアンスMicrosoft 側より提供されています。今回はこちらに対してオレオレのコンプライアンスを登録する方法を紹介します。

しかし、念のためご注意頂きたいのが、仮に Microsoft Defender for Cloud の項目にすべて順守したからと言ってもコンプライアンス準拠は保証されない点です。これまた PCI-DSS を例に挙げると Qualified Security Assessor (QSA) と呼ばれる人間のみ審査可能であり、外部ベンダーが行うことはできません。そうはいっても通常は「どうやってコンプライアンス対応したらいいか分からない」からスタートせざるを得ない状況が多い中、非常に有用度が高いサービスだともいえる認識です。

オレオレコンプライアンスを登録する

ガバナンスやコンプライアンスという言葉が大好きな自分で手を動かさない方々にとって、ルールを敷いたら現場で順守が当たり前、仮に何かあったら現場での実施ルールが足りなかった等の揚げ足取りの地獄がまっており、実際に作業する方々(特に現場のリーダー層等)にとっては辟易することが多いと思います。Microsoft Defender for Cloud の Regulatory compliance に対し、オレオレコンプライアンスを登録することができるとしたらどうでしょう。どのようなルールが実施されているかは一覧化され、順守状況の可視化もされて一目瞭然となり、ガバナンス運用の手間は大きく減らせることでしょう(その手のガバナンスチームとのコミュニケーションも難関という点は一旦不問でお願いいたします)。
上記で記載した通り Regulatory compliance の中身は Azure Policy であり、特に Initiative と呼ばれる個別 Policy の集合体となります。さっそく自分向けの Initiative を作って試してみましょう。まずは Azure ポータルを開き、Azure Policy の画面から以下の様に Initiative の作成を実施します。

まず最初にルール名と割り当て領域を策定しますが、それ以上に大事なのが Category に Regulatory Compliance を選択することです。これにより後で Microsoft Defender for Cloud の管理画面に登録することが可能になります。

Initiative の作成時、以下の様に Control タブから各 Policy 向けのグルーピングができます(なぜか Policy 選択後なので先にしてほしい…)。こちらで作成する Control がそのまま Regulatory compliance 画面で表示されるグルーピング名になるので注意してください。

次に Policies タブで利用する Azure Policy を選択します。この画面でどの Control に属するかも選択可能です。参考のため、ここではあえてどの Control にも含めない Policy を一つ追加しました。

Initiative の作成後は Assignment を行う必要があります。今回は割愛しますが、監査対象である subscription に作成した Initiative を割り当ててください

当然これだけでは Regulatory compliance 画面に表示されません。ここからは Microsoft Defender for Cloud の画面に戻り Environment setting メニューを選び、監査対象の Subscription の「…」をクリックして表示される Edit settings を選択します。

こちらで遷移した先の画面からさらに Security Policy メニューを選択することで Your custom initiatives 画面が表示されるので、ここから自身で作成した Initiative を追加します。

これでもう終わったと思ってしまうせっかちさんも多いと思いますが、Regulatory Compliance の監視周期は即時自実行ではありません。以下の記事を参照して頂ければと思いますが、Azure Policy や Initiative 自体は 15 分後には利用可能になりますが、Regulatory Compliance については24時間ごとの実行となります。つまりその間は Microsoft Defender for Cloud の画面に反映されません。
learn.microsoft.com

それが待てないせっかちさんの貴方に朗報です。Azure cli 等を利用することで即時実行が可能です。Get policy compliance data - Azure Policy | Microsoft Learn を参考に以下のコマンドを実行して即時反映が可能です(といっても私の場合、コマンド自体の実行完了には15分程度かかりました)。

az login
az account set -s "your subscription id"
az policy state trigger-scan

コマンド実行後は Microsoft Defender for Cloud の Regulatory Compliance の画面からオレオレコンプライアンスが無事に参照可能です。

上記で分かる通り、Initiative 作成時の Control 毎にグルーピングされ、特に Control を割り当てなかったものは Additional Recommendations というカテゴリになります。

以上でオレオレコンプライスを Microsoft Defender for Cloud で表示する方法を紹介しました。他のコンプライアンス含めてマルチクラウドマルチプラットフォームが一元管理可能できるので有用度は高いのではないでしょうか。