しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure App Service (Windows / Linux) に Java アプリケーションをデプロイして動かす簡単な方法

最近 Spring Boot なアプリケーションを Azure App Service にデプロイしてみたら、いつの間にかに Java アプリケーションを動かすのがかなり簡単になっていたのでメモとして残しておきます。今回は Docker Image ではなく JAR をデプロイする方法を使います。

大学の授業で Eclipse と Swing を使った以来なのでかなり知識は古かったのですが、最近は Java に特化した Visual Studio Code のインストーラーを使うとサクッと環境が作れるので楽でした。

このインストーラーで入る Java は PATH が通ってない状態だったのが少し罠でした。実体のパスは JAVA_HOME から辿れるので、適当に追加しておくと良さそうです。

Spring Boot プロジェクトの作成は Extension として提供されている Spring Initializr を使うと、コマンドパレットから対話式で作成できるのであっさり終わりました。

f:id:shiba-yan:20200202225828p:plain

今回は Maven を使いましたが、Gradle も使うことが出来るようです。使用する言語や Spring のバージョン、そして追加する依存関係の選択まで一通りコマンドパレットで行えるのはかなり良いです。

f:id:shiba-yan:20200202225837p:plain

Visual Studio Code なのでプロジェクトを作成すれば、後は F5 を押せばデバッグ実行されます。大学時代には Eclipse を使ったデバッグ実行で四苦八苦したので、このあたりでモチベーションが高まりました。

今回はサンプルなので適当なコントローラーを追加して、プロジェクトを Azure Repos に入れました。

f:id:shiba-yan:20200202204945p:plain

Visual Studio Code からデプロイする機能はありますが、最初からちゃんと CI を組んでおくことが重要だと思っているので、今回もデプロイには Azure Pipelines を使います。

Java 向けの App Service を作成する

App Service を新規作成する際には Java SE / Tomcat / WildFly の 3 つが選べるようになっていますが、Windows と Linux で選べる項目に結構差があるのと、App Service の設定とは微妙に差異があります。

特に Java SE を選ぼうとすると Linux 固定になってしまいますが、実は Windows でも使えるようになっています。後から変えればよいので、Windows の場合は ASP.NET V4.7 あたりを選んでおくのが安パイです。

f:id:shiba-yan:20200128234237p:plain

Linux では使用する Docker Image の選択になるので重要ですが、Windows の場合は VM Image にランタイムが予めインストールされているので、設定を切り替えるぐらいの意味しか持ってません。*1

Windows 向けの Java 設定

一応ドキュメントは用意されていますが、App Service 側の設定というよりは Java のオプション回りの話が多かったです。一応 Jetty もインストールされているはずですが、なかったことにされてる気がします。

Azure Portal から Stack として Java を選択するといろんな項目が追加で表示されます。

Java version と Java container は好きなやつを選べばよいと思いますが、今回はシンプルに最新の Java 11 と素の JAR をデプロイする設定にしました。

f:id:shiba-yan:20200128234245p:plain

昔は Auto-update の選択肢がなかったはずなので、このあたり楽になったなと思います。全く関係ないですが Node.js では ~12 のような指定をすると v12 の最新を自動で使うようにできます。

後は Always on を有効化しておいて、コールドスタートを出来るだけ避けるようにしましょう。

Linux 向けの Java 設定

Linux に関してもドキュメントは Java のオプション回りの話が多いです。一応目を通しておきましょう。

Windows とはまた設定が異なり、Linux ではバージョンを細かくは指定できないようになってます。

f:id:shiba-yan:20200128234254p:plain

基本的には App Service が提供している latest の Docker Image を使う仕組みなので、バージョンを細かく固定したい場合には独自の Docker Image を使うように設定することになります。

とはいえ、そこまでやる場合は Docker Image でのデプロイにした方が楽なので、カスタムな Runtime Stack を作るのではなく Docker Image を CI で作るようにしましょう。

アプリケーションのデプロイ

最初に App Service が Java に対応した際には HttpPlatformHandler を使うために Web.config を用意する必要がありましたが、そういう時代は既に終わっていたようです。

ドキュメントにも書いてあるように app.jar というファイル名でデプロイすると、App Service が自動的に適切な設定でアプリケーションを立ち上げてくれるようになってました。

ちなみにこの挙動は Windows と Linux で共通となっています。

By default, App Service expects your JAR application to be named app.jar. If it has this name, it will be run automatically.

Configure Linux Java apps - Azure App Service | Microsoft Docs

何時から変更されたのかは分からないですが、Stack として Java を選択すると HttpPlatformHandler の設定は自動的に行われ、起動スクリプトまで用意されるという手の込みようです。

実際に app.jarwwwroot 直下に置いただけの Process Explorer の情報ですが、IIS は用意された起動スクリプトを使って Java アプリケーションを実行しています。

f:id:shiba-yan:20200202213314p:plain

起動時のパラメータを調整したい場合には startup.cmd というファイルを用意しておけば、それが優先して使われる仕組みになっていました。

デフォルトのスクリプトは要約すると以下のコマンドを実行しているだけなので、カスタマイズも簡単です。

"%JAVA_HOME%\bin\java.exe" -noverify -Djava.net.preferIPv4Stack=true -Dserver.port=%HTTP_PLATFORM_PORT% -jar "%_WEBAPP_DIR%\app.jar"

メモリ周りのオプションを設定したい場合は、このコマンドに追加して startup.cmd を作成すればよいはずです。そして app.jar とセットでデプロイすると自動的に使われます。

ちなみに Linux の場合は JAVA_OPTS を App Settings に追加すれば良さそうでした。

Azure Pipelines を使ってビルドとデプロイを行う

最後は Azure Pipelines でビルドを行い、App Service へのデプロイまで行います。長くなってきて説明が面倒なので、先に Azure Pipelines の定義を出しておきます。

やっていることは簡単で Maven を使って app.jar を作成し、Zip Deploy で App Service にデプロイしているだけです。今回は単一ファイルなので Run From Package ではなく Zip Deploy にしました。

# Maven package Java project Web App to Linux on Azure
# Build your Java project and deploy it to Azure as a Linux web app
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/java

trigger:
- master

variables:
  vmImageName: 'ubuntu-latest'
  MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository

stages:
- stage: Build
  displayName: Build stage
  jobs:
  - job: MavenPackageAndPublishArtifacts
    displayName: Maven Package and Publish Artifacts
    pool:
      vmImage: $(vmImageName)
    
    steps:
    - task: Cache@2
      inputs:
        key: 'maven | "$(Agent.OS)" | **/pom.xml'
        restoreKeys: |
           maven | "$(Agent.OS)"
           maven
        path: $(MAVEN_CACHE_FOLDER)
      displayName: Cache Maven local repo

    - task: Maven@3
      displayName: 'Maven Package'
      inputs:
        mavenPomFile: 'pom.xml'
        options: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)'

    - publish: target
      artifact: webapp
      displayName: 'Publish artifacts'

- stage: Deploy
  displayName: Deploy stage
  jobs:
  - job: DeployWebApp
    displayName: Deploy Web App
    pool: 
      vmImage: $(vmImageName)
    steps:
    - checkout: none
    - download: current
      artifact: webapp

    - task: ArchiveFiles@2
      inputs:
        rootFolderOrFile: '$(Pipeline.Workspace)/webapp/'
        includeRootFolder: false
        archiveType: 'zip'
        archiveFile: '$(Pipeline.Workspace)/$(Build.BuildId).zip'

    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'java11test-1'
        package: '$(Pipeline.Workspace)/*.zip'
        deploymentMethod: 'zipDeploy'

Maven は Pipeline Caching を使っておくとかなり処理時間を短縮できました。まだプレビュー扱いですが Task のバージョンも順調に上がっているのと、効果が劇的だったので使う価値があります。

ちなみに Pipeline Caching なしの場合、Maven の実行は 2 分 30 秒ぐらいかかってました。

f:id:shiba-yan:20200202205023p:plain

それが Pipeline Caching を有効にすると 15 秒ぐらいまでに短縮されました。Cache に必要な処理時間を考慮しても 30 秒ぐらいで完了しています。

f:id:shiba-yan:20200202205008p:plain

上の Pipeline 定義では Windows 向けのデプロイ定義しか書いてないですが、Linux も全く同じように Zip Deploy が使えるので、ほとんど同じ定義が使えるようになってます。

実際に Windows と Linux の App Service に Azure Pipelines からデプロイしましたが、問題なく同一の JAR が実行されていることが確認できました。

