27
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TerraformとSAMで実現するモダンAWSアーキテクチャ 構築ガイドライン

Last updated at Posted at 2025-10-30

はじめに

TerraformとAWS SAM(Serverless Application Model)を組み合わせることで、インフラストラクチャとアプリケーションの責務を明確に分離し、チーム別の適切な権限管理とセキュアなIaC運用を実現できます。

この構成の核心的なメリット

  • アプリ開発者: SAMで頻繁に更新されるLambda関数を迅速にデプロイ
  • インフラチーム: Terraformでネットワークや重要なリソースを厳格に管理
  • セキュリティ: 最小権限の原則に基づいた権限分離でリスクを軽減

本記事では、実際に動作する完全なサンプルプロジェクトを通して、以下を解説します:

  • 基礎編: TerraformとSAMの責務分離とチーム別権限管理
  • 実践編: VPC、DynamoDB、複数環境対応の実装
  • 運用編: モニタリング、トラブルシューティング、CI/CD
  • 検証結果: 実際のデプロイと動作確認

この記事で学べること

✅ TerraformとSAMの適切な使い分け
✅ VPC内Lambdaのセキュアな構成
✅ DynamoDB Single Table Designの実装
✅ 環境別(dev/staging/prod)の管理方法
✅ CloudWatchによる監視とアラート設定
✅ GitHub Actionsを使ったCI/CD構築
✅ 実際に発生するエラーと解決方法

サンプルプロジェクト

完全なソースコードは以下で公開しています:
https://github.com/higakikeita/test

アーキテクチャ図(編集可能):
https://github.com/higakikeita/test/blob/main/docs/architecture.drawio

なぜTerraform + SAMなのか?

それぞれの強み

ツール 得意なこと 苦手なこと
Terraform インフラ全体の管理、他サービスとの統合 Lambdaのビルド、ローカルテスト
SAM Lambdaの開発・デプロイ、ローカルテスト VPCなど汎用的なインフラ管理

責務分離の原則

┌─────────────────────────────────────┐
│        Terraform (インフラ層)         │
│      👷 インフラチームが管理            │
├─────────────────────────────────────┤
│ • VPC / サブネット / セキュリティG     │
│ • DynamoDB テーブル / IAM ロール      │
│ • S3 バケット / CloudWatch 設定       │
│                                     │
│ 変更頻度: 低 (週次〜月次)              │
│ 影響範囲: 大 (セキュリティ・ネットワーク) │
└─────────────────────────────────────┘
              ↓ outputs
┌─────────────────────────────────────┐
│       SAM (アプリケーション層)         │
│      👨‍💻 アプリ開発者が管理            │
├─────────────────────────────────────┤
│ • Lambda 関数 / Lambda レイヤー       │
│ • API Gateway / イベントソース         │
│ • アプリケーションロジック              │
│                                     │
│ 変更頻度: 高 (日次〜時間単位)           │
│ 影響範囲: 小 (アプリケーション内)       │
└─────────────────────────────────────┘

チーム別の責務分離によるセキュアなIaC管理

この構成の最大のメリットは、チームの役割に応じた適切な権限分離が実現できることです。

🏗️ インフラチーム(Terraform)

管理対象:

  • ネットワーク構成(VPC、サブネット、セキュリティグループ)
  • データストア(DynamoDB、RDS等)
  • IAMロール・ポリシー
  • 監視・ログ基盤(CloudWatch)

特徴:

  • 変更頻度が低い(週次〜月次)
  • セキュリティに直接影響する設定
  • 本番環境への影響範囲が大きい
  • レビュープロセスが厳格

権限:

# インフラチームのみが実行可能
terraform apply -var-file=environments/prod.tfvars

👨‍💻 アプリケーション開発チーム(SAM)

管理対象:

  • Lambda関数のコード
  • API Gatewayの設定
  • Lambda レイヤー
  • イベントソース(DynamoDB Streams、EventBridge)

特徴:

  • 変更頻度が高い(日次〜時間単位)
  • アプリケーションロジックの改善・バグ修正
  • インフラへの影響は最小限
  • 迅速なデプロイが可能

