Help us understand the problem. What is going on with this article?

無料で 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 のカスタムハンドラー (プレビュー)

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 の設定を追加します。

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 は以下のようにしました。

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

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

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 リクエストだけに対応するので、そういう風に構成しました。

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

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

image.png

後は express でアプリ作っていって対応させたいエンドポイントごとに function.json 生やしていけばいい感じですね。今回は説明しませんが queue とかにも対応できるので興味がある人はカスタムハンドラーのドキュメント見てみてね!

フロントを作ろう

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

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 を作って追加します。

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

デプロイしてみよう

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

https://github.com/runceel/azurestaticwebapps_vue_test

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

image.png

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

image.png

作成します。

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

image.png

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

image.png

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

image.png

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

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

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

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

image.png

ばっちりですね!

まとめ

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

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

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

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

okazuki
日本マイクロソフトでサポート系のエンジニアとして働いています。 好きな言語は C# と TypeScript。メインの興味領域は Windows クライアントアプリ開発と Xamarin によるモバイルアプリ開発。その延長として API を作るための Azure の PaaS 系サービスが好きです。 SPA はたしなむ程度に。 お約束ですが、ここでの発言は個人の見解になります。
https://blog.okazuki.jp
microsoft
マイクロソフトのメンバーが最新の技術情報をお届けします。Twitterアカウント(@msdevjp)やYouTubeチャンネル「クラウドデベロッパーちゃんねる」も運用中です。
https://aka.ms/MSFT-Docs-JPN
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした