0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AMPLS で App Insights を完全閉域化検証

0
Last updated at Posted at 2026-05-06

本記事は GitHub Copilot CLI による自動検証ワークフローのレポートです。人間のレビューを経て公開しています。コマンド出力やリソース ID は検証環境のもの(機密値はマスキング済み)です。

本検証は dev 環境です。本番採用時は Bastion / Jumpbox の運用、監査要件、AMPLS の DNS 設計(複数 VNet・ハブスポーク)など追加考慮が必要です。

TL;DR (3 行)

  • AMPLS PrivateOnly で Application Insights / Log Analytics を完全閉域化、Functions のテレメトリを VNet 経由で ロスなく 到達確認(Jumpbox 8 calls = AppI 8 件 = LAW 8 件)
  • 三層閉域反証 に成功: Function App inbound / AppI ingestion / LAW query すべて Public IP から HTTP 403 で拒否(実コマンド + レスポンス本文付き)
  • EP1 + 閉域化の現実的な罠(allowSharedKeyAccess=false GA 未対応、AppServiceHTTPLogs 非対応、AMPLS Diagnostic Settings 非対応、az monitor private-link-scope show の CLI バグ等)を整理

背景・目的

Azure Functions を「Application Insights / Log Analytics への テレメトリ送信を VNet 内 Private Endpoint 経由のみ に限定」できるかを 1 ResourceGroup / 1 VNet で検証した。具体的に確認したいのは次の 4 点。

  1. AMPLS の ingestionAccessMode = PrivateOnly / queryAccessMode = PrivateOnlyPublic ingestion / query が確実に拒否 されること
  2. Functions の VNet integration (WEBSITE_VNET_ROUTE_ALL=1) からのテレメトリが AMPLS PE → AppI(workspace-based) → LAW に到達すること
  3. Workspace-based Application Insights における Private Link 配線(amplsScopedResources)の正しい書き方
  4. Functions inbound の Private Endpoint と組み合わせた 完全閉域(Public IP からアクセス不可)が実現すること

アーキテクチャ

要点:

  • テレメトリ閉域経路 (青): Function App → VNet integration → pep-ampls (groupId azuremonitor) → AMPLS → AppI → LAW
  • inbound 閉域経路 (緑): Developer → Bastion → Jumpbox → pep-func → Function App
  • 拒否経路 (赤): Public Internet → 公開 URL → 403(Function inbound / AppI ingestion / LAW query の 三層
  • Private DNS Zone 9 個 vnet linked: azurewebsites.net, monitor.azure.com, ods.opinsights.azure.com, oms.opinsights.azure.com, agentsvc.azure-automation.net, blob/file/queue/table.core.windows.net
  • AAD 認証: APPLICATIONINSIGHTS_AUTHENTICATION_STRING=ClientId=<UAMI>;Authorization=AAD で UAMI 経由送信

構築のポイント(Bicep)

AMPLS モジュール(ここが今回の主役)

Microsoft.Insights/privateLinkScopes@2021-07-01-previewaccessModeSettings を指定。API バージョンが古いと PrivateOnly が指定できないので注意。

infra/modules/ampls.bicep
resource ampls 'Microsoft.Insights/privateLinkScopes@2021-07-01-preview' = {
  name: amplsName
  location: 'global'
  tags: tags
  properties: {
    accessModeSettings: {
      ingestionAccessMode: 'PrivateOnly'
      queryAccessMode: 'PrivateOnly'
    }
  }
}

resource scopedLaw 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = {
  parent: ampls
  name: 'scoped-law'
  properties: { linkedResourceId: logAnalyticsWorkspaceId }
}

resource scopedAppI 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = {
  parent: ampls
  name: 'scoped-appi'
  properties: { linkedResourceId: appInsightsId }
}

Workspace-based Application Insights

LAW / AppI 両方で publicNetworkAccessForIngestion=Disabled / publicNetworkAccessForQuery=Disabled を必ず指定する。これがないと AMPLS PrivateOnly にしても「保険なし」状態になる。

infra/modules/monitoring.bicep
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
  name: logAnalyticsName
  location: location
  tags: tags
  properties: {
    sku: { name: 'PerGB2018' }
    retentionInDays: 30
    publicNetworkAccessForIngestion: 'Disabled'
    publicNetworkAccessForQuery: 'Disabled'
  }
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: appInsightsName
  location: location
  tags: tags
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalytics.id
    IngestionMode: 'LogAnalytics'
    publicNetworkAccessForIngestion: 'Disabled'
    publicNetworkAccessForQuery: 'Disabled'
  }
}