権限:

# アプリ開発者が自由に実行可能
sam deploy --stack-name my-app-dev

🔒 セキュアなIaC管理のメリット

  1. 最小権限の原則

    • アプリ開発者はVPCやIAMを変更できない
    • インフラチームはアプリの頻繁なデプロイに関与しない
  2. 変更管理の分離

    • インフラ変更:厳格なレビュー・承認プロセス
    • アプリ変更:迅速なCI/CDパイプライン
  3. セキュリティリスクの軽減

    • 不要な権限昇格を防止
    • ネットワーク設定への意図しない変更を防止
    • IAMロールの誤変更を防止
  4. 開発速度の向上

    • アプリ開発者はインフラを気にせず開発に集中
    • Lambda関数の更新を待ち時間なくデプロイ
  5. 監査・コンプライアンス対応

    • 誰が何を変更したか明確
    • 変更履歴がGitで追跡可能
    • 環境別の権限管理が容易

実装例:権限分離

# GitHub Actions - インフラデプロイ(main ブランチのみ)
deploy-infrastructure:
  if: github.ref == 'refs/heads/main'
  environment: production
  # インフラチームのみが承認可能

# GitHub Actions - アプリデプロイ(feature ブランチでも可)
deploy-application:
  if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main'
  # アプリ開発者が自由にデプロイ可能

この構成により、セキュリティを維持しながら、開発速度を最大化することができます。

アーキテクチャ概要

今回構築するシステムのアーキテクチャは以下の通りです:

システムアーキテクチャ図

Python の diagrams ライブラリで自動生成された、AWS公式アイコンを使用したアーキテクチャ図です。

📋 シンプル版

基本的なデータフローを一目で理解できる簡潔な図です。

システム概要

🏗️ 詳細版

すべてのコンポーネントとその関係性を詳細に表示した図です。

Terraform + SAM アーキテクチャ

🔄 データフロー詳細

リクエストから応答までのデータの流れを層別に表示した図です。

データフロー詳細

]

主要コンポーネント詳細

1. Lambda Functions

  • API Function (256MB, ARM64, 30s timeout)

    • REST API エンドポイント
    • CRUD操作、バリデーション
    • エラーハンドリング
  • Processor Function (256MB, ARM64)

    • DynamoDB Streams イベント処理
    • バッチサイズ: 10、ウィンドウ: 5秒
    • リトライ設定、DLQ有効
  • Scheduled Function (256MB, ARM64, 60s timeout)

    • 定期実行タスク(毎日 UTC 00:00)
    • メンテナンス、データクリーンアップ

2. DynamoDB Table

  • 設計: Single Table Design
  • キー: PK (String), SK (String)
  • GSI: EntityTypeIndex, GSI1
  • Streams: NEW_AND_OLD_IMAGES
  • 課金: PAY_PER_REQUEST
  • PITR: 本番環境のみ有効

3. VPC構成

  • CIDR: 環境別 (dev: 10.0.0.0/16)
  • Public Subnets: NAT Gateway配置
  • Private Subnets: Lambda配置
  • VPC Endpoints: S3, DynamoDB (無料)
  • NAT Gateway: dev=1個、prod=2個

4. 監視・アラート

  • CloudWatch Logs: 7-90日保持
  • Metrics: Lambda, DynamoDB, API Gateway
  • Alarms: エラー、スロットリング検知
  • Dashboard: 統合ビュー

プロジェクト構成

