WEBサービスのログの保管、監視環境をTerraformで構築する【AWS】

# 概要
ECS/Fargateを実行環境とするWEBサービスを運用する場合の、サービスのログ、保管、監視、解析の環境をTerraformで構築する方法をまとめています。また、メトリクスを利用した監視についても解説しています。

最終的な構成は以下の図のようになります。  
![スクリーンショット 2024-02-07 17.15.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/9481c788-97c9-5fa4-322d-7dcd952992c5.png)

説明する範囲はログの関係するリソースに限定しており、アプリケーションの実行環境は対象外にしています。アプリケーションの実行環境の構築方法については以下の記事で解説していますので、気になる点があれば参照ください。

https://qiita.com/__yamakawa/items/cde04ca6ff62fe483a05

また、Terraformのコードはモジュール化を行っていますが、mainファイルでのモジュールの呼び出しなどは省略しています。全体の構成については以下で公開していますので、気になる点があれば参照ください。

https://github.com/HarukiYamakawa/kaku-operation

## セットアップ
各リソースの監視環境を構築する前に共通で利用するリソースを作成します。
### SNSトピックを作成する
メトリクスのアラームが発生した際に通知を発信するためのSNSトピックを作成します。通知先には運用者のメールアドレスを指定しています。
```terraform:SNSトピックを作成する
module "sns" {
  source = "./module/sns"

  name_prefix = var.name_prefix
  tag_name = var.tag_name
  tag_group = var.tag_group

  #運用者のメールアドレスを指定
  email = data.aws_ssm_parameter.alart_mail_address.value
}
```
### Athenaのクエリログの保管用のS3バケットを作成する
Athenaで解析するためのクエリログを保管するためのS3バケットを作成します。これを作成しないとAthenaでの解析ができないため、必須です。  
このリソースについてはTerraformで管理せずに、コンソール上で作成しています。アプリケーションとは直接関係していないため、管理対象外としています。
![athena-s3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/1cee49d5-a876-b828-6c78-86a583255289.png)


## WAFのトラフィックログの管理環境
WAFのトラフィックログを保存する環境を以下の図のように構築します。  
WAFのルールが検知したリクエストのカウント等のメトリクスを監視して、問題があればalartから通知が飛び、問題の内容はAthenaで解析する、といった状況を想定しています。  
S3のアクセスログはトラフィックログを管理するS3バケットが誤ってpublicになった時の影響範囲などを解析するために利用することを想定しています。  

![waf4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/71e2a224-213c-956a-d1ec-fe28088881ff.png)


