AWS Well-Architected を「小規模開発」に最適化する実践的アプローチ

|
AWS Well-Architected Architecture Best Practice 設計 中小企業

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 を理解している証明になる。

「全部入れる」は思考停止だ。「要件に対して最小コストで最大のリスク軽減をする」という判断軸を持つことが、実務でのアーキテクチャ設計の本質だ。

この記事をシェア

Twitter / X LinkedIn
記事一覧に戻る
🤖
Cloud Assistant
IBM Watson powered

こんにちは!クラウドエンジニアのポートフォリオサイトへようこそ。AWS構成・副業サービス・お仕事のご相談など、何でも聞いてください 👋