ストーリー
コンテナアーキテクチャ
コンテナとは
コンテナは、アプリケーションとその依存関係を1つのパッケージに閉じ込めた実行単位です。どの環境でも同じように動作することを保証します。
graph TD
subgraph Host["ホストOS"]
subgraph CA["Container A"]
AppA["App"]
LibsA["Libs"]
RtA["Runtime"]
AppA --> LibsA --> RtA
end
subgraph CB["Container B"]
AppB["App"]
LibsB["Libs"]
RtB["Runtime"]
AppB --> LibsB --> RtB
end
CR["Container Runtime(Docker)"]
RtA --> CR
RtB --> CR
end
classDef appStyle fill:#4a90d9,stroke:#2c5f8a,color:#fff
classDef libStyle fill:#67b7dc,stroke:#3a8ab5,color:#fff
classDef rtStyle fill:#5cb85c,stroke:#3d8b3d,color:#fff
classDef crStyle fill:#e8a838,stroke:#b07c1e,color:#fff
class AppA,AppB appStyle
class LibsA,LibsB libStyle
class RtA,RtB rtStyle
class CR crStyle
Dockerfileの例
# マルチステージビルドでイメージサイズを最小化
# Stage 1: ビルド
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: 実行
FROM node:20-alpine AS runner
WORKDIR /app
# セキュリティ: 非rootユーザーで実行
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 appuser
COPY --from=builder --chown=appuser:nodejs /app/dist ./dist
COPY --from=builder --chown=appuser:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:nodejs /app/package.json ./
USER appuser
EXPOSE 3000
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]
Docker Composeによるマルチサービス構成
# docker-compose.yml
version: '3.8'
services:
# APIサービス
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
deploy:
replicas: 2
resources:
limits:
cpus: '0.5'
memory: 512M
# ワーカーサービス(イベント処理)
worker:
build:
context: ./worker
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- QUEUE_URL=amqp://rabbitmq:5672
depends_on:
- rabbitmq
# データベース
db:
image: postgres:16-alpine
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myapp
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
# キャッシュ
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# メッセージブローカー
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "15672:15672"
volumes:
db_data:
Kubernetes(本番環境)
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
replicas: 3
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
containers:
- name: api
image: myapp/api:v1.2.0
ports:
- containerPort: 3000
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
---
# 水平オートスケーリング
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
サーバーレスアーキテクチャ
サーバーレスとは
サーバーレスは、サーバーの管理をクラウドプロバイダーに完全に委託し、関数単位でコードを実行するアーキテクチャです。リクエストがあった時だけ実行され、使った分だけ課金されます。
graph LR
A["リクエスト"] --> B["API Gateway"]
B --> C["Lambda Function"]
C --> D["DynamoDB"]
C --> E["実行後に自動的に終了
課金: 実行時間 x メモリ"]
classDef reqStyle fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef gwStyle fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
classDef lambdaStyle fill:#d1fae5,stroke:#059669,color:#065f46
classDef dbStyle fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
classDef noteStyle fill:#f3e8ff,stroke:#7c3aed,color:#5b21b6
class A reqStyle
class B gwStyle
class C lambdaStyle
class D dbStyle
class E noteStyle
サーバーの管理は不要 / 0リクエスト時のコストは0
AWS Lambda + API Gatewayの例
// handler.ts - Lambda関数
import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDBClient, PutItemCommand, GetItemCommand } from '@aws-sdk/client-dynamodb';
const dynamodb = new DynamoDBClient({});
const TABLE_NAME = process.env.TABLE_NAME!;
// 注文作成
export const createOrder: APIGatewayProxyHandler = async (event) => {
try {
const body = JSON.parse(event.body ?? '{}');
const orderId = crypto.randomUUID();
const order = {
id: orderId,
customerId: body.customerId,
items: body.items,
totalAmount: body.totalAmount,
status: 'CREATED',
createdAt: new Date().toISOString(),
};
await dynamodb.send(new PutItemCommand({
TableName: TABLE_NAME,
Item: {
PK: { S: `ORDER#${orderId}` },
SK: { S: 'METADATA' },
data: { S: JSON.stringify(order) },
GSI1PK: { S: `CUSTOMER#${body.customerId}` },
GSI1SK: { S: order.createdAt },
},
}));
return response(201, { orderId, message: '注文を作成しました' });
} catch (error) {
console.error('注文作成エラー:', error);
return response(500, { message: '内部エラーが発生しました' });
}
};
// 注文取得
export const getOrder: APIGatewayProxyHandler = async (event) => {
const orderId = event.pathParameters?.orderId;
if (!orderId) return response(400, { message: 'orderIdが必要です' });
const result = await dynamodb.send(new GetItemCommand({
TableName: TABLE_NAME,
Key: {
PK: { S: `ORDER#${orderId}` },
SK: { S: 'METADATA' },
},
}));
if (!result.Item) return response(404, { message: '注文が見つかりません' });
return response(200, JSON.parse(result.Item.data.S!));
};
function response(statusCode: number, body: unknown): APIGatewayProxyResult {
return {
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
};
}
serverless.yml(Serverless Framework)
service: order-service
provider:
name: aws
runtime: nodejs20.x
region: ap-northeast-1
memorySize: 256
timeout: 30
environment:
TABLE_NAME: !Ref OrdersTable
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
- dynamodb:Query
Resource:
- !GetAtt OrdersTable.Arn
- !Sub "${OrdersTable.Arn}/index/*"
functions:
createOrder:
handler: handler.createOrder
events:
- httpApi:
path: /orders
method: post
getOrder:
handler: handler.getOrder
events:
- httpApi:
path: /orders/{orderId}
method: get
# イベント駆動: DynamoDB Streamsから在庫を更新
processOrderStream:
handler: handler.processOrderStream
events:
- stream:
type: dynamodb
arn: !GetAtt OrdersTable.StreamArn
batchSize: 10
startingPosition: TRIM_HORIZON
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-orders
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
- AttributeName: GSI1SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- AttributeName: GSI1PK
KeyType: HASH
- AttributeName: GSI1SK
KeyType: RANGE
Projection:
ProjectionType: ALL
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
コンテナ vs サーバーレス:詳細比較
コールドスタート
graph TD
subgraph Container["コンテナ(常時起動): 合計 50ms"]
CR["リクエスト"] --> CC["既に起動済みの
コンテナ
0ms"] --> CRes["レスポンス
50ms"]
end
subgraph Serverless["サーバーレス(コールドスタート): 合計 550ms"]
SR["リクエスト"] --> SB["コンテナ起動
200ms"] --> SI["ランタイム初期化
300ms"] --> SE["関数実行
50ms"] --> SRes["レスポンス"]
end
classDef reqStyle fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef fastStyle fill:#d1fae5,stroke:#059669,color:#065f46
classDef slowStyle fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
class CR,SR,CRes,SRes reqStyle
class CC fastStyle
class SB,SI,SE slowStyle
2回目以降はウォームスタートで50ms程度。軽減方法: Provisioned Concurrency / 定期的なウォームアップ / 軽量ランタイム使用
コストモデル
graph LR
subgraph Container["コンテナ(ECS Fargate例)"]
C1["vCPU: 0.5 / Memory: 1GB
$0.04048/時 x 24h x 30日
= $29.15/月
x 2インスタンス = $58.30/月
特徴: 固定費
リクエスト0でもコスト発生"]
end
subgraph Serverless["サーバーレス(Lambda例)"]
S1["Memory: 256MB / 平均200ms
月間100万リクエスト
リクエスト料金: $0.20
実行時間料金: $0.83
合計: 約$1.03/月
特徴: 従量課金
リクエスト0ならコスト0"]
end
classDef containerStyle fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
classDef serverlessStyle fill:#d1fae5,stroke:#059669,color:#065f46
class C1 containerStyle
class S1 serverlessStyle
リクエスト数が少ないほどサーバーレスが有利。リクエスト数が多い(数千万/月〜)とコンテナが有利になる
総合比較表
| 特性 | コンテナ(ECS/K8s) | サーバーレス(Lambda) |
|---|---|---|
| コールドスタート | なし(常時起動) | あり(100ms〜数秒) |
| 最大実行時間 | 制限なし | 15分(Lambda) |
| スケーリング速度 | 分単位 | ミリ秒〜秒単位 |
| スケーリング上限 | ノード数に依存 | 同時実行数(デフォルト1000) |
| コストモデル | 固定費+変動費 | 完全従量課金 |
| 運用負荷 | 中〜高(パッチ、監視、スケーリング設定) | 低(プロバイダーが管理) |
| デバッグ | 容易(コンテナにアタッチ可能) | 困難(ローカル再現が難しい) |
| ステートフル処理 | 可能 | 原則ステートレス |
| ベンダーロックイン | 低い(OCI標準) | 高い(プロバイダー固有) |
| ローカル開発 | 容易(Docker Compose) | ツールが必要(SAM, Serverless) |
| WebSocket/長時間接続 | 可能 | 制限あり |
| GPU利用 | 可能 | 限定的 |
ハイブリッドアプローチ
実際のプロジェクトでは、コンテナとサーバーレスをワークロードの特性に応じて使い分けることが多いです。
graph TD
GW["API Gateway"]
GW --> L1["Lambda<br/>認証処理"]
GW --> L2["Lambda<br/>画像リサイズ"]
GW --> ECS["ECS/Fargate<br/>メインAPI<br/>(常時起動)"]
ECS --> RDS["RDS DB"]
ECS --> L3["Lambda<br/>バッチ処理"]
classDef gwStyle fill:#e8a838,stroke:#b07c1e,color:#fff
classDef lambdaStyle fill:#5cb85c,stroke:#3d8b3d,color:#fff
classDef ecsStyle fill:#4a90d9,stroke:#2c5f8a,color:#fff
classDef dbStyle fill:#d9534f,stroke:#b52b27,color:#fff
class GW gwStyle
class L1,L2,L3 lambdaStyle
class ECS ecsStyle
class RDS dbStyle
ハイブリッドの判断フレームワーク
// ワークロードの特性に基づく計算プラットフォーム選定
interface Workload {
name: string;
characteristics: {
requestPattern: 'CONSTANT' | 'SPIKY' | 'PERIODIC';
executionDuration: 'SHORT' | 'MEDIUM' | 'LONG'; // <1s, 1s-5min, >5min
stateRequirement: 'STATELESS' | 'STATEFUL';
coldStartTolerance: 'LOW' | 'MEDIUM' | 'HIGH';
monthlyInvocations: number;
};
}
function recommendPlatform(workload: Workload): string {
const { characteristics: c } = workload;
// 長時間実行やステートフルはコンテナ
if (c.executionDuration === 'LONG' || c.stateRequirement === 'STATEFUL') {
return 'CONTAINER';
}
// コールドスタート耐性が低い常時トラフィックはコンテナ
if (c.coldStartTolerance === 'LOW' && c.requestPattern === 'CONSTANT') {
return 'CONTAINER';
}
// スパイクのあるショートリクエストはサーバーレス
if (c.requestPattern === 'SPIKY' && c.executionDuration === 'SHORT') {
return 'SERVERLESS';
}
// 低頻度の定期実行はサーバーレス
if (c.requestPattern === 'PERIODIC' && c.monthlyInvocations < 100000) {
return 'SERVERLESS';
}
// 高頻度の安定トラフィックはコンテナ
if (c.monthlyInvocations > 10_000_000) {
return 'CONTAINER';
}
return 'SERVERLESS'; // デフォルトはサーバーレス
}
// 適用例
const workloads: Workload[] = [
{
name: 'メインAPI',
characteristics: {
requestPattern: 'CONSTANT',
executionDuration: 'SHORT',
stateRequirement: 'STATELESS',
coldStartTolerance: 'LOW',
monthlyInvocations: 50_000_000,
},
},
{
name: '画像リサイズ',
characteristics: {
requestPattern: 'SPIKY',
executionDuration: 'MEDIUM',
stateRequirement: 'STATELESS',
coldStartTolerance: 'HIGH',
monthlyInvocations: 500_000,
},
},
{
name: '日次レポート生成',
characteristics: {
requestPattern: 'PERIODIC',
executionDuration: 'LONG',
stateRequirement: 'STATELESS',
coldStartTolerance: 'HIGH',
monthlyInvocations: 30,
},
},
];
workloads.forEach(w => {
console.log(`${w.name}: ${recommendPlatform(w)}`);
});
// メインAPI: CONTAINER
// 画像リサイズ: SERVERLESS
// 日次レポート生成: CONTAINER(長時間実行のため)
まとめ
| ポイント | 内容 |
|---|---|
| コンテナ | 一貫した実行環境、柔軟な設定、運用負荷はやや高い |
| サーバーレス | 運用不要、従量課金、コールドスタートと実行時間制限あり |
| コスト | 低トラフィックはサーバーレス有利、高トラフィックはコンテナ有利 |
| スケーリング | サーバーレスは瞬時、コンテナは分単位 |
| ハイブリッド | ワークロード特性に応じて使い分けるのが実用的 |
| 判断基準 | 実行時間、トラフィックパターン、コールドスタート耐性、コスト |
チェックリスト
- コンテナアーキテクチャの基本構成を説明できる
- Dockerfile のマルチステージビルドの目的を理解した
- サーバーレスのコールドスタート問題を説明できる
- コストモデルの違い(固定費 vs 従量課金)を理解した
- ワークロードの特性に基づくプラットフォーム選定ができる
- ハイブリッドアプローチの考え方を理解した
次のステップへ
次は「CQRSとイベントソーシング」を学びます。データの読み書きを分離し、イベントを活用した高度なアーキテクチャパターンを理解しましょう。
推定読了時間: 30分