f:id:shiba-yan:20200202225500p:plain

単純に app.jar という名前のファイルをデプロイするだけで済むので、Web.config といった IIS 固有の知識も必要なくなり、かなり使い勝手が良くなったのではないかと感じます。

欲を言うと実体は zip な JAR を一度 zip にしてからデプロイというのは手間かなと思いました。

Zip Deploy 後に変更が反映されない場合

今回 Windows と Linux の両方で JAR のデプロイを試していたのですが、Windows 側だけデプロイしてもアプリケーションが更新されなかった現象に遭遇しました。App Service を再起動すれば反映されるので、単純に JAR をデプロイしたタイミングで IIS の Worker Process がリサイクルされていないようです。

Web.config が不要になって、JAR のみデプロイする形になったので IIS が変更を追跡できていないのかもしれません。調べてみると特殊なオプションが追加されていたので、以下の 2 つを設定すると Zip Deploy のタイミングでリサイクルが走るようになりました。

  • SCM_RESTART_APP_CONTAINER_AFTER_DEPLOYMENT
  • WEBSITE_RECYCLE_PREVIEW_ENABLED

上の設定は元々 Linux だけで利用可能なものでしたが、ちょっと前に WEBSITE_RECYCLE_PREVIEW_ENABLED と組み合わせることで Windows でも利用可能になったようです。

f:id:shiba-yan:20200202213454p:plain

これで Azure Pipelines からのデプロイ後に、ちゃんとデプロイしたアプリケーションが立ち上がるようになりました。本番で運用する場合にはちゃんと Deployment Slot を使って Swap する運用にしましょう。

Azure Spring Cloud は設計がかなりいけてる

今回の内容を書こうと思ったきっかけは、Ignite Tour Osaka で寺田さんに Java と App Service についていろいろ聞いたからなんですが、その時に紹介された Azure Spring Cloud がかなりいけてたので紹介します。

詳細は寺田さんのブログと動画を見てもらえれば理解できるはずです。特に動画は必見ですね。

App Service のようなマネージドサービスですが、裏側は AKS になっています。非常に良いと感じたのが利用者に裏側で Kubernetes が動いていることを意識させることなく、Java のアプリケーションを実行できるという点です。今の技術で App Service を再構成すると同じようなアーキテクチャになるでしょう。

個人的には Kubernetes は利用者が直接触るようなものではなく、Azure Cloud Services のようにサービスを構築するためのサービス、そういった足回りを支えるものだと感じています。なので Azure Spring Cloud の方向性は最高だと思いました。

*1:Framework JIT という機能があるようだけど、特に気にしなくてもよい

Terraform と Azure Pipelines を使って複数の環境を管理する

以前に Terraform と Azure Pipelines を使ってシンプルに App Service を作るのを試しましたが、現実的にはあんな単純な定義で済むはずはなく、開発環境や本番環境といった複数の環境への対応が必要になってきます。

実際に Terraform でインフラ周りの管理を行っているので、採用した定義をメモとして残します。Azure Pipelines での Terraform 利用については前回のエントリを見てください。

今回の方針は以下の通りになります。最初は 1 つの Pipeline で全部やらせようかと思ってましたが、YAML が複雑になってきたので放棄して分離する方向にもっていきました。

今なら Terraform Workspace を使った方が良いと言われそうですが、一応調べたところ管理が複雑になりそうだったのでシンプルに tfstate を別に管理する方法を選びました。

  • Pipeline は Dev / Prd で分ける
    • YAML も Dev 向けと Prd 向けで 2 つに分ける
    • ただしある程度の共通化はする
  • PR ではそれぞれの環境向けの tfstate を使って plan を実行
    • Branch Policy で PR 作成時に実行する Pipeline を定義する
  • マージされたタイミングで適切な tfstate を使って apply を実行
    • YAML の Trigger でビルドするブランチを指定
    • 今回はとりあえず master / release の 2 つで行う

といろいろと書いてますが、単純に環境ごとに Pipeline と tfstate を分離して混ざらないようにしつつ、かつ実行の条件を少なくしてシンプルな定義にするということです。

Azure Pipelines は stage / job / step で複数定義して condition で複雑な条件を作れますが、結局のところ一本道にした方が管理コストが低いです。

先に今回作成した Terraform と Azure Pipelines の定義を公開しておきます。GitHub にディレクトリ構造そのまま上げているので、Terraform で複数の環境に対応する際の参考にしてください。

ディレクトリ構造はまあ良く見るタイプのやつです。envs 以下には環境の定義と依存する変数や設定を、modules には実際の Terraform 定義を入れていくという構造です。

f:id:shiba-yan:20200120191920p:plain

環境毎に固有のリソースを持ちたい場合は module としてディレクトリを追加してあげればよいですね。あまり環境に固有のリソースを作りたくないので、今回は定義してません。

GitHub にも上げてありますが、一部分だけ Azure Pipelines の定義を引っ張ってきました。Terraform CLI のインストールと初期化は全ての Job で同じなので Template を使って共通化してあります。

trigger:
- master

variables:
  terraformVersion: '0.12.19'
  azureSubscription: 'Azure Sponsorship'
  workingDirectory: '$(Build.SourcesDirectory)/envs/dev'
  tfstateName: 'develop.tfstate'
  vmImage: 'ubuntu-latest'

stages:
- stage: Terraform_Plan
  condition: ne(variables['Build.SourceBranch'], 'refs/heads/master')
  jobs:
  - job: Develop
    pool:
      vmImage: $(vmImage)
    steps:
    - template: templates/terraform-init.yml

    - task: TerraformTaskV1@0
      inputs:
        provider: 'azurerm'
        command: 'plan'
        environmentServiceNameAzureRM: $(azureSubscription)
        workingDirectory: $(workingDirectory)
      displayName: terraform plan

重要なのは workingDirectorytfstateName ぐらいで、環境に依存する値が入るので、環境を追加する際は修正する必要があります。あとは condition で plan と apply の実行を分けているぐらいです。

定義が出来れば Azure Pipelines で 2 つ Pipeline を作成していくのですが、先に Git に YAML を置いておくと Pipeline の作成時にどの定義を使うか選択できるようになるので便利です。

f:id:shiba-yan:20200120200648p:plain

Pipeline 定義の YAML は今回 .azure とか適当にディレクトリを切って入れるようにしました。ルートに YAML がずらずらと並んでいるのは見にくいので。

今回は Dev / Prd の 2 環境なので Pipeline も 2 つ用意しておきます。さらに Stg 環境を追加する場合は、YAML と Pipeline を増やしていく形です。

f:id:shiba-yan:20200120192044p:plain

Pipeline を作成したら、Branch Policy から Build Validation を追加しておきます。今回は master / release という 2 つのブランチ*1を使うので、それぞれの環境向けの Pipeline を選んで追加すればよいです。

f:id:shiba-yan:20200120192410p:plain

これで Pull Request が作成されると、自動で terraform plan が実行されるようになります。もちろんビルドが通るまでは Pull Request のマージがブロックされるので安全です。

f:id:shiba-yan:20200120192559p:plain

Pull Request をマージすると YAML の trigger に従ってビルドが走るので、そのブランチに紐づいた環境向けの terraform apply が実行されます。暫くするとリソースが作成されるはずです。

f:id:shiba-yan:20200120194927p:plain

開発環境で作ってきたリソースを本番環境へ適用するには release への Pull Request を作成します。release への Pull Request を作成すると、同様に本番向けの terraform plan が走ります。

f:id:shiba-yan:20200120195116p:plain

そして Pull Request をマージすると、本番向けの terraform apply が実行されてリソースが作成されます。

f:id:shiba-yan:20200120195425p:plain

アプリケーションのデプロイと同じように、Pull Request と CI ベースでの運用になるので簡単ですね。

Service connection の Approval を使えば承認必須にできるかなと思いましたが、terraform plan の実行でも要求されてしまうので微妙です。一応は Branch Policy を使って特定ユーザーのレビューを強制させるぐらいは出来るので、そっちで妥協かなと考えています。

*1:develop / master という構成のが一般的かもしれない

Azure Pipelines の Azure Resource Manager 接続は 2 年で期限が切れるので注意

Azure App Service や Azure Functions に対してデプロイする場合には、必ず Service connections に Azure Resource Manager を追加していると思います。

その際は大体が Azure Pipelines から Service Principal を自動生成していると思います。

f:id:shiba-yan:20200117014008p:plain

