メインコンテンツまでスキップ

Azure Static Web Apps

Azure Static Web Appsは、GitHubやAzure DevOpsからコードを自動的にビルドし、Azureにデプロイするフルマネージドサービスです。静的コンテンツをグローバルに配信し、Azure FunctionsによるサーバーレスAPIをシームレスに統合できます。

概要

Azure Static Web Apps とは

Azure Static Web Appsは、モダンなWebアプリケーション開発のためのホスティングサービスで、以下の特徴を持ちます:

  • 自動ビルド・デプロイ: GitHubまたはAzure DevOpsと統合し、プッシュ時に自動的にビルド・デプロイ
  • グローバルCDN: 静的コンテンツを世界中のエッジロケーションから配信
  • 統合API: Azure Functionsを使ったサーバーレスAPIの自動統合
  • 組み込み認証: 複数の認証プロバイダーを設定なしで利用可能
  • カスタムドメイン・SSL: 無料のSSL証明書とカスタムドメインのサポート
  • プレビュー環境: プルリクエストごとに自動的にステージング環境を作成

アーキテクチャ

┌─────────────────────────────────────────────────────┐
│ GitHub / Azure DevOps │
│ (ソースコード) │
└──────────────────┬──────────────────────────────────┘
│ Push/PR

┌─────────────────────────────────────────────────────┐
│ GitHub Actions / Azure Pipelines │
│ (自動ビルド・デプロイ) │
└──────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│ Azure Static Web Apps │
│ ┌──────────────────┐ ┌───────────────────┐ │
│ │ 静的コンテンツ │ │ Azure Functions │ │
│ │ (HTML/JS/CSS) │ │ (API) │ │
│ └──────────────────┘ └───────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 認証・認可 │ │
│ │ (GitHub, Azure AD, Twitter, Google等) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘


ユーザー (世界中のエッジから配信)

主な機能

1. 静的コンテンツのホスティング

  • HTML、CSS、JavaScript、画像などの静的ファイルをホスト
  • React、Angular、Vue.js、Blazor WebAssemblyなどのSPA(Single Page Application)に対応
  • Next.js、Gatsby、Hugoなどの静的サイトジェネレーターをサポート

2. サーバーレスAPI統合

  • /api パスで Azure Functions を自動的に統合
  • Node.js、Python、.NET、Javaなどをサポート
  • 静的コンテンツと同じドメインで提供されるため、CORSの問題が解消

3. 組み込み認証

以下の認証プロバイダーがデフォルトで利用可能:

  • Azure Active Directory (AAD)
  • GitHub
  • Twitter
  • Google
  • Facebook

認証エンドポイント:

  • /.auth/login/<provider>: ログイン
  • /.auth/logout: ログアウト
  • /.auth/me: ユーザー情報取得

4. カスタムドメインとSSL

  • 無料のSSL証明書(Let's Encrypt)を自動発行
  • カスタムドメインの設定が簡単
  • Apexドメインとサブドメインの両方をサポート

料金プラン

機能Free プランStandard プラン
帯域幅100 GB/月100 GB/月 + 超過分課金
ストレージ0.5 GB2 GB
カスタムドメイン2個5個
ステージング環境3個10個
Azure Functions無料枠のみ専用プラン可能
SLAなし99.95%
認証ユーザー数無制限無制限

Azure Functionsを使ったカスタムロール実装

Azure Static Web Appsでは、Azure Functionsを使ってカスタムの認証ロジックやロール割り当てを実装できます。

カスタム認証の仕組み

  1. ユーザーが認証プロバイダーでログイン
  2. /.auth/login/<provider>/callback にリダイレクト
  3. Azure Functions で /api/auth/roles または カスタムエンドポイントを実装
  4. ユーザー情報に基づいてカスタムロールを返却
  5. 以降のリクエストでロール情報が利用可能

カスタムロールAPIの実装例 (Node.js)

プロジェクトディレクトリに api フォルダを作成し、以下のようなAzure Functionを実装します。

api/GetUserRoles/index.js

module.exports = async function (context, req) {
// 認証されたユーザー情報を取得
const userInfo = req.headers['x-ms-client-principal'];

if (!userInfo) {
context.res = {
status: 401,
body: { error: "認証が必要です" }
};
return;
}

// Base64デコード
const decoded = Buffer.from(userInfo, 'base64').toString('utf-8');
const user = JSON.parse(decoded);

// ユーザー情報に基づいてロールを決定
const roles = determineUserRoles(user);

context.res = {
status: 200,
body: {
userId: user.userId,
userDetails: user.userDetails,
roles: roles
}
};
};

function determineUserRoles(user) {
const roles = ['authenticated'];

// メールアドレスに基づいてロールを割り当て
if (user.userDetails && user.userDetails.endsWith('@company.com')) {
roles.push('employee');
}

// 特定のユーザーIDに管理者ロールを付与
const adminIds = [
'github|123456',
'aad|abcd-1234-efgh-5678'
];

if (adminIds.includes(user.userId)) {
roles.push('admin');
}

// データベースからロール情報を取得する例
// const dbRoles = await fetchRolesFromDatabase(user.userId);
// roles.push(...dbRoles);

return roles;
}

api/GetUserRoles/function.json

{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"],
"route": "auth/roles"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}

データベースと連携したロール管理

実際のアプリケーションでは、ロール情報をデータベースで管理することが一般的です。

const { CosmosClient } = require('@azure/cosmos');

// Cosmos DBクライアントの初期化
const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING);
const database = client.database('UsersDB');
const container = database.container('UserRoles');