terraform-sam-demo/
├── terraform/              # インフラ定義
│   ├── main.tf
│   ├── variables.tf
│   ├── outputs.tf
│   ├── iam.tf             # IAMロール・ポリシー
│   ├── vpc.tf             # VPC設定
│   ├── dynamodb.tf        # DynamoDBテーブル
│   ├── cloudwatch.tf      # 監視設定
│   └── environments/      # 環境別設定
│       ├── dev.tfvars
│       ├── staging.tfvars
│       └── prod.tfvars
├── sam/                   # SAMアプリケーション
│   ├── template.yaml      # SAMテンプレート
│   ├── functions/         # Lambda関数
│   │   ├── api/
│   │   │   ├── index.py
│   │   │   └── requirements.txt
│   │   └── processor/
│   │       ├── index.py
│   │       └── requirements.txt
│   ├── layers/            # Lambda レイヤー
│   │   └── common/
│   └── events/            # テストイベント
├── scripts/               # スクリプト
│   ├── deploy.sh          # デプロイスクリプト
│   ├── validate.sh        # 検証スクリプト
│   └── generate_diagrams.py  # 図の自動生成
├── .github/workflows/     # CI/CD
│   └── deploy.yml
└── docs/                  # ドキュメント
    ├── architecture.md
    ├── TROUBLESHOOTING.md
    ├── BEST_PRACTICES.md
    └── images/            # アーキテクチャ図
        ├── architecture.png
        ├── architecture_simple.png
        ├── dataflow.png
        └── README.md      # 図の生成方法

アーキテクチャ図の自動生成

このプロジェクトでは、Python の diagrams ライブラリを使用してAWS公式アイコンのアーキテクチャ図を自動生成しています。

生成方法

# 必要なツールのインストール
brew install graphviz
pip3 install diagrams

# 図の生成
python3 scripts/generate_diagrams.py

実行すると、docs/images/ に以下の3つのPNG画像が生成されます:

  • architecture_simple.png - シンプルな概要図
  • architecture.png - 詳細なフル構成図
  • dataflow.png - データフロー詳細図

図の特徴

レイアウトの工夫:

graph_attr = {
    "splines": "ortho",    # 直角の美しい線
    "nodesep": "0.8",      # ノード間の間隔
    "ranksep": "1.0",      # 階層間の余白
}

色分けによる視覚化:

# メインフロー
users >> Edge(color="darkblue", style="bold", label="HTTPS") >> apigw
apigw >> Edge(color="darkgreen", style="bold", label="Invoke") >> lambda_api

# Stream処理
dynamodb >> Edge(color="orange", style="bold", label="Streams") >> lambda_processor

# ロギング
lambda_api >> Edge(color="gray", style="dotted") >> cloudwatch

メリット:

  • コードで管理できるため、変更履歴が追跡可能
  • 構成変更時に自動で再生成
  • AWS公式アイコンでプロフェッショナルな仕上がり
  • バージョン管理が容易

カスタマイズ

scripts/generate_diagrams.py を編集することで、簡単にカスタマイズできます:

# 新しいAWSサービスの追加例
from diagrams.aws.network import CloudFront
from diagrams.aws.security import WAF

# 図に追加
cloudfront = CloudFront("CloudFront")
waf = WAF("WAF")

詳しくは diagrams公式ドキュメント を参照してください。

実装:Terraformでインフラ構築

1. VPC設定(vpc.tf)

VPC内にLambdaを配置することで、セキュアな環境を実現します。

# VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${local.resource_prefix}-vpc"
  }
}

# プライベートサブネット(Lambda配置用)
resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = {
    Name = "${local.resource_prefix}-private-subnet-${count.index + 1}"
  }
}

# NAT Gateway(Lambda から外部API アクセス用)
resource "aws_nat_gateway" "main" {
  count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.public_subnet_cidrs)) : 0

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "${local.resource_prefix}-nat-${count.index + 1}"
  }
}

# VPCエンドポイント(コスト削減)
resource "aws_vpc_endpoint" "s3" {
  vpc_id       = aws_vpc.main.id
  service_name = "com.amazonaws.${var.aws_region}.s3"

  route_table_ids = concat(
    [aws_route_table.public.id],
    aws_route_table.private[*].id
  )

  tags = {
    Name = "${local.resource_prefix}-s3-endpoint"
  }
}

ポイント:

  • NAT Gatewayは開発環境では単一、本番環境では各AZに配置
  • S3/DynamoDBはVPCエンドポイント経由でアクセス(無料 & 高速)
  • セキュリティグループでアウトバウンドのみ許可

2. DynamoDBテーブル(dynamodb.tf)

シングルテーブル設計で複数のエンティティを効率的に管理します。

