Azure Static Web Apps をローカルソースから直接デプロイする

Azure Static Web Apps は通常、Github か Azure DevOps 上のリポジトリからデプロイをすると思いますが、Azure Static Web Apps CLI というコマンドラインツールを使ってローカルのソースを直接デプロイすることもできます。  
更に、ローカルで Azure 環境をエミューレートすることもできるので、開発効率上がります。
リポジトリに上げるまでもないけど、ちょっと Azure 上で試してみたいことがある場合などに有効な方法かなと思います。
今回はこのコマンドラインツールでのデプロイを試していきたいと思います。

# 前提条件

- npm がインストール済みであること

# 作業の流れ

以下の流れで進めていきます。
- フロントエンドの開発環境構築
- バックエンド（API）の開発環境構築
- ローカルでAzure Static Web Apps をエミュレートするための準備
- Azure へデプロイ

# Azure Static Web Apps のローカル環境構築

まずはローカルの開発環境を整えていきます。  
今回は以下の構成で進めていきます。

- フロントエンド
  - Vite + React
  - Typescript
- バックエンド（API）
  - Azure Functions（Azure Static Web Apps のマネージド関数）
  - Node
  - Typescript
- CI/CD ツール
  - Azure Static Web Apps CLI（ローカルソースを直接デプロイ）

## Vite + React のプロジェクト作成

まずはフロントエンドの開発環境を作ります。  
以下のコマンドを実行すると

```bash
npm create vite@latest
```

色々聞いてくるので答えます。  
今回は以下のようにしました。

```bash
Need to install the following packages:
  create-vite@4.0.0
Ok to proceed? (y) y
✔ Project name: … swa-app                       # アプリケーション名
✔ Select a framework: › React                   # フレームワーク
✔ Select a variant: › TypeScript                # 言語

Scaffolding project in /path/to/app/swa-app...

Done. Now run:

  cd swa-app                                     # 起動コマンド
  npm install
  npm run dev
```

一番最後に 3 行コマンドが表示されているので実行すると、簡単にアプリが起動します。

```bash
 VITE v4.0.3  ready in 281 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
```

### フロントエンドの起動確認

`http://localhost:5173/` にアクセスするとページが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/3e3ec97b-5437-865b-117e-22d5dd7d9a0e.png)

## 関数アプリの作成

Azure Static Web Apps ではマネージド関数という、組み込みの関数アプリを作成することが可能です。  
マネージド関数で API を作成することにより、API のエンドポイントがフロントエンドと同じオリジンとなり、CORS 等の制限を意識することなく API を利用することができます。  

https://learn.microsoft.com/ja-jp/azure/static-web-apps/apis-functions

### 必要なツールのインストール

関数アプリを作成するために `Azure Functions Core Tools` をインストールします。  
**※グローバルにインストールするか、関数用のディレクトリを作成して、それよりも上の階層でインストールする必要があります(理由は後述)**

```bash
npm install --global azure-functions-core-tools
```

### 関数アプリのプロジェクト作成

以下のコマンドを実行して関数アプリを作成します。

```bash
# プロジェクトルートで実行
mkdir api      # 関数アプリのルートディレクトリ
cd api
npx func new
```

色々聞かれるので、以下のように設定。

- runtime： node
- language： typescript
- template： HTTP trigger
- Function name： swafunc

```bash
Select a number for worker runtime:
1. dotnet
2. dotnet (isolated process)
3. node
4. python
5. powershell
6. custom
Choose option: 3               # node
node
Select a number for language:
1. javascript
2. typescript
Choose option: 2               # typescript
typescript
Writing .funcignore
Writing package.json
Writing tsconfig.json
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /path/to/app/swa-app/api/.vscode/extensions.json
Select a number for template:
・・・
10. HTTP trigger
・・・
Choose option: 10              # HTTP trigger
HTTP trigger
Function name: [HttpTrigger] swafunc
Writing /path/to/app/swa-app/swafunc/index.ts
Writing /path/to/app/swa-app/swafunc/function.json
The function "swafunc" was created successfully from the "HTTP trigger" template.
```

package.json から Azure Functions Core Tools を 削除します。

```diff_json:api/package.json
{
  ...
  "devDependencies": {
    "@azure/functions": "^3.0.0",
-    "azure-functions-core-tools": "^4.x",
    "@types/node": "16.x",
    "typescript": "^4.0.0"
  }
}
```

