無料で Vue.js + express で作った API を Azure Static Web Apps でホストしてみよう

Build 2020 見てたら Azure App Service Static Web Apps が公開されました。現在は public preview ですが、Azure Functions と統合されていて API も統合されているのは便利そうだなぁと思いました。控え目にいって最高！

Cosmos DB も 5GB で 400RU(性能の指標です) まで無料なのでフロントから API とデータを保存するところまで全部無料でいけるようになったのは大きいですね。

## 今回やりたいこと

今回は、Azure App Service Static Web Apps (長いので Static Web Apps って書きます）に Vue.js のアプリをデプロイしつつ API は express を使って書いてみたいと思います。実は Azure Functions は、プレビュー機能なんですがカスタム ハンドラーという機能があって、これを使うと HTTP の API 作れる言語なら、なんでも Azure Functions に対応できるという機能があったりします。

[Azure Functions のカスタムハンドラー (プレビュー)](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-custom-handlers)

Azure Functions は JavaScript/TypeScript をサポートしてますが、JavaScript で Web API 作るときにデファクト（だと思ってます）の express ではないんですよね。Azure Functions で express を使うサードパーティのライブラリもありますが、今は知りませんが前使った時は完全互換というわけではないといった感じでした。
なので、express を使いたい場合は、このカスタムハンドラーを使った方が良さそうな感じです。

ということで、Static Web Apps と Azure Functions のカスタム ハンドラーを使って Vue.js + express を Azure にデプロイしてみようと思います。

## アプリの準備

とりあえずひな型でいいですね。適当なフォルダーで以下のコマンドをうって Vue.js のアプリを作ります。

```
> vue create client -n
```

TypeScript で、class じゃなくて Vue.extend を使うようにしました。

API 側も作ります。

```
> mkdir api
> cd api
> func init
```

node の JavaScript を選んで作成しました。
api フォルダーの host.json を開いて以下のように httpWorker の設定を追加します。

```json:host.json
{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  },
  "httpWorker": {
    "description": {
      "defaultExecutablePath": "node",
      "defaultWorkerPath": "dist/server.js"
    }
  }
}
```

express のコードを書いていきましょう。TypeScript の設定の生成と express を追加しておきます。

```
> tsc -init
> npm i express
> npm i @types/express
```

tsconfig.json は以下のようにしました。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",  
    "module": "commonjs",
    "strict": true, 
    "esModuleInterop": true,  
    "sourceRoot": "./src", 
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "sourceMap": true
  }
}
```

api フォルダーに src フォルダーを作って server.ts を作りましょう。
とりあえず、こんな感じで作っておきました。

```typescript:server.ts
import express from 'express';

const app = express();
app.use(express.json());
app.use(express.urlencoded());

const router = express.Router();
router.get('/getMessage', (req, res) => {
    res.json({ message: `Hello ${req.query.name} from express`});
});

app.use(router);

const PORT = process.env.FUNCTIONS_HTTPWORKER_PORT ?? 8080;
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));
```

tsc でコンパイルしておきます。最後に getMessage フォルダーを api フォルダーの下に作って function.json を作って getMessage に対して GET リクエストが来たときに反応するように定義しておきます。

一番簡単なのは `func function create` で Http Trigger の関数を getMessage という名前で作って getMessage フォルダーから index.js などのプログラムを消して function.json だけ残すことです。

function.json から余計なものを取り除きます。今回は get リクエストだけに対応するので、そういう風に構成しました。

```json:function.json
{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}
```

func host start をすると Azure Functions が起動します。以下のように getMessage を GET で呼び出せるって出てきました！
その下に dist/server.js が起動しているログが続いています。いいね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/eb2f815d-f569-d108-6ecf-be5bf694d2e9.png)

叩いてみると、ちゃんと動きますね！Azure Functions から express のアプリが呼び出されてます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/0729cb1d-c67b-54ad-fa24-40eb4c50f42e.png)

後は express でアプリ作っていって対応させたいエンドポイントごとに function.json 生やしていけばいい感じですね。今回は説明しませんが queue とかにも対応できるので興味がある人は[カスタムハンドラーのドキュメント](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-custom-handlers)見てみてね！

## フロントを作ろう

client/components/HelloWorld.vue を改造していこうと思います。
といっても、先ほどの getMessage を叩くだけのシンプルな感じですが。

```vue:HelloWorld.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>{{ message }}</p>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';

type ApiResponse = {
  message: string;
};

export default Vue.extend({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  data() {
    return {
      message: '',
    };
  },
  async created() {
    console.log(process.env);
    const res = await fetch('/api/getMessage?name=Vue.js');
    if (res.ok) {
      const responseMessage = await res.json() as ApiResponse;
      this.message = responseMessage.message;
    } else {
      this.message = 'failed';
    }
  },
});
</script>
```

最後にローカルで /api に対するアクセスを Azure Functions のほうに回す proxy の設定を client フォルダーに vue.config.js を作って追加します。

```javascript:vue.config.js
module.exports = {
    configureWebpack: {
        devServer: {
            proxy: {
                '/api': {
                    target: 'http://localhost:7071'
                }
            }
        }
    }
};
```

サーバー側のアプリを func host start で起動して、クライアントを npm run serve で起動して http://localhost:8081 にアクセスしてみましょう。うまくいってれば Vue.js の見慣れたアイコンと、Azure Functions で生成されたメッセージが以下のように表示されているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/f1307886-4d60-0dd4-908b-981a404a2671.png)

## デプロイしてみよう

では、こちらのコードを github に push しましょう。とりあえずここに上げました。

https://github.com/runceel/azurestaticwebapps_vue_test

Azure Portal で Static Web Apps を作ります。
リポジトリと紐づけて…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/786e47df-fc75-631c-6ca9-b0686e854962.png)

ビルドでフォルダーを指定して…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/990c29a3-d713-e1c2-2cb1-7bfedf10855c.png)

作成します。

GitHub Actions のほうの進捗を確認するとビルドエラー orz

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/f364f233-ca95-a1ae-6bb3-b978a65faed9.png)

アプリの成果物の場所は dist だけでよかったみたいです。ということで、GitHub Actions の app_artifact_location を dist に変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/19c66b32-6bc7-44e2-1eb9-5db7d8679e15.png)

次はデプロイが成功したのに、Azure Functions のほうがデプロイされていませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/7b0f5fbe-b2e7-3077-ef5a-c6aced3c67ec.png)

ビルドログを見てみると api に指定したフォルダーは単純に zip で固めてデプロイされてるような雰囲気を感じたので、今回のようなカスタムハンドラーかつ TypeScript を使っていると自前でビルドしないといけないように見えますね。
もしかしたら、`Azure/static-web-apps-deploy@v0.0.1-preview` にそういうのを書く設定があるのかもしれませんが、とりあえず Build And Deploy のジョブの前に Build Azure Functions という名前でジョブを作って api フォルダーの中身をコンパイルしておくようにしました。

ということで最終的に GitHub Actions のファイルは以下のようになりました。

```yaml
name: Azure Static Web Apps CI/CD

on:
  push:
    branches:
    - master
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
    - master

jobs:
  build_and_deploy_job:
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
    steps:
    - uses: actions/checkout@v1
    - name: Build Azure Functions
      run: |
        cd api
        npm install
        tsc
    - name: Build And Deploy
      id: builddeploy
      uses: Azure/static-web-apps-deploy@v0.0.1-preview
      with:
        azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BEACH_085F94700 }}
        repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
        action: 'upload'
        ###### Repository/Build Configurations - These values can be configured to match you app requirements. ######
        app_location: 'client' # App source code path
        api_location: 'api' # Api source code path - optional
        app_artifact_location: 'dist' # Built app content directory - optional
        ###### End of Repository/Build Configurations ######

  close_pull_request_job:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request Job
    steps:
    - name: Close Pull Request
      id: closepullrequest
      uses: Azure/static-web-apps-deploy@v0.0.1-preview
      with:
        azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_PURPLE_BEACH_085F94700 }}
        action: 'close'
```

このワークフローが成功したのを見届けて Static Web Apps の関数を見てみると getMessage 関数が追加されていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/60b8da9f-8313-f5af-4223-3f11014f25d1.png)

ということで、デプロイされた Vue.js を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/a5c88526-5bb6-cd78-a118-904e56b37f49.png)

ばっちりですね！

## まとめ

ということで Azure App Service Static Web Apps を試してみました。Azure Functions と統合されていて、CORS とか気にせずに API もまとめてデプロイ出来るのでいい感じですね。
今回は試してませんが認証機能もあったりして正式にリリースされるのが楽しみです。

正式にリリースされたら、プレミアプランとかスタンダートプランとか、そこらへんも選択できるようになるのかなぁ？？

因みに現時点の Free 版の制限事項については、以下のドキュメントにまとまってます。

https://docs.microsoft.com/ja-jp/azure/static-web-apps/quotas