[Tips] GitHub Actions で VNet 内の Azure Functions (C#) にデプロイする方法

これまでは、GitHub は個人で利用し、業務では Azure DevOps を使う機会が多く、結果として CI/CD パイプラインは Azure DevOps が提供する Azure Pipelines を使うことがほとんどでした。GitHub が Microsoft に Join し、GitHub / GitHub Enterprise を業務で利用できるようになったことで、私自身、GitHub Actions を使う機会が徐々に増えてきました。 

今回は、アプリケーション開発で頻繁に利用する Azure Functions (Function App) に対して、GitHub Actions を使ったデプロイ自動化の手順を整理します。エンタープライズ向けの開発では、Function App を Private Endpoint (インバウンド側の組み込み) を利用して、VNet に組み込む方法が一般的になってきましたが、その場合、インバウンドはプライベート通信しかサポートされない為、標準の GitHub-hosted Runner (ビルド＆デプロイ サーバー) を使ったパブリック IP 通信でのデプロイができず、その VNet に配置した VM 上に Self-hosted Runner をインストールして利用する必要があります。

#１. 環境
本稿では、以下の環境を利用しています。尚、GitHub リポジトリや GitHub Desktop の詳細な使い方は割愛します。

* [Visual Studio Community もしくは Visual Studio Code](https://visualstudio.microsoft.com/ja/free-developer-offers/)
* [GitHub アカウント](https://github.com/)
* [GitHub Desktop](https://desktop.github.com/)
* [Azure サブスクリプション](https://azure.microsoft.com/ja-jp/)

#２. Azure Functions (Function App) の作成
Azure Portal から以下のリソースを作成します。VNet に組み込んだ環境も作る為、Function App 以外のリソースも作成します。

* **Resource Group** ： TEST_GitHub
* **VNet** ： VNET_JE_GitHub
* **Linux VM** ： seijim-linux-vm-github
* **Function App** ： seijim-func-github

###(1) Resource Group の作成
![50_Azure_RG.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/143db2d3-5d06-f3fb-edea-fd5c94696e5b.png)

###(2) VNet の作成
サブネットは以下を作成します。

* **Private Endpoint 配置用サブネット** ： func-inbound-pe
* **Function App VNet 統合用サブネット** ： func-outbound-integrated
* **VM 配置用サブネット** ： vm

![51_Azure_VNet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/b1469afd-c9ca-9252-17f9-19f2aae6a78a.png)

![52_Azure_VNet.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/a3d1ecb9-6934-6947-7985-a01d974e6b05.png)

###(3) Linux VM の作成
イメージは、Data Science Virtual Machine - Ubuntu (ソフトウェアの追加インストールが不要) を指定し、ネットワークは、VNET_JE_GitHub の vm サブネットを指定します。
![03_Azure_VM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/148678e6-f575-922d-1cff-01bf3b694023.png)

![54_Azure_VM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/2494bb01-5306-0386-0872-f18e747a5df7.png)

###(4) Function App (C#) の作成
Function App は、Private Endpoint による接続をサポートしている Functions Premium Plan を選択します。Private Endpoint は、App Service Plan の PremiumV2, PremiumV3 でもサポートされています。閉域網でのデプロイが不要な場合は、実行プランの制約はありません。この段階では、VNet への組み込みはせずに、後のステップで実施します。

![55_Azure_FuncApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/3eb8587c-12b8-649b-c52f-7a3a27f81084.png)

![56_Azure_FuncApp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/b396c83a-7b90-7450-5427-c7fe693605b3.png)

#３. Function App のコード作成
###(1) GitHub リポジトリの作成
GitHub Desktop を使って、Function App のコードを作成する前に、新しいリポジトリを作成しておきます。
![01_GitHub_Desktop_NewRepo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/117bebc4-9d78-9ec6-040c-56e41e99fb1c.png)

###(2) Visual Studio で Function App のコード作成
Visual Studio を起動して、「新しいプロジェクトの作成」をクリックします。
![02_VS_NewProject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/203cd553-ec46-f4ea-f451-d033ad59bc85.png)

テンプレートの検索で、「function」と入力して、「Azure Functions」を選択します。
![03_VS_SearchTemplate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/33c980b1-a6c2-260f-8056-54a7f8a2724f.png)

プロジェクト名を入力して、ローカル Git リポジトリのパスを指定します。
![04_VS_FileName.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/bd187c55-bdeb-253a-381a-18213fa9a76f.png)

「Http trigger」を選択します。
![05_VS_TriggerType.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/a95bf000-65da-ccaa-4cc3-2709d9c7d2dd.png)

関数名を変更したい場合は、FunctionName 属性の「Function1」を変更してください。ここではそのままにしておきます。
![06_VS_EditFunc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/99d7b2bb-6b36-760b-2829-1905d60732a0.png)

デバッグ実行「▷」ボタンをクリックして、ローカル環境で動作確認を行います。
![07_VS_DebugFunc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/5b3d19d8-1dd0-daa5-4c47-f977c12bc583.png)

ブラウザを起動し、アドレス バーに、コマンド ウィンドウに表示されている URI をコピーして、クエリ パラメーター「name」に適当な文字列を設定して実行します。以下のような画面が表示されれば成功です。
![08_VS_DebugFuncEdge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/ebd7d64b-750a-ed59-a2b7-bde63ab1c09b.png)

###(3) GitHub に Function App ソリューション/プロジェクトをプッシュ
Function App のコードをコミットして、GitHub リポジトリのプッシュします。
![09_GitHub_Desktop_PushOrigin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/5b59bf4d-1579-4d36-b78c-11501b9ecf2c.png)

#４. GitHub Actions の構成
手順については、GitHub リポジトリ [Azure/functions-action](https://github.com/Azure/functions-action) の Readme の「Using Publish Profile as Deployment Credential (recommended)」セクションに記載されています。以下の手順となりますので、このステップを１つずつ確認して行きます。

* Azure Portal で当該 Function App に行く
* 「発行プロファイルの取得」をクリックして、.PublishSettings ファイルをダウンロードする
* .PublishSettings ファイルを開き、中身をコピーしておく
* GitHub に行き、Repository > Settings > Secrets で、コピーしておいた XML を貼り付けて新しいシークレットを作成する
* [Azure/functions-action](https://github.com/Azure/functions-action) に用意されたテンプレートを利用して、リポジトリに .github/workflows/<your-workflow>.yml を作成する
* 「env:」セクションにある変数を当該 Function App に合わせて変更する
* プロジェクト (コード) を GitHub リポジトリにコミット＆プッシュすると、新しい GitHub ワークフローが Actions タブで開始される

###(1) Azure Portal で Function App 発行プロファイルを取得
Azure Portal で対象の Function App に行き、概要ページの上部にある「発行プロファイルの取得」をクリックし、.PublishSettings ファイルをダウンロードします。

![20_Azure_Func_Profile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/4d23d3bb-5582-e2a6-24c1-d307c8822d83.png)
![21_Azure_Func_Profile_download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/0bd3a227-a2be-0685-3045-e8d284008e1c.png)

###(2) .PublishSettings ファイルの中身をコピー
ダウンロードした .PublishSettings ファイルを開き、中身をコピーします。
![22_Azure_Func_Profile_Copy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/73a11dd1-1d6a-557b-c15c-141bb8cfb4ac.png)

###(3) GitHub で新しいシークレットの作成
自分の GitHub リポジトリに行き、「Settings」タブの「Secrets」を選択して、「New repository secret」ボタンをクリックします。
![23_GitHub_SettingsSecrets.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/ac64a81f-68d4-5c3f-9258-d25b9d240a08.png)

「Name」に **AZURE_FUNCTIONAPP_PUBLISH_PROFILE**、「Value」には直前の手順でコピーしておいた XML を貼り付け、「Add secret」ボタンをクリックして、新しいシークレットを作成します。
![24_GitHub_New_Secret.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/8da1408c-5a14-bab6-39e2-65949eb3bbc3.png)
![25_GitHub_New_Secret.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/c1bfc195-b2c8-7a35-67ee-cd099f03607c.png)

###(4) GitHub ワークフロー (GitHub Actions) の作成
GitHub [Azure/functions-action](https://github.com/Azure/functions-action) に行き、Readme の以下セクションにあるテンプレートの表から DotNet 行 / Windows 列にある「windows-dotnet-functionapp-on-azure.yml」のリンクをクリックします。
![26_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/30bdb3c7-f746-c52b-20c0-e8a4a2d4be8d.png)

このテンプレート ファイル「windows-dotnet-functionapp-on-azure.yml」の中身をコピーしておきます。
![27_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/ecf96364-a2b3-3a1e-7848-5beed9296dcc.png)

自分の GitHub リポジトリの「Actions」タブで、「set up a workflow yourself」リンクをクリックします。
![28_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/4d6355e5-e5d0-62f8-3c4f-2d76a6f14e45.png)

生成された「main.yml」ファイルの中身を事前にコピーしておいたテンプレートの中身で置き換えます。
![29_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/3a37092b-2955-7844-e1fc-667c8a5cbf93.png)

###(5) GitHub ワークフロー (GitHub Actions) の修正
「main.yml」にコピペしたテンプレート (Function App のビルド＆デプロイ) を自分の Function App の環境に合わせる為、「env:」セクションにある以下の変数を修正します。尚、DOTNET_VERSION については、[こちらのサイト](https://versionsof.net/core/)を確認して、3.x の最新バージョンを選択しています。

* **AZURE_FUNCTIONAPP_NAME**: seijim-func-github  # Function App name
* **AZURE_FUNCTIONAPP_PACKAGE_PATH**: './FuncGitHub/FuncGitHub/'  # VS Project (csproj) file directory
* **DOTNET_VERSION**: '3.1.413'  # Latest .NET SDK 3.x version

![30_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/cf915609-eb0f-7de9-f842-73ebe92a9d49.png)

変数を修正したら、「Start commit」をクリックします。
![31_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/1a5672aa-19a3-46f8-d819-0c7bb9d9fe32.png)

###(6) GitHub ワークフロー (GitHub Actions) の実行結果を確認
「Actions」タブを開くと、ワークフロー (この場合、main.yml) の実行結果を確認することができます。今回は、main.yml のコミットでワークフローが実行されましたが、ソースコードの変更がコミットされると、同様にワークフローが実行されます。
![32_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/b63c79cc-4e8c-4315-5cc0-f6731a4f46f8.png)

「Start commit」を実施した際のコミット名称をクリックすると、ジョブ毎の実行結果が表示されます。
![33_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/147c73be-0397-4b18-fe91-66708dd15450.png)

ジョブ名をクリックすると、各ステップ毎の実行結果が表示されます。
![34_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/eeec558a-f53b-9637-865f-b631f45010c0.png)

###(7) Function App のデプロイ後のテスト
Azure Portal で対象の Function App に行き、「関数」ページから対象の関数 (この場合、Function1) をクリックし、「コードとテスト」ページ上部の「関数の URL の取得」をクリックします。
![35_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/892b368d-2fdf-c0ae-ed59-4b8a3f986666.png)
![36_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/ab2d4bf9-464c-085e-2a46-4aeb288ddd96.png)

表示された URL をコピーし、ブラウザを起動して、アドレスバーに貼り付け、クエリパラメーター「name=Azure!!」を追加して実行してみます。ローカルで実行した時と同じように、以下のような表示になれば、成功です。
![37_GitHub_New_GitHub_Actions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/9ffd71e8-9a4d-0592-91b9-a0f75bb5d3f6.png)

#５. Function App を VNet に組み込む (閉域化)
２章の手順 4 でも記載しましたが、Function App が Functions Premium Plan (もしくは、App Service Plan - PremiumV2 or PremiumV3) になっていることを確認してください。

###(1) インバウンド通信の閉域化
Function App では、インバウンド通信とアウトバウンド通信それぞれで VNet への組み込み方が異なります。ここでは、インバウンド通信を Private Endpoint/Link 機能を使って組み込みます。

Azure Portal で対象の Function App に行き、「ネットワーク」ページで、「受信トラフィック」ボックスの「プライベート エンドポイント」をクリックします。
![60_Azure_FunctionApp_閉域化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/7d30e7c3-b5bb-7503-0a5c-83f28dbda904.png)

「プライベート エンドポイント接続」ページで、「＋ 追加」をクリックします。
![61_Azure_FunctionApp_閉域化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/a62df61e-4d08-a037-e5be-1c265ba99620.png)

Private Endpoint の名称の他、２章で作成した VNet および サブネットを指定し、「OK」をクリックします。
![62_Azure_FunctionApp_閉域化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/99e9b318-a824-be2a-03b0-cb2fb09db2c9.png)

「接続状態」が「Approved」になっていれば、Private Endpoint は有効です。
![63_Azure_FunctionApp_閉域化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/92ce42a2-e910-810e-4738-ddef9de7460b.png)

###(2) アウトバウンド通信の閉域化
アウトバウンド通信は、VNet 統合機能を使って組み込みます。「ネットワーク」ページの「送信トラフィック」ボックスの「VNET 統合」に該当しますが、今回の検証において、アウトバウンド通信 (DB などへのアクセス) は発生しない為、ここでは **割愛**します。
![60_Azure_FunctionApp_閉域化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/7d30e7c3-b5bb-7503-0a5c-83f28dbda904.png)

###(3) GitHub ワークフローで再度ビルド＆デプロイを実行
GitHub リポジトリの「Code」タブで、Function1.cs ファイルを編集モードで開き、ログ出力部分のコードを修正し、コミットします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/2e870071-c559-3f2c-5cdd-65ffe10e3ed8.png)

**修正前**

```c#:Function1.cs
log.LogInformation("C# HTTP trigger function processed a request.");
```

**修正後**

```c#:Function1.cs
log.LogInformation("C# HTTP trigger function processed a request!!!!!");
```

###(4) GitHub ワークフローの実行結果
Function App の閉域化によって、デプロイ先にアクセスが出来ず、ワークフローが失敗することが分かります。
![65_Azure_FunctionApp_閉域化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/8332d6ca-b1e7-1d19-8287-10e26c4eafd1.png)
![66_Azure_FunctionApp_閉域化.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/d225a8c7-7033-56a6-2075-cab8ce817de3.png)

#６. Self-hosted Runner による閉域環境への対応
５章で、標準の GitHub-hosted Runner (ビルド＆デプロイ サーバー) では、パブリック IP 通信でのデプロイができないことが分かりました。これに対処する為、Function App を組み込んだ VNet に Self-hosted Runner を立てる必要があります。ここでは、その手順を記載します。

###(1) GitHub での操作
GitHub リポジトリで、「Settings」タブの「Actions」>「Runners」を開き、「New self-hosted runner」ボタンをクリックします。
![70_GitHub_Self-hostedRunner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/1e2fb350-6bb2-7a95-8a2d-0be1664f9904.png)

２章で作成した Linux VM を使う為、「Runner Image」は「Linux」を選択します。ここに Self-hosted Runner のダウンロード、セットアップの手順が示されています。
![71_GitHub_Self-hostedRunner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/7f8aa035-08f3-f4cb-ff16-61ae274dddd2.png)

###(2) SSH クライアントでの操作
２章で作成した Linux VM に SSH 接続を行い、前の手順 (1) で表示しているページに記載された Self-hosted Runner のダウンロード、セットアップ手順通りに実行します。以下は実行例ですので、必ず自分の GitHub 上の手順に従ってください。

```bash:Download（例）
# Create a folder
mkdir actions-runner && cd actions-runner
# Download the latest runner package
curl -o actions-runner-linux-x64-2.283.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.283.1/actions-runner-linux-x64-2.283.1.tar.gz
# Optional: Validate the hash
echo "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  actions-runner-linux-x64-2.283.1.tar.gz" | shasum -a 256 -c
# Extract the installer
tar xzf ./actions-runner-linux-x64-2.283.1.tar.gz
```

```bash:Configure（例）
# Create the runner and start the configuration experience
./config.sh --url https://github.com/seijim/seijim-func-github --token XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

./config.sh のステップを実行すると、GitHub に接続した後、いくつか入力を促されますが、基本的には Enter で大丈夫です。ラベル付けについては、既定のラベル ('self-hosted', 'Linux', 'X64') 以外が必要な場合に入力しておきます。ここでは、「runner1」と入力しました。
![72_GitHub_Self-hostedRunner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/a41d6865-b5e7-f0ba-8705-2403153591a1.png)

最後に ./run.sh で Self-hosted Runner を起動します。

```bash:Configure（例）
# Last step, run it!
./run.sh
```

以下のメッセージが表示されれば、Self-hosted Runner が正常に起動し、ジョブを待機している状態を示しています。
![73_GitHub_Self-hostedRunner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/bd4cb247-2ac8-7820-49be-c268b9f85828.png)

###(3) GitHub での状況確認
GitHub リポジトリの「Settings」タブで、「Actions」>「Runners」を開きます。作成した Self-hosted Runner が表示され、「Status」が「Idle」となっていれば、正常です。
![74_GitHub_Self-hostedRunner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/3ecf94ca-70d0-b6f5-a5af-5350301d47e0.png)

###(4) GitHub ワークフローの修正
GitHub リポジトリの「Code」タブで、./github/workflow/main.yml を開き、「runs-on」セクションで「windows-latest」から Self-hosted Runner のラベル ('self-hosted', 'Linux', 'X64', 'runner1') を設定することで、ワークフローを Self-hosted Runner で実行することができます。

```yaml:main.yml
jobs:
  build-and-deploy:
    runs-on: [self-hosted, linux, x64, runner1]
```

###(5) GitHub ワークフロー実行結果の確認
GitHub リポジトリの「Actions」タブで、ワークフローの実行結果を確認します。Function App の閉域環境においても、デプロイが成功することが分かります。
![76_GitHub_Self-hostedRunner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/5e25935c-32af-0bd4-625e-76e61fea90f6.png)

SSH クライアントでも成功を確認することができます。
![75_GitHub_Self-hostedRunner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/166299/e9aacc1a-4cc7-d03d-de37-c87fb344c92e.png)

###(6) Self-hosted Runner のサービス化
サービス化については、[こちらのページ](https://docs.github.com/ja/actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service)で、手順をご確認ください。

#まとめ
いかがだったでしょうか。比較的つまずくポイントは少ないのではないかと思います。
また、Windows OS を選択した Azure Functions へのビルド＆デプロイに Linux VM を使えるというのは、面白かったのではないでしょうか。
.NET (C#) が完全なマルチプラットフォーム OSS 言語ランタイムであることが分かるケースとなります。

#参照

* [Continuous delivery by using GitHub Action for Azure Functions (GitHub)](https://github.com/Azure/functions-action)
* [Continuous delivery by using GitHub Action for Azure Functions (MS Docs)](https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-github-actions?tabs=dotnet)
* [セルフホストランナーの追加](https://docs.github.com/ja/actions/hosting-your-own-runners/adding-self-hosted-runners)
* [ワークフローでのセルフホストランナーの利用](https://docs.github.com/ja/actions/hosting-your-own-runners/using-self-hosted-runners-in-a-workflow)
* [セルフホストランナーをサービスとして設定する](https://docs.github.com/ja/actions/hosting-your-own-runners/configuring-the-self-hosted-runner-application-as-a-service)
* [.NET Version](https://versionsof.net/)