サーバーレスPaaSインフラでデプロイ後に直面する実務問題の解決策
TuBrief 편집팀
2026년 7월 24일
0
Internet Technology원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
フロントエンドとバックエンドを数クリックでデプロイしてくれるオールインワンPaaSは、最初は完璧に見える。問題は、実際のユーザーが流入し始めてからだ。ローカルでは問題なく動いていたコードがプロダクションに上がるとタイムアウトを吐き出し、DBスキーマを1つ変更しようとしただけでサービス全体が不通になることもある。さらに、ベンダーロックインによって身動きが取れない状況に直面すると、頭が痛くなってくる。
このようなプラットフォームの利便性を享受しつつ、後からついてくる障害物をきれいに取り除くインフラパターンをいくつかまとめた。
ローカルのNode.jsプロセスは常時起動しているが、サーバーレス環境はリクエストが入ってきたときだけ一時的に起動して消滅する。コールドスタートのタイミングでグローバル変数やDBシングルトン接続を曖昧に保持すると、コネクションプールがあっという間に枯渇してしまう。
ステージングサーバーへ毎回デプロイして確認するわけにもいかない。ネットワークレイヤー自体をローカルでモックし、ハンドラーを立ち上げる方がはるかに早い。
`typescript
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/v1/user/profile', ({ request }) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
return new HttpResponse(null, { status: 401, statusText: 'Unauthorized' });
}
return HttpResponse.json({
id: 'usr_102938',
email: 'dev@example.com',
role: 'ADMIN',
createdAt: new Date().toISOString()
});
})
];
// src/mocks/node.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
`
このパターンを適用する手順はシンプルだ。
setupServer で外部API通信を隔離する。.env.local ファイルのDB接続アドレスをプロダクションのトランザクションプーラーエンドポイントと徹底的に分離する。デプロイボタンを押さなくても、バックエンドインターフェースの動作をローカルで100%同一に検証できる。不安定なリモートテスト時間を捨て、コード作成後すぐにフィードバックを受け取る構造だ。
SupabaseやNeonのようなサーバーレスPostgreSQLを使う際、単純な ALTER TABLE 文をそのまま実行するのは危険だ。対象テーブルに ACCESS EXCLUSIVE ロックがかかり、後続のクエリが続々と待機状態に陥って最終的にタイムアウト障害につながる。
まずマイグレーションセッションにタイムアウトの上限を設定しておく必要がある。
`sql
SET lock_timeout = '2000ms';
SET statement_timeout = '5000ms';
ALTER TABLE users ADD COLUMN bio VARCHAR(255);
`
運用中のDB構造を安全に変更するには、コードとDBを同時に拡張した後に古いものを削っていく方式を取るべきだ。
`typescript
import { db } from './db';
interface UpdateUserProfileInput {
userId: string;
fullName: string;
}
export async function updateUserProfile({ userId, fullName }: UpdateUserProfileInput) {
await db.transaction(async (tx) => {
await tx.user.update({
where: { id: userId },
data: {
full_name: fullName,
name: fullName
}
});
});
}
`
安全な作業のための3ステップだ。
lock_timeout を設定し、ロック競合時に即座に作業を中断する安全装置を作る。この手順を守れば、ユーザーのリクエストを1つもこぼさずにスキーマを交換できる。
サーバーレス環境で console.log だけを残しておくと、複数のインスタンスのログが入り交じって惨状と化す。1つのリクエストが入って出ていくまでの動線を追跡するには、一意の Request ID と構造化された JSON 形式が必須だ。
`typescript
import { Hono } from 'hono';
import { requestId } from 'hono/request-id';
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label.toUpperCase() })
},
base: { env: process.env.NODE_ENV }
});
const app = new Hono();
app.use('', requestId());
app.use('', async (c, next) => {
const reqId = c.var.requestId;
const startTime = performance.now();
c.set('logger', logger.child({ reqId }));
await next();
const durationMs = Math.round(performance.now() - startTime);
logger.info({
reqId,
method: c.req.method,
path: c.req.path,
status: c.res.status,
durationMs
}, 'Request processing finished');
});
`
外部モニタリングツールを使う際の上限超過料金爆弾を回避する設定だ。
Server-Timing を注入し、ボトルネックとなるポイントをモニタリングシステムへ送信する。`typescript
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
beforeSend(event, hint) {
const error = hint.originalException;
if (error && error instanceof Error && error.message.includes('ECONNRESET')) {
return null;
}
return event;
}
});
`
無意味な 200 OK ログやノイズ的な例外状況を間引くだけでも、モニタリングプラットフォームの無料枠内でシステムを十分に制御できる。
特定のPaaS専用SDKをビジネスロジックに直接結合させると、将来別のプラットフォームに移行する際にコードを全面的に書き直す羽目になる。Martin Fowlerが提唱したアダプターパターンを使い、アプリケーションのコアロジックと外部クラウドSDKとの接点を分離するのはそのためだ。
`typescript
export interface IStorageService {
uploadFile(path: string, fileBuffer: Buffer, mimeType: string): Promise<{ url: string }>;
deleteFile(path: string): Promise;
}
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
export class S3StorageAdapter implements IStorageService {
private s3: S3Client;
private bucket: string;
constructor(region: string, bucket: string) {
this.s3 = new S3Client({ region });
this.bucket = bucket;
}
async uploadFile(path: string, fileBuffer: Buffer, mimeType: string): Promise<{ url: string }> {
await this.s3.send(new PutObjectCommand({
Bucket: this.bucket,
Key: path,
Body: fileBuffer,
ContentType: mimeType
}));
return { url: https://${this.bucket}.s3.amazonaws.com/${path} };
}
async deleteFile(path: string): Promise {
await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: path }));
}
}
`
いつでも脱出できるようにデータバックアップも自動化しておく。
`bash
#!/usr/bin/env bash
set -euo pipefail
TIMESTAMP=(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/tmp/db_backup_{TIMESTAMP}"
S3_BUCKET="s3://my-app-exit-backups/pg_dumps"
export PGPASSWORD="${DB_PASSWORD}"
mkdir -p "${BACKUP_DIR}"
pg_dump -h "{DB_PORT}" -U "{DB_NAME}"
-Fc --exclude-table-data='logs_*' > "${BACKUP_DIR}/full_schema_data.dump"
tar -czvf "{BACKUP_DIR}" .
aws s3 cp "{S3_BUCKET}/{R2_ENDPOINT_URL}"
rm -rf "{BACKUP_DIR}.tar.gz"
`
pg_dump を活用してスキーマとデータを抽出するシェルスクリプトを作成する。crontab に登録する。特定プラットフォームに依存するリスクを減らしておけば、インフラ単価の上昇やプラットフォーム障害の際にもはるかに対応しやすくなる。