挨拶
ども!6月は結構激しめにブログを投稿していたんですが、7月もがっつり忙しくなりそうで、ウハウハの龍ちゃんです。ブログサムネイル評価用プロンプトを結構な時間をかけて作りました。今までのように人にSlack投げて確認ではなく、生成AIにチャットを投げるだけで確認出来ており、サムネイルにも力が入っています。
さて!今回は「Azure Static Web Apps+Bicepの発展編:Azure Static Web Apps+Managed Functions+GitHub(カスタム認証)の環境をGitHub Actions after Bicepでデプロイ」というてんこ盛りの内容になっています。こちらを理解できれば、Azure Static Web Appsの基礎は出来上がったように感じます。
なかなかの長編になったので、初めての方は5日ほどかけてじっくりと、中級者以上の方はミスを指摘してやるってテンション感でご覧ください。
はじめに・概要
この記事で学べること
今回は、環境構築からデプロイまで横断的に解説していきます。以下にまとめます。
環境構築:DevContainer
- ローカル環境でAzure Static Web AppsとAzure Functionsを開発するために必要な環境
- ローカル環境からBicepでAzureリソースを管理するのに必要な環境
Azure Functions開発(Managed Functions Node with TypeScript)
- APIの簡易的な仕様書から、TypeScriptによる実際のコーディング
Azure Static Web Apps開発(Next.js)
- GitHub OAuthを活用した認証基盤
- Managed Functionsと連携して認証状態を管理する方法
Bicep:ローカル環境からAzureリソースのコマンドデプロイ
- デプロイ環境をコード一発でデプロイ・クリーンナップできるようなbashファイル管理
GitHub Actions
- Azure Static Web AppsとAzure Functions(Managed Functions)をGitHub Actionsからデプロイする
設計から、Azure Static Web AppsとAPI統合、認証機能の実装方法。DevContainerからBicep、GitHub Actionsまでの一連の開発フローとして学習することができます。
以降はAzure Static Web AppsはSWAと省略した形で解説していきます。
技術スタック
使用している技術などは以下にまとめました。
カテゴリ | 技術・ツール | バージョン | 備考 |
---|---|---|---|
デプロイ環境 | Azure Static Web Apps | – | Standardプラン:カスタム認証(GitHub) |
Azure Functions | – | Managed Functions | |
開発環境 | DevContainer | – | – |
Azure CLI | 2.74.0 | – | |
SWA CLI | 2.0.6 | – | |
GitHub CLI | 2.74.2 | – | |
Function Core Tools | 4.0.7317 | – | |
フロントエンド | Next.js | 15.3.4 | 静的エクスポート |
React | 19 | – | |
バックエンド | Node.js + TypeScript | – | Azure Functions v4 Programming Model |
今回は、データベースなどは抜きにしてSWA内ですべてが完結しているように構成しています。1点注意点としては、カスタム認証を使用する関係上、SWAのプラン設定をStandardにする必要があります。無料枠でも十分強力ですが、認証プロバイダーの追加設定などはStandardプランからでないと使用することができません。認証機能が必要ない方は、Maneged Functionsも無料枠があるのでBicepテンプレートをカスタマイズしてご利用ください。
Azure Static Web Appsには、個人のプロジェクトや小規模なウェブサイトに適したFreeプランと、運用環境のアプリケーションや大規模なプロジェクトに適したStandardプランがあります。それぞれのプランで利用できる機能の範囲が異なり、特にManaged Functionsの利用方法に影響があります。
プラン別機能比較
機能 | Freeプラン<br/>(個人/小規模プロジェクト向け) | Standardプラン<br/>(運用/大規模プロジェクト向け) |
---|---|---|
ウェブホスティング | ✔ | ✔ |
GitHub/Azure DevOps統合 | ✔ | ✔ |
グローバル分散 | ✔ (静的コンテンツ) | ✔ (静的コンテンツ) |
SSL証明書 | ✔ (無料、自動更新) | ✔ (無料、自動更新) |
ステージング環境 | アプリあたり3つまで | アプリあたり10個まで |
アプリの最大サイズ | アプリあたり250MB | アプリあたり500MB |
カスタムドメイン | アプリあたり2つまで | アプリあたり5つまで |
API (Azure Functions) | マネージド (SWAに統合) | マネージド または 独自のFunctionsアプリ利用 |
認証プロバイダー | 事前構成済み (サービス定義済み) | カスタム登録も可能 |
関数によるカスタムロール | ❌ | ✔ |
プライベートエンドポイント | ❌ | ✔ |
SLA (サービスレベル契約) | なし | 99.95% |
帯域幅 | サブスクリプションあたり月100GBまで (無料) | サブスクリプションあたり月100GBまで (超過分課金) |
カスタマーサポート | なし | ✔ |
Managed Functionsが受ける影響
Azure Static Web AppsにおけるAPIバックエンドとしてのManaged Functionsは、選択するプランによって利用方法が大きく異なります。
Freeプランの場合
- Managed Functionsのみが利用可能です。これはAzure Static Web Appsに組み込まれたFunctionsアプリであり、ユーザーが個別にAzure Functionsリソースを作成・管理する必要がありません
- APIの実行回数には、月間100万回の無料実行が含まれます
- コールドスタートが発生しやすく、API呼び出しの最初の応答に時間がかかる場合があります
Standardプランの場合
- Managed Functionsの利用に加えて、既存の独自のAzure FunctionsアプリをStatic Web AppsのAPIバックエンドとして持ち込むことが可能です
- 独自のFunctionsアプリを利用することで、HTTP以外のトリガー(例: タイマートリガー、キュートリガー)や、より複雑なバインディングを利用できるようになります
- 既存のFunctionsアプリをStatic Web Appsに統合したい場合に特に便利です
どちらのプランを選ぶべきか?
以下の3つの観点から、Standardプランを検討してください:
1. アーキテクチャ戦略(マイクロサービス化)
- フロントエンドとバックエンドを分離したマイクロサービス構成にしたい場合
- 既存のAzure Functionsアプリを再利用したい、または独立して管理したい場合
- HTTPエンドポイント以外のトリガー(タイマー、キュー等)が必要な場合
2. プロジェクトの成長・運用要件
- 予想されるトラフィック量が多い、または帯域幅の無料枠を超える可能性がある場合
- ミッションクリティカルなアプリケーションで99.95%のSLAが必要な場合
- 3つ以上のステージング環境(開発、検証、本番等)が必要な場合
3. 認証・セキュリティ要件
- カスタム認証プロバイダーの登録が必要な場合(GitHub、Google以外の独自認証等)
- 関数によるカスタムロール制御が必要な場合
- プライベートエンドポイントでのセキュアな通信が必要な場合
まとめ
基本的には、個人的なプロジェクトやシンプルなウェブサイトであればFreeプランで十分ですが、ビジネス用途や大規模なアプリケーションにはStandardプランの検討をお勧めします。
今回の記事では、カスタム認証機能を使用するためStandardプランを選択していますが、認証が不要な場合はFreeプランでも同様の構成を構築できます!というか、課金が毎秒走っていると考えると怖いので、極力Freeプランを使うようにしています。
最終的な成果物
今回解説するアプリの動画です。ページとAPIをそれぞれ2つずつ作成し、認証あり・なしのパターンを網羅できるよう設計しています。
前提条件・準備
前提条件は以下の通りです。
- Azureで有効なサブスクリプションを持っている
- GitHub.comに登録済み
- Dockerを使用できる環境
今回は、Azure Static Web AppsのStandardプランを使用するため、有効なサブスクリプションが必要です。また、GitHub Actionsを使用してデプロイを行い、認証プロバイダーにGitHub OAuthを使用するため、GitHub.comのアカウントは必須となります。
Dockerに関しては、今回の技術スタックはNode.jsに統一しているため、ローカル環境に有効なNode.js環境があれば開発を進めることができます。ただし、Dockerが入っていればコピー&ペーストで環境が立ち上がるため、こちらを推奨します。
プロジェクト設計・アーキテクチャ
システム構成図

システム構成に関しては、上記のようになっています。作成するAzureリソースとしては、SWAのみとなっており、カスタム認証プロバイダーとしてGitHub OAuthを使用します。デプロイに関しては、Azureリソースに関してはBicep、アプリケーションのデプロイに関してはGitHub Actionsでそれぞれ管理します。
フロントエンドに関しては、Next.jsの静的エクスポートを使用してデプロイを行います。
ディレクトリ構造
.
├── .github # GitHub Actions用
├── api # API用ディレクトリ
├── frontend # フロント用ディレクトリ Next.js静的エクスポート
├── infra # Infrastructure as Code用ディレクトリ
└── swa-cli.config.json # SWA CLIのローカル起動用設定ファイル
全体のディレクトリ構成としては上記のようになります。それぞれ、他のリソースを参照せずに個別のディレクトリでそれぞれ開発をすることができます。
フロントエンド設計
ページ構成と各ページの目的
それでは、今回のアプリケーションのページ構成について見ていきましょう。まず、作成するルート一覧を明示しておきますね。
作成ルート一覧
/
– トップページ(パブリック)/protected
– 保護ページ(認証必須)
シンプルな構成ですが、それぞれに明確な役割があります。
トップページ(/
)
トップページは、いわば「認証の玄関口」として設計しています。ここでの主な目的は以下の通りです:
- 認証状態の可視化: ユーザーが現在ログインしているかどうかを一目で分かるようにする
- 認証フローの起点: 未認証ユーザーにとってのログイン入口
- ユーザー情報の表示: 認証済みユーザーには基本的なプロフィール情報を提示
- ナビゲーションハブ:
/protected
へのリンク
実装としては、認証状態に応じて動的にUIを切り替える設計になっています。未認証時は「GitHubでログイン」ボタンを表示し、認証済み時はユーザー名やアバター、そして保護ページへのリンクを表示します。これにより、ユーザーは自分の状態を即座に把握できるというわけですね。
保護ページ(/protected
)
保護ページは、Azure SWAの認証機能の真価を発揮する場所です。ここでの設計ポイントは:
- Azure SWAによる保護: フロントエンドでの認証チェックに依存しない
- 認証済みユーザー専用コンテンツ: より詳細なユーザー情報や機能の提供
- セキュリティ情報の表示: 認証システムの仕組みを理解してもらう
この設計の面白いところは、ソースコード内で「認証チェック」を行わないことです。Azure SWAを使う場合は、サーバーレベルで保護されているため、そもそも未認証ユーザーはこのページにたどり着けません。
各ページのデザインは今回主題ではないので、生成AIを活用してデザインを生成してもらいます。
ルート保護設計とアクセスフロー
Azure SWAの最大の魅力の一つが、宣言的なルート保護による強力なセキュリティ機能です。設定一つで堅牢な認証システムを実現できるというのは、本当に画期的ですよね。
アクセスフロー全体像

