C# Functions v2 + Git Submodule + Pipeline デプロイ

VS2019 + C# Functions v2 on Windows + Azure Repos + Git Submodule + Azure Pipelines を使用して、新規 Functions アプリの作成、リポジトリ登録から、Pipelines を使ったデプロイまでの一連の手順についての説明。 リポジトリからのデプロイは最終的には Pipelines から行うが、途中で Azure ポータルから App Service Kudu を使ったデプロイも試す。

想定環境

  • Visual Studio 2019 Community
  • C# Functions V2 (.NET Core) on Windows
  • Functions はスロット機能を使用
  • .NET Standard の共通ライブラリ用プロジェクト (Functions からプロジェクト参照で使用)
  • Azure Repos
  • Git Submodule (共通ライブラリ用プロジェクトは Submodule として使用)
  • Azure Pipeline (Builds + Releases) による Functions stg スロットへのデプロイ

前提条件

  • デプロイ先の Functions v2 リソースは Azure ポータルで作成済み
  • Functions のスロット機能で、std スロット作成済み
  • リポジトリ登録先の Azure DevOps プロジェクトは準備済み
  • VS2019 から Azure Repos に登録可能な状態。且つ、Git コマンドラインツールがインストールされて使用可能な状態

手順

共通ライブラリ用プロジェクトの作成・リポジトリ登録
  1. VS で「クラス ライブラリ (.NET Standard)」プロジェクトを新規作成
  2. 必要なコードを記述したら、ソリューションをソース管理に追加して、その後、Azure Repos に登録する (Push する)
  3. VS を閉じる
C# Functions v2 プロジェクトの作成・Submodule 追加・リポジトリ登録
  1. VS で C# Azure Functions v2 (.NET Core) のプロジェクトを新規作成する。なおここでは、プロジェクトを物理フォルダ単位で管理したいため、新規プロジェクト作成時の「ソリューションとプロジェクトを同じディレクトリに配置する」のチェックは外して作成する
  2. ソリューションをソース管理に追加して、その後、Azure Repos に登録する (Push する)
  3. VS を閉じる
  4. 事前に作成・リポジトリ登録した「共通ライブラリ用プロジェクト」のリポジトリを Azure DevOps ポータルで開き、右上の「Clone」から「https」のパスをコピーしておく

    f:id:poke_dev:20191013113025p:plain

  5. コマンドラインで当該 Function ソリューションのルート (.git 隠しフォルダがあるフォルダ) まで移動し、以下の git コマンドを実行して、Submodule として共通ライブラリ用プロジェクトを追加する

    > git submodule add 'コピーしておいた共通ライブラリのリポジトリの https URL'

  6. エクスプローラで、当該 Functions プロジェクトと同じレベルに共通ライブラリのプロジェクトフォルダが作成されていることを確認する

    f:id:poke_dev:20191013120452p:plain

  7. VS で当該 Function のソリューションを開く

  8. コマンドで追加した Submodule がコミット待ち状態になっているので、コミットする

    f:id:poke_dev:20191013120525p:plain

  9. Git 上は、親プロジェクト (ここではソリューション) の Submodule として共通プロジェクトが追加されたが、C# プロジェクトとしてはまだ利用する状態になっていないので、VS ソリューション エクスプローラでソリューションを右クリックし、[追加] - [既存のプロジェクト] で、当該 Functions フォルダに作成された共通ライブラリのプロジェクト (Submodule として追加されたプロジェクトであってオリジナルのプロジェクトではないので、注意) を追加する

    f:id:poke_dev:20191013121514p:plain

  10. 当該 Function プロジェクトから共通ライブラリプロジェクトをプロジェクト参照する。後は、通常のプログラム作成を行い、動作確認できる状態になったらリポジトリに Push する

    f:id:poke_dev:20191013121828p:plain

  11. Azure DevOps 上で当該リポジトリの状況を確認すると、参照しているサブモジュールは以下のように commit id のみ含む状態での管理となっていることがわかる

    f:id:poke_dev:20191013122435p:plain

  12. サブモジュールを使用するソリューションのリポジトリをクローンする時は、以下のように「サブモジュール付きで複製」を選ぶこと。ただの複製だと、サブモジュールのプロジェクト(リポジトリ)がプルされないので注意

    f:id:poke_dev:20191013125625p:plain

(補足) サブモジュールの更新について

Git submodule は、サブモジュールを使用している側ではソース変更はできないっぽいので(参照専用の扱い)、サブモジュール側の変更は必ずサブモジュールのリポジトリ ソリューションで行い、それをプッシュしたコミットを、使う側でそれぞれ取り込む流れにする必要があるっぽい。