:::note warn
デプロイ時にサイズ超過エラーとなってしまう(関数があるディレクトリの node_modules までアップロードしようとする)ため、別階層またはグローバルにインストールしたものを使用するようにします。
:::

以下のコマンドで起動します。

```bash
# apiディレクトリで実行
npm install
npm run start
```

### 関数アプリの起動確認

ブラウザで `http://localhost:7071/api/swafunc` にアクセスすると、API のレスポンスが返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/25d8d641-b874-5f46-3838-201522c03ce8.png)

## フロントエンドから API の呼び出し

フロントエンド側から API を呼び出すように修正します。

```diff_tsx:src/App.tsx
import { useEffect, useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)
+  const [data, setData] = useState<Record<string, any> | null>(null)

+  const getData = useCallback(async () => {
+    const response = await fetch('/api/swafunc')
+    const data = await response.json()
+    setData(data)
+  }, [setData])

+  useEffect(() => {
+    getData()
+  }, [])

  return (
    <div className='App'>
      <div>
        <a href='https://vitejs.dev' target='_blank'>
          <img src='/vite.svg' className='logo' alt='Vite logo' />
        </a>
        <a href='https://reactjs.org' target='_blank'>
          <img src={reactLogo} className='logo react' alt='React logo' />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className='card'>
        <button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className='read-the-docs'>Click on the Vite and React logos to learn more</p>
+      {data === null ? null : (
+        <div>
+          <p>{JSON.stringify(data)}</p>
+        </div>
+      )}
    </div>
  )
}

export default App
```

```diff_typescript:api/swafunc/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    context.log('HTTP trigger function processed a request.');
    const name = (req.query.name || (req.body && req.body.name));
    const responseMessage = name
        ? "Hello, " + name + ". This HTTP triggered function executed successfully."
        : "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.";

    context.res = {
        // status: 200, /* Defaults to 200 */
-        body: responseMessage
+        // JSON形式で返すようにする
+        body: {
+          responseMessage,
+        },
    };

};

export default httpTrigger;
```

## Azure Static Web Apps のローカル構成

フロントエンドと API の準備ができたので、Azure 上にデプロイした時と同じようにローカル環境で実行するための構成を行います。

### 必要なツールのインストール

Static Web Apps プロジェクトの初期化、デプロイを行うために `Azure Static Web Apps CLI` をインストールします。

```bash
# プロジェクトルートで実行
npm install --save-dev @azure/static-web-apps-cli
```

https://github.com/Azure/static-web-apps-cli

### 設定ファイルの生成

以下のコマンドを実行して、Static Web Apps（**以降「SWA」と表記します**）CLI の設定ファイルを生成します。

```bash
# プロジェクトルートで実行
npx swa init    # コマンド

Welcome to Azure Static Web Apps CLI (1.0.4)

✔ Choose a configuration name: … swa-app           # デフォルトのまま

Detected configuration for your app:
- Framework(s): React, with API: Node.js, TypeScript
- App location: .
- Output location: build
- API location: api
- App build command: npm run build
- API build command: npm run build --if-present
- App dev server command: npm start
- App dev server URL: http://localhost:3000

✔ Are these settings correct? … yes                # yes

Configuration successfully saved to swa-cli.config.json.    # swa-cli.config.jsonが作成される

Get started with the following commands:
- Use swa start to run your app locally.
- Use swa build to build your app.
- Use swa deploy to deploy your app to Azure.
```

生成された設定ファイル(swa-cli.config.json)にある、フロントエンドのビルドファイル出力先が実態と違うので、合わせておきます。

```diff_json:swa-cli.config.json
{
  "$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
  "configurations": {
    "swa-app": {
      "appLocation": ".",
      "apiLocation": "api",
-      "outputLocation": "build",
+      "outputLocation": "dist",
      "appBuildCommand": "npm run build",
      "apiBuildCommand": "npm run build --if-present",
      "run": "npm start",
      "appDevserverUrl": "http://localhost:3000"
    }
  }
}
```

### アプリ起動スクリプト設定

フロントエンド + API をまとめて SWA として起動（＆ファイル更新時のリロード）できるようにするため、package.json に script を追加しておきます。

```diff_json:api/package.json
{
  "name": "",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "prestart": "npm run build",
    "start": "func start",
+    "start:watch": "run-p watch start",
    "test": "echo \"No tests yet...\""
  },
  ...
}
```