今回の設計で重要となる点としては、401エラー(未認証)が発生した瞬間に、ユーザーを/.auth/login/github
に自動リダイレクトする仕組みです。これにより、ユーザーは「アクセス拒否」のような不親切な画面を見ることなく、スムーズに認証フローに誘導されます。
認証完了後は、元々アクセスしようとしていたページに戻ってくるのも、Azure SWAが自動的に処理してくれます。フロントエンド側で複雑なリダイレクト制御を書く必要がないというのは、大きなメリットです。
API設計
エンドポイント仕様の全体像
今回のシステムでは、以下の2つの主要エンドポイントを設計しました。
/api/user-info
: ユーザー情報取得(認証状態問わず)/api/protected-data
: 保護されたデータ取得(認証必須)
/api/user-info
では、フロントエンドから認証状態と未認証状態でのアクセスを確認するために、認証状態を問わずアクセスできるエンドポイントとして設計しています。
/api/protected-data
では、認証されていない場合はエラーを返答し、ダミーデータを返すエンドポイントとして設計しています。
user-infoエンドポイントの設計
user-infoエンドポイントは、現在のユーザーの認証状態と基本情報を返すAPIです。未認証時でもエラーにならずにリクエストが成功するように設計します。
リクエスト仕様
- Method: GET
- Path:
/api/user-info
- Authentication: 不要
- Parameters: なし
レスポンス型定義
interface UserInfo {
userId: string | null;
name: string | null;
email: string | null;
provider: string | null;
roles: string[];
isAuthenticated: boolean;
}
成功レスポンス例(認証済み)
{
"userId": "github|12345678",
"name": "yamada.taro",
"email": "yamada.taro@example.com",
"provider": "github",
"roles": ["authenticated"],
"isAuthenticated": true
}
成功レスポンス例(未認証)
{
"userId": null,
"name": null,
"email": null,
"provider": null,
"roles": [],
"isAuthenticated": false
}
ここで重要なのは、認証されていない場合もエラーではなく正常なレスポンスを返すことです。isAuthenticated: false
として状態を明示することで、フロントエンド側で適切な処理分岐ができます。
protected-dataエンドポイントの設計
protected-dataエンドポイントは、認証が必須のAPIです。認証されていないユーザーには401エラーを返し、アクセスを拒否します。
リクエスト仕様
- Method: GET
- Path:
/api/protected-data
- Authentication: 必須
- Parameters: なし
レスポンス型定義
interface ProtectedData {
userId: string;
message: string;
timestamp: string;
userNumber: number;
}
成功レスポンス例
{
"userId": "github|12345678",
"message": "こんにちは、ユーザーgithub|12345678さん!",
"timestamp": "2024-12-15T15:45:30.000Z",
"userNumber": 456
}
エラーレスポンスの統一設計
APIの一貫性を保つため、エラーレスポンス形式を統一しています。すべてのエラーレスポンスは以下の構造に従います。
エラーレスポンス型定義
interface ErrorResponse {
error: string;
message: string;
}
認証エラー(401 Unauthorized)
{
"error": "Unauthorized",
"message": "保護されたデータにアクセスするには認証が必要です"
}
サーバーエラー(500 Internal Server Error)
{
"error": "Internal server error",
"message": "Application Crash"
}
この統一により、フロントエンド側でのエラーハンドリングが大幅に簡素化されます。どのAPIでも同じ構造でエラー情報を取得できるためです。
認証チェック方法の設計
Azure SWAでは、認証済みユーザーの情報がx-ms-client-principal
ヘッダーにBase64エンコードされたJSON形式で渡されます。これは、Azure SWAの大きな特徴の一つですね。
ClientPrincipal型定義
interface ClientPrincipal {
identityProvider: string;
userId: string;
userDetails: string;
userRoles: string[];
}
認証チェックの流れは以下のようになります:
- リクエストヘッダーから
x-ms-client-principal
を取得 - Base64デコードしてJSON解析
- 成功時は認証済みとして処理、失敗時は未認証として処理
このヘッダーはAzure SWAのリバースプロキシによって自動的に付与されるため、偽装される心配はありません。
開発環境構築(DevContainer)
この章では、DevContainer環境を構築していきます。作成するディレクトリ構成は以下になります。
.
└── .devcontainer
└── devcontainer.json
DevContainer設定 devcontainer.json
今回の開発に必要なツールを全て含んだDevContainer設定を作成します。Node.js、Azure CLI、Functions Core Tools、SWA CLIを含む開発環境を構築していきましょう。
{
"name": "Azure SWA include API & Bicep Development",
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye",
"features": {
"ghcr.io/devcontainers/features/azure-cli:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
// npm@11.4.2に関してはnoticeが出ていたので対応
"postCreateCommand": "npm install -g npm@11.4.2 @azure/static-web-apps-cli azure-functions-core-tools@4",
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-bicep",
"GitHub.vscode-github-actions",
"ms-vscode.azure-account"
]
}
},
"forwardPorts": [
3000,
7071,
4280
],
"mounts": [
"source=${localEnv:HOME}/.azure,target=/home/node/.azure,type=bind,consistency=cached"
]
}
ベースイメージとインストール
ベースイメージとしては、Microsoftが提供しているNode環境を使用しています。Node.js 20系の安定版が含まれており、今回のプロジェクトに必要な環境が整っています。
features
として、Azure CLIとGitHub CLIを導入しています。これにより、Azureリソースの管理とGitHubとの連携が可能になります。
postCreateCommand
で、npmパッケージとしてFunctions Core Tools@v4とSWA CLIをグローバルインストールしています。npmのバージョンアップも行っており、これは最新の機能とセキュリティ修正を適用するためです。
VS Code拡張機能
開発効率を上げるため、以下の拡張機能を自動インストールします:
- Bicep: Azureリソース定義のシンタックスハイライトと補完
- GitHub Actions: ワークフロー編集の支援
- Azure Account: Azure認証とリソース管理
ポートフォワーディング
開発中に使用する主要ポートを事前に設定しています:
- 3000: Next.jsフロントエンド
- 7071: Azure Functions API
- 4280: SWA CLI統合サーバー
マウント設定
"mounts": [
"source=${localEnv:HOME}/.azure,target=/home/node/.azure,type=bind,consistency=cached"
]
ホスト側にある.azure
ディレクトリをマウントしています。これにより、ホスト側でaz login
をしていればDevContainer環境内でも認証済みとして扱うことができます。
セキュリティに関する注意
この設定は、Azure認証情報をコンテナ内で共有するため、セキュリティリスクがあります。使用する場合は十分に注意し、本番環境や共有環境では適切な認証方法を検討してください。
開発環境の起動確認
環境を立ち上げた後は、各CLIが正常にインストールされているか確認しましょう。
Azure CLI確認
az --version
# azure-cli 2.74.0
# core 2.74.0
# telemetry 1.1.0
#
# Dependencies:
# msal 1.32.3
# azure-mgmt-resource 23.3.0
#
# Python location '/opt/az/bin/python3'
# Config directory '/home/node/.azure'
# Extensions directory '/home/node/.azure/cliextensions'
#
# Python (Linux) 3.12.10 (main, May 27 2025, 09:13:17) [GCC 10.2.1 20210110]
#
# Legal docs and information: aka.ms/AzureCliLegal
#
#
# Your CLI is up-to-date.
併せてマウント設定が正しく動作しているか。認証状態の確認をしておきましょう。
az account show
ホスト側で認証済みの場合、アカウント情報が表示されます。認証されていない場合は、以下のコマンドで認証を行ってください。
az login
GitHub CLI確認
gh --version
# gh version 2.74.2 (2025-06-18)
# https://github.com/cli/cli/releases/tag/v2.74.2
SWA CLI確認
swa --version
# 2.0.6
Functions Core Tools確認
func --version
# 4.0.7317
それぞれのバージョン情報が表示されれば成功です。これで、Azure Static Web Appsの開発に必要な全てのツールが使用可能になりました!
これで、DevContainer環境での開発準備が完了しました。次章からは、実際にAzure FunctionsのAPI実装に入っていきます。
Azure Functions API実装
この章では、Azure Functions環境を構築・実装をしていきます。作成するディレクトリ構成は以下になります。ファイルの作成自体は、直接作るのではなくFunctions Core Toolsを介して生成をしていきます。
プロジェクト初期化
.
└── api
├── .funcignore
├── .gitignore
├── host.json
├── local.settings.json
├── package.json
├── package-lock.json
├── src
│ └── functions
│ ├── protected-data.ts
│ └── user-info.ts
├── tsconfig.json
└── .vscode
└── extensions.json
まずは、api
ディレクトリを用意してディレクトリに遷移します。中で初期化コマンドを実行すると以下のファイルが自動生成され、npm install
が実行されます。
mkdir api
cd api
func init --typescript
これで初期化は完了です。次は、各エンドポイントの作成を進めていきます。エンドポイントはFunctions Core Tools(公式リファレンス)で生成します。コマンドを通して作成することで、基本の関数が準備された状態でスムーズな開発を進めることができます。使用することができるテンプレートは以下のコマンドで取得することができます。
func templates list
/api/user-info
エンドポイント実装
/api/user-info
エンドポイントの作成を進めていきます。api
ディレクトリで以下のコマンドを実行してください。
func new --name user-info --template "HTTP trigger" --authlevel "anonymous"
上記のコマンドでsrc/functions
内にuser-info.ts
というファイルが生成されます。認証レベル「匿名」でuser-info
というファイル名で作成されます。作成されたファイルの中には、--name
で指定した名前のパスに対してHELLO Worldを返す関数が作成されています。
中身を確認して、変更を加えてください。
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
interface ClientPrincipal {
identityProvider: string;
userId: string;
userDetails: string;
userRoles: string[];
}
interface UserInfo {
userId: string;
name: string;
email: string;
provider: string;
roles: string[];
isAuthenticated: boolean;
}
export async function userInfo(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log('Processing user-info request');
try {
// x-ms-client-principalヘッダーの取得
const clientPrincipalHeader = request.headers.get('x-ms-client-principal');
if (!clientPrincipalHeader) {
context.log('ヘッダーにx-ms-client-principalが確認することができず、認証状態を確認することができませんでした。');
return {
status: 200,
jsonBody: {
isAuthenticated: false,
userId: null,
name: null,
email: null,
provider: null,
roles: []
}
};
}
// Base64デコード
const clientPrincipalData = Buffer.from(clientPrincipalHeader, 'base64').toString('utf-8');
const clientPrincipal: ClientPrincipal = JSON.parse(clientPrincipalData);
context.log('Client Principal:', clientPrincipal);
// ユーザー情報の整形
const userInfo: UserInfo = {
userId: clientPrincipal.userId,
name: clientPrincipal.userDetails,
email: clientPrincipal.userDetails, // GitHubの場合はユーザー名,Googleならメールアドレス
provider: clientPrincipal.identityProvider,
roles: clientPrincipal.userRoles || [],
isAuthenticated: true
};
return {
status: 200,
headers: {
'Content-Type': 'application/json'
},
jsonBody: userInfo
};
} catch (error) {
context.log('Applicatoin Crash', error);
return {
status: 500,
jsonBody: {
error: 'Internal server error',
message: 'Failed to process user information'
}
};
}
}
app.http('user-info', {
methods: ['GET'],
authLevel: 'anonymous',
route: 'user-info',
handler: userInfo
});
こちらの関数での処理は、現在のユーザーの認証状態と基本情報を返すAPIです。特徴としては、未認証時でもエラーにならずにリクエストが成功します。
コードは大きく3つに分かれています。
- ヘッダーから
x-ms-client-principal
を取得し、ない場合はstatus:200
(ユーザー情報なし)を返答 - ヘッダーからデコードしてユーザー情報を取得し、
status:200
(ユーザー情報あり)を返答 - 上記の処理で問題が発生した場合は
status:500
を返答
デコードの処理は、公式リファレンスで紹介されている手法をもとに構築しています。その他のAPIの実装はAPI設計に準拠しています。
/api/protected-data
エンドポイント実装
/api/protected-data
エンドポイントの作成を進めていきます。api
ディレクトリで以下のコマンドを実行してください。
func new --name protected-data --template "HTTP trigger" --authlevel "anonymous"
上記のコマンドでsrc/functions
内にprotected-data.ts
というファイルが生成されます。認証レベル「匿名」でprotected-data
というファイル名で作成されます。作成されたファイルの中には、--name
で指定した名前のパスに対してHELLO Worldを返す関数が作成されています。
中身を確認して、変更を加えてください。
import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
interface ClientPrincipal {
identityProvider: string;
userId: string;
userDetails: string;
userRoles: string[];
}
interface ProtectedData {
userId: string;
message: string;
timestamp: string;
userNumber: number;
}
// 認証チェック用のヘルパー関数
function checkAuthentication(request: HttpRequest, context: InvocationContext): ClientPrincipal | null {
const clientPrincipalHeader = request.headers.get('x-ms-client-principal');
if (!clientPrincipalHeader) {
context.log('認証ヘッダーが見つかりませんでした');
return null;
}
try {
const clientPrincipalData = Buffer.from(clientPrincipalHeader, 'base64').toString('utf-8');
return JSON.parse(clientPrincipalData);
} catch (error) {
context.log('Client principalの解析に失敗しました:', error);
return null;
}
}
// 軽量なユーザー固有データ生成
function generateUserData(userId: string): ProtectedData {
// userIdをベースにした簡単なシード値
const seed = userId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
return {
userId,
message: `こんにちは、ユーザー${userId}さん!`,
timestamp: new Date().toISOString(),
userNumber: seed % 1000 // 0-999の範囲のユーザー番号
};
}
export async function protectedData(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
context.log('protected-dataリクエストを処理中');
try {
// 認証チェック
const clientPrincipal = checkAuthentication(request, context);
if (!clientPrincipal) {
return {
status: 401,
headers: {
'Content-Type': 'application/json'
},
jsonBody: {
error: 'Unauthorized',
message: '保護されたデータにアクセスするには認証が必要です'
}
};
}
context.log('認証済みユーザー:', clientPrincipal.userId);
// 軽量なダミーデータを生成
const userData = generateUserData(clientPrincipal.userId);
return {
status: 200,
headers: {
'Content-Type': 'application/json'
},
jsonBody: userData
};
} catch (error) {
context.log('protected-dataリクエストの処理中にエラー:', error);
return {
status: 500,
jsonBody: {
error: 'Internal server error',
message: '保護されたデータの取得に失敗しました'
}
};
}
}
app.http('protected-data', {
methods: ['GET'],
authLevel: 'anonymous', // SWAのManaged Functionsのため
route: 'protected-data',
handler: protectedData
});
こちらの関数の処理は、認証されていないユーザーには401エラーを返し、認証済みの場合はダミーデータを返答します。
処理として、/api/user-info
と大差ありません。先ほどのヘッダーの取得からデコードまでをヘルパー関数として切り出しています。
処理取しては、大きく4つに分類されます。
checkAuthentication
:ヘッダーからx-ms-client-principal
を取得、デコードした情報を返答する。情報がない場合は、null
を返答checkAuthentication
からの戻り値がnull
:status:401
を返答checkAuthentication
からの戻り値がユーザー情報:generateUserData
を使用してダミー情報を作成して、status:200
で返答- 上記の処理で問題が発生した場合は
status:500
を返答
デコードの処理は、公式リファレンスで紹介されている手法をもとに構築しています。その他のAPIの実装はAPI設計に準拠しています。
ローカルでのAPI動作確認
それでは、作成したAPIが正しく動作するかローカル環境で確認していきましょう。
ローカル環境の制限事項
ローカル環境では認証ヘッダーが自動付与されないため、テスト範囲は限定的ですが、基本的な動作確認は可能です。
認証ヘッダーの模擬
- Azure SWA の認証ヘッダー(
x-ms-client-principal
)は自動付与されません - 認証済み状態のテストはローカルでは困難です
- 本格的な認証テストは本番環境またはSWA CLI統合環境で行う必要があります
Azure Functions API のローカル起動
まず、APIディレクトリで Functions Core Tools を使ってローカルサーバーを起動します。
cd api
func start
正常に起動すると、以下のような出力が表示されます:
Azure Functions Core Tools
Core Tools Version: 4.0.7317 Commit hash: N/A +5ca56d37938824531b691f094d0a77fd6f51af20 (64-bit)
Function Runtime Version: 4.1038.300.25164
[2025-06-30T14:09:22.328Z] Worker process started and initialized.
Functions:
protected-data: [GET] http://localhost:7071/api/protected-data
user-info: [GET] http://localhost:7071/api/user-info
For detailed output, run func with --verbose flag.
エンドポイントが正しく表示されていることを確認してください。
/api/user-info エンドポイントのテスト
まずは認証状態を問わずアクセス可能な user-info
エンドポイントをテストします。こちらはcURLでもよいですし、ブラウザで実行してもよいです。
cURLでのテスト
curl http://localhost:7071/api/user-info
期待されるレスポンス
ローカル環境では認証ヘッダーが存在しないため、未認証状態のレスポンスが返されます:
{
"userId": null,
"name": null,
"email": null,
"provider": null,
"roles": [],
"isAuthenticated": false
}
確認ポイント
- ステータスコードが
200 OK
であること - レスポンスの型が設計通りであること
isAuthenticated
がfalse
になっていること- 各プロパティが正しく
null
または空配列になっていること
/api/protected-data
エンドポイントのテスト
次に、認証が必須の protected-data
エンドポイントをテストします。
cURLでのテスト
curl http://localhost:7071/api/protected-data
期待されるレスポンス
認証ヘッダーが存在しないため、401エラーが返されます:
{
"error": "Unauthorized",
"message": "保護されたデータにアクセスするには認証が必要です"
}
確認ポイント
- ステータスコードが
401 Unauthorized
であること - エラーレスポンスの形式が統一されていること
error
とmessage
プロパティが含まれていること
デバッグとログ確認
開発中は、Function Core Tools のコンソール出力でデバッグ情報を確認できます。
ログ出力の確認
API関数内の context.log()
による出力がコンソールに表示されます:
[2025-06-30T14:13:03.946Z] Executing 'Functions.protected-data' (Reason='This function was programmatically called via the host APIs.', Id=3570bfd5-7ed0-4ea7-a34f-262df44324ac)
[2025-06-30T14:13:03.951Z] protected-dataリクエストを処理中
[2025-06-30T14:13:03.951Z] 認証ヘッダーが見つかりませんでした
[2025-06-30T14:13:03.952Z] Executed 'Functions.protected-data' (Succeeded, Id=3570bfd5-7ed0-4ea7-a34f-262df44324ac, Duration=5ms)
詳細ログの表示
より詳細なログが必要な場合は、--verbose
オプションを使用します:
func start --verbose
トラブルシューティング
開発時に詰まった点をトラブルシューティングとしてまとめておきます。
エンドポイントにアクセスできない
- Functions Core Tools が正常に起動しているか確認
- ポート 7071 が他のプロセスで使用されていないか確認
- ファイアウォール設定を確認
TypeScript コンパイルエラー
npm run build
でビルドエラーがないか確認- 型定義の不整合がないかチェック
レスポンスが期待通りでない
context.log()
でデバッグ出力を追加- リクエストヘッダーが正しく設定されているか確認
ソースコードに変更を加えたのに変更が反映されない
func start
は変更後のソースを読み取って同期してくれません- 一度
Ctrl+C
で実行を落として、再起動してみてください
これで、ローカル環境でのAPI基本動作確認が完了しました。次に、フロントエンドの実装に移りましょう。認証機能を含む完全なテストは、SWA CLI での統合テスト環境で行います。
Azure Functions では通常、API の認証レベルを function
、admin
、anonymous
などから選択できます。しかし、Azure Static Web Apps の Managed Functions では anonymous
を設定するのが適切です。
Azure Functions単体での認証レベル
function
:Function Key が必要admin
:Master Key が必要anonymous
:認証不要
SWA + Managed Functions での認証の仕組み
Azure Static Web Apps では、認証・認可を SWA側で一元管理 します:
- SWAレベルでの保護:
staticwebapp.config.json
でルート保護を定義 - 統合セキュリティ:
/.auth
システムによる認証管理 - Functions側はanonymous:SWAが認証済みリクエストのみをFunctionsに転送
つまり、Functionsレベルでの認証をバイパスし、SWAの統合認証システムを活用することで、よりシームレスで一元化された認証管理が可能になります。Managed Functions では、この仕組みにより安全性を保ちながら開発効率を向上させることができるのです。
Next.jsフロントエンド実装
この章では、Next,js環境を構築・実装をしていきます。作成するディレクトリ構成は以下になります。初期設定はcreate-next-app経由で作成するため、作成するファイルはuseAuth.ts
/page.tsx
/protected>page.tsx
の3ファイルになります。
./frontend
├── eslint.config.mjs
├── next.config.ts
├── next-env.d.ts
├── package.json
├── package-lock.json
├── postcss.config.mjs
├── README.md
├── public
├── src
│ ├── app
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.tsx // トップページ
│ │ └── protected
│ │ └── page.tsx // 認証済みルート
│ └── hooks
│ └── useAuth.ts // 認証系Hooks
└── tsconfig.json
Next.js設定(静的エクスポート対応)
まずはfrontend
ディレクトリを作成して、ディレクトリに移動します。作成自体は、npx create-next-app
コマンドを通して自動生成を行っています。バージョンとしては15固定で、プロジェクト設定を行っています。
mkdir frontend
cd frontend
npx create-next-app@15 . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
静的エクスポート設定のためにnext.config.ts
を変種する必要があります。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'export', // 静的エクスポート設定
distDir: 'out', // ビルドファイルのエクスポート先設定
images: {
unoptimized: true // 静的エクスポートでは使用できない画像圧縮の無効化
}
};
export default nextConfig;
これで、静的エクスポートが実装することができます。
useAuth認証カスタムフック実装
こちらのファイルはhooks
ディレクトリを作成し、useAuth.ts
というファイル内にコピー&ペーストをしてください。
import { useState, useEffect } from 'react';
interface UserInfo {
isAuthenticated: boolean;
user?: {
name?: string;
email?: string;
login?: string;
};
}
export const useAuth = () => {
const [userInfo, setUserInfo] = useState<UserInfo>({ isAuthenticated: false });
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUserInfo = async () => {
try {
const response = await fetch('/.auth/me');
if (response.ok) {
const data = await response.json();
if (data.clientPrincipal) {
setUserInfo({
isAuthenticated: true,
user: {
name: data.clientPrincipal.userDetails,
email: data.clientPrincipal.userDetails,
login: data.clientPrincipal.userId
}
});
} else {
setUserInfo({ isAuthenticated: false });
}
} else {
setUserInfo({ isAuthenticated: false });
}
} catch (err) {
console.error('認証情報の取得に失敗しました:', err);
setError('認証情報の取得に失敗しました');
setUserInfo({ isAuthenticated: false });
} finally {
setLoading(false);
}
};
fetchUserInfo();
}, []);
const login = () => {
window.location.href = '/.auth/login/github';
};
const logout = () => {
window.location.href = '/.auth/logout';
};
return {
...userInfo,
loading,
error,
login,
logout
};
};
処理としては、ログインとログアウトロジックがあります。こちらは、認証プロバイダー(GitHub)としてチューニングされています。
SWAでは認証済みの場合、直接エンドポイントとして/.auth/me
に認証情報が含まれます。そちらに対して確認を行うことで、フロントエンド側から認証状態の確認を行うことができます。
認証状態の確認中はuseState
のloading
で制御をしています。
トップページ実装
こちらはsrc > app > page.tsx
ファイルを編集してください。まずはコピー&ペーストをしてください。
'use client';
import Link from 'next/link';
import { useAuth } from '@/hooks/useAuth';
import { useState } from 'react';
interface ApiUserInfo {
userId: string;
name: string;
email: string;
provider: string;
roles: string[];
isAuthenticated: boolean;
}
interface ApiError {
error: string;
message: string;
}
export default function HomePage() {
const { isAuthenticated, user, loading, login, logout } = useAuth();
const [apiData, setApiData] = useState<ApiUserInfo | null>(null);
const [apiError, setApiError] = useState<string | null>(null);
const [apiLoading, setApiLoading] = useState(false);
const fetchUserInfo = async () => {
setApiLoading(true);
setApiError(null);
setApiData(null);
try {
const response = await fetch('/api/user-info', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // 認証情報を含める
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiUserInfo | ApiError = await response.json();
if ('error' in data) {
setApiError(data.message || data.error);
} else {
setApiData(data);
}
} catch (error) {
console.error('API呼び出しエラー:', error);
setApiError(error instanceof Error ? error.message : 'APIの呼び出しに失敗しました');
} finally {
setApiLoading(false);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">認証状態を確認中...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="container mx-auto px-4 py-16">
<div className="max-w-4xl mx-auto text-center">
<h1 className="text-5xl font-bold text-gray-900 mb-8">
Next.js 15 + Azure SWA
</h1>
<p className="text-xl text-gray-600 mb-12">
GitHub認証を使用したセキュアなWebアプリケーション
</p>
<div className="bg-white rounded-lg shadow-lg p-8 mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-6">
認証状態
</h2>
{isAuthenticated ? (
<div className="space-y-4">
<div className="flex items-center justify-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-green-700 font-medium">認証済み</span>
</div>
{user && (
<div className="bg-gray-50 rounded-lg p-4 text-left max-w-md mx-auto">
<h3 className="font-medium text-gray-800 mb-2">ユーザー情報(クライアント側)</h3>
{user.name && (
<p className="text-sm text-gray-600">
<span className="font-medium">名前:</span> {user.name}
</p>
)}
{user.email && (
<p className="text-sm text-gray-600">
<span className="font-medium">メール:</span> {user.email}
</p>
)}
{user.login && (
<p className="text-sm text-gray-600">
<span className="font-medium">ログイン:</span> {user.login}
</p>
)}
</div>
)}
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-6">
<Link
href="/protected"
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition duration-200"
>
保護されたページへ
</Link>
<button
onClick={logout}
className="bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-6 rounded-lg transition duration-200"
>
ログアウト
</button>
</div>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-center space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<span className="text-red-700 font-medium">未認証</span>
</div>
<p className="text-gray-600 mb-6">
保護されたコンテンツにアクセスするには、GitHubアカウントでログインしてください。
</p>
<button
onClick={login}
className="bg-gray-900 hover:bg-gray-800 text-white font-medium py-3 px-6 rounded-lg transition duration-200 flex items-center space-x-2 mx-auto"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
</svg>
<span>GitHubでログイン</span>
</button>
</div>
)}
</div>
{/* API情報取得セクション */}
<div className="bg-white rounded-lg shadow-lg p-8 mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-6">
API情報取得
</h2>
<div className="space-y-4">
<button
onClick={fetchUserInfo}
disabled={apiLoading}
className="bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white font-medium py-3 px-6 rounded-lg transition duration-200 flex items-center space-x-2 mx-auto"
>
{apiLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>取得中...</span>
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>サーバーからユーザー情報を取得</span>
</>
)}
</button>
{apiError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-left max-w-md mx-auto">
<h3 className="font-medium text-red-800 mb-2">エラー</h3>
<p className="text-sm text-red-600">{apiError}</p>
</div>
)}
{apiData && (
<div className={" rounded-lg p-4 text-left max-w-md mx-auto" +
(apiData.isAuthenticated ? ' bg-green-50 border border-green-200' : ' bg-red-50 border border-red-200')}
>
<h3 className={"font-medium mb-2" + (apiData.isAuthenticated ? " text-green-800": " text-red-800")}>サーバー側ユーザー情報</h3>
<div className={"space-y-1 text-sm " + (apiData.isAuthenticated ? " text-green-700": " text-red-700")}>
<p><span className="font-medium">認証状態:</span> {apiData.isAuthenticated ? '認証済み' : '未認証'}</p>
<p><span className="font-medium">ユーザーID:</span> {apiData.userId}</p>
<p><span className="font-medium">名前:</span> {apiData.name}</p>
<p><span className="font-medium">メール:</span> {apiData.email}</p>
<p><span className="font-medium">プロバイダー:</span> {apiData.provider}</p>
<p><span className="font-medium">ロール:</span> {apiData.roles.length > 0 ? apiData.roles.join(', ') : 'なし'}</p>
</div>
</div>
)}
</div>
</div>
<div className="text-center text-gray-500">
<p className="text-sm">
このアプリケーションはAzure Static Web Appsで認証を管理しています
</p>
</div>
</div>
</div>
</div>
);
}
ロジック部分
ロジック部分を抽出しました。こちらは、ボタンを押した際に/api/user-info
に対してGETリクエストを送信して問い合わせを行っています。APIリクエストの状態管理はuseState
のapiLoading
でエラーに関してはuseState
のapiError
で管理をしています。
interface ApiUserInfo {
userId: string;
name: string;
email: string;
provider: string;
roles: string[];
isAuthenticated: boolean;
}
interface ApiError {
error: string;
message: string;
}
const [apiData, setApiData] = useState<ApiUserInfo | null>(null);
const [apiError, setApiError] = useState<string | null>(null);
const [apiLoading, setApiLoading] = useState(false);
const fetchUserInfo = async () => {
setApiLoading(true);
setApiError(null);
setApiData(null);
try {
const response = await fetch('/api/user-info', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ApiUserInfo | ApiError = await response.json();
if ('error' in data) {
setApiError(data.message || data.error);
} else {
setApiData(data);
}
} catch (error) {
console.error('API呼び出しエラー:', error);
setApiError(error instanceof Error ? error.message : 'APIの呼び出しに失敗しました');
} finally {
setApiLoading(false);
}
};
表示部分
表示部分としてはコードとして膨大なので、ソース添付としては割愛します。処理としては単純です。
認証確認中はuseAuth
から提供されるloading
をフラグとしてローディング画面を表示しています。
認証後は、APIアクセス中はapiLoadingでボタンの非活性化とローディングの表示、エラーが発生した場合はエラーの表示を行っています。
protectedページ実装
こちらはsrc > app > protected > page.tsx
ファイルを新規作成してください。まずはコピー&ペーストをしてください。
'use client';
import Link from 'next/link';
import { useAuth } from '@/hooks/useAuth';
import { useState } from 'react';
interface ProtectedData {
userId: string;
message: string;
timestamp: string;
userNumber: number;
}
interface ApiError {
error: string;
message: string;
}
export default function ProtectedPage() {
const { user, loading, logout } = useAuth();
const [protectedData, setProtectedData] = useState<ProtectedData | null>(null);
const [apiError, setApiError] = useState<string | null>(null);
const [apiLoading, setApiLoading] = useState(false);
const fetchProtectedData = async () => {
setApiLoading(true);
setApiError(null);
setProtectedData(null);
try {
const response = await fetch('/api/protected-data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('認証が必要です');
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ProtectedData | ApiError = await response.json();
if ('error' in data) {
setApiError(data.message || data.error);
} else {
setProtectedData(data);
}
} catch (error) {
console.error('保護されたAPI呼び出しエラー:', error);
setApiError(error instanceof Error ? error.message : '保護されたデータの取得に失敗しました');
} finally {
setApiLoading(false);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">ユーザー情報を読み込み中...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100">
<div className="container mx-auto px-4 py-16">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<div className="flex items-center justify-center space-x-2 mb-4">
<div className="w-4 h-4 bg-green-500 rounded-full"></div>
<span className="text-green-700 font-medium text-lg">保護されたエリア</span>
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">
🎉 認証成功!
</h1>
<p className="text-xl text-gray-600">
このページは認証済みユーザーのみがアクセスできます
</p>
</div>
<div className="bg-white rounded-lg shadow-lg p-8 mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-6 text-center">
あなたの情報
</h2>
{user ? (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{user.name && (
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-500 font-medium">名前</div>
<div className="text-lg text-gray-900">{user.name}</div>
</div>
)}
{user.email && (
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-500 font-medium">メールアドレス</div>
<div className="text-lg text-gray-900">{user.email}</div>
</div>
)}
{user.login && (
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-500 font-medium">GitHubユーザー名</div>
<div className="text-lg text-gray-900">@{user.login}</div>
</div>
)}
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-500 font-medium">アクセス日時</div>
<div className="text-lg text-gray-900">
{new Date().toLocaleString('ja-JP')}
</div>
</div>
</div>
</div>
) : (
<div className="text-center py-8">
<p className="text-gray-500">ユーザー情報が見つかりません</p>
</div>
)}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
<h3 className="text-lg font-semibold text-blue-900 mb-3">
🔒 セキュリティ情報
</h3>
<ul className="text-blue-800 space-y-2">
<li>• このページはAzure SWAレベルで保護されています</li>
<li>• 未認証ユーザーは自動的にGitHubログインにリダイレクトされます</li>
<li>• 認証情報はAzure Static Web Appsで安全に管理されています</li>
</ul>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/"
className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition duration-200 text-center"
>
ホームに戻る
</Link>
<button
onClick={logout}
className="bg-red-500 hover:bg-red-600 text-white font-medium py-3 px-6 rounded-lg transition duration-200"
>
ログアウト
</button>
</div>
</div>
{/* 保護されたデータ取得セクション */}
<div className="bg-white rounded-lg shadow-lg p-8 mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-6 text-center">
🛡️ 保護されたデータ
</h2>
<div className="space-y-6">
<div className="text-center">
<p className="text-gray-600 mb-4">
サーバー側で認証を確認し、あなた専用のデータを取得します
</p>
<button
onClick={fetchProtectedData}
disabled={apiLoading}
className="bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400 text-white font-medium py-3 px-6 rounded-lg transition duration-200 flex items-center space-x-2 mx-auto"
>
{apiLoading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>データ取得中...</span>
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span>保護されたデータを取得</span>
</>
)}
</button>
</div>
{apiError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<div className="flex items-center space-x-2 mb-2">
<svg className="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="font-medium text-red-800">エラー</h3>
</div>
<p className="text-sm text-red-600">{apiError}</p>
</div>
)}
{protectedData && (
<div className="bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-200 rounded-lg p-6">
<div className="flex items-center space-x-2 mb-4">
<svg className="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="font-medium text-purple-800">取得成功 🎯</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-500 font-medium">ユーザーID</div>
<div className="text-lg text-gray-900 font-mono">{protectedData.userId}</div>
</div>
<div className="bg-white rounded-lg p-4">
<div className="text-sm text-gray-500 font-medium">ユーザー番号</div>
<div className="text-lg text-gray-900 font-bold ">
#{protectedData.userNumber}
</div>
</div>
<div className="bg-white rounded-lg p-4 md:col-span-2">
<div className="text-sm text-gray-500 font-medium">パーソナライズメッセージ</div>
<div className="text-lg text-gray-900">{protectedData.message}</div>
</div>
<div className="bg-white rounded-lg p-4 md:col-span-2">
<div className="text-sm text-gray-500 font-medium">データ生成時刻</div>
<div className="text-lg text-gray-900 font-mono">
{new Date(protectedData.timestamp).toLocaleString('ja-JP')}
</div>
</div>
</div>
<div className="mt-4 p-3 bg-purple-100 rounded-lg">
<p className="text-sm text-purple-700">
💡 このデータはあなたのユーザーIDに基づいて動的に生成され、
サーバー側で認証を確認した後にのみ提供されます。
</p>
</div>
</div>
)}
</div>
</div>
<div className="text-center text-gray-500">
<p className="text-sm">
Azure Static Web Apps + GitHub認証で実現したセキュアなアプリケーション
</p>
</div>
</div>
</div>
</div>
);
}
ロジック部分
ロジック部分を抽出しました。こちらは、ボタンを押した際に/api/protected-data
に対してGETリクエストを送信して問い合わせを行っています。APIリクエストの状態管理はuseState
のapiLoading
でエラーに関してはuseState
のapiError
で管理をしています。
SWA側で401エラーが発生した場合は、アクセスできないように組みますが将来的なエラーハンドリングのために仮で実装しています。
interface ProtectedData {
userId: string;
message: string;
timestamp: string;
userNumber: number;
}
interface ApiError {
error: string;
message: string;
}
const [protectedData, setProtectedData] = useState<ProtectedData | null>(null);
const [apiError, setApiError] = useState<string | null>(null);
const [apiLoading, setApiLoading] = useState(false);
const fetchProtectedData = async () => {
setApiLoading(true);
setApiError(null);
setProtectedData(null);
try {
const response = await fetch('/api/protected-data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('認証が必要です');
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ProtectedData | ApiError = await response.json();
if ('error' in data) {
setApiError(data.message || data.error);
} else {
setProtectedData(data);
}
} catch (error) {
console.error('保護されたAPI呼び出しエラー:', error);
setApiError(error instanceof Error ? error.message : '保護されたデータの取得に失敗しました');
} finally {
setApiLoading(false);
}
};
表示部分
表示部分としてはコードとして膨大なので、ソース添付としては割愛します。処理としては単純です。
認証確認中はuseAuth
から提供されるloading
をフラグとしてローディング画面を表示しています。
認証後は、APIアクセス中はapiLoadingでボタンの非活性化とローディングの表示、エラーが発生した場合はエラーの表示を行っています。
ローカルでのフロントエンド動作確認
ローカルでフロントエンドのテストをします。APIとの接続を行っていないので、APIアクセスはすべて失敗するので画面上のテストのみになります。
npm run dev
# http://localhost:3000
ローカル結合テスト(SWA CLI)
ここでは、SWA CLIを使用してフロントエンドとAzure Functionsの結合テストを行っていきます。SWA CLIを使うことで、SWAに上げた時の挙動を認証部分を含めてエミュレーターで確認することができます。
SWA CLI設定
プロジェクトルートで、以下のコマンドを入力することで対話的にswa-cli.config.json
を作成することが可能です。
swa init
設定項目は公式のこちらにまとまっています。若干設定が複雑な部分があるため、今回の環境に合わせた構成ファイルに対して解説します。
swaConfigLocation
に関してのみ次の節で解説を行います。
フロントもバックもSWA CLIで起動する
こちらの構成ファイルでは、swa start
でフロントもバックのリソースも立ち上がるようになります。利点としては、コマンド一つで立ち上げることができるので楽な点ですね。
フロントに関しては、devコマンドで起動されているためフロントのコードを変更することで自動的に更新(ホットリロード)が入ります。バックに関しては、更新を反映させるためには一度コマンドを落として再度実行する必要があります。
{
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
"configurations": {
"swa-api-bicep-sample": {
"appLocation": "frontend",
"apiLocation": "api",
"outputLocation": "out",
"apiLanguage": "node",
"appBuildCommand": "npm run build",
"apiBuildCommand": "npm run build --if-present",
"run": "npm run dev",
"appDevserverUrl": "http://localhost:3000",
"swaConfigLocation": "frontend/config/local"
}
}
}
フロントのみSWA CLIで起動してバックは起動済みのものを使用する
こちらの構成ファイルでは、swa start
でフロントのリソースのみを立ち上げ、バックエンドのリソースは提供済みのものを使用します。こちらの利点としては、バックエンドのリソースのみを分離して立ち上げ直しができる点ですね。その代わり、実行にはfunc start
コマンドを別途実行してあげる必要があります。
{
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
"configurations": {
"swa-api-bicep-sample": {
"appLocation": "frontend",
"apiLocation": "api",
"outputLocation": "out",
"apiLanguage": "node",
"appBuildCommand": "npm run build",
"apiBuildCommand": "npm run build --if-present",
"run": "npm run dev",
"appDevserverUrl": "http://localhost:3000",
"apiDevserverUrl": "http://localhost:7071",
"swaConfigLocation": "frontend/config/local"
}
}
}
SWA設定ファイル(staticwebapp.config.json)
こちらのファイルは、認証ルートの定義やSWA側でサーバー側で発生したエラーをキャッチしてルーティングを制御します。先ほどのswa-cli.config.json
でswaConfigLocation
を設定していましたが、これは開発環境と本番環境で二つのファイルを構成する必要があるためです。開発環境では、認証プロバイダーの設定をSWA CLIのエミュレーターで代用します。本番環境では、SWAの環境変数から認証プロバイダーに必要な情報を読み取って認証プロバイダーを構成します。
本番環境用の構成ファイルを使用してSWA CLIでローカルで立ち上げると認証時に失敗します。
SWA 設定ファイルは大部分がフロントのルーティング定義であるため、frontend > config
とディレクトリを定義してその中にディレクトリ単位でlocal
とprod
と分割して同名ファイルを保存します。
./frontend
└── config
├── local
│ └── staticwebapp.config.json
└── prod
└── staticwebapp.config.json
開発環境用
SWAに認証されるとユーザーには自動的に「authenticated」と「anoymous」というロールが割り振られます。SWA Configでは、ロールによって表示することができるルート制御というのがあり、今回であれば/protected
ルートではauthenticated
ロールにアクセスを許可しています。それ以外のルートに関しては、200で許可しています。もし未認証の方が/protected
にアクセスした場合はSWA側で401エラーが発生して、自動的に認証画面にリダイレクトするようにresponseOverrdes
で設定しています。ここをエラー用のページに設定しておけば、自動でエラー画面を表示するなんてこともできます。
platform
の設定はAzure Functionsの設定になります。今回であればnode系を使用していたため設定しています。
{
"routes": [
{
"route": "/protected/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/*",
"statusCode": 200
}
],
"responseOverrides": {
"401": {
"redirect": "/.auth/login/github",
"statusCode": 302
}
},
"platform": {
"apiRuntime": "node:18"
}
}
本番環境
大枠は開発環境と同じですが、こちらではauth
でカスタム認証プロバイダーの設定がされています。設定方法は公式のこちらで言及があります。こちらはSWAの環境変数から値を読み取っているため、SWA側の設定としてGITHUB_CLIENT_ID
とGITHUB_CLIENT_SECRET
を設定してあげる必要があります。こちらは、Azureのリソース作成の手順の際に合わせて設定します。
{
"auth": {
"identityProviders": {
"github": {
"registration": {
"clientIdSettingName": "GITHUB_CLIENT_ID",
"clientSecretSettingName": "GITHUB_CLIENT_SECRET"
}
}
}
},
"routes": [
{
"route": "/protected/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/*",
"statusCode": 200
}
],
"responseOverrides": {
"401": {
"redirect": "/.auth/login/github",
"statusCode": 302
}
},
"platform": {
"apiRuntime": "node:18"
}
}
フロントエンド + API結合確認
それでは、接続してテストを開始しましょう。これまでの設定が完了していれば、ルートディレクトリで以下のコマンドでエミュレーターが起動します。もし起動しない方は、swa-cli.config.jsonの設定を再度確認してください。http://localhost:7071 ready
で1分以上固まっている方は、apiディレクトリでfunc start
コマンドを実行してください。
# swa-cli.config.jsonでの設定による起動
swa start
動作確認テストの実施
さて、ここまででフロントエンドとAPIの実装が完了しましたね!いよいよ統合テストの時間です。今回は正常系の動作確認に集中して進めていきます。Azure SWAの素晴らしいところは、認証周りの面倒な処理を自動でやってくれることなので、エラーハンドリングよりも「ちゃんと期待通りに動いているか」を確認することが重要ですね。
実際のテストでは、未認証状態と認証状態の両方で動作を確認していきます。特に認証フローがスムーズに動作するかどうかが、このプロジェクトの肝になる部分です。
未認証状態での動作確認
まずは未認証状態から確認していきましょう。ブラウザでシークレットモードを開くか、既存のセッションをクリアしてからテストを開始します。
トップページ(/
)での確認項目:
- ページにアクセスすると「未認証状態」と表示される
- 「サーバーからユーザー情報を取得」ボタンをクリックしても、ユーザー情報は取得できない(nullやundefinedが返される)
- 「GitHubでログイン」ボタンが表示され、クリックすると認証画面に遷移する
保護ページ(/protected
)での確認項目:
- URLを直接入力してアクセスを試みると、自動的に認証画面に遷移する
この時点では、Azure SWAが未認証ユーザーを自動的に認証フローに誘導してくれるので、エラー画面が表示されることはありません。これがSWAの便利なところですね!
認証状態での動作確認
続いて、GitHub認証を完了した状態での動作を確認します。認証が成功すると、トップページにリダイレクトされるはずです。認証画面は以下のような画面になります。Providerに関しては、埋められた状態で開かれるはずです。UsernameはGitHub認証の場合はユーザー名になります。(表示名ではありません @以下です)

トップページ(/
)での確認項目:
- ページ上部に「認証状態」と表示される
- 「サーバーからユーザー情報を取得」ボタンをクリックすると、GitHubから取得したユーザー情報が表示される
- 「保護されたページ」ボタンと「ログアウト」ボタンが表示される
保護ページ(/protected
)での確認項目:
- ページに正常にアクセスできる
- 「保護されたデータを取得」ボタンをクリックすると、サーバーサイドのAPIからダミーデータが取得される
認証が正しく動作していれば、Azure Functionsに送信されるx-ms-client-principal
ヘッダーにユーザー情報が含まれているので、APIから適切なレスポンスが返ってきます。
テスト実施時のポイント
テストを実施する際に気をつけておきたいポイントをいくつか挙げておきますね。
Azure SWAの自動処理について: 未認証時の保護リソースへのアクセスや、/api/protected-data
への直接アクセスは、SWAの設定により自動的に認証画面にリダイレクトされます。そのため、401エラーが画面に表示されることはなく、ユーザーは常にスムーズな認証フローを体験できます。
セッション管理の確認: ページをリロードしても認証状態が維持されることを確認してください。これはAzure SWAが内部的にセッション管理を行っているためです。
API呼び出しの動作: 認証状態でのAPI呼び出しでは、Azure SWAが自動的に認証ヘッダーを付与してくれるので、フロントエンド側で特別な処理は不要です。これも開発者としてはとても楽な部分ですね。
今回のテストはシンプルですが、実際のプロダクションで必要な基本的な動作はすべてカバーできています。正常系がしっかり動作することを確認できれば、Azure SWAとNext.js、Azure Functionsの統合は成功です!
インフラ構築(Bicep)
これまでの章でローカルの開発は完了しました。ここからは、実際にAzure環境へのデプロイをするための準備を行います。具体的にはBicepとパラメーターファイルを作成してAzureのリソースの定義を行い、作成したファイルを使用してGitHubにデプロイに必要な変数の定義とAzureリソースを作成するbashファイルを用意します。
注意点:ここら先はAzure Static Web Appsに対してリソースの作成を行います。今回はStandardプランでリソースを作成するため、もし不安な方はFree版でのデプロイで練習をしておきましょう。別の環境を使って詳細にデプロイするためのガイドを書いているので、こちらのブログを参照して練習してみてください。
事前準備
Bicepファイルとパラメーターファイルの説明などはこちらで詳しく解説しています。ぜひ一度お読みください。
こちらの章で目指す内容は以下になります。
- Azure CLI:Azure Static Web AppsのリソースをStandardプランで作成する
- 手動:GitHub認証プロバイダーの設定完了
- GitHub CLI:GitHub Enviromentにデプロイに必要な情報が設定されている
作成に当たって目指すディレクトリ構成は以下になります。
./infra
├── bicep
│ ├── main.bicep
│ └── parameters.json
└── scripts
└── deploy.sh
GitHubの設定
まずは今まで設定したリソースをGitHub上にpushしておきましょう。GitHub CLIでenvironment
を設定するためには事前にenvironment
を作成しておく必要があります。production
という名前でenvironment
を作成してください。設定は公式のドキュメントを参考に設定してください。 値の登録自体は、GitHub CLI経由で実行することができます。ですが、Enviromentの作成はGitHub API経由でしか行うことができません。ちょっと設定がややこしいので、ここは手動で作成しています。
ここで取得するべき情報としてはリポジトリのURLになります。
Bicepテンプレート作成 main.bicep
詳細な設定に関しては公式リファレンスを参照してください。
@description('Static Web App名')
param staticWebAppName string
@description('デプロイするリージョン')
param location string = resourceGroup().location
@description('GitHubリポジトリのURL')
param repositoryUrl string
@description('デプロイ対象のブランチ')
param branch string = 'main'
@description('フロントエンドのソースフォルダパス')
param appLocation string = 'frontend'
@description('APIのソースフォルダパス')
param apiLocation string = 'api'
@description('ビルド出力フォルダパス')
param outputLocation string = 'out'
@description('GitHub OAuth App のClient ID')
param githubClientId string
@description('GitHub OAuth App のClient Secret')
@secure()
param githubClientSecret string
// Static Web App リソース
resource staticWebApp 'Microsoft.Web/staticSites@2024-11-01' = {
name: staticWebAppName
location: location
sku: {
name: 'Standard'
tier: 'Standard'
}
properties: {
repositoryUrl: repositoryUrl
branch: branch
buildProperties: {
appLocation: appLocation
apiLocation: apiLocation
outputLocation: outputLocation
skipGithubActionWorkflowGeneration: false
}
stagingEnvironmentPolicy: 'Enabled'
}
}
// アプリケーション設定(GitHub OAuth用の環境変数)
resource staticWebAppSettings 'Microsoft.Web/staticSites/config@2022-03-01' = {
name: 'appsettings'
parent: staticWebApp
properties: {
GITHUB_CLIENT_ID: githubClientId
GITHUB_CLIENT_SECRET: githubClientSecret
}
}
// アウトプット
@description('Static Web Appsのエンドポイント')
output appBaseUrl string = 'https://${staticWebApp.properties.defaultHostname}'
@description('Static Web Appsの名前')
output resourceName string = staticWebAppName
@description('GitHubリポジトリのURL')
output repositoryUrl string = repositoryUrl
@description('フロントエンドのソースフォルダパス')
output appLocation string = appLocation
@description('APIのソースフォルダパス')
output apiLocation string = apiLocation
@description('ビルド出力フォルダパス')
output outputLocation string = outputLocation
課金に重要なパラメーターとしてはsku
になります。ここをFreeにするかStandardにするかで課金が走るか決定します。(個人的には検証目的で建てたらすぐ消しましょう)
staticWebAppSettings
に関しては、SWAでカスタム認証プロバイダー(GitHub)を構成するのに必要な値を環境変数として設定しています。機密情報であるため、パラメーターファイルには記述せずに環境変数として定義して、デプロイ時のみ呼び出して埋め込むという方法で管理します。
パラメーターファイルで定義している内容をそのままアウトプットしている値が複数ありますが、こちらは後続のbashファイルでGitHub CLIを通してEnvriomentにValiableとして登録するために出力しています。
パラメータファイル設定 parameters.json
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"staticWebAppName": {
"value": "swa-api-deploy-bicep"
},
"location": {
"value": "East Asia"
},
"repositoryUrl": {
"value": "https://github.com/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxx"
},
"branch": {
"value": "main"
},
"appLocation": {
"value": "deploy/frontend"
},
"apiLocation": {
"value": "api"
},
"outputLocation": {
"value": "."
}
}
}
appLocation
/apiLocation
/outputLocation
に関しては後続で設定するGitHub Actionsと連携した値になります。
repositoryUrl
に関しては自分のリポジトリの値に変更してください。
デプロイ用bashスクリプト deploy.sh
Bicepファイルとパラメーターファイルが完成したのでデプロイする準備が整いました。ですが、このままではAzure CLIのコマンドとGitHub CLIコマンドを別々にたたく必要があります。それを解消するためにデプロイの流れをbashファイルにまとめます。
まずは、ルートディレクトリに.env
ファイルを作成してください。こちらのファイルは.git
と同じディレクトリ(リポジトリルート)に配置するようにしてください。
GITHUB_CLIENT_ID=xxxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxx
GITHUB_ENVIRONMENT="production"
RESOURCE_GROUP_NAME=swa-bicep-test
GITHUB_CLIENT_ID
/GITHUB_CLIENT_SECRET
に関しては、まだ設定していないのでマスクしたままで大丈夫です。こちらの設定には、SWAをデプロイしてURLを確定させる必要があります。
これで.envが立派な機密情報になりました。.gitignoreに設定して間違ってもpushしないように気を付けましょう。
#!/bin/bash
# エラー時に停止
set -e
log_info() { echo -e "\033[1;34m$1\033[0m"; }
log_success() { echo -e "\033[1;32m$1\033[0m"; }
log_warning() { echo -e "\033[1;33m$1\033[0m"; }
log_error() { echo -e "\033[1;31m$1\033[0m"; }
DEPLOYMENT_NAME="swa-deployment-$(date +%Y%m%d-%H%M%S)"
CONFIG_FILE="$(git rev-parse --show-toplevel)/.env"
if [ -f "$CONFIG_FILE" ]; then
echo "📁 設定ファイル読み込み: $CONFIG_FILE"
source "$CONFIG_FILE"
else
echo "⚠️ 設定ファイルが見つかりません: $CONFIG_FILE"
echo " .env.example をコピーして設定してください"
exit 1
fi
# 必要な環境変数の確認(機密情報のみ)
required_vars=("GITHUB_CLIENT_ID" "GITHUB_CLIENT_SECRET" "RESOURCE_GROUP_NAME")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "エラー: 環境変数 $var が設定されていません"
exit 1
fi
done
# GitHub CLIがインストールされているかチェック
if ! command -v gh &> /dev/null; then
log_error "GitHub CLI (gh) がインストールされていません"
log_error "インストール方法: https://cli.github.com/"
exit 1
fi
# GitHub CLIの認証チェック
if ! gh auth status &> /dev/null; then
log_error "GitHub CLIが認証されていません"
log_error "認証方法: gh auth login"
exit 1
fi
# リポジトリのルートディレクトリかチェック
if ! git rev-parse --is-inside-work-tree &> /dev/null; then
log_error "Gitリポジトリ内で実行してください"
exit 1
fi
# 現在のリポジトリ情報を取得(デバッグ用)
CURRENT_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "unknown")
log_info "📍 対象リポジトリ: $CURRENT_REPO"
# リソースグループ作成(存在しない場合)
log_info ""
log_info "📦 ローカル開発用リソースグループ確認・作成..."
if ! az group show --name $RESOURCE_GROUP_NAME &> /dev/null; then
log_info "🔧 リソースグループ作成中: $RESOURCE_GROUP_NAME"
az group create --name $RESOURCE_GROUP_NAME --location $LOCATION
log_success "✅ リソースグループ作成完了"
else
log_success "✅ リソースグループ既存: $RESOURCE_GROUP_NAME"
fi
echo "Azure Static Web Appをデプロイ中..."
az deployment group validate \
--resource-group "$RESOURCE_GROUP_NAME" \
--template-file infra/bicep/main.bicep \
--parameters @infra/bicep/parameters.json \
--parameters githubClientId="$GITHUB_CLIENT_ID" \
--parameters githubClientSecret="$GITHUB_CLIENT_SECRET"
# Bicepテンプレートでデプロイ(機密情報のみ環境変数から渡す)
az deployment group create \
--resource-group "$RESOURCE_GROUP_NAME" \
--template-file infra/bicep/main.bicep \
--parameters @infra/bicep/parameters.json \
--parameters githubClientId="$GITHUB_CLIENT_ID" \
--parameters githubClientSecret="$GITHUB_CLIENT_SECRET" \
--name ${DEPLOYMENT_NAME}
# 全outputを一度に取得
OUTPUTS=$(az deployment group show \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$DEPLOYMENT_NAME" \
--query "properties.outputs" -o json)
echo "✅ デプロイ結果を取得しました"
# 各値を抽出
APP_BASE_URL=$(echo "$OUTPUTS" | jq -r '.appBaseUrl.value')
STATIC_WEB_APP_NAME=$(echo "$OUTPUTS" | jq -r '.resourceName.value')
REPOSITORY_URL=$(echo "$OUTPUTS" | jq -r '.repositoryUrl.value')
APP_LOCATION=$(echo "$OUTPUTS" | jq -r '.appLocation.value')
API_LOCATION=$(echo "$OUTPUTS" | jq -r '.apiLocation.value')
OUTPUT_LOCATION=$(echo "$OUTPUTS" | jq -r '.outputLocation.value')
# 結果を表示
log_success ""
log_success "Azure Static Web Appのデプロイが完了しました!"
log_success "アプリのベースURL: $APP_BASE_URL"
log_success "リソース名: $STATIC_WEB_APP_NAME"
log_success "リポジトリURL: $REPOSITORY_URL"
log_success "アプリの場所: $APP_LOCATION"
log_success "APIの場所: $API_LOCATION"
log_success "出力の場所: $OUTPUT_LOCATION"
# === 新機能: デプロイトークンの取得とGitHub Environment設定 ===
log_info ""
log_info "🔑 Azure Static Web Appsのデプロイトークンを取得中..."
# デプロイトークンを取得
DEPLOY_TOKEN=$(az staticwebapp secrets list \
--name "$STATIC_WEB_APP_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--query "properties.apiKey" -o tsv)
if [ -z "$DEPLOY_TOKEN" ]; then
log_error "デプロイトークンの取得に失敗しました"
exit 1
fi
log_success "✅ デプロイトークンを取得しました"
# GitHub環境の作成と設定
log_info ""
log_info "🌍 GitHub環境 '$GITHUB_ENVIRONMENT' を設定中..."
# 注意: GitHub CLIは現在のGitリポジトリを自動認識します
# このスクリプトはGitリポジトリ内で実行する必要があります
# Environment変数の設定
log_info ""
log_info "📝 Environment変数を設定中..."
# app_location
if gh variable set APP_LOCATION --body "$APP_LOCATION" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then
log_success "✅ 変数 'app_location' を設定: $APP_LOCATION"
else
log_error "❌ 変数 'app_location' の設定に失敗しました"
fi
# api_location
if gh variable set API_LOCATION --body "$API_LOCATION" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then
log_success "✅ 変数 'api_location' を設定: $API_LOCATION"
else
log_error "❌ 変数 'api_location' の設定に失敗しました"
fi
# output_location
if gh variable set OUTPUT_LOCATION --body "$OUTPUT_LOCATION" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then
log_success "✅ 変数 'output_location' を設定: $OUTPUT_LOCATION"
else
log_error "❌ 変数 'output_location' の設定に失敗しました"
fi
# デプロイトークンをシークレットに設定
log_info ""
log_info "🔐 デプロイトークンをシークレットに設定中..."
# 注意: gh secret setは現在のGitリポジトリの指定した環境にシークレットを設定します
if gh secret set AZURE_STATIC_WEB_APPS_API_TOKEN --body "$DEPLOY_TOKEN" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then
log_success "✅ シークレット 'deploy_token' を設定しました"
else
log_error "❌ シークレット 'deploy_token' の設定に失敗しました"
log_warning "⚠️ 手動で設定してください:"
log_warning " リポジトリ設定 > Environments > $GITHUB_ENVIRONMENT > Secrets"
log_warning " シークレット名: deploy_token"
log_warning " 値: [デプロイトークン]"
fi
# 最終結果サマリー
log_success ""
log_success "🎉 すべての設定が完了しました!"
log_success ""
log_success "📋 設定内容サマリー:"
log_success " - Azure Static Web App: $STATIC_WEB_APP_NAME"
log_success " - アプリURL: $APP_BASE_URL"
log_success " - GitHub環境: $GITHUB_ENVIRONMENT"
log_success " - 設定された変数:"
log_success " • app_location: $APP_LOCATION"
log_success " • api_location: $API_LOCATION"
log_success " • output_location: $OUTPUT_LOCATION"
log_success " - 設定されたシークレット:"
log_success " • deploy_token: [設定済み]"
log_success ""
log_success "🚀 GitHub Actionsでのデプロイが可能になりました!"
長いbashファイルですが、以下のステップを順次実行しています。
- 環境変数の取得
- 各種環境の確認
- Bicepを使用したデプロイ検証・実行
- Azureリソースから情報取得
- GitHub CLIを経由したSecret・Valiableの設定
- リザルト表示
各コマンドに関しての詳細な説明はこちらで行っています。権限を振って実行してみてください。
chmod +x ./infra/scripts/deploy.sh
./infra/scripts/deploy.sh
# リザルトで以下が出たら成功!!
#📋 設定内容サマリー:
# - Azure Static Web App: swa-api-deploy-bicep
# - アプリURL: https://blue-bay-01b32fb00.2.azurestaticapps.net
# - GitHub環境: production
# - 設定された変数:
# • app_location: deploy/frontend
# • api_location: api
# • output_location: .
# - 設定されたシークレット:
# • deploy_token: [設定済み]
1分ぐらいで実行されると思います。リザルトが表示されたら、「アプリURL」にアクセスしてみてください。Welcomeページが表示されれば成功です。
環境作成
無事デプロイが確認出来たら、次は認証プロバイダーの設定を行いましょう。GitHub > Settings > Developer Settingsにアクセスしてください。
リポジトリのSettingsではなくUser Settingsのほうにアクセスしてね!
OAuth Appsの作成を行いましょう。


設定項目としては、
設定項目 | 値 |
---|---|
Application name | 自由に決めてください |
Homepage URL | アプリURL |
Authorization callback URL | アプリURL/.auth/login/github/callback |
Enable Device Flow | True |
設定をするとClient IDとClient Secretが表示されます。そちらの値を.env
ファイルに入れて再度deploy.sh
を起動させましょう。
./infra/scripts/deploy.sh
# リザルトで以下が出たら成功!!
#📋 設定内容サマリー:
# - Azure Static Web App: swa-api-deploy-bicep
# - アプリURL: https://blue-bay-01b32fb00.2.azurestaticapps.net
# - GitHub環境: production
# - 設定された変数:
# • app_location: deploy/frontend
# • api_location: api
# • output_location: .
# - 設定されたシークレット:
# • deploy_token: [設定済み]
これで、SWAの環境変数にGITHUB_CLIENT_ID
/GITHUB_CLIENT_SECRET
が設定されました。もし不安な方はAzure Portalから確認しましょう。
CI/CD構築(GitHub Actions)
ここでは、GitHub Actionsの設定を行っています。前提条件として、GitHubリポジトリのEnviroment(production)に値が設定済みである必要があります。
Name | 説明 |
---|---|
AZURE_STATIC_WEB_APPS_API_TOKEN | SWAのデプロイトークン |
API_LOCATION | APIのディレクトリ |
APP_LOCATION | アプリのディレクトリ |
OUTPUT_LOCATION | ビルドファイルの位置 |
今回作成するファイルのディレクトリ構成は以下になります。
./.github
└── workflows
└── deploy.yaml
ワークフロー設計・アーキテクチャ
ワークフローでは、フロントビルドとデプロイを分離しています。フロントのビルドと並行してAPIのテストを実行するように設計しています。
- build-frontend: フロントエンドの並列ビルド
- test-api: APIの並列ビルド
- deploy-swa: アーティファクトを使用したデプロイ
視覚化した図を以下に示します。

全体ワークフロー
こちらのファイルをコピー&ペーストしてGitHub >Actionsのタブから手動実行しましょう。設定が完璧であれば、無事デプロイされるかと思います。
name: Build and Deploy to Azure Static Web Apps
on:
workflow_dispatch:
jobs:
# フロントエンドビルドジョブ
build-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js for Frontend
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: "frontend/package-lock.json"
- name: Install Frontend Dependencies
working-directory: ./frontend
run: npm ci
- name: Build Frontend
working-directory: ./frontend
run: npm run build
- name: Upload Frontend Build Artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: |
frontend/out/
retention-days: 1
# APIビルドジョブ
test-api:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js for API
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: "api/package-lock.json"
- name: Install API Dependencies
working-directory: ./api
run: npm install
- name: Build API
working-directory: ./api
run: npm run build
- name: Run API Tests
working-directory: ./api
run: npm run test
# Azure Static Web Appsデプロイジョブ
deploy-swa:
needs: [build-frontend, test-api]
runs-on: ubuntu-latest
environment: production
env:
APP_LOCATION: ${{ vars.APP_LOCATION || 'deploy/frontend' }}
OUTPUT_LOCATION: ${{ vars.OUTPUT_LOCATION || '.' }}
API_LOCATION: ${{ vars.API_LOCATION || 'api' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Download Frontend Artifacts
uses: actions/download-artifact@v4
with:
name: frontend-build
path: ./deploy/frontend
- name: Copy Static Web App Configuration
run: |
cp frontend/config/prod/staticwebapp.config.json deploy/frontend/
- name: check
working-directory: ./deploy/frontend/
run: |
ls -la
cat staticwebapp.config.json
- name: Deploy to Azure Static Web Apps
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
app_location: ${{ env.APP_LOCATION }}
api_location: ${{ env.API_LOCATION }}
output_location: "${{ env.OUTPUT_LOCATION }}"
skip_app_build: true
skip_api_build: false
フロントエンドビルドジョブ
こちらでは、フロントエンドのビルドを行い結果をArtifactとして保存しています。actions/setup-node@v4
を利用してnode_modulesをキャッシュしています。
build-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js for Frontend
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: "frontend/package-lock.json"
- name: Install Frontend Dependencies
working-directory: ./frontend
run: npm ci
- name: Build Frontend
working-directory: ./frontend
run: npm run build
- name: Upload Frontend Build Artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: |
frontend/out/
retention-days: 1
APIテストジョブ
こちらでは、バックエンドのビルドを行いテストを実行しています。今回はテストが失敗したら通知を行う等の対応を行っていませんが、本格運用ではここにテスト関連の処理をまとめて切り出すことができます。
actions/setup-node@v4
を利用してnode_modulesをキャッシュしています。
# APIビルドジョブ
test-api:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js for API
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: "api/package-lock.json"
- name: Install API Dependencies
working-directory: ./api
run: npm install
- name: Build API
working-directory: ./api
run: npm run build
- name: Run API Tests
working-directory: ./api
run: npm run test
SWAデプロイジョブ実装
こちらでは、以下の手順を順次実行しています。
- 「フロントエンドビルドジョブ」で作成した静的ファイルを復元
- 内部に本番環境用ルート定義を埋め込み
Azure/static-web-apps-deploy@v1
を使用してデプロイ- フロントエンドはビルド済みファイルをデプロイのみ任せて実行
- バックエンドはビルドとデプロイを任せて実行
# Azure Static Web Appsデプロイジョブ
deploy-swa:
needs: [build-frontend, test-api]
runs-on: ubuntu-latest
environment: production
env:
APP_LOCATION: ${{ vars.APP_LOCATION || 'deploy/frontend' }}
OUTPUT_LOCATION: ${{ vars.OUTPUT_LOCATION || '.' }}
API_LOCATION: ${{ vars.API_LOCATION || 'api' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Download Frontend Artifacts
uses: actions/download-artifact@v4
with:
name: frontend-build
path: ./deploy/frontend
- name: Copy Static Web App Configuration
run: |
cp frontend/config/prod/staticwebapp.config.json deploy/frontend/
- name: check
working-directory: ./deploy/frontend/
run: |
ls -la
cat staticwebapp.config.json
- name: Deploy to Azure Static Web Apps
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
app_location: ${{ env.APP_LOCATION }}
api_location: ${{ env.API_LOCATION }}
output_location: "${{ env.OUTPUT_LOCATION }}"
skip_app_build: true
skip_api_build: false
Copy Static Web App Configuration
では、本番環境用のstaticwebapp.config.json
を復元したフロントのビルド済みディレクトリにコピーしています。
Azure/static-web-apps-deploy@v1
では、skip_app_build
とskip_api_build
でアクション内のビルドプロセスを制御しています。そもそもこのアクションは、アプリの構成を自動で読み取りビルドとデプロイを行っています。フロントエンドでルーティングファイルの制御を行うためにフロントエンドはビルドをスキップしてデプロイだけで使用しています。
GitHub Actions効率化紹介
今回のワークフローでは、ビルド時間の短縮とリソース効率化を重視した設計を行いました。特に並列処理とキャッシュ戦略により、従来の逐次処理と比較して大幅な時間短縮を実現できます。
独立したNodeキャッシュ戦略
各ジョブで異なるcache-dependency-path
を指定することで、フロントエンドとAPIで独立したキャッシュを管理できます。これにより、一方の依存関係が変更されても他方のキャッシュが無効化されることを防げます。
# フロントエンドジョブでのキャッシュ設定
- name: Setup Node.js for Frontend
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: "frontend/package-lock.json"
# APIジョブでのキャッシュ設定
- name: Setup Node.js for API
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: "api/package-lock.json"
cache-dependency-path
を明示的に指定することで、GitHub Actionsはそのファイルのハッシュをキャッシュキーとして使用します。フロントエンドとAPIで異なるpackage-lock.jsonを参照することで、以下のメリットがあります:
- フロントエンドの依存関係のみ変更時:APIのキャッシュはそのまま利用
- APIの依存関係のみ変更時:フロントエンドのキャッシュはそのまま利用
- 両方変更時:それぞれ個別にキャッシュを再構築
フロントエンドジョブではnpm ci
を使用しています。これはCI環境向けの最適化されたコマンドで、package-lock.jsonを厳密に遵守し、高速インストールを実現します。
アーティファクト活用による並列処理最適化
アーティファクトを使用することで、ビルドジョブとデプロイジョブを完全に分離できます。これにより、デプロイ時にビルドを再実行する必要がなくなり、処理時間を大幅に短縮できます。
# ビルドジョブでのアーティファクト保存
- name: Upload Frontend Build Artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: |
frontend/out/
retention-days: 1
# デプロイジョブでのアーティファクト復元
- name: Download Frontend Artifacts
uses: actions/download-artifact@v4
with:
name: frontend-build
path: ./deploy/frontend
今回の構成では、build-frontend
とtest-api
が並列で実行されます。これにより、従来の逐次処理と比較して大幅な時間短縮が期待できます。
将来的なテストへの拡張性
段階的テスト導入への対応
現在のAPIテストジョブは、将来的なテスト拡張を見据えた設計になっています。
- name: Build API
working-directory: ./api
run: npm run build
- name: Run API Tests
working-directory: ./api
run: npm run test
テスト失敗時の処理拡張
本格運用時には、テスト失敗時の通知機能を追加できます。以下のような拡張が可能です:
- name: Run API Tests
working-directory: ./api
run: npm run test
continue-on-error: false # テスト失敗時にワークフローを停止
# 将来的な拡張例
- name: Notify Test Failure
if: failure()
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'APIテストが失敗しました。詳細はワークフローログを確認してください。'
})
並列テスト実行の利点
フロントエンドビルドとAPIテストを並列実行することで、以下のメリットがあります:
- 時間効率: 最も時間のかかる処理に全体時間が依存
- 早期発見: APIテストの失敗を早期に検出
- リソース効率: GitHub Actionsの同時実行枠を有効活用
シークレット・環境変数の効率的管理
GitHub Actionsでは、Secretsと環境変数を適切に分離して管理することが重要です。
environment: production
env:
APP_LOCATION: ${{ vars.APP_LOCATION || 'deploy/frontend' }}
OUTPUT_LOCATION: ${{ vars.OUTPUT_LOCATION || '.' }}
API_LOCATION: ${{ vars.API_LOCATION || 'api' }}
Secrets(機密情報)にはAZURE_STATIC_WEB_APPS_API_TOKEN
などのデプロイトークンを、Variables(環境固有設定)にはAPP_LOCATION
などのディレクトリ設定を使い分けます。
本番環境での動作確認
お疲れさまでした!ここまでで無事にAzure SWAのデプロイが完了しましたね。でも、ここからが本当の勝負です。実際に本番環境で全ての機能が期待通りに動作するかを確認していきましょう。
私も最初の頃は「ローカルで動いてるから大丈夫だろう」と思っていましたが、本番環境では思わぬ設定の違いでハマることがよくありました。特に認証周りは環境による差が出やすい部分なので、しっかりと検証していきますね。
もし何かうまく動作しない部分があった場合は、まずはブラウザの開発者ツールでエラーがないか確認して、必要に応じてAzure Portalでのログ確認も行ってみてください。認証周りは環境設定に依存する部分が多いので、OAuth Appの設定やSWAの環境変数も再度チェックしてみると良いでしょう。
未認証状態での動作確認
シークレットモードやプライベートブラウジングで本番URLにアクセスしてみてください。まずはトップページ(/
)から確認します。
期待される動作:
- ページ上部に「未認証状態」と表示される
- 「GitHubでログイン」ボタンが表示される
- 「サーバーからユーザー情報を取得」ボタンをクリックしても、ユーザー情報は取得できない(
isAuthenticated: false
が返される)
次に、保護されたページへの直接アクセスを試してみます。URLバーに /protected
を入力してアクセスしてください。
期待される動作:
- 自動的にGitHub認証画面にリダイレクトされる
- エラー画面が表示されることはない
これがAzure SWAの素晴らしいところですね。設定ファイル(staticwebapp.config.json
)で定義したルート保護が自動的に働いて、未認証ユーザーを認証フローに誘導してくれます。
GitHub認証の実行
「GitHubでログイン」ボタンをクリックして、実際の認証フローを確認します。
認証フローの流れ:
- ボタンクリックで
/.auth/login/github
にリダイレクト - GitHub認証画面が表示される
- GitHubでの認証完了後、元のページにリダイレクト
GitHub認証画面では、作成したOAuth Appの情報が表示されます。App nameやAuthorization callback URLが正しく設定されていることを確認してください。
認証完了後は、トップページに戻ってきて以下のような変化が確認できるはずです:
- ページ上部に「認証済み」と表示される
- ユーザー情報(GitHubのユーザー名など)が表示される
- 「保護されたページへ」「ログアウト」ボタンが表示される
セッション管理の確認
認証状態でページをリロードしても、認証状態が維持されることを確認してください。これはAzure SWAが内部的にセッション管理を行っているためです。
また、別タブで同じサイトを開いても、認証状態が共有されることも確認してみましょう。
API動作テスト
認証が正しく動作することが確認できたら、次はAPIの動作を確認していきます。
user-infoエンドポイントのテスト
認証済み状態で「サーバーからユーザー情報を取得」ボタンをクリックしてください。
期待される結果:
{
"userId": "github|あなたのGitHubユーザーID",
"name": "あなたのGitHubユーザー名",
"email": "あなたのGitHubユーザー名",
"provider": "github",
"roles": ["authenticated"],
"isAuthenticated": true
}
これで、Azure SWAからAzure Functionsに認証ヘッダー(x-ms-client-principal
)が正しく送信されて、API側で適切にデコードされていることが確認できます。
ブラウザの開発者ツールのネットワークタブでも確認してみてください。/api/user-info
へのリクエストが200で成功していて、上記のようなレスポンスが返っていることが確認できるはずです。
未認証状態でのuser-infoテスト
シークレットモードで未認証状態でも同じボタンをクリックしてみてください。
期待される結果:
{
"userId": null,
"name": null,
"email": null,
"provider": null,
"roles": [],
"isAuthenticated": false
}
このAPIは認証状態を問わずアクセス可能な設計になっているので、未認証でもエラーにならずに適切なレスポンスが返ることを確認できます。
protected-dataエンドポイントのテスト
保護されたページ(/protected
)にアクセスして、「保護されたデータを取得」ボタンをクリックしてください。
期待される結果:
{
"userId": "github|あなたのGitHubユーザーID",
"message": "こんにちは、ユーザーgithub|あなたのGitHubユーザーIDさん!",
"timestamp": "2024-12-15T15:45:30.000Z",
"userNumber": 数値
}
userNumber
は、ユーザーIDをベースにした簡単なシード値から生成される0-999の範囲の数値です。同じユーザーなら常に同じ値が返されることも確認してみてください。
未認証でのprotected-dataアクセステスト
これは少し技術的なテストですが、直接APIエンドポイントにアクセスしてみましょう。
シークレットモードで https://あなたのサイトURL/api/protected-data
に直接アクセスしてください。
期待される動作:
- 401エラーが返される
- または、Azure SWAのルート保護により認証画面にリダイレクトされる
実際には、SWAの設定によってはAPI直接アクセスも認証フローに誘導される可能性があります。どちらの動作でも、未認証ユーザーがデータにアクセスできないことが重要です。
セキュリティチェック
最後に、セキュリティ面での動作を確認していきます。
ルート保護の確認
未認証状態で以下のURLに直接アクセスしてみてください:
/protected
/protected/任意のパス
どちらも認証画面にリダイレクトされることを確認してください。これは staticwebapp.config.json
で設定した以下のルールが動作しているためです:
{
"route": "/protected/*",
"allowedRoles": ["authenticated"]
}
認証情報の適切な伝達
開発者ツールのネットワークタブで、認証済み状態でのAPI呼び出しを確認してください。
重要なポイント:
- API呼び出し時に、フロントエンド側で特別な認証ヘッダーを設定していない
- Azure SWAが自動的に
x-ms-client-principal
ヘッダーを付与している - このヘッダーは開発者ツールでは見えないが、サーバー側では受信できている
これがAzure SWAの大きなメリットの一つですね。フロントエンド側で複雑な認証処理を書く必要がなく、SWAが自動的に認証情報をバックエンドに伝達してくれます。
セッション継続性の確認
認証済み状態で以下を試してみてください:
- ページのリロード
- 別タブでの同サイトアクセス
- ブラウザを閉じて再度開く(セッションストレージのテスト)
通常のWebアプリケーションと同様に、ブラウザを閉じるまでは認証状態が維持されることを確認してください。
ログアウト機能の確認
「ログアウト」ボタンをクリックして、正常にログアウトできることを確認してください。
期待される動作:
/.auth/logout
にリダイレクト- 認証状態がクリアされる
- トップページに戻り、未認証状態の表示になる
お疲れさまでした!これで本番環境での動作確認は完了です。すべての機能が期待通りに動作していれば、Azure SWA + Next.js + Azure Functionsの認証統合システムが正常に稼働していることが確認できました。
リソースのクリーナップ
今回はAzure SWAをStandardプランで使用しています。こちらは動かしていれば、課金が走るようになっています。検証が確認できたら、心苦しいですがクリーナップしましょう。
Azure Portalからの削除でもよいのですが、せっかくなのでクリーナップ用のbashスクリプトを作成しました。bashスクリプト化しておくことのメリットはコマンド一つで環境の作成・削除ができる点です。GitHubのOAuth Appsの設定とEnvironmentの作成は手動ですが、完全自動化ではないですけどね…
クリーナップスクリプト
#!/bin/bash
# エラー時に停止
set -e
log_info() { echo -e "\033[1;34m$1\033[0m"; }
log_success() { echo -e "\033[1;32m$1\033[0m"; }
log_warning() { echo -e "\033[1;33m$1\033[0m"; }
log_error() { echo -e "\033[1;31m$1\033[0m"; }
CONFIG_FILE="$(git rev-parse --show-toplevel)/.env" 2>/dev/null || CONFIG_FILE=".env"
if [ -f "$CONFIG_FILE" ]; then
log_info "📁 設定ファイル読み込み: $CONFIG_FILE"
source "$CONFIG_FILE"
else
log_error "⚠️ 設定ファイルが見つかりません: $CONFIG_FILE"
log_error " .env.example をコピーして設定してください"
exit 1
fi
# 必要な環境変数の確認
required_vars=("RESOURCE_GROUP_NAME" "STATIC_WEB_APP_NAME")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
log_error "エラー: 環境変数 $var が設定されていません"
exit 1
fi
done
log_info "🎯 削除対象: $STATIC_WEB_APP_NAME (リソースグループ: $RESOURCE_GROUP_NAME)"
# Azure CLIがインストールされているかチェック
if ! command -v az &> /dev/null; then
log_error "Azure CLI (az) がインストールされていません"
log_error "インストール方法: https://docs.microsoft.com/cli/azure/install-azure-cli"
exit 1
fi
# Azure CLIの認証チェック
if ! az account show &> /dev/null; then
log_error "Azure CLIが認証されていません"
log_error "認証方法: az login"
exit 1
fi
# リソースグループの存在確認
log_info ""
log_info "📦 リソースグループの確認..."
if ! az group show --name $RESOURCE_GROUP_NAME &> /dev/null; then
log_warning "⚠️ リソースグループ '$RESOURCE_GROUP_NAME' が見つかりません"
log_warning " 削除する必要がありません"
exit 0
else
log_success "✅ リソースグループ確認: $RESOURCE_GROUP_NAME"
fi
# Static Web Appリソースの存在確認
log_info ""
log_info "🔍 Azure Static Web App リソースを確認中..."
# 指定されたリソースの存在確認
if az staticwebapp show \
--name "$STATIC_WEB_APP_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--output none 2>/dev/null; then
# リソース詳細の取得
SWA_DETAILS=$(az staticwebapp show \
--name "$STATIC_WEB_APP_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--query "{name:name, defaultHostname:defaultHostname, sku:sku.name, location:location}" \
-o json)
log_success "✅ 削除対象リソースが見つかりました:"
echo "$SWA_DETAILS" | jq -r '" • 名前: \(.name)"'
echo "$SWA_DETAILS" | jq -r '" • URL: \(.defaultHostname)"'
echo "$SWA_DETAILS" | jq -r '" • プラン: \(.sku)"'
echo "$SWA_DETAILS" | jq -r '" • リージョン: \(.location)"'
RESOURCE_EXISTS=true
else
log_warning "⚠️ 指定されたAzure Static Web App '$STATIC_WEB_APP_NAME' が見つかりません"
log_warning " リソースグループ: $RESOURCE_GROUP_NAME"
log_info "削除する必要がありません"
exit 0
fi
# 確認プロンプト
log_warning ""
log_warning "⚠️ 以下の操作を実行します:"
log_warning " 1. Azure Static Web App '$STATIC_WEB_APP_NAME' の削除"
log_warning " 2. リソースグループ '$RESOURCE_GROUP_NAME' は保持します"
log_warning " 3. GitHub環境設定は保持します"
log_warning ""
read -p "続行しますか? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "操作をキャンセルしました"
exit 0
fi
# === Azure Static Web App リソースの削除 ===
log_info ""
log_info "🗑️ Azure Static Web App リソースを削除中..."
log_info "🔧 削除中: $STATIC_WEB_APP_NAME"
if az staticwebapp delete \
--name "$STATIC_WEB_APP_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--yes 2>/dev/null; then
log_success "✅ 削除完了: $STATIC_WEB_APP_NAME"
else
log_error "❌ 削除失敗: $STATIC_WEB_APP_NAME"
exit 1
fi
log_success "✅ Azure Static Web App リソースの削除が完了しました"
# === 削除完了サマリー ===
log_success ""
log_success "🎉 クリーンアップが完了しました!"
log_success ""
log_success "📋 実行内容サマリー:"
log_success " ✅ Azure Static Web App削除: $STATIC_WEB_APP_NAME"
log_success " ✅ リソースグループ保持: $RESOURCE_GROUP_NAME"
log_success " ✅ GitHub環境設定保持: 変更なし"
log_success ""
log_success "📌 注意事項:"
log_success " • リソースグループ '$RESOURCE_GROUP_NAME' は保持されています"
log_success " • GitHub環境設定はそのまま残っているので、再デプロイ可能です"
log_success ""
log_success "🚀 Azure リソースのクリーンアップ完了!"
クリーナップスクリプトの実行
作成時と同じく、環境変数を .env
ファイルから読み込んで削除処理を実行します。既にdeployスクリプト実行時に .env
ファイルは設定済みのはずなので、そのまま使用できます。
chmod +x ./infra/scripts/cleanup.sh
./infra/scripts/cleanup.sh
実行すると、以下のような確認が表示されます:
🎯 削除対象: swa-api-deploy-bicep (リソースグループ: swa-bicep-test)
⚠️ 以下の操作を実行します:
1. Azure Static Web App 'swa-api-deploy-bicep' の削除
2. リソースグループ 'swa-bicep-test' は保持します
3. GitHub環境設定は保持します
続行しますか? (y/N):
y
を入力すると削除処理が開始されます。
削除される内容と保持される内容
削除されるもの:
- Azure Static Web Appリソース
- 関連するManaged Functions
- カスタム認証プロバイダー設定
保持されるもの:
- リソースグループ(他のリソースがある可能性を考慮)
- GitHub Environment設定(再デプロイ時に再利用可能)
- GitHub OAuth App(再利用可能)
完全削除を希望する場合
もし完全にクリーンな状態に戻したい場合は、以下も手動で削除してください:
Azure側:
- リソースグループ全体(他にリソースがない場合)
- 必要に応じて Azure CLI の認証情報もクリア
GitHub側:
- OAuth App(Settings > Developer settings > OAuth Apps から削除)
- Environment設定(リポジトリの Settings > Environments から削除)
これで、課金を気にすることなく安心して検証を終了できますね。再度環境を作りたい場合は、deployスクリプトを実行すれば簡単に復旧できるのも、Infrastructure as Codeのメリットです!
まとめ
今回は Azure Static Web Apps と Azure Functions を使った Next.js アプリケーションの認証 API 統合について、DevContainer での開発環境構築から本番デプロイまでの完全なワークフローを詳しく見てきました。
技術統合のポイント
この構成の最大の魅力は、フロントエンドとバックエンドを統一プラットフォームで管理できる点にあります。Azure SWA が提供する認証機能と Azure Functions の API を組み合わせることで、複雑な認証フローを比較的シンプルに実装できました。特に x-ms-client-principal
ヘッダーを使った認証情報の受け渡しは、SWA 独自の仕組みとして理解しておくと実装がスムーズになります。
開発体験の向上
DevContainer を使った開発環境は、チーム開発での環境差異を解消する大きなメリットがありました。Azure CLI や SWA CLI、Functions Core Tools がすべて統一環境で利用でき、ローカルでの統合テストも swa start
コマンド一つで実行できる点は、開発効率を大幅に向上させます。
運用面での実用性
GitHub Actions を使った CI/CD パイプラインでは、フロントエンドビルドと API テストを並列実行することで、デプロイ時間の短縮を実現しました。また、Bicep を使った IaC により、インフラの再現性と管理性も確保できています。Standard プランは課金が発生しますが、カスタム認証プロバイダーや SLA が必要な本格的なプロジェクトには必須の選択肢です。
コメント
ここまで出来たら、Azure Static Web Appsの基本は一通り理解できたかともいます。あとは運用の要望に合わせてチューニングするエンタープライズの領域に入っていきます。Bicepを使用して人間の手が入る可能性を減らせば減らすほど、問題が少なくなると思うので積極的にスクリプト化できるものはしていきましょう!
長々とした記事ですがお疲れ様でした!!