### 保管用のS3バケットを作成する
WAFのトラフィックログを保存するためのS3バケットを作成します。WAFからのアクセスを許可するバケットポリシーを定義しています。ライフサイクルとしては、解析が不要となることを想定した30日後にGlacierに移行するように設定しています。  
バケットの名称には「aws-waf-logs」というプレフィクスが必須である点に注意してください。
```terraform:トラフィックログを保管するS3バケットを作成する
resource "aws_s3_bucket" "waf_traffic_log_bucket" {
  bucket = "aws-waf-logs-${var.name_prefix}"

  tags = {
    Name = "${var.tag_name}-waf-traffic-log"
    group = "${var.tag_group}"
  }
}

resource "aws_s3_bucket_public_access_block" "waf_traffic_log_bucket_public_access_block" {
  bucket = aws_s3_bucket.waf_traffic_log_bucket.id

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

data "aws_iam_policy_document" "waf_traffic_log_bucket_policy_document" {
  statement {
    actions = [
      "s3:PutObject",
    ]

    resources = [
      "${aws_s3_bucket.waf_traffic_log_bucket.arn}/*"
    ]

    principals {
      type        = "Service"
      identifiers = ["delivery.logs.amazonaws.com"]
    }
  }
  statement {
    actions = [
      "s3:GetBucketAcl"
    ]

    resources = [
      aws_s3_bucket.waf_traffic_log_bucket.arn
    ]

    principals {
      type        = "Service"
      identifiers = ["delivery.logs.amazonaws.com"]
    }
  }
}

resource "aws_s3_bucket_policy" "waf_traffic_log_bucket_policy" {
  bucket = aws_s3_bucket.waf_traffic_log_bucket.id
  policy = data.aws_iam_policy_document.waf_traffic_log_bucket_policy_document.json
}

resource "aws_s3_bucket_lifecycle_configuration" "waf_traffic_log_bucket_lifecycle_configuration" {
  bucket = aws_s3_bucket.waf_traffic_log_bucket.id

  rule {
    id     = "transfer to glacier"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "GLACIER"
    }
  }

}

# アクセスログの送信先を定義
resource "aws_s3_bucket_logging" "waf_traffic_log_bucket_logging" {
  bucket        = aws_s3_bucket.waf_traffic_log_bucket.id
  target_bucket = aws_s3_bucket.waft_traffic_log_bucket_bclg.id
  target_prefix = "waf-traffic-log-bclg"
}
```
### S3のアクセスログを保存するためのS3バケットを作成する
トラフィックログを保管するS3バケットのアクセスログも保存するために、もう一つのS3バケットを作成します。
```terraform:アクセスログを保存するS3バケットを作成する
resource "aws_s3_bucket" "waft_traffic_log_bucket_bclg" {
  bucket = "aws-waf-logs--${var.name_prefix}-bclg"

  tags = {
    Name = "${var.tag_name}-waf-traffic-log-bclg"
    group = "${var.tag_group}"
  }
}

resource "aws_s3_bucket_public_access_block" "waf_traffic_log_bucket_bclg_public_access_block" {
  bucket = aws_s3_bucket.waft_traffic_log_bucket_bclg.id

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

data "aws_iam_policy_document" "waf_traffic_log_bucket_bclg_policy_document" {
  statement {
    actions = [
      "s3:PutObject",
    ]

    resources = [
      "${aws_s3_bucket.waft_traffic_log_bucket_bclg.arn}/*"
    ]

    principals {
      type        = "Service"
      identifiers = ["logging.s3.amazonaws.com"]
    }
  }
  statement {
    actions = [
      "s3:GetBucketAcl"
    ]

    resources = [
      aws_s3_bucket.waft_traffic_log_bucket_bclg.arn
    ]

    principals {
      type        = "Service"
      identifiers = ["logging.s3.amazonaws.com"]
    }
  }
}

resource "aws_s3_bucket_policy" "waf_traffic_log_bucket_bclg_policy" {
  bucket = aws_s3_bucket.waft_traffic_log_bucket_bclg.id
  policy = data.aws_iam_policy_document.waf_traffic_log_bucket_bclg_policy_document.json
}

resource "aws_s3_bucket_lifecycle_configuration" "waf_traffic_log_bucket_bclg_lifecycle_configuration" {
  bucket = aws_s3_bucket.waft_traffic_log_bucket_bclg.id

  rule {
    id     = "transfer to glacier"
    status = "Enabled"

    transition {
      days          = 1
      storage_class = "GLACIER"
    }
  }

}
```
### WAFのログ転送の設定をする
WAFの設定にS3バケットへログを転送するように設定します。  
```terraform:WAFのログ転送の設定
resource "aws_wafv2_web_acl_logging_configuration" "default" {
  #上記で作成したS3バケットのARNを指定
  log_destination_configs = ["${var.waf_traffic_log_bucket_arn}"]
  #紐づけるWAFのリソースのARNを指定
  resource_arn            = aws_wafv2_web_acl.default.arn
}
```
### ログの出力を確認する
上記の設定をしてからWAFを経由する通信をリクエストすると、以下のようにS3バケットにログが出力されるようになります。
![waf-log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/f18e6faa-024b-4370-6f11-21e1fd96f393.png)


### alarmを設定する
WAFの監視環境を構築するために、メトリクスをもとにアラームを設定します。  
今回はWAFのマネージドルールであるAWSManagedRulesSQLiRuleSetに検知されたリクエストをカウントする「BlockedRequests」メトリクスをもとにしたalarmを設定します。  
以下のようにして、60秒間に5回を超えたリクエストが検知された場合にアラームが発生するように設定しています。
```terraform:alarmを設定する
resource "aws_cloudwatch_metric_alarm" "waf-sql-rule-set-count" {
  alarm_name          = "waf-sql-rule-set-count"
  namespace           = "AWS/WAFV2"
  metric_name         = "BlockedRequests"
  dimensions = {
    ManagedRuleGroup = "AWSManagedRulesSQLiRuleSet"
    WebACL           = "kaku-waf"
  }
  period              = 60
  statistic           = "Sum"
  threshold           = 5
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1

  alarm_actions     = ["${var.alart_topic_arn}"]
}
```
このalarmを「アラーム状態」とするために自ドメインに対してSQLインジェクション攻撃を行い、以下のように閾値を超えるようにします。
![waf-meto.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/f5ec01fe-2c25-21a7-5084-ff57ddcda53b.png)

