🦅

Terraformのfor_eachとCFnのStackSetsを使って、効率良くGuardDutyを全リージョンで有効化する方法

2025/01/05に公開

はじめに

突然ですが、私はTerraform教の人間です。
Terraform教には、全てのAWSリソースの作成/設定をTerraformでやりなさいという教えがあります(嘘です)。
Terraformは非常に強力なIaCツールなので、この教えに従うことは大して難しくありません。

ただ、Terraformには「マルチリージョンのリソース作成/設定が非効率」という弱点があります。
Terraformでリソース定義を再利用するテクニックにfor_eachというメタ引数の活用があるのですが、for_eachでは同じメタ引数であるproviderに異なる値を渡せないという制約があります(v1.10時点では)。
その為、マルチリージョンのリソース作成/設定をする場合は、リージョンの数だけリソース定義が必要で、これが弱点の理由です。

NG🙅‍♂‍

provider "aws" {
  alias  = "apne1"
  region = "ap-northeast-1"
}

provider "aws" {
  alias  = "use1"
  region = "us-east-1"
}

resource "aws_guardduty_detector" "this" {
  # Meta arguments
  for_each = toset([
    "aws.apne1",
    "aws.use1"
  ])
  provider = each.value
  # Resource arguments
  enable = true
  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = false
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }
}

OK🙆‍♂‍

provider "aws" {
  alias  = "apne1"
  region = "ap-northeast-1"
}

provider "aws" {
  alias  = "use1"
  region = "us-east-1"
}

resource "aws_guardduty_detector" "apne1" {
  # Meta arguments
  provider = aws.apne1
  # Resource arguments
  enable = true
  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = false
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }
}

resource "aws_guardduty_detector" "use1" {
  # Meta arguments
  provider = aws.use1
  # Resource arguments
  enable = true
  datasources {
    s3_logs {
      enable = true
    }
    kubernetes {
      audit_logs {
        enable = false
      }
    }
    malware_protection {
      scan_ec2_instance_with_findings {
        ebs_volumes {
          enable = true
        }
      }
    }
  }
}

module化すれば多少は効率化が可能ですが、それでもリージョンの数だけproviderの定義とmoduleを呼び出す定義は必要になります。

ただ、「マルチリージョンのリソース作成/設定が非効率」といっても、2つや3つのリージョンに同じリソースを作成/設定する分には、リージョンの数だけmoduleを呼び出してやれば良いので、この程度ではさほど問題はありません。

しかし、GuardDutyのような全リージョンで有効化することがベストプラクティスとされているサービスは別で、全リージョン分のmoduleを呼び出すのは、流石のTerraform教の人間でも「ぐぬぬ・・・」となります(私だけかもしれませんが)。

その点、AWS純正のIaCツールであるCFnには、StackSetsというマルチリージョン/マルチアカウントのリソース作成/設定を効率良く実施できる機能があります。
が、Terraform教の教えがあるので、GuardDutyの設定だけCFnでするようなことはできません(そんなことはありません)。

ここで、私はあることを閃きました💡
「GuardDutyの設定自体はCFnで実施して、CFnのテンプレートとかキックはTerraformで管理すれば、Terraform教の教えに背かなくね?」

前置きが長くなりましたが、本記事では、Terraformのfor_eachとCFnのStackSetsを使って、効率良くGuardDutyを全リージョンで有効化する方法を紹介します。

設定手順

StackSets用のIAMロールの作成

StackSetsの作成で必要になるIAMロールをTerraformで作成する。

