0
2

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 × Terraform】CloudFront + S3 + ALB + ECS + Cognito 構成をIaCで構築②【実装詳細】

0
Last updated at Posted at 2026-01-01

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

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

目次

コード全体は以下から確認ください。

1. 構成の説明

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

  • 静的フロントエンド: S3 + CloudFront
  • 動的バックエンド: ALB + ECS Fargate
  • 認証・認可: Amazon Cognito による「ALB認証」の導入
  • セキュリティ: AWS WAFによる保護、ACMによるHTTPS化、S3のOAC
  • 管理: TerraformによるIaC

17669366212026421859020893644490.jpg


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 レコード

3. Terraform構成

3-1. フォルダ構成例

.
├── Dockerfile
├── backend.tf
├── ecs.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からのアクセスのみ許可するバケットポリシーを設定しています。


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(静的コンテンツ)と ALB(動的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を指定
    }

   # ALBへのオリジン設定
    origin {
    origin_id = "alb-origin"
    domain_name = aws_lb.backend_alb.dns_name
    origin_path = "" # アプリケーション側でパスを制御するのでCloudFrontではパスの設定はしない

    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経由に限定
origin_protocol_policy オリジン(ALB)との通信を必ず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ファイルなど)のキャッシュに最適化された設定
    }

# APIエンドポイントへのリクエストをALBにルーティング
    ordered_cache_behavior {
    path_pattern     = "/api/*"
    target_origin_id = "alb-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 = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # Managed-CachingDisabled ポリシーのID
    origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id
    }

    # 既存のprodパスもALBにルーティング
    ordered_cache_behavior {
    path_pattern     = "/prod/*" # アプリケーション側で/prod/*に対してリクエストする動作の場合にALBにつなぐ設定
    target_origin_id = "alb-origin" # 先ほどのcustom_origin_configを指定
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods = ["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"]
    cached_methods  = ["GET", "HEAD"]
    compress = true
    cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # Managed-CachingDisabled ポリシーのID(キャッシュをしない設定)
    origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id # ユーザーからのリクエスト情報(クエリ文字列、ヘッダー、クッキーなど)を、そのまま全て後ろのオリジン(ALB)に転送する設定
    } 

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