そうするとSNS経由で運用者にメールが送信され、運用者はAthenaで状況調査や原因の解析を行うことになります。

### Athenaでログを解析する
WAFのトラフィックログを対象として、Athenaで解析を行います。  
まずは以下の記事を参考に、WAFのトラフィックログのテーブルを作成します。

https://dev.classmethod.jp/articles/analyzing-waflogs-with-athena/

テーブルが作成できたら、以下のようにしてSQLを実行して、SQLインジェクション攻撃の状況を確認します。terminatingruleidカラムにAWSManagedRulesSQLiRuleSetが含まれるリクエストを抽出することで、SQLインジェクション攻撃のログを確認できます。
```SQL:SQLインジェクションの調査
SELECT from_unixtime(timestamp/1000, 'Asia/Tokyo') AS JST,
*
FROM
       "waflogs"
where
terminatingruleid = 'AWSManagedRulesSQLiRuleSet'
order by timestamp desc
```
![waf-athena.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/ebcf11e8-85ae-0d4c-65f4-48486fff1862.png)


以上でWAFのトラフィックログの管理環境の構築は完了です。監視対象となるメトリクスは他にもありますので、運用の方針に合わせてalarmを設定してください。


## ALBのアクセスログの管理環境
ALBのアクセスログを保存する環境を以下の図のように構築します。
ALBへのリクエスト回数やエラーを返したリクエストの数等のメトリクスを監視して、問題があればalartから通知が飛び、問題の内容はAthenaで解析する、といった状況を想定しています。  
![alb-zu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/aa261e49-f285-d596-6c0e-a8bcec1cf1fd.png)


### 保管用のS3バケットを作成する
ALBのアクセスログを保存するためのS3バケットを作成します。ALBからのアクセスを許可するバケットポリシーを定義しています。ライフサイクルとしては、解析が不要となることを想定した30日後にGlacierに移行するように設定しています。
```terraform:アクセスログを保管するS3バケットを作成する
resource "aws_s3_bucket" "alb_access_log_bucket" {
  bucket = "${var.name_prefix}-alb-access-log"

  tags = {
    Name = "${var.tag_name}-alb-access-log"
    group = "${var.tag_group}"
  }
}

resource "aws_s3_bucket_public_access_block" "alb_access_log_bucket_public_access_block" {
  bucket = aws_s3_bucket.alb_access_log_bucket.id

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

data "aws_iam_policy_document" "alb_access_log_bucket_policy_document" {
  statement {
    actions = [
      "s3:PutObject",
    ]

    resources = [
      "${aws_s3_bucket.alb_access_log_bucket.arn}/*"
    ]

    principals {
      type        = "Service"
      identifiers = ["delivery.logs.amazonaws.com"]
    }
  }
  statement {
    actions = [
      "s3:GetBucketAcl"
    ]

    resources = [
      aws_s3_bucket.alb_access_log_bucket.arn
    ]

    principals {
      type        = "Service"
      identifiers = ["delivery.logs.amazonaws.com"]
    }
  }
}

resource "aws_s3_bucket_policy" "alb_access_log_bucket_policy" {
  bucket = aws_s3_bucket.alb_access_log_bucket.id
  policy = data.aws_iam_policy_document.alb_access_log_bucket_policy_document.json
}

resource "aws_s3_bucket_lifecycle_configuration" "alb_access_log_bucket_lifecycle_configuration" {
  bucket = aws_s3_bucket.alb_access_log_bucket.id

  rule {
    id     = "transfer to glacier"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "GLACIER"
    }
  }

}
```

### ALBのログ転送の設定をする
ALBの設定にS3バケットへログを転送するように設定します。  
```terraform:ALBのログ転送の設定
resource "aws_lb" "default" {
  ・・・

  access_logs {
    #上記で作成したS3バケットのARNを指定
    bucket  = "${var.alb_access_log_bucket_id}"
    prefix  = "alb-access-log"
    enabled = true
  }

  ・・・
}
```
上記の設定をすることで、ALBを経由する通信をリクエストすると、S3バケットにログが出力されるようになります。

以上でALBのアクセスログの管理環境の構築は完了です。alarmの設定などはWAFの欄で説明したものと同じ手順で設定できますので、運用の方針に合わせてalarmを設定してください。

## RDSの監査ログの管理環境
RDSの監査ログを保存する環境を以下の図のように構築します。
RDSのCPU使用率等のメトリクスを監視して、問題があればalartから通知が飛び、その内容について調査するといった状況を想定しています。 監査ログ自体はメトリクスの監視とは直接関係しておらず、データの不整合や不正アクセスが発生した際にAthenaで解析することを想定しています。運用の方針に合わせて一般ログやエラーログの出力、監視の設定を行ってください。

