blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2024.11.26 技術記事

Terraform を活用した効率的な S3 バケット管理手法 [DeNA インフラ SRE]

by Tatsuki Mutsuro

#infrastructure #sre #aws #s3 #terraform #infra-quality #infra-delivery

はじめに

こんにちは。 IT 基盤部第四グループの無津呂です。 全世界向けゲームタイトルのインフラ運用を担当しています。

当グループでは、複数のゲームタイトルのインフラを開発環境・検証環境・本番環境にわたって管理しています。 AWS の S3 は、当グループが管理しているインフラリソースの 1 つです。 その S3 のバケットは、合計数百個になります。

当グループはこれまで、大量の S3 バケットをうまく管理できていませんでした。 Terraform を利用して IaC 化してはいたのですが、その Terraform が乱雑になってしまっていたのです。

本記事では、こうした大量の S3 バケットを簡単に管理するためにどのように Terraform コードを改善したか、そしてその結果たどり着いた管理手法をご紹介します。

これまでの S3 バケットの Terraform 管理における課題

これまで、S3 バケットの Terraform コードは以下のように書かれていました。

locals {
  env = "dev"
  title_map = {
    "A01" = "titleA"
    "A02" = "titleA"
    "B03" = "titleB"
  }
}

module "common_buckets" {
  source = "../modules/aws/s3/common_bucket"
  bucket_names = [
    for val in concat(
      setproduct(
        [
          "nginxlog",
          "lblog",
          "website",
        ],
        [
          "A01",
          "A02",
      ]),
      setproduct(
        [
          "nginxlog",
          "lblog",
        ],
        [
          "B03",
      ]),
    ) : "${val[1]}-${val[0]}-${local.title_map[val[1]]}-${local.env}"
  ]
}

上のコードは複数のバケットを管理する例です。 common_buckets モジュールの bucket_names にバケット名のリストを渡すことによって、モジュール内でバケットリソースを作成します。

コードの中でひときわ目立つ setproduct 関数、concat 関数、for 式たちは、バケット名リストを作る役割を持っています。 これらを駆使して "${val[1]}-${val[0]}-${local.title_map[val[1]]}-${local.env}" に各種変数を埋め込むことで、バケット名を作る仕組みです。

このバケット名を作る部分が、なかなか厄介です。 bucket_names にどんなバケット名リストが渡されるか直感的にわかる、という方はいないでしょう。

この例のように、Terraform コード上のバケット名が変数で埋め尽くされ、一見しただけではどんな名前のバケットなのかわからないという課題がありました。

また、これまでの Terraform 管理における課題はもう 1 つありました。 それは、すべてのバケットが Terraform 管理されていたわけではなく、管理から漏れていたバケットも多数存在したということです。 さきほどのコードの可読性のなさと相まって、どのバケットが管理されていて、どのバケットが管理されていないのかを把握するのも大変な状況でした。

Terraform コード改善の難点

これまでの課題から、コードの可読性を損なわずに大量の S3 バケットを漏れなく Terraform で管理する必要がありました。

「コード上のバケット名が変数で埋め尽くされ、一見しただけではどんな名前のバケットなのかわからない」という問題に対しては、バケット名をベタ書きすれば解決できそうです。 先ほどのコード例を書き換えると、下のようになります。

module "common_buckets" {
  source = "../modules/aws/s3/common_bucket"
  bucket_names = [
    "A01-nginxlog-titleA-dev",
    "A01-lblog-titleA-dev",
    "A01-website-titleA-dev",
    "A02-nginxlog-titleA-dev",
    "A02-lblog-titleA-dev",
    "A02-website-titleA-dev",
    "B03-nginxlog-titleB-dev",
    "B03-lblog-titleB-dev",
  ]
}

一目見てバケット名がわかるようになりました。

ただ、これだけではバケット名の一覧性を高く保てません。

上のコードは 1 つのモジュールだけ利用していますが、それは全バケットが同じ設定だからです。 現実的にはバケットによって設定が異なるため、バケット設定ごとにモジュールを分けて管理することになります。

module "common_buckets" {
  source = "../modules/aws/s3/common_bucket"
  bucket_names = [
    "A01-nginxlog-titleA-dev",
    "A01-lblog-titleA-dev",
    "A01-website-titleA-dev",
    "A02-nginxlog-titleA-dev",
    "A02-lblog-titleA-dev",
    "A02-website-titleA-dev",
    "B03-nginxlog-titleB-dev",
    "B03-lblog-titleB-dev",
  ]
}

module "public_buckets" {
  source = "../modules/aws/s3/public_bucket"
  bucket_names = [
    "A01-public-titleA-dev",
    "B03-public-titleB-dev",
  ]
}

module "expire_1week_buckets" {
  source = "../modules/aws/s3/expire_bucket"
  bucket_names = [
    "A01-dbbackup-titleA-dev",
  ]
  expiration_days = 7
}

このように、設定に応じた多数のモジュールブロックを書けば、コード全体を見たときのバケット名の一覧性が悪化します。 加えて、バケットが設定ごとにグループ化されることで、関連性の高いバケットどうしが散らばってしまうという弊害もあります。

