【Terraform】AWS S3構築用のmoduleを作成してみた

# はじめに
Terraformで簡単にS3を作りたいよね。ということで module 作ってみました。
どうせ作ったなら皆さんの参考になればと共有することに。

バージョンは以下の通り。
このバージョン以外での動作は確認してません。

```terraform
terraform {
  required_version = "1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.3.0"
    }
  }
}
```

GitHubにソースコードを上げておりますので、もしよろしければ参考に。
⇒ [こちらをクリック](https://github.com/onishi-app/terraform)

参考にしていただくためにGitHubに残しております。
いかなる損失が発生しても一切責任は負いませんのでよろしくお願いしますね。


# module
コードが多いので一部抜粋してややこしいところのみ説明していきます。
基本的には見たままです。

## main.tf
まずは main.tf から。
ややこしいのは`Lifecycle Rule`のリソースのみですかね。

```terraform:modules/s3/main.tf
#
# Lifecycle Rule
#
resource "aws_s3_bucket_lifecycle_configuration" "this" {
  count  = var.rule_created ? 1 : 0    ※１
  bucket = aws_s3_bucket.this.id

  rule {
    id     = var.rule_id
    status = var.rule_status

    # AND条件のフィルター
    dynamic "filter" {   ※２
      for_each = length(local.lifecycle_and_filter) > 0 ? local.lifecycle_and_filter : []  ※３
      content {
        dynamic "and" {
          for_each = local.lifecycle_and_filter
          content {
            object_size_greater_than = and.value.object_size_greater_than
            object_size_less_than    = and.value.object_size_less_than
            prefix                   = and.value.prefix
          }
        }
      }
    }

    # OR条件のフィルター
    dynamic "filter" {
      for_each = length(local.lifecycle_and_filter) == 0 ? var.filter : []
      content {
        object_size_greater_than = filter.value.object_size_greater_than
        object_size_less_than    = filter.value.object_size_less_than
        prefix                   = filter.value.prefix
      }
    }

    dynamic "transition" {
      for_each = var.transition   ※４
      content {
        date          = transition.value.date
        days          = transition.value.days
        storage_class = transition.value.storage_class
      }
    }
    ・・・
  }
}
```

### ※１  
`count  = var.rule_created ? 1 : 0`
こちらは変数の rule_created が true の場合に、aws_s3_bucket_lifecycle_configuration を１回繰り返すよ。というループ処理になります。
terraform では IF文ではなく三項演算子を使用します。（三項演算子はググってください。。）

ちなみに、ループには count や for_each 、for があるのですが、
0 or 1 の時は count で 0 or 1以上の場合は for_each がいいって記事を見ました。

まあ、今回の場合は rule_created を true にすればライフサイクルルールが作成され、
rule_created を false にすればライフサイクルルールは作成されないということです。


### ※２  
`dynamic "filter" { ・・・ }`

こちらは filter というブロックの定義を動的に変更する場合に使用します。
dynamic についての説明は [コチラ](https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks)

ライフサイクルルールでフィルターを定義したい場合としたくない場合があると思うので
変数が渡されているか否かで filter ブロックの処理を制御します。

ライフサイクルルールのフィルター設定には AND条件と OR条件の設定が可能です。
フィルターに引っかかったオブジェクトのみがライフサイクルの対象となります。
ANDの例：オブジェクトサイズが10MB以上 **かつ** error/ フォルダ配下のオブジェクト
ORの例：オブジェクトサイズが10MB以上 **もしくは** error/ フォルダ配下のオブジェクト


### ※３
`for_each = length(local.lifecycle_and_filter) > 0 ? local.lifecycle_and_filter : []`

こちらは※２とセットです。ループの分だけ filter ブロックを回します。
こちらは１回しかループさせたくない場合でも for_each を使用します。

### ※４
`for_each = var.transition`

dynamic の後の for_each はこんな感じで簡潔に書くことも可能です。
※３の場合は、AND条件とOR条件の二つの `dynamic "filter" {} `を定義していたので少し複雑になりました。

## variables.tf

次は module 内で使用する変数の定義ファイル
こちらも大したことはしてないです。

```terraform:module/s3/variables.tf
#
# aws_s3_bucket_lifecycle_configuration
#
variable "rule_created" {
  type        = bool
  default     = false
  description = "ライフサイクルルール作成の有効可否"
}
variable "filter" {
  type = list(object({    ※１
    and                      = bool   # AND条件適用の有効可否
    object_size_greater_than = number # ルールが適用されるオブジェクトの最小サイズ
    object_size_less_than    = number # ルールが適用されるオブジェクトの最大サイズ
    prefix                   = string # ルールが適用される1つまたは複数のオブジェクトを識別するプレフィックス
  }))
  default     = []
  description = "ライフサイクルルールのフィルター設定"
}
variable "transition" {
  type = list(object({
    date          = number # 指定されたストレージクラスに移動する日。日付の値はRFC3339形式
    days          = number # 指定されたストレージクラスに移動するまでの日数。正の整数のみ指定可能
    storage_class = string # 移動先のストレージクラス
  }))
  default     = []   ※２
  description = "現行バージョンのストレージ移動アクション"
}
```

### ※１
`type = list(object({・・・}))`

この形式で値を渡しなさいね！って感じで定義できます。
ただ、定義されている値が存在しない場合はエラーが発生します。
変数filter の場合は以下のような感じ。

```terraform
filter = [
  {
    and                      = true   ### AND条件 の dynamic "filter" で処理される
    object_size_greater_than = null   ### null を渡すことも可能
    object_size_less_than    = null
    prefix                   = "error/"
  }
]
```

### ※２
`default = []`

何も渡されなかった場合のデフォルト値を定義できます。
type = list(object~)って感じなので、デフォルト値もリスト型にする必要があります。（nullも可）

main.tf の ※４ですが、デフォルトは空のリストなので for_each がループせず
`dynamic "transition" {・・・}`は処理しないという流れになります。

## outputs.tf
こちらは記述少ないです。
ご覧の通り、作成したS3のバケットIDとバケットARNを返り値として使用できます。
後ほど使用しているところが出てきます。

```terraform:module/s3/outputs.tf
output "bucket_id" {
  value       = aws_s3_bucket.this.id
  description = "バケットのID"
}
output "bucket_arn" {
  value       = aws_s3_bucket.this.arn
  description = "バケットのARN"
}
```

module はこんな感じです。


# module 呼び出し元
module 呼び出し元の説明です。
デフォルト値でS3を作成する場合と、色々と設定を変更している場合の２種類をGitHubにはあげてます。

## storage.tf
まずはデフォルト値でS3を作成する場合
prefix と subname については、module の変数内でデフォルト値が定義されていない変数です。
なので module 呼び出し元での代入は必須となります。

```terraform:storage.tf
module "s3_logs" {   ※１
  source = "./modules/s3"   ※２

  # common
  prefix  = "${var.system}-${var.env}"
  subname = "logs"
}
```

### ※１
`module "s3_logs" { }`

module を呼び出す場合は`module "XXX" { }`の形式で記述する必要があります。

### ※２
`source = "./modules/s3"`

どのmoduleを使用するか指定する必要があります。相対パスです。

<hr>

色々と設定を変更している場合。
基本的には module の変数ファイルで指定されている型で代入していけばOK。

```terraform:storage.tf
module "s3_sample_bucket" {
  source = "./modules/s3"

  # common
  prefix  = "${var.system}-${var.env}"
  subname = "sample-bucket"

  # Transfer Acceleration
  accelerate_status = "Enabled"

  # Bucket Ownership
  object_ownership = "ObjectWriter"

  # Public Access
  public_access_block_config = {
    block_public_acls       = false
    ignore_public_acls      = false
    block_public_policy     = false
    restrict_public_buckets = false
  }

  # Logging
  logging_enabled = true
  target_bucket   = module.s3_logs.bucket_id   ※３
  target_prefix   = "sample-bucket/"

  # SSE Encrypt
  sse_algorithm     = "aws:kms"
  kms_master_key_id = "aws/s3"

  # Versioning
  versioning_status = "Enabled"

  # Lifecycle Rule
  rule_created = true
  rule_id      = "sample-lifecycle-rule"
  filter = [{
      and                      = true
      object_size_greater_than = null
      object_size_less_than    = null
      prefix                   = "error/"
  }]
  transition = [{
    date          = null
    days          = 2
    storage_class = "GLACIER"
  }]
  expiration = [{
    date                         = null
    days                         = 4
    expired_object_delete_marker = false
  }]
  # non_ver_transition = [{
  #   newer_noncurrent_versions = 6
  #   noncurrent_days           = 10
  #   storage_class             = "GLACIER"
  # }]
  non_ver_expiration = [{
    newer_noncurrent_versions = 4
    noncurrent_days           = 12
  }]
}
```


### ※３
`target_bucket   = module.s3_logs.bucket_id`

先ほどチラッと出てきた outputs.tf で定義した内容は上記のように使用することが可能です。

module.s3_logs はデフォルト値で作成した S3。
その S3 の バケットIDを、変数target_bucket に代入しています。


# AWSリソース
実際に storage.tf の内容でリソースを作成するとこんな感じになります。
想定通り２つのバケットが作成されてる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/797483/188a2e25-80ae-4428-b78c-df5a3515c8fd.png)

terra-dev-s3-sample-bucketバケットはバージョニングの設定なども問題なく出来てる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/797483/61db3f29-bd43-588a-6a47-8c416ad14695.png)

サーバアクセスログは先ほどの outputs.tf で取得したバケットIDを使用して指定しています。
問題ないですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/797483/2bb2d050-0b85-9a5d-eee0-1cf3bdcbe11f.png)

ライフサイクルルールもいい感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/797483/f65b0d7e-10ee-9070-acb3-0527e2c6ea2e.png)

### 2023/06/23 追記
バケットポリシーを設定できるように module を修正しました。
コードは GitHub をご覧ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/797483/23e42628-cb83-2b8b-8ecc-eced85e0d42b.png)

アクセスログも問題なく出力されているのでOKそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/797483/e8029a5c-21a4-2fbb-e53e-c4e85bb890d9.png)




# さいごに

最後までご覧いただきありがとうございました。
一度 module を作ってしまえばその後はとても楽なので、ぜひ皆さんも module を活用しましょう！

今後もGitHubは更新していく予定なので役に立ったらコメントでもしてくれると嬉しくて泣きます。
module の変更案などもコメントしてください。

では、良いTerraformライフをお送りください。