
Webアプリケーション開発 基本フロー教科書
目次
1. Webアプリケーション開発入門
1.1 Webアプリケーションとは
Webアプリケーション(Webアプリ)は、Webブラウザを通じてアクセス・利用できるソフトウェアアプリケーションです。従来のデスクトップアプリケーションとは異なり、インストールが不要でどのデバイスからでもアクセス可能です。
1.2 主な特徴
プラットフォーム非依存: あらゆるOS、デバイスで動作
リアルタイム更新: サーバーサイドの更新が即座に反映
共同作業: 複数ユーザーが同時にアクセス可能
メンテナンス性: 中央集約型の管理
1.3 基本的なアーキテクチャ
[クライアント] ←→ [Webサーバー] ←→ [アプリケーションサーバー] ←→ [データベース]
↓ ↓ ↓ ↓
HTML/CSS/JS Apache/Nginx Node.js/Python/PHP MySQL/PostgreSQL1.4 開発フローの概要
企画・要件定義 (1-2週間)
設計 (2-3週間)
開発 (4-8週間)
テスト (1-2週間)
デプロイ (1週間)
保守・運用 (継続的)
2. 企画・要件定義フェーズ
2.1 目的と目標の設定
2.1.1 ステークホルダーの特定
プロジェクトオーナー: 最終決定権者
エンドユーザー: 実際の利用者
開発チーム: 技術実装担当
運用チーム: リリース後の保守担当
2.1.2 ビジネス目標の明確化
解決したい課題は何か?
期待される効果・成果は?
成功指標(KPI)の設定
2.2 要件収集
2.2.1 機能要件
## 機能要件例(ECサイト)
### 必須機能
- [ ] ユーザー登録・ログイン
- [ ] 商品一覧・詳細表示
- [ ] カート機能
- [ ] 決済機能
- [ ] 注文履歴
### 希望機能
- [ ] レコメンド機能
- [ ] レビュー・評価機能
- [ ] お気に入り機能2.2.2 非機能要件
パフォーマンス: レスポンス時間、同時接続数
セキュリティ: 認証、暗号化、脆弱性対策
可用性: 稼働率、ダウンタイム許容範囲
拡張性: 将来の機能追加への対応
2.3 スコープ定義
2.3.1 WBS(Work Breakdown Structure)作成
プロジェクト全体
├── 企画・要件定義
│ ├── 要件収集
│ ├── 仕様書作成
│ └── 承認
├── 設計
│ ├── システム設計
│ ├── データベース設計
│ └── UI/UX設計
└── 開発・テスト・リリース
├── フロントエンド開発
├── バックエンド開発
├── テスト
└── デプロイ2.3.2 スケジュール策定
マイルストーンの設定
クリティカルパスの特定
バッファ時間の確保
2.4 成果物
要件定義書
機能仕様書
プロジェクト計画書
WBS
スケジュール表
3. 設計フェーズ
3.1 システム設計
3.1.1 アーキテクチャの選択
モノリシックアーキテクチャ
┌─────────────────────────────────┐
│ Webアプリケーション │
├─────────────────────────────────┤
│ フロントエンド (HTML/CSS/JS) │
├─────────────────────────────────┤
│ ビジネスロジック │
├─────────────────────────────────┤
│ データアクセス層 │
└─────────────────────────────────┘マイクロサービスアーキテクチャ
┌──────────┐ ┌──────────┐ ┌──────────┐
│サービスA │ │サービスB │ │サービスC │
├──────────┤ ├──────────┤ ├──────────┤
│ DB-A │ │ DB-B │ │ DB-C │
└──────────┘ └──────────┘ └──────────┘3.1.2 技術スタックの選定
フロントエンド
React.js / Vue.js / Angular
TypeScript
CSS Framework (Bootstrap, Tailwind CSS)
バックエンド
Node.js (Express.js)
Python (Django, Flask)
Java (Spring Boot)
PHP (Laravel)
データベース
リレーショナル: MySQL, PostgreSQL
NoSQL: MongoDB, Redis
3.2 データベース設計
3.2.1 ER図の作成
-- ユーザーテーブル例
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 商品テーブル例
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL,
stock_quantity INT DEFAULT 0,
category_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id)
);3.2.2 インデックス戦略
主キー、外部キーには自動的にインデックス
よく検索されるカラムにインデックス作成
複合インデックスの適切な使用
3.3 UI/UX設計
3.3.1 ワイヤーフレーム作成
画面レイアウトの基本構造
ナビゲーション設計
情報アーキテクチャ
3.3.2 プロトタイプ作成
インタラクティブなモックアップ
ユーザビリティテストの実施
フィードバックの収集・反映
3.3.3 デザインシステム構築
/* カラーパレット */
:root {
--primary-color: #007bff ;
--secondary-color: #6c757d ;
--success-color: #28a745 ;
--danger-color: #dc3545 ;
--warning-color: #ffc107 ;
}
/* タイポグラフィ */
h1 { font-size: 2.5rem; font-weight: 700; }
h2 { font-size: 2rem; font-weight: 600; }
h3 { font-size: 1.75rem; font-weight: 500; }
/* コンポーネント */
.btn {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
border: none;
cursor: pointer;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}3.4 成果物
システム設計書
データベース設計書
API仕様書
ワイヤーフレーム
デザインモックアップ
4. 開発フェーズ
4.1 開発環境の構築
4.1.1 バージョン管理システム
# Gitリポジトリの初期化
git init
git remote add origin https://github.com/username/project.git
# .gitignoreファイルの設定
echo "node_modules/
.env
dist/
*.log" > .gitignore4.1.2 開発ツールの導入
パッケージ管理
// package.json例
{
"name": "web-app",
"version": "1.0.0",
"scripts": {
"dev": "npm run dev:client & npm run dev:server",
"dev:client": "vite",
"dev:server": "nodemon server/index.js",
"build": "vite build",
"test": "jest"
},
"dependencies": {
"express": "^4.18.0",
"react": "^18.0.0"
},
"devDependencies": {
"vite": "^4.0.0",
"nodemon": "^2.0.0",
"jest": "^29.0.0"
}
}4.2 フロントエンド開発
4.2.1 コンポーネント設計
// React コンポーネント例
import React, { useState, useEffect } from 'react';
const ProductList = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProducts();
}, []);
const fetchProducts = async () => {
try {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
} catch (error) {
console.error('商品データの取得に失敗しました:', error);
} finally {
setLoading(false);
}
};
if (loading) return <div>読み込み中...</div>;
return (
<div className="product-list">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
export default ProductList;4.2.2 状態管理
// Redux Toolkitを使用した状態管理例
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchProducts = createAsyncThunk(
'products/fetchProducts',
async () => {
const response = await fetch('/api/products');
return response.json();
}
);
const productsSlice = createSlice({
name: 'products',
initialState: {
items: [],
loading: false,
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchProducts.pending, (state) => {
state.loading = true;
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});4.3 バックエンド開発
4.3.1 RESTful API設計
// Express.js API例
const express = require('express');
const router = express.Router();
// 商品一覧取得
router.get('/products', async (req, res) => {
try {
const { page = 1, limit = 10, category } = req.query;
const offset = (page - 1) * limit;
let query = 'SELECT * FROM products';
let params = [];
if (category) {
query += ' WHERE category_id = ?';
params.push(category);
}
query += ' LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
const products = await db.query(query, params);
res.json({
data: products,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: await getProductCount(category)
}
});
} catch (error) {
res.status(500).json({ error: '商品データの取得に失敗しました' });
}
});
// 商品詳細取得
router.get('/products/:id', async (req, res) => {
try {
const { id } = req.params;
const product = await db.query('SELECT * FROM products WHERE id = ?', [id]);
if (!product.length) {
return res.status(404).json({ error: '商品が見つかりません' });
}
res.json(product[0]);
} catch (error) {
res.status(500).json({ error: '商品データの取得に失敗しました' });
}
});4.3.2 認証・認可
// JWT認証ミドルウェア
const jwt = require('jsonwebtoken');
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'アクセストークンが必要です' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: '無効なトークンです' });
}
req.user = user;
next();
});
};
// ログイン処理
router.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// ユーザー検証
const user = await findUserByEmail(email);
if (!user || !await verifyPassword(password, user.password_hash)) {
return res.status(401).json({ error: 'メールアドレスまたはパスワードが間違っています' });
}
// トークン生成
const token = jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token, user: { id: user.id, email: user.email, name: user.name } });
} catch (error) {
res.status(500).json({ error: 'ログインに失敗しました' });
}
});4.4 コーディング規約
4.4.1 JavaScript/TypeScript
// 変数命名: camelCase
const userName = 'user123';
const fetchUserData = async () => {};
// 定数: UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';
// 関数: 動詞から始める
const calculateTotalPrice = (items) => {
return items.reduce((total, item) => total + item.price, 0);
};
// クラス: PascalCase
class UserService {
constructor(apiClient) {
this.apiClient = apiClient;
}
}4.4.2 コメント規約
/**
* ユーザー情報を取得する
* @param {number} userId - ユーザーID
* @param {Object} options - オプション設定
* @param {boolean} options.includeProfile - プロフィール情報を含めるか
* @returns {Promise<Object>} ユーザー情報
*/
const fetchUser = async (userId, { includeProfile = false } = {}) => {
// APIエンドポイントを構築
let endpoint = `/api/users/${userId}`;
if (includeProfile) {
endpoint += '?include=profile';
}
// TODO: エラーハンドリングを改善する
const response = await fetch(endpoint);
return response.json();
};4.5 成果物
フロントエンドコード
バックエンドコード
API仕様書(更新版)
データベースマイグレーションファイル
設定ファイル
5. テストフェーズ
5.1 テストの種類と目的
テストピラミッド
/\
/UI\ 少数:E2Eテスト(統合テスト)
/____\
/ \
/Integration\ 中程度:統合テスト
/____________\
/ \
/ Unit Test \ 多数:単体テスト
/______________\5.2 単体テスト(Unit Test)
5.2.1 フロントエンド単体テスト
// React Testing Library使用例
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import ProductCard from '../ProductCard';
describe('ProductCard', () => {
const mockProduct = {
id: 1,
name: 'テスト商品',
price: 1000,
image: 'test-image.jpg'
};
test('商品情報が正しく表示される', () => {
render(<ProductCard product={mockProduct} />);
expect(screen.getByText('テスト商品')).toBeInTheDocument();
expect(screen.getByText('¥1,000')).toBeInTheDocument();
});
test('カートに追加ボタンがクリックできる', async () => {
const mockOnAddToCart = jest.fn();
render(
<ProductCard
product={mockProduct}
onAddToCart={mockOnAddToCart}
/>
);
const addButton = screen.getByText('カートに追加');
fireEvent.click(addButton);
await waitFor(() => {
expect(mockOnAddToCart).toHaveBeenCalledWith(mockProduct.id);
});
});
});5.2.2 バックエンド単体テスト
// Jest + Supertest使用例
const request = require('supertest');
const app = require('../app');
describe('GET /api/products', () => {
test('商品一覧を正常に取得できる', async () => {
const response = await request(app)
.get('/api/products')
.expect(200);
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.body.pagination).toBeDefined();
});
test('カテゴリフィルタが正常に動作する', async () => {
const response = await request(app)
.get('/api/products?category=electronics')
.expect(200);
expect(response.body.data.every(
product => product.category === 'electronics'
)).toBe(true);
});
test('存在しない商品IDで404エラーが返される', async () => {
await request(app)
.get('/api/products/99999')
.expect(404);
});
});5.3 統合テスト(Integration Test)
5.3.1 API統合テスト
// データベースを含む統合テスト例
const request = require('supertest');
const app = require('../app');
const db = require('../database');
describe('Product API Integration', () => {
beforeEach(async () => {
// テストデータのセットアップ
await db.query('DELETE FROM products');
await db.query(`
INSERT INTO products (name, price, category_id)
VALUES ('テスト商品1', 1000, 1), ('テスト商品2', 2000, 1)
`);
});
afterAll(async () => {
await db.close();
});
test('商品作成から取得までの一連の流れ', async () => {
// 商品作成
const createResponse = await request(app)
.post('/api/products')
.send({
name: '新商品',
price: 3000,
category_id: 1
})
.expect(201);
const productId = createResponse.body.id;
// 作成した商品を取得
const getResponse = await request(app)
.get(`/api/products/${productId}`)
.expect(200);
expect(getResponse.body.name).toBe('新商品');
expect(getResponse.body.price).toBe(3000);
});
});5.4 E2Eテスト(End-to-End Test)
5.4.1 Playwrightを使用したE2Eテスト
// E2Eテスト例
const { test, expect } = require('@playwright/test');
test.describe('商品購入フロー', () => {
test('ユーザーが商品を購入できる', async ({ page }) => {
// ホームページにアクセス
await page.goto('/');
// 商品一覧から商品を選択
await page.click('[data-testid="product-1"]');
// 商品詳細ページで「カートに追加」をクリック
await page.click('[data-testid="add-to-cart"]');
// カートページに移動
await page.click('[data-testid="cart-icon"]');
// 商品がカートに追加されていることを確認
await expect(page.locator('[data-testid="cart-item"]')).toBeVisible();
// チェックアウトページに進む
await page.click('[data-testid="checkout-button"]');
// 配送情報を入力
await page.fill('[data-testid="shipping-name"]', 'テスト太郎');
await page.fill('[data-testid="shipping-email"]', 'test@example.com');
await page.fill('[data-testid="shipping-address"]', '東京都渋谷区1-1-1');
// 注文確定
await page.click('[data-testid="place-order"]');
// 注文完了ページが表示されることを確認
await expect(page.locator('h1')).toContainText('注文が完了しました');
});
});5.5 パフォーマンステスト
5.5.1 負荷テスト
// Artillery.jsを使用した負荷テスト設定例
// artillery.yml
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 10
- duration: 120
arrivalRate: 50
- duration: 60
arrivalRate: 100
scenarios:
- name: "商品一覧取得"
flow:
- get:
url: "/api/products"
- think: 1
- name: "商品詳細取得"
flow:
- get:
url: "/api/products/{{ $randomInt(1, 100) }}"
- think: 25.6 テスト自動化
5.6.1 GitHub Actionsを使用したCI/CD
# .github/workflows/test.yml
name: Test
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: test_db
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: mysql://root:password@localhost:3306/test_db
- name: Run E2E tests
run: npm run test:e2e5.7 成果物
テストコード
テスト実行結果レポート
カバレッジレポート
パフォーマンステスト結果
バグレポート
6. デプロイフェーズ
6.1 デプロイ環境の準備
6.1.1 環境分離
開発環境 (Development)
↓
ステージング環境 (Staging)
↓
本番環境 (Production)6.1.2 環境設定管理
// config/environment.js
const environments = {
development: {
port: 3000,
database: {
host: 'localhost',
name: 'app_dev',
user: 'dev_user',
password: 'dev_pass'
},
redis: {
host: 'localhost',
port: 6379
}
},
staging: {
port: process.env.PORT || 8080,
database: {
host: process.env.DB_HOST,
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
},
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
}
},
production: {
port: process.env.PORT || 8080,
database: {
host: process.env.DB_HOST,
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: true
},
redis: {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
}
}
};
module.exports = environments[process.env.NODE_ENV || 'development'];6.2 サーバー構成
6.2.1 基本的なサーバー構成
[ロードバランサー] (Nginx/ALB)
↓
[Webサーバー] (Nginx)
↓
[アプリケーションサーバー] (Node.js/PM2)
↓
[データベース] (MySQL/PostgreSQL)
↓
[キャッシュサーバー] (Redis)6.2.2 Nginx設定例
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name myapp.com www.myapp.com;
# HTTPからHTTPSへのリダイレクト
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
# SSL設定
ssl_certificate /path/to/certificate.crt;
ssl_certificate_key /path/to/private.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
# セキュリティヘッダー
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# 静的ファイルの配信
location /static/ {
alias /var/www/myapp/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# API リクエストのプロキシ
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# SPAのルーティング対応
location / {
try_files $uri $uri/ /index.html;
root /var/www/myapp/dist;
}
}6.3 デプロイ戦略
6.3.1 ブルーグリーンデプロイ
#!/bin/bash
# blue-green-deploy.sh
# 現在のアクティブ環境を確認
CURRENT_ENV=$(curl -s http://localhost/health | jq -r '.environment')
if [ "$CURRENT_ENV" = "blue" ]; then
TARGET_ENV="green"
TARGET_PORT=3001
else
TARGET_ENV="blue"
TARGET_PORT=3000
fi
echo "デプロイ先: $TARGET_ENV環境 (ポート: $TARGET_PORT)"
# 新しいバージョンをデプロイ
docker-compose -f docker-compose.$TARGET_ENV.yml down
docker-compose -f docker-compose.$TARGET_ENV.yml up -d
# ヘルスチェック
echo "ヘルスチェック実行中..."
for i in {1..30}; do
if curl -s http://localhost:$TARGET_PORT/health > /dev/null; then
echo "ヘルスチェック成功"
break
fi
sleep 10
done
# ロードバランサーの切り替え
echo "トラフィックを$TARGET_ENV環境に切り替え"
sed -i "s/proxy_pass http:\/\/localhost:[0-9]*/proxy_pass http:\/\/localhost:$TARGET_PORT/" /etc/nginx/sites-available/myapp
nginx -s reload
echo "デプロイ完了"6.3.2 ローリングデプロイ
# Kubernetes Deployment設定例
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web-app
image: myapp:v1.2.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-secret
key: host
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 306.4 Docker化
6.4.1 Dockerfile
# マルチステージビルド
FROM node:18-alpine AS builder
WORKDIR /app
# パッケージファイルをコピーして依存関係をインストール
COPY package*.json ./
RUN npm ci --only=production
# ソースコードをコピー
COPY . .
# アプリケーションをビルド
RUN npm run build
# 本番環境用の軽量イメージ
FROM node:18-alpine AS production
WORKDIR /app
# 非rootユーザーでアプリケーションを実行
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# 必要なファイルのみをコピー
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
USER nextjs
EXPOSE 3000
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["npm", "start"]6.4.2 docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
networks:
- app-network
db:
image: postgres:13
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
redis:
image: redis:6-alpine
networks:
- app-network
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- web
networks:
- app-network
volumes:
postgres_data:
networks:
app-network:
driver: bridge6.5 CI/CDパイプライン
6.5.1 GitHub Actions CI/CD
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run security audit
run: npm audit --audit-level high
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: |
docker build -t myapp:${{ github.sha }} .
docker tag myapp:${{ github.sha }} myapp:latest
- name: Push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push myapp:${{ github.sha }}
docker push myapp:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
docker pull myapp:${{ github.sha }}
docker-compose down
docker-compose up -d
docker system prune -f6.6 成果物
デプロイスクリプト
Docker設定ファイル
サーバー設定ファイル
CI/CDパイプライン設定
デプロイドキュメント
7. 保守・運用フェーズ
7.1 モニタリング
7.1.1 アプリケーションモニタリング
// アプリケーションメトリクス収集
const prometheus = require('prom-client');
// カスタムメトリクス定義
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status']
});
const activeUsers = new prometheus.Gauge({
name: 'active_users_total',
help: 'Number of active users'
});
// ミドルウェアでメトリクスを記録
const metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestDuration
.labels(req.method, req.route?.path || req.path, res.statusCode)
.observe(duration);
});
next();
};
// ヘルスチェックエンドポイント
app.get('/health', (req, res) => {
const healthCheck = {
uptime: process.uptime(),
message: 'OK',
timestamp: Date.now(),
checks: {
database: checkDatabase(),
redis: checkRedis(),
memory: {
used: process.memoryUsage().heapUsed / 1024 / 1024,
total: process.memoryUsage().heapTotal / 1024 / 1024
}
}
};
res.status(200).json(healthCheck);
});7.1.2 ログ管理
// Winston logger設定
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'web-app' },
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
// 本番環境以外ではコンソール出力も行う
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
// 使用例
app.use((req, res, next) => {
logger.info('HTTP Request', {
method: req.method,
url: req.url,
userAgent: req.get('user-agent'),
ip: req.ip
});
next();
});
// エラーハンドリング
app.use((err, req, res, next) => {
logger.error('Application Error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method
});
res.status(500).json({
error: 'Internal Server Error',
requestId: req.id
});
});7.1.3 アラート設定
# Prometheus Alert Rules
groups:
- name: web-app-alerts
rules:
- alert: HighResponseTime
expr: http_request_duration_seconds{quantile="0.95"} > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High response time detected"
description: "95th percentile response time is {{ $value }}s"
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 2m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} requests/sec"
- alert: DatabaseConnectionFailure
expr: up{job="database"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Database connection failure"
description: "Database is unreachable"7.2 セキュリティ対策
7.2.1 脆弱性スキャン
#!/bin/bash
# security-scan.sh
echo "NPMパッケージの脆弱性チェック"
npm audit --audit-level high
echo "Dockerイメージの脆弱性スキャン"
trivy image myapp:latest
echo "ソースコードの静的解析"
eslint . --ext .js,.jsx,.ts,.tsx
sonar-scanner
echo "セキュリティヘッダーチェック"
curl -I https://myapp.com | grep -E "(X-Frame-Options|X-Content-Type-Options|X-XSS-Protection)"7.2.2 セキュリティミドルウェア
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
// セキュリティヘッダーの設定
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// レート制限
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分
max: 100, // 最大100リクエスト
message: 'リクエストが多すぎます。後でもう一度お試しください。',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);7.3 バックアップ戦略
7.3.1 データベースバックアップ
#!/bin/bash
# database-backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/var/backups/database"
DB_NAME="myapp"
DB_USER="backup_user"
# バックアップディレクトリ作成
mkdir -p $BACKUP_DIR
# PostgreSQLダンプ作成
pg_dump -h localhost -U $DB_USER -d $DB_NAME > $BACKUP_DIR/backup_$DATE.sql
# 圧縮
gzip $BACKUP_DIR/backup_$DATE.sql
# S3にアップロード
aws s3 cp $BACKUP_DIR/backup_$DATE.sql.gz s3://myapp-backups/database/
# 7日以上古いバックアップを削除
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +7 -delete
echo "バックアップ完了: backup_$DATE.sql.gz"7.3.2 アプリケーションファイルバックアップ
#!/bin/bash
# app-backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
APP_DIR="/var/www/myapp"
BACKUP_DIR="/var/backups/application"
# アプリケーションファイルをアーカイブ
tar -czf $BACKUP_DIR/app_backup_$DATE.tar.gz \
--exclude=node_modules \
--exclude=.git \
--exclude=logs \
$APP_DIR
# S3にアップロード
aws s3 cp $BACKUP_DIR/app_backup_$DATE.tar.gz s3://myapp-backups/application/
echo "アプリケーションバックアップ完了: app_backup_$DATE.tar.gz"7.4 スケーリング
7.4.1 水平スケーリング
# Kubernetes HPA設定
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 807.4.2 データベーススケーリング
// 読み取り専用レプリカの活用
const mysql = require('mysql2/promise');
const masterPool = mysql.createPool({
host: process.env.DB_MASTER_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 10
});
const replicaPool = mysql.createPool({
host: process.env.DB_REPLICA_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 20
});
class DatabaseService {
async executeRead(query, params = []) {
// 読み取り専用クエリはレプリカを使用
return await replicaPool.execute(query, params);
}
async executeWrite(query, params = []) {
// 書き込みクエリはマスターを使用
return await masterPool.execute(query, params);
}
}7.5 アップデート管理
7.5.1 カナリアリリース
#!/bin/bash
# canary-deployment.sh
# カナリア版をデプロイ(トラフィックの10%)
kubectl apply -f canary-deployment.yml
kubectl patch service web-app -p '{"spec":{"selector":{"version":"canary"}}}'
# 5分間モニタリング
sleep 300
# エラー率チェック
ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query?query='rate(http_requests_total{status=~"5.."}[5m])' | jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
echo "エラー率が高すぎます。ロールバックします。"
kubectl rollout undo deployment/web-app-canary
exit 1
fi
# 段階的にトラフィックを増加
kubectl patch ingress web-app --type='json' -p='[{"op": "replace", "path": "/spec/rules/0/http/paths/0/backend/service/name", "value": "web-app-canary"}]'
echo "カナリアリリース成功"7.6 成果物
モニタリングダッシュボード
アラート設定
バックアップスクリプト
運用手順書
インシデント対応マニュアル
8. ベストプラクティスと注意点
8.1 開発ベストプラクティス
8.1.1 コード品質
Clean Code原則
// ❌ Bad
function calc(d, r) {
return d * 365 * r;
}
// ✅ Good
function calculateAnnualInterest(principal, interestRate) {
const DAYS_IN_YEAR = 365;
return principal * DAYS_IN_YEAR * interestRate;
}SOLID原則の適用
// Single Responsibility Principle (単一責任の原則)
class UserValidator {
validate(user) {
if (!user.email || !this.isValidEmail(user.email)) {
throw new Error('無効なメールアドレスです');
}
if (!user.password || user.password.length < 8) {
throw new Error('パスワードは8文字以上である必要があります');
}
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
class UserRepository {
async save(user) {
// データベースへの保存処理
}
async findById(id) {
// IDによるユーザー検索
}
}
class UserService {
constructor(validator, repository) {
this.validator = validator;
this.repository = repository;
}
async createUser(userData) {
this.validator.validate(userData);
return await this.repository.save(userData);
}
}8.1.2 エラーハンドリング
// グローバルエラーハンドラー
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// カスタムエラークラス
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
}
}
// エラーハンドリングミドルウェア
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// ログ出力
logger.error(err);
// Mongoose重複キーエラー
if (err.code === 11000) {
const message = '重複したリソースです';
error = new AppError(message, 400);
}
// Mongoose バリデーションエラー
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = new AppError(message, 400);
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'サーバーエラー'
});
};8.2 セキュリティベストプラクティス
8.2.1 入力値検証
const joi = require('joi');
// バリデーションスキーマ定義
const userSchema = joi.object({
email: joi.string().email().required(),
password: joi.string().min(8).pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])')).required(),
name: joi.string().min(1).max(100).required()
});
// SQLインジェクション対策
const getUserById = async (id) => {
// ❌ Bad - SQLインジェクション脆弱性
// const query = `SELECT * FROM users WHERE id = ${id}`;
// ✅ Good - プリペアードステートメント使用
const query = 'SELECT * FROM users WHERE id = ?';
return await db.query(query, [id]);
};
// XSS対策
const sanitizeHtml = require('sanitize-html');
const sanitizeUserInput = (input) => {
return sanitizeHtml(input, {
allowedTags: [],
allowedAttributes: {}
});
};8.2.2 認証・認可
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
class AuthService {
async hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
generateToken(payload) {
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '24h',
issuer: 'myapp',
audience: 'myapp-users'
});
}
verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'myapp',
audience: 'myapp-users'
});
} catch (error) {
throw new AppError('無効なトークンです', 401);
}
}
}
// ロールベースアクセス制御
const requireRole = (roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '認証が必要です' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'アクセス権限がありません' });
}
next();
};
};
// 使用例
app.get('/api/admin/users', authenticateToken, requireRole(['admin']), getUserList);8.3 パフォーマンスベストプラクティス
8.3.1 データベース最適化
// インデックスの適切な使用
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_products_category_price ON products(category_id, price);
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
// N+1問題の解決
// ❌ Bad - N+1問題
const getOrdersWithItems = async () => {
const orders = await Order.findAll();
for (let order of orders) {
order.items = await OrderItem.findAll({ where: { order_id: order.id } });
}
return orders;
};
// ✅ Good - JOIN使用
const getOrdersWithItems = async () => {
return await Order.findAll({
include: [{
model: OrderItem,
as: 'items'
}]
});
};8.3.2 キャッシング戦略
const redis = require('redis');
const client = redis.createClient();
class CacheService {
async get(key) {
try {
const value = await client.get(key);
return value ? JSON.parse(value) : null;
} catch (error) {
logger.error('Cache get error:', error);
return null;
}
}
async set(key, value, ttl = 3600) {
try {
await client.setex(key, ttl, JSON.stringify(value));
} catch (error) {
logger.error('Cache set error:', error);
}
}
async del(key) {
try {
await client.del(key);
} catch (error) {
logger.error('Cache delete error:', error);
}
}
}
// 使用例
const getProduct = async (id) => {
const cacheKey = `product:${id}`;
// キャッシュから取得を試行
let product = await cacheService.get(cacheKey);
if (!product) {
// データベースから取得
product = await Product.findById(id);
// キャッシュに保存
if (product) {
await cacheService.set(cacheKey, product, 1800); // 30分
}
}
return product;
};8.4 よくある落とし穴と対策
8.4.1 メモリリーク
// ❌ Bad - メモリリーク
let userSessions = {};
app.post('/login', (req, res) => {
const sessionId = generateSessionId();
userSessions[sessionId] = { user: req.body.user, timestamp: Date.now() };
res.json({ sessionId });
});
// ✅ Good - 適切なクリーンアップ
const userSessions = new Map();
app.post('/login', (req, res) => {
const sessionId = generateSessionId();
userSessions.set(sessionId, {
user: req.body.user,
timestamp: Date.now()
});
// 1時間後に自動削除
setTimeout(() => {
userSessions.delete(sessionId);
}, 60 * 60 * 1000);
res.json({ sessionId });
});8.4.2 競合状態(Race Condition)
// ❌ Bad - 競合状態の可能性
const updateUserBalance = async (userId, amount) => {
const user = await User.findById(userId);
user.balance += amount;
await user.save();
};
// ✅ Good - トランザクション使用
const updateUserBalance = async (userId, amount) => {
const transaction = await sequelize.transaction();
try {
const user = await User.findById(userId, {
lock: true,
transaction
});
user.balance += amount;
await user.save({ transaction });
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
};8.4.3 過剰なAPIリクエスト
// ❌ Bad - 過剰なAPIリクエスト
const loadUserData = async (userIds) => {
const users = [];
for (const id of userIds) {
const user = await fetch(`/api/users/${id}`);
users.push(await user.json());
}
return users;
};
// ✅ Good - バッチリクエスト
const loadUserData = async (userIds) => {
const response = await fetch('/api/users/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: userIds })
});
return await response.json();
};