RDSのログの転送は、CloudWatchLogsへ出力して、EventiBridgeスケジュールにより24時間ごとにS3へログをエクスポートする、という環境を構築します。これは監査ログがリアルタイム性を要求されないこと、またログの出力量も少ないためコスト的に問題にならないと判断したためです。

CloudWatchLogsでメトリクスフィルターを利用してログの監視を行うこともできますが、監査ログではその必要性が薄いと判断したため、今回は考慮していません。

![rds-zu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/2f136417-7170-3a1c-56d4-35cd350bfb6e.png)


### 保管用のS3バケットを作成する
RDSの監査ログを保存するためのS3バケットを作成します。
```terraform:保管用のS3バケットを作成する
resource "aws_s3_bucket" "rds_audit_log_bucket" {
  bucket = "${var.name_prefix}-rds-audit-log"

  tags = {
    Name = "${var.tag_name}-rds-audit-log"
    group = "${var.tag_group}"
  }
}

resource "aws_s3_bucket_public_access_block" "rds_audit_log_bucket_public_access_block" {
  bucket = aws_s3_bucket.rds_audit_log_bucket.id

  #外部からの読み込みを許可しない
  block_public_acls       = true
  ignore_public_acls      = true
  block_public_policy     = true
  restrict_public_buckets = true
}

#CloudWatch LogsからS3へのログ出力を許可する
data "aws_iam_policy_document" "rds_audit_log_bucket_policy_document" {
  statement {
    actions = [
      "s3:PutObject",
    ]

    resources = [
      "${aws_s3_bucket.rds_audit_log_bucket.arn}/*"
    ]

    principals {
      type        = "Service"
      identifiers = ["logs.ap-northeast-1.amazonaws.com"]
    }
  }
  statement {
    actions = [
      "s3:GetBucketAcl"
    ]

    resources = [
      aws_s3_bucket.rds_audit_log_bucket.arn
    ]

    principals {
      type        = "Service"
      identifiers = ["logs.ap-northeast-1.amazonaws.com"]
    }
  }
}

resource "aws_s3_bucket_policy" "rds_audit_log_bucket_policy" {
  bucket = aws_s3_bucket.rds_audit_log_bucket.id
  policy = data.aws_iam_policy_document.rds_audit_log_bucket_policy_document.json
}

resource "aws_s3_bucket_lifecycle_configuration" "rds_audit_log_bucket_lifecycle_configuration" {
  bucket = aws_s3_bucket.rds_audit_log_bucket.id

  rule {
    id     = "transfer to glacier"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "GLACIER"
    }
  }

}

# アクセスログの送信先を定義
resource "aws_s3_bucket_logging" "rds_audit_log_bucket_logging" {
  bucket        = aws_s3_bucket.rds_audit_log_bucket.id
  target_bucket = aws_s3_bucket.rds_audit_log_bucket_bclg.id
  target_prefix = "rds-audit-log-bclg"
}
```

### RDSの監査ログの出力を設定する
RDSの設定にCloudWatchLogsへログを転送するように設定します。  
```terraform:RDSの監査ログの出力を設定する
resource "aws_rds_cluster" "default" {
  ...

   #監査ログをCloudWatch Logsに出力
   enabled_cloudwatch_logs_exports = ["audit"]

  ...
}
```

### EventBridgeのスケジュールに紐づけるIAMロールを作成する
EventBridgeのスケジュールに紐づけるIAMロールを作成します。  
EventBridgeからCloudWatchLogsにログのエクスポートタスクを実行させるように権限を持たせます。
```terraform:EventBridgeのスケジュールに紐づけるIAMロールを作成する
resource "aws_iam_policy" "event_bridge_export_task_policy" {
  name = "${var.name_prefix}-event-bridge-export-task-policy"
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateExportTask",
                "logs:CancelExportTask",
                "logs:DescribeExportTasks",
                "logs:DescribeLogStreams",
                "logs:DescribeLogGroups"
            ],
            "Resource": "*"
        }
    ]
})
}

resource "aws_iam_role" "event_bridge_export_task_role" {
  name = "${var.name_prefix}-event-bridge-export-task-role"

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

resource "aws_iam_role_policy_attachment" "event_bridge_export_task_policy_attachment" {
  role       = aws_iam_role.event_bridge_export_task_role.name
  policy_arn = aws_iam_policy.event_bridge_export_task_policy.arn
}
```

