学習のために構築した環境を紹介します。
S3やLambdaなどの主要リソースおよびTerraformを触ったことがある人向けの記事です。
本記事は「①設計・構成解説編」と「②実装詳細編」の2章構成です。
本章では実装詳細の解説をします。
①設計・構成解説編はこちらです。
目次
コード全体は以下のリンクを参照ください。
1. 構成の説明
1-1. 何をしたいか
下記を実装し動作させることが今回の学習目的です。
- 静的フロントエンドは S3 + CloudFront
- 動的処理は API Gateway + Lambda
- CloudFront を入口に集約
- ACMを用いて証明書を発行してHTTPS化
- API Key を CloudFront から付与
- WAFによるセキュリティ強化
- インフラは Terraform でコード管理
セキュリティを意識しつつ、サーバーレスで一般的に使われている構成にしました。
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上は依存関係で解決されますが、
マネジメントコンソールを使う場合は以下の順に作成します。
- Route53 Hosted Zone(既存)
- ACM(us-east-1)
- S3(静的サイト用)
- CloudFront(OAC + WAF)
- API Gateway
- Lambda
- CloudFront の API オリジン設定
- Route53 Alias レコード
2-3. 注意点
- CloudFront の ACM は必ず us-east-1
- API Gateway は Deployment を明示的に作らないと反映されない
- API Gateway の変更検知用に
triggersを使っている - 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設定が適用されるオリジンの種類を指定
}
- OACを作成
- CloudFrontでS3にアクセスする時にこのOACを使うように設定
- 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" # 新たに作成するパス名を定義
}
- CloudFront が /data へのリクエストを受け取る
- 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 ルールの追加検討