resource "aws_dynamodb_table" "main" {
  name           = local.dynamodb_table_name
  billing_mode   = var.dynamodb_billing_mode
  hash_key       = "PK"
  range_key      = "SK"

  attribute {
    name = "PK"
    type = "S"
  }

  attribute {
    name = "SK"
    type = "S"
  }

  attribute {
    name = "EntityType"
    type = "S"
  }

  attribute {
    name = "CreatedAt"
    type = "N"
  }

  # GSI: エンティティタイプ別クエリ用
  global_secondary_index {
    name            = "EntityTypeIndex"
    hash_key        = "EntityType"
    range_key       = "CreatedAt"
    projection_type = "ALL"
  }

  # DynamoDB Streams(Processor Lambda用)
  stream_enabled   = var.enable_dynamodb_streams
  stream_view_type = "NEW_AND_OLD_IMAGES"

  # Point-in-Time Recovery(本番環境)
  point_in_time_recovery {
    enabled = var.enable_dynamodb_point_in_time_recovery
  }

  # TTL設定
  ttl {
    enabled        = true
    attribute_name = "ExpiresAt"
  }
}

シングルテーブル設計の例:

# ユーザーエンティティ
PK: USER#123, SK: METADATA
EntityType: User
Name: "John Doe"

# ユーザーの注文
PK: USER#123, SK: ORDER#456
EntityType: Order
Amount: 1000

3. IAMロール(iam.tf)

最小権限の原則に基づいて、Lambda用のIAMロールを作成します。

