【AWS】AWS ConfigルールをTerraformを利用して展開する

# 1. はじめに
こんにちは。
最近は社内でも有志でTerraform勉強会があったり、案件でも使う場面があったりとTerraformに触れる機会が多くなってきました。

私はAWSサービスの中でもAWSConfigを扱うことが多いのですが、案件で扱うConfigルールは数十単位になるため手動設定はかなりの手間になってしまうということから、数年前からConfigルール設定のIaC化に取り組んできました。
今回はその中でもTerraformで実装する方法について共有したいと思います。

# 2. 構成
AWS Configを設定する際に必要な基本的な設計要素のみで今回は実装していきたいと思います。
![構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/091f1fd9-429b-55e7-0ebd-93f9a0329b96.png)

# 3. 実装
## 3.1 フォルダ構成
以下のようなフォルダ構成で実装していきます。
リソース名などは変数管理としたいため、Local Valuesを利用します。
参考：[Local Values](https://developer.hashicorp.com/terraform/language/values/locals)

```
Terraform/
└─ dev
    ├─ config.tf
    ├─ s3.tf
    ├─ locals.tf
    └─ terraform.tf
```

## 3.2 terraform.tf
以下の内容でterraform.tfを設定します。
アカウントIDとリージョンをコード内で取得できるようにしたいので、```aws_caller_identity```と```aws_region```のdata source設定を記載しておきます。

```terraform.tf
# providerの定義
provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  required_version = ">=1.0"
  required_providers {
    awscc = {
      source  = "hashicorp/awscc"
      version = "0.45.0"
    }
  }
}

# 実行するAWSアカウント情報取得用
data "aws_caller_identity" "self" {}

# 実行するリージョン情報取得用
data "aws_region" "self" {}
```

## 3.3 s3.tf
S3のバケット名は後述する```locals.tf```に記載するため、以下のコード内ではlocalsを参照するようにしています。
ConfigとS3の連携にはS3バケットポリシーの設定が必要となるため、それも記載します。

```s3.tf
# S3 Bucket
resource "aws_s3_bucket" "this" {
  for_each      = toset(local.s3_bucket)
  force_destroy = true

  bucket = "${local.env}-${each.value}"

  tags = {
    Name = "${local.env}-${each.value}"
  }
}

# S3 Bucket policy
resource "aws_s3_bucket_policy" "config-service" {
  bucket = "${local.env}-awsconfig-${data.aws_caller_identity.self.account_id}"
  policy = data.aws_iam_policy_document.config-service.json
}

data "aws_iam_policy_document" "config-service" {
  version = "2012-10-17"
  statement {
    sid    = "AWSConfigBucketPermissionsCheck"
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }
    actions = [
      "s3:GetBucketAcl"
    ]
    resources = [
      "${aws_s3_bucket.this["awsconfig-${data.aws_caller_identity.self.account_id}"].arn}"
    ]
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceAccount"
      values   = ["${data.aws_caller_identity.self.account_id}"]
    }
  }
  statement {
    sid    = "AWSConfigBucketExistenceCheck"
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }
    actions = [
      "s3:ListBucket"
    ]
    resources = [
      "${aws_s3_bucket.this["awsconfig-${data.aws_caller_identity.self.account_id}"].arn}"
    ]
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceAccount"
      values   = ["${data.aws_caller_identity.self.account_id}"]
    }
  }
  statement {
    sid    = "AWSConfigBucketDelivery"
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["config.amazonaws.com"]
    }
    actions = [
      "s3:PutObject"
    ]
    resources = [
      "${aws_s3_bucket.this["awsconfig-${data.aws_caller_identity.self.account_id}"].arn}/AWSLogs/${data.aws_caller_identity.self.account_id}/Config/*"
    ]
    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceAccount"
      values   = ["${data.aws_caller_identity.self.account_id}"]
    }
  }
}

# S3 Public Access Block
resource "aws_s3_bucket_public_access_block" "config-service" {
  bucket = aws_s3_bucket.this["awsconfig-${data.aws_caller_identity.self.account_id}"].id

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


## 3.4 config.tf
IAMロールはConfigのサービスリンクロールを作成します。
Configの配信先S3バケットに、3.3で作成したS3バケットを指定します。

Configを有効化・Configルールを作成するためには以下のリソースを利用します。
- aws_config_configuration_recorder
- aws_config_configuration_recorder_status
- aws_config_delivery_channel
- aws_config_config_rule

設定レコーダー（aws_config_configuration_recorder）が設定されてからでないと配信チャネル（aws_config_delivery_channel）の設定ができないといったように設定内容に依存関係が存在するため、```depends_on```で依存関係を明記してあげます。

Configルールの設定パラメータは```locals.tf```からcan文を用いて取得するように記載しております。
can文の使い方についてはまた別の記事にしたいと思います。

```config.tf
# IAM
# AWSConfigサービスロールの作成
resource "aws_iam_service_linked_role" "config-service" {
  aws_service_name = "config.amazonaws.com"
}

# Config
resource "aws_config_configuration_recorder_status" "config-service" {
  name       = aws_config_configuration_recorder.config-service.name
  is_enabled = true
  depends_on = [aws_config_delivery_channel.config-service]
}

resource "aws_config_configuration_recorder" "config-service" {
  name     = "${local.env}-awsconfig"
  role_arn = aws_iam_service_linked_role.config-service.arn
  recording_group {
    all_supported                 = "true"
    include_global_resource_types = "true"
  }
}

resource "aws_config_delivery_channel" "config-service" {
  name           = "${local.env}-awsconfig"
  s3_bucket_name = aws_s3_bucket.this["awsconfig-${data.aws_caller_identity.self.account_id}"].id
  depends_on     = [aws_config_configuration_recorder.config-service]
}

#Config rule
resource "aws_config_config_rule" "config_rules" {
  for_each = local.config_rules
  name     = each.key
  source {
    owner             = "AWS"
    source_identifier = each.value.source_identifier
  }
  input_parameters = can(each.value.input_parameters) ? each.value.input_parameters : null
  scope {
    compliance_resource_types = can(each.value.compliance_resource_types) ? each.value.compliance_resource_types : null
  }
  maximum_execution_frequency = can(each.value.maximum_execution_frequency) ? each.value.maximum_execution_frequency : null

  depends_on = [aws_config_configuration_recorder.config-service]
}
```


## 3.5 locals.tf
```locals.tf```では以下の内容を記載します。
- 環境名：dev
- S3バケット名：awsconfig-{AWSアカウントID}
- Configルール：
    - wafv2-logging-enabled
    - db-instance-backup-enabled
    - api-gw-endpoint-type-check
    - cloudwatch-alarm-action-check
    - fms-webacl-resource-policy-check


```locals.tf
# 環境名
locals {
  env = "dev"
}

# S3バケット名
locals {
  s3_bucket = [
    "awsconfig-${data.aws_caller_identity.self.account_id}"
  ]
}

# Configルール有効化
locals {
  config_rules = {
    wafv2-logging-enabled = {
      source_identifier           = "WAFV2_LOGGING_ENABLED"
      maximum_execution_frequency = "TwentyFour_Hours"
    }
    db-instance-backup-enabled = {
      source_identifier = "DB_INSTANCE_BACKUP_ENABLED"
      compliance_resource_types = [
        "AWS::RDS::DBInstance"
      ]
    }
    api-gw-endpoint-type-check = {
      source_identifier = "API_GW_ENDPOINT_TYPE_CHECK"
      compliance_resource_types = [
        "AWS::ApiGateway::RestApi"
      ]
      input_parameters = jsonencode(
        {
          endpointConfigurationTypes = "EDGE"
        }
      )
    }
    cloudwatch-alarm-action-check = {
      source_identifier = "CLOUDWATCH_ALARM_ACTION_CHECK"
      compliance_resource_types = [
        "AWS::CloudWatch::Alarm"
      ]
      input_parameters = jsonencode(
        {
          alarmActionRequired            = "true",
          insufficientDataActionRequired = "false",
          okActionRequired               = "false"
        }
      )
    }
    fms-webacl-resource-policy-check = {
      source_identifier = "FMS_WEBACL_RESOURCE_POLICY_CHECK"
      compliance_resource_types = [
        "AWS::ApiGateway::Stage",
        "AWS::ElasticLoadBalancingV2::LoadBalancer",
        "AWS::WAFRegional::WebACL"
      ]
      input_parameters = jsonencode(
        {
          webACLId = "00000000-0000-0000-0000-000000000000,11111111-1111-1111-1111-111111111111"
        }
      )
    }
  }
}
```

# 4. デプロイ
まず```terraform plan```を実行してエラーがないことを確認してから```terraform apply```を実行して、AWS環境にデプロイします。

```
> terraform apply
data.aws_region.self: Reading...
data.aws_caller_identity.self: Reading...
data.aws_region.self: Read complete after 0s [id=ap-northeast-1]
data.aws_caller_identity.self: Read complete after 0s [id={account_id}]

Terraform used the selected providers to generate the following execution plan.    
Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

~~~中略~~~

Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
```
ログに```Apply complete!```と出力されたらデプロイ完了です。

# 5. 実機確認
AWSマネジメントコンソールにアクセスし、実際にリソースが作成されているか確認します。

- IAMロール
![IAMロール_0424.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/bd8cbb9f-03b9-2852-c5d8-b3c0ed326b52.jpeg)

- S3バケット
![S3.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/d4dc4b0a-ecf4-9555-045f-471028c276e0.jpeg)

- S3バケットポリシー
![S3_バケットポリシー.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/52165425-225b-18f0-6fc5-ba2d40281e5e.jpeg)

- Config設定
![Config_0424.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/a33618a5-d6d2-73fa-7330-c357ddab65a3.jpeg)

- Configルール
![Configルール_0424.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/caecc374-823b-df85-1d45-16453463615c.jpeg)

    - wafv2-logging-enabled
![wafv2-logging-enabled.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/df38dea9-a080-0276-0c74-435dbb31a30a.jpeg)

    - db-instance-backup-enabled
![db-instance-backup-enabled.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/975d30ce-74a4-2c04-83d9-70fbc79dd8c2.jpeg)

    - api-gw-endpoint-type-check
![api-gw-endpoint-type-check.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/32f024fd-6c58-d98b-0cb4-a44174cd281d.jpeg)

    - cloudwatch-alarm-action-check
![cloudwatch-alarm-action-check.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/dd1816b5-8d0c-386d-eae4-9a5485d70250.jpeg)

    - fms-webacl-resource-policy-check
![fms-webacl-resource-policy-check.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/f2f93d20-7800-b9cd-f476-e8cdb8b982b6.jpeg)


Terraformコードで記載した通りの設定が反映されていることが確認できました。

# 6. おわりに
今回はAWS Configと周辺環境をTerraformを利用して展開する方法について記事にしてみました。
依存関係や設定する権限が不足していたなど、実際にTerraformを実行してみて初めて気づいた注意点などもあったので、こちらの記事が参考になれば幸いです。

# 7. 参考
https://developer.hashicorp.com/terraform/language/values/locals

https://registry.terraform.io/providers/hashicorp/aws/latest/docs