AWS Well-Architected を「小規模開発」に最適化する実践的アプローチ
AWSのWell-Architectedフレームワーク5つの柱を、中小企業向け静的サイト構成(S3+CloudFront+Lambda+SES)に当てはめる実践的アプローチ。
はじめに:Well-Architectedは「大企業向けのお作法」ではない
AWS Well-Architected Framework(以下 W-A)を初めて読んだとき、「これは数百人規模のチームが運用するシステムの話だ」と感じるエンジニアは多い。実際、ドキュメントは膨大で、全てを適用しようとすれば小規模プロジェクトでは逆にオーバーエンジニアリングになる。
しかし、小規模システムこそ事故率が高い。 理由は単純だ。
小規模システムが事故を起こしやすい構造:
❌ 「後で直す」が永続化する
→ 開発速度優先でセキュリティグループを 0.0.0.0/0 にしたまま
→ IAMに AdministratorAccess を付けたまま本番稼働
❌ 専任の運用担当がいない
→ 障害発生時に「誰が何をすべきか」が不明確
→ CloudWatchアラートが設定されておらずダウンに気づかない
❌ 「動いているから良い」の慣性
→ コスト異常に気づかず月末に請求書で発覚
→ 依存パッケージの脆弱性を放置
この記事では、W-A の5つの柱をLP+連絡フォーム構成(S3 + CloudFront + Lambda + SES)という実際の小規模構成に当てはめ、「何を・どこまでやるか」の取捨選択を示す。
対象アーキテクチャ:LP + 連絡フォームの構成
記事全体を通じて、次の構成を前提とする。
対象アーキテクチャ:
[ユーザー]
↓ HTTPS
[CloudFront]
├── /index.html, /assets/* → S3(静的サイト)
└── /api/contact → Lambda Function URL
↓
[SES] → メール送信
↓
[DynamoDB] → 送信記録保存
管理:
- Terraform でインフラ管理
- GitHub Actions で自動デプロイ
- Route53 + ACM でカスタムドメイン + HTTPS
月額コスト目安:$5〜$15(トラフィック規模による)。この規模に W-A をどう適用するかを柱ごとに見ていく。
柱1:運用上の優秀性(Operational Excellence)
小規模での優先度:⭐⭐⭐ 高
最も即効性が高い柱だ。「なにか起きたとき、誰が何をするか」をコードとして定義しておく。
やること:アラートの設定
# Terraform: CloudWatch アラート + SNS 通知
resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
alarm_name = "contact-form-lambda-errors"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
metric_name = "Errors"
namespace = "AWS/Lambda"
period = 300 # 5分
statistic = "Sum"
threshold = 1 # エラーが1件でもアラート
dimensions = {
FunctionName = aws_lambda_function.contact_form.function_name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "cloudfront_5xx" {
alarm_name = "lp-cloudfront-5xx-rate"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "5xxErrorRate"
namespace = "AWS/CloudFront"
period = 300
statistic = "Average"
threshold = 1.0 # 5xxエラー率 1% 超でアラート
dimensions = {
DistributionId = aws_cloudfront_distribution.lp.id
Region = "Global"
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
やること:デプロイ後の自動検証
# GitHub Actions: デプロイ後のスモークテスト
- name: Smoke Test
run: |
# トップページが 200 を返すか
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://example.com)
if [ "$STATUS" != "200" ]; then
echo "Smoke test failed: HTTP $STATUS"
exit 1
fi
# API エンドポイントが応答するか
API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST https://example.com/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"test","email":"test@example.com","message":"smoke test"}')
echo "API status: $API_STATUS"
やらなくて良いこと(小規模では過剰)
省略可能な項目:
- AWS Organizations の使用(単一アカウントで十分)
- Service Catalog によるテンプレート管理
- AWS Config による全リソースのコンプライアンス追跡
→ Terraform の state で代替できる
柱2:セキュリティ(Security)
小規模での優先度:⭐⭐⭐ 高
セキュリティだけはコストをかけても妥協できない。小規模でも適用すべき項目は多い。
やること:S3 バケットのパブリックアクセス遮断
resource "aws_s3_bucket_public_access_block" "lp" {
bucket = aws_s3_bucket.lp.id
# 全てのパブリックアクセスをブロック
# CloudFront OAC 経由のみ許可する
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# CloudFront から S3 へのアクセスを OAC で制御
resource "aws_cloudfront_origin_access_control" "lp" {
name = "lp-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
やること:Lambda の最小権限(具体例)
# 連絡フォーム Lambda に必要な権限だけを付与
data "aws_iam_policy_document" "lambda_policy" {
# SES: 特定の送信元アドレスからのみ送信可能
statement {
effect = "Allow"
actions = ["ses:SendEmail"]
resources = ["*"]
condition {
test = "StringEquals"
variable = "ses:FromAddress"
values = ["noreply@example.com"]
}
}
# DynamoDB: 特定テーブルへの書き込みのみ
statement {
effect = "Allow"
actions = ["dynamodb:PutItem"]
resources = [aws_dynamodb_table.contact_records.arn]
}
# CloudWatch Logs: このFunction専用のロググループのみ
statement {
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = [
"arn:aws:logs:${var.region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${var.function_name}:*"
]
}
}
やること:AWS WAF(Web Application Firewall)最小コスト構成
# AWS WAF(Web Application Firewall): レートリミットのみ有効化(月 $5 程度)
resource "aws_wafv2_web_acl" "lp" {
name = "lp-waf"
scope = "CLOUDFRONT"
default_action {
allow {}
}
rule {
name = "RateLimitRule"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
limit = 1000 # 5分間に同一IPから1000リクエストで遮断
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRule"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "lp-waf"
sampled_requests_enabled = true
}
}
やらなくて良いこと(小規模では過剰)
省略可能な項目:
- AWS GuardDuty(月 $30〜、単一アカウントの静的サイトには過剰)
- AWS Security Hub(GuardDuty・Config の統合ダッシュボード)
- AWS Macie(S3の個人情報検出、LPのassets bucketには不要)
- VPC フローログの全量保存(Lambda は VPC 外で良い)
柱3:信頼性(Reliability)
小規模での優先度:⭐⭐ 中
S3 + CloudFront の構成は、AWSが可用性を担保している。追加で実装すべき点は限定的だ。
やること:S3 バージョニング + ライフサイクル
resource "aws_s3_bucket_versioning" "lp" {
bucket = aws_s3_bucket.lp.id
versioning_configuration {
status = "Enabled"
}
}
# 古いバージョンのコストを抑える
resource "aws_s3_bucket_lifecycle_configuration" "lp" {
bucket = aws_s3_bucket.lp.id
rule {
id = "expire-old-versions"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 30 # 30日後に古いバージョンを削除
}
}
}
やること:Lambda の予約同時実行数の上限設定
resource "aws_lambda_function" "contact_form" {
# ... 省略 ...
# 同時実行の上限を設定(コスト上限・過負荷防止)
reserved_concurrent_executions = 10
}
Lambda のデフォルト同時実行上限はアカウント全体で 1,000。連絡フォーム程度のLambdaに無制限の枠を使わせる必要はない。上限を設定することで、万が一の大量リクエスト時に他のLambdaへの影響を防げる。
やらなくて良いこと(小規模では過剰)
省略可能な項目:
- マルチリージョン構成(S3+CloudFrontは元々グローバル)
- Aurora Serverless の Multi-AZ(連絡フォームにそこまでの可用性は不要)
- Dead Letter Queue(DLQ)の設定
→ 連絡フォームの送信失敗はメール通知で十分対応可能
柱4:パフォーマンス効率(Performance Efficiency)
小規模での優先度:⭐ 低〜中
S3 + CloudFront の構成は、追加対応なしで高パフォーマンスだ。確認すべき点のみ示す。
チェックリスト
# 1. CloudFront のキャッシュヒット率の確認
aws cloudwatch get-metric-statistics \
--namespace AWS/CloudFront \
--metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=<your-dist-id> Name=Region,Value=Global \
--start-time 2025-01-01T00:00:00Z \
--end-time 2025-01-08T00:00:00Z \
--period 86400 \
--statistics Average
# 理想: 90% 以上
# 低い場合: Cache-Control ヘッダーを確認する
# 2. Lambda のコールドスタート時間の確認
aws logs filter-log-events \
--log-group-name /aws/lambda/contact-form \
--filter-pattern "Init Duration" \
--query 'events[].message' \
| grep "Init Duration"
# Init Duration が出ている = コールドスタートが発生している
やること(コスト0で効果が高い)
# S3 へのアップロード時に適切な Cache-Control を設定する
# (GitHub Actions のデプロイスクリプトで指定)
# HTML: ブラウザキャッシュなし(常に最新を取得)
aws s3 sync ./dist s3://my-lp \
--exclude "*" --include "*.html" \
--cache-control "public, max-age=0, must-revalidate"
# JS/CSS: 長期キャッシュ(ハッシュ付きファイル名が前提)
aws s3 sync ./dist s3://my-lp \
--exclude "*.html" \
--cache-control "public, max-age=31536000, immutable"
柱5:コスト最適化(Cost Optimization)
小規模での優先度:⭐⭐⭐ 高
小規模案件ほど「気づいたら想定外の請求」が起きやすい。コスト管理は必須だ。
やること:Budget アラートの設定
resource "aws_budgets_budget" "monthly" {
name = "lp-monthly-budget"
budget_type = "COST"
limit_amount = "20" # 月 $20 を上限として設定
limit_unit = "USD"
time_unit = "MONTHLY"
notification {
comparison_operator = "GREATER_THAN"
threshold = 80 # 80% ($16) で警告
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = ["your-email@example.com"]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 100 # 100% 超過で緊急通知
threshold_type = "PERCENTAGE"
notification_type = "FORECASTED"
subscriber_email_addresses = ["your-email@example.com"]
}
}
やること:未使用リソースの検出
# 使われていない Elastic IP の確認(1個 $3.6/月)
aws ec2 describe-addresses \
--query 'Addresses[?AssociationId==`null`].[PublicIp, AllocationId]' \
--output table
# 空のS3バケット(誤作成の検出)
aws s3api list-buckets --query 'Buckets[].Name' --output text | \
xargs -I {} sh -c 'COUNT=$(aws s3 ls s3://{} | wc -l); echo "{}: $COUNT objects"'
# 停止中のEC2インスタンス(EBSコストが発生し続ける)
aws ec2 describe-instances \
--filters "Name=instance-state-name,Values=stopped" \
--query 'Reservations[].Instances[].[InstanceId,Tags[?Key==`Name`].Value|[0]]' \
--output table
取捨選択の整理
LP + 連絡フォーム構成に対する W-A 適用の優先度まとめ。
| 柱 | 優先度 | やること | やらないこと |
|---|---|---|---|
| 運用上の優秀性 | ⭐⭐⭐ | CloudWatchアラート / デプロイ後スモークテスト | Config / Service Catalog |
| セキュリティ | ⭐⭐⭐ | S3パブリックアクセス遮断 / Lambda最小権限 / AWS WAFレートリミット | GuardDuty / Macie |
| 信頼性 | ⭐⭐ | S3バージョニング / Lambda同時実行上限 | マルチリージョン / DLQ |
| パフォーマンス効率 | ⭐ | Cache-Controlヘッダー設定 / キャッシュヒット率モニタリング | Lambda Provisioned Concurrency |
| コスト最適化 | ⭐⭐⭐ | Budgetアラート / 未使用リソース定期検出 | Savings Plans(使用量が安定してから) |
Conclusion:「適用しない理由を言語化する」がアーキテクチャの成熟度を示す
W-A を使う上で最も重要なのは「全部やること」ではなく、**「何をやって、何をやらないか、その理由を言語化できること」**だ。
成熟したアーキテクチャの判断:
「GuardDutyは導入しない。
理由: 月$30以上のコストに対して、S3+CloudFrontの静的サイトでは
検出されるリスクの種類(EC2の不審なアクティビティ等)が
本構成には存在しない。同じコストで Budgetアラートと
AWS WAF レートリミットを入れる方が費用対効果が高い。」
これを言えること自体が、W-A を理解している証明になる。
「全部入れる」は思考停止だ。「要件に対して最小コストで最大のリスク軽減をする」という判断軸を持つことが、実務でのアーキテクチャ設計の本質だ。
この記事をシェア