S3 バケットの新たな Terraform 管理方法

これらの課題に対して、どのように Terraform を書き、多数のバケットを管理するようにしたかを紹介します。

まずは管理イメージからお見せします。

バケットの管理イメージ

管理の肝は、バケットの名前・設定・用途を、1 行につき 1 バケットずつ並べて書くことです。 先ほどの例と同じバケットを管理するコードは次のようになります。

locals {
  buckets = {
    # バケット名                   バケット設定                           バケットの用途
    "A01-nginxlog-titleA-dev" = module.s3conf.common_bucket       # nginx のログ
    "A01-lblog-titleA-dev"    = module.s3conf.common_bucket       # ロードバランサのログ
    "A01-website-titleA-dev"  = module.s3conf.common_bucket       # ウェブサイト
    "A01-public-titleA-dev"   = module.s3conf.public_bucket       # 公開データ
    "A01-dbbackup-titleA-dev" = module.s3conf.expire_1week_bucket # DB のバックアップ
    "A02-nginxlog-titleA-dev" = module.s3conf.common_bucket       # nginx のログ
    "A02-lblog-titleA-dev"    = module.s3conf.common_bucket       # ロードバランサのログ
    "A02-website-titleA-dev"  = module.s3conf.common_bucket       # ウェブサイト
    "B03-nginxlog-titleB-dev" = module.s3conf.common_bucket       # nginx のログ
    "B03-lblog-titleB-dev"    = module.s3conf.common_bucket       # ロードバランサのログ
    "B03-public-titleB-dev"   = module.s3conf.public_bucket       # 公開データ
  }
}

どんな名前のバケットがどんな設定と用途で存在するか、一目瞭然です。 (module.s3conf.xxx_buckets については後ほど説明します。)

この形式であれば、異なる設定のバケットがたくさんあったとしても 1 バケット 1 行ずつしか増えないので、長期的な運用にも耐えられます。

さらに、バケットの用途をコメントに添えることにより、Terraform コードをドキュメントとして扱えるようにもなります。 インフラ・コード・ドキュメントが一体化されたも同然になり、メンテナンスコストを削減できます。

バケットの追加・削除・設定変更

この管理方法なら、実際の運用のコストも削減できます。 たとえば、バケットの追加・削除は、バケット一覧の行を追加・削除するだけで済みます。

バケットの設定を変えることも簡単です。 以下は、もともとデフォルト設定 (common_bucket) だった A01-nginxlog-titleA-devA01-lblog-titleA-dev のバージョニングを有効にした例です。