### EventBridgeのスケジュールを設定する
EventBridgeのスケジュールを設定して、CloudWatchLogsからS3へログをエクスポートするように設定します。  
```terraform:EventBridgeのスケジュールを設定する
resource "aws_scheduler_schedule" "rds_audit_log_export_scheduler" {
  name = "rds-audit-log-export-scheduler"

  flexible_time_window {
    mode = "OFF"
  }

  #24時間ごとにログをエクスポート
  schedule_expression = "rate(24 hours)"

  target {
    # CloudWatchLogsのエクスポートタスクを指定
    arn      = "arn:aws:scheduler:::aws-sdk:cloudwatchlogs:createExportTask"
    #上記で作成したIAMロールを指定
    role_arn = "${var.iam_role_event_bridge_export_task_arn}"
    input = jsonencode({
      "Destination": "${var.s3_bucket_log_rds_name}",
      "DestinationPrefix": "exported-logs",
      "From": 1670000000000,
      "LogGroupName": "/aws/rds/cluster/kaku-rds-cluster/audit",
      "To": 5000000000000
  })
}
}
```
上記の設定をすることで、24時間ごとにCloudWatchLogsからS3へログがエクスポートされるようになります。

以上でRDSの監査ログの管理環境の構築は完了です。alarmの設定などはWAFの欄で説明したものと同じ手順で設定できますので、運用の方針に合わせてalarmを設定してください。



## ECS/Fargate(Rails)のログの管理環境
ECS/Fargate上で稼働するRailsアプリケーションのログを保存する環境を以下の図のように構築します。  
CPU使用率やメトリクスフィルターで作成したログ基準のメトリクス等を監視して、問題があればalartから通知が飛び、問題の内容はAthenaで解析する、といった状況を想定しています。

また、RailsのログはFireLensを利用して集約し、エラーログはCloudWatchLogsへ、エラーログ含めた全てのログはFirehoseを介してS3へ転送するように設定します。これによりCloudWatchLogsでのログの取り込み量が減るのでコストの削減につながります。またFirehoseでリアルタイムにログを転送することで、リアルタイム解析が可能になります。
![rails-zu3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/94eed6f6-9589-97f5-80bb-9fc5871d6046.png)



### 保管用のS3バケットを作成する
Railsのログを保存するためのS3バケットを作成します。ライフサイクルとしては、解析が不要となることを想定した30日後にGlacierに移行するように設定しています。  
S3へのアクセスポリシーはFirehose側で指定するので、S3バケットのポリシーでは設定しないことに注意してください。
```terraform:保管用のS3バケットを作成する
resource "aws_s3_bucket" "ecs_rails_log_bucket" {
  bucket = "${var.name_prefix}-ecs-rails-log"

  tags = {
    Name = "${var.tag_name}-ecs-rails-log"
    group = "${var.tag_group}"
  }
}

resource "aws_s3_bucket_public_access_block" "ecs_rails_log_bucket_public_access_block" {
  bucket = aws_s3_bucket.ecs_rails_log_bucket.id

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

# アクセスログの送信先を定義
resource "aws_s3_bucket_logging" "ecs_rails_log_bucket_logging" {
  bucket        = aws_s3_bucket.ecs_rails_log_bucket.id
  target_bucket = aws_s3_bucket.alb_access_log_bucket_bclg.id
  target_prefix = "ecs-rails-log"
}
```

### Firehose用のIAMロールを作成する
FirehoseでS3にログを転送するために、Firehose用のIAMロールを作成します。上記で作成したS3バケットにログを書き込むための権限を持たせます。
```terraform:Firehose用のIAMロールの作成
resource "aws_iam_policy" "firehose_ecs_rails_log_policy" {
  name = "${var.name_prefix}-firehose-ecs-rails-log-policy"
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
              "s3:AbortMultipartUpload",
              "s3:GetBucketLocation",
              "s3:GetObject",
              "s3:ListBucket",
              "s3:ListBucketMultipartUploads",
              "s3:PutObject"
            ],
            "Resource": "*"
        }
    ]
})
}

resource "aws_iam_role" "firehose_ecs_rails_log_role" {
  name = "${var.name_prefix}-firehose-ecs-rails-log-role"

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

resource "aws_iam_role_policy_attachment" "firehose_ecs_rails_log_policy_attachment" {
  role       = aws_iam_role.firehose_ecs_rails_log_role.name
  policy_arn = aws_iam_policy.firehose_ecs_rails_log_policy.arn
}
```