data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "assume_role_exec" {
  statement {
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "exec" {
  name               = "AWSCloudFormationStackSetExecutionRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role_exec.json
  tags = {
    Name = "AWSCloudFormationStackSetExecutionRole"
  }
}

resource "aws_iam_role_policy_attachment" "exec" {
  role       = aws_iam_role.exec.name
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

data "aws_iam_policy_document" "assume_role_admin" {
  statement {
    actions = [
      "sts:AssumeRole"
    ]
    effect = "Allow"
    principals {
      type = "Service"
      identifiers = [
        "cloudformation.amazonaws.com"
      ]
    }
  }
}

resource "aws_iam_role" "admin" {
  name               = "AWSCloudFormationStackSetAdministrationRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role_admin.json
  tags = {
    Name = "AWSCloudFormationStackSetAdministrationRole"
  }
}

data "aws_iam_policy_document" "admin" {
  statement {
    effect    = "Allow"
    resources = [aws_iam_role.exec.arn]
    actions   = ["sts:AssumeRole"]
  }
}

resource "aws_iam_policy" "admin" {
  name   = "AssumeRole-AWSCloudFormationStackSetExecutionRole"
  policy = data.aws_iam_policy_document.admin.json
  tags = {
    Name = "AssumeRole-AWSCloudFormationStackSetExecutionRole"
  }
}

resource "aws_iam_role_policy_attachment" "admin" {
  role       = aws_iam_role.admin.name
  policy_arn = aws_iam_policy.admin.arn
}

StackSets、StackSetsInstanceの作成

GuardDutyを設定するCFnを配布するStackSetsを作成する。
StackSetsInstanceをfor_eachでループし、全リージョンでCFnをキックする。
リージョン毎にパラメータを変えたいので、リージョン名を条件に異なるパラメータをStackSetsInstanceに設定する。

locals {
  guardduty_regions = [
    "us-east-1",
    "us-east-2",
    "us-west-1",
    "us-west-2",
    "ap-south-1",
    "ap-northeast-3",
    "ap-northeast-2",
    "ap-southeast-1",
    "ap-southeast-2",
    "ap-northeast-1",
    "ca-central-1",
    "eu-central-1",
    "eu-west-1",
    "eu-west-2",
    "eu-west-3",
    "eu-north-1",
    "sa-east-1"
  ]
}

resource "aws_cloudformation_stack_set" "main" {
  # Resource arguments
  ## Template selection
  administration_role_arn = aws_iam_role.admin.arn
  execution_role_name     = aws_iam_role.exec.name
  template_body           = file("${path.module}/templates/enable-guardduty-template.yml")
  ## Specify StackSet details
  name        = "EnableAmazonGuardDuty"
  description = null
  parameters = {
    S3Protection              = "DISABLED"
    RuntimeMonitoring         = "DISABLED"
    ECSFargateAgentManagement = "DISABLED"
    RDSProtection             = "DISABLED"
    LambdaProtection          = "DISABLED"
  }
  ## Setting StackSet options
  tags = {
    Name = "EnableAmazonGuardDuty"
  }
  ## Setting deployment options
  operation_preferences {
    max_concurrent_count    = 20
    failure_tolerance_count = 20
    region_concurrency_type = "PARALLEL"
  }
}

resource "aws_cloudformation_stack_set_instance" "this" {
  # Meta arguments
  for_each       = toset(local.guardduty_regions)
  # Resource arguments
  region         = each.value
  stack_set_name = aws_cloudformation_stack_set.main.name
  parameter_overrides = {
    S3Protection              = each.key == "ap-northeast-1" ? "ENABLED" : null
    RuntimeMonitoring         = each.key == "ap-northeast-1" ? "ENABLED" : null
    ECSFargateAgentManagement = each.key == "ap-northeast-1" ? "ENABLED" : null
    RDSProtection             = each.key == "ap-northeast-1" ? "ENABLED" : null
    LambdaProtection          = each.key == "ap-northeast-1" ? "ENABLED" : null
  }
}
AWSTemplateFormatVersion: 2010-09-09
Description: Enable Amazon GuardDuty

Parameters:
  S3Protection:
    Type: String
  RuntimeMonitoring:
    Type: String
  ECSFargateAgentManagement:
    Type: String
  RDSProtection:
    Type: String
  LambdaProtection:
    Type: String

Resources:
  GuardDutyDetector:
    Type: AWS::GuardDuty::Detector
    Properties:
      Enable: True
      FindingPublishingFrequency: SIX_HOURS
      Features:
        - Name: S3_DATA_EVENTS
          Status:
            Ref: S3Protection
        - Name: EKS_AUDIT_LOGS
          Status: DISABLED
        - Name: RUNTIME_MONITORING
          Status:
            Ref: RuntimeMonitoring
          AdditionalConfiguration:
            - Name: EKS_ADDON_MANAGEMENT
              Status: DISABLED
            - Name: ECS_FARGATE_AGENT_MANAGEMENT
              Status:
                Ref: ECSFargateAgentManagement
            - Name: EC2_AGENT_MANAGEMENT
              Status: DISABLED
        - Name: EBS_MALWARE_PROTECTION
          Status: DISABLED
        - Name: RDS_LOGIN_EVENTS
          Status:
            Ref: RDSProtection
        - Name: LAMBDA_NETWORK_LOGS
          Status:
            Ref: LambdaProtection

さいごに

このテクニックはGuardDutyに限らず、CFnで設定できる全てのサービスで使えます。
Terraformでネイティブに、for_eachでマルチプロバイダー(マルチリージョン/マルチアカウント)設定ができるようになれば良いんですけどね。

Discussion