async function getUserRolesFromDatabase(userId) {
try {
const { resource } = await container
.item(userId, userId)
.read();

return resource ? resource.roles : ['authenticated'];
} catch (error) {
console.error('ロール取得エラー:', error);
return ['authenticated']; // デフォルトロール
}
}

module.exports = async function (context, req) {
const userInfo = req.headers['x-ms-client-principal'];

if (!userInfo) {
context.res = { status: 401 };
return;
}

const decoded = Buffer.from(userInfo, 'base64').toString('utf-8');
const user = JSON.parse(decoded);

// データベースからロールを取得
const roles = await getUserRolesFromDatabase(user.userId);

context.res = {
status: 200,
body: {
userId: user.userId,
roles: roles
}
};
};

staticwebapp.config.json による認証設定

staticwebapp.config.json ファイルをプロジェクトのルートに配置することで、ルーティング、認証、認可を詳細に設定できます。

基本的な設定ファイル

{
"routes": [
{
"route": "/admin/*",
"allowedRoles": ["admin"]
},
{
"route": "/api/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/public/*",
"allowedRoles": ["anonymous"]
}
],
"navigationFallback": {
"rewrite": "/index.html",
"exclude": ["/images/*", "/css/*", "/api/*"]
},
"responseOverrides": {
"401": {
"rewrite": "/login.html",
"statusCode": 302
},
"403": {
"rewrite": "/forbidden.html"
},
"404": {
"rewrite": "/404.html"
}
}
}

主要な設定項目

1. ルートとロールベースのアクセス制御

{
"routes": [
{
"route": "/admin/*",
"allowedRoles": ["admin"],
"statusCode": 403
},
{
"route": "/profile",
"allowedRoles": ["authenticated"]
},
{
"route": "/login",
"allowedRoles": ["anonymous"]
}
]
}
  • route: アクセス制御を適用するパス(ワイルドカード * 使用可能)
  • allowedRoles: アクセスを許可するロール(配列)
    • "anonymous": 未認証ユーザーを含むすべてのユーザー
    • "authenticated": 認証済みユーザーすべて
    • カスタムロール: "admin", "editor", "employee" など
  • statusCode: アクセス拒否時のHTTPステータスコード(オプション)

2. 認証プロバイダーのカスタマイズ

{
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/<tenant-id>/v2.0",
"clientIdSettingName": "AAD_CLIENT_ID",
"clientSecretSettingName": "AAD_CLIENT_SECRET"
},
"login": {
"loginParameters": ["scope=openid profile email"]
}
},
"customOpenIdConnectProviders": {
"myCustomProvider": {
"registration": {
"clientIdSettingName": "CUSTOM_CLIENT_ID",
"clientCredential": {
"clientSecretSettingName": "CUSTOM_CLIENT_SECRET"
},
"openIdConnectConfiguration": {
"wellKnownOpenIdConfiguration": "https://provider.com/.well-known/openid-configuration"
}
},
"login": {
"nameClaimType": "name",
"scopes": ["openid", "profile", "email"]
}
}
}
}
}
}

3. リダイレクトルール

{
"routes": [
{
"route": "/old-page",
"redirect": "/new-page",
"statusCode": 301
},
{
"route": "/external",
"redirect": "https://example.com",
"statusCode": 302
}
]
}

4. ヘッダーのカスタマイズ

{
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src 'self'"
},
"routes": [
{
"route": "/api/*",
"headers": {
"Cache-Control": "no-cache, no-store, must-revalidate",
"X-Custom-Header": "Custom-Value"
}
}
]
}

5. MIMEタイプの設定

{
"mimeTypes": {
".json": "application/json",
".wasm": "application/wasm",
".csv": "text/csv"
}
}

実践的な設定例

マルチテナントアプリケーション

{
"routes": [
{
"route": "/admin/*",
"allowedRoles": ["admin", "superadmin"]
},
{
"route": "/tenant/*/dashboard",
"allowedRoles": ["authenticated"]
},
{
"route": "/api/tenant/*",
"allowedRoles": ["tenant-user", "tenant-admin"]
}
],
"navigationFallback": {
"rewrite": "/index.html"
},
"responseOverrides": {
"401": {
"redirect": "/.auth/login/aad",
"statusCode": 302
},
"403": {
"rewrite": "/access-denied.html"
}
},
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"Strict-Transport-Security": "max-age=31536000"
}
}

SPA(Single Page Application)用の設定

