Article

Serverlessアーキテクチャ入門:サーバーレスで変わる開発と運用

高田晃太郎
Serverlessアーキテクチャ入門:サーバーレスで変わる開発と運用

100万リクエストは毎月無料¹、1億リクエストでもおよそ30ドル前後² というのが、代表的なFaaS(Function as a Service。関数単位で実行できるクラウド実行環境)であるAWS Lambdaの料金モデルに基づくシミュレーションの結論です。ここでの想定はメモリ128MB・実行50msのワークロードで、リクエスト課金とGB秒課金(割り当てメモリ×実行時間の合算課金)を合算した概算に過ぎませんが、従来の常時起動サーバーと比べてアイドル時間のコストがほぼゼロに近づくことは直感的に伝わるはずです。公開レポートやベンダーの資料を照合すると、イベント駆動・従量課金・自動スケーリングという三点セットは、ピークトラフィックや短時間バーストが読みにくい現代のSaaSに適合します³。複数の公開事例を横断的に見ると、少人数チームの開発速度と本番運用の一貫性が同時に高められるという評価も広く見られます⁵。サーバーを“見ない”ことは無責任ではなく、責務の再配置です。どこまでをクラウド(マネージドサービス)に委ね、どこからをコードと設定で制御するのか。以降では、Serverlessアーキテクチャの定義と判断軸、開発・運用の変化、実装パターン、性能とコストの現実を、ソースコードと実数値を交えて解説します。

サーバーレスの定義と判断軸:何を“持たない”設計なのか

サーバーレスはサーバーがないわけではありません。インフラのプロビジョニング(リソースの割り当て)、パッチ適用、容量計画といった運用タスクをクラウドに委譲し、アプリケーションは関数単位やマネージドサービスの組み合わせで構成します。FaaS(関数)だけでなく、マネージドなデータベース、メッセージング、認証、オーケストレーションを組み合わせるアーキテクチャスタイルを指すのが実務上の理解です。判断軸としては、ワークロードのトラフィック変動、処理時間の上限、ステート管理の必要性、起動遅延(いわゆるコールドスタート)の許容度、そして組織の運用能力配分が重要になります。例えば秒間スパイクが激しくアイドル時間が長いAPI、イベント駆動の非同期処理、時限バッチはサーバーレスの適性が高い一方で、長時間のストリーミングや低レイテンシの恒常接続が求められるケースでは、マネージドコンテナや専用ホストと組み合わせるハイブリッド設計が現実的です。

研究データでは、関数実行のコールドスタートはランタイム・パッケージサイズ・ネットワーク設定(VPCなど)に強く影響されます³。Node.jsやPythonの軽量関数ではおおむねp50が50〜120ms、p95が150〜400ms程度のレンジに収まる観測が一般的で、VPC接続や大型依存の増加で悪化します³。ここでのp50/p95は応答時間の中央値/95パーセンタイルを指し、遅い方の裾野を把握するための実務指標です。Javaや.NETは工夫なしでは遅延が大きめですが、プロビジョンドコンカレンシー(事前起動の同時実行枠)やスナップショット起動(例:JavaのSnapStart)の活用で80〜90%の起動遅延削減が報告されています⁴。これらは公式ドキュメントや公開事例に基づく傾向値で、チューニング余地が大きい領域です。

ベンダーを跨いでも共通する原則

どのクラウドでも、イベントを小さく保ち、関数を単機能で疎結合に設計し、状態はマネージドストアに外出しし、ネットワークやパッケージを最小に保つほど、レイテンシとコストが安定します。加えて、観測性(メトリクス・ログ・トレーシングによる可視化)は最初から内蔵するのが鉄則で、分散トレーシングコンテキスト(リクエストの経路を追跡)、構造化ログ、メトリクスの三点をコードとIaCに組み込むことが、スケール時の運用コストを下げます⁶。ServerlessでもDevOpsのプラクティスは変わらず有効です。

どこで線を引くか:サーバーレスを選ぶ条件

ピークが読めずアイドルが多い、規制要件を満たすマネージドサービスが揃っている、コールドスタートの影響がビジネスSLO(Service Level Objective。サービスの目標品質)内に収まる、そしてチームがIaCとCI/CDで設定をコード化できる。この四つが揃うと、サーバーレスは技術的合理性とビジネス合理性を同時に満たします。逆に、1リクエストあたり数秒以上の計算が常態で,かつ継続的セッションが必要な場合は、サーバーレス関数に閉じるよりも、キューワーカーのスケールアウトやマネージドコンテナのオートスケールを前提とした設計が素直です。

