Azure デプロイで人生が楽になった話:Static Web Apps と 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: "注文を受け付けました"
}
}
}
開発して分かったコツ:
-
エラーハンドリングは丁寧に 最初は雑にやってたら、本番で謎のエラーが...ちゃんとログ出力しましょう。
-
環境変数は必須 ローカルと本番でDB接続先を変える。当たり前だけど大事。
-
非同期処理を活用 メール送信とか時間かかる処理は、レスポンス返してから実行。
失敗談: 最初、全処理を同期的にやってたら、タイムアウトエラーの嵐。非同期処理の重要性を痛感しました。
フロントエンドとの連携:「つながった!」の感動
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),
}
開発のコツ:
- 型定義は必須:TypeScriptの恩恵を最大限に
- エラーハンドリング:ユーザーに優しいメッセージを
- ローディング状態:「処理中...」の表示は必須
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>
)
}
学んだポイント:
-
必ず3つの状態を管理
- データ本体(products)
- ローディング状態(loading)
- エラー状態(error)
-
useEffectの依存配列は超重要 忘れると無限ループ、間違えると更新されない...
-
エラーハンドリングは必須 「エラーなんて起きない」は幻想でした。
感動した瞬間: ローカルで動いてたものが、デプロイ後もそのまま動いた時。「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へのアクセス制御
- セキュリティヘッダー自動付与
- 管理画面の認証必須化
初回デプロイで感動した瞬間
-
GitHubと連携 Azure PortalでポチポチするだけでGitHub連携完了。
-
自動でworkflow作成 なんと、GitHub Actionsの設定ファイルを自動生成してくれた!
-
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"
すごいポイント:
-
プルリクごとにプレビュー環境 PRを作ると自動でプレビューURLが生成される!
-
環境変数の管理 GitHub Secretsに登録するだけで、安全に管理。
-
ビルドエラーで自動停止 壊れたコードは絶対に本番に行かない。
実際の開発フロー
-
機能開発
git checkout -b feature/new-function # コード書く git push origin feature/new-function
-
自動プレビュー PR作成 → 5分後にはプレビューURL発行 「https://purple-beach-12345.azurestaticapps.net」みたいな
-
レビュー&マージ プレビューで確認 → 問題なければマージ → 自動で本番デプロイ
感動ポイント: 「デプロイ職人」が不要になった。誰でも安全にデプロイできる環境が実現!
モニタリング:「何が起きてるか分からない」を解決
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' }
}
}
}
ログで救われた実例:
-
「たまに遅い」問題
- ログで処理時間を記録
- 特定のユーザーだけ10秒かかってた
- 原因:データ量が異常に多いユーザーだった
-
「エラーが出る」問題
- エラーログにユーザーIDを含める
- 特定のユーザーで再現
- 原因:特殊文字を含む名前だった
-
「使われてない機能」の発見
- 各エンドポイントの呼び出し回数を記録
- 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>
}
学んだ教訓:
-
環境変数は必ずAzure Portalで設定 GitHubに秘密情報をコミットしない!
-
ロールベースアクセス制御 全員に同じ権限は危険。必要最小限に。
-
APIも必ず保護 フロントだけ守ってもダメ。APIも認証必須。
環境変数で大失敗した話
「なんで本番で動かないの!?」
原因:環境変数の設定忘れ。これ、本当によくやります。
失敗から生まれたチェックリスト:
- 必須環境変数の検証
// 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')
- ローカル開発用の.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. 実際の値を設定
- 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 で変わったこと:
-
開発速度が5倍に
- サーバー構築不要
- 自動デプロイ
- プレビュー環境
-
運用コストが1/10に
- サーバーレスで従量課金
- 無料枠が充実
- 運用作業がほぼゼロ
-
精神的な余裕
- デプロイの恐怖から解放
- 障害時も慌てない
- 週末も安心
一番伝えたいこと:
「難しそう」と思ってる方、大丈夫です。私も最初はそうでした。 でも、実際にやってみたら想像以上に簡単で、今では手放せません。
特に小規模〜中規模のプロジェクトなら、これ以上の選択肢はないと思います。
これから始める方へ:
- まずは公式チュートリアルを試す(30分で完了)
- 小さなプロジェクトで実践
- 徐々に機能を追加
きっと「もっと早く使えばよかった」と思うはずです。
エンハンスド株式会社では、Azure Static Web Apps を活用したモダンなWeb開発を支援しています。「うちのプロジェクトでも使えるかな?」という相談から、お気軽にお問い合わせください。一緒に、デプロイが楽しい開発環境を作りましょう!
参考リンク
執筆者: エンハンスド株式会社 クラウドアーキテクトチーム
公開日: 2025年4月13日
カテゴリ: Azure, サーバーレス, CI/CD
タグ: #Azure #StaticWebApps #AzureFunctions #サーバーレス #デプロイ自動化