ここから Service Principal を作成すると、Azure Portal の Azure AD から一覧を確認できます。

Azure Pipelines から作成された Service Principal の名前は組織名 + プロジェクト名 + サブスクリプション ID になるのでわかりやすいです。

f:id:shiba-yan:20200116194320p:plain

重要なのがクライアントシークレットで、Azure Pipelines が作成したものは 2 年の期限付きなので Service connection を追加してから 2 年が経過すると、ログイン出来なくなります。

f:id:shiba-yan:20200116193949p:plain

期限切れの通知などはしてくれないので、Pipeline の実行でエラーになってから気が付くことが多そうです。既に使ってる人は一度確認しておいた方がよいでしょう。

正直このあたりの情報を見た覚えがないので、既存の Service connection の更新方法と期限切れを起こさない Service Principal を作成して設定するまで試しました。

手動でクライアントシークレットを更新する

Service connection の設定には更新する機能は無さそうに見えますが、実は該当する Azure Resource Manager 接続から Edit を選ぶと更新が出来るようになってました。

Edit を選んで表示されるダイアログは、そのまま OK を押せるようになってます。OK を押すと、再度 Azure AD での認証が走るのでログイン画面が表示されるはずです。

f:id:shiba-yan:20200116201127p:plain

しばらくするとダイアログが閉じられるので、その後 Azure AD から Service Principal のクライアントシークレットが更新されたことが確認できます。

f:id:shiba-yan:20200116193959p:plain

Service connection の作成しなおしとは異なるので、既存の設定を壊すことなく更新できるのは良いです。更新日から 2 年間有効ですが、この作業がまた発生すると考えると面倒です。

無期限のクライアントシークレットを使う

Azure Pipelines から Service Principal を作成すると 2 年間のクライアントシークレットが作成されますが、手動の場合は無期限のクライアントシークレットを作成できます。

簡単に Service Principal を作成するのには Azure CLI がおすすめです。1 コマンドで作れます。

無期限という指定はないので、100 年間有効なクライアントシークレットを作成します。Azure CLI を以下のようなコマンドで実行すると作成されます。

az ad sp create-for-rbac -n "azpipeline" --role contributor --years 100

一応 Azure AD の画面から期限を確認しておくと、ちゃんと 100 年後になっています。

f:id:shiba-yan:20200116200847p:plain

あとは Service connection の追加画面から "use the full version of the service connection dialog." というリンクを選ぶと、既存の Service Principal を設定できるダイアログになるので、作成されたクライアント ID とクライアントシークレットを入力します。

設定後は Verify connection を選んで Service Principal のチェックを行っておきます。

f:id:shiba-yan:20200116195752p:plain

追加してしまえば、あとは通常の Service connection と同じように利用できます。YAML の編集時には Task アシスタントで Azure 上のリソース一覧を取得してくれますが、エラーが出ることなく動いてます。

f:id:shiba-yan:20200116200857p:plain

実質無期限のクライアントシークレットを使っているので、最初の設定だけが少し面倒ですが 2 年毎に更新する必要がないので楽です。更新は簡単とはいえ高権限のユーザーが行う必要があるのが少しネックです。

期限が長すぎるのもどうかなと思いますが、シークレットの失効は行えるのでまあ良いかという気持ちです。

後悔しないための Azure App Service 設計パターン (2020 年版)

Azure App Service (Web Apps) がリリースされて 6 年、情報のアップデートを行いつつ気になった情報は適当にブログに書くという日々ですが、Regional VNET Integration や Service Endpoins が使えるようになって設計に大きな変化が出るようになったのでまとめます。

最近は Microsoft で HackFest を行うことも多いのですが、App Service をこれから使い始めたいという場合に、失敗しない構成を共有したい、知ってほしいという意図もあります。多いですが中身は単純です。

アーキテクチャという観点では App Service だけで収まるものではないのですが、今回は App Service 周りに限って書いています。単純に App Service は機能がめちゃくちゃ多いというのもあります。

ひとまず自分が思いついたものを一通り書いたはずですが、足りないものがあれば後で追記するかもしれません。一部 Preview な機能も混ざってはいます。

基本設定

64bit Worker は必要な場合のみ利用する

最初の方で地味に迷うのが 32bit / 64bit の設定だと思います。デフォルトでは 32bit になっていますが、大量にメモリを消費することがわかっているアプリケーション以外では 32bit のままで問題ありません。

f:id:shiba-yan:20200112184344p:plain

64bit にすると扱えるメモリは増えますが、同時にメモリの使用量も増えます。

App Service は 2 コア以上の App Service Plan を使わない限り、大体は 4GB 以下のインスタンスなのを忘れないようにしましょう。あとアプリケーションだけではなく Kudu も同時に 64bit になるので注意。

FTP / Web Deploy をオフにする

FTP / FTPS の無効化はセキュリティ的な観点でも重要ですが、FTP / Web Deploy といったデプロイ方法は、今となってはアトミックではなくバージョン管理もされない悪手です。

利用できる状態にしていると、簡単にアプリを壊すことが出来るのでオフにします。

本当に最初期の開発ならともかく、それ以降でも Visual Studio から Web Deploy を使って新しいアプリケーションのデプロイを行うのは、あり得ない状態だと認識してください。

Always on を有効化する

App Service はアーキテクチャ的にコールドスタートは発生しうるものですが、Always on を有効化することでトラフィックが無い時にインスタンスが落ちるのを防ぐことが出来ます。

f:id:shiba-yan:20200112193039p:plain

これで常にウォームスタートになるので、いつトラフィックが来ても素早く応答することが可能です。

ウォームアップリクエストを受け取ったタイミングで、コネクションやキャッシュといった必要な初期化処理を行っておくと更に効果的です。

ARR affinity をオフにする

Session Affinity のために自動的に App Service がクッキーを発行して、2 回目以降も同じインスタンスにルーティングする仕組みですが、自動的にインスタンスの入れ替えが発生する App Service とは根本的に相性が悪いです。全てのリクエストにクッキーが付くので効率的にも微妙です。

ステートレスなアプリケーション設計ならば不要なのでオフにします。

アプリケーションがセッションを利用している場合には、ちゃんと Redis Cache や Cosmos DB といったインスタンス間での共有ストアを用意して、リクエストを受けたインスタンスに依存した作りにしないことです。

当然ながら ARR Affinity をオンにしていると、ロードバランサーは偏ったルーティングを行うこともあるので、一部のインスタンスに負荷が集中してしまうことも容易に考えられます。

HTTP/2 の有効化を検討する

HTTP/2 を有効化したからと言って全てが改善するわけではないですが、App Service では HTTP/2 と HTTP/1.1 が簡単に切り替え出来るのでテストが行いやすいです。

f:id:shiba-yan:20200112192450p:plain

実際のところケースバイケースという話が多いので、実際のアプリケーションでベンチマークを行っていくしかないですが、試してみる価値はあるでしょう。

Health Checks の有効化を検討する

プレビュー中の機能ですが、Health Checks を有効化すると App Service が設定されたエンドポイントへのリクエストを行い、失敗が続いているインスタンスは LB から切り離してくれるようになります。

LB から切り離されたインスタンスは、リクエストが成功すると再び LB に戻されます。この動作によって不良インスタンスがリクエストを処理するのを最小限に出来ます。

切り離されたインスタンスが 1 時間失敗し続けている場合には自動でリサイクルが行われて、完全に最初からアプリケーションの立ち上げが行われます。

タイムゾーンの設定を検討する

App Service に限らずパブリッククラウドを使っていて戸惑うことが多いのが、タイムゾーンが UTC で設定されていることだと思います。

日付をタイムゾーン込みで扱うのが理想ではありますが、国内のみ対象にした場合には割り切ってタイムゾーンを変更してしまうのが簡単です。

昔はタイムゾーンの設定は出来なかったので +9 時間するコードがよく書かれていましたが、今は App Service のアプリケーション単位での切り替えが出来るようになっています。

認証が必要な場合は App Service Authentication (Easy Auth) を利用する

関係者のみ見れるようにしたいステージングサイトや、本番向けでも認証が必要な場合には App Service Authentication (Easy Auth) を使うのが非常に簡単です。

後述する Deployment slot と組み合わせると、ステージングサイトを簡単に保護できるようになります。

f:id:shiba-yan:20200112190930p:plain

昔は Basic 認証を行うモジュールを個別にインストールするケースもありましたが、Easy Auth を使うと数クリックで Azure AD に登録されているユーザーのみ閲覧可能に出来ます。