開発・運用はどう変わるか:責務の再配置とSLO設計

開発はAPI境界とイベントスキーマ設計が中心になります。テーブル設計だけでなく、イベントのバージョニングポリシー、リトライとデッドレタリング(失敗イベントの隔離キュー)、冪等性キー(同じ入力を一度だけ処理するための識別子)の扱いを最初に決めるほど、コード量は減りエラーパターンが単純化します。運用は“スケールさせる”から“スケールしてしまうことを制御する”に重心が移り、同時実行数の制限、外部APIのスロットリング、キュー深度の監視、そしてコストのSLO化が不可欠になります。コストSLOとは、例えば1取引あたりのプラットフォームコスト上限を定義し、ダッシュボードで実時間に近い粒度で追跡する考え方です。

チーム構造にも影響があります。単一のインフラチームが全ワークロードを横断管理するモデルから、ドメインチームが自分たちの関数群とステートマシンを自走するモデルに寄っていきます。このときガバナンスは中央でテンプレート化し、スタックポリシー、タグ、監査ログ、セキュアデフォルトを強制します。具体的には、IaCモジュールで暗号化必須、VPCエンドポイント経由のデータプレーン、IAM境界の最小権限を初期状態に持たせ、逸脱はレビューなしではできないようにします。これにより、開発速度を落とさずにセキュリティ姿勢を維持できます。

セキュリティとコンプライアンスの観点

パッチ適用の多くはクラウド側の責務になりますが、依存ライブラリとアプリケーションランタイムの脆弱性は開発側の責任です。関数ごとにSBOM(Software Bill of Materials。ソフトウェア部品表)を生成し、デプロイ前スキャンと本番のランタイム保護を併用します。シークレットはKMS連携のマネージドストアに置き、環境変数での平文注入は避けます。ゼロトラストの原則で、関数間通信もアイデンティティとポリシーで許可し、ネットワーク境界に過度に依存しない設計に寄せると、マルチアカウントやマルチリージョンでの拡張が容易になります。

主要パターンと実装例:API、イベント、バッチ、エッジ

抽象論に留めないため、6つの最小実装を示します。サンプルはいずれもエラーハンドリングと観測性を含み、依存のインポートを明示します。ここで示すのは理解促進のための最小例であり、実運用では監査、認可、テストの拡充を推奨します。

同期API(AWS Lambda + API Gateway、Node.js)

// runtime: nodejs20.x, ESM
import { captureAWSv3Client } from "aws-xray-sdk-client";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";

const ddb = captureAWSv3Client(new DynamoDBClient({}));

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  const requestId = event.requestContext.requestId;
  try {
    const id = event.pathParameters?.id;
    if (!id) return { statusCode: 400, body: JSON.stringify({ message: "id is required" }) };
    const res = await ddb.send(new GetItemCommand({ TableName: process.env.TABLE, Key: { pk: { S: id } } }));
    if (!res.Item) return { statusCode: 404, body: JSON.stringify({ message: "not found", requestId }) };
    return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: res.Item, requestId }) };
  } catch (err) {
    console.error("error", { requestId, err });
    return { statusCode: 500, body: JSON.stringify({ message: "internal error", requestId }) };
  }
};

APIのパスパラメータ検証と見つからない場合の404、内部エラーの500を明確にし、分散トレーシングを有効化しておくと、p95で200ms台のレスポンスでも根因追跡が容易です。VPC接続を避け、依存を必要最小限に保つことでコールドスタートのばらつきを抑えられます³。

非同期バッチ(S3イベント、Python)

# runtime: python3.11
import json
import os
import boto3
from PIL import Image
from botocore.exceptions import BotoCoreError, ClientError

s3 = boto3.client("s3")

