この記事の対象読者
- サーバー代を限りなくゼロに近づけたい人
- EC2やECSの運用に疲れた人
- 「デプロイが怖い」から解放されたい人
- 小さく始めて、スケールする設計を学びたい人
この記事では、サーバーレスが激安な理由、Lambdaのバージョン・エイリアス管理、Blue-Greenデプロイの実装、そして**「UIを捨てる」設計思想**まで解説します。
サーバーレスが「激安」な理由
従量課金の本質
【従来のサーバー】
24時間365日稼働 = 24時間365日課金
使っていない深夜も、アクセスがない時間も課金
【サーバーレス】
リクエストがあった時だけ課金
0リクエスト = 0円
コスト比較(月間10万リクエスト想定)
| 項目 | EC2 (t3.micro) | Lambda |
|---|---|---|
| 基本料金 | 約$8.5/月 | $0 |
| リクエスト | - | $0.02(100万リクエストまで無料) |
| 実行時間 | - | 約$0.20(128MB×100ms×10万回) |
| 月額合計 | 約$8.5 | 約$0.22 |
約40倍の差。
無料枠がすごい
AWS Lambda 無料枠(毎月):
- 100万リクエスト
- 40万GB秒の実行時間
これで足りるサービスは多い。
月額 $0 でAPIが動く。
サーバーレスアーキテクチャの全体像
┌─────────────────────────────────────────────────────────────┐
│ クライアント │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ API Gateway │
│ (HTTPS エンドポイント) │
│ │
│ /api/users ─────→ Lambda (users) │
│ /api/orders ─────→ Lambda (orders) │
│ /api/products ───→ Lambda (products) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Lambda Functions │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ users │ │ orders │ │products │ │
│ │ 128MB │ │ 256MB │ │ 128MB │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼────────────┼────────────┼───────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ DynamoDB / Aurora Serverless │
│ (従量課金DB) │
└─────────────────────────────────────────────────────────────┘
第1部:Lambdaのバージョンとエイリアス
なぜバージョン管理が必要か
問題:Lambda を直接更新すると...
デプロイ前: 正常に動作
↓ デプロイ
デプロイ後: バグがある!
↓
全ユーザーに影響 💀
戻し方が分からない 💀💀
解決策: バージョンとエイリアス
バージョンとは
バージョン は、Lambdaコードの スナップショット(不変のコピー) です。
Lambda: my-function
├── $LATEST (常に最新、変更可能)
├── Version 1 (不変)
├── Version 2 (不変)
├── Version 3 (不変)
└── Version 4 (不変) ← 現在の最新
# バージョンを発行
aws lambda publish-version \
--function-name my-function \
--description "v1.2.0 - ユーザー認証機能追加"
# バージョン一覧
aws lambda list-versions-by-function \
--function-name my-function
エイリアスとは
エイリアス は、特定のバージョンを指す ポインタ(名前付きの参照) です。
Lambda: my-function
│
├── Alias: prod ────→ Version 3 (安定版)
├── Alias: dev ────→ Version 4 (最新版)
└── Alias: staging ─→ Version 4
デプロイ時:
1. Version 5 を作成
2. dev エイリアスを Version 5 に向ける
3. 検証OK後、prod を Version 5 に向ける
# エイリアスを作成
aws lambda create-alias \
--function-name my-function \
--name prod \
--function-version 3 \
--description "本番環境"
# エイリアスを更新(別バージョンに向ける)
aws lambda update-alias \
--function-name my-function \
--name prod \
--function-version 4
# エイリアス一覧
aws lambda list-aliases \
--function-name my-function
エイリアスのARN
# $LATEST を直接呼び出し(非推奨)
arn:aws:lambda:ap-northeast-1:123456789:function:my-function
# バージョンを指定
arn:aws:lambda:ap-northeast-1:123456789:function:my-function:3
# エイリアスを指定(推奨)
arn:aws:lambda:ap-northeast-1:123456789:function:my-function:prod
arn:aws:lambda:ap-northeast-1:123456789:function:my-function:dev
API Gateway からはエイリアスを呼ぶ。
第2部:Blue-Greenデプロイ
Blue-Greenデプロイとは
【従来のデプロイ】
旧バージョン ──(停止)──> 新バージョン
↓
ダウンタイム発生
【Blue-Greenデプロイ】
Blue (現行) ────────────────> そのまま稼働
↓ 切り替え
Green (新版) ───────────────> 新しいトラフィック
ダウンタイムなし、即座にロールバック可能
Lambdaでの実現
【デプロイ前】
prod エイリアス ──→ Version 3 (Blue)
dev エイリアス ──→ Version 4
【デプロイ】
1. 新しいコードをアップロード
2. Version 5 を発行 (Green)
3. dev を Version 5 に向ける
4. 検証
5. prod を Version 5 に向ける ← 本番切り替え
【ロールバック(問題発生時)】
prod を Version 3 に戻す ← 即座に復旧
トラフィックシフト(カナリアリリース)
# prod エイリアスのトラフィックを分割
# 90% → Version 3(現行)
# 10% → Version 5(新版)
aws lambda update-alias \
--function-name my-function \
--name prod \
--function-version 3 \
--routing-config '{"AdditionalVersionWeights": {"5": 0.1}}'
# 問題なければ 100% 切り替え
aws lambda update-alias \
--function-name my-function \
--name prod \
--function-version 5 \
--routing-config '{}'
トラフィック配分:
┌────────────────────────────────────────────────────┐
│ ██████████████████████████████████████████ 90% │ → Version 3
│ ████ 10% │ → Version 5
└────────────────────────────────────────────────────┘
第3部:実践的な構成
ディレクトリ構成
my-serverless-app/
├── terraform/ # インフラ定義
│ ├── main.tf
│ ├── lambda.tf
│ ├── api_gateway.tf
│ ├── dynamodb.tf
│ └── variables.tf
├── src/ # Lambdaコード
│ ├── handlers/
│ │ ├── users.py
│ │ ├── orders.py
│ │ └── products.py
│ ├── lib/
│ │ ├── db.py
│ │ └── utils.py
│ └── requirements.txt
├── scripts/
│ ├── deploy.sh
│ ├── rollback.sh
│ └── promote.sh
├── tests/
│ └── ...
└── .github/
└── workflows/
└── deploy.yml
Terraform でインフラ定義
lambda.tf
# Lambda関数
resource "aws_lambda_function" "api" {
function_name = "${var.project_name}-api"
role = aws_iam_role.lambda_role.arn
handler = "handlers.main.handler"
runtime = "python3.11"
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
memory_size = 128
timeout = 10
environment {
variables = {
DYNAMODB_TABLE = aws_dynamodb_table.main.name
ENVIRONMENT = "production"
}
}
# $LATEST は直接使わない
publish = true
lifecycle {
ignore_changes = [
# エイリアスで管理するため、これらの変更は無視
filename,
source_code_hash,
]
}
}
# dev エイリアス(最新バージョンを指す)
resource "aws_lambda_alias" "dev" {
name = "dev"
function_name = aws_lambda_function.api.function_name
function_version = aws_lambda_function.api.version
lifecycle {
ignore_changes = [function_version]
}
}
# prod エイリアス(安定バージョンを指す)
resource "aws_lambda_alias" "prod" {
name = "prod"
function_name = aws_lambda_function.api.function_name
function_version = aws_lambda_function.api.version
lifecycle {
# デプロイスクリプトで管理するため無視
ignore_changes = [function_version, routing_config]
}
}
# Lambda用IAMロール
resource "aws_iam_role" "lambda_role" {
name = "${var.project_name}-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy" "dynamodb_access" {
name = "dynamodb-access"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
]
Resource = aws_dynamodb_table.main.arn
}]
})
}
api_gateway.tf
# API Gateway (HTTP API - 安い)
resource "aws_apigatewayv2_api" "main" {
name = "${var.project_name}-api"
protocol_type = "HTTP"
cors_configuration {
allow_origins = ["*"]
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers = ["Content-Type", "Authorization"]
}
}
# prod ステージ(prod エイリアスを使用)
resource "aws_apigatewayv2_stage" "prod" {
api_id = aws_apigatewayv2_api.main.id
name = "prod"
auto_deploy = true
stage_variables = {
lambdaAlias = "prod"
}
}
# dev ステージ(dev エイリアスを使用)
resource "aws_apigatewayv2_stage" "dev" {
api_id = aws_apigatewayv2_api.main.id
name = "dev"
auto_deploy = true
stage_variables = {
lambdaAlias = "dev"
}
}
# Lambda統合(エイリアスを動的に参照)
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.main.id
integration_type = "AWS_PROXY"
integration_method = "POST"
# ステージ変数でエイリアスを切り替え
integration_uri = "arn:aws:lambda:${var.aws_region}:${data.aws_caller_identity.current.account_id}:function:${aws_lambda_function.api.function_name}:$${stageVariables.lambdaAlias}"
}
# ルート定義
resource "aws_apigatewayv2_route" "default" {
api_id = aws_apigatewayv2_api.main.id
route_key = "$default"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
# Lambda実行権限(prod)
resource "aws_lambda_permission" "api_gateway_prod" {
statement_id = "AllowAPIGatewayProd"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api.function_name
qualifier = aws_lambda_alias.prod.name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
}
# Lambda実行権限(dev)
resource "aws_lambda_permission" "api_gateway_dev" {
statement_id = "AllowAPIGatewayDev"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api.function_name
qualifier = aws_lambda_alias.dev.name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
}
# 出力
output "api_endpoint_prod" {
value = "${aws_apigatewayv2_api.main.api_endpoint}/prod"
}
output "api_endpoint_dev" {
value = "${aws_apigatewayv2_api.main.api_endpoint}/dev"
}
dynamodb.tf
# DynamoDB(従量課金モード = 激安)
resource "aws_dynamodb_table" "main" {
name = "${var.project_name}-table"
billing_mode = "PAY_PER_REQUEST" # オンデマンド課金
hash_key = "PK"
range_key = "SK"
attribute {
name = "PK"
type = "S"
}
attribute {
name = "SK"
type = "S"
}
# GSIが必要な場合
global_secondary_index {
name = "GSI1"
hash_key = "GSI1PK"
range_key = "GSI1SK"
projection_type = "ALL"
}
attribute {
name = "GSI1PK"
type = "S"
}
attribute {
name = "GSI1SK"
type = "S"
}
tags = {
Environment = "production"
}
}
デプロイスクリプト
scripts/deploy.sh
#!/bin/bash
set -e
# 設定
FUNCTION_NAME="my-app-api"
ALIAS_DEV="dev"
ALIAS_PROD="prod"
echo "=== Deploying Lambda ==="
# 1. コードをパッケージング
echo "Packaging code..."
cd src
pip install -r requirements.txt -t ./package
cp -r handlers lib package/
cd package
zip -r ../../lambda.zip .
cd ../..
# 2. Lambda を更新
echo "Updating Lambda function..."
aws lambda update-function-code \
--function-name $FUNCTION_NAME \
--zip-file fileb://lambda.zip \
--publish
# 3. 最新バージョンを取得
LATEST_VERSION=$(aws lambda list-versions-by-function \
--function-name $FUNCTION_NAME \
--query 'Versions[-1].Version' \
--output text)
echo "Published version: $LATEST_VERSION"
# 4. dev エイリアスを更新
echo "Updating dev alias to version $LATEST_VERSION..."
aws lambda update-alias \
--function-name $FUNCTION_NAME \
--name $ALIAS_DEV \
--function-version $LATEST_VERSION
echo "=== Deployment complete ==="
echo "dev endpoint is now on version $LATEST_VERSION"
echo ""
echo "To promote to prod, run:"
echo " ./scripts/promote.sh $LATEST_VERSION"
scripts/promote.sh
#!/bin/bash
set -e
FUNCTION_NAME="my-app-api"
ALIAS_PROD="prod"
VERSION=$1
if [ -z "$VERSION" ]; then
echo "Usage: $0 <version>"
echo "Example: $0 5"
exit 1
fi
# 現在のprodバージョンを取得(ロールバック用)
CURRENT_PROD_VERSION=$(aws lambda get-alias \
--function-name $FUNCTION_NAME \
--name $ALIAS_PROD \
--query 'FunctionVersion' \
--output text)
echo "=== Promoting to Production ==="
echo "Current prod version: $CURRENT_PROD_VERSION"
echo "New version: $VERSION"
echo ""
read -p "Are you sure? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Aborted"
exit 1
fi
# カナリアリリース(10%から開始)
echo "Starting canary release (10%)..."
aws lambda update-alias \
--function-name $FUNCTION_NAME \
--name $ALIAS_PROD \
--function-version $CURRENT_PROD_VERSION \
--routing-config "{\"AdditionalVersionWeights\": {\"$VERSION\": 0.1}}"
echo "Waiting 60 seconds for monitoring..."
sleep 60
# エラー率をチェック(CloudWatch)
# 簡易版:手動確認
read -p "Is the new version healthy? (yes/no): " HEALTHY
if [ "$HEALTHY" != "yes" ]; then
echo "Rolling back..."
aws lambda update-alias \
--function-name $FUNCTION_NAME \
--name $ALIAS_PROD \
--function-version $CURRENT_PROD_VERSION \
--routing-config '{}'
echo "Rolled back to version $CURRENT_PROD_VERSION"
exit 1
fi
# 100%に切り替え
echo "Promoting to 100%..."
aws lambda update-alias \
--function-name $FUNCTION_NAME \
--name $ALIAS_PROD \
--function-version $VERSION \
--routing-config '{}'
echo "=== Promotion complete ==="
echo "prod is now on version $VERSION"
echo ""
echo "To rollback, run:"
echo " ./scripts/rollback.sh $CURRENT_PROD_VERSION"
scripts/rollback.sh
#!/bin/bash
set -e
FUNCTION_NAME="my-app-api"
ALIAS_PROD="prod"
VERSION=$1
if [ -z "$VERSION" ]; then
echo "Usage: $0 <version>"
exit 1
fi
echo "=== Rolling back to version $VERSION ==="
aws lambda update-alias \
--function-name $FUNCTION_NAME \
--name $ALIAS_PROD \
--function-version $VERSION \
--routing-config '{}'
echo "=== Rollback complete ==="
GitHub Actions CI/CD
.github/workflows/deploy.yml
name: Deploy Lambda
on:
push:
branches:
- main
- develop
workflow_dispatch:
inputs:
promote_version:
description: 'Version to promote to prod'
required: false
env:
AWS_REGION: ap-northeast-1
FUNCTION_NAME: my-app-api
jobs:
deploy:
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r src/requirements.txt -t src/package
cp -r src/handlers src/lib src/package/
- name: Package Lambda
run: |
cd src/package
zip -r ../../lambda.zip .
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to Lambda
id: deploy
run: |
# コード更新 & バージョン発行
VERSION=$(aws lambda update-function-code \
--function-name $FUNCTION_NAME \
--zip-file fileb://lambda.zip \
--publish \
--query 'Version' \
--output text)
echo "version=$VERSION" >> $GITHUB_OUTPUT
# dev エイリアスを更新
aws lambda update-alias \
--function-name $FUNCTION_NAME \
--name dev \
--function-version $VERSION
echo "Deployed version $VERSION to dev"
- name: Run integration tests
run: |
# dev エンドポイントでテスト
curl -f https://xxx.execute-api.ap-northeast-1.amazonaws.com/dev/health
- name: Notify
run: |
echo "Version ${{ steps.deploy.outputs.version }} deployed to dev"
echo "To promote to prod, run promote workflow"
promote:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' && github.event.inputs.promote_version != ''
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Promote to prod
run: |
VERSION=${{ github.event.inputs.promote_version }}
# 現在のバージョンを保存
CURRENT=$(aws lambda get-alias \
--function-name $FUNCTION_NAME \
--name prod \
--query 'FunctionVersion' \
--output text)
echo "Promoting version $VERSION (current: $CURRENT)"
# prod エイリアスを更新
aws lambda update-alias \
--function-name $FUNCTION_NAME \
--name prod \
--function-version $VERSION
echo "Promoted to prod"
第4部:Lambda コード例
handlers/main.py
import json
import os
from lib.db import DynamoDB
from lib.utils import response, parse_body
db = DynamoDB(os.environ['DYNAMODB_TABLE'])
def handler(event, context):
"""
メインハンドラー
API Gatewayからのリクエストをルーティング
"""
path = event.get('rawPath', '/')
method = event.get('requestContext', {}).get('http', {}).get('method', 'GET')
# ルーティング
routes = {
('GET', '/health'): health_check,
('GET', '/users'): list_users,
('POST', '/users'): create_user,
('GET', '/users/{id}'): get_user,
('PUT', '/users/{id}'): update_user,
('DELETE', '/users/{id}'): delete_user,
}
# パスパラメータを抽出
path_params = event.get('pathParameters', {}) or {}
# ルート検索
for (route_method, route_path), handler_func in routes.items():
if method == route_method and match_path(path, route_path):
try:
return handler_func(event, path_params)
except Exception as e:
print(f"Error: {e}")
return response(500, {'error': 'Internal server error'})
return response(404, {'error': 'Not found'})
def match_path(actual: str, pattern: str) -> bool:
"""簡易パスマッチング"""
actual_parts = actual.strip('/').split('/')
pattern_parts = pattern.strip('/').split('/')
if len(actual_parts) != len(pattern_parts):
return False
for a, p in zip(actual_parts, pattern_parts):
if p.startswith('{') and p.endswith('}'):
continue
if a != p:
return False
return True
def health_check(event, params):
"""ヘルスチェック"""
return response(200, {
'status': 'healthy',
'version': os.environ.get('AWS_LAMBDA_FUNCTION_VERSION', 'unknown')
})
def list_users(event, params):
"""ユーザー一覧"""
users = db.query('USER#', limit=100)
return response(200, {'users': users})
def create_user(event, params):
"""ユーザー作成"""
body = parse_body(event)
user_id = db.generate_id()
item = {
'PK': f'USER#{user_id}',
'SK': 'PROFILE',
'id': user_id,
'name': body.get('name'),
'email': body.get('email'),
}
db.put(item)
return response(201, {'user': item})
def get_user(event, params):
"""ユーザー取得"""
user_id = params.get('id')
user = db.get(f'USER#{user_id}', 'PROFILE')
if not user:
return response(404, {'error': 'User not found'})
return response(200, {'user': user})
def update_user(event, params):
"""ユーザー更新"""
user_id = params.get('id')
body = parse_body(event)
updated = db.update(
f'USER#{user_id}',
'PROFILE',
body
)
return response(200, {'user': updated})
def delete_user(event, params):
"""ユーザー削除"""
user_id = params.get('id')
db.delete(f'USER#{user_id}', 'PROFILE')
return response(204, None)
lib/db.py
import boto3
import uuid
from typing import Any, Dict, List, Optional
class DynamoDB:
def __init__(self, table_name: str):
self.table = boto3.resource('dynamodb').Table(table_name)
def generate_id(self) -> str:
return str(uuid.uuid4())
def get(self, pk: str, sk: str) -> Optional[Dict]:
response = self.table.get_item(Key={'PK': pk, 'SK': sk})
return response.get('Item')
def put(self, item: Dict) -> None:
self.table.put_item(Item=item)
def update(self, pk: str, sk: str, updates: Dict) -> Dict:
update_expr = 'SET ' + ', '.join(f'#{k} = :{k}' for k in updates.keys())
expr_names = {f'#{k}': k for k in updates.keys()}
expr_values = {f':{k}': v for k, v in updates.items()}
response = self.table.update_item(
Key={'PK': pk, 'SK': sk},
UpdateExpression=update_expr,
ExpressionAttributeNames=expr_names,
ExpressionAttributeValues=expr_values,
ReturnValues='ALL_NEW'
)
return response['Attributes']
def delete(self, pk: str, sk: str) -> None:
self.table.delete_item(Key={'PK': pk, 'SK': sk})
def query(self, pk_prefix: str, limit: int = 100) -> List[Dict]:
response = self.table.query(
KeyConditionExpression='begins_with(PK, :pk)',
ExpressionAttributeValues={':pk': pk_prefix},
Limit=limit
)
return response.get('Items', [])
lib/utils.py
import json
from typing import Any, Dict, Optional
def response(status_code: int, body: Optional[Dict]) -> Dict:
"""API Gateway レスポンスを生成"""
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
'body': json.dumps(body) if body else ''
}
def parse_body(event: Dict) -> Dict:
"""リクエストボディをパース"""
body = event.get('body', '{}')
if event.get('isBase64Encoded'):
import base64
body = base64.b64decode(body).decode('utf-8')
return json.loads(body) if body else {}
第5部:UIを捨てる設計思想
「管理画面がない」ことの価値
従来の考え方
「管理画面を作らないと運用できない」
「UIがないとユーザーが使えない」
「ダッシュボードは必須」
発想の転換
管理画面を作る = 開発コスト + 保守コスト + セキュリティリスク
本当に必要なのは「データの操作」であって「UI」ではない
UIがない方が顧客が喜ぶケース
ケース1:バッチ連携システム
【UI あり】
顧客: 毎日手動で画面からデータをアップロード
→ 人的ミス、作業時間、担当者依存
【UI なし(API連携)】
顧客のシステム → API → 自動処理
→ 完全自動化、24時間稼働、ミスなし
顧客の声:「毎日の作業がなくなって助かる」
ケース2:IoT/センサーデータ
【UI あり】
端末 → 管理画面 → 人が確認 → 対応
【UI なし(イベント駆動)】
端末 → API → Lambda → 異常検知 → 自動通知
→ 人間の介在なしで24時間監視
ケース3:決済・課金システム
【UI あり】
管理者が画面から手動で請求処理
→ 月末に残業、ミスのリスク
【UI なし(自動化)】
月末 → EventBridge → Lambda → 請求処理 → メール送信
→ 完全自動、ミスなし、深夜でも動く
API-Firstの設計
┌─────────────────────────────────────────────────────────────┐
│ API (Lambda) │
│ │
│ 全ての操作は API 経由 │
│ UI は API の「1つの消費者」に過ぎない │
│ │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Web UI │ │ 外部連携 │ │ CLI │
│(必要なら)│ │ システム │ │ ツール │
└─────────┘ └─────────┘ └─────────┘
↑
これは後から
作ってもいい
UIを作らない場合の代替手段
1. CLIツール
#!/usr/bin/env python3
# cli.py - 管理用CLIツール
import click
import requests
API_BASE = "https://xxx.execute-api.ap-northeast-1.amazonaws.com/prod"
@click.group()
def cli():
pass
@cli.command()
def list_users():
"""ユーザー一覧を表示"""
response = requests.get(f"{API_BASE}/users")
users = response.json()['users']
for user in users:
click.echo(f"{user['id']}: {user['name']} ({user['email']})")
@cli.command()
@click.argument('name')
@click.argument('email')
def create_user(name, email):
"""ユーザーを作成"""
response = requests.post(f"{API_BASE}/users", json={
'name': name,
'email': email
})
user = response.json()['user']
click.echo(f"Created: {user['id']}")
@cli.command()
@click.argument('user_id')
def delete_user(user_id):
"""ユーザーを削除"""
response = requests.delete(f"{API_BASE}/users/{user_id}")
click.echo(f"Deleted: {user_id}")
if __name__ == '__main__':
cli()
# 使用例
./cli.py list-users
./cli.py create-user "田中太郎" "tanaka@example.com"
./cli.py delete-user abc123
2. Slackbot
# Slack から操作できるようにする
import os
from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
@app.command("/user-list")
def list_users(ack, respond):
ack()
users = fetch_users() # API呼び出し
respond(f"ユーザー数: {len(users)}")
@app.command("/user-create")
def create_user(ack, respond, command):
ack()
name, email = command['text'].split()
user = create_user_api(name, email)
respond(f"作成しました: {user['id']}")
handler = SlackRequestHandler(app)
def lambda_handler(event, context):
return handler.handle(event, context)
3. スプレッドシート連携
# Google Sheets をUIとして使う
import gspread
from google.oauth2.service_account import Credentials
def sync_to_sheet():
"""DynamoDBのデータをスプレッドシートに同期"""
gc = gspread.authorize(credentials)
sheet = gc.open("ユーザー管理").sheet1
users = fetch_all_users()
# ヘッダー
sheet.update('A1', [['ID', 'Name', 'Email', 'Created']])
# データ
rows = [[u['id'], u['name'], u['email'], u['created_at']] for u in users]
sheet.update('A2', rows)
コスト計算例
月間1万リクエストの場合
Lambda:
リクエスト: 10,000回 × $0.0000002 = $0.002
実行時間: 10,000回 × 100ms × 128MB = 128,000 GB-ms
128,000 / 1024 / 1000 = 0.125 GB秒
0.125 × $0.0000166667 = $0.000002
合計: 約 $0.002(ほぼ無料枠内)
API Gateway (HTTP API):
10,000回 × $1.00/100万 = $0.01
DynamoDB (オンデマンド):
読み取り: 10,000回 × $0.25/100万 = $0.0025
書き込み: 5,000回 × $1.25/100万 = $0.00625
合計: 約 $0.01
月額合計: 約 $0.02(約3円)
月間100万リクエストの場合
Lambda:
リクエスト: 無料枠内
実行時間: 約 $1.67
API Gateway:
100万 × $1.00/100万 = $1.00
DynamoDB:
読み取り: 100万 × $0.25/100万 = $0.25
書き込み: 50万 × $1.25/100万 = $0.625
合計: 約 $0.88
月額合計: 約 $3.55(約500円)
まとめ
サーバーレスの利点
1. 激安(使った分だけ課金)
2. スケール自動(設定不要)
3. 運用不要(サーバー管理なし)
4. デプロイ簡単(コードをアップロードするだけ)
Blue-Greenデプロイのポイント
1. バージョン: コードのスナップショット(不変)
2. エイリアス: バージョンへのポインタ
3. dev/prod: エイリアスで環境を分離
4. ロールバック: エイリアスを戻すだけ(秒速)
5. カナリア: トラフィックを徐々にシフト
UIを捨てる判断基準
✅ 顧客が本当にUIを使うか?
✅ 自動化で置き換えられないか?
✅ CLI/Slack/スプレッドシートで十分では?
✅ UIの開発・保守コストは妥当か?
心がけ
1. 小さく始める(Lambda 1個から)
2. バージョンを必ず発行する
3. prod は直接変更しない
4. ロールバック手順を用意する
5. UIは最後の手段