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 のプロジェクト作成
まずはフロントエンドの開発環境を作ります。
以下のコマンドを実行すると
npm create vite@latest
色々聞いてくるので答えます。
今回は以下のようにしました。
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 行コマンドが表示されているので実行すると、簡単にアプリが起動します。
 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/ にアクセスするとページが表示されます。

関数アプリの作成
Azure Static Web Apps ではマネージド関数という、組み込みの関数アプリを作成することが可能です。
マネージド関数で API を作成することにより、API のエンドポイントがフロントエンドと同じオリジンとなり、CORS 等の制限を意識することなく API を利用することができます。
必要なツールのインストール
関数アプリを作成するために Azure Functions Core Tools をインストールします。
※グローバルにインストールするか、関数用のディレクトリを作成して、それよりも上の階層でインストールする必要があります(理由は後述)
npm install --global azure-functions-core-tools
関数アプリのプロジェクト作成
以下のコマンドを実行して関数アプリを作成します。
# プロジェクトルートで実行
mkdir api      # 関数アプリのルートディレクトリ
cd api
npx func new
色々聞かれるので、以下のように設定。
- runtime: node
- language: typescript
- template: HTTP trigger
- Function name: swafunc
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 を 削除します。
{
  ...
  "devDependencies": {
    "@azure/functions": "^3.0.0",
-    "azure-functions-core-tools": "^4.x",
    "@types/node": "16.x",
    "typescript": "^4.0.0"
  }
}
デプロイ時にサイズ超過エラーとなってしまう(関数があるディレクトリの node_modules までアップロードしようとする)ため、別階層またはグローバルにインストールしたものを使用するようにします。
以下のコマンドで起動します。
# apiディレクトリで実行
npm install
npm run start
関数アプリの起動確認
ブラウザで http://localhost:7071/api/swafunc にアクセスすると、API のレスポンスが返ってきます。

フロントエンドから API の呼び出し
フロントエンド側から API を呼び出すように修正します。
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
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 をインストールします。
# プロジェクトルートで実行
npm install --save-dev @azure/static-web-apps-cli
設定ファイルの生成
以下のコマンドを実行して、Static Web Apps(以降「SWA」と表記します)CLI の設定ファイルを生成します。
# プロジェクトルートで実行
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)にある、フロントエンドのビルドファイル出力先が実態と違うので、合わせておきます。
{
  "$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 を追加しておきます。
{
  "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...\""
  },
  ...
}
{
  "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 つをプロキシして同一オリジンで起動しているように扱うことができるようにしています。
swa start <フロントエンドのURL> --api-location <APIのURL>
修正したら以下のコマンドを実行して起動します。
# プロジェクトルートで実行
npm install --save-dev npm-run-all  # 追加したscriptで使用するツールをインストール
npm run start-all
コンソールに以下の内容が出力されていれば起動成功です。
[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 のローカル開発環境が整いました。

Azure へのデプロイ
デプロイの準備が整ったので、Azure 環境へデプロイしていきます。
SWA の構成ファイルをフロントエンドプロジェクトのルートに作成します。
{
  "$schema": "https://json.schemastore.org/staticwebapp.config.json",
  "platform": {
    "apiRuntime": "node:18"
  }
}
次に Azure にサインインします。
今回は対話形式でサインインします。
npx swa login --no-use-keychain
ブラウザで Azure へのログインを促されるので、SWA をデプロイするアカウントでサインインします。

サインインすると以下のメッセージが表示されれば OK です。ページは閉じて良いです。

複数のディレクトリに所属していると、コンソールでテナントの選択を求められるので、使用するテナントを選択します。
最終的に以下のようなメッセージが出力されていれば OK です。
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!
最後にデプロイです。
# プロジェクトルートで実行
npx swa build
npx swa deploy --env production --no-use-keychain
--env オプションを指定しないと、デフォルトではプレビュー環境にデプロイされます。
プレビュー環境については以下を参照。
コマンド実行後、再度ブラウザにサインイン画面が出てくるので、サインインします。
コンソールに戻って、Create a new application を選択、アプリ名を入力します。
以下の様なメッセージが出力されていればデプロイ成功です。
? 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 上でもリソースが作成されていることを確認できます。

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