Azure デプロイで人生が楽になった話:Static Web Apps と Functions の魔法

46分で読めます
エンハンスド技術チーム
AzureStatic Web AppsFunctionsデプロイCI/CD
Azure Static Web AppsとAzure Functionsを使って、デプロイの恐怖から解放された実体験をお話しします。

Azure デプロイで人生が楽になった話:Static Web Apps と Functions の魔法

はじめに:「デプロイが怖い」から「デプロイが楽しい」へ

「また本番デプロイか...胃が痛い」 「手順書どこだっけ?前回と同じだよね?」 「あれ、環境変数設定したっけ...?」

3年前の私です。デプロイの日は朝から憂鬱で、金曜日のデプロイなんて絶対NGでした。

でも今は違います。プルリクエストをマージするだけで本番デプロイ完了。しかも、失敗したら自動でロールバック。Azure Static Web Apps と Azure Functions のおかげで、デプロイが「イベント」から「日常」になりました。

今回は、どうやってデプロイ地獄から抜け出したのか、実体験をお話しします。

Azure Static Web Apps との出会い

「これ、本当に無料なの?」

最初にAzure Static Web Appsを知った時の正直な感想です。

衝撃的だった機能:

  • GitHubにプッシュするだけで自動デプロイ
  • プルリクごとにプレビュー環境が自動生成
  • SSL証明書も勝手に更新される
  • 世界中のCDNで爆速配信
  • なのに無料プランがある!

「こんなの使わない理由がない」と思い、早速試してみました。

初めてのデプロイで感動した瞬間

git push origin main

たったこれだけ。5分後にはもう世界中からアクセスできる状態に。

「え、もうデプロイ終わったの?」

今まで2時間かけてやってた作業が5分で終わる。この瞬間、私のデプロイ人生が変わりました。

実際にやってみた:ゼロから本番デプロイまで30分

「本当に30分でできるの?」→ できました!

半信半疑で始めたセットアップ。タイマーをセットして挑戦。

10分経過:プロジェクト作成

# Next.jsプロジェクトを作成
npx create-next-app@latest my-azure-app --typescript --tailwind --app

# 必要なパッケージを追加(これが地味に大事)
npm install @azure/functions @azure/storage-blob

20分経過:フォルダ構成を整理

私の黄金フォルダ構成/
├── src/          # フロントエンド
├── api/          # Azure Functions(ここがミソ!)
│   ├── GetData/
│   └── SaveData/
└── staticwebapp.config.json  # 魔法の設定ファイル

30分経過:デプロイ完了!

本当に30分でできて、自分でも驚きました。

つまずきポイントと解決法

失敗1:「api」フォルダの場所を間違えた 最初、src/apiに作ってしまい動かず...ルート直下に作る必要がありました。

失敗2:Node.jsのバージョン ローカルは18、Azureは16で動かず。package.jsonで指定して解決。

Azure Functions:バックエンドが一瞬で完成する魔法

「サーバー立てなくていいの?」の衝撃

従来のバックエンド開発:

  • EC2インスタンス起動して...
  • Node.js環境構築して...
  • PM2でプロセス管理して...
  • Nginxでリバースプロキシ設定して...

Azure Functions:

// api/HelloWorld/index.ts
export default async function (context, req) {
  return {
    body: "Hello World!"
  };
}

たったこれだけ!しかも自動でスケールする!

実際に作ってみた:データ取得API

最初は「Hello World」で感動してましたが、実務で使えるAPIも簡単に作れました:

// api/GetUserData/index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions"

const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  // リクエストパラメータを取得
  const userId = req.query.userId || req.body?.userId
  
  if (!userId) {
    context.res = {
      status: 400,
      body: "userId is required"
    }
    return
  }
  
  try {
    // ここでデータベースアクセスなど
    // 今回は簡単な例
    const userData = {
      id: userId,
      name: "山田太郎",
      lastLogin: new Date().toISOString()
    }
    
    context.res = {
      status: 200,
      body: userData
    }
  } catch (error) {
    context.log.error('Error:', error)
    context.res = {
      status: 500,
      body: "Internal Server Error"
    }
  }
}

