tech.guitarrapc.cóm

Technical updates

Deployment Groupを用いてManagedにVMへのデプロイを行う

現状、サーバーサイドの多くはコンテナで動かすことが可能です。そのため、VMに直接アプリケーションをデプロイする機会はかなり減りました。最高ですね。しかし、UWPをはじめとして一定の要件下においてはコンテナ対応が技術的に難しく、VMへのデプロイをせざるを得ないケースがあります。

VM へのデプロイ、古くからおこなわれていますがクラウドネイティブにマネージドにとなると、案外手が限られます。なるべくクラウド側の意図しているであろう方法に沿うように組むのは大事な選択です。

今回は、Azure のVMに対してアプリケーションをデプロイするにあたり、Azure DevOps の Deployment Group を使ってマネージドさを見ながら実現してみましょう。

目次

TL;DR

Azure DevOps Pipeline を使うと、VMへのデプロイ時に動的にDeployment Targetを設定できます。 DevOps Pipeline を使わず、Terraform でVM作成時にDeployment Targetを設定することも可能です。

動的な構成ならAzureDevOps Pipeline 、静的な構成ならTerraform がよいのでオススメです。 いずれの方法でも、VMへのDeployment Targetのインストール/アンインストールがマネージドにハンドルされるので、なるべくこちらを使うといいでしょう。いずれもにしても、「VMにリモートログインしてマニュアルでDeployment Targetをインストールする」のはなるべく避けるのは大事です。

なおDeployment Group を用いるということは、VM Scaleset とは別のコンセプトになるので混ぜるな注意です。

概要

AWS においてVMへのデプロイとなると、Code Deploy を用いてCode Pipeline からの Managedなデプロイが定番です。VMに対してCI/CD側からパッケージを指定して、VM上のエージェントが指示された通り展開するわけです。

これと同様の仕組みをAzure で達成するのが、Azure DevOps のDeployment Group / Deployment Target です。 本稿では、ホストとなるVM群に対して、デプロイ時に停止したVMの開始、Deployment Targetでデプロイを実施、というコストも鑑みつつよくある感じの流れを見てみます。

Deployment Target とは

Deployment Target は、Azure DevOps の用語でデプロイ対象となるホスト(Windows/Linux) をさします。Build Agent と同様に、Deployment Agentがホスト上でサービス稼働し、Azure DevOps でDeployment Group として複数のDeployment Targetがグルーピングさて、生存確認や最終リリースを確認できます。

Deployment Targetは、Azure DevOps Pipeline からのデプロイ指示を受けてデプロイタスクが実行されます。(Build Agent同様ですね) どんな処理を行うかは、DevOps Pipeline 上でタスクとして定義、実行/停止指示ができ、Deployment Targetが入っているホスト上でその処理が直接実行されます。

WinRM/SSHなどを通したリモート展開と比べて考えることが少ない一方で、エージェントを展開しておく必要があります。とはいえ、AzureDevOps的にはDeployment Groupが処理の単位のため、デプロイ前にDeployment Groupを経由してホストの生存が確認できればデプロイに支障がありません。つまり、デプロイ前にインストールされていなくてもデプロイ時にDeployment Agent を動的に展開することで、ホストへの事前インストールを気にせずにデプロイに集中できます。

Use deployment groups in a release pipeline - Azure Pipelines | Microsoft Docs

Deployment Group の作成

Deployment Agent を作成する前に、デプロイ対象をグルーピングする単位である Deployment Groupを作成しておきましょう。

このあたりはドキュメントの通りに進めれば問題ありません。

Use deployment groups in a release pipeline - Azure Pipelines | Microsoft Docs

Azure DevOps で Pipelines から Deployment groups を選択しましょう。

Pipelines > Deployment groups を選択

Deployment Group 一覧が表示されるので、+ New で追加しましょう。

Deployment Group の追加を行う

Azure あるあるですが、ここでつける Deployment Group の名前が Deployment Agentと直接紐づくため重要になります。(つまり変更が難しい)

Deployment group name を付けるのは慎重に

今回は、Hogemoge-Dev-Deploy という名前でDeployment Groupを作成します。

作成したDeployment Group を開くと、次のようにエージェントを展開するための設定が表示されます。

PowerShellやBash でエージェントをインストールするコマンドが表示されている

ポイントはPersonal Access Token で、Deployment Agent が Azure DevOps と通信するときには現状必ずPATが必要です。このあたり、AWSのIAMのような統合ができておらず、本当にイケテナイです。PAT期限がることもあり、なかなか本当に厳しい。

Authenticate with personal access tokens - Azure DevOps | Microsoft Docs

Deployment Agent の展開

さて、AWS の Code Deployの場合は AWS提供ホストには事前にインストールされているのでインストールを考える必要はありません。

CodeDeploy とは - AWS CodeDeploy

しかし、Azure提供ホストには Deployment Agentが事前にインストールされていません。Deployment Agentを展開するには2つの方法があります。

  1. Deployment Agentを手動で展開する
  2. VM拡張を使って展開する

Agent の展開に必要なPersonal Access Tokenの権限

インストールの前に、罠となるPATの権限について触れておきます。Deployment Group Agent の展開に関するドキュメントを見ると、エージェントのインストールには PATを利用するとあります。

Deploy a build and release agent on Windows - Azure Pipelines | Microsoft Docs

ドキュメントにはDeployment GroupsRead & manage権限が必要とありますが、実際にエージェントを展開するときはこの権限だけでは足りず、「Deployment Group が見つかりません」と怒られます。

Deployment Groups の Read & manage権限をつける

追加で、Project and TeamRead 権限をPATに与えてあげましょう。

Project and Team の Read 権限をつける

ではインストールを見ていきます。

Deployment Agentを手動で展開する

この記事の主題ではないので手動展開は無視してokです。しかし、Azure VMではなく、オンプレミスのホストにインストールする場合は手動で展開したいでしょう。その場合は、次の公式ドキュメントを見ればいいです。ただこの方法は、ホスト構成時に手作業やAnsibleなどでの余計な構成が必要となるため、Azure VM が対象の場合は仕方ないとき以外は受け入れたくはない選択肢です。

Deploy a build and release agent on Windows - Azure Pipelines | Microsoft Docs

マニュアルで展開する場合は、Use a personal access token in the script for authentication を有効にして、Deployment Groupの展開PowerShell(Windows)/Bash(Linux) をコピペします。