Functions の VNet integration と AppI 接続

infra/modules/functions.bicep
properties: {
  virtualNetworkSubnetId: snetFunctionsId
  // ...
  appSettings: union(baseSettings, {
    WEBSITE_VNET_ROUTE_ALL: '1'
    APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
    APPLICATIONINSIGHTS_AUTHENTICATION_STRING: 'ClientId=${managedIdentityClientId};Authorization=AAD'
  })
}

WEBSITE_VNET_ROUTE_ALL=1全 outbound を VNet 経由 にすることで、AppI への送信が pep-ampls に向かう。

検証結果サマリ (14 シナリオ / すべて PASS)

ID シナリオ 結果
S1 全リソース provisioningState=Succeeded
S2 Function App が VNet 統合
S3 AMPLS PE が Approved
S4 AMPLS scopedResources に AppI/LAW
S5 AMPLS access mode = PrivateOnly (両方)
S6 Functions に AppI 接続設定 (AAD)
S7 AppI が Workspace-based
S8 Functions テレメトリが閉域経路で AppI/LAW 到達
S9 Public network access=Disabled (4 項目)
S10 Public ingestion 拒否 (403)
S11 Public query 拒否 (403)
S12 Function inbound 公開 URL 403
S13 Diagnostic Settings → LAW 流入
S14 Storage 閉域 (PE 4 + publicAccess=Disabled)

S8: テレメトリ閉域到達(★最重要)

  1. Jumpbox から Invoke-WebRequest https://<func>.azurewebsites.net/api/hello?name=verify-S88 回 流す(Private DNS で pep-func の PE IP に解決され HTTP 200)
  2. ingestion latency 5 分待機
  3. Jumpbox MI に Monitoring Reader を付与し、AppI / LAW Query API を private 経路 から実行
--- QUERY: requests | where timestamp > ago(45m) | summarize cnt=count() by resultCode
rows: [["200", 8]]

--- QUERY: AppRequests | where TimeGenerated > ago(1h) | summarize cnt=count() by ResultCode
rows: [["200", 8]]

--- QUERY: customEvents | where name == "public-probe-S10" | summarize cnt=count()
rows: [[0]]

8 calls = AppI 8 件 = LAW 8 件で完全一致。同時に「公開経路で送ろうとした public-probe-S10 イベントは AppI に 0 件」も独立確認。「閉域化したらテレメトリが消えるのでは?」という不安をエビデンスで払拭できた。

S10: Public ingestion 拒否 (403)

curl -i -X POST https://japaneast-1.in.applicationinsights.azure.com/v2/track \
  -H "Content-Type: application/json" \
  -d '[{"name":"Microsoft.ApplicationInsights.Event","time":"...","iKey":"<INSTRUMENTATION_KEY>","data":{"baseType":"EventData","baseData":{"ver":2,"name":"public-probe-S10"}}}]'

レスポンス:

{
  "itemsReceived": 1,
  "itemsAccepted": 0,
  "appId": null,
  "errors": [
    {
      "index": 0,
      "statusCode": 403,
      "message": "Component has publicNetworkAccessForIngestion=Disabled"
    }
  ]
}

S11: Public query 拒否 (403)

