ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Microsoft Defender for App Service で Web アプリケーションを保護する

Microsoft Defender for App Service は、App Service のリソースに対する脅威を検出するサービスです。
外部からの攻撃へのセキュリティ対策として、App Service の前に Font Door の WAF をいれることはありましたが、Defender for App Service は使ったことがなかったので試してみました。
docs.microsoft.com

Defender for App Service とは

Defender for App Service は、Microsoft Defender for Cloud(旧 Azure Security Center)のうち、リソースに対する脅威を検出するカテゴリーに分類されます。

App Service 以外にも、次のようなリソース向けのサービスが提供されています。

  • Defender for Servers
  • Defender for Storage
  • Defender for SQL
  • Defender for Containers
  • Defender for Key Vault
  • Defender for Resource Manager
  • Defender for DNS

Defender for App Service で検知できる脅威には、Web サーバー内の不審なプロセスや異常な操作があり、昨年話題になった未解決の DNS とサブドメイン乗っ取りも含まれています。
検知できるアラートの一覧については、公式ドキュメントを参照してください。
docs.microsoft.com

Defender for App Service を有効化する

Defender for Cloud の environment settings から、基本的にはサブスクリプション単位で有効化することが推奨されています。

Defender で保護できるリソース一覧が表示されるので、App Service を有効化します。

App Service の Security でも、Defender が有効化されたことを確認できます。


Defender for App Service のアラートを検証する

実際にテスト用のアラートを発生させて、Defender for App Service を検証してみます。

今回は、App Service の URL の末尾に /This_Will_Generate_ASC_Alert を付けて HTTP リクエストを行うことで発生させられるアラートを試してみます。

$ curl https://<app service name>.azurewebsites.net/This_Will_Generate_ASC_Alert
The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

しばらく待っていると、App Service の Security にセキュリティのアラートが表示されます。

Defender for Cloud の Secirity Alert にも、アラートの詳細が表示されます。


まとめ

Defender for App Service を有効化して、テスト用のアラートを発生させてみました。
Font Door の WAF だけでは保護しきれない脅威もあるので、アプリケーションの要件によっては導入を検討してみてもよさそうだと思いました。

Defender for Storage を試してみた記事は、こちらを参照してください。
gooner.hateblo.jp

Azure Managed Grafana で Azure のメトリクスを監視する

先月、フルマネージドのサービスして Preview での提供が開始された Azure Managed Grafana を試してみました。
従来は VM や AKS に Grafana をインストールする必要があり、過去に記事を書いたこともありました。(ちょっと前の感覚だったけど、5年前だった)
gooner.hateblo.jp

Azure Managed Grafana を作成する

公式ドキュメントの通りに、Azure ポータルからポチポチと作れば OK です。ちなみに、まだ日本リージョンはありません。
作成が完了すると、Public Endpoint で公開された Grafana にアクセスできます。


Grafana のダッシュボードを参照できるメンバーを追加する

Grafana のダッシュボードは、Azure AD の認証認可で保護されています。 3つのロールが用意されているので、Access control (IAM) からメンバーを追加することで参照できるようになります。

  • Grafana Admin
  • Grafana Editor
  • Grafana Viewer


Azure Monitor をデータソースに設定する

Grafana はさまざまなデータソースをサポートしていますが、Azure Managed Grafana では Azure Monitor がデフォルトのデータソースとして用意されています。
Managed Identity で Azure Monitor に認証するように設定し、とりあえず Storage と Cosmos DB のダッシュボードを有効化しておきます。



監視するリソースを設定する

メトリクスを取得するリソースを設定したいので、Azure ポータルの Identity から Permission を追加します。

Monitoring Reader のロールに対して、grafana-target というリソースグループを追加しました。このリソースグループには、Storage と Cosmos DB を作成済みです。

なお、Azure Managed Grafana のリソースグループには、あらかじめ Permission が追加されています。

ダッシュボードを表示する

Azure Storage Insights




Cosmos DB Insights





まとめ

Azure Managed Grafana で Azure のメトリクスを監視してみました。
VM や AKS に Grafana をインストールする場合、Grafana が動いているインフラを監視する Grafana とか欲しくなってしまうので、Azure 上にフルマネージドなサービスとして提供されるのは良いことだと思います。