当初、軽く流すつもりでしたがドキュメントとPowerShellスクリプトの両方に対応できていないトラップがあるので、簡単に触れておきます。自動生成されるPowerShellスクリプトは、--auth と --token 引数が間違っているため、これを削除して実行する必要があります。幸い、Auth と Tokenは実行中に聞かれるので、クリップボードにコピーした値を入れなおしましょう。*1

こんな感じになります。タグは何もなしでok、アカウントもデフォルトでokです。

修正したコマンドで実行する

これでDeployment Target が登録されます。おおむね、Build Agent と一緒です。

Deployment Targetが追加された

VM拡張を使って展開する

手動でインストールをしたくない時に、Azure がVMに対してマネージドに何かしらの処理を差し込むために提供している方法が「VM Extensions (VM拡張) 」です。

Windows 用の Azure VM 拡張機能とその機能 | Microsoft Docs

Azure DevOps の Deployment Agent も VM拡張が提供されており、ドキュメントも提供されています。この方法であれば、VM拡張の導入を定義をするだけで、Deployment Agentのインストール作業はマネージドに行われます。この記事は、マネージドを意図しているのでこちらを利用しましょう。

Provision agents for deployment groups - Azure Pipelines | Microsoft Docs

余談ですが、先ほどの手動エージェントインストールは Concepts-> Agents > Self-hosted <OS> agents にあり、VM拡張でのインストールは Concepts > Releases > Provision Deployment Agents にあるのなんというかドキュメントの作りが残念で実際にたどり着けてない人が周りに多いのでまずそう。

VM拡張でのDeployment Agentの展開

さて、一口にVM拡張でのDeployment Agentの展開といっても方法は2つあります。

  1. VMを構成時にDeployment Agentを展開する
  2. デプロイ時にResource Group単位でホストにDeployment Agentを展開する

それぞれどう違うのでしょうか。

もし、Resource Group内でVM が1つのDeployment Groupにまとめてokなら、「デプロイ時にResource Group単位でホストにDeployment Agentを展開する」を選択するのがオススメです。この方法なら、デプロイ時に自動的にエージェントが展開されて、VMの構成とデプロイが完全に分離されるだけでなく、Deployment Targetの管理が完全にマネージドになります。

一方で、Resource Gorup内の対象VMごとにDeployment Groupを分けたい場合は、構成時にDeployment Agentを展開する必要があります。TerraformやPlumiなどどの方法を使っても、VMに対してエージェント展開を管理する必要があるので避けたいところです。

順番にそれぞれの展開を見てみましょう。

VMを構成時にDeployment Agentを展開する

Terraform のModuleでさくっとやります。Windows を例にしますが、LinuxでもWindowsでも変わりません。

resource "azurerm_virtual_machine" "main" {
  name                          = "${var.name_prefix}-vm"
  location                      = "${var.location}"
  resource_group_name           = "${var.resource_group_name}"
  network_interface_ids         = ["${azurerm_network_interface.main.id}"]
  vm_size                       = "${var.vm_size}"
  delete_os_disk_on_termination = true

  storage_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2016-Datacenter"
    version   = "latest"
  }

  storage_os_disk {
    name              = "${var.name_prefix}-os-disk"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "${var.managed_disk_type}"
    disk_size_gb      = "${var.os_disk_size_gb}"
  }

  os_profile {
    computer_name  = "${var.computer_name}"
    admin_username = "${var.vm_username}"
    admin_password = "${var.vm_password}"
  }

  os_profile_windows_config = {
    provision_vm_agent = true

    winrm {
      protocol = "HTTP"
    }
  }

  tags = "${var.tags}"
}

resource "azurerm_virtual_machine_extension" "main" {
  name                       = "TeamServicesAgent"
  resource_group_name        = "${data.azurerm_resource_group.current.name}"
  location                   = "${var.location}"
  virtual_machine_name       = "${module.assetgenerator.vm_name}"
  publisher                  = "Microsoft.VisualStudio.Services"
  type                       = "TeamServicesAgent"
  type_handler_version       = "1.0"
  auto_upgrade_minor_version = true
  tags                       = "${local.tags}"

  settings = <<SETTINGS
    {
        "VstsAccountName": "${var.VstsAccountName}",
        "TeamProject": "${var.TeamProject}",
        "DeploymentGroup": "${var.DeploymentGroup}",
        "AgentName": "${module.assetgenerator.vm_name}"
    }
SETTINGS

  protected_settings = <<SETTINGS
    {
        "PATToken": "${var.PAT_DEPLOYMENT_AGENT}"
    }
SETTINGS
}

適当にvariables は公開してもらうとして、azurerm_virtual_machine_extension を使うことでVM作成時にDeployment Agent が自動的に展開されて、Deployment GroupにTargetが登録されます。手動での展開に比べて、圧倒的に楽なうえに自動展開できます。

最も重要なのは、azurerm_virtual_machine リソースの os_profile_windows_config にある provision_vm_agent = true です。(os_profile_linux_configにはない) 有効でないとVM拡張が利用できず、デフォルトが false なので項目が抜けないようにします。provision_vm_agentが有効化できるのは「VM作成時」だけなので作ってから気づいても手遅れです。

Azure Resource Manager: azurerm_virtual_machine - Terraform by HashiCorp

デプロイ時にResource Group単位でホストにDeployment Agentを展開する

Azure DevOps Pipelines のリリース時に、Resource Group内のVMに対してDeployment Agent をインストールします。この方法を使うと、デプロイの流れの中で対象が動的に定まるため、スケールアウト時も考慮不要になります。

リリースパイプラインのタスクを見てみましょう。

リリースパイプラインのタスクはAgent Job と Deployment group job で分けるのがコツ

ポイントは、「Build Agent」と「Deployment Group」の処理が分かれることです。実際のデプロイは、Deployment Group単位で実行されますが、そのデプロイ先をBuild Agent で動的に作成します。

Build Agent でやることは2つです。

  • 停止したVM のStart
  • VM Extensions 経由で Deployment Agent のインストール

YAML を見ていきましょう。

「停止したVM のStart」は、AzureResourceGroupDeployment を使うことでstart actionでAzure DevOps から実行できます。ポイントは、Resource Group単位の処理になります。止めたままにしたいVMとか混じるのだめ。融通があんまり利きません。

steps:
- task: AzureResourceGroupDeployment@2
  displayName: 'Azure Deployment:Start action on VM'
  inputs:
    azureSubscription: YOUR_SUBSCRIPTION
    action: Start
    resourceGroupName: 'YOUR_RESOURCE_GROUP'