```diff_json:package.json
{
  "name": "swa-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
+    "start-all": "run-p start:*",
+    "start:api": "cd api && npm run start:watch",
+    "start:client": "npm run dev",
+    "start:swa": "swa start http://localhost:5173 --api-location http://localhost:7071 -t 180"
  },
  ...
}
```

フロントエンドと API は別々のポートで立ち上がるので、以下のコマンドで 2 つをプロキシして同一オリジンで起動しているように扱うことができるようにしています。

```bash
swa start <フロントエンドのURL> --api-location <APIのURL>
```

修正したら以下のコマンドを実行して起動します。

```bash
# プロジェクトルートで実行
npm install --save-dev npm-run-all  # 追加したscriptで使用するツールをインストール
npm run start-all
```

コンソールに以下の内容が出力されていれば起動成功です。

```bash
[swa] Using dev server for static content:
[swa]   http://localhost:5173
[swa]
[swa] Using dev server for API:
[swa]   http://localhost:7071
[swa]
[swa] Azure Static Web Apps emulator started at http://localhost:4280. Press CTRL+C to exit.
```

### SWA の起動確認

ブラウザで `http://localhost:4280` にアクセスすると、ページに API から取得したレスポンスが表示されるはずです。  
これで SWA のローカル開発環境が整いました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/4f21c4cb-a6ac-f9e5-cdf3-d59f0c122e6d.png)

## Azure へのデプロイ

デプロイの準備が整ったので、Azure 環境へデプロイしていきます。

SWA の構成ファイルをフロントエンドプロジェクトのルートに作成します。

```json:staticwebapp.config.json
{
  "$schema": "https://json.schemastore.org/staticwebapp.config.json",
  "platform": {
    "apiRuntime": "node:18"
  }
}
```

次に Azure にサインインします。  
今回は対話形式でサインインします。

```bash
npx swa login --no-use-keychain
```

ブラウザで Azure へのログインを促されるので、SWA をデプロイするアカウントでサインインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/93bea181-a8fa-4d61-ca68-c5e56272131c.png)

サインインすると以下のメッセージが表示されれば OK です。ページは閉じて良いです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/5181e052-c359-088b-c858-1a263d340b7e.png)

複数のディレクトリに所属していると、コンソールでテナントの選択を求められるので、使用するテナントを選択します。  
最終的に以下のようなメッセージが出力されていれば OK です。

```bash
Welcome to Azure Static Web Apps CLI (1.0.4)

Using configuration "swa-app" from file:
  /path/to/app/swa-app/swa-cli.config.json

Checking Azure session...
✔ Successfully logged into Azure!
✔ Choose your tenant › <your tenant id>
✔ Successfully logged into Azure tenant: <your tenant id>
✔ Saved project credentials in .env file.
✔ Successfully setup project!
```

最後にデプロイです。

```bash
# プロジェクトルートで実行
npx swa build
npx swa deploy --env production --no-use-keychain
```

`--env` オプションを指定しないと、デフォルトではプレビュー環境にデプロイされます。  
プレビュー環境については以下を参照。

https://learn.microsoft.com/ja-jp/azure/static-web-apps/preview-environments

コマンド実行後、再度ブラウザにサインイン画面が出てくるので、サインインします。  
コンソールに戻って、`Create a new application` を選択、アプリ名を入力します。  
以下の様なメッセージが出力されていればデプロイ成功です。

```bash
? Choose your Static Web App › - Use arrow-keys. Return to submit.
✔ Choose your Static Web App › >> Create a new application        # 新規作成
✔ Choose a project name: … swa-app                                # アプリ名
✔ Project created successfully!
✔ Successfully setup project!

Deploying to environment: production


Found configuration file:
  /path/to/app/swa-app/staticwebapp.config.json

Deploying project to Azure Static Web Apps...
✔ Project deployed to https://<your app host>.2.azurestaticapps.net 🚀       # デプロイ先URL
```

ブラウザでデプロイ先 URL にアクセスすると、デプロイしたアプリページが表示されるはずです。

Azure Portal 上でもリソースが作成されていることを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/151bb7de-7119-3dc8-df4b-3ad50bb0bfe5.png)

# おわりに

リモートリポジトリを作らずに Azure へのデプロイが簡単にできました。  
ローカルでフロント＋ API を一緒に起動して Azure 環境をエミュレートできるのも便利ですね。  
ちょっとした検証がしたいときなどに使っていこうと思います。