export default httpTrigger

驚いたポイント:

  • ローカルでもAzureでも同じコードが動く
  • 自動でHTTPS対応
  • ログも勝手に収集される
  • 使った分だけの課金(ほぼ無料)

実践編:本格的な注文処理システムを作ってみた

「Hello Worldは動いたけど、実際の業務で使えるの?」

そんな疑問を持った私は、ECサイトの注文処理システムを作ってみました。

作ったもの:

  • 注文受付API
  • 在庫確認
  • データベース保存
  • メール通知

驚いたのは、これら全部をFunctions一つで実現できたこと!

// api/CreateOrder/index.ts
// シンプルな注文処理の例
const httpTrigger: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  const order = req.body
  
  // 1. 入力チェック(これ大事!)
  if (!order.items || order.items.length === 0) {
    context.res = {
      status: 400,
      body: { error: "商品を選択してください" }
    }
    return
  }
  
  // 2. 在庫確認(実際はDBを見る)
  const hasStock = await checkInventory(order.items)
  if (!hasStock) {
    context.res = {
      status: 400,
      body: { error: "在庫が不足しています" }
    }
    return
  }
  
  // 3. 注文を保存
  const orderId = await saveOrder(order)
  
  // 4. メール送信(非同期でOK)
  context.bindings.sendEmail = {
    to: order.email,
    subject: "ご注文ありがとうございます",
    body: `注文番号: ${orderId}`
  }
  
  context.res = {
    status: 201,
    body: { 
      orderId,
      message: "注文を受け付けました" 
    }
  }
}

開発して分かったコツ:

  1. エラーハンドリングは丁寧に 最初は雑にやってたら、本番で謎のエラーが...ちゃんとログ出力しましょう。

  2. 環境変数は必須 ローカルと本番でDB接続先を変える。当たり前だけど大事。

  3. 非同期処理を活用 メール送信とか時間かかる処理は、レスポンス返してから実行。

失敗談: 最初、全処理を同期的にやってたら、タイムアウトエラーの嵐。非同期処理の重要性を痛感しました。

フロントエンドとの連携:「つながった!」の感動

APIクライアント作成で学んだこと

最初は「fetchでいいじゃん」と思ってました。でも、実際に使い始めると...

Before(素のfetch):

// 毎回これ書くの面倒...
const response = await fetch('/api/products', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(data)
})

if (!response.ok) {
  // エラー処理も毎回...
}

After(APIクライアント):

// 一度作れば使い回せる!
const product = await productApi.create({
  name: "新商品",
  price: 1000
})

この差は大きい!開発速度が3倍は違います。

実際に作ったAPIクライアント

// src/lib/api.ts
// シンプルだけど強力なAPIクライアント
class ApiClient {
  private baseUrl: string
  
  constructor() {
    // 環境変数で切り替え(これ重要!)
    this.baseUrl = process.env.NEXT_PUBLIC_API_URL || '/api'
  }
  
  async request(endpoint: string, options: RequestInit = {}) {
    const url = `${this.baseUrl}${endpoint}`
    
    try {
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      })
      
      if (!response.ok) {
        throw new Error(`API Error: ${response.statusText}`)
      }
      
      return response.json()
    } catch (error) {
      console.error('API call failed:', error)
      throw error
    }
  }
  
  // 便利メソッド
  get(endpoint: string) {
    return this.request(endpoint)
  }
  
  post(endpoint: string, data: any) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }
}

// 使いやすいAPIを export
export const api = new ApiClient()

// 実際の使い方
export const productApi = {
  getAll: () => api.get('/products'),
  getById: (id: string) => api.get(`/products/${id}`),
  create: (data: any) => api.post('/products', data),
}