「VM Extensions 経由で Deployment Agent のインストール」もAzureResourceGroupDeployment を使うことで、VM拡張でインストールされます。先ほど作ったDeployment Groupを特定できるように AzureDevOps のチーム名、Deployment Group名を指定しましょう。

steps:
- task: AzureResourceGroupDeployment@2
  displayName: 'Azure Deployment:Select Resource Group action on ResourceGroup'
  inputs:
    azureSubscription: YOUR_SUBSCRIPTION
    action: 'Select Resource Group'
    resourceGroupName: 'YOUR_RESOURCE_GROUP'
    enableDeploymentPrerequisites: ConfigureVMWithDGAgent
    teamServicesConnection: 'YOUR_SERVICE_CONNECTION'
    teamProject: 'YOUR_AZUREDEVOPS_TEAM'
    deploymentGroupName: 'YOUR_DEPLOYMENT_GROUP_NAME'

これらの処理がデプロイ前に実行されることで、実際にDeployment Groupでデプロイする前にVMの起動とエージェントの展開が約束されます。

ポイントは、このタスク自体にはPATの指定がなく、Azure DevOps Team Service Connection で設定します。

Azure DevOps Team Service ConnectionでDeployment Group の追加に必要な接続URLやPATを設定する

Connection URL には、https://YOUR_TEAM.visualstudio.com/ と指定します。 PATの権限に注意してください。VMへのエージェントインストール、デプロイ実施の両方が必要なので、

Service connections in Azure Pipelines and Team Foundation Server - Azure Pipelines | Microsoft Docs

あとはいい感じでデプロイしてあげればokです。

Q&A

いくつかやっている中ではまりどころがあったものの、文脈とずれるのでここで。

VM拡張で Deployment Agent をインストールしたあとアンインストールしたい

現時点では、アンインストールしようとするとエラーが出ます。VM拡張でのアンインストールは一部しか対応されていないのでそれに該当するらしい.... つまりVM作り直しましょう。

Failed to delete the virtual machine extension in Azure ARM VM - Stack Overflow

VM拡張のARM Templateはないの

あるっぽ。私は ARM Teamplte は使いません。

VSTS Agent Extension in ARM template · Issue #1044 · Microsoft/azure-pipelines-agent

PATの認証って365日だけどどうするの

永続できないのはセキュリティモデル的にはいいですが、マシンユーザー的な利用を想定していないあたりAzureの融通の利かなさ.... Azure DevOps 的には、Service Connectionを複数作れるので、次のいずれかがいいでしょう。

  • PATのRegenerate で期限を新しくしてPATを再生成して、今のService ConnectionのPATを更新
  • 期限前にPATを新しく発行して別にService Connection を作って割り当てを変える

Authenticate with personal access tokens - Azure DevOps | Microsoft Docs

Ref

Deploying to Azure VM using Deployment Groups | Azure DevOps Hands-on-Labs

How to install VSTS deployment group agents on Azure VMs | Mickaël Derriey's blog

*1:お、AnsibleやTerraform のprovisionerどうするの?隙が多い

MSIを使ったStorage Account(Blob, Queue) の認証を使ってQueueの監視を行う

Azure の Storage Account アクセスといえばConnection String ですが、Managed Service Identity (MSI) による AzureAD認証が可能です。(2019/3/25 に GAしたはず.... あれ?)

ここでは、Storage Account ではなく MSI を使ったAzure Functions からのアクセスについてみてみます。

目次

TL;DR

Azure Functions の Binding でのMSI認証はできない。

StorageCredentials を作ってから、CloudBlobClient/CloudQueueClient を作ることになる。

ローカルのMSI認証は現在バグあり。

なぜMSIなのか

MSIを使うことで認証情報をいちいちStorage Account からひっぱてきて参照可能な形で渡す必要がなくなります。 KeyVaultを使うにしても、AppSettingsを使うにしてもConnectionStrings を埋めるのは嫌ですし、IAM = RBAC = AzureAD の認証下でアプリケーションのアクセスが制御されるのは望ましいでしょう。

とくにTerraform を使って構成している場合は、MSIの有効化、IAM でのStorageへの割り当てまで構成時点で完了するのでアプリケーションから見ると透過的に扱えてより効果的でしょう。

AppServiceでMSIを使う

AppService (AzureFunctions) でMSIを使うには、SystemIdentity を有効にします。

Azure Portal で設定する

Azure Portal だと「アクセスを持ちたい側でMSIを設定」、「アクセスされる側のIAMでロール設定」を行います。

まずは、Function App でMSIを設定します。

FunctionApp > AppSettings > Identity

SystemAssigned を Onにする

これでAzureAD のAppRegistration にアプリケーションが登録されたので、アクセスされるStorage Account のIAMでアクセス権限を設定してあげます。

アクセスを許可するリソースでIAM > Role assignments でロール設定する

IAM は Subscription > Resource Group > Resource で継承されるので、そのResourceに限定したいなら ResourceのIAMでロール設定すればokです。もしResourceGroup全体で利用したいなら、 Resource Group のIAMでルール設定すればok です。

Terraform で構成する

こんなことをやっていたら時間がなくなるので、Terraform でFunctionAppの作成からロール設定をします。

gist.github.com

Terraform でMSIを設定するポイントについて説明します。MSIを使う側であるazurerm_function_app でMSIを有効化するのはidentityです。これはVMでもACIでも変わらないのでまず設定するといいでしょう。

  identity = {
    type = "SystemAssigned"
  }

続いて、azurerm_role_assignment を使ってIAMを設定します。ここではわかりやすいように、Resource Groupに対してContributerロールを設定しています。AzureRM の返り値的に、principal_idがlookup必須なのがあんまりイケテマセンが公式です。

resource "azurerm_role_assignment" "main" {
  scope                = "${data.azurerm_resource_group.current.id}"
  role_definition_name = "Contributor"
  principal_id         = "${lookup(azurerm_function_app.main.identity[0], "principal_id")}"
}

さぁこれでMSIの準備はできました。

AzureFunctions でMSIを使ってStorage Account にアクセスする

前回の記事で、StorageAccountのConnection String を使って認証をとりましたが、これをMSIに切り替えます。

tech.guitarrapc.com

とはいえ、MSIはAzure 上のリソースで利用可能な他、ローカルでも az login や Visual Studio の Option > Account から Azureに接続していれば利用できます。と思うじゃないですか? Storage Account に関しては現状動かないです。

https://github.com/Azure/azure-libraries-for-net/issues/557 Local MSI Login using AAD account · Issue #557 · Azure/azure-libraries-for-net · GitHub