# Lambda API Function用ロール
resource "aws_iam_role" "lambda_api" {
  name               = "${local.resource_prefix}-lambda-api-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}

# DynamoDBアクセスポリシー
data "aws_iam_policy_document" "lambda_dynamodb_access" {
  statement {
    effect = "Allow"
    actions = [
      "dynamodb:GetItem",
      "dynamodb:Query",
      "dynamodb:PutItem",
      "dynamodb:UpdateItem",
      "dynamodb:DeleteItem"
    ]
    resources = [
      aws_dynamodb_table.main.arn,
      "${aws_dynamodb_table.main.arn}/index/*"
    ]
  }
}

resource "aws_iam_role_policy" "lambda_api_dynamodb" {
  role   = aws_iam_role.lambda_api.id
  policy = data.aws_iam_policy_document.lambda_dynamodb_access.json
}

セキュリティのポイント:

  • リソースARNを明示的に指定(* を使用しない)
  • 必要最小限のアクションのみ許可
  • Conditionでさらに制限可能

4. CloudWatch設定(cloudwatch.tf)

監視とアラートを設定します。

# ロググループ
resource "aws_cloudwatch_log_group" "lambda_api" {
  name              = "/aws/lambda/${local.lambda_function_prefix}-api"
  retention_in_days = var.log_retention_days
}

# Lambda エラーアラーム
resource "aws_cloudwatch_metric_alarm" "lambda_api_errors" {
  alarm_name          = "${local.resource_prefix}-lambda-api-errors"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = 300
  statistic           = "Sum"
  threshold           = 5

  dimensions = {
    FunctionName = "${local.lambda_function_prefix}-api"
  }
}

# CloudWatch Dashboard
resource "aws_cloudwatch_dashboard" "main" {
  dashboard_name = "${local.resource_prefix}-dashboard"

  dashboard_body = jsonencode({
    widgets = [
      {
        type = "metric"
        properties = {
          metrics = [
            ["AWS/Lambda", "Invocations"],
            [".", "Errors"],
            [".", "Duration"]
          ]
          period = 300
          stat   = "Average"
          region = var.aws_region
          title  = "Lambda Metrics"
        }
      }
    ]
  })
}

5. Outputs(outputs.tf)

SAMで使用する値を出力します。

output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

output "lambda_security_group_id" {
  value = aws_security_group.lambda.id
}

output "lambda_api_role_arn" {
  value = aws_iam_role.lambda_api.arn
}

output "dynamodb_table_name" {
  value = aws_dynamodb_table.main.name
}

output "sam_artifacts_bucket" {
  value = aws_s3_bucket.sam_artifacts.id
}

# SAM用デプロイコマンドを生成
output "sam_deploy_command" {
  value = <<-EOT
    sam deploy \
      --stack-name ${local.resource_prefix}-app \
      --s3-bucket ${aws_s3_bucket.sam_artifacts.id} \
      --parameter-overrides \
        VpcId=${aws_vpc.main.id} \
        SubnetIds=${join(",", aws_subnet.private[*].id)}
  EOT
}

実装:SAMでアプリケーション構築

1. SAMテンプレート(template.yaml)

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

# Terraformから渡されるパラメータ
Parameters:
  Environment:
    Type: String
  VpcId:
    Type: String
  SubnetIds:
    Type: CommaDelimitedList
  SecurityGroupId:
    Type: String
  LambdaApiRoleArn:
    Type: String
  DynamoDBTableName:
    Type: String

# グローバル設定
Globals:
  Function:
    Runtime: python3.11
    Timeout: 30
    MemorySize: 256
    Architectures:
      - arm64  # Graviton2(20%コスト削減)
    Environment:
      Variables:
        ENVIRONMENT: !Ref Environment
        DYNAMODB_TABLE: !Ref DynamoDBTableName
        LOG_LEVEL: INFO
    VpcConfig:
      SecurityGroupIds:
        - !Ref SecurityGroupId
      SubnetIds: !Ref SubnetIds
    Tracing: Active  # X-Ray有効化

Resources:
  # Lambda レイヤー(共通ライブラリ)
  CommonLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub ${Environment}-common-layer
      ContentUri: layers/common/
      CompatibleRuntimes:
        - python3.11

  # API Lambda Function
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub terraform-sam-demo-${Environment}-api
      CodeUri: functions/api/
      Handler: index.lambda_handler
      Role: !Ref LambdaApiRoleArn
      Layers:
        - !Ref CommonLayer
      Events:
        GetItems:
          Type: Api
          Properties:
            Path: /items
            Method: GET
        CreateItem:
          Type: Api
          Properties:
            Path: /items
            Method: POST
        GetItem:
          Type: Api
          Properties:
            Path: /items/{id}
            Method: GET

  # Processor Lambda Function (DynamoDB Streams)
  ProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub terraform-sam-demo-${Environment}-processor
      CodeUri: functions/processor/
      Handler: index.lambda_handler
      Role: !Ref LambdaProcessorRoleArn
      Events:
        DynamoDBStream:
          Type: DynamoDB
          Properties:
            Stream: !Ref DynamoDBStreamArn
            StartingPosition: LATEST
            BatchSize: 10

Outputs:
  ApiEndpoint:
    Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}

2. API Lambda関数(functions/api/index.py)

import json
import os
import boto3
from boto3.dynamodb.conditions import Key
import logging

logger = logging.getLogger()
logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO'))

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['DYNAMODB_TABLE'])

def create_response(status_code, body):
    """API Gateway レスポンスを作成"""
    return {
        'statusCode': status_code,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        'body': json.dumps(body, default=str)
    }

def get_items(event):
    """GET /items - アイテム一覧取得"""
    try:
        response = table.query(
            IndexName='EntityTypeIndex',
            KeyConditionExpression=Key('EntityType').eq('Item'),
            Limit=20
        )

        items = response.get('Items', [])
        logger.info(f"Retrieved {len(items)} items")

        return create_response(200, {
            'items': items,
            'count': len(items)
        })
    except Exception as e:
        logger.error(f"Error: {str(e)}")
        return create_response(500, {'error': str(e)})

def create_item(event):
    """POST /items - アイテム作成"""
    try:
        body = json.loads(event['body'])

        import uuid
        item_id = str(uuid.uuid4())

        item = {
            'PK': f'ITEM#{item_id}',
            'SK': 'METADATA',
            'EntityType': 'Item',
            'ItemId': item_id,
            'Name': body['name'],
            'CreatedAt': int(time.time())
        }

        table.put_item(Item=item)
        logger.info(f"Created item: {item_id}")

        return create_response(201, {
            'message': 'Item created',
            'item': item
        })
    except Exception as e:
        logger.error(f"Error: {str(e)}")
        return create_response(500, {'error': str(e)})

