Azure Functions をデプロイする GitHub Actions の仕組みを理解する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29416/b88ca0df-e54d-577c-fd06-f6838b7a6a1c.png)
[Microsoft Azure Tech Advent Calendar 2019](https://qiita.com/advent-calendar/2019/microsoft-azure-tech) の21日目です。

# Intro

パイプラインを実現する様々な GitHub Actions の開発がオープンソースで進んでいますが、利用するだけでなく独自の Actions を作成することができます。

- [GitHub アクションでワークフローを自動化する](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions)
  - [アクションのビルド](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/building-actions)

この記事では、まずは Azure Functions を GitHub Actions でデプロイしたうえで、パイプラインの動きをログで確認しながら、Actions のソースコードを見ていきたいと思います。

# Azure Functions を GitHub Actions でデプロイする

正式な手順やドキュメントは以下に情報があります。

- [Docs: Continuous delivery by using GitHub Action](https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-github-actions?tabs=javascript)
- https://github.com/Azure/actions
  - [Azure/functions-action](https://github.com/Azure/functions-action)
  - [Azure/functions-container-action](https://github.com/Azure/functions-container-action)

大枠の流れは以下のとおりです。

1. デプロイ先の Azure Functions を作成する
2. その Functions に対する権限を持ったサービスプリンシパルを作成する
3. GitHub Actions が Functions に対して操作できるよう、Settings > Secrets に登録する
4. .github/workflows/***.yml を用意して、パイプライン処理を実行する

今回、なるべくデプロイまでは簡単に済むようにサンプルコードを用意したので、以下からの手順を参考ください。

## 手順

### GitHub リポジトリ

このリポジトリをご自身のレポジトリへフォークします。
https://github.com/hoisjp/hello-ghactions-azfunc-nodejs.git

git clone します。`YOUR_ACCOUNT` をご自身のアカウントに置き換えてください。

```
git clone https://github.com/YOUR_ACCOUNT/hello-ghactions-azfunc-nodejs.git
```

次に、Azure 側の準備をします。用意するのは2つ、(1) Azure Functions 、(2) サービスプリンシパルの作成です。

### Azure Functions

デプロイ先の Azure Functions を作成します。Azure ポータルから作成しましょう。今回は以下の構成で作成します。

- 従量課金プラン
- ランタイム： Node.js

### サービスプリンシパル

サービスプリンシパルを作成します。Azure Cloud Shell にログインして、以下のコマンドを実行します。上の手順で作成した Functions の `<SUBSCRIPTION_ID>`、`<RESOURCE_GROUP>`、`<APP_NAME>`を置き換えてください。

```
az ad sp create-for-rbac \
  --name "myApp" \
  --role contributor \
  --scopes /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Web/sites/<APP_NAME> \
  --sdk-auth
```
実行すると、以下のようにサービスプリンシパルのJSONが表示されます。これを後ほど設定に利用するため、控えておきます。

```
{
  "clientId": "{UUID_MASKED}",
  "clientSecret": "{UUID_MASKED}",
  "subscriptionId": "{UUID_MASKED}",
  "tenantId": "{UUID_MASKED}",
  "activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
  "resourceManagerEndpointUrl": "https://management.azure.com/",
  "activeDirectoryGraphResourceId": "https://graph.windows.net/",
  "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
  "galleryEndpointUrl": "https://gallery.azure.com/",
  "managementEndpointUrl": "https://management.core.windows.net/"
}
```

### GitHub Secrets 設定

次に GitHub 側に Secrets を登録していきます。

#### AZURE_CREDENTIALS

`AZURE_CREDENTIALS` に先ほど作成したサービスプリンシパルを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29416/d426c806-4934-50d2-0577-0d8aa7cd3804.png)

#### AZURE_FUNCTIONAPPNAME

続いて、`AZURE_FUNCTIONAPPNAME` に Functions の名前を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29416/89c40d73-9b9c-967b-4fcc-dc2495dd9769.png)
※補足：この手順は、本来のドキュメントでは直接 YAML ファイルを書き換えるようになっています。
サンプルコードを公開するうえで、デプロイ先の名前がリポジトリ上に記載されるのが気持ち悪いと思いますので、以下のように `secrets.AZURE_FUNCTIONAPPNAME` から名前を取得するようにしました。

.github/workflows/windows-nodejs-functionapp-on-azure.yml

```
    - name: 'Run Azure Functions Action'
      uses: Azure/functions-action@v1
      id: fa
      with:
        app-name: ${{ secrets.AZURE_FUNCTIONAPPNAME }}
```

さて、設定は以上です。

### GitHub Actions をトリガー

あとは git に変更があるたびに、GitHub Actions が実行されます。適当に Hello *** などの部分を変更してみます。

/HttpTrigger1/index.js

```
            body: "Hello GitHub Actions " + (req.query.name || req.body.name)
```

push 直後から、GitHub レポジトリの、`Actions` というタブから実行されたことを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29416/41319265-93f7-5404-c8e9-6e7650e3ed7f.png)


# GitHub Actions 本体の仕組みを調べていく

さてここからは GitHub Actions のソースコードを覗いていきたいと思います。

## ソースコードを探索

### GitHub Actions 実行ログ

先ほど実行された Actions の `Run Azure Functions Action` を展開すると実行ログの詳細を確認することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29416/63dc0220-8d56-7824-72be-9723982f6b4d.png)

以下のような実行ログを確認することができます。

```
Run Azure Functions Action1m 1s
[GET]***.scm.azurewebsites.net/api/settings
Run Azure/functions-action@v1
##[Initialize]
##[ValidateParameter]
Finding files matching input: .
##[ValidateAzureResource]
Using RBAC for authentication, GitHub Action will perform resource validation.
[GET]https://management.azure.com/subscriptions/***/resources?$filter=resourceType EQ 'Microsoft.Web%2FSites' AND name EQ '***'&api-version=2016-07-01
[POST]https://management.azure.com/subscriptions/***/resourceGroups/***-rg/providers/Microsoft.Web/sites/***/config/publishingcredentials/list?api-version=2016-08-01
[GET]https://management.azure.com/subscriptions/***/resourceGroups/***-rg/providers/Microsoft.Web/sites/***/?api-version=2016-08-01
Sucessfully acquired site configs from function app!
Detected function app sku: Consumption
[POST]https://management.azure.com/subscriptions/***/resourceGroups/***-rg/providers/Microsoft.Web/sites/***/config/appsettings/list?api-version=2016-08-01
Sucessfully acquired app settings from function app (RBAC)!
Detected function app language: Node
[POST]https://management.azure.com/subscriptions/***/resourceGroups/***-rg/providers/Microsoft.Web/sites/***/publishxml?api-version=2016-08-01
Could not parse response: ***
Response: undefined
##[PreparePublishContent]
This is folder package 
Will archive . into D:\a\_temp\temp_web_package_***.zip as function app content
Archiving . to D:\a\_temp\temp_web_package_***.zip
Successfully created archive D:\a\_temp\temp_web_package_***.zip
Will use api/zipdeploy to deploy (rbac authentication)
[GET]***.scm.azurewebsites.net/api/settings
```

ここで出力されている `Initialize` や `Sucessfully acquired site configs from function app!` のようなログから、ソースコードを探索してみます。

Functions の GitHub Actions のソースコードはこちらです。
https://github.com/Azure/functions-action

### src/main.ts

エントリポイントとなる箇所は、以下の `src/main.ts` です。
https://github.com/Azure/functions-action/blob/master/src/main.ts

`actionManager` に対して、`Initializer` や `ParameterValidator` といった Handler を登録して各処理を順番に実行していることがわかります。名前からわかりやすいのは、`ParameterValidator` でパラメータの検証、`ContentPublisher` で実際のデプロイ、を行うものですね。

```ts
....
async function main(): Promise<void> {
    const actionManager = new Orchestrator();
    actionManager.register(StateConstant.Initialize, new Initializer());
    actionManager.register(StateConstant.ValidateParameter, new ParameterValidator());
    actionManager.register(StateConstant.ValidateAzureResource, new ResourceValidator());
    actionManager.register(StateConstant.PreparePublishContent, new ContentPreparer());
    actionManager.register(StateConstant.PublishContent, new ContentPublisher());
    actionManager.register(StateConstant.ValidatePublishedContent, new PublishValidator());
....
```

### src/handlers/***

では次に、各種 Handler を見ていきましょう。`src/handlers/***` の中にあります。
https://github.com/Azure/functions-action/tree/master/src/handlers

一番メインとなりそうな `ContentPublisher.ts` を見ていきます。
https://github.com/Azure/functions-action/blob/master/src/handlers/contentPublisher.ts

```ts
export class ContentPublisher implements IOrchestratable {

    public async invoke(state: StateConstant, _1: IActionParameters, context: IActionContext): Promise<StateConstant> {
        switch (context.publishMethod) {
            case PublishMethodConstant.ZipDeploy:
                await ZipDeploy.execute(state, context);
                break;
            case PublishMethodConstant.WebsiteRunFromPackageDeploy:
                await WebsiteRunFromPackageDeploy.execute(state, context);
                break;
            default:
                throw new ValidationError(state, "publisher", "can only performs ZipDeploy and WebsiteRunFromPackageDeploy");
        }
        return StateConstant.ValidatePublishedContent;
    }
}
```

`context.publishMethod` を見て、デプロイ方法を `ZipDeploy` か `WebsiteRunFromPackageDeploy` かを操作しています。
この値は事前に `src/handlers/contentPreparer.ts` で設定されています。
https://github.com/Azure/functions-action/blob/master/src/handlers/contentPreparer.ts

```ts
    private derivePublishMethod(
....
        // Uses api/zipdeploy endpoint if scm credential is provided
        if (authenticationType == AuthenticationType.Scm) {
            Logger.Log('Will use api/zipdeploy to deploy (scm credential)');
            return PublishMethodConstant.ZipDeploy;
        }

        // Linux Consumption sets WEBSITE_RUN_FROM_PACKAGE app settings when scm credential is not available
        if (osType === RuntimeStackConstant.Linux && sku === FunctionSkuConstant.Consumption) {
            Logger.Log('Will use WEBSITE_RUN_FROM_PACKAGE to deploy');
            return PublishMethodConstant.WebsiteRunFromPackageDeploy;
        }
....
```

### src/publishers/***

では最後に、実際のデプロイ部分である、publisher の部分を見ていきます。GitHub Actions の実行ログから、以下のように ZipDeploy がされていることがわかります。

```
Package deployment using ZIP Deploy initiated.
```

それでは、以下 publishers ディレクトリの中から、
https://github.com/Azure/functions-action/tree/master/src/publishers

zipDeploy を見ていきます。メインの箇所は以下のあたりです。
https://github.com/Azure/functions-action/blob/master/src/publishers/zipDeploy.ts

```ts
    public static async execute(state: StateConstant, context: IActionContext): Promise<string> {
...
        try {
            await this.patchApplicationSettings(context);
            await this.waitForSpinUp(state, context.appUrl);
            deploymentId = await context.kuduServiceUtil.deployUsingZipDeploy(filePath);
            isDeploymentSucceeded = true;
        }
...
```
スピンアップを待ったり、`context.kuduServiceUtil.deployUsingZipDeploy(filePath)` のように、デプロイは Kudu の機能を使っていることが確認できました。

# まとめ

さて GitHub Actions の仕組みをソースコードから追いかけてみました。
大きなフレームワークなどと違って、**あくまでパイプライン上の末端部分の仕組みなので、何が行われているか中身を見れば大体把握できる**感覚をつかんでいただけたら嬉しいです。
中身が把握できていることで、パイプラインがどうしても期待通りに動かない時などに、実行ログから実際に何が問題なのか切り分けするための大きな手助けになります。
そして、**どの GitHub Actions も構造は大きく変わりません。ご自身が必要とするパイプラインを自作してしまうことも可能です。** このあたりは私もぜひ挑戦してみたいと思います。

## 参考リンク
記事中のリンクをまとめて再掲します。

### GitHub Actions 

- [GitHub アクションでワークフローを自動化する](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions)
  - [アクションのビルド](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/building-actions)

### Azure Functions を GitHub Actions でデプロイする

正式な手順やドキュメントは以下に情報があります。

- [Docs: Continuous delivery by using GitHub Action](https://docs.microsoft.com/en-us/azure/azure-functions/functions-how-to-github-actions?tabs=javascript)
- https://github.com/Azure/actions
  - [Azure/functions-action](https://github.com/Azure/functions-action)
  - [Azure/functions-container-action](https://github.com/Azure/functions-container-action)