ドキュメントには記載されていませんが、OpenID Connect に対応しているので Azure Portal に設定項目がないプロバイダーでも対応可能です。Azure Functions との組み合わせが特に強力です。

アプリケーション設定 / 接続文字列

Web.config / appsettings.json にべた書きしない

開発環境用の設定や接続文字列であれば問題ないケースは多いですが、本番環境向けの設定は Web.config や appsettings.json といったリポジトリに入るファイルには書かずに App Service 側でオーバーライドします。

たまに全く App Settings / Connection Strings を使っていないケースもありますが、ファイルに書いていると簡単に事故るので分けて管理しましょう。

App Service に設定する方法は ARM Template や Terraform といった IaC を利用すると、コードで分かりやすく定義でき更にバージョン管理もされるのでお勧めです。

アプリケーションレベルで対応する場合には、Key Vault や App Configuration を直接利用するという方法もありますが、後述する Managed Identity と組み合わせても最低限の設定は必要となります。

Key Vault Reference の利用を検討する

SQL Database や Azure Storage などの接続文字列を個別に App Settings に追加するのではなく、Key Vault の Secret として追加して App Service からは Key Vault を参照する形にすると安全に一元管理が行えます。

一部制約があり、現在は Key Vault の Service Endpoints を有効化すると、App Service が VNET Integration を使っていてもアクセスできなくなります。

Managed Identity の利用を検討する

理想としては Managed Identity を使って App Service 単位でアクセス可能なリソースを割り当てることで、キーの管理自体を無くしてしまうことです。寿命の短い Bearer Token を使った認証になるので安全です。

いくつかの Azure サービスが Managed Identity を使ったアクセスに対応してきていますが、クライアントが Managed Identity に適した設計になっていないケースもあるので、まだ全面導入は難しい部分があります。

ネットワーク設定

HTTPS を常に有効化する

世界的に常時 HTTPS での通信は当たり前になっています。App Service はデフォルトでオンになっていないですが、チェックを入れるだけで常に HTTPS へリダイレクトさせることが出来ます。

f:id:shiba-yan:20200112191031p:plain

TLS バージョンの選択も出来ますが、ここはデフォルトの TLS 1.2 で普通は問題ありません。こちらも世界的に TLS 1.0 / 1.1 はセキュアではないので、TLS 1.2 以降を利用する流れとなっています。

Regional VNET Integration を利用する

以前から VNET Integration は実装されていましたが、Point-to-Point VPN を使っていたので VNET ゲートウェイが必要で Service Endpoints や ExpressRoute が使えないなど制約が非常に多かったです。

新しい Regional VNET Integration ではそういった制約はなくなっています。

f:id:shiba-yan:20200112191246p:plain

ASE や VM のように専用のインスタンスが VNET 内に直接配置されるわけではなく、あくまでもマルチテナントでの実行となりますが、これまでは ASE でのみ実現可能だったアーキテクチャをマルチテナントの App Service でも構築出来るようになりました。

IP 制限ではなく Service Endpoints を利用する

App Service では元から IP アドレスベースでのアクセス制限は行えましたが、Service Endpoints が追加されたことにより特定の VNET の Subnet からの通信のみ許可するといった設定が行えるようになっています。

フロントエンドに用意した App Service が VNET を経由して、Service Endpoints でアクセス元を制限したバックエンドの App Service を実行するという構成も簡単に構築できます。

Service Endpoints は既に数多くの Azure サービスで対応しているので、App Service で VNET Integration を使っていればアクセス元の制御が簡単に行えるようになっています。

特に SQL Database や Cosmos DB といったストレージ系のサービスでは IP アドレスでのアクセス制限ぐらいしか出来なかったのが、Subnet 単位で許可出来るようになったのはかなり大きいです。

VNET Integration と合わせて積極的に利用していきましょう。IP アドレスでの制限は全て捨てます。

CDN / Front Door の導入を検討する

App Service の前に CDN や Front Door を置くことで、コンテンツ配信の最適化をすることが簡単に行えるようになっています。設定も Azure Portal から行えるので迷うことは無いでしょう。

最近のアプリケーションは静的ファイルが非常に多くなってきているので、適切なキャッシュ戦略と CDN を使った最適化は必須になってきています。

f:id:shiba-yan:20200112191116p:plain

特に Front Door は CDN と L7 LB を組み合わせたサービスなので、アプリケーションの負荷分散や DR にも使えます。ルーティングも柔軟に行えるので、アーキテクチャを変更した時にも Front Door で対応できます。

静的なコンテンツの配信には CDN / Front Door が圧倒的に有利なので、早めに導入を検討してお行きたいサービスです。Traffic Manager とは異なり Front Door は L7 LB なのでバックエンドの切り替えもすぐです。

スケーリング設定

本番環境では Premium V2 を利用する

App Service Plan はいくつも Tier があって最初は悩むかもしれませんが、最低でも Premium V2 の P1v2 を選んでおきましょう。Premium V2 はコストとパフォーマンスのバランスが取れています。

f:id:shiba-yan:20200112192306p:plain

今は Premium V2 を使う予定はないからといって、Standard や Basic で作ってしまうと Regional VNET Integration が利用できない Scale unit に当たってしまうこともあるので、最初は必ず Premium V2 を指定してインスタンスを作るのが正解です。

作成後は Standard や Basic に変更しても問題ありませんが、Premium V2 とそれ以外の Tier 間の変更には Outbound IP アドレスの変更が伴うので、その点だけ注意が必要です。

今は変更のタイミングで警告メッセージが表示されるようになっていますが、IP 制限に使っている場合には問題となるので VNET Integration と Service Endpoints を使っておくと安心です。

オートスケーリング設定を検討する

今の App Service のオートスケーリング設定は Azure Monitor ベースで、様々なメトリックを元にルールを作成できるようになっています。それ以外にも曜日での固定スケーリングルールも作成できます。

App Service はスケーリングが非常に高速に行われるので、オートスケーリング設定との相性が非常に良いです。常時同じアクセスが発生するアプリケーション以外ではオートスケーリングを検討する価値があります。

Consumption / Premium Plan を活用する

イベントドリブンなアプリケーションの場合は Azure Functions と Consumption / Premium Plan を使って、イベントベースでのスケーリングを利用するようにします。

App Service Plan を使うよりスケーリングが早く、インスタンス数の上限も多いため大量のイベントを簡単に処理できるようになります。Premium Plan を使うとコールドスタートを避けつつも、イベントベースでのスケーリングが利用できるので検討しましょう。

アプリケーションのデプロイ

最低でも Zip Deploy を利用する

App Service には複数のデプロイ方法が用意されていて、FTP / Source Control / Web Deploy / Zip Deploy などがありますが、カジュアルな Web サイト以外では Zip Deploy を利用しましょう。

FTP / Web Deploy はデプロイがアトミックではないので、一部のファイルがデプロイ失敗すると整合性が破綻してアプリケーションが正しく動作しなくなります。

よくデプロイで発生する問題が古いファイルが残っていて、良くわからないエラーが発生することですが、FTP / Web Deploy を使っているから発生する問題と言えます。

特に Visual Studio からデプロイする際に使われる Web Deploy は今となってはパフォーマンスも悪く、デプロイに失敗しやすいのでメリットがほぼありません。CI を導入して Zip Deploy に切り替えると安定します。

Run From Package の利用を検討する

Zip Deploy ではアップロードされた Zip ファイルを展開して wwwroot 以下にデプロイする仕組みですが、Run From Package を使うと zip をそのままマウントするので、デプロイ時間の短縮や起動パフォーマンス改善につながります。もちろん単一ファイルをマウントするのでアトミックです。

Consumption / Premium Plan で Azure Functions を利用する場合には必須、App Service で Web アプリケーションを利用する場合にはローカルストレージへの書き込みを行わない場合に利用を検討出来ます。

App Service はローカルストレージのパフォーマンスが良くはないので、状態は外部のストレージに持たせる設計が多いはずです。その場合は Run From Package が使えます。

Deployment slot を使って Swap でのリリースを行う

開発中はともかく、本番環境へのデプロイ時には動作している App Service に対して直接デプロイするのではなく、別のスロットへのデプロイを行った後にスワップでリリースを行うようにします。

スワップのタイミングで App Service がウォームアップを行ってくれるので、初回のリクエストが遅いといった問題を回避しつつ、新バージョンでエラーが発生した場合には再スワップで元のバージョンに戻す運用が簡単に実現できます。