def handler(event, context):
    try:
        rec = event["Records"][0]
        bucket = rec["s3"]["bucket"]["name"]
        key = rec["s3"]["object"]["key"]
        tmp = "/tmp/input"
        s3.download_file(bucket, key, tmp)
        with Image.open(tmp) as img:
            img.thumbnail((128, 128))
            out = "/tmp/out.png"
            img.save(out, "PNG")
        dest_bucket = os.environ.get("DEST_BUCKET")
        s3.upload_file(out, dest_bucket, f"thumbs/{key}.png")
        return {"status": "ok", "key": key}
    except (BotoCoreError, ClientError, KeyError, OSError) as e:
        print(json.dumps({"level": "error", "err": str(e), "key": key if 'key' in locals() else None}))
        raise

非同期処理はリトライ設定とデッドレターキューでデータ損失を防ぎます。冪等性を担保するため、出力キーに元のオブジェクトキーと固定のサフィックスを用い、同じイベントが二重に流れてきても上書き安全な設計にします。

インフラをコードで管理(Terraform、最小API)

terraform {
  required_providers { aws = { source = "hashicorp/aws", version = ">= 5.0" } }
}
provider "aws" { region = var.region }

resource "aws_iam_role" "lambda_role" {
  name = "api-lambda-role"
  assume_role_policy = data.aws_iam_policy_document.assume.json
}

data "aws_iam_policy_document" "assume" {
  statement { actions = ["sts:AssumeRole"], principals { type = "Service", identifiers = ["lambda.amazonaws.com"] } }
}

resource "aws_lambda_function" "api" {
  function_name = "api-get"
  filename      = "dist.zip"
  handler       = "index.handler"
  runtime       = "nodejs20.x"
  role          = aws_iam_role.lambda_role.arn
  environment { variables = { TABLE = aws_dynamodb_table.items.name } }
}

resource "aws_apigatewayv2_api" "http" { name = "api" protocol_type = "HTTP" }
resource "aws_apigatewayv2_integration" "int" {
  api_id           = aws_apigatewayv2_api.http.id
  integration_type = "AWS_PROXY"
  integration_uri  = aws_lambda_function.api.invoke_arn
}
resource "aws_apigatewayv2_route" "route" { api_id = aws_apigatewayv2_api.http.id route_key = "GET /items/{id}" target = "integrations/${aws_apigatewayv2_integration.int.id}" }

IaCでロール、関数、HTTP API、ルートまでを宣言することで、リージョンや環境間の再現性が担保されます。最初からタグとポリシーを組み込むとコスト配賦や監査も滑らかです。

エッジでの軽量ロジック(Cloudflare Workers、TypeScript)

// wrangler.toml でKVバインドを設定済み
import { Hono } from 'hono'

const app = new Hono()
app.get('/config/:key', async (c) => {
  const key = c.req.param('key')
  const value = await c.env.KV.get(key)
  if (!value) return c.notFound()
  return c.json({ key, value })
})

export default app

エッジ実行は地理的にユーザーに近く、静的に近い読み取りやA/B判定、認証前の軽量チェックに有効です。起動遅延が極小で、p50が一桁ミリ秒台になることも珍しくありません⁷。

イベント消費(Google Cloud Functions、Go + Pub/Sub)

package functions

import (
  "context"
  "log"
)

type PubSubMessage struct { Data []byte `json:"data"` }

func Consume(ctx context.Context, m PubSubMessage) error {
  if len(m.Data) == 0 { return nil }
  log.Printf("event bytes=%d", len(m.Data))
  // business logic here
  return nil
}

Goはコールドスタートのオーバーヘッドが小さく、CPU集約の軽作業にも向きます。複数クラウドで同一のイベント駆動スタイルを採ると、学習資産が再利用できます。

ワークフローのオーケストレーション(Step Functions)

{
  "StartAt": "Validate",
  "States": {
    "Validate": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:...:function:validate",
      "Next": "Process",
      "Catch": [{"ErrorEquals": ["States.ALL"], "Next": "Fail"}]
    },
    "Process": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:...:function:process",
      "End": true
    },
    "Fail": {"Type": "Fail"}
  }
}

分岐、リトライ、タイムアウト、補償処理を宣言で持てるため、関数側は純粋なビジネスロジックに集中できます。観測性と失敗の局所化に寄与し、平均修復時間(MTTR)の短縮につながります。

性能・コスト・SLOの現実と意思決定