def lambda_handler(event, context):
    """Lambda エントリーポイント"""
    logger.info(f"Event: {json.dumps(event)}")

    method = event['httpMethod']
    path = event['path']

    if path == '/items' and method == 'GET':
        return get_items(event)
    elif path == '/items' and method == 'POST':
        return create_item(event)
    else:
        return create_response(404, {'error': 'Not found'})

実装のポイント:

  • boto3クライアントはグローバルスコープで初期化(コネクション再利用)
  • 構造化ログでCloudWatch Logsでの検索を容易に
  • エラーハンドリングを適切に実装

3. Processor Lambda関数(functions/processor/index.py)

import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def process_insert(new_image):
    """INSERT イベント処理"""
    logger.info(f"New item created: {new_image.get('ItemId')}")
    # 通知送信、集計処理など

def process_modify(old_image, new_image):
    """MODIFY イベント処理"""
    logger.info(f"Item modified: {new_image.get('ItemId')}")
    # 変更内容の分析、通知など

def lambda_handler(event, context):
    """DynamoDB Streams イベント処理"""
    logger.info(f"Processing {len(event['Records'])} records")

    for record in event['Records']:
        event_name = record['eventName']

        if event_name == 'INSERT':
            new_image = record['dynamodb']['NewImage']
            process_insert(new_image)
        elif event_name == 'MODIFY':
            old_image = record['dynamodb']['OldImage']
            new_image = record['dynamodb']['NewImage']
            process_modify(old_image, new_image)

    return {'statusCode': 200}

デプロイ手順

1. 前提条件

# 必要なツールのインストール確認
terraform --version  # >= 1.5.0
sam --version        # >= 1.100.0
aws --version        # >= 2.0

# AWS認証情報の設定
aws configure

2. Terraformでインフラ構築

cd terraform

# 初期化
terraform init

# プランの確認
terraform plan -var-file=environments/dev.tfvars

# 適用
terraform apply -var-file=environments/dev.tfvars

# 出力値を保存(SAMで使用)
terraform output -json > ../sam/terraform-outputs.json

実行結果:

Apply complete! Resources: 45 added, 0 changed, 0 destroyed.

Outputs:

vpc_id = "vpc-0123456789abcdef0"
private_subnet_ids = [
  "subnet-0123456789abcdef0",
  "subnet-0123456789abcdef1",
]
dynamodb_table_name = "terraform-sam-demo-dev-data"
sam_artifacts_bucket = "terraform-sam-demo-dev-sam-artifacts-123456789012"

3. SAMでアプリケーションデプロイ

cd ../sam

# ビルド
sam build

# ローカルテスト(オプション)
sam local invoke ApiFunction -e events/event.json

# デプロイ
sam deploy \
  --stack-name terraform-sam-demo-dev-app \
  --s3-bucket $(cat terraform-outputs.json | jq -r '.sam_artifacts_bucket.value') \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides \
    Environment=dev \
    VpcId=$(cat terraform-outputs.json | jq -r '.vpc_id.value') \
    SubnetIds=$(cat terraform-outputs.json | jq -r '.private_subnet_ids.value | join(",")') \
    # ...その他のパラメータ

実行結果:

Successfully created/updated stack - terraform-sam-demo-dev-app

Outputs:
Key                 ApiEndpoint
Description         API Gateway endpoint URL
Value               https://abc123def.execute-api.ap-northeast-1.amazonaws.com/dev

4. デプロイスクリプトを使用(推奨)

# 一括デプロイ
./scripts/deploy.sh dev

# Terraformのみ
./scripts/deploy.sh dev --tf-only

# SAMのみ
./scripts/deploy.sh dev --sam-only

動作確認

APIエンドポイントのテスト

# ヘルスチェック
curl https://abc123def.execute-api.ap-northeast-1.amazonaws.com/dev/health