スワップ時のウォームアップへの対応や App Settings に追加すると良いキーなどいろんな情報がありますが、Ruslan のブログに書いてある内容が非常に参考になります。

Azure Pipelines / GitHub Actions などを利用して自動化する

初期の開発中はともかく、基本的には App Service へのデプロイは Azure Pipelines や GitHub Actions などの CI SaaS を使ってビルドとデプロイを自動化します。

App Service へのデプロイ自体は Task や Action が用意されているので簡単です。

Visual Studio や VS Code などを使って手動でデプロイするのはバージョン管理出来ていないですし、デプロイに必要な権限すら適切に管理出来てないことになります。

権限管理が出来ていないのはあり得ないので、予め CI 前提で考えていく必要があります。App Service のデプロイに必要な権限は実際かなり強いです。

リソース構築のコード化 (IaC)

ARM Template / Terraform を使って構築する

App Service は機能が多いので、その分設定項目も非常に多いです。なので複数の環境を用意する際に Azure Portal から手作業で作成するとミスが発生したり、設定が微妙に異なるという問題が簡単に発生します。

そういった問題を避けるためにも、最初から ARM Template や Terraform を使ってコード化して管理します。

App Settings や Connection Strings に関しても、ARM Template や Terraform を使うと管理できるため、実行時に動的に変更する必要がないものは IaC に寄せた方が管理しやすくなります。

Terraform を使う場合は、それ自体を Azure Pipelines などを使って自動デプロイするように構築します。

モニタリング / 診断

Application Insights を有効化する

アプリケーションに直接 Application Insights SDK を入れている場合はキーの設定だけ良いですが、それ以外の場合は Azure Portal から有効化するか、必要な App Settings を追加することで有効に出来ます。

f:id:shiba-yan:20200112194210p:plain

Application Insights を利用したモニタリングと実践については去年の de:code で話しているので、使用したスライドを参考にしてもらえれば良いかと。

モニタリング系は問題が発生してからでは遅いので、きっちりと最初から設定しておく必要があります。

特に Application Insights は 5GB まで無料の容量課金なので設定しない手はありません。

Azure Monitor を使ったログ転送を有効化する

歴史的経緯から App Service のログはローカルストレージや Azure Storage にしか書き出せませんでしたが、Azure Monitor との統合が行われて他のサービスと同じ機能が使えるようになりました。

f:id:shiba-yan:20200112194120p:plain

ファイルとして書き出すだけではなく、Log Analytics や Event Hub への書き出せるので、KQL を使って複雑な条件で絞り込むことも簡単に出来るようになっています。クエリを簡単に実行できるのは重要です。

Event Hub を使えばストリームでログを処理できます。異常検知を行いたい場合にも使えそうです。

App Service Diagnostics を積極的に活用する

App Service は組み込みの診断機能が非常に優秀で機能が豊富です。Azure Portal から "Diagnose and solve problems" を選ぶと対話式に診断を開始できます。

様々なメトリックから問題を検出してくれるので、App Service で動かしているアプリケーションに問題が発生した場合は、必ず最初に試しておくことを推奨しています。

特に TCP Connections は SNAT 絡みで問題となるケースが多いので便利です。一部の診断は Consumption で使えない制約がありますが、一時的に App Service Plan 上に移動してしまえば問題ありません。

API Management にカスタムドメインと SSL/TLS 証明書を追加してみた

これまでに API Management をちゃんと弄ったことがなかったのと、運用するとなるとカスタムドメインと証明書設定は必須になるので絞って確認してみました。

カスタムドメインは Azure の場合、確認用の CNAME / TXT レコードを作成する必要があることが多いので、その辺りを調べておきたいという意図です。特に証明書に関してはローテーションが必要になるので、出来れば Key Vault に寄せておきたいです。

ドキュメントによると Gateway / Portal / Management / SCM の 4 つに対して、個別にカスタムドメインの割り当てが可能と書いてありますが、基本は Gateway を抑えておけばよいです。

テスト用に API Management を作成しておきました。Consumption 以外はデプロイに 15 分ぐらいかかるので注意が必要です。バックエンドが Cloud Services なのでインフラ周りは改善してほしいです。

API Management を作成した後は、適当に Swagger 定義を読み込ませて API を作成しておきます。

f:id:shiba-yan:20191220171012p:plain

ここで作成した API は Product と紐づけておけば利用できるようになります。API Management は結構重厚なサービスなので Subscription や Product といった要素があるので、理解するのは少し大変です。

とはいえ、使わないという方法や Consumption を使うとかなりの機能が制限されているので、基本的な API Gateway としてまず使うのが良いかと思いました。

Azure DNS に CNAME レコードを追加

先に DNS レコードの追加が必要かと思っていましたが、実は全く関係ないドメイン名でも API Gateway に追加可能のようです。App Service のように検証はしてくれなかったので、この点は注意が必要です。

Consumption 以外では A / CNAME の両方が使えるようですが、CNAME を使うのが安全です。

f:id:shiba-yan:20191222144229p:plain

これで保存すれば、設定したドメイン名で API Management への解決が行えるようになります。

Key Vault に証明書を作成

証明書は API Management へのカスタムドメイン追加時に同時に指定する必要があるので、先に Key Vault 側に証明書を作成しておきます。

自己署名証明書でも問題ないかもしれませんが、今回は Let's Encrypt の証明書を使いました。

f:id:shiba-yan:20191222144642p:plain

Azure DNS と Key Vault を使って Let's Encrypt を作るには、前に作った keyvault-acmebot を使いました。サクサク証明書を Key Vault に作成できるので便利です。

Key Vault を使うと証明書の一元管理が簡単に行えるようになるので、App Service で使う場合でも Key Vault 経由で証明書をインポートした方が便利なことが多いです。

API Management にカスタムドメインと証明書を追加

ここまでで DNS レコードと Key Vault 証明書の準備が完了したので、API Management に設定を追加していきます。Custom domains から Add を選ぶとドメイン追加の画面になります。

ドメインの割当先は Gateway にしておきます。Developer Portal への割り当ても考え方は同じなので、必要な場合は追加しておけばよいです。

f:id:shiba-yan:20191220014412p:plain

ログインに OpenID Connect を使っている場合は、リダイレクト URL の変更も必要になるはずです。

証明書は PFX をアップロードする方法と Key Vault 証明書を参照する方法の 2 つが選べますが、PFX の手動管理は事故の元なので Key Vault を使うようにしていきましょう。

f:id:shiba-yan:20191220014420p:plain

対象となる Key Vault と証明書を選ぶだけなので、非常に簡単に設定できます。

保存しようとすると、初回の場合は以下のようなメッセージが表示されると思うので、Yes を選んで Managed Identity と Key Vault のアクセスポリシーの設定を自動的に行ってもらいましょう。

f:id:shiba-yan:20191220014452p:plain

ドキュメントでは自分で設定が必要という風に書いてありましたが、Azure Portal からの場合は全て自動でやってくれるので便利です。App Service と同様の仕組みですね。

f:id:shiba-yan:20191220014537p:plain

Consumption Tier 以外の場合は設定完了までに 10 数分かかるようなのでしばらく待ちます。Cloud Services で動いてるインスタンスにデプロイしに行っているからか、割と時間がかかります。

f:id:shiba-yan:20191220014630p:plain

設定が完了すれば、追加したカスタムドメインを使って API Management へのアクセスが行えるようになります。Key Vault 証明書を選んだ場合は、更新があれば自動的にアップデートしてくれるようでした。

API を呼び出して確認

設定が完了したので適当に API を呼び出して、カスタムドメインと証明書が設定されているか確認します。

f:id:shiba-yan:20191220015830p:plain

ちゃんと Let's Encrypt で発行した証明書が使われていることが確認できます。

Protocol settings からは SSL/TLS の暗号スイートやプロトコル、HTTP/2 の有無なども設定できるので、要件に合わせて適宜選ぶことができます。デフォルトでは TLS 1.2 のみ有効になっています。

f:id:shiba-yan:20191225190408p:plain

基本的には安全側に倒すような設定になっているので、変更する際はその副作用をしっかりと理解しておく必要があります。ほとんどのケースではデフォルト設定のままで良いはずです。

利用する際には Consumption Tier がかなり良いと感じているので、時間があればスケールについても調べてみたいですね。ちなみに Consumption は App Service 上に作られているので、多少は制限があります。

Azure Pipelines に追加された特定の Service Connection で承認を必須にする機能が便利

