0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】CloudFront + S3 + API Gateway + Lambda を Terraform で構築②【実装詳細】

Last updated at Posted at 2025-12-27

学習のために構築した環境を紹介します。
S3やLambdaなどの主要リソースおよびTerraformを触ったことがある人向けの記事です。
本記事は「①設計・構成解説編」と「②実装詳細編」の2章構成です。
本章では実装詳細の解説をします。

①設計・構成解説編はこちらです。

目次

コード全体は以下のリンクを参照ください。

1. 構成の説明

1-1. 何をしたいか

下記を実装し動作させることが今回の学習目的です。

  • 静的フロントエンドは S3 + CloudFront
  • 動的処理は API Gateway + Lambda
  • CloudFront を入口に集約
  • ACMを用いて証明書を発行してHTTPS化
  • API Key を CloudFront から付与
  • WAFによるセキュリティ強化
  • インフラは Terraform でコード管理

セキュリティを意識しつつ、サーバーレスで一般的に使われている構成にしました。

Lambda構成図.jpg


1-2. この構成を選んだ理由

  • CloudFront をS3, API Gatewayの前段に置く理由

    • OACによるS3の完全非公開化
    • HTTPS化
    • キャッシュによる高速化
    • S3, API GatewayのWAF の集約
    • S3, API Gatewayのドメインの集約
    • API Key を CloudFront から付与
  • API Gateway + Lambdaを使用する理由

    • サーバーレスで管理コストを下げたい
    • フロントと疎結合な API を提供したい

2. 作成方法

2-1. 前提条件

  • AWS アカウント
  • Route53 に パブリック Hosted Zoneを作成済み
  • Terraform
  • AWS CLI(認証済み)

2-2. 構築順序

Terraform上は依存関係で解決されますが、
マネジメントコンソールを使う場合は以下の順に作成します。

  1. Route53 Hosted Zone(既存)
  2. ACM(us-east-1)
  3. S3(静的サイト用)
  4. CloudFront(OAC + WAF)
  5. API Gateway
  6. Lambda
  7. CloudFront の API オリジン設定
  8. Route53 Alias レコード

2-3. 注意点

  1. CloudFront の ACM は必ず us-east-1
  2. API Gateway は Deployment を明示的に作らないと反映されない
  3. API Gateway の変更検知用に triggers を使っている
  4. CloudFront のキャッシュ挙動は パス単位で分ける

1. ACM は必ず us-east-1

CloudFrontでHTTPS(独自ドメイン)を使うためのSSL証明書は、必ずバージニア北部リージョン(us-east-1)で作成・管理しなければならないという制約があるためです。

2. API Gateway は Deployment を明示的に作らないと反映されない

API Gatewayの設定(リソースやメソッドの追加)を変更しても、デプロイを明示的に行わない限り、外部からのアクセスには反映されません。
API Gatewayは本番稼働中の環境に影響を与えずに設定をいじれるよう、「編集中の設定」と「公開済みの設定(ステージ)」が分離されているためです。

3. API Gateway の変更検知用に triggers を使っている

TerraformでAPI Gatewayの設定を変えたとき、自動的に新しいデプロイを走らせるためです。

aws_api_gateway_deployment リソース内の triggers ブロックで、メソッドやLambdaのIDなどを監視しています。これらの中身(ハッシュ値)が変わると、Terraformが再デプロイを実行してくれます。
これがないと、Terraform上でコードを書き換えて apply しても、API Gatewayの内部ではデプロイが実行されず、古い挙動のままになります。