開発のコツ:

  1. 型定義は必須:TypeScriptの恩恵を最大限に
  2. エラーハンドリング:ユーザーに優しいメッセージを
  3. ローディング状態:「処理中...」の表示は必須

Reactコンポーネント:データが画面に表示された瞬間

「APIはできた。でも画面に表示されない...」

初心者あるあるですよね。私も最初はuseEffectの使い方で悩みました。

最初の失敗コード:

// 無限ループになった...
useEffect(() => {
  loadProducts()
}) // 依存配列忘れた!

改善後の実装:

// src/app/products/page.tsx
'use client'

import { useState, useEffect } from 'react'
import { productApi } from '@/lib/api'

export default function ProductsPage() {
  const [products, setProducts] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    // データ取得関数
    const loadProducts = async () => {
      try {
        setLoading(true)
        const data = await productApi.getAll()
        setProducts(data)
      } catch (err) {
        setError('読み込みに失敗しました')
        console.error(err)
      } finally {
        setLoading(false)
      }
    }
    
    loadProducts()
  }, []) // 空配列で初回のみ実行
  
  // ローディング中
  if (loading) {
    return <div className="text-center py-8">読み込み中...</div>
  }
  
  // エラー時
  if (error) {
    return (
      <div className="text-center py-8 text-red-500">
        {error}
      </div>
    )
  }
  
  // 正常表示
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-6">商品一覧</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        {products.map((product) => (
          <div key={product.id} className="border rounded-lg p-4">
            <h3 className="font-semibold">{product.name}</h3>
            <p className="text-gray-600">{product.description}</p>
            <p className="text-xl font-bold mt-2">
              ¥{product.price.toLocaleString()}
            </p>
          </div>
        ))}
      </div>
    </div>
  )
}

学んだポイント:

  1. 必ず3つの状態を管理

    • データ本体(products)
    • ローディング状態(loading)
    • エラー状態(error)
  2. useEffectの依存配列は超重要 忘れると無限ループ、間違えると更新されない...

  3. エラーハンドリングは必須 「エラーなんて起きない」は幻想でした。

感動した瞬間: ローカルで動いてたものが、デプロイ後もそのまま動いた時。「Azure Functions最高!」と叫びました。

いよいよデプロイ!魔法の設定ファイル

staticwebapp.config.json:これ一つで全部解決

「設定ファイル多すぎ...」と思ってたら、Azureは1ファイルでOKでした!