Azure DevOps の Sprint 158 Update で紹介されていた割に全然公開されていなかった Service Connection に対する承認機能が、最近になって自分のテナントでも使えるようになってました。

Sprint 158 Update は 9 月なので内容を既に忘れている人も居そうです。

紹介されているようなインターフェースとは異なっていて、Edit の横の「…」ボタンを選ぶと Approvals and Checks の項目が出てきます。機能としては Environments のものと同じです。

f:id:shiba-yan:20191217182255p:plain

何が嬉しいかというと、本番向けの Azure Subscription など気軽に弄られたら困るような Service Connection に対して、ここで設定をしておけば Task で使われた場合に自動的に承認が必須になるという点です。

これまでは Environments を使うしかなかったので、必要な処理は Deployment job にまとめて実行する必要がありましたが、もっとシンプルに承認機能を使えるようになりました。

f:id:shiba-yan:20191217182305p:plain

設定方法などは完全に Environments と同じなので、既に使ったことがある人は簡単に設定できるはずです。この辺りも徐々に改善されていて、今後もオプションの追加も予定されているとか。

Environments や Deployment job は今後 Kubernetes 以外のリソースへの対応や、Deployment strategy も追加予定なのでアプリケーションのデプロイ向けには便利ですが、それ以外の IaC などの用途では Service Connection を使った方が簡単に実現できるのでお勧めです。

色々と試しているうちに、複数の Task で Service Connection を使っている場合に、どのような挙動になるのかが気になったので思いついたパターンを検証してみました。

Stage が順番に動く場合

恐らく一番単純な、承認が必要な Stage が順番に動いていくパターンです。サンプルとして App Service へのデプロイを行っていますが、ここは Service Connection を使う Task なら何でも良いです。

- stage: Deploy_to_Dev
  jobs:
  - job: Deploy_to_Dev
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

- stage: Deploy_to_Prd
  jobs:
  - job: Deploy_to_Prd
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

dependsOn で依存関係を定義していないので、書かれた順に実行されていきます。動かすと 1 つめの Stage に差し掛かったタイミングで承認待ちの状態に入りました。

f:id:shiba-yan:20191217182117p:plain

承認するかどうか選択する画面では、対象の Service Connection が表示されるので分かりやすいです。この辺りの UI は大きくは変わっていないように感じます。

f:id:shiba-yan:20191217182125p:plain

ここで承認すると、現在の Stage の実行完了後に後続の Stage でも承認待ち状態になりました。

f:id:shiba-yan:20191217182138p:plain

承認待ちは Stage 単位で毎回走るようです。1 度の承認で同じ Service Connection を使っている Stage が実行されても困るので、この挙動は安心しました。

Stage が同時に動く場合

次は dependsOn を設定して、2 つの Stage が同時に動く場合にどうなるかを試してみました。

- stage: Deploy_to_Dev
  dependsOn: Publish
  jobs:
  - job: Deploy_to_Dev
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

- stage: Deploy_to_Prd
  dependsOn: Publish
  jobs:
  - job: Deploy_to_Prd
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

同時に同じリソースにデプロイしてるのはテストなので気にしないでください。

実行してみると、この場合は 2 つの Stage が両方とも同時に承認待ち状態になりました。

f:id:shiba-yan:20191218015608p:plain

そして承認するかどうかの画面には 2 つ表示されました。それぞれの Stage 毎に個別に承認・非承認を行えるようになっているみたいです。

f:id:shiba-yan:20191217182106p:plain

なので片方だけ承認しないということが簡単に行えました。Stage の設計が重要になりそうです。

f:id:shiba-yan:20191219003446p:plain

ちなみに承認しなかった Stage に表示されている Rerun failed jobs ボタンを押すと、もう一度承認待ちからやり直しになります。間違った時のリカバリーに使えます。

複数の承認待ちを扱えることは知らなかったので、興味深い結果でした。何 Stage まで同時に待てるのか気になりますが、検証が結構面倒なので確認まではしないでおきます。

1 つの Stage 内で 2 つの Job が動く場合

最後はこれまで使った 2 つの Stage を 1 つにまとめた場合です。具体的には Job を複数にします。

- stage: Deploy
  jobs:
  - job: Deploy_to_Dev
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

  - job: Deploy_to_Prd
    pool:
      vmImage: 'windows-latest'
    steps:
    - download: current
    - task: AzureWebApp@1
      inputs:
        azureSubscription: 'Azure Sponsorship'
        appType: 'webApp'
        appName: 'approvalstest'
        package: '$(Pipeline.Workspace)/**/WebApplication1.zip'
        deploymentMethod: 'auto'

これも実行してみると Stage の実行前に承認待ち状態になりました。Stage 単位でしか承認待ちは行えないので、1 つだけ表示されています。

f:id:shiba-yan:20191218014202p:plain

この状態で承認すると 2 つの Job がそれぞれ同時に動き出しました。

f:id:shiba-yan:20191218015145p:plain

従って Service Connection を 1 つだけ利用する場合は、適当に使っていても問題ないケースが多そうです。

しかし Azure Subscription などリソースグループ単位で権限が与えられるような場合は、複数の Service Connection を用意することもあると思います。その場合には Stage を分けておくのが安全です。

Deno を Azure App Service (Windows / IIS) で動かしてみた

Twitter で記事が流れてきたり、通知が飛んできたので Deno というランタイムを知ったわけですが、どうやらそのままだと Windows の App Service 上では動かなかったらしいです。

App Service はサンドボックス環境で動くので多少制限が厳しいですが、エラー内容に違和感を持ったので自分でも試してみることにしました。

Deno インストール用の posh はそのままだと動かなかったので、Kudu から curl と 7za を使って手動で D:\home\.deno\bin に exe を置きました。こういうのは Site Extension で撒いても良い感じです。

おもむろに実行してみると確かに Panic が発生して立ち上がってくれません。

f:id:shiba-yan:20191217173355p:plain

Home Directory が取れないというのは謎いので、もうちょっと深堀してみます。

該当の Issue に当たってみると FOLDERID_Profile についてコメントが書かれていたので、ユーザープロファイル周りが怪しいと感じ始めました。

Rust のドキュメントには Windows の場合は SHGetKnownFolderPath を使っていると書かれていたので、App Service はデフォルトでユーザープロファイルを読み込んでないことが原因だと思いました。

Windows:
This function retrieves the user profile folder using SHGetKnownFolderPath.

dirs::home_dir - Rust

なので App Settings に WEBSITE_LOAD_USER_PROFILE の設定を追加します。あまり表には出てこないですが、証明書を読み込む場合などでたまに使うこともあるので知っておいて損はないです。

f:id:shiba-yan:20191217173509p:plain

これで Worker Process がユーザープロファイルを読み込むようになるので、Deno をもう一度叩いてみると Panic を発生させることなく立ち上がりました。

f:id:shiba-yan:20191217173629p:plain

プロセスが立ち上がってしまえば、後は HttpPlatformHandler でプロセスの管理と HTTP のプロキシをさせてしまえばよいので簡単です。

考え方は Node.js のデプロイの時と同じなので、特に難しいことはありません。

以下のような web.config を作成しました。HttpPlatformHandler の一般的な定義です。

example.ts は元記事のものをそのまま貰ってきて wwwroot 以下に保存しました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="httpPlatformHandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified" />
    </handlers>
    <httpPlatform processPath="D:\home\.deno\bin\deno" arguments="--allow-net example.ts -p %HTTP_PLATFORM_PORT%" />
  </system.webServer>
</configuration>

これで全ての準備が整ったので、App Service にアクセスしてみると無事にページが返ってくるはずです。

f:id:shiba-yan:20191217174100p:plain

Process Explorer を開くとフロント用の w3wp の下に deno がぶら下がっていることが確認できます。

f:id:shiba-yan:20191217174110p:plain

コールドスタートに関しては Node.js よりも速い気がしたので、今後に期待したいです。

App Service では PATH を通すことは出来ないですが、Site Extension を使うとランタイムの追加は自由に出来るので、暇があれば作ってみたいところです。

Azure Cosmos DB v3 SDK の Bulk Support がとても使い勝手が良かった

少し前にリリースされた Azure Cosmos DB v3 SDK の Bulk Support を試そうと思いつつ放置してたのですが、仕事で割と良い感じのお題が降ってきたのでこれを機に試してみました。

開発チームの人がブログで色々と紹介してくれているので、一通り目を通しておくと良い感じです。Medium の方も同じようなタイトルですが、内容は違うので両方読んでおいた方が良いです。