### Firehoseストリームを作成する
上記で定義したS3バケットにログを転送するためのFirehoseストリームを作成します。  
ログの転送は10mb毎にファイルを分割し、またその容量に達さずとも300秒毎にはファイルを分割するように設定しています。
```terraform:Firehoseストリームを作成する
resource "aws_kinesis_firehose_delivery_stream" "rails_log_delivery_stream" {
  name        = "rails-log-delivery-stream"
  destination = "extended_s3"

  extended_s3_configuration {
    #上記で作成したIAMロールを指定
    role_arn   = "${var.iam_role_firehose_arn}"
    #上記で作成したS3バケットのARNを指定
    bucket_arn = "${var.s3_bucket_log_rails_arn}"
    #10MB毎にファイルを分割
    buffering_size     = 10
    #300秒毎にファイルを分割
    buffering_interval  = 300
    prefix = "rails-log-streaming/"
  }
}
```

### FluentBitのの設定をする
エラーログはCloudWatchLogsへ、エラーログ含めた全てのログはFirehoseを介してS3へ転送するようにFluentBitの設定を行います。  
まずはDockerfileに以下のようにFluentBitの設定ファイルをコピーします。
```terraform:Dockerfile
FROM --platform=linux/x86_64 amazon/aws-for-fluent-bit:latest
COPY extra.conf /fluent-bit/etc/extra.conf
```
次にextra.confに以下のように設定を記述します。  
エラーログのフィルターは正規表現を利用して、詳細に設定する必要がありますが、今回は500エラーが発生した場合にエラーログとして扱うような簡易的な設定を行っています。
```terraform:extra.conf
#エラーの対象とするログのフィルターを設定
[FILTER]
    Name         rewrite_tag
    Match        *-firelens-*
    Rule         $log (500) 500-error false

#エラーのタグが付与されたログをCloudWatchLogsへ転送
[OUTPUT]
    Name   cloudwatch_logs
    Match  500-error
    region ap-northeast-1
    log_group_name /kaku/puma
    log_stream_prefix fluentbit

#全てのログをFirehoseへ転送
[OUTPUT]
    Name   kinesis_firehose
    Match  *
    region ap-northeast-1
    delivery_stream rails-log-delivery-stream
```
ECSタスク等の解説はここでは行いませんが、この設定のFireLensコンテナをrailsコンテナと同じタスクで起動すると、ログが集約されて、上記の設定で転送されるようになります。


### メトリクスフィルターでログの監視を行う
Railsのログを監視するために、メトリクスフィルターを作成します。  
メトリクスフィルターを作成することで、特定の文字列が含まれるログをメトリクスとして管理することができるようになります。  
以下の例では、pumaのログに「Completed 500」という文字列が含まれる場合にメトリクスフィルターを作成し、そのメトリクスフィルターに基づいてアラームを設定しています。
```terraform:メトリクスフィルターを作成し、alarmを設定する
#メトリクスフィルターの作成
resource "aws_cloudwatch_log_metric_filter" "puma-status-500-filter" {
  name           = "status-500"
  #検知する文字列を指定
  pattern        = "Completed 500"
  log_group_name = "${var.rails_log_group_name}"

  metric_transformation {
    #メトリクスフィルターの名前空間
    namespace = "${var.name_prefix}/puma/metric-filter"
    #メトリクス名
    name      = "Status-500"
    #メトリクスの値
    value     = "1"
  }
}

#アラームの設定
#500エラーが5分間で5回を超えて発生した場合にアラームする
resource "aws_cloudwatch_metric_alarm" "puma-status-500-alarm" {
  alarm_name          = "status-500"
  namespace           = "${var.name_prefix}/puma/metric-filter"
  metric_name         = "Status-500"
  period              = 300
  statistic           = "Sum"
  threshold           = 5
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1

  alarm_actions     = ["${var.alart_topic_arn}"]
}
```

以上でECS/Fargate(Rails)のログの管理環境の構築は完了です。


## ECS/Fargate(Nextjs)のログの管理環境
ECS/Fargate上で稼働するNextjsアプリケーションのログを保存する環境を以下の図のように構築します。  
CPU使用率やメトリクスフィルターで作成したログ基準のメトリクス等を監視して、問題があればalartから通知が飛び、問題の内容はAthenaで解析する、といった状況を想定しています。