locals {
  buckets = {
    # バケット名                   バケット設定                         バケットの用途
    "A01-nginxlog-titleA-dev" = module.s3conf.versioning_bucket # nginx のログ
    "A01-lblog-titleA-dev"    = module.s3conf.versioning_bucket # ロードバランサのログ
    "A01-website-titleA-dev"  = module.s3conf.common_bucket     # ウェブサイト
    ...

ここでは、バージョニングが有効な versioning_bucket という設定を用意して、変更したいバケットを一覧から versioning_bucket に変更しました。

もちろん、もう 1 つの設定変更方法として、デフォルト設定になっているすべてのバケットを一括でバージョニング有効にする場合、common_bucket という設定側をバージョニング有効にしてしまうことも可能です。

ただし、開発環境から本番環境まで一括で設定変更するのは事故の元になるため、一部の環境・バケットから段階的に設定を変えていくのが安全です。 先ほどのように versioning_bucket 設定を新設する方法であれば、特定の環境・バケットだけ設定を変えるのも簡単なので、基本的にはバケット設定を新設する方法を採用します。

S3 バケットの一覧管理を実現する Terraform コード

それでは、バケットを一覧管理する Terraform コードの中身について触れていきます。

ディレクトリ構成

全体の構成は以下になります。

.
├── modules
│   └── aws
│       └── s3
│           └── bucket_configs # バケット設定モジュール
│               ├── configs.tf # メイン設定
│               ├── policies.tf
│               └── policy
│                   ├── allow_all_get_object.tftpl
│                   └── elb_accesslog.tftpl
├── titleA-prod
│   └── s3.tf # titleA 本番環境の S3 バケット
└── titleB-dev
    └── s3.tf # titleB 開発環境の S3 バケット

modules/aws/s3/bucket_configs ディレクトリ以下に、バケットの設定を具体的に定義するコード、バケット設定モジュールがあります。

titleA-prod, titleB-dev ディレクトリはタイトル・環境ごとのリソースを定義する場所です。 ここに s3.tf を配置して、S3 バケットを一覧化します。

実装の詳細

実際のコードはこのように実装します。

まず、バケット設定モジュールで、common_bucket などのバケット設定をモジュールの output として定義します。

# modules/aws/s3/bucket_configs/configs.tf
output "common_bucket" {
  value = {
    versioning = local.disabled_versioning
    ...
  }
}

locals {
  # versioning
  disabled_versioning = {
    status = "Disabled"
  }
  ...
}

次に、各環境の s3.tf 内で、バケット設定モジュールを s3conf という名前で呼び出します。 そして s3conf モジュールのバケット設定を利用して、バケットを一覧形式で定義します。

# titleA-prod/s3.tf
module "s3conf" {
  source = "../modules/aws/s3/bucket_configs"
}

locals {
  buckets = {
    # バケット名                   バケット設定                     バケットの用途
    "A01-nginxlog-titleA-dev" = module.s3conf.common_bucket # nginx のログ
    ...
  }
}

この時点で、A01-nginxlog-titleA-dev バケットには以下の設定が割り当てられることになりますが、バケットや設定が実体として存在するわけではありません。

{
  versioning = {
    status = "Disabled"
  }
  ...
}

実際にリソースを作成する部分は、OSS のモジュールを活用させてもらっています。 各バケットの設定を terraform-aws-s3-bucket モジュールに渡すことで、バケットとその周辺リソースを作成します。

# titleA-prod/s3.tf
module "s3_bucket" {
  source   = "terraform-aws-modules/s3-bucket/aws"
  for_each = local.buckets
  bucket   = each.key

  versioning = try(each.value.versioning, {})
  ...
}

逆にたどると、terraform-aws-s3-bucket モジュールの引数に合う形で、バケット設定モジュールの output を記述する必要があります。

基本実装は以上です。

補足:バケットポリシーの管理方法

バケットポリシーは、バケット一覧とは別に一覧を作って管理しています。

これは、バケットポリシーをメイン設定に組み込むことが難しかったからです。 バケットポリシーにはバケット名などが含まれるため、モジュールとして汎用化するためにはテンプレート化が必要です。 コードの構造的に、パラメータの多いバケットポリシーをメイン設定に入れてしまうと、バケット一覧を見やすく保つことが難しくなります。

苦肉の策ですが、バケットポリシーはこのように設定するのが丸く収まると考えました。

まず、バケット設定モジュールの中に policy ディレクトリを作り、1 ファイルにつき 1 つのバケットポリシー(テンプレート)を書きます。 具体的には以下のようなファイルです。 ここではバケット名が入る箇所を ${bucket_name} で変数化しています。

# modules/aws/s3/bucket_configs/policy/allow_all_get_object.tftpl
${jsonencode({
  "Version" : "2012-10-17",
  "Statement" : [
    {
      "Sid" : "AllowAll",
      "Effect" : "Allow",
      "Principal" : "*",
      "Action" : "s3:GetObject",
      "Resource" : "arn:aws:s3:::${bucket_name}/*",
    },
  ],
})}

バケット設定モジュールの policies.tf に、次のコードを記述します。 これにより、設定モジュールを呼び出す側では、テンプレートファイルのパスが module.s3conf.allow_all_get_object_policy で取得できるようになります。

# modules/aws/s3/bucket_configs/policies.tf
output "allow_all_get_object_policy" {
  value = "${path.module}/policy/allow_all_get_object.tftpl"
}

そして s3.tfbucket_policies を追加して、バケット名とバケットポリシーの対応を書きます。 templatefile 関数を使い、テンプレートの bucket_name 変数にバケット名を渡してバケットポリシーをレンダリングしています。

# titleA-prod/s3.tf
module "s3conf" {
  source = "../modules/aws/s3/bucket_configs"
}

locals {
  buckets = {
    # バケット名                   バケット設定                     バケットの用途
    "A01-nginxlog-titleA-dev" = module.s3conf.common_bucket # nginx のログ
    ...
  }

  bucket_policies = {
    # バケット名                   バケットポリシー
    "A01-nginxlog-titleA-dev" = templatefile(module.s3conf.allow_all_get_object_policy, { bucket_name = "A01-nginxlog-titleA-dev" })
    ...
  }
}

バケットのメイン設定と同様に、 terraform-aws-s3-bucket モジュールにバケットポリシーを渡し、リソースを作成します。

# titleA-prod/s3.tf
module "s3_bucket" {
  source   = "terraform-aws-modules/s3-bucket/aws"
  for_each = local.buckets
  bucket   = each.key
  
  versioning = try(each.value.versioning, {})

  # ここでバケットポリシーを渡す
  attach_policy = contains(keys(local.bucket_policies), each.key)
  policy        = try(local.bucket_policies[each.key], null)
}

以上が、一覧管理を実現する Terraform コードの中身になります。

まとめ

これまでは、バケット名の定義が読みづらくなっていたり、バケットが Terraform 管理から漏れたりしており、IaC の恩恵を十分に享受できているとは言えない状態でした。

そのような状態から脱し、大規模な数の S3 バケットを Terraform 管理するために、バケットを一覧化する書き方へ変えました。 バケットの一覧化により、インフラの運用品質と透明性が増し、大規模インフラ管理においてもより高い信頼性を確保できるようになりました。

もし同様の課題に取り組んでいる方がいましたら、この記事が何かしらの参考になれば幸いです。

IT 基盤部では、さまざまなサービスのインフラを効率よく管理するための改善を日々続けています。 こちらでチームの紹介をしていますので、ぜひご覧ください。

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。