サブモジュールの概念として、ブランチに追従するのではなく、コミット ID に追従するらしく、単にサブモジュールを pull しただけだと同じコミット ID を pull することになり、それ以降の更新を取得できない。

また、git submodule コマンドは VS GUI ではまだ未対応の様子。サブモジュールを最新にするには(最新のコミット ID へ参照し直すには)、以下をコマンドラインで実行する。

> git submodule foreach git pull origin master

上記を行った後のコミット作業からは、コマンドではなく、VS GUI 上で作業できる。 以下のコマンドで、今どのコミット ID を参照しているか、確認できる

> git submodule

もし VS 作業時にサブモジュールを参照しているソリューションでサブモジュールのソースを変更してしまったら(VS IDE でのソース変更自体は普通に出来てしまう)、その状態だとプッシュもできないし、上述の最新化もできないので、以下のコマンドでサブモジュールを元の状態に戻す。

> git submodule update -f

元に戻した後、サブモジュールを最新にできる。

Azure ポータルでの App Service Kudu デプロイ
  1. 最終的には Azure DevOps Pipelines によるデプロイを行うが、ここでは App Service Kudu を使用した Azure Repos からのデプロイを一旦試す
  2. Azure ポータルで当該 Functions を開き、stg スロットを選択し、[プラットフォーム機能] - [デプロイ センター] を選択する

    f:id:poke_dev:20191022155309p:plain

  3. 「Azure Repos」を選択して、「続行」をクリック

    f:id:poke_dev:20191022155601p:plain

  4. 「App Service のビルド サービス」を選択して、「続行」をクリック

    f:id:poke_dev:20191022155715p:plain

  5. 当該 Function アプリのリポジトリを選択して、「続行」をクリック

  6. 「完了」をクリック
  7. 自動的に Repos からの取得及びビルド・デプロイが行われるが、恐らく、失敗する
  8. 失敗した場合、エクスプローラーで当該 Function プロジェクトのソリューションのフォルダを確認し、「.gitmodules」ファイルをエディタで開く
  9. 共通ライブラリのプロジェクトが、url = https://xxx/projectname/_git/commonprj みたいな感じでフルパス定義されていると思うので、これを、url = ../commonprj のように相対パス指定に変更して、保存する (https://github.com/Microsoft/azure-pipelines-agent/issues/577)

    f:id:poke_dev:20191022165015p:plain

  10. 同様に、Functions ソースファイルの文字エンコードUTF-8 でない場合、error CS1009: Unrecognized escape sequence が発生してビルド失敗することがある。この場合、以下の手順でエンコードを変更する

    1. VS で Funtions (= 関数エントリポイント) のソースファイルを選択し、メニューから [ファイル] - [名前を付けてファイルを保存] をクリックする
    2. 保存オプションから [エンコード付きで保存] をクリックし、上書き確認が表示されたら、[はい] をクリックする

      f:id:poke_dev:20191022180917p:plain

    3. エンコードで「日本語 (シフト JIS)」が選ばれていたら、これを「Unicode (UTF-8 シグネチャ付き) - コードページ 65001」に変更し、[OK] をクリックして上書き保存する

      f:id:poke_dev:20191022181732p:plain

    4. これを、全ての Functions ファイルに対して行う。なお、後から追加したクラス ファイルなどがある場合、それらは既定で UTF-8 になっているはず。気にするのはあくまで関数 (= 関数エントリポイント) として追加されたファイルのみで良い

  11. VS で上記変更を Commit、Push する

  12. Push により、先程設定した App Service Kudu の自動デプロイが起動するので、今度は成功するのを確認する
  13. Function の関数の状態を確認し、デプロイが正常に行われたことを確認する
  14. デプロイ成功したが定義した関数が出てこない場合、Zip Deploy になっていないか、確認する。アプリ設定で WEBSITE_RUN_FROM_PACKAGE の値が 1 になっていると Zip Deploy なので、0 にするか、項目自体を削除して Web Deploy に変更する。App Service Kudu デプロイだと Web Deploy 専用になるっぽい(?)

    f:id:poke_dev:20191022175628p:plain