Azure Cosmos DB Gremlin API を使ってレコメンド機能を作ってみた

前回の記事では、Azure Cosmos DB Gremlin API のグラフデータを Linkurious Enterprise で可視化してみました。
gooner.hateblo.jp
グラフデータベースは、データの関連性を辿れる特徴を活用して、EC サイトなどで購入した商品と関連のある商品をレコメンドするケースにも使われます。
今回は、Azure Cosmos DB Gremlin API を使ってお勧めのアニメをレコメンドしてくれる機能を作ってみます。

データを用意する

今回は Kaggle 上で公開されている Anime Recommendations Database を利用することにしました。
このサイトから、anime.csv と rating.csv の2つのデータをダウンロードできます。

anime_id,name,genre,type,episodes,rating,members
32281,Kimi no Na wa.,"Drama, Romance, School, Supernatural",Movie,1,9.37,200630
5114,Fullmetal Alchemist: Brotherhood,"Action, Adventure, Drama, Fantasy, Magic, Military, Shounen",TV,64,9.26,793665
28977,Gintama°,"Action, Comedy, Historical, Parody, Samurai, Sci-Fi, Shounen",TV,51,9.25,114262
user_id,anime_id,rating
1,20,-1
1,24,-1
1,79,-1

その他にも、ユーザーのマスターデータが欲しかったので、Mockaroo のサイトで適当に 1,000 件を作りました。

f:id:TonyTonyKun:20220319185641p:plain

user_id,user_name
1,vmonni0
2,cdonaghie1
3,araymond2

グラフデータベースを作成する

Azure Portal から Gremlin API を選択して Azure Cosmos DB を作成し、適当な名前で新しい Graph を作成します。

f:id:TonyTonyKun:20220320141701p:plain

ノードを作成する

C# のコンソールアプリを作り、Gremlin.Net を利用してデータをアップロードします。
www.nuget.org

GremlinClient のインスタンスを生成して、ノードやエッジを操作していきます。インスタンスを生成するパラメータはあまり精査しておらず適当です。

var Host = "xxx.gremlin.cosmos.azure.com";
var PrimaryKey = "xxx";
var Database = "sample-database";
var Container = "recommend-graph";
var containerLink = "/dbs/" + Database + "/colls/" + Container;
var gremlinServer = new GremlinServer(Host, 443, enableSsl: true, username: containerLink, password: PrimaryKey);
var connectionPoolSettings = new ConnectionPoolSettings()
{
    MaxInProcessPerConnection = 10,
    PoolSize = 30,
    ReconnectionAttempts = 3,
    ReconnectionBaseDelay = TimeSpan.FromMilliseconds(500)
};
var webSocketConfiguration =
    new Action<ClientWebSocketOptions>(options =>
    {
        options.KeepAliveInterval = TimeSpan.FromSeconds(10);
    });

using (var gremlinClient = new GremlinClient(gremlinServer, new GraphSON2Reader(), new GraphSON2Writer(),
                                    "application/vnd.gremlin-v2.0+json", connectionPoolSettings, webSocketConfiguration))
{
    // ここにロジックを書いていく
}

anime.csv から読み取ったレコードでノードを作成します。12,295 件あります。
anime という label をもつ Vertex を登録し、property には name だけを設定します。