また、NextjsのログはCloudWatchLogsに保管され、またFirehoseでS3に転送されるようにします。RailsのようにFireLensを利用することもできますが、Next.jsから出力されるログの量が極端に少ないため、コストの考慮は不要であるとして、今回は全てCloudWatchLogsで取り込むようにしています。
![next-zu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/55eb8f1c-1066-229e-2bc6-0b50f682ef47.png)



### 保管用のS3バケットを作成する
Next.jsのログを保存するためのS3バケットを作成します。ライフサイクルとしては、解析が不要となることを想定した30日後にGlacierに移行するように設定しています。  
S3へのアクセスポリシーはFirehose側で指定するので、S3バケットのポリシーでは設定しないことに注意してください。
```terraform:保管用のS3バケットを作成する
resource "aws_s3_bucket" "ecs_nextjs_log_bucket" {
  bucket = "${var.name_prefix}-ecs-nextjs-log"

  tags = {
    Name = "${var.tag_name}-ecs-nextjs-log"
    group = "${var.tag_group}"
  }
}

resource "aws_s3_bucket_public_access_block" "ecs_nextjs_log_bucket_public_access_block" {
  bucket = aws_s3_bucket.ecs_nextjs_log_bucket.id

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

# アクセスログの送信先を定義
resource "aws_s3_bucket_logging" "ecs_nextjs_log_bucket_logging" {
  bucket        = aws_s3_bucket.ecs_nextjs_log_bucket.id
  target_bucket = aws_s3_bucket.alb_access_log_bucket_bclg.id
  target_prefix = "ecs-nextjs-log"
}
```

### Firehose用のIAMロールを作成する
FirehoseでS3にログを転送するために、Firehose用のIAMロールを作成します。上記で作成したS3バケットにログを書き込むための権限を持たせます。
```terraform:Firehose用のIAMロールの作成
resource "aws_iam_policy" "firehose_ecs_nextjs_log_policy" {
  name = "${var.name_prefix}-firehose-ecs-nextjs-log-policy"
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
              "s3:AbortMultipartUpload",
              "s3:GetBucketLocation",
              "s3:GetObject",
              "s3:ListBucket",
              "s3:ListBucketMultipartUploads",
              "s3:PutObject"
            ],
            "Resource": "*"
        }
    ]
})
}

resource "aws_iam_role" "firehose_ecs_nextjs_log_role" {
  name = "${var.name_prefix}-firehose-ecs-nextjs-log-role"

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

resource "aws_iam_role_policy_attachment" "firehose_ecs_nextjs_log_policy_attachment" {
  role       = aws_iam_role.firehose_ecs_nextjs_log_role.name
  policy_arn = aws_iam_policy.firehose_ecs_nextjs_log_policy.arn
}
```

### Firehoseストリームを作成する
上記で定義したS3バケットにログを転送するためのFirehoseストリームを作成します。  
ログの転送は10mb毎にファイルを分割し、またその容量に達さずとも300秒毎にはファイルを分割するように設定しています。
```terraform:Firehoseストリームを作成する
#firehoseのストリーミング
resource "aws_kinesis_firehose_delivery_stream" "nextjs_log_delivery_stream" {
  name        = "nextjs-log-delivery-stream"
  destination = "extended_s3"

  extended_s3_configuration {
    role_arn   = "${var.iam_role_firehose_nextjs_arn}"
    #保存先のS3バケット
    bucket_arn = "${var.s3_bucket_log_nextjs_arn}"
    #10MB毎にファイルを分割
    buffering_size     = 10
    #300秒毎にファイルを分割
    buffering_interval  = 300
    prefix = "nextjs-log-streaming/"
  }
}

resource "aws_cloudwatch_log_subscription_filter" "cloudwatch_to_firehose_nextjs" {
  name            = "cloudwatch-to-firehose-nextjs"
  log_group_name  = "/kaku/nodejs"
  #ロググループのログを全て取得
  filter_pattern  = ""
  destination_arn = aws_kinesis_firehose_delivery_stream.nextjs_log_delivery_stream.arn
  role_arn        = "${var.iam_role_cwl_firehose_nextjs_arn}"
}
```

### CloudWatchLogsからFirehoseへログを転送するためのIAMロールを作成する
CloudWatchLogsからFirehoseへログを転送するためのIAMロールを作成します。  
```terraform:CloudWatchLogsからFirehoseへログを転送するためのIAMロールを作成する
resource "aws_iam_policy" "cwl-nextjs-policy" {
  name = "${var.name_prefix}-cwl-nextjs-policy"
  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "firehose:*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
})
}

resource "aws_iam_role" "cwl-nextjs-role" {
  name = "${var.name_prefix}-cwl-nextjs-role"

  assume_role_policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "logs.ap-northeast-1.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
})
}

resource "aws_iam_role_policy_attachment" "cwl-nextjs-policy-attachment" {
  role       = aws_iam_role.cwl-nextjs-role.name
  policy_arn = aws_iam_policy.cwl-nextjs-policy.arn
}
```