認証ヘッダがうまく動いていないので、直るまでローカルはConnection String にバイパスします。バイパスはAzure環境かどうかを WEBSITE_INSTANCE_ID環境変数でチェックして分岐することにしましょう。

gist.github.com

前回のコードから、CloudQueueClient の取得部分をMSIに変えるようにしてみます。変更箇所はCloudQueueClientの作成部分だけです。

gist.github.com

MSI から取得するときのポイントは、3つあります。

  1. AzureServiceTokenProvider を使って認証TokenのProvider経由で認証を作ります
    • MSIは認証プロバイダー経由で認証を作りましょう
  2. azureServiceTokenProvider.GetAccessTokenAsync("アクセストークンの請求先") で適切なURIを指定する必要があります
  3. new CloudQueueClient(new StorageUri(new Uri($"https://{storageAccountName}.queue.core.windows.net")), storageCredentials);のように、URIとCredentialを指定してクライアントを作成します
    • ConnectionString を使っている時は接続先のStorageAccountが明示されていたので、account.CreateCloudQueueClient(); と書けましたが、MSIではStorageAccountが不明なので接続先が作れません
    • このため、StorageCredential からStorageAccountを作るのではなく、URIでアクセス先のStorageAccountNameと一緒に指定してあげます

MSIがローカルで使えるようにバグ修正されるまで、Azure環境とローカルで分岐してあげます。

// connect to Azure Storage
var queueClient = context.IsAzureEnvironment()
    ? await CreateQueueClientAsync("YOUR_STORAGE_ACCOUNT_NAME")
    : CreateQueueClient(Environment.GetEnvironmentVariable("queue_storage_connection_string"));

ということでMSIに対応したコード全体像です。

gist.github.com

まとめ

MSI を使えるなら使いましょう。ConnectionStringとは使うのやめましょ。

MSI、Azureの仕組みは分かりやすいです。一方で、アプリからの利用が分かりにくいというか整理されたドキュメントがないので認証先のドキュメント把握、仕組みの整理に手間取ったです。

余談

2019/3/25 にStorage Account (Blob, Queue) の AzureAD認証がGAしたはずの記事が出ているのですが、現在アクセスできません。

https://azure.microsoft.com/en-us/blog/azure-storage-support-for-azure-ad-based-access-control-now-generally-available/

これがキャッシュです。

Google Cache : Azure Storage support for Azure Active Directory based access control generally available | Blog | Microsoft Azure

ブログ一覧にもないのと、アクセス先URIでもPreviewになっているので、GAキャンセルでまだPreview っぽい?

Azure Services that support managed identities for Azure resources | Microsoft Docs

Azure updates | Microsoft Azure

Ref:

Azure リソース (プレビュー) の Azure Active Directory 管理 ID を持つ blob およびキューへのアクセスの認証 - Azure Storage | Microsoft Docs

仮想マシン上で Azure リソースのマネージド ID を使用してアクセス トークンを取得する方法 | Microsoft Docs

c# - Azure Storage authentication via AzureServiceTokenProvider for CloudTableClient - Stack Overflow

Azure AD Authentication with Azure Storage + Managed Service Identity - Joonas W's blog

Local MSI Login using AAD account · Issue #557 · Azure/azure-libraries-for-net · GitHub

*1:びっくりするぐらいわかりにくい。

Azure Storage Queue を Application Insightsで監視する

あるあるな Queue の監視ですが、自前でやらなきゃいけないなら Serverless でぺちって任せるのは楽ちんですよ、というのはよくあるパターンです。 実際にQueue Storage のモニタリングをしてみましょう。

TL;DR

Azure Storage Queue や Service Bus などのキューサービスは、疎結合な構成を組んだ時に中心を担うため、そのキュー長が想定よりたまっていないか、推移はどうなっているかなどが気になります。

この監視を、Azure Functions の Timer Trigger で行ってみましょう。

Disclaimer

Datadog や 複数のモニタリングサービスは、Azure のAPIをたたいて自分でクラウドリソースのメトリクスを見に行ってくれます。もしそれらで済むならこの記事は不要です、すぐに閉じましょう。この記事は、自分でリソースの状態をポーリングするときの対応例です。

Consumption Planを前提に毎30秒監視しています。(2 * 60 * 24 * 30 = 86400run) 無料枠内ですが、料金がどうなっても私は責任は取りかねます。(免責事項)

なぜ AzureFunctions なのか

モニタリングはアプリケーションの内部リソースと、クラウドなどの外部リソースで分けて考えられます。

アプリがスケールしたときに個別のアプリのメトリクスが見たければ、アプリケーションで監視するのが妥当な場合が多いのは同意を得られるかと思います。一方で、アプリケーションの外にあるリソースは、アプリケーションからモニタリングする必然性はありません。おのずと次のような欲求が高まってきます。

  • アプリケーション自身のリソースをモニタリングに消費したくない
  • モニタリング自身がスケールして重複した値が取れてほしくない
  • アプリケーション自身が、どのようなリソースに依存んして動いているのか関心がない

自前でメトリクスを見たいときは、どこかで実行する必要があります。しかしアプリケーションでは実行したくありません。監視対象を定義しておいて、メトリクスを取得するだけのシンプルな監視の場合、Serverless でアプリケーションとは別個の存在として実行させるのが便利です。

Azure Storage Queue なら、Azure Functions の Consumption Plan + Timer Trigger が定時ポーリング監視になり、定性モニタリングとして負荷なく実行できるでしょう。

監視対象

Azure でキューといえば、Azure Storage Queue と Service Bus がありますが、さくっと Storage Queue で確認してみましょう。ここでは myqueue というキューを用意して監視することにします。

  • myqueue : Storage Queue API で通常投げつけるQueueです。主にこの子が気になる
  • myqueue-poison : WebJobs で失敗した時に自動的に作成されるQueueで-posison が末尾につきます。手動でなんとかすることになります

なお、Storage Queue は Dead Letter Queue (DLQ) に対応していないのでまぁほげもげ。

実行環境

  • Function Runtime: v2
  • Language: C# (.NET Core 2.1)
  • Trigger: Timer Trigger (毎30秒)
  • Metrics: Queue Length
  • Monitoring: Application Insights

Azure Functions

Azure Functions は、v2 においても裏で Application Insights を使っています。 とはいえ、明示的にMicrosoft.ApplicationInsights パッケージを取り込まないと、アプリからApplication Insights をたたけないので入れておきます。

dotnet add package Microsoft.ApplicationInsights

メトリクスを送信するため、Azure Functions の Application Configで Application Insights の Instrumentation Key を設定します。もし Azure Functions の Application Insights にメトリクスを飛ばすのでいいなら APPINSIGHTS_INSTRUMENTATIONKEY でいいでしょう。