using (var reader = new StreamReader("anime.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    while (csv.Read())
    {
        var record = csv.GetRecord<Anime>();
        var query = $"g.addV('anime').property('id', '{record.anime_id}').property('title', '{record.name}').property('pk', '{record.anime_id}')";
        await gremlinClient.SubmitAsync<dynamic>(query);
    }
}

同じように、user.csv から読み取ったレコードでノードを作成します。
user という label をもつ Vertex を登録し、property には name だけを設定します。

using (var reader = new StreamReader("user.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    while (csv.Read())
    {
        var record = csv.GetRecord<User>();
        var query = $"g.addV('user').property('id', 'user{record.user_id}').property('name', '{record.user_name}').property('pk', 'user{record.user_id}')";
        await gremlinClient.SubmitAsync<dynamic>(query);
    }
}

エッジを作成する

rating-demo.csv から読み取ったレコードでエッジを作成することで、ユーザーが評価するアニメのレイティングを表現します。元データは 780 万件あったので、1,000 人分のレイティング(96,480 件)に減らしました。
rates という Edge を登録し、useranime の関係を表します。

using (var reader = new StreamReader("rating-demo.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    while (csv.Read())
    {
        var record = csv.GetRecord<Rating>();

        try
        {
            var query = $"g.V().hasLabel('user').has('id', 'user{record.user_id}').addE('rates').property('weight', {record.rating}).to(g.V().has('id', '{record.anime_id}'))";
            await gremlinClient.SubmitAsync<dynamic>(query);
        }
        catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "ValueKind")
        {
            // ↓ の例外が発生するが登録自体は成功しているため、正常終了とする
            // JSON type not supported. (Parameter 'ValueKind')
            // Actual value was Number.
        }
    }
}

動作確認用のデータを登録する

レコメンドさせたいユーザーを登録します。

g.addV('user').property('id', 'user9999').property('name', 'test9999').property('pk', 'user9999')

anime.csv には ONE PIECE の作品がいくつか登録されています。

21,One Piece,"Action, Adventure, Comedy, Drama, Fantasy, Shounen, Super Power",TV,Unknown,8.58,504862
4155,One Piece Film: Strong World,"Action, Adventure, Comedy, Drama, Fantasy, Shounen",Movie,1,8.42,85020
12859,One Piece Film: Z,"Action, Adventure, Comedy, Drama, Fantasy, Shounen",Movie,1,8.39,76051
31490,One Piece Film: Gold,"Action, Adventure, Comedy, Drama, Fantasy, Shounen",Movie,1,8.32,18642
16287,One Piece: Romance Dawn,"Action, Comedy, Fantasy, Shounen, Super Power",OVA,1,7.60,8326

このユーザーには、ONE PIECE の関連作品を高評価するエッジを登録します。

g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '21'))
g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '4155'))
g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '12859'))
g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '31490'))
g.V().hasLabel('user').has('id', 'user9999').addE('rates').property('weight', 10).to(g.V().has('id', '16287'))

レコメンドする

この ONE PIECE が好きなユーザーに、お勧めの作品をレコメンドする Gremlin クエリを組み立てていきます。

まず、このユーザーが高評価している作品を取得するクエリ。

g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude')

つぎに、このユーザーが高評価している作品を高評価しているユーザーを取得するクエリ。

g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude')
    .inE('rates').has('weight', gte(8)).outV()

さらに、このユーザーが高評価している作品を高評価しているユーザーが高評価している作品(このユーザーが観た作品は除く)を取得するクエリ。

g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude')
    .inE('rates').has('weight', gte(8)).outV()
    .outE('rates').has('weight', gte(8)).inV().where(neq('exclude'))

さいごに、作品の重複を除外して、評価順にソートするクエリ。

g.V().hasLabel('user').has('id', 'user9999').outE('rates').has('weight', gte(8)).inV().as('exclude')
    .inE('rates').has('weight', gte(8)).outV()
    .outE('rates').has('weight', gte(8)).inV().where(neq('exclude'))
    .dedup().order().by(inE('rates').count(), decr).limit(5).values('title')

さいごの Gremlin クエリを実行した結果が、こちらです。

f:id:TonyTonyKun:20220320150304p:plain

レコメンドの精度を判定するのは難しいですが、幅広い作品が登録されている中で、ONE PIECE と同じ週刊少年ジャンプの「デスノート」や「進撃の巨人」、アクション・アドベンチャー系の「ソードアート・オンライン」がランクインしており、わりと妥当な結果な気がします。

まとめ

Azure Cosmos DB Gremlin API を使って、お勧めのアニメをレコメンドしてくれる機能を作ってみました。アニメのジャンルやタイプを関連づけることで、より精度の高いレコメンドができると思います。
Gremlin.Net を使ってデータをアップロードするコードは、こちらのリポジトリで公開しています。
github.com