4. CloudFront のキャッシュ挙動は パス単位で分ける

  • 静的コンテンツ (/index.html, /static/*): 頻繁に変わらないので、CloudFrontにキャッシュさせて高速化する。
  • APIリクエスト (/data): 常に最新のDB情報を返してほしいので、キャッシュさせない(Managed-CachingDisabled を使う)。

3. Terraform構成

3-1. フォルダ構成例

.
├── frontend/
│   └── index.html
├── lambda/
│   ├── backend.py
│   └── backend.zip
├── variable.tf
├── main.tf
├── outputs.tf
├── backend.tf
└── frontend.tf

※今回の学習目的から逸れるためモジュール化はしていません。


3-2. コード

パブリックホストゾーンの情報取得

data "aws_route53_zone" "primary" {
    name         = var.domain_name //ドメイン名を指定
    private_zone = false //取得対象がパブリックホストゾーン
}

ACM証明書発行

resource "aws_acm_certificate" "cert" {
    provider          = aws.us-east-1 # us-east-1を指定
    domain_name       = var.domain_name # ドメイン名を指定
    validation_method = "DNS" # DNS検証を指定
    subject_alternative_names = ["*.${var.domain_name}"] # www 付きのドメインも使用するのでサブドメインも含めて検証
    lifecycle {
        create_before_destroy = true # 既存の証明書を消す前に新規作成することでダウンタイムをなくす
    }
}

証明書の検証

resource "aws_route53_record" "cert_validation" {
    for_each = { # DNSレコード検証に必要な情報(レコードセット : レコード名、タイプ、値など) を格納
        for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
            name   = dvo.resource_record_name
            type   = dvo.resource_record_type
            record = dvo.resource_record_value
        }
    }
    allow_overwrite = true  # レコードの上書きを許可
    zone_id = data.aws_route53_zone.primary.zone_id # DNSレコードを作成するホストゾーンを指定
    name    = each.value.name # for_eachで取得したnameの値を取得
    type    = each.value.type # CNAMEなどのレコードタイプを取得
    ttl     = 60              # レコードのキャッシュ有効期限を60秒に設定
    records = [each.value.record] # DNSレコードの値を取得
}

ACM証明書の所有権を証明するために、Route 53にDNSレコードを登録するリソースです。

ステップ リソース名 役割
1. 申請 aws_acm_certificate 証明書をリクエストし、検証用レコードの情報を生成
2. 登録 aws_route53_record (今回のコード) 生成された情報を Route 53 の DNS レコードとして書き込む
3. 待機 aws_acm_certificate_validation レコードが反映され、ACMが「検証成功」の状態になるまで待機

パブリックアクセスブロック

resource "aws_s3_bucket_public_access_block" "frontend" {
    bucket = aws_s3_bucket.frontend.id

    block_public_acls       = true
    block_public_policy     = true
    ignore_public_acls      = true
    restrict_public_buckets = true
}

S3への直接アクセスを禁止し、CloudFrontのみを入り口にすることで、WAFによる保護を確実に適用できます。
また、S3の設定ミスによる情報の流出を防ぐことができます。


バケットポリシー

resource "aws_s3_bucket_policy" "frontend" {
    bucket = aws_s3_bucket.frontend.id # バケットを指定
    policy = data.aws_iam_policy_document.s3_policy.json # ポリシーを設定
}

# OACからのアクセスを許可するためのポリシーを定義
data "aws_iam_policy_document" "s3_policy" {
    statement {
       principals {
            type        = "Service"
            identifiers = ["cloudfront.amazonaws.com"]
        }

    actions = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.frontend.arn}/*"]
    
    condition {
    test     = "StringEquals"
    variable = "AWS:SourceAccount"
    values   = [data.aws_caller_identity.current.account_id]
    }
  }
}

CloudFrontからのアクセスのみ許可するバケットポリシーを設定しています。


API Gateway用のカスタムorigin request policy(カスタムヘッダー転送用)

resource "aws_cloudfront_origin_request_policy" "api_gateway_policy" {
  provider = aws.us-east-1
  name     = "api-gateway-policy"
  comment  = "Policy for API Gateway origin with custom headers"
  
  cookies_config {
    cookie_behavior = "none" # クッキーを一切転送しない
  }
  
  headers_config {
    header_behavior = "whitelist" # 指定した特定のヘッダーだけをAPI Gatewayに送る
    headers {
      items = ["Content-Type", "User-Agent", "Referer"]
    }
  }
  
  query_strings_config {
    query_string_behavior = "all" # 全てのクエリパラメータをそのままAPI Gatewayに送る
  }
}

CloudFrontが受け取ったリクエストをAPI Gateway(オリジン)に転送する際、どの情報を引き継ぐかを定義しています。
クエリパラメータを使ってデータをフィルタリングしたり検索したりするため、全て転送する必要があります。


OAC

resource "aws_cloudfront_origin_access_control" "frontend_oac" {
    name                              = "frontend-oac" # OAC設定につける名前
    description                       = "OAC for frontend distribution"
    signing_behavior                  = "always" # CloudFrontがS3バケットにアクセスする際に、常にリクエストに署名するよう指定
    signing_protocol                  = "sigv4" # リクエストに署名するために使用するプロトコルを指定
    origin_access_control_origin_type = "s3" # OAC設定が適用されるオリジンの種類を指定
}
  1. OACを作成
  2. CloudFrontでS3にアクセスする時にこのOACを使うように設定
  3. S3のバケットポリシーでこのOACを持っているCloudFrontからのアクセスのみ許可

CloudFrontディストリビューション

1. 基本設定・全般

resource "aws_cloudfront_distribution" "s3_distribution" {
    provider = aws.us-east-1
    enabled = true                                        # CloudFrontディストリビューションを有効化
    aliases = [var.domain_name, "www.${var.domain_name}"] # wwwありなし両方でCloudFrontディストリビューションにアクセス可能
    default_root_object = "index.html"                   # ルートパスアクセス時のデフォルトファイル

S3(静的コンテンツ)と API Gateway(動的API)を1つのドメインで共存させるための設定です。

2. オリジン設定

    # S3のオリジン設定
    origin {
        domain_name                = aws_s3_bucket.frontend.bucket_regional_domain_name # CloudFrontがコンテンツを取得するS3バケットのドメイン名を指定
        origin_id                  = aws_s3_bucket.frontend.id # オリジンを一意に識別するためのIDを指定
        origin_access_control_id   = aws_cloudfront_origin_access_control.frontend_oac.id # *オリジンアクセス制御(OAC)**のIDを指定
    }

    # API Gatewayのオリジン設定
    origin {
    origin_id = "api-gateway-origin"
    domain_name = "${aws_api_gateway_rest_api.api.id}.execute-api.${var.region}.amazonaws.com"
    origin_path = "/prod"  # ステージパスを指定
    # カスタムヘッダーでAPIキーを渡す設定
    custom_header {
      name  = "X-API-Key"
      value = aws_api_gateway_api_key.api_key.value
    }

    custom_origin_config {
    http_port              = 80
    https_port             = 443
    origin_protocol_policy = "https-only"
    origin_ssl_protocols   = ["TLSv1.2"]
    origin_read_timeout    = 60
    origin_keepalive_timeout = 5
      }
    }

項目 意味
S3オリジン フロントエンドのHTML/CSS/JSファイルを配置している場所
origin_access_control_id OACを使用し、S3への直接アクセスを禁止してCloudFront経由に限定
API Gatewayオリジン Lambdaなどを呼び出すバックエンドAPIの入り口
origin_path = "/prod" CloudFrontがAPIに転送する際、自動で /prod パスを先頭に付与
custom_header X-API-Key ヘッダーにAPIキーをセットし、API Gatewayの認証を通す
origin_protocol_policy オリジン(API Gateway)との通信を必ずHTTPS(暗号化)で行う設定

3. キャッシュ挙動

# デフォルト(S3)のキャッシュ挙動
    default_cache_behavior {
        allowed_methods = ["GET", "HEAD", "OPTIONS"] # CloudFrontがオリジンにリクエストを送信する際に許可されるHTTPメソッド
        cached_methods  = ["GET", "HEAD"]            # CloudFrontがキャッシュできるHTTPメソッド
        target_origin_id = aws_s3_bucket.frontend.id # デフォルトのキャッシュ動作が適用されるオリジンのID
        viewer_protocol_policy = "redirect-to-https" # HTTPリクエストをHTTPSにリダイレクト
        compress = true                              # CloudFrontがコンテンツを圧縮して配信
        cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # Managed-CachingOptimized ポリシーのID。静的コンテンツ(例:画像、CSS、JavaScriptファイルなど)のキャッシュに最適化された設定
    }

    # S3でホスティングされた静的ファイル(例:/index.html)へのリクエストはS3に、動的データ取得(例:/data、/data/info)へのリクエストはAPI Gatewayにルーティング
    # /data(末尾スラッシュなし)へのリクエスト用
    ordered_cache_behavior {
    path_pattern     = "/data" # CloudFrontのパス(origin_pathの/prodと組み合わせて/prod/dataになる)
    target_origin_id = "api-gateway-origin"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods = ["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"]
    cached_methods  = ["GET", "HEAD"]
    compress = true
    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # Managed-CachingDisabled ポリシーのID。CloudFrontでのキャッシュを無効にするための設定
    origin_request_policy_id = aws_cloudfront_origin_request_policy.api_gateway_policy.id # カスタムポリシー(カスタムヘッダー転送)
    }

    # /data/*(サブパス)へのリクエスト用
    ordered_cache_behavior {
    path_pattern     = "/data/*" # CloudFrontのパス(origin_pathの/prodと組み合わせて/prod/data/*になる)
    target_origin_id = "api-gateway-origin"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods = ["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"]
    cached_methods  = ["GET", "HEAD"]
    compress = true
    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # Managed-CachingDisabled ポリシーのID。CloudFrontでのキャッシュを無効にするための設定
    origin_request_policy_id = aws_cloudfront_origin_request_policy.api_gateway_policy.id # カスタムポリシー(カスタムヘッダー転送)
    }

パスによって、S3に行くかAPIに行くかを振り分けています。

設定ブロック 対象パス 内容
default_cache_behavior 全て(S3) S3用。画像やJSを効率よく配信するためキャッシュを有効化
ordered_cache_behavior /data, /data/* API用。リアルタイム性が重要なためキャッシュを無効化
viewer_protocol_policy 共通 HTTPでのアクセスを自動的にHTTPSへリダイレクト
allowed_methods 共通 API用ではPOSTやPUTなど、全HTTPメソッドの通過を許可

4. セキュリティ・証明書

    restrictions {
    geo_restriction {
        restriction_type = "whitelist" # リストにある国からのアクセスを許可する
        locations        = ["JP"]      # 許可する国を日本(JP)に設定
      }
    }

    viewer_certificate {
        acm_certificate_arn = aws_acm_certificate_validation.cert.certificate_arn # CloudFrontがHTTPS接続を確立するために使用するACM証明書のARNを指定
        ssl_support_method  = "sni-only" # SSL/TLSのサポート方法を指定
        minimum_protocol_version = "TLSv1.2_2021" # 最低限のTLSバージョンを指定
    }
項目 意味
geo_restriction 地理制限。今回は「日本 (JP)」からのアクセスのみ許可
acm_certificate_arn HTTPS通信に必要なSSL証明書を適用
minimum_protocol_version 古い脆弱な通信プロトコルを禁止

カスタムヘッダー

    custom_header {
      name  = "X-API-Key"
      value = aws_api_gateway_api_key.api_key.value
    }

CloudFrontからAPI Gatewayへリクエストを転送する際にAPIキーを自動でリクエストヘッダーに入れて送るための設定です。


WAF

1. 基本設定

resource "aws_wafv2_web_acl" "frontend_waf" {
    provider = aws.us-east-1
    name = "frontend-waf" # WAFの名前
    scope = "CLOUDFRONT" # WAFの適用範囲
    description = "WAF for frontend distribution"

    default_action { # Web ACLに含まれるどのルールにも一致しなかったリクエストに対して、WAFが取るべきアクションを定義
        allow {} # デフォルトのアクションを「許可」に設定。つまり、明示的に拒否するルールがない限り、すべてのトラフィックが通過
    }
項目 設定値 意味
provider aws.us-east-1 CloudFront用WAFは、必ずバージニア北部リージョンで作成
scope CLOUDFRONT このWAFがCloudFront用であることを指定
default_action allow {} 基本は通す設定。どのルールにも該当しない通信を許可

2. ルール設定

# クロスサイトスクリプティングをブロックするルールを追加
    rule {
        name     = "AWSManagedRulesCommonRuleSetRule"
        priority = 1
        
        statement {
            managed_rule_group_statement {
                name        = "AWSManagedRulesCommonRuleSet"
                vendor_name = "AWS"
            }
        }
        
        visibility_config {
            cloudwatch_metrics_enabled = true
            metric_name                = "AWSManagedRulesCommonRuleSetRuleMetric"
            sampled_requests_enabled   = true
        }
        
        override_action {
            none {}
        }
    }

    visibility_config {
        cloudwatch_metrics_enabled = true
        metric_name                = "FrontendWebAcl"
        sampled_requests_enabled   = true
    }
}

マネージドルールを利用することで、一般的なウェブ攻撃を自動で防ぐように設定しています。

項目 意味
name (Rule) AWSManagedRulesCommonRuleSet
priority 1
override_action none {}

visibility_config ブロックは、WAFがどう動いているかを記録するための設定です。

項目 設定値 意味
cloudwatch_metrics_enabled true 検知したリクエスト数をCloudWatchでグラフ化
metric_name 任意 CloudWatch上で表示されるメトリクスの名前
sampled_requests_enabled true ブロックされたリクエストのIPアドレスやURLをサンプルとして数件確認できるようにする

Route53レコード

resource "aws_route53_record" "root" {
  zone_id = data.aws_route53_zone.primary.zone_id # レコードを追加する対象のホストゾーンID
  name    = var.domain_name # 作成するレコード名
  type    = "A"

  alias {
    name                   = aws_cloudfront_distribution.s3_distribution.domain_name # CloudFrontのドメイン名
    zone_id                = aws_cloudfront_distribution.s3_distribution.hosted_zone_id # CloudFrontサービス自体のホストゾーンID
    evaluate_target_health = false
  }
}

取得した独自ドメインへのアクセスを、CloudFrontに繋げるための設定です。
CloudFrontは特定のIPアドレスを持たないので通常のAレコードは使えません。
そのためAlias機能を使い、Route 53がCloudFrontのIPアドレスを解決してAレコードを生成するようにします。
example.com のようなドメイン名そのものに対しては、CNAMEを設定できないのでAレコードでAlias機能を使います。


API Gateway

# CloudFrontのオリジンとなるREST APIを作成
resource "aws_api_gateway_rest_api" "api" {
    name       = "my-backend-api"
    endpoint_configuration {
      types = ["REGIONAL"] # APIのエンドポイントをリージョナルタイプにすることを指定
  }
}

今回は自分でCloudFrontを構築するのでリージョナルタイプを選択しました。
もしエッジ最適化にすると、「自分のCloudFront ➔ AWSのCloudFront ➔ API Gateway」という構成になります。

REST APIを選択した理由
標準で「APIキー認証」の仕組みを持っており、手軽にCloudFrontから X-API-Key を送って認証できるから。

API Gatewayの中に新しいパスを作成

# 新しいパスの作成(各パスに独自の機能を持たせるために作成)
resource "aws_api_gateway_resource" "data" {
    rest_api_id = aws_api_gateway_rest_api.api.id # どのAPI Gatewayリソースか指定
    parent_id   = aws_api_gateway_rest_api.api.root_resource_id # どのパスの下に新しいパスを作成するかを指定
    path_part   = "data" # 新たに作成するパス名を定義
}
  1. CloudFront が /data へのリクエストを受け取る
  2. API Gateway のこのリソース(/data)へ転送

API Gatewayメソッド

# AWS API Gateway で特定の API エンドポイントに対する HTTP メソッド を定義
resource "aws_api_gateway_method" "data_get" {
    rest_api_id = aws_api_gateway_rest_api.api.id # どのREST APIか指定
    resource_id = aws_api_gateway_resource.data.id # パスを指定
    http_method = "GET" # HTTPメソッドを指定
    authorization = "NONE" # このメソッドへのアクセスは(ユーザーを特定する)認証不要
    api_key_required = true # このメソッドへのアクセスはAPIキーが必要
}

/data というパスに対して、「どのような操作(GETやPOSTなど)を受け付けるか」という具体的な窓口を設定しています。
ここでAPIキーの必須化を行っています。

API Gateway統合

resource "aws_api_gateway_integration" "data_get_integration" {
    rest_api_id = aws_api_gateway_rest_api.api.id # どのAPI Gatewayリソースか指定
    resource_id = aws_api_gateway_resource.data.id # パスを指定
    http_method = aws_api_gateway_method.data_get.http_method # 
    type        = "AWS_PROXY" # Lambda統合でLambdaファンクションを呼び出し
    integration_http_method = "POST" # Lambdaファンクションを呼び出す際のHTTPメソッド(常にPOST)
    uri         = aws_lambda_function.backend_lambda.invoke_arn # Lambdaファンクションのinvoke ARN
}

API Gatewayの窓口(メソッド)に届いたリクエストを、実際に処理を行う「Lambda関数」へ橋渡し(統合)するための設定です。

項目 設定値 意味
rest_api_id ...api.id 対象となるAPI(my-backend-api)を指定
resource_id ...data.id /data というパスに関連付け
http_method ...data_get.http_method メソッドと統合処理を紐付けます。
type "AWS_PROXY" リクエスト内容を丸ごとLambdaに送り、Lambdaのレスポンスをそのままユーザーに返す
integration_http_method "POST" Lambdaを起動するAPIの仕様上、内部的な呼び出しは必ずPOSTで行う必要があるため、固定でPOSTを指定
uri ...invoke_arn 対象のLambda関数のARNを指定

「AWS_PROXY」とは
API Gatewayはリクエストの中身を加工せず、そのままLambdaに渡します。
URLパラメータやHTTPヘッダーの解析、ステータスコード(200 OK や 404 Not Found など)の決定を、すべてLambda側のプログラムの中で自由に行えます。

integration_http_methodがPOSTの理由

ブラウザから届くリクエストが GET であっても、「AWSがLambdaという関数を起動する」というアクション自体が内部的にPOSTリクエストとして設計されているからです。

API Gatewayデプロイ設定

# API GatewayのAPIをデプロイ, REST APIは明示的にデプロイの設定をしないと自動デプロイできない
resource "aws_api_gateway_deployment" "api_deployment" {
    rest_api_id = aws_api_gateway_rest_api.api.id # デプロイ対象となるREST APIを指定

    triggers = { # デプロイメントリソースを再実行するためのトリガー
        redeployment = sha1(jsonencode([ # デプロイメントの変更を監視したいリソースのIDやボディをJSON形式の文字列に変換
        # terraform apply実行時に↑のJSON文字列のハッシュ値を再計算し、変更有の場合このaws_api_gateway_deploymentリソースが再実行されAPI Gatewayで新しいデプロイが作成される
            aws_api_gateway_rest_api.api.body,
            aws_api_gateway_resource.data.id,
            aws_api_gateway_method.data_get.id,
            aws_api_gateway_integration.data_get_integration.id,
            aws_api_gateway_method_response.data_get_200.id,
            aws_api_gateway_integration_response.data_get_200.id,
            aws_api_gateway_method.options_method.id,
            aws_api_gateway_integration.options_integration.id,
            aws_api_gateway_method_response.options_200.id,
            aws_api_gateway_integration_response.options_200.id,
            aws_lambda_function.backend_lambda.source_code_hash,
            aws_lambda_permission.api_gateway_invoke.statement_id,
            aws_lambda_permission.api_gateway_invoke_data.statement_id
        ]))
    }

    lifecycle {
      create_before_destroy = true # 既存のAPI Gatewayを削除してから新しいリソースを作成することでダウンタイムを最小限に抑える
    }
}

APIの変更内容を、デプロイするための設定です。
API Gateway(REST API)は、「デプロイ」を行わないと設定変更が反映されません。

項目 意味
rest_api_id どのAPIをデプロイするかを指定
triggers ここにリストアップしたリソース(パス、メソッド、Lambdaなど)に1つでも変更があれば、Terraformが自動で再デプロイが必要と判断する
sha1(jsonencode([...])) リストの中身をハッシュ値に変換し、変更検知に利用
lifecycle 新しい設定を有効にしてから古いものを消すことで、ダウンタイムをゼロにする

triggers が必要な理由

REST APIには「自動デプロイ」機能がありません。そのため、Terraformで「APIキーの設定を有効にした」「Lambdaのコードを書き換えた」という変更を行っても、このデプロイリソース自体に変化がないと、AWS側には反映されないという問題が起こります。
triggers 内に監視対象を並べておくことで下記のようにTerraformで更新できるようになります。

:
Lambdaのコード(source_code_hash)が変われば、ハッシュ値が変わる

Terraform実行

aws_api_gateway_deployment が新しく作り直される

最新のLambdaを参照するAPIが公開される。

API Gatewayのステージ

resource "aws_api_gateway_stage" "prod" {
  deployment_id = aws_api_gateway_deployment.api_deployment.id
  rest_api_id   = aws_api_gateway_rest_api.api.id # REST APIのID
  stage_name    = "prod"
}

デプロイしたAPIに 「本番用」や「開発用」といった名前(ステージ名)を付けて、実際にアクセス可能なURLを確定させる 設定です。

項目 設定値 意味
deployment_id ...api_deployment.id デプロイをどれにするか指定
rest_api_id ...api.id 対象となるREST APIのIDを指定
stage_name "prod" これにより、URLが https://xxx.execute-api.yyy.amazonaws.com/prod/ になる

APIキーとステージの関連付け

resource "aws_api_gateway_usage_plan" "usage_plan" {
  name = "prod-usage-plan"
  description = "Usage plan for production stage"

  # 全体のスロットリングを定義(適度な値に設定)
  throttle_settings {
    rate_limit  = 100
    burst_limit = 200
  }

  api_stages { # この利用プランを適用するAPIとステージを指定
    api_id = aws_api_gateway_rest_api.api.id # どのAPIにこのプランを適用するか指定
    stage  = aws_api_gateway_stage.prod.stage_name # どのステージにこのプランを適用するか指定
    
    # レート制限の設定, DDos攻撃対策
    throttle {
      path        = "/data/GET"
      rate_limit  = 100
      burst_limit = 200
    }
  }
}

# 作成したAPIキーと利用プランを関連付け
# 特定の API キーを持つユーザーが、その利用プランで定義されたスロットリングやクォータの制限を受ける
resource "aws_api_gateway_usage_plan_key" "usage_plan_key" {
  key_id        = aws_api_gateway_api_key.api_key.id # APIキーを指定
  key_type      = "API_KEY" # キーのタイプを指定
  usage_plan_id = aws_api_gateway_usage_plan.usage_plan.id # 利用プランを指定
}
リソース名 役割 主な設定内容
usage_plan 利用ルールの定義 秒間リクエスト数(100回/秒)、対象ステージ(prod)
usage_plan_key キーとルールの紐付け どの「APIキー」に「利用プラン」を適用するか

なぜこの設定が必要か

  • バックエンド保護: スロットリング(100回/秒)により、過剰なアクセスからLambdaのパンクを防ぐ
  • セキュリティ向上: CloudFrontからAPIキーが付与されていないリクエストを遮断

Lambda

# Lambda関数
resource "aws_lambda_function" "backend_lambda" {
  filename         = data.archive_file.lambda_zip.output_path
  function_name    = "backend-lambda"
  role            = aws_iam_role.lambda_role.arn
  handler         = "backend.lambda_handler"
  runtime         = "python3.9"
  timeout         = 30
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  environment {
    variables = {
      LOG_LEVEL = "INFO"
    }
  }
}
項目 設定値 意味
filename ...output_path デプロイするZIPファイルの場所
role ...lambda_role.arn IAMロールを紐付け
handler "backend.lambda_handler" プログラム内の「最初に実行する関数名」を指定
runtime "python3.9" 使用するプログラミング言語とバージョン
source_code_hash data.archive_file.lambda_zip.output_base64sha256 これが変わるとコードが書き換えられたと判断して再デプロイ
environment LOG_LEVEL = "INFO" プログラム内で読み込める環境変数

source_code_hashがないと、.py ファイルを書き換えて terraform apply しても、AWS上のコードが更新されません。

Lambda関数のソースコードのZIP化

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "${path.module}/lambda/backend.py"
  output_path = "${path.module}/lambda/backend.zip"
}

ソースの自動パッケージ化: Pythonファイルをデプロイ直前に自動でZIPにまとめます。
手動作業の排除: 「ZIPを作ってアップロード」という手作業をなくし、terraform apply だけで完結させます。

リソースベースポリシー

resource "aws_lambda_permission" "api_gateway_invoke" {
  statement_id  = "AllowExecutionFromAPIGateway-v2"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.backend_lambda.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/*/*"
}