Queueに接続するため、QueueがあるStorage Account のConnection Strings を設定しておきます。ここでは、queue_storage_connection_string としました。

では実際に実行してみましょう。コードはこのような感じになります。

gist.github.com

これで毎10秒ごとにQueue の長さをモニタリングして、Application Insights に投げられます。ローカルで実行した場合、コンソールにQueueの長さがでるでしょう。

Application Insights の確認

今回はAzure FunctionsのApplication Insights 連携に乗っかているので見てみます。

ちょうどQueue を投げてなかったので0 が継続して取れているのがわかります。

QueueLength を Application Insights で監視

あとは Azure Monitor という名のApplication Insights の閾値による Action で Webhook なりを投げるといいでしょう。Body の調整できない子ですが。

Azure Monitor | Microsoft Azure

Tips

Cloud Table とか絡んでくると、いまだにMicrosoft.WindowsAzure.Storage パッケージが安定なので、Azureの NuGet パッケージつらい。

Ref

やろうとしたら、だいたいすでにやってるKloudさんつよい。v1 ですが、だいたいやっていることは一緒です。

Monitoring Azure Storage Queues with Application Insights and Azure Monitor - Kloud Blog

Slotを用いたAppService のStaging環境とAzureDevOps PipelineのリリースによるBlueGreen Deployment

Azure の App Service には Slotがあります。

Slotはただ利用してもそれなりにうれしいのですが、Terraform での構成とAzure DevOps の リリースパイプラインでの展開を行えるようにすることで、「CI/CD による App Service の Slot による展開前のStaging環境での確認」と「リリースゲートを使った任意でのSwapによるStaging->Production展開」、「Azure上のSlot環境の明確な管理」ができるようになります。

AzureをTerraform で展開しておくことで、AppService + Slot 構成がDev/Production の両方で展開でき、運用上の負担も小さいのがいい感じです。 実際にどんな感じで組むのか紹介します。

こういうのよくありますが、実際に設定を含めてどう組めばいいのかまで含めた公開はあんまりされないのでしておきましょう。

www.edmondek.com

目次

TL;DR

Slotを用いることで、Prod環境への適用前のStaging環境確認が簡単に行えます。 また、IISなどのAppSettingsの入れ替えに伴う再起動でWebAppsが応答できず500を返すのもSwap入れ替え中のWarm upで解消されます。 Terraform + AzureDevOps のCI/CD で自動化しつつ組んでみましょう。

ここで注意するべきは、Terraform の azurerm_app_service_active_slot を使ったSwapではないということです。このリソースは制約が大きいので、CDに利用することは避けたほうがいいでしょう。

www.terraform.io

概要

Azure AppService にはSlot機能があり、本番に適用する前にStaging環境で検証、Prod環境にSwapで反映できます。 Azure DevOps のPipelineでリリース時にDeployとSwapを分けることで、「本番に反映」のタイミングだけ承認を求めて、普段のStaging展開は自動CI/CD を行い開発者はステージング環境で常に確認できます。

想定されるワークフロー

  • Git Commit により、PRごとにCIが実行
  • CIの成功をtriggerに、CDのdeployタスクにより成果物を常にstaging slot にデプロイ
  • 開発者は AppService の staging slotを確認することでステージング環境を確認(ここまで毎PRで自動CD)
  • ステージングの動作に問題がなければ、CDのswapのゲートの承認を行う
  • CDはswap承認を受けて、自動的にswap を実行し ステージング環境が production と入れ替わり本番リリースされる (Swapによるユーザー影響は明確にリリース制御できる)

App Service Slot と AzureDevOps のリリースタスクによるBlue/Green デプロイ

Azure環境はTerraform で構成されており、AppService は各Envごとに自動的に構成されます。

Slotとは

どんな資料をみるよりも公式資料がわかりやすいので見てみましょう。

https://docs.microsoft.com/en-us/azure/app-service/deploy-staging-slots

Slotでポイントとなるのは、Slot専用の環境変数を持てることです。

  • Slot環境ではStaging へ接続
  • Slot環境では環境変数をStagingにして、アプリの動作を変える

といったことが可能です。

Slot の用意

ここではTerraform でSlotを用意します。Azure Portal から用意したいからはそちらでどうぞ。

今回は ASP.NET Coreで組んでいますが、JavaでもGolang でもほぼ変わりません。 Golangなどを使うなら Linux Container がいい感じです。*1

https://www.terraform.io/docs/providers/azurerm/r/app_service_slot.html

このようなWebAppsの構成がある前提です。

resource "azurerm_app_service_plan" "webapps" {
  name                = "prod-plan"
  location            = "${local.location}"
  resource_group_name = "${data.azurerm_resource_group.current.name}"
  kind                = "Windows"
  tags                = "${local.tags}"

  sku {
    tier = "Standard"
    size = "S1"
  }
}

resource "azurerm_app_service" "webapps" {
  name                    = "prod-webapp"
  location                = "${local.location}"
  resource_group_name     = "${data.azurerm_resource_group.current.name}"
  app_service_plan_id     = "${azurerm_app_service_plan.webapps.id}"
  client_affinity_enabled = false
  tags                    = "${local.tags}"

  app_settings {
    ASPNETCORE_ENVIRONMENT                   = "Production"
    APPINSIGHTS_INSTRUMENTATIONKEY           = "${azurerm_application_insights.webapps.instrumentation_key}"
    "ApplicationInsights:InstrumentationKey" = "${azurerm_application_insights.webapps.instrumentation_key}"
    "ConnectionStrings:Storage"              = "${azurerm_storage_account.webapps.primary_connection_string}"
    "ConnectionStrings:LogStorage"           = "${azurerm_storage_account.logs.primary_connection_string}"
  }
  site_config {
    dotnet_framework_version = "v4.0"
    always_on                = "true"
    remote_debugging_enabled = false
    remote_debugging_version = "VS2017"
    http2_enabled            = true
  }
  identity {
    type = "SystemAssigned"
  }
  lifecycle {
    ignore_changes = [
      "app_settings.%",
      "app_settings.MSDEPLOY_RENAME_LOCKED_FILES",
      "app_settings.WEBSITE_NODE_DEFAULT_VERSION",
      "app_settings.WEBSITE_RUN_FROM_PACKAGE",
      "app_settings.WEBSITE_HTTPLOGGING_CONTAINER_URL",
      "app_settings.WEBSITE_HTTPLOGGING_RETENTION_DAYS",
    ]
  }
}