{
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/api/*", "/_next/*"]
  },
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/admin/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "globalHeaders": {
    "X-Frame-Options": "DENY",
    "X-Content-Type-Options": "nosniff"
  }
}

この設定で実現できること:

  • SPAのルーティング対応
  • APIへのアクセス制御
  • セキュリティヘッダー自動付与
  • 管理画面の認証必須化

初回デプロイで感動した瞬間

  1. GitHubと連携 Azure PortalでポチポチするだけでGitHub連携完了。

  2. 自動でworkflow作成 なんと、GitHub Actionsの設定ファイルを自動生成してくれた!

  3. 5分後にはもう公開 プッシュして、コーヒー飲んでたら完了通知が。

つまずいたポイント:

  • Node.jsのバージョン指定忘れ(16→18に変更必要だった)
  • 環境変数の設定場所が分からず30分悩んだ
  • ビルドコマンドのカスタマイズ方法

GitHub Actions:自動化の極み

「CI/CDって難しそう...」

そう思ってた私に、Azureが自動生成してくれたワークフローファイル。これが革命的でした!

自動生成されたワークフロー(簡略版):

# .github/workflows/azure-static-web-apps.yml
name: Azure Static Web Apps CI/CD

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build And Deploy
        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: "/"
          api_location: "api"
          output_location: "out"

すごいポイント:

  1. プルリクごとにプレビュー環境 PRを作ると自動でプレビューURLが生成される!

  2. 環境変数の管理 GitHub Secretsに登録するだけで、安全に管理。

  3. ビルドエラーで自動停止 壊れたコードは絶対に本番に行かない。

実際の開発フロー

  1. 機能開発

    git checkout -b feature/new-function
    # コード書く
    git push origin feature/new-function
    
  2. 自動プレビュー PR作成 → 5分後にはプレビューURL発行 「https://purple-beach-12345.azurestaticapps.net」みたいな

  3. レビュー&マージ プレビューで確認 → 問題なければマージ → 自動で本番デプロイ

感動ポイント: 「デプロイ職人」が不要になった。誰でも安全にデプロイできる環境が実現!

モニタリング:「何が起きてるか分からない」を解決

Application Insights で目から鱗

「本番で何が起きてるか分からない...」 「エラーが出てるらしいけど、再現できない...」

こんな悩みが、Application Insightsで一発解決しました。

最小限の実装で始める:

// src/lib/monitoring.ts
import { ApplicationInsights } from '@microsoft/applicationinsights-web'

// シンプルに始める
export const initMonitoring = () => {
  const appInsights = new ApplicationInsights({
    config: {
      connectionString: process.env.NEXT_PUBLIC_APPINSIGHTS_CONNECTION_STRING,
      enableAutoRouteTracking: true, // ページ遷移を自動記録
    }
  })
  
  appInsights.loadAppInsights()
  return appInsights
}

// 使い方
const ai = initMonitoring()

// エラーを記録
try {
  // 何か処理
} catch (error) {
  ai.trackException({ error })
}

// カスタムイベント
ai.trackEvent({ name: '購入完了', properties: { amount: 1000 } })

実際に役立った場面

1. 謎のエラーを特定

ユーザー:「たまにエラーが出る」
私:「Application Insights見てみよう」

結果:特定のブラウザでのみ発生するエラーだった!

2. パフォーマンス問題の発見

ダッシュボード:「このAPIだけ3秒かかってる」
私:「え、気づかなかった...」

調査したら、N+1問題が原因でした。

3. ユーザー行動の理解

データ:「90%のユーザーがここで離脱」
私:「UIを改善しよう」

結果:コンバージョン率が2倍に!

Functionsのログも簡単

// Azure Functionsでのログ
const httpTrigger: AzureFunction = async (context, req) => {
  // これだけでApplication Insightsに記録される
  context.log('リクエスト受信', { userId: req.query.userId })
  
  try {
    // 処理
  } catch (error) {
    context.log.error('エラー発生', error)
    // Application Insightsで確認できる!
  }
}

感動ポイント:

  • セットアップが超簡単(5分で完了)
  • リアルタイムでログが見れる
  • エラーの詳細情報が自動収集
  • 無料枠で十分使える

実践的なログ活用術

最初は「console.log」だらけでした。でも、本番環境では見れない...

Before(ダメな例):

const httpTrigger = async (context, req) => {
  console.log('リクエスト来た') // 本番で見れない!
  
  try {
    // 処理
    console.log('成功した')
  } catch (error) {
    console.error('エラー!', error) // これも見れない!
  }
}

After(正しい方法):

const httpTrigger = async (context, req) => {
  const startTime = Date.now()
  
  // context.logを使う!
  context.log('リクエスト処理開始', {
    method: req.method,
    url: req.url,
    userId: req.query.userId
  })
  
  try {
    // 処理
    const result = await processRequest(req)
    
    // 処理時間も記録
    const duration = Date.now() - startTime
    context.log('処理完了', {
      duration,
      resultCount: result.length
    })
    
    context.res = {
      status: 200,
      body: result
    }
  } catch (error) {
    // エラーは必ずcontext.log.errorで
    context.log.error('処理失敗', {
      error: error.message,
      stack: error.stack,
      userId: req.query.userId
    })
    
    context.res = {
      status: 500,
      body: { error: 'Internal Server Error' }
    }
  }
}

ログで救われた実例:

  1. 「たまに遅い」問題

    • ログで処理時間を記録
    • 特定のユーザーだけ10秒かかってた
    • 原因:データ量が異常に多いユーザーだった
  2. 「エラーが出る」問題

    • エラーログにユーザーIDを含める
    • 特定のユーザーで再現
    • 原因:特殊文字を含む名前だった
  3. 「使われてない機能」の発見

    • 各エンドポイントの呼び出し回数を記録
    • 1ヶ月で0回の機能を発見
    • 削除してコード量30%削減!

プロのコツ:

  • 必ず「誰が」「いつ」「何を」したかを記録
  • エラーの時は入力値も記録(個人情報注意)
  • 処理時間は必ず測定する

セキュリティ:「やらかした」経験から学んだこと

認証なしで公開してしまった話

恥ずかしい話をします。

開発環境では認証かけてたのに、本番デプロイしたら誰でもアクセスできる状態に... 幸い、すぐ気づいて対処しましたが、冷や汗が止まりませんでした。

Azure ADで簡単認証

Static Web Appsの神機能:設定だけで認証完了

// staticwebapp.config.json
{
  "routes": [
    {
      "route": "/admin/*",
      "allowedRoles": ["authenticated"]
    },
    {
      "route": "/api/admin/*",
      "allowedRoles": ["admin"]
    }
  ],
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/YOUR-TENANT-ID",
          "clientIdSettingName": "AAD_CLIENT_ID",
          "clientSecretSettingName": "AAD_CLIENT_SECRET"
        }
      }
    }
  }
}

たったこれだけで:

  • /admin/* は認証必須
  • ログイン画面も自動生成
  • セッション管理も自動

コードでの認証実装

// src/lib/auth.ts
// シンプルな認証ヘルパー
export const useAuth = () => {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    // Static Web Appsの認証情報を取得
    fetch('/.auth/me')
      .then(res => res.json())
      .then(data => {
        if (data.clientPrincipal) {
          setUser({
            name: data.clientPrincipal.userDetails,
            roles: data.clientPrincipal.userRoles
          })
        }
        setLoading(false)
      })
  }, [])
  
  const login = () => {
    window.location.href = '/.auth/login/aad'
  }
  
  const logout = () => {
    window.location.href = '/.auth/logout'
  }
  
  return { user, loading, login, logout }
}

// 使い方
export default function AdminPage() {
  const { user, loading, login } = useAuth()
  
  if (loading) return <div>Loading...</div>
  
  if (!user) {
    return (
      <div>
        <p>ログインが必要です</p>
        <button onClick={login}>ログイン</button>
      </div>
    )
  }
  
  return <div>ようこそ、{user.name}さん!</div>
}

学んだ教訓:

  1. 環境変数は必ずAzure Portalで設定 GitHubに秘密情報をコミットしない!

  2. ロールベースアクセス制御 全員に同じ権限は危険。必要最小限に。

  3. APIも必ず保護 フロントだけ守ってもダメ。APIも認証必須。

環境変数で大失敗した話

「なんで本番で動かないの!?」

原因:環境変数の設定忘れ。これ、本当によくやります。

失敗から生まれたチェックリスト:

  1. 必須環境変数の検証
// src/lib/config.ts
// 起動時に必ずチェック!
const requiredEnvVars = [
  'NEXT_PUBLIC_API_URL',
  'DATABASE_URL',
  'API_KEY'
]

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    console.error(`❌ Missing: ${envVar}`)
    throw new Error(`環境変数 ${envVar} が設定されていません`)
  }
}

console.log('✅ 環境変数チェックOK')
  1. ローカル開発用の.env.example
# .env.example(これをコミット)
NEXT_PUBLIC_API_URL=http://localhost:7071/api
DATABASE_URL=your-database-url
API_KEY=your-api-key

# 使い方
# 1. このファイルをコピー: cp .env.example .env.local
# 2. 実際の値を設定
  1. Azure Portalでの設定
Static Web Apps > 設定 > 環境変数

本番用:
- NEXT_PUBLIC_API_URL = https://api.example.com
- DATABASE_URL = [実際の接続文字列]

ステージング用:
- NEXT_PUBLIC_API_URL = https://staging-api.example.com
- DATABASE_URL = [テスト用DB]

プロのコツ:

  • 秘密情報は絶対にコードに書かない
  • .env.local.gitignoreに追加
  • チーム内で環境変数リストを共有
  • デプロイ前に必ず環境変数チェック

パフォーマンス改善:「遅い」から「爆速」へ

CDNの威力を実感した瞬間

日本からアクセス:50ms アメリカからアクセス:2000ms

「海外ユーザーから遅いって言われた...」

Static Web AppsのCDNを有効にしたら: 世界中どこからでも50ms以下!

画像最適化で劇的改善

Before:

  • 1枚3MBの画像
  • ページ読み込み10秒
  • ユーザー離脱率50%

After:

// next.config.js
const nextConfig = {
  images: {
    // Azure CDNで画像を自動最適化
    loader: 'custom',
    loaderFile: './src/lib/image-loader.ts',
  },
}

// src/lib/image-loader.ts
export default function imageLoader({ src, width, quality }) {
  // サイズに応じて自動リサイズ
  return `https://mycdn.azureedge.net${src}?w=${width}&q=${quality || 75}`
}

結果:

  • 画像サイズ90%削減
  • ページ読み込み1秒以下
  • 離脱率5%に改善!

キャッシュ戦略

// 静的ファイルは1年キャッシュ
headers: [
  {
    source: '/_next/static/:path*',
    headers: [{
      key: 'Cache-Control',
      value: 'public, max-age=31536000, immutable'
    }]
  },
  // APIは都度更新
  {
    source: '/api/:path*',
    headers: [{
      key: 'Cache-Control',
      value: 'no-cache'
    }]
  }
]

学んだこと:

  • 画像は必ず最適化する
  • CDNは本当に速い(当たり前だけど)
  • キャッシュ設定は慎重に(更新できなくなる)

まとめ:デプロイが怖くなくなった日

3年前の私:

  • デプロイ = 胃痛
  • 金曜デプロイ = 絶対NG
  • 本番障害 = 週末出勤

今の私:

  • デプロイ = git push
  • 金曜デプロイ = 全然OK
  • 本番障害 = 自動ロールバック

Azure Static Web Apps + Functions で変わったこと:

  1. 開発速度が5倍に

    • サーバー構築不要
    • 自動デプロイ
    • プレビュー環境
  2. 運用コストが1/10に

    • サーバーレスで従量課金
    • 無料枠が充実
    • 運用作業がほぼゼロ
  3. 精神的な余裕

    • デプロイの恐怖から解放
    • 障害時も慌てない
    • 週末も安心

一番伝えたいこと:

「難しそう」と思ってる方、大丈夫です。私も最初はそうでした。 でも、実際にやってみたら想像以上に簡単で、今では手放せません。

特に小規模〜中規模のプロジェクトなら、これ以上の選択肢はないと思います。

これから始める方へ:

  1. まずは公式チュートリアルを試す(30分で完了)
  2. 小さなプロジェクトで実践
  3. 徐々に機能を追加

きっと「もっと早く使えばよかった」と思うはずです。

エンハンスド株式会社では、Azure Static Web Apps を活用したモダンなWeb開発を支援しています。「うちのプロジェクトでも使えるかな?」という相談から、お気軽にお問い合わせください。一緒に、デプロイが楽しい開発環境を作りましょう!

参考リンク


執筆者: エンハンスド株式会社 クラウドアーキテクトチーム
公開日: 2025年4月13日
カテゴリ: Azure, サーバーレス, CI/CD
タグ: #Azure #StaticWebApps #AzureFunctions #サーバーレス #デプロイ自動化

技術的な課題をお持ちですか?

記事でご紹介した技術や実装について、
より詳細なご相談やプロジェクトのサポートを承ります

無料技術相談を申し込む