v3 SDK の Bulk Support の特徴は、クライアント作成時にオプションを有効化すれば、後は透過的に複数のリクエストをバッチに変換して投げてくれるところです。

これまでも Bulk Executor を使うと Cosmos DB の RU を最大限に使って項目の追加が行えていましたが、未だに .NET Core への対応はプレビューのままで改善が期待できなかったり、初期化周りで準備が色々と必要でライブラリとしては使い勝手が悪かったです。

Bulk Executor のドキュメントにも v3 SDK を勧めるメッセージが表示されています。

If you are using bulk executor, please see the latest version 3.x of the .NET SDK, which has bulk executor built into the SDK.

Azure Cosmos DB: Bulk executor .NET API, SDK & resources | Microsoft Docs

同じように Change Feed も v3 SDK に組み込まれているので、v3 を勧めるメッセージが出てきます。

If you are using change feed processor, please see the latest version 3.x of the .NET SDK, which has change feed built into the SDK.

Azure Cosmos DB: .NET Change Feed Processor API, SDK & resources | Microsoft Docs

将来的に Bulk Executor と Change Feed Processor は v3 / v4 を使うコードに変更する必要が出てくるはずです。ただし Azure Functions の Trigger に関しては事情が複雑です。

Bulk Support が公開されたのは v3.4.0 なので、それより新しいライブラリをインストールしておきます。v3 SDK はかなり開発ペースが速いのであっという間に古くなるので注意。

そもそも v3 SDK をまだ使っていない人はサクッとアップデートしておきましょう。DocumentClient から v3 の CosmosClient に移行すると API がシンプルになっています。

基本的な使い方については GA のタイミングでまとめておいたので参考にしてください。

説明はこのくらいにして実際にコードを書いて試していきます。今回は適当なデータを大量に Cosmos DB に投入するコードを v3 SDK と Bulk Executor の両方で書いて試してみました。

折角なので Cosmos DB 側は Autopilot を有効にしたデータベースを用意しておきました。

f:id:shiba-yan:20191212192701p:plain

Autopilot もちゃんと検証しておこうと思っていましたが、何だかんだで後回しにしていたので丁度良かったです。上の設定では最低 2000 RU/s から最大 20000 RU/s まで自動的にスケールします。

まだプレビューだからかもしれないですが、上限が固定値だったりメトリクスが分かりにくいので今後に期待してます。あと RU の単価が 1.5 倍になるのはちょっと高い気がします。

v3 SDK の Bulk Support を使ったバージョン

先に新しい v3 SDK を使ったコードです。1 万件のデータを Cosmos DB に投入するだけの単純なコードですが、AllowBulkExecution を設定するだけで内部的にバッチで処理されます。

コード的には Task.WhenAll を使って複数 Task の完了を待つようにしているだけです。この辺りのイディオムは C# では日常的に出てくるやつです。

class Program
{
    static async Task Main(string[] args)
    {
        var connectionString = "AccountEndpoint=https://xxxx.documents.azure.com:443/;AccountKey=xxxx;";

        var cosmosClient = new CosmosClient(connectionString, new CosmosClientOptions
        {
            AllowBulkExecution = true,
            SerializerOptions = new CosmosSerializationOptions
            {
                PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
            }
        });

        var container = cosmosClient.GetContainer("TestDB", "TestContainer");

        var tasks = Enumerable.Range(1, 10000)
                              .Select(x => new DemoItem
                              {
                                  Id = Guid.NewGuid().ToString(),
                                  Name = $"kazuakix-{x}",
                                  CreatedOn = DateTime.Now,
                                  Pk = Guid.NewGuid().ToString()
                              })
                              .Select(x => container.CreateItemAsync(x, new PartitionKey(x.Pk)));

        await Task.WhenAll(tasks);
    }
}

Bulk Executor と同じように消費した RU を見ながら適切に処理を行っているので、Cosmos DB を使っているときによく見る 429 が出続けたりすることはありませんでした。

今回は Autopilot で 20000 RU/s までスケールするので 429 は発生しにくいですが、固定 RU/s で AllowBulkExecution の設定をせずに実行すると、普通に 429 の連発になります。

古い Bulk Executor を使ったバージョン

そして今度は Bulk Executor を使ったコードです。処理自体は BulkImportAsync を呼び出すだけなのでシンプルですが、実行するまでの準備が地味に複雑です。

リトライ設定などを手動で上書きしないといけないのはかなり分かりにくいですし、DocumentClient ベースなので Database / Container の扱いが冗長です。

class Program
{
    static async Task Main(string[] args)
    {
        var endpoint = new Uri("https://xxxx.documents.azure.com:443");
        var accountKey = "xxxx";

        var connectionPolicy = new ConnectionPolicy
        {
            ConnectionMode = ConnectionMode.Direct,
            ConnectionProtocol = Protocol.Tcp
        };

        var client = new DocumentClient(endpoint, accountKey, connectionPolicy);

        var documentCollection = await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri("TestDB", "TestContainer"));

        client.ConnectionPolicy.RetryOptions.MaxRetryWaitTimeInSeconds = 30;
        client.ConnectionPolicy.RetryOptions.MaxRetryAttemptsOnThrottledRequests = 9;

        var bulkExecutor = new BulkExecutor(client, documentCollection.Resource);

        await bulkExecutor.InitializeAsync();

        client.ConnectionPolicy.RetryOptions.MaxRetryWaitTimeInSeconds = 0;
        client.ConnectionPolicy.RetryOptions.MaxRetryAttemptsOnThrottledRequests = 0;

        var documents = Enumerable.Range(1, 10000)
                                  .Select(x => new DemoItem
                                  {
                                      Id = Guid.NewGuid().ToString(),
                                      Name = $"kazuakix-{x}",
                                      CreatedOn = DateTime.Now,
                                      Pk = Guid.NewGuid().ToString()
                                  });

        await bulkExecutor.BulkImportAsync(documents);
    }
}

こちらも当然ながら 429 を連発することなく、スループットを見ながら最適な処理を行ってくれます。

それぞれのコードを実行してみたところ、処理時間はほぼ変わっていないので良い感じです。気になった点としては RU/s の消費が v3 SDK の方が多かったことですが、処理時間はほぼ同じなので問題ないです。

f:id:shiba-yan:20191213180637p:plain

RU/s は時間課金なのでこういうケースの場合は上限まで使い切れた方が効率が良いです。書き込みデータサイズは同じはずなので、内部処理に違いが結構あるのかもしれません。

Azure Functions で v3 SDK を使う

前述したように Azure Functions の Cosmos DB Trigger は残念ながら v2 ベースなので、v3 の CosmosClient を使うためには手動でのインストールが必要です。

一応内部的には v3 への移行を行っているらしいですが、まだ時間がかかりそうです。

とはいえ、v3 を使った方が圧倒的にコードが書きやすいので、Change Feed 以外は CosmosClient を使って書いていくようにした方が良いです。

Azure App Service の .NET Core 3.1 対応と Azure Functions v3 が Go Live リリース

.NET Core 2.2 と 3.0 の EOL が近づいてきていますが、App Service と Azure Functions は無事に年内に LTS となる .NET Core 3.1 への対応が完了しました。

特に 2.2 の EOL は今年中なので、サクッと .NET Core 3.1 へのアップデートを行っておくと良いです。EOL になったからと言って即座にダメになるわけではないですが、数バージョン飛ばしのアップデートは検証が大変になるので定期的に行いましょう。

App Service の .NET Core 3.1 対応

今週中に App Service への .NET Core 3.1 デプロイが完了するという予定でしたが、順調に進んだからか今朝全てのリージョンへのデプロイが完了したらしいです。

例によって Azure Portal からは .NET Core 3.1 が選べないようですが、Windows 側にはインストールされているので問題なく利用できます。

.NET Core 3.0 の時と同様に SDK は含まれずランタイムだけデプロイされています。なので Kudu を使ったビルドでは .NET Core 3.x が使えないのはこれまで通りなので、Azure Pipelines などでビルドしたパッケージを Run From Package でデプロイというのが鉄板です。

f:id:shiba-yan:20191210153740p:plain

既に .NET Core 3.0 への移行を行っている場合は TFM を netcoreapp3.1 に変えれば大体問題ないですが、Cookie の SameSite 周りで 1 点だけ影響範囲の大きい Breaking change があったので、ドキュメントを読んでからの設定と動作確認は必須です。

それ以外は特に影響は無さそうなので、3.0 へのアップデートが終わっている環境であれば完了です。2.2 から 3.1 へのアップデートは 3.0 での Breaking changes の対応を行う必要があるので割とやることが多いです。

Azure Functions v3 (.NET Core 3.1) が Go Live

数日前に .NET Core 3.1 が使えるようになって嬉しさ爆発してたんですが、あっという間に Go Live で使えるランタイムがリリースされました。Go Live と言ってますが、残りはツール周りの整合性を取るぐらいらしいので、ランタイム自体は実質 GA 相当のようです。

実質 GA になっているので、何もしなくても Visual Studio から v3 向けプロジェクトが作成できます。あの裏技っぽい環境変数の設定は必要なくなっています。

f:id:shiba-yan:20191210124113p:plain

v3 の GA は来年の 1 月ぐらいになりそうです。v3 は高い後方互換性を持っているので移行は楽です。

この間書いたエントリがそのまま Go Live としてリリースされたランタイムを使っていたので、基本的な使い方はこっちを読んでおいてください。

v2 からの移行に関しては TFM と Function Version を変えて SDK をアップデートするぐらいです。

移行のドキュメントも公開されてますが、JavaScript はまあまあ Breaking changes があります。.NET Core に関しては普通に書いていれば問題ないレベルなので、特に気にしなくても良いです。

v2 から実装が変わった部分があるらしく、FUNCTIONS_V2_COMPATIBILITY_MODE という設定が追加されています。これに true をセットすると古い実装に戻るようですが、パッと見た感じでは使う必要なさそうです。

移行自体は楽ですが Entity Framework Core 3.1 が使えるようになったりするので、そういった部分の対応が必要となります。

その辺りはケースバイケースなので頑張るしかないですが、ASP.NET Core アプリと共有プロジェクトを持っている場合は、全てを .NET Standard 2.1 か .NET Core App 3.1 に統一できるので楽になるはずです。

ここまでは前回書いた内容とあまり違いはないですが、.NET Core 3.1 と対応した Azure Functions Runtime が App Service にデプロイされたので、Azure にデプロイして動かすことが出来るようになっています。

Azure Portal から Runtime Version が 3.0.12930 以降になっていれば使えます。

f:id:shiba-yan:20191210154106p:plain

適当に作成した v3 Function をデプロイすると、サクッと .NET Core 3.1 上で実行されます。

f:id:shiba-yan:20191210154215p:plain

動かしてみるとこれだけですが、Azure Functions v3 の魅力は .NET Core 3.x で追加された新しいライブラリや言語機能を使えるという点なので、積極的にアップデートしていきたいです。

Azure Pipelines の .NET Core 3.1 対応

App Service へデプロイするために重要なのが Azure Pipelines ですが、例によって .NET Core 3.1 SDK がインストールされた Hosted Agent の更新が遅れているようで、まだ手持ちの環境では入ってませんでした。

なので基本的には UseDotNet task を使ってバージョンを固定するのが良いです。

f:id:shiba-yan:20191210160922p:plain

一応、今月リリースされた Image には .NET Core 3.1 SDK が含まれていましたが、実際に使えるようになるのが何時なのかは不明です。今月中にデプロイが間に合わない可能性もあります。

Hosted Agent の更新タイミングによってアップデートが左右されるのはイマイチなので、バージョンは明示的に指定してインストールするようにしましょう。

Azure Functions v3 で .NET Core 3.1 が利用可能になった

Ignite 2019 のタイミングで Azure Functions v3 のプレビューが正式に公開されましたが、Azure Functions SDK 側の問題で .NET Standard 2.1 ターゲットのライブラリ*1が使えなかったので弄って来ませんでした。

その後 Runtime も一時的に非公開になりましたが、最近全て解決したので試します。現在 v3 はプレビューとプレリリースの 2 種類が公開されています。

f:id:shiba-yan:20191206215355p:plain

プレビューとプレリリースの差が分かりにくいですが、今のところはプレリリースの方が新しい Core Tools とテンプレートとなっています。それぞれの違いは大体以下の通りです。

  • v3-preview
    • .NET Core 3.0 (netcoreapp3.0)
    • Functions SDK v1.0.30-beta2
    • Core Tools v3.0.4
  • v3-prerelease
    • .NET Core 3.1 (netcoreapp3.1 も使える)
    • Functions SDK v3.0.1
    • Core Tools v3.1.0

次の更新ぐらいで v3-preview にも v3-prerelease の内容が降ってくると思うので、v3 のプレビュー中は v3-prerelease を使っておいた方が良さそうな気配です。

Azure Functions v3 の GA 時には .NET Core 3.1 がターゲットになるので、実際にデプロイしたいケースを除いて v3-prerelease 版が出た今となっては .NET Core 3.0 向けを弄る必要はないです。

Timelines
A public preview will be available in October 2019
General Availability, running on .NET Core 3.1 (LTS release), scheduled for Q1 2020

Azure Functions 3.0 · Issue #200 · Azure/app-service-announcements · GitHub

今のところ v3 に必要な .NET Core 3.1 が App Service にデプロイされていないのと、Functions Runtime が古いままなので v3-prerelease をデプロイ可能になるには少し時間がかかります。

App Service の .NET Core 3.1 対応は 12/13 頃に完了する予定らしいです。リージョンやスケールユニットによって多少は前後しそうですが、LTS 版が早めに展開されるのは喜ばしいですね。

前置きはこのくらいにして、実際に Entity Framework Core 3.1 を使う Azure Functions のコードを書いてみることにします。v3-prerelease を使っていきます。

Azure Functions v3 のプレビュー / プレリリースは環境変数を設定しないと出てこないので、以下の記事を参照して設定を追加する必要があります。ユーザー環境変数に追加して VS の再起動で OK です。

Functions SDK はこれまでの 1.0.x から一気に上がって 3.0.1 になりました。SDK はプレリリース版になっていないですが、依存関係は netcoreapp3.0 になっているので v3 専用です。

v3-prerelease を選んでプロジェクトを作成し、Microsoft.EntityFrameworkCore.SqlServer と DI 用の Microsoft.Azure.Functions.Extensions をインストールします。最近の Azure Functions ではインスタンスのライフサイクル管理のために DI を使うのが一般的です。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3-prerelease</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.1" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

デフォルトでは netcoreapp3.0 で作成されますが、手動で netcoreapp3.1 に変更しました。

DI 周りと Entity Framework Core を使うためのコードは以下のように用意しました。エンティティ周りは適当に用意して、接続文字列は SQL Database Serverless を作成して設定しました。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddDbContextPool<AppDbContext>(options =>
            options.UseSqlServer(Environment.GetEnvironmentVariable("DefaultSqlConnection")));
    }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

public class Blog
{
    public int Id { get; set; }

    [Required]
    public string Title { get; set; }

    [Required]
    public string Body { get; set; }

    [Required]
    public DateTime CreatedAt { get; set; }
}

あまり見ないであろう AddDbContextPool を使っているのは何となくです。作成してある SQL Database Serverless 側にテーブルを作成して、適当なデータを追加しておきました。

f:id:shiba-yan:20191206232039p:plain

Function 本体はコンストラクタで AppDbContext を受け取って、関数の本体で利用しているだけです。単純に DB に入っているものを JSON として返すだけの簡単なコードです。

public class Function1
{
    public Function1(AppDbContext appDbContext)
    {
        _appDbContext = appDbContext;
    }

    private readonly AppDbContext _appDbContext;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
        ILogger log)
    {
        log.LogInformation($".NET Core Version: {Environment.Version}");

        var list = await _appDbContext.Blogs.ToListAsync();

        return new OkObjectResult(list);
    }
}

Visual Studio からデバッグ実行すると、.NET Core 3.1 で動作していることがログから確認できます。

f:id:shiba-yan:20191206235736p:plain

Entity Framework Core 周りでもエラーが出ることなく、DB からデータを取得して応答が返ってくることが確認できました。サクッと .NET Core 3.1 / .NET Standard 2.1 のライブラリが使えたので内容が地味です。

f:id:shiba-yan:20191206234255p:plain

自分は使う予定が今のところないですが .NET Core に対応した Entity Framework 6 も .NET Standard 2.1 がターゲットなので、Azure Functions v3 で使えるようになっているはずです。

少し前に v2 と v3 で Cold start 周りの検証をしたところ、v3 の方が応答時間のばらつきが少なかったのでパフォーマンス面でも v3 のリリースを割と期待しています。*2

*1:Entity Framework Core 3.0 のこと

*2:https://twitter.com/shibayan/status/1195190127917592576