Slot は次の内容で作成してみます。

  • staging というSlot名にする
  • ASPNETCORE_ENVIRONMENT をProductionとStaging で入れ替える

この場合、Slot名となるnamestagingapp_settingsASPNETCORE_ENVIRONMENT=Staging を設定し、他はProductionとなるApp Service設定と同様に組んでおきます。

resource "azurerm_app_service_slot" "staging" {
  name                    = "staging"
  app_service_name        = "${azurerm_app_service.webapps.name}"
  location                = "${local.location}"
  resource_group_name     = "${data.azurerm_resource_group.current.name}"
  app_service_plan_id     = "${azurerm_app_service_plan.webapps.id}"
  client_affinity_enabled = false
  https_only              = true
  tags                    = "${local.tags}"

  app_settings {
    ASPNETCORE_ENVIRONMENT                   = "Staging"
    APPINSIGHTS_INSTRUMENTATIONKEY           = "${azurerm_application_insights.webapps.instrumentation_key}"
    "ApplicationInsights:InstrumentationKey" = "${azurerm_application_insights.webapps.instrumentation_key}"
    "ConnectionStrings:Storage"              = "${azurerm_storage_account.webapps.primary_connection_string}"
    "ConnectionStrings:LogStorage"           = "${azurerm_storage_account.logs.primary_connection_string}"
  }
  site_config {
    dotnet_framework_version = "v4.0"
    always_on                = "true"
    remote_debugging_enabled = false
    remote_debugging_version = "VS2017"
    http2_enabled            = true
  }
  identity {
    type = "SystemAssigned"
  }

  lifecycle {
    ignore_changes = [
      "app_settings.%",
      "app_settings.MSDEPLOY_RENAME_LOCKED_FILES",
      "app_settings.WEBSITE_NODE_DEFAULT_VERSION",
      "app_settings.WEBSITE_RUN_FROM_PACKAGE",
      "app_settings.WEBSITE_HTTPLOGGING_CONTAINER_URL",
      "app_settings.WEBSITE_HTTPLOGGING_RETENTION_DAYS",
    ]
  }
}

あとはTerraformを実行することで、Slotが生成され維持されます。

AzureDevOps Pipeline

DevOps Pipeline でリリースパイプラインを組みます。Azure Portal でSwapしたい方はやる必要がありません。

完成図は次の通りです。

リリースパイプラインのステージ構成はDeployとSwapで分割する

  • Deploy: Staging までのPackage Deploy を行います
  • Swap: Staging Slot と Production を Swap します

DeployをSwapを分けることで、実際にProductionが入れ替わるタイミングだけGateをかけることができるようになります。

  • Deployまでを自動CI/CDにして事前動作確認
  • Swap は、Slackやほかの手段でチームの合意があった時だけデプロイ

Deploy

Deployタスクは、WebApps のSlot環境へのデプロイを行います。

DeployはRun as package の構成まで(app servce in linux ならcliやFTP展開)

デプロイはRun from package を用いていますが、このRun from zip/Run from packageはWindows Web Apps でのみ有効です。

https://docs.microsoft.com/en-us/azure/devops/pipelines/targets/webapp?view=azure-devops&tabs=yaml

もしLinuxにしたい場合は別の手段を使いましょう。Container であればDevOps Pipeline があります。

https://docs.microsoft.com/en-us/azure/devops/pipelines/apps/cd/deploy-docker-webapp?view=azure-devops

このリリースタスクは、CIでPackageをArtifactに上がっている前提です。 あとは、CD時点の時間を取得、Zip生成、Blobにアップロード、SASを取得してSlot のAppSettings に埋め込みをすることでRun from Packageが実行できます。

PowerShell Scriptのタスクは次の通りです。time環境変数を作っています。

steps:
- powershell: |
   $date=$([System.DateTimeOffset]::UtcNow.AddHours(9).ToString("yyyyMMddHHmmss"))
   Write-Host "##vso[task.setvariable variable=time]$date"
   
  displayName: 'PowerShell Script'

Zipタスクは次の通りです。time環境変数の時間をzipのファイル名に利用しています。

steps:
- task: ArchiveFiles@2
  displayName: 'Archive $(System.DefaultWorkingDirectory)/$(RELEASE.PRIMARYARTIFACTSOURCEALIAS)/drop'
  inputs:
    rootFolderOrFile: '$(System.DefaultWorkingDirectory)/$(RELEASE.PRIMARYARTIFACTSOURCEALIAS)/drop'
    includeRootFolder: false
    archiveFile: '$(Release.DefinitionName)-$(time)-$(Release.ReleaseName)-$(Build.SourceVersion).zip'

Blob コピータスクは次の通りです。Storage Blobにアップロードして、storageUri 環境変数にURIを取得します。

steps:
- task: AzureFileCopy@1
  displayName: 'AzureBlob File Copy'
  inputs:
    SourcePath: '$(Release.DefinitionName)-$(time)-$(Release.ReleaseName)-$(Build.SourceVersion).zip'
    azureSubscription: YOUR_SUBSCRIPTION
    Destination: AzureBlob
    storage: YOUR_STORAGE_ACCOUNT
    ContainerName: YOUR_STORAGE_CONTAINER
    BlobPrefix: YOUR_STORAGE_BLOB_PREFIX
    outputStorageUri: storageUri

SAS生成タスクは次の通りです。SasTokenをstorageToken 変数にかき出しています。

steps:
- task: pascalnaber.PascalNaber-Xpirit-CreateSasToken.Xpirit-Vsts-Release-SasToken.createsastoken@1
  displayName: 'Create SAS Token for Storage Account'
  inputs:
    ConnectedServiceName: YOUR_SUBSCRIPTION
    StorageAccountRM: YOUR_STORAGE_ACCOUNT
    SasTokenTimeOutInHours: 87600
    Permission: 'r'
    StorageContainerName: packages
    outputStorageUri: 'storageUri'
    outputStorageContainerSasToken: 'storageToken'

最後にRun from Package を行います。前のタスクまでで生成した、time環境変数、アップロードしたstorageUri変数、SasトークンのstorageToken 変数を利用します。 ポイントは、slotです。ここで作成しておいたstagingを指定することで、production環境ではなく、staging Slotへデプロイします。