TOKEN=$(az account get-access-token --resource https://api.loganalytics.io --query accessToken -o tsv)
curl -i -H "Authorization: Bearer $TOKEN" \
  "https://api.loganalytics.io/v1/workspaces/<LAW_CUSTOMER_ID>/query?query=AppRequests%20%7C%20take%201"

レスポンス(clientIp がエラーに反射されており IP ベース拒否 の確証となる):

{
  "error": {
    "code": "InsufficientAccessError",
    "innererror": {
      "code": "NspValidationFailedError",
      "message": "Access to workspace 'log-pappi-dev-jpe' from '<DEVELOPER_PUBLIC_IP>' is denied. ..."
    }
  }
}

S12: Function inbound 公開 URL 403

curl -i https://func-pappi-dev-jpe.azurewebsites.net/api/hello
# HTTP/1.1 403  (resolved to App Service shared front-door IP)

Function inbound / AppI ingestion / LAW query の三層すべてが Public IP から拒否される ことを実コマンドで実証完了。

S5: AMPLS access mode の確認は REST 必須

az monitor private-link-scope show は CLI バグで accessModeSettings を返さない(後述 H2)。REST 直叩きで確認:

az rest --method get \
  --url "https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/rg-private-app-insights-dev/providers/microsoft.insights/privateLinkScopes/ampls-pappi-dev-jpe?api-version=2021-07-01-preview"
"accessModeSettings": {
  "exclusions": null,
  "ingestionAccessMode": "PrivateOnly",
  "queryAccessMode": "PrivateOnly"
}

ハマったポイント(★ 強調)

H1: AMPLS は Diagnostic Settings をサポートしない

ERROR: (ResourceTypeNotSupported) The resource type
'microsoft.insights/privatelinkscopes' does not support diagnostic settings.

AMPLS 自体のアクセスログは取得不可。Activity Log / NSG flow logs / scoped 先(AppI・LAW)側のログで間接観測する設計にする。「AMPLSAccess テーブル」は現状存在しない。

H2: az monitor private-link-scope showaccessModeSettings を返さない

az CLI が 2019-10-17-preview 等の古い API version を内部で使っているため accessModeSettingsnull になる。accessModeSettings2021-07-01-preview 以降のフィールド。REST 直叩き (az rest) を CI に組み込む のが安全。

H3: EP1 + allowSharedKeyAccess=false は GA 未対応

identity-based content share(AzureWebJobsStorage__blobServiceUri の triple 構成)を EP1 で組むと Function ホストが起動しない。Functions Premium plan では content share の identity-based access が GA 未対応(Flex Consumption / 一部 Linux Consumption は対応)。

→ 検証スコープが「AppI / LAW の閉域化」だったので、Storage は allowSharedKeyAccess=true + connection string で妥協。Storage 自体は PE 4 種で閉域(S14)。本気で keyless にしたい場合は Flex Consumption への移行を検討

H4: kind=functionapp,linux の Diagnostic 対応カテゴリ

AppServiceHTTPLogs を Function App の Diagnostic Settings に含めるとデプロイ失敗する。kind=functionapp,linuxFunctionAppLogs のみ対応(Web App 用カテゴリは選べない)。Bicep では categoryGroup ではなく category: 'FunctionAppLogs' を明示指定。

H5: azd --no-prompt で secure parameter が未供給だと停止

adminPassword(Jumpbox 用 secure string)を hook 側で生成する設計だと azd up --no-promptvalue not provided で止まる。azd は secure parameter は必ず env から読む ため、hooks/preprovision.sh の冒頭で azd env set ADMIN_PASSWORD ... を呼んでおく必要がある。

hooks/preprovision.sh
#!/usr/bin/env bash
set -euo pipefail
if ! azd env get-values | grep -q '^ADMIN_PASSWORD='; then
  azd env set ADMIN_PASSWORD "$(openssl rand -base64 24)Aa1!"
fi

コスト(連続稼働、停止せず)

リソース SKU 日額目安
Functions Premium plan EP1 (1 instance) ≈ ¥1,800
Azure Bastion Basic ≈ ¥800
Jumpbox VM Standard B2ms (Windows) ≈ ¥600
Log Analytics ingestion PerGB2018 (< 1 GB/日) ≈ ¥400
Private Endpoint × 6 ¥1.2/h × 6 ≈ ¥200
Storage Account Standard LRS + 4 PE ≈ ¥150
Public IP (Bastion) Standard ≈ ¥30
AMPLS 課金なし(PE のみ課金) ¥0
合計 ≈ ¥4,800 / 日

EP1 + Bastion + Jumpbox + 6 PE の連続稼働で月 14 万円規模になる。検証完了後は必ず azd down --purge --force か、最低でも Bastion 削除 + VM Stop + Plan を Free に切替する。

まとめ

  • AMPLS PrivateOnly + workspace-based AppI + Functions VNet integration + 6 PE の組合せで、テレメトリ閉域到達と Public 拒否の両方を 1 RG で実証できた
  • 三層閉域反証(Function inbound / AppI ingestion / LAW query すべて 403)と、テレメトリ件数の完全一致(8 = 8 = 8)が同時に取れたのが今回の最大成果
  • 一方で EP1 + 閉域化 には GA 未対応の機能や CLI バグといった現実的な罠が多い。本記事の H1〜H5 を「踏まないためのチェックリスト」として活用してほしい
展開: 主要リソース命名(CAF 準拠)
リソース 名前
Function App func-pappi-dev-jpe
App Insights appi-pappi-dev-jpe
Log Analytics log-pappi-dev-jpe
AMPLS ampls-pappi-dev-jpe
Storage st<project>dev<unique>
UAMI id-pappi-dev-jpe
Bastion bas-pappi-dev-jpe
Jumpbox vm-jump-pappi-dev

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?