設定ブロック 対象パス 内容
default_cache_behavior 全て(S3) S3用。画像やJSを効率よく配信するためキャッシュを有効化
ordered_cache_behavior /api/*、/prod/* 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バージョンを指定
    }

CloudFrontで下記の設定をしています。

項目 意味
geo_restriction 地理制限。今回は「日本 (JP)」からのアクセスのみ許可
acm_certificate_arn HTTPS通信に必要なSSL証明書を適用
minimum_protocol_version 古い脆弱な通信プロトコルを禁止

WAF(CloudFront用)

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"
            }
        }

# Web ACLの中に組み込まれている特定のルール(この場合はAWSが提供する一般的な攻撃対策ルールセット)専用の監視設定        
        visibility_config {
            cloudwatch_metrics_enabled = true
            metric_name                = "AWSManagedRulesCommonRuleSetRuleMetric"
            sampled_requests_enabled   = true
        }
        
        override_action {
            none {}
        }
    }

# WAF(Web ACL)を通過したすべての通信を対象にした全体の監視設定。通常、コードの最下部(Web ACLブロックの直下)に記述される。
    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をサンプルとして数件確認できるようにする

visibility_config ブロックが2箇所にあるのは、個別のルール用とWeb ACL全体用で役割が分かれているからです。


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

登録してあるドメインを使ってAレコードを作って、そのドメインにアクセス来たらCloudFrontにつなぐ設定です。
CloudFrontは特定のIPアドレスを持たないので通常のAレコードは使えません。
そのためAlias機能を使い、Route 53がCloudFrontのIPアドレスを解決してAレコードを生成するようにします。
example.com のようなドメイン名そのものに対しては、CNAMEを設定できないのでAレコードでAlias機能を使います。


ALB

resource "aws_lb" "backend_alb" {
  name               = "backend-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = [aws_subnet.public_1.id, aws_subnet.public_2.id]

  enable_deletion_protection = false
パラメータ名 設定値 / 内容 説明
internal false インターネット向けかどうかの設定
load_balancer_type "application" ALB を指定
security_groups [aws_security_group.alb_sg.id] 80/443番ポートを許可するSGを適用
subnets [...public_1.id, ...public_2.id] 2つの異なるAZのパブリックサブネットを指定
enable_deletion_protection false Terraformから削除操作が可能

ALBのターゲットグループ

resource "aws_lb_target_group" "backend_alb_tg" {
  name        = "backend-alb-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id # ALBを配置する対象のVPCを指定。
  target_type = "ip"
パラメータ名 設定値 説明
port 80 ターゲットが待ち受けているポート番号
protocol "HTTP" ALBからターゲットへの通信に使用するプロトコル
vpc_id aws_vpc.main.id ターゲットが存在するVPCを指定
target_type "ip" Fargateを使用する場合は ip を指定

AWS Fargateを利用する場合、各タスクに個別のIPアドレスが割り当てられるため、ターゲットタイプを ip に設定するのが一般的です。

ALBのヘルスチェック

 health_check {
    enabled             = true
    healthy_threshold   = 2
    interval            = 30
    matcher             = "200"
    path                = "/"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 5
    unhealthy_threshold = 2
  }

パラメータ名 設定値 説明
enabled true ヘルスチェックを有効化
path "/" 正常確認のためにALBがアクセスするパス
matcher "200" 成功とみなすHTTPステータスコード
interval 30 チェックを行う間隔(秒)
timeout 5 応答を待つタイムアウト時間(秒)
healthy_threshold 2 連続何回成功したら「正常」とみなすか
unhealthy_threshold 2 連続何回失敗したら「異常」とみなし、通信を遮断するか

ALBのリスナー

resource "aws_lb_listener" "backend_alb_https_listener" {
  load_balancer_arn = aws_lb.backend_alb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = aws_acm_certificate_validation.alb_cert.certificate_arn
パラメータ名 設定値 説明
load_balancer_arn aws_lb.backend_alb.arn 紐付けるALB本体の識別子
port "443" 待ち受けるポート(HTTPS標準)
protocol "HTTPS" 通信プロトコル
ssl_policy "ELBSecurityPolicy-TLS-1-2-2017-01" 使用するSSL/TLSのセキュリティポリシー
certificate_arn aws_acm_certificate_validation... ACM(AWS Certificate Manager)で管理しているSSL証明書を指定

ALBのリスナーのデフォルトアクション

  default_action {
    order = 1
    type = "authenticate-cognito"
    
    authenticate_cognito { # Cognitoを使った認証(ユーザー確認)を実行
      user_pool_arn       = aws_cognito_user_pool.backend_user_pool.arn # 使用するCognitoの 「ユーザープール(ユーザーのIDやパスワード、アカウント情報を管理するデータベースのようなもの)」 の識別子(ARN)を指定
      user_pool_client_id = aws_cognito_user_pool_client.backend_user_pool_client.id # ユーザープールの中に登録されている 「アプリクライアント(認証の窓口設定)のID」 を指定
      user_pool_domain    = aws_cognito_user_pool_domain.backend_user_pool_domain.domain # Cognitoが提供する 「ログイン画面のドメイン名」 を指定
    }
  }

  default_action {
    order = 2
    type  = "forward"
    
    forward {
      target_group {
        arn = aws_lb_target_group.backend_alb_tg.arn
      }
    }
  }

デフォルトアクション
このコードでは order(優先順位)を使って、2つのアクションを順番に実行させています。

順序 (Order) アクションタイプ 内容
1 authenticate-cognito 認証: AWS Cognitoを使用してユーザーがログインしているか確認します。未ログインならログイン画面へ飛ばします。
2 forward 転送: 認証が成功した場合のみ、ターゲットグループ(ECS)へリクエストを流します。
  • マネージドな認証実装(Cognito連携)
    アプリケーション側にログイン機能を実装しなくても、ALBがフロントに立って認証を代行してくれます。
  • HTTPS(SSL/TLS)の必須化
    Cognito認証を利用する場合、通信の安全性を確保するためにHTTPS(443番ポート)とSSL証明書の紐付けが必須となります。
  • SSLポリシーの指定
    ssl_policy により、古い脆弱なプロトコル(TLS 1.0など)を拒否し、安全な通信のみを許可するように制限しています。
  • 順序(Order)の重要性
    必ず order = 1 で認証を行い、それが通った時だけ order = 2 で転送されるように設計されています。

ALBのリスナーのリダイレクトのアクション

resource "aws_lb_listener" "backend_alb_http_listener" {
  load_balancer_arn = aws_lb.backend_alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

HTTPアクセスを、強制的に安全なHTTPSアクセスへ転送する設定です。

パラメータ名 設定値 説明
load_balancer_arn aws_lb.backend_alb.arn 設定を適用するALB本体の識別子
port "80" 待ち受けるポート番号
protocol "HTTP" 通信プロトコルとしてHTTPを指定
default_action.type "redirect" 受け取ったリクエストを別のURLに飛ばす

リダイレクト(Redirect)ブロックの内容

項目 設定値 説明
port "443" 転送先のポート番号
protocol "HTTPS" 転送後のプロトコル
status_code "HTTP_301" 恒久的な移動を意味するステータスコード

認証不要のAPIエンドポイント用リスナールール

resource "aws_lb_listener_rule" "public_api" {
  listener_arn = aws_lb_listener.backend_alb_https_listener.arn
  priority     = 100

  action {
    type = "forward"
    forward {
      target_group {
        arn = aws_lb_target_group.backend_alb_tg.arn
      }
    }
  }

  condition {
    path_pattern {
      values = ["/api/public/*"]
    }
  }
}

ALBのリスナーにおいて特定のパス(URL)に限って、Cognitoによるログイン認証をスキップさせるための設定です。

パラメータ名 設定値 説明
listener_arn backend_alb_https_listener.arn 紐付ける対象のHTTPSリスナー(443ポート)を指定
priority 100 ルールの優先順位
action.type "forward" リクエストの処理方法
target_group_arn backend_alb_tg.arn 転送先のバックエンド(ECSなど)のターゲットグループ
path_pattern "/api/public/*" このルールが適用されるURLのパターン

この設定が必要な理由

  • ヘルスチェックの正常動作
    ALBがターゲット(ECSコンテナなど)の生存確認をする際、認証がかかっていると「302 Redirect(ログイン画面へ)」が返り、ALBが異常と判断してコンテナを強制終了させます。これを防ぐために一部のパスを解放します。
  • ユーザー登録・ログイン前の機能提供
    「新規会員登録API」や「パスワードリセットAPI」などは、ログイン前(認証前)に叩ける必要があるため、この設定で除外します。

設定のポイント

  • 優先度
    デフォルトのCognito認証よりも優先度を高くすることでログインを求められないようにします。

  • パスの設計
    values = ["/api/public/*"] のように、特定のプレフィックス(/public/)をつけておくことでどのAPIが認証不要かコードを見ただけでわかるようにします。

ALB用のセキュリティグループ

項目 設定値 / 内容
インバウンド (80) 0.0.0.0/0 からの TCP 80番
インバウンド (443) 0.0.0.0/0 からの TCP 443番
アウトバウンド 全てのポート、全ての宛先 (-1, 0.0.0.0/0)
  • Web標準ポートの開放
    WebサイトやAPI公開に必須となる 80 (HTTP) と 443 (HTTPS) のみに絞って受け入れ(Ingress)を許可
  • フルオープンのアウトバウンド
    egress(送信)の設定で protocol = "-1" となっているのは、すべてのプロトコルを許可し、ALBがバックエンドのターゲットグループ(ECS)へリクエストを転送する際に通信がブロックされるのを防ぐ

WAF(ALB用)

resource "aws_wafv2_web_acl" "backend_waf" {
  name  = "backend-waf"
  scope = "REGIONAL" # 特定の地域(リージョン)内で動くリソースに適用する場合に指定
  description = "WAF for backend ALB"

  default_action {
    allow {}
  }

  rule { # WAF(Web ACL)の中に、新しい「セキュリティの判定ルール」を1つ追加することを宣言
    name     = "AWSManagedRulesCommonRuleSetRule" # 自分で付けるルール名
    priority = 1
    
    statement { # 「どういう条件の通信を対象にするか」という、ルールの具体的な中身(条件文)を記述
      managed_rule_group_statement { # AWSやサードパーティが提供している既製のルールグループ(マネージドルール)を利用
        name        = "AWSManagedRulesCommonRuleSet" # 導入するマネージドルールの 「正式名称」
        vendor_name = "AWS"
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSManagedRulesCommonRuleSetRuleMetric"
      sampled_requests_enabled   = true
    }
    
    override_action {
      none {}
    }
  }

  rule {
    name     = "AWSManagedRulesKnownBadInputsRuleSetRule"
    priority = 2
    
    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesKnownBadInputsRuleSet"
        vendor_name = "AWS"
      }
    }
    
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSManagedRulesKnownBadInputsRuleSetRuleMetric"
      sampled_requests_enabled   = true
    }
    
    override_action {
      none {}
    }
  }
パラメータ名 設定値 説明
scope "REGIONAL" ALBに適用するためにリージョン指定
default_action allow {} どのルールにも合致しなかったリクエストを、デフォルトで許可

適用されているマネージドルール詳細

ルール名 (Priority) マネージドルール名 防御する主な脅威
AWSManagedRulesCommonRuleSetRule (1) CommonRuleSet (コアルールセット) 一般的なWeb攻撃、OWASP Top 10に含まれるような脆弱性、サイズ制限超過など
AWSManagedRulesKnownBadInputsRuleSetRule (2) KnownBadInputsRuleSet 悪意のあることが判明している入力パターンや、不正なリクエスト元からの攻撃

可視化・モニタリング設定(visibility_config)

項目 設定値 説明
cloudwatch_metrics_enabled true ルールごとの検知状況をCloudWatchメトリクスとして送信
metric_name (各ルール固有の名称) CloudWatch上でグラフ表示される際のメトリクス名
sampled_requests_enabled true 実際に検知・ブロックされたリクエストの詳細を確認

CloudFrontとALBの両方にWAFを導入することで、ALBのパブリックIP/DNSが漏洩した場合でも、バックエンドが直接攻撃にさらされるのを防ぐことができます。


ECS

# ECS クラスターの作成
resource "aws_ecs_cluster" "backend_cluster" { # 作成したコンテナ(ECSタスク)をまとめて所属させるための論理的な箱(クラスター)を作成することを宣言
  name = "backend-cluster"

  configuration {
    execute_command_configuration { # ECS Exec(本番環境などで動いているLinuxコンテナの中に対して、「SSHを使わずに、ブラウザやコマンドラインから直接セキュアにログインしてコマンドを叩ける機能)という機能に関する設定ブロック
      logging = "DEFAULT"
    }
  }
}

# ECS タスク定義
resource "aws_ecs_task_definition" "backend_task" { # タスク定義(どんなコンテナを、どのくらいのスペックで、どうやって動かすか)を書いた設計図を作成することを宣言
  family                   = "backend-task" # タスク定義の名前
  requires_compatibilities = ["FARGATE"] # サーバーレスの起動タイプを指定
  network_mode             = "awsvpc" # 起動したコンテナ(タスク)の1つ1つに、VPC内から固有のプライベートIPアドレスが直接割り当てられる設定。Fargateを使う場合はこの設定が必須。
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = aws_iam_role.ecs_execution_role.arn # ECSタスク実行ロールを指定

  container_definitions = jsonencode([ #コンテナそのものの詳細設定JSON形式で記述
    {
      name  = "nginx-container"
      image = "${var.ecr_uri}:${var.ecr_version}" # Dockerイメージの場所(URL)とバージョン(タグ) を指定
      portMappings = [ # コンテナが通信を受け付ける 「ポート番号」と「プロトコル」 の設定
        {
          containerPort = 80
          protocol      = "tcp"
        }
      ]
      essential = true # このコンテナがエラーなどで異常終了した場合、タスク(コンテナのグループ全体)も連動して自動的に停止・再起動
      logConfiguration = { # コンテナが出力するログ(アクセスログやエラーログ)の 「出力先(ログドライバー)」 の設定
        logDriver = "awslogs"
        options = { # CloudWatch Logsに転送する際の詳細な仕分けルール
          awslogs-group         = aws_cloudwatch_log_group.ecs_logs.name
          awslogs-region        = "ap-northeast-1"
          awslogs-stream-prefix = "ecs"
        }
      }
      healthCheck = {
        command = ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] # curl コマンドを使って、自分自身(localhost)のWebページにアクセスを試みる
        interval = 30
        timeout = 5
        retries = 3
      }
    }
  ])
}

# CloudWatch ログ グループ
resource "aws_cloudwatch_log_group" "ecs_logs" {
  name              = "/ecs/backend-task"
  retention_in_days = 7
}

# ECS サービス(タスク定義を使って、コンテナを何個、どこのネットワークで動かし続けるか設定)
resource "aws_ecs_service" "backend_service" { # コンテナが落ちたら自動で立ち上げ直したり、指定した数を維持したりする
  name            = "backend-service"
  cluster         = aws_ecs_cluster.backend_cluster.id # ECSクラスター」を指定
  task_definition = aws_ecs_task_definition.backend_task.arn # タスク定義を指定
  desired_count   = 1 # コンテナを常に何個起動させておきたいかの設定
  launch_type     = "FARGATE"

  network_configuration { # サブネットやセキュリティグループなどの設定
    subnets          = [aws_subnet.private_1.id, aws_subnet.private_2.id]
    security_groups  = [aws_security_group.ecs_sg.id]
    assign_public_ip = false
  }

  load_balancer { # ALBから届いた通信を、どうやってこのコンテナに届けるかという連携設定
    target_group_arn = aws_lb_target_group.backend_alb_tg.arn # ターゲットグループを指定
    container_name   = "nginx-container" # どのコンテナに 通信を流すかを指定
    container_port   = 80
  }

  depends_on = [aws_lb_listener.backend_alb_https_listener]
}

# ECS 実行ロール
resource "aws_iam_role" "ecs_execution_role" { # 
  name = "ecs-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      }
    ]
  })
}

# ECS 実行ロールにポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" {
  role       = aws_iam_role.ecs_execution_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# ECS用セキュリティグループ
resource "aws_security_group" "ecs_sg" {
  name        = "ecs-sg"
  description = "Allow inbound traffic from ALB to ECS"
  vpc_id      = aws_vpc.main.id

  # ALBからのHTTPアクセスを許可
  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb_sg.id]
  }

  # 外部への通信を許可(ECRからのイメージプルなどに必要)
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ecs-sg"
  }
}

コンテナの動きのイメージ

  1. ECSサービス が動き出す。
  2. サービスが タスク定義(設計図) を読み込む。
  3. 指定された ECSクラスターの中に、設計図通りの コンテナ(タスク) を実体化させる。
  4. サービスが監視を続けてコンテナが落ちたら自動で立ち上げ直したり、指定した数を維持したりする。

ECSの核となる3つのリソース

リソース パラメータ 説明
Cluster name コンテナを実行する論理的なグループ
Task Definition cpu, memory 1つのコンテナを動かすのに必要なリソース量
Service desired_count 常に起動させておくタスクの数

コンテナ内部とログの設定

設定項目 内容 説明
image ${var.ecr_uri} ECRから取得するイメージを指定
logConfiguration awslogs コンテナの標準出力をCloudWatch Logsへ送信し、ブラウザからログを確認
healthCheck curl -f... コンテナ単体で正常にアプリが動いているか、内部から定期的にチェック

ネットワークとセキュリティ(Service / SG内)

設定項目 設定値 説明
subnets private_1, 2 タスクをプライベートサブネットに配置
assign_public_ip false プライベート配置のため、パブリックIPは不要
Ingress (SG) Port 80 ALBのセキュリティグループからのみ接続を許可
Egress (SG) All ECRからのイメージ取得、Cognitoとの通信、外部APIとの通信のために全開放

設定のポイント

  • 依存関係の制御 (depends_on)
    aws_lb_listener が完成してからサービスを起動するように指定しています。これにより、入口(ALB)が準備できていないのにコンテナが先に立ち上がってエラーになるのを防いでいます。
  • 高セキュアなネットワーク設計
    コンテナをプライベートサブネットに隠し、通信はALB経由のみに絞ることで、インフラ層での防御を固めています。
  • ECSのアウトバウンド(egress)は全開放にしておく
    「タスクが一切起動しない(しかもログすら残らない)」という原因不明の沼にハマるため、0.0.0.0/0 で全開放にしておくのが実務上のベストプラクティスです。安全面は、ECSを「プライベートサブネット」に配置して出口をNATゲートウェイに絞ることで担保しています。

4. 改善点

  • CI/CDの導入
  • Terraform の module 化
  • 各リソースの改善(下記に詳細を記載)

ECS

可用性の向上(マルチAZの活用)

  • desired_count = 2 に設定。
    タスク数を増やしてマルチAZにすることで可用性が向上します。

デプロイ・サーキットブレーカーの導入

  • deployment_circuit_breaker { enable = true, rollback = true } を追加。
    ECSサービスのデプロイに失敗した際、いつまでもリトライを繰り返してデプロイが完了しない状況を防ぎます。

WAF

  • コード内のマネージドポリシーIDの動的取得
    マネージドポリシーのIDそのものが更新されて無効になることはほとんどありませんが、Dataソースを用いることでIDを動的に取得でき、コードの意図も明確になります。
data "aws_cloudfront_cache_policy" "optimized" {
  name = "Managed-CachingOptimized"
}

# 使うときはIDを自動で代入
cache_policy_id = data.aws_cloudfront_cache_policy.optimized.id
  • レートリミット(Rate-based Rule)を入れる
    「同じIPアドレスから5分間に100回以上アクセスがあったら自動でそのIPを5分間ブロックする」というWAFルールを追加します。これにより、DDOS攻撃などによるアクセスが数百万回規模に膨れ上がる前に、特定の攻撃元からの通信をWAFが超低コストで自動遮断してくれます。

S3

S3バケットのバージョニング

  • aws_s3_bucket_versioning を有効化。
    ファイルを誤って削除・更新しても過去のバージョンに戻せます。

SPA(Single Page Application)への対応

  • aws_cloudfront_distribution に custom_error_response ブロックを追加し、403/404エラーを /index.html にリダイレクト(ステータス200)するように設定。

ルート以外のパス(例:/about)に直接アクセスした際、 S3 で 403 エラーが発生するのを防ぎます。

Route53

  • Route53のevaluate_target_health が false になっているのを true に変更。
    CloudFrontのヘルスチェックと連動し、以下のようなメリットがあります。

Route 53 ヘルスチェック有効化の主要なメリット

メリット 具体的な内容 導入後の効果
正常な接続先への誘導 異常を検知した接続先(CloudFront/ALB)をDNSの回答から自動的に除外します。 ユーザーがエラー画面を見る確率を下げ、常に稼働している環境へ繋ぎます。
エラーの切り分け DNSレベルでの「健全性」が可視化され、インフラ層かアプリ層のどちらに問題があるか即座に判断できます。 障害時の調査時間を大幅に短縮し、見当違いな修正による二次被害を防ぎます。
自己修復のトリガー CloudWatchアラートと連携し、異常検知を合図に予備システムの起動や管理者の呼び出しを自動化します。 人の手を介さない「セルフヒーリング」の仕組みを構築でき、復旧までの時間を短縮します。
無駄なリクエストの削減 そもそも「死んでいる(応答できない)」場所へリクエストを流さないようにします。 無駄な通信コストの抑制や、障害中のサーバーへのさらなる負荷(追い打ち)を防ぎます。

Cognito

  • advanced_security_mode = "OFF"を AUDIT または ENFORCE への変更。
    漏洩した資格情報によるログイン試行を検知したり、リスクベースの認証(普段と違う場所からのログインに警告を出す等)が可能になります。

ALB

  • CloudFrontが付与するカスタムヘッダーをALB側で検証し、ヘッダーがないリクエストを拒否し、ALBに直接攻撃されるのを防ぐ。

料金への影響: ALBに直接届いた通信は、ALBのリスナールールの最初の条件チェックで合言葉がないと判断され、即座に403エラーとして切り捨てられます。これにより、裏側にあるCognitoを呼び出す処理が発生しないため、Cognitoの料金急増を防げます(※ALB自体の403応答による微小な負荷はかかりますが、Cognitoが動くことに比べれば遥かに安価です)。
※AWSの仕組み上、CognitoはALBの前段にあるのではなく、ALBの「内部(リスナールールの中)」に組み込まれて動きます。そのため、ALBへの直接アクセスを防ぐことで、DDOS攻撃などによるCognitoの料金急増を防ぐことができます。

CloudFrontが付与するカスタムヘッダーをALB側で検証し、CloudFront経由でのみALBに通信するように修正した場合、CloudFrontを通過した時点で綺麗な通信しか残っていないため、ALBのWAFは何も検知せずスルーするだけになります。そのため無駄な固定費が発生するだけになるので、ALBのWAFは削除します。


5. 参照

CloudFrontドキュメント
S3ドキュメント
ECSドキュメント
ELBドキュメント
Terraformドキュメント

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?