steps:
- task: hboelman.AzureAppServiceSetAppSettings.Hboelman-Vsts-Release-AppSettings.AzureAppServiceSetAppSettings@2
  displayName: 'Run from package : Set App Settings'
  inputs:
    ConnectedServiceName: YOUR_SUBSCRIPTION
    WebAppName: 'prod-webapp'
    ResourceGroupName: 'YOUR_RESOURCE_GROUP_NAME'
    Slot: staging
    AppSettings: 'WEBSITE_RUN_FROM_PACKAGE=''$(storageUri)/prod/$(Release.DefinitionName)-$(time)-$(Release.ReleaseName)-$(Build.SourceVersion).zip$(storageToken)''

Swap

Deploy後のSwap タスクのpre-deployment condition で、実行前の条件を付けることができます。 ここで、承認を要するようにすることで、チームメンバーのだれかの承認があったときだけ実行、ということが可能です。

Swap実行前にPre-deployment approval でチームの承認による任意展開をひっかける

タスクを見てみましょう。タスクは簡潔にSwapを行うだけです。

SwapはSlot Swap を実行するだけ

Swapは、Deployタスクでデプロイしたstaging Slot とProductionの入れ替えを行うだけです。

steps:
- task: AzureAppServiceManage@0
  displayName: 'Manage Azure App Service - Slot Swap'
  inputs:
    azureSubscription: '$(Parameters.ConnectedServiceName)'
    WebAppName: '$(Parameters.WebAppName)'
    ResourceGroupName: '$(Parameters.ResourceGroupName)'
    SourceSlot: '$(Parameters.SlotName)'
    SwapWithProduction: True

これで CIが実行されると Releaseパイプラインで、Deploy タスク、承認後にSwap タスクが実行されます。

CIからトリガーしてCD実行完了時の様子

Deployタスク

Deployの各タスクの状態

Swap タスク

Swapの各タスクの状態

Swap はおおよそ 60sec - 90sec かかりますが、これは Azure RM APIのレスポンスとApp Serviceの制約なので諦めます。

改善点

Deploy と Swap を直接でつなぐことで、Deployタスクの完了時に連動するように依存性を組んでいます。しかしタスクを分けたために、DeployとSwapそれぞれで CIの成果物(Artifact) のダウンロード処理が2sec程度かかっています。無視してokなレベルですが、無駄は無駄。

Swap ごときに60-90sec かかるのなんというか、シカタナイとは言えやりようあると思うんですが... Azureの制約にかかるので諦めです。(Slot Warmup とこのSwapの時間により無停止でアプリが展開できるのは皮肉というべきか)

Azure Functionsの場合も、ほぼ同様に行けますが、Durable Functions で Slot でちょっと挙動がおかしい感じです。(Slot変数が展開しないような動きが見えるような)

Ref

blogs.msdn.microsoft.com

docs.microsoft.com

www.azuredevopslabs.com

mikepfeiffer.io

*1:現在、同一Resource Group で Windows/Linux のApp Service Plan が混在できないので注意が必要ですが!

PowerShell 6.0 のImport-Csv に W3C 拡張ログ ファイル形式のサポートを追加 #2482 について調べてみた

これを調べていたのは本を書いていたときなので、そろそろ一年経つのですがお蔵入りの前に出しておきます。 PowerShell 6.0 において、W3C 拡張ログが Import-Csv で読み込み可能になったという内容でリリースノートが出ているのですがその内容について。

docs.microsoft.com

目次

TL;DR

Import-Csv がW3Cログに対応したといっても、それはIISログではなくMicrosoft Exchange のログ (W3Cログ形式)となる。 なんという期待を裏切る罠。

まとめおいておきます。

Import-Csv in PowerShell 6.0 supports w3c log format, if delimiter is `,`. https://github.com/PowerShell/PowerShell/pull/2482 · GitHub

W3C 拡張ログとは

W3C に定義があります。

www.w3.org

Introduction で用途がだいたいわかるので引用します。

Most Web servers offer the option to store logfiles in either the common log format or a proprietary format. The common log file format is supported by the majority of analysis tools but the information about each server transaction is fixed. In many cases it is desirable to record more information. Sites sensitive to personal data issues may wish to omit the recording of certain data. In addition ambiguities arise in analyzing the common log file format since field separator characters may in some cases occur within fields. The extended log file format is designed to meet the following needs:

* Permit control over the data recorded.
* Support needs of proxies, clients and servers in a common format
* Provide robust handling of character escaping issues
* Allow exchange of demographic data.
* Allow summary data to be expressed.

重要な箇所はFormat にあります、引用します。

Format
An extended log file contains a sequence of lines containing ASCII characters terminated by either the sequence LF or CRLF. Log file generators should follow the line termination convention for the platform on which they are executed. Analyzers should accept either form. Each line may contain either a directive or an entry.

Entries consist of a sequence of fields relating to a single HTTP transaction. Fields are separated by whitespace, the use of tab characters for this purpose is encouraged. If a field is unused in a particular entry dash "-" marks the omitted field. Directives record information about the logging process itself.

Lines beginning with the # character contain directives. The following directives are defined:

Version: <integer>.<integer>
The version of the extended log file format used. This draft defines version 1.0.
Fields: [<specifier>...]
Specifies the fields recorded in the log.
Software: string
Identifies the software which generated the log.
Start-Date: <date> <time>
The date and time at which the log was started.
End-Date:<date> <time>
The date and time at which the log was finished.
Date:<date> <time>
The date and time at which the entry was added.
Remark: <text>
Comment information. Data recorded in this field should be ignored by analysis tools.
The directives Version and Fields are required and should precede all entries in the log. The Fields directive specifies the data recorded in the fields of each entry.

Example
The following is an example file in the extended log format:

#Version: 1.0
#Date: 12-Jan-1996 00:00:00
#Fields: time cs-method cs-uri
00:34:23 GET /foo/bar.html
12:21:16 GET /foo/bar.html
12:45:52 GET /foo/bar.html
12:57:34 GET /foo/bar.html

ここで最も重要なのは、Fields are separated by whitespace という箇所で、いわゆるデリミター(Delimiter/区切り文字) が 空白スペースであるという記述です。Exampleもそのようになっています。

Import-Csv でIIS ログを読んでみる

リリースノートを見ただけだと、W3Cログ = IIS ログが読み込めるのかと思った方もいらっしゃるかもしれません。 IISログを試していましょう。

#Software: Microsoft Internet Information Services 7.5
#Version: 1.0
#Date: 2013-06-24 10:56:45
#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken
2013-06-24 10:56:45 192.168.0.1 POST /xas/ - 80 - 222.222.222.222 Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/27.0.1453.110+Safari/537.36 404 0 2 471

しかし次のようにデリミターは空白文字であり、Import-Csvでは区切りを認識できません。