# アイテム一覧取得
curl https://abc123def.execute-api.ap-northeast-1.amazonaws.com/dev/items

# アイテム作成
curl -X POST https://abc123def.execute-api.ap-northeast-1.amazonaws.com/dev/items \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Item"}'

レスポンス例:

{
  "message": "Item created",
  "item": {
    "PK": "ITEM#123e4567-e89b-12d3-a456-426614174000",
    "SK": "METADATA",
    "EntityType": "Item",
    "ItemId": "123e4567-e89b-12d3-a456-426614174000",
    "Name": "Test Item",
    "CreatedAt": 1704067200
  }
}

CloudWatch Logsの確認

# リアルタイムでログを確認
aws logs tail /aws/lambda/terraform-sam-demo-dev-api --follow

# エラーログのみフィルタ
aws logs tail /aws/lambda/terraform-sam-demo-dev-api --filter-pattern "ERROR"

CloudWatch Metricsの確認

# Lambda実行回数
aws cloudwatch get-metric-statistics \
  --namespace AWS/Lambda \
  --metric-name Invocations \
  --dimensions Name=FunctionName,Value=terraform-sam-demo-dev-api \
  --start-time 2024-01-01T00:00:00Z \
  --end-time 2024-01-01T23:59:59Z \
  --period 3600 \
  --statistics Sum

環境別管理

開発環境(dev)

# terraform/environments/dev.tfvars
environment = "dev"

# コスト削減設定
enable_nat_gateway = true
single_nat_gateway = true  # 単一NAT Gateway

dynamodb_billing_mode = "PAY_PER_REQUEST"
log_retention_days = 7
enable_lambda_insights = false

本番環境(prod)

# terraform/environments/prod.tfvars
environment = "prod"

# 高可用性設定
enable_nat_gateway = true
single_nat_gateway = false  # 各AZにNAT Gateway

dynamodb_billing_mode = "PAY_PER_REQUEST"
enable_dynamodb_point_in_time_recovery = true

log_retention_days = 90
enable_lambda_insights = true

環境の切り替え

# dev環境
./scripts/deploy.sh dev

# staging環境
./scripts/deploy.sh staging

# prod環境
./scripts/deploy.sh prod

CI/CD: GitHub Actions

ワークフロー設定(.github/workflows/deploy.yml)

name: Deploy to AWS

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - uses: aws-actions/setup-sam@v2

      - name: Terraform Validate
        run: |
          cd terraform
          terraform init -backend=false
          terraform validate

      - name: SAM Validate
        run: |
          cd sam
          sam validate

  deploy-dev:
    needs: validate
    if: github.ref == 'refs/heads/develop'
    environment: dev
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - 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: ap-northeast-1

      - name: Deploy
        run: ./scripts/deploy.sh dev

GitHub Secrets の設定

  1. GitHubリポジトリ → Settings → Secrets and variables → Actions
  2. 以下を追加:
    • AWS_ACCESS_KEY_ID
    • AWS_SECRET_ACCESS_KEY

トラブルシューティング

よくあるエラーと解決方法

1. Lambda がタイムアウトする

症状:

Task timed out after 30.00 seconds

原因と解決方法:

  1. VPC Lambda でインターネット接続ができない
# NAT Gatewayが存在するか確認
aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=<vpc-id>"

# プライベートサブネットのルートテーブルを確認
aws ec2 describe-route-tables --filters "Name=association.subnet-id,Values=<subnet-id>"
  1. タイムアウト設定を増やす
# sam/template.yaml
Globals:
  Function:
    Timeout: 60  # 30 → 60に変更

2. CloudFormation スタックが ROLLBACK_COMPLETE

症状:

Error: Stack is in ROLLBACK_COMPLETE state

解決方法:

# 失敗の原因を確認
aws cloudformation describe-stack-events \
  --stack-name terraform-sam-demo-dev-app \
  --max-items 20

# スタックを削除して再作成
aws cloudformation delete-stack --stack-name terraform-sam-demo-dev-app
aws cloudformation wait stack-delete-complete --stack-name terraform-sam-demo-dev-app

# 再デプロイ
sam deploy

