Terraformのfor_eachとCFnのStackSetsを使って、効率良くGuardDutyを全リージョンで有効化する方法
はじめに
突然ですが、私は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