信頼関係の構築: API Gatewayからのアクセスを明示的に許可する「リソースベースポリシー」です。

# 追加の権限設定: 特定のメソッドに対する権限
resource "aws_lambda_permission" "api_gateway_invoke_data" {
  statement_id  = "AllowExecutionFromAPIGatewayData-v2"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.backend_lambda.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/GET/data"
}

特定のAPIルート(この場合は /data の GET)からのみ、Lambdaを実行して良いという許可をAWS内で発行する設定です。

項目 設定内容 意味
action lambda:InvokeFunction 「Lambdaを実行する」というアクションを許可
principal apigateway... 実行を許可する相手(主体)として「API Gateway」を指定
source_arn .../*/GET/data 「このAPIの、このメソッド、このパス」からの呼び出しだけに限定

WAFとカスタムヘッダーによる直叩き防止

resource "aws_cloudfront_distribution" "api_distribution" {
  # ...他の設定...
  origin {
    domain_name = "${aws_api_gateway_rest_api.api.id}.execute-api.${data.aws_region.current.name}.amazonaws.com"
    origin_id   = "apigw"

    custom_header {
      name  = "X-Origin-Verify"
      value = var.secret_custom_header # 外部に公開しない秘密の文字列
    }
  }
}
# 2. WAFで「秘密のヘッダーがないリクエスト」をブロックするルールを追加
resource "aws_wafv2_web_acl" "api_waf" {
  # ...他の設定...
  rule {
    name     = "AllowOnlyFromCloudFront"
    priority = 1
    action { block {} } # 条件に合わない(CloudFront以外)ならブロック

    statement {
      not_statement { # 「以下の条件に合致しない場合」
        statement {
          byte_match_statement {
            field_to_match {
              single_header { name = "x-origin-verify" }
            }
            positional_constraint = "EXACTLY"
            search_string         = var.secret_custom_header # 上記のvalueと一致させる
            text_transformation {
              priority = 0
              type     = "NONE"
            }
          }
        }
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AllowOnlyFromCloudFront"
      sampled_requests_enabled   = true
    }
  }
}
設定項目 内容
CloudFrontの custom_header オリジンへのリクエスト時に、自動でヘッダーを付与
WAFの not_statement ヘッダーを持っていない通信を、API Gatewayに届く前にすべて遮断

APIキー認証だけでなく、WAFとカスタムヘッダーを組み合わせることで、インフラレベルで正規ルート以外の通信をシャットアウトできます。


4. 改善点

  • CI/CDの導入
  • Terraform の module 化
  • WAF ルールの追加検討

5. 参照

CloudFrontドキュメント

APIGatewayドキュメント

S3ドキュメント

Lambdaドキュメント

Terraformドキュメント

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?