性能については、関数のパッケージを10MB未満に保ち、コネクションプールを使い回し、VPCが必要ならVPCレスの代替(DynamoDBやS3などのサービスエンドポイント)を優先するだけで、p95レイテンシが数十パーセント改善することが珍しくありません⁵。プロビジョンドコンカレンシーを最小で維持しピーク直前だけ引き上げる運用は、コールドスタートの分散を抑えながらコストを下げる現実解です。スループットは同時実行数と外部依存のスロットリングでほぼ決まり、外部APIのクォータが律速になる場合は、キューで平滑化しバックプレッシャーを明示的に設計します。

コストの試算をもう少し踏み込みます。Lambdaにおけるメモリ128MB、実行50ms、1億回/月の想定では、GB秒課金は0.128GB × 0.05秒 × 1億 ≒ 640,000GB秒で、単価$0.0000166667/GB秒なら約$10.67、リクエスト課金は最初の100万無料を差し引いて約$19.8、合算で**$30前後**です¹²。データ転送料や付随サービスの費用は別途であり、リージョンや価格改定による変動も考慮が必要です。常時稼働の仮想サーバーがアイドル9割の想定と比べると、アイドルコストの差がROIを左右します。運用人件費まで含めれば、深夜帯の障害当番やOSパッチの手当が不要になる分、年間のTCOが有利になると示されるケースもあります。

ベンチマークと観測性:小さく測り、すぐ直す

APIのp50/p95を継続計測するには、関数内にタイミングを仕込み、外からの負荷試験で追い込みます。以下はk6でHTTP APIを測る最小スクリプトです。

import http from 'k6/http'
import { check, sleep } from 'k6'

export const options = { vus: 50, duration: '1m' }
export default function () {
  const res = http.get(__ENV.URL + '/items/123')
  check(res, { 'status is 200': (r) => r.status === 200 })
  sleep(0.1)
}

本番相当の環境で、p95がビジネスSLO(例えば500ms)以内に収まるか、バースト時に外部依存のスロットリングを踏んでいないか、コストメトリクスと合わせてダッシュボード化すると、回帰を早期に検出できます。構造化ログのフィールドにリクエストID、ユーザーIDのハッシュ、ビジネスキーを入れるのも、集計とインシデント対応の定石です⁶。

導入期間の目安と組織インパクト

練度のあるチームであれば、単一ドメインのCRUD APIと非同期ワーカー、観測性、最低限のガバナンスを含むプロダクション品質の最初のリリースは、テンプレートとIaCの再利用を前提に2〜4週間で到達可能という報告例が多く見られます。既存モノリスからの段階移行はドメイン単位でイベントを切り出し、ストラングラーパターンでラップしていくのが現実解で、3〜6カ月の計画で主要な機能を段階移設する例も一般的です。人材面では、従来のOS運用スキルをクラウドのアイデンティティ、ネットワーク、セキュリティポリシー、IaCへとスライドさせるリスキリング投資が、移行の成否を分けます。

まとめ:小さく始めて、測り続けるサーバーレス

サーバーレスは魔法ではありませんが、適切な境界と観測性を備えれば、少人数チームでも大きな可用性と俊敏性を両立できます。アイドルコストを最小化し、スパイクに自動で追随し、運用の多くをコード化するという性質は、プロダクトの探索とスケールの両局面に効きます。だからこそ、最初の一歩は小さく、シンプルなドメインでAPIとイベントを立て、p50/p95と単位コストをダッシュボードで見える化し、SLOに照らして設計を磨くのが近道です。あなたのチームが今抱えるボトルネックは、容量計画でしょうか、デプロイの調整でしょうか、それとも夜間の運用負荷でしょうか。どれであっても、サーバーレスは試すに値します。次は、既存イベント駆動の基礎とアンチパターンを整理した解説を参照し、今日作った最小関数をIaCのテンプレートに昇格させてください。内部ドキュメントとテンプレートを磨き込むほど、次のサービスは速く、安全に生まれてきます。

関連トピックもあわせてどうぞ。イベント駆動の設計原則、観測性の基礎、クラウド時代の権限設計で詳述しています。

参考文献

  1. AWS Lambda 料金
  2. AWS Lambda 料金(コンピューティング料金の計算例)
  3. Serverless research overview (arXiv 2023)
  4. Reducing Java cold starts on AWS Lambda functions with SnapStart
  5. Best practices for organizing larger serverless applications
  6. Observability using Amazon CloudWatch and AWS X-Ray for serverless modern applications
  7. Eliminating cold starts with Cloudflare Workers