3. DynamoDB アクセス権限エラー

症状:

AccessDeniedException: User is not authorized

解決方法:

# terraform/iam.tf でポリシーを確認
data "aws_iam_policy_document" "lambda_dynamodb_access" {
  statement {
    effect = "Allow"
    actions = [
      "dynamodb:GetItem",
      "dynamodb:PutItem"
    ]
    resources = [
      aws_dynamodb_table.main.arn  # 特定のテーブルのみ
    ]
  }
}

詳細は TROUBLESHOOTING.md を参照。

コスト見積もり

開発環境(月間)

リソース 数量 単価 月額
NAT Gateway 1 $32.40 $32.40
Lambda (100万実行) - $0.20 $0.20
API Gateway (100万) - $3.50 $3.50
DynamoDB (少量) - - $1.00
CloudWatch Logs - - $0.50
合計 $37.60

コスト削減のポイント

  1. VPCエンドポイント活用

    • S3/DynamoDBはVPCエンドポイント経由(無料)
    • NAT Gatewayのトラフィック削減
  2. ARM64アーキテクチャ

    • Lambdaコストが20%削減
    • パフォーマンスも向上
  3. 単一NAT Gateway(開発環境)

    • 開発環境では単一構成でコスト半減
    • 本番環境では高可用性のため各AZに配置
  4. ログ保持期間の最適化

    • 開発: 7日
    • ステージング: 30日
    • 本番: 90日

ベストプラクティス

セキュリティ

IAM権限の最小化

  • リソースARNを明示的に指定
  • * の使用を避ける
  • Condition で制限を追加

シークレット管理

# AWS Secrets Manager を使用
import boto3

secretsmanager = boto3.client('secretsmanager')
response = secretsmanager.get_secret_value(SecretId='prod/api/key')
api_key = json.loads(response['SecretString'])['api_key']

VPCセキュリティ

  • Lambda は Private Subnet に配置
  • セキュリティグループでアウトバウンドのみ許可
  • VPCエンドポイントでAWSサービスアクセス

パフォーマンス

コネクションの再利用

# グローバルスコープで初期化
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['DYNAMODB_TABLE'])

def lambda_handler(event, context):
    # 接続を再利用
    table.put_item(Item=item)

バッチ操作

# BatchWriteItem で効率化
with table.batch_writer() as batch:
    for item in items:
        batch.put_item(Item=item)

モニタリング

必須アラーム

  • Lambda エラー率
  • API Gateway 5XXエラー
  • DynamoDB スロットリング
  • Lambda タイムアウト

X-Ray トレーシング

Globals:
  Function:
    Tracing: Active

まとめ

TerraformとAWS SAMを適切に組み合わせることで、以下が実現できました:

明確な責務分離

  • Terraform: インフラストラクチャ
  • SAM: アプリケーションロジック

環境別管理

  • dev/staging/prodの設定分離
  • tfvarsファイルで環境別設定

セキュアな構成

  • VPC内Lambda
  • IAM最小権限
  • シークレット管理

運用性

  • CloudWatch監視
  • アラート設定
  • CI/CDパイプライン

コスト最適化

  • VPCエンドポイント
  • ARM64アーキテクチャ
  • 適切なリソースサイジング

ドキュメント自動化

  • AWS公式アイコンを使ったアーキテクチャ図の自動生成
  • コードとしての図管理(diagrams library)
  • バージョン管理とレビューが容易

次のステップ

さらに機能を拡張する場合:

  1. 認証・認可

    • Cognito ユーザープール
    • API Gateway Authorizer
  2. 非同期処理

    • SQS キュー
    • Step Functions
  3. マルチリージョン

    • DynamoDB Global Tables
    • Route 53 フェイルオーバー
  4. モニタリング強化

    • OpenTelemetry
    • カスタムメトリクス

リポジトリ

完全なソースコードはこちら:
https://github.com/higakikeita/test

ドキュメント:

参考資料

AWS公式:

ツール:


質問やフィードバックがあれば、コメントやGitHub Issuesでお気軽にどうぞ!

27
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?