PS> import-csv .\iis_log.log
 
 <#
 date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken
------------------------------------------------------------------------------------------------------------------------------------------
2013-06-24 10:56:45 192.168.0.1 POST /xas/ - 80 - 222.222.222.222 Mozilla/5.0+(Windows+NT+6.1;+WOW64)+AppleWebKit/537.36+(KHTML
 #>

-Delimiter に " " を指定すると読み込みエラーが起こることから、ログのデリミターを変えるしかなさそうです。 IISログに関して、デリミターを変更できるか IIS_SCHEMA.xml で調整を試みるとできないことが分かります。

<!-- just for logFile section -->
<!-- there's no attribute, value to change delimiter -->
      <element name="logFile">
        <attribute name="logExtFileFlags" type="flags" defaultValue="Date, Time, ClientIP, UserName, ServerIP, Method, UriStem, UriQuery, TimeTaken, HttpStatus, Win32Status, ServerPort, UserAgent, HttpSubStatus, Referer">
          <flag name="Date" value="1"/>
          <flag name="Time" value="2"/>
          <flag name="ClientIP" value="4"/>
          <flag name="UserName" value="8"/>
          <flag name="SiteName" value="16"/>
          <flag name="ComputerName" value="32"/>
          <flag name="ServerIP" value="64"/>
          <flag name="Method" value="128"/>
          <flag name="UriStem" value="256"/>
          <flag name="UriQuery" value="512"/>
          <flag name="HttpStatus" value="1024"/>
          <flag name="Win32Status" value="2048"/>
          <flag name="BytesSent" value="4096"/>
          <flag name="BytesRecv" value="8192"/>
          <flag name="TimeTaken" value="16384"/>
          <flag name="ServerPort" value="32768"/>
          <flag name="UserAgent" value="65536"/>
          <flag name="Cookie" value="131072"/>
          <flag name="Referer" value="262144"/>
          <flag name="ProtocolVersion" value="524288"/>
          <flag name="Host" value="1048576"/>
          <flag name="HttpSubStatus" value="2097152"/>
        </attribute>
        <attribute name="customLogPluginClsid" type="string" defaultValue=""/>
        <attribute name="logFormat" type="enum" defaultValue="W3C">
          <enum name="IIS" value="0"/>
          <enum name="NCSA" value="1"/>
          <enum name="W3C" value="2"/>
          <enum name="Custom" value="3"/>
        </attribute>
        <attribute name="logTargetW3C" type="flags" defaultValue="File">
          <flag name="File" value="1"/>
          <flag name="ETW" value="2"/>
        </attribute>
        <attribute name="directory" type="string" expanded="true" defaultValue="%SystemDrive%\inetpub\logs\LogFiles" validationType="nonEmptyString" />
        <attribute name="period" type="enum" defaultValue="Daily">
          <enum name="MaxSize" value="0"/>
          <enum name="Daily" value="1"/>
          <enum name="Weekly" value="2"/>
          <enum name="Monthly" value="3"/>
          <enum name="Hourly" value="4"/>
        </attribute>
        <attribute name="truncateSize" type="int64" defaultValue="20971520" validationType="integerRange" validationParameter="1048576,4294967295" />
        <attribute name="localTimeRollover" type="bool" defaultValue="false"/>
        <attribute name="enabled" type="bool" defaultValue="true" />
        <attribute name="logSiteId" type="bool" defaultValue="true" />
        <attribute name="flushByEntryCountW3CLog" type="uint" defaultValue="0" />
        <attribute name="maxLogLineLength" type="uint" validationType="integerRange" validationParameter="2,65536" defaultValue="65536" />
        <element name="customFields">
          <attribute name="maxCustomFieldLength" type="uint" validationType="integerRange" validationParameter="2,65536" defaultValue="4096" />
          <collection addElement="add" clearElement="clear">
            <attribute name="logFieldName" type="string" required="true" isUniqueKey="true" validationType="nonEmptyString" />
            <attribute name="sourceName" type="string" required="true" validationType="nonEmptyString" />
            <attribute name="sourceType" type="enum" required="true" >
              <enum name="RequestHeader" value="0"/>
              <enum name="ResponseHeader" value="1"/>
              <enum name="ServerVariable" value="2"/>
            </attribute>
          </collection>
        </element>
      </element>
      <element name="traceFailedRequestsLogging">
        <attribute name="enabled" type="bool" defaultValue="false" />
        <attribute name="directory" type="string" expanded="true" defaultValue="%SystemDrive%\inetpub\logs\FailedReqLogFiles"/>
        <attribute name="maxLogFiles" type="uint" defaultValue="50" validationType="integerRange" validationParameter="1,10000"/>
        <attribute name="maxLogFileSizeKB" type="uint" defaultValue="1024" validationType="integerRange" validationParameter="0,1048576"/>
        <attribute name="customActionsEnabled" type="bool" defaultValue="false"/>
      </element>

web.config でも変更できないのは、IIS10 でも変わっていません。

stackoverflow.com

https://docs.mendix.com/refguide5/review-log-files-ms-iis-serverdocs.mendix.com

Advanced Logging ならわんちゃん....?

forums.iis.net

Import-Csv でExchange ログを読んでみる

Import-Csv が対象にしているのは、Delimiter が カンマ , つまり、csv のフォーマットです。 W3C でCSV フォーマットのログ、パッとでませんでしたがあります、Microsoft Exchange です。

docs.microsoft.com

これは実際に、PowerShell チームが Import-Csv で W3C を読むときのテストデータからもわかります。

https://github.com/PowerShell/PowerShell/blob/master/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/TestImportCsv_W3C_ELF.csv

#Software: Microsoft Exchange Server
#Version: 15.0.0.0
#Log-type: Transport Connectivity Log
#Date: 2016-09-16T23:30:07.338Z
#Fields: Column1,Column2,Column 3
data1,1,A
data2,2,B
data3,3,C
data4,4,D

これを読んでみると、ヘッダ部分の # が無視されてデータが読み込まれたのが分かります。

PS> import-csv .\iis_log.csv
 
 # https://github.com/iSazonov/PowerShell/blob/0818b6c921c1970dc294669134266f878352891a/test/powershell/Modules/Microsoft.PowerShell.Utility/assets/TestImportCsv_W3C_ELF.csv
 <#
 Column1 Column2 Column 3
------- ------- --------
data1   1       A
data2   2       B
data3   3       C
data4   4       D
 #>

まとめ

Import-Csv で IISログの W3C ログは読めない、Exchange は読める。なるほど、Import-Csv ですからね。