### CloudWatchLogsでサブスクリプションフィルタを作成する
CloudWatchLogsからFirehoseへログを転送するためにサブスクリプションフィルタを作成します。  
フィルターには全てのログを転送するように設定しています。
```terraform:CloudWatchLogsでサブスクリプションフィルタを作成する
resource "aws_cloudwatch_log_subscription_filter" "cloudwatch_to_firehose_nextjs" {
  name            = "cloudwatch-to-firehose-nextjs"
  log_group_name  = "/kaku/nodejs"
  #ロググループのログを全て取得
  filter_pattern  = ""
  destination_arn = aws_kinesis_firehose_delivery_stream.nextjs_log_delivery_stream.arn
  role_arn        = "${var.iam_role_cwl_firehose_nextjs_arn}"
}
```
上記の設定で、Next.jsのログがCloudWatchLogsに保管され、またFirehoseでS3に転送されるようになります。

サブスクリプションフィルターによるログの監視等はRailsと同様の手順で実現できるため、運用の方針に合わせて追加してください。

## ElastiCache(Redis)のログの管理環境
ElastiCacheのログを保存する環境を以下の図のように構築します。
ElastiCacheのメトリクスを監視して、問題があればalartから通知が飛び、問題の内容はCloudWatchlogsInsightsで解析する、といった状況を想定しています。

この構成はElastiCacheのスロークエリログのみを出力ことを想定しているため、監査目的や解析目的でS3にログを転送するよう必要はないという判断です。もしも、エンジンログなどを出力する場合は、S3への転送を検討する必要があります。
![redis-zu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2987556/3793d115-a1a4-5546-903b-befdb2808433.png)


スロークエリログを保管するCloudWatchLogsのロググループを作成します。  
```terraform:スロークエリログを保管するCloudWatchLogsのロググループを作成する
resource "aws_cloudwatch_log_group" "redis-log" {
  name              = "/${var.name_prefix}/redis"
  retention_in_days = 3
}
```
ElasitCacheのスロークエリログをCloudWatchLogsに出力するための設定を行います。  
```terraform:ElasitCacheのスロークエリログをCloudWatchLogsに出力するための設定を行う
resource "aws_elasticache_cluster" "default" {
  ・・・
  log_delivery_configuration {
    destination      = "${var.redis_log_group}"
    destination_type = "cloudwatch-logs"
    log_format       = "text"
    #スロークエリログの出力を有効化
    log_type         = "slow-log"
  }
  ・・・
}
```
上きの設定で、ElastiCacheのスロークエリログがCloudWatchLogsに出力されるようになります。


### まとめ
以上でAWSにおけるWEBサービスのログの管理環境の構築は完了です。必要に応じて、運用の方針に沿ったalarmの設定などを行ってください。

## 参考文献
本記事は以下の書籍、記事を参考にさせていただきました。
- [AWSではじめるクラウドセキュリティ　クラウドで学ぶセキュリティ設計/実装](https://amzn.asia/d/cXcNIJ0)
- [AWS運用入門　押さえておきたいAWSの基本と運用ノウハウ](https://amzn.asia/d/csXAXv4)
- [AWS継続的セキュリティ実践ガイド ログの収集／分析による監視体制の構築](https://amzn.asia/d/6IWd4dE)
- [個人的AWS ログ管理のベースライン](https://mazyu36.hatenablog.com/entry/2023/03/17/184805)
- [AWSであらゆるものをモニタリングする方法を考える](https://zenn.dev/tech4anyone/articles/f2aa1728b82865)
- [CloudWatch LogsをS3に転送するためにEventBridge Schedulerを使用する](https://zenn.dev/fy0323/articles/0c2b5b556d5a0a)
- [AWSでの法令に則ったログ設計及び実装/分析](https://blog.engineer.adways.net/entry/2023/10/20/140000)
- [AWS WAFカウントモードの調査方法を整理してみた](https://dev.classmethod.jp/articles/waf-count-research/)
- [AWS 監視のための主要なメトリクス](https://www.datadoghq.com/ja/blog/aws-monitoring/)
- [Terraformでfirelensを使ったECSアプリログのデリバリー](https://zenn.dev/bun913/articles/c25765744352a4)