{
"routes": [
{
"route": "/api/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/admin",
"allowedRoles": ["admin"],
"rewrite": "/index.html"
}
],
"navigationFallback": {
"rewrite": "/index.html",
"exclude": ["/assets/*", "/images/*", "/*.{css,js,json,ico,png,jpg,svg}"]
},
"responseOverrides": {
"401": {
"redirect": "/.auth/login/github",
"statusCode": 302
}
}
}

ベストプラクティス

1. セキュリティ

  • 最小権限の原則: ロールには必要最小限の権限のみを付与
  • 機密情報の保護: APIキーや接続文字列は環境変数を使用
  • HTTPSの強制: すべての通信でHTTPSを使用(デフォルトで有効)
  • セキュリティヘッダー: CSP、X-Frame-Optionsなどを適切に設定
{
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
}
}

2. パフォーマンス

  • キャッシュ戦略: 静的アセットに適切なキャッシュヘッダーを設定
  • ファイルサイズの最適化: 画像やJavaScriptの圧縮・最適化
  • CDNの活用: Azure Static Web Appsは自動的にグローバルCDNを使用
  • APIの効率化: 不要なデータ転送を避け、レスポンスを最小化
{
"routes": [
{
"route": "/assets/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
},
{
"route": "/api/*",
"headers": {
"Cache-Control": "no-cache, no-store, must-revalidate"
}
}
]
}

3. デプロイとCI/CD

  • 環境変数の管理: 環境ごとに異なる設定はAzure Portalで管理
  • ステージング環境の活用: PRごとに自動作成されるプレビュー環境でテスト
  • 段階的ロールアウト: 本番環境への変更は段階的に適用
  • ロールバック計画: 問題発生時の迅速なロールバック手順を準備

4. 監視とログ

  • Application Insights: アプリケーションのパフォーマンスとエラーを監視
  • ログ記録: Azure Functions内で適切なログを記録
  • アラート設定: 異常なトラフィックやエラー率の上昇を検知
// Azure Functions内でのログ記録例
const appInsights = require('applicationinsights');
appInsights.setup(process.env.APPINSIGHTS_INSTRUMENTATIONKEY);
const client = appInsights.defaultClient;

module.exports = async function (context, req) {
try {
// 処理
client.trackEvent({
name: 'UserRoleAssigned',
properties: { userId: user.userId, role: 'admin' }
});
} catch (error) {
client.trackException({ exception: error });
context.log.error('エラー発生:', error);
}
};

トラブルシューティング

よくある問題と解決方法

1. 認証後にリダイレクトされない

原因: post_login_redirect_uri の設定が不適切

解決策:

// ログインリンクに正しいリダイレクトURIを指定
<a href="/.auth/login/github?post_login_redirect_uri=/dashboard">
GitHubでログイン
</a>

2. APIへのアクセスが403エラー

原因: allowedRoles の設定が適切でない

解決策: staticwebapp.config.json を確認

{
"routes": [
{
"route": "/api/*",
"allowedRoles": ["authenticated"] // anonymousから変更
}
]
}

3. カスタムロールが反映されない

原因: ロール割り当てAPIが正しく呼び出されていない

解決策:

  • ブラウザの開発者ツールでネットワークタブを確認
  • /.auth/me エンドポイントで現在のロール情報を確認
  • Azure Functionsのログを確認してエラーをチェック
# Azure CLIでログを確認
az staticwebapp functions logs show --name <app-name> --resource-group <rg-name>

4. ローカル開発での認証テスト

解決策: Azure Static Web Apps CLI を使用

# インストール
npm install -g @azure/static-web-apps-cli

# ローカルで実行
swa start ./build --api-location ./api

# 認証エミュレーション付きで実行
swa start ./build --api-location ./api --app-devserver-url=http://localhost:3000

デバッグ手法

  1. ユーザー情報の確認

    // フロントエンドで現在のユーザー情報を取得
    fetch('/.auth/me')
    .then(res => res.json())
    .then(data => console.log('User info:', data));
  2. ロール情報の確認

    // Azure Function内でユーザー情報をログ出力
    const userInfo = req.headers['x-ms-client-principal'];
    const decoded = Buffer.from(userInfo, 'base64').toString('utf-8');
    context.log('User details:', JSON.parse(decoded));
  3. 設定ファイルの検証

    • Azure PortalでStatic Web Appの「Configuration」を確認
    • デプロイログでビルドエラーをチェック
    • GitHub Actionsのログを確認

まとめ

Azure Static Web Appsは、モダンなWebアプリケーション開発に最適なプラットフォームです:

  • シンプルなデプロイ: GitHubと統合した自動デプロイ
  • スケーラブル: グローバルCDNによる高速配信
  • セキュア: 組み込み認証とカスタムロールによる柔軟なアクセス制御
  • コスト効率: 無料プランでも充実した機能
  • 開発者体験: ローカル開発からプレビュー環境まで充実したツールチェーン

staticwebapp.config.json とAzure Functionsを組み合わせることで、エンタープライズレベルの認証・認可システムを構築できます。

参考リンク