Azure DevOps Pipeline Builds の作成
  1. Azure DevOps ポータルで、Pipelines から [Builds] - [New] - [New build pipeline] をクリックする

    f:id:poke_dev:20191022161852p:plain

  2. 画面下の [Use the classic editor] リンクをクリックする (ここでは YAML は使わない)

    f:id:poke_dev:20191022162153p:plain

  3. [Azure Repos Git] で当該 Function プロジェクトのリポジトリを選択し、[Continue] をクリックする

    f:id:poke_dev:20191022162421p:plain

  4. テンプレート リストから [Azure Functions for .NET] を選択し、[Apply] をクリックする

    f:id:poke_dev:20191022162746p:plain

  5. Name を適切な名前に変える。Agent pool と Agent Specification は、既定で大丈夫みたい

    f:id:poke_dev:20191022163116p:plain

  6. Tasks から [Get sources] を選択し、右側で「Checkout submodules」のチェックを入れる

    f:id:poke_dev:20191022163642p:plain

  7. 一旦この状態で、「Save & queue」をクリックして、ビルドを行う

    f:id:poke_dev:20191022163831p:plain

  8. App Service Kudu ビルドの説明で記載したソースファイルのエンコード変更を行っていないと、Build フェーズで以下のエラーが起きるかもしれない。起きたら、上述した変更を行って再度ビルドを行う

    f:id:poke_dev:20191022165844p:plain

  9. 「Archive files」のタスクで、「ENOENT: no such file or directory, stat 'D:\a\1\s\publish_output'」みたいなエラーが出ることがある。出た場合は次の作業を行う

    1. 当該 Build の Edit で編集画面に戻り、タスクリストから「Archive files」を選択し、右側の [Root folder or file to archive] の値を、「publish_output/」から「$(Build.Repository.Name)/publish_output/」に変更する

      f:id:poke_dev:20191022183333p:plain

    ここではリポジトリ名と対象 Function アプリ名が同じ想定でいるのでこのようにしているが、要は、一つ前のビルドで生成された成果物が想定するパスに存在しないのが原因で、恐らくその原因はプロジェクト名が publish_output フォルダの上に存在するためなので、そこを指定できるようになればいい。変数を無理に使わなくても、直値でも OK。なお、ここでアプリ名を追加して通っても、今度はアプリ名が不要になることがあるようで、その場合はアプリ名をつけてるとダメになることがある・・・。

  10. 「Save & queue」をクリックして、ビルドを行う。今度はビルド成功することを確認する

  11. ビルドは成功しても、出来た成果物の中身が空っぽだったりすることがたまにあるので、この後のデプロイで期待したデプロイとならなかった場合は、右上の [Artifacts] - [drop] をクリックして、成果物をダウンロードして、中身を直接確認してみる。中身が想定してなかった状態の場合、build の設定に問題がある
Azure DevOps Pipeline Releases の作成
  1. Azure DevOps ポータルで、Pipelines から [Releases] - [New] - [New release pipeline] をクリックする

    f:id:poke_dev:20191022184119p:plain

  2. テンプレート リストから [Deploy a function app to Azure Functions] を選択し、[Apply] をクリックする (デプロイ先はスロットを対象とするが、ここでは with slot ではないテンプレートを使う)

    f:id:poke_dev:20191022184743p:plain

  3. [Add an artifact] をクリックし、先程作成した build pipeilne の成果物を選択し、[Add] をクリックする

    f:id:poke_dev:20191022185136p:plain

  4. [Tasks] をクリックし、「Azure Subscription」でデプロイ先リソースが存在する Azure サブスクリプションを選択し(権限がないと選択できない場合があるので注意)、「App type」で「Function App on Windows」を選択し、「App Service name」でデプロイ先 App Service の名称を選択する

    f:id:poke_dev:20191022185741p:plain

  5. タスクリストで「Deploy Azure Function App」を選択し、右側の詳細画面で「Deploy to Slot or App Service Environment」にチェックをつけ、「Resource group」でデプロイ先のリソース グループを選択し、「Slot」でデプロイ先のスロット名、ここでは「stg」を入力する。入力欄はドロップリストになっているものの、自動では対象スロット名が出てこないようで、手入力する必要があることに注意

    f:id:poke_dev:20191022191518p:plain

  6. 画面上部でリリース設定名を適宜入力し、[Save] をクリックし、確認ダイアログで [OK] をクリックする

    f:id:poke_dev:20191022192057p:plain

  7. [Create release] - [Create] をクリックして、Azure にデプロイを行い、成功するのを確認する。デプロイ方法は、既定で Zip Deploy になる様子

    f:id:poke_dev:20191022201630p:plain

  8. 成功した場合、Azure ポータルのデプロイ センターからも、どこからデプロイ設定されたかと、今までのデプロイ履歴が参照できるようになる

    f:id:poke_dev:20191022202142p:plain