TerraformでS3オブジェクトを作成する方法いろいろ

2024.05.01

TerraformでS3オブジェクトを作成する方法がいくつかあったので まとめてみます。

以下3通りで aws_s3_object を作成してみました。

  • 方法1: source を指定する
  • 方法2: content を指定する
  • 方法3: content_base64 を使う

方法1: source を指定する

source 引数を使う方法です。ローカルにあるファイルのパスを指定します。

以下サンプルになります。 ローカルにある "src/clanyan.json" を S3オブジェクトとしてアップロードしています。

resource "aws_s3_object" "clanyan_source" {
  # アップロード元(ローカル)
  source = "src/clanyan.json"

  # アップロード先(S3)
  bucket = "DOC-EXAMPLE-BUCKET"
  key    = "clanyan-from-source.json"

  # エンティティタグ (ファイル更新のトリガーに必要)
  etag   = filemd5("src/clanyan.json")
}

ファイル更新を検知できるように etag もしくは source_hash 引数を指定しましょう。 (使い分けについては後述のTipsで説明してます)。

方法2: content を指定する

content 引数を使う方法です。直接文字列を打ち込みます。

以下サンプルになります。jsonencode を使ってテキスト(JSON)をアップロードします。 (heredoc あたりを使って直列文字列を指定しても良いでしょう)。

resource "aws_s3_object" "clanyan_content" {
  bucket = "DOC-EXAMPLE-BUCKET"
  key    = "clanyan-from-content.json"

  content = jsonencode(
    {
      "name" : "くらにゃん",
      "type" : "三毛猫",
      "features" : [
        "フレンドリー",
        "優秀なアシスタント",
      ],
      "hobbies" : [
        "プログラミング",
        "翻訳",
      ],
      "favorite_food" : "さかな",
    }
  )
}

content の中身を変更すれば、それが更新トリガーになります。 以下に terraform plan のサンプルを記載します。

Terraform will perform the following actions:

  # aws_s3_object.clanyan_content will be updated in-place
  ~ resource "aws_s3_object" "clanyan_content" {
      ~ content                = jsonencode(
          ~ {
              ~ hobbies       = [
                    "プログラミング",
                  - "翻訳",
                  + "またたび",
                ]
                name          = "くらにゃん"
                # (3 unchanged attributes hidden)
            }
        )
        id                     = "clanyan-from-content.json"
        tags                   = {}
      + version_id             = (known after apply)
        # (10 unchanged attributes hidden)
    }
...

ただし、terraform 外の変更を検知したい場合は、 やはり etag が必要です。 以下のようにすると良いでしょう。

locals {
  clanyan = {
    "name" : "くらにゃん",
    "type" : "三毛猫",
    "features" : [
      "フレンドリー",
      "優秀なアシスタント",
    ],
    "hobbies" : [
      "プログラミング",
      "翻訳",
    ],
    "favorite_food" : "さかな",
  }
}

resource "aws_s3_object" "clanyan_content" {
  bucket = "DOC-EXAMPLE-BUCKET"
  key    = "clanyan-from-content.json"

  content = jsonencode(local.clanyan)
  etag    = md5(jsonencode(local.clanyan))
}

方法3: content_base64 を使う

content_base64 引数を使う方法です。 Base64エンコードされた文字列を指定します。

以下サンプルになります。 base64encode を使っています。

resource "aws_s3_object" "clanyan_content_base64" {
  bucket = "DOC-EXAMPLE-BUCKET"
  key    = "clanyan-from-content-base64.json"

  content_base64 = base64encode(
    jsonencode(
      {
        "name" : "くらにゃん",
        "type" : "三毛猫",
        "features" : [
          "フレンドリー",
          "優秀なアシスタント"
        ],
        "hobbies" : [
          "プログラミング",
          "翻訳"
        ],
        "favorite_food" : "さかな"
      }
    )
  )
}

Tips

使い分け

基本的には source で良いのかなと思います。

ただし小規模の情報量で、 わざわざファイルを分ける必要性が無い場合は content が取り回しが良さそうです。 Terraform 内のリソースや変数を参照できるのもメリットです。

etag と source_hash の使い分け

etag source_hash は、両方とも更新をトリガーするのに役立つ引数です。

違いは「AWS側で保持される値かどうか」です。 source_hash は AWS側では保持されないです。

Terraformステート AWS側(Etag)
etag 保持される 保持される
source_hash 保持される ★保持されない

source_hash は オブジェクトのEtag が「MD5ダイジェストでない」ときに役に立ちます。 具体的には以下のパターンのときです。

  • オブジェクトが SSE-C もしくは SSE-KMS によって暗号化されている
  • オブジェクトが Multipart Upload で作成される
  • オブジェクトが 16MB より大きい ( ※ Multipart Upload になるため )

例えば 16MB 以上のファイルを etag 引数を指定してアップロードしたとします(以下コード)。

resource "aws_s3_object" "very_large_clanyan_pkg" {
  bucket = "DOC-EXAMPLE-BUCKET"
  key    = "very-large-clanyan.pkg"

  source      = "src/very-large-clanyan.pkg"
  etag        = filemd5("src/very-large-clanyan.pkg")
}

一応アップロードは成功します。 しかし、その後 何も更新がない状態でも、 terraform plan(apply) でずっと差分が出てきます。

Terraform will perform the following actions:

  # aws_s3_object.very_large_clanyan_pkg will be updated in-place
  ~ resource "aws_s3_object" "very_large_clanyan_pkg" {
      ~ etag                   = "16febd313367913f7e719a49664c7c1c" -> "408a517f9e35e9e927d9af97ae1aee00"
        id                     = "very-large-clanyan.pkg"
        tags                   = {}
      + version_id             = (known after apply)
        # (10 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

これは「Terraformステート側の etag は MD5ダイジェストの値である」が、 「AWS側の etag は MD5ダイジェストの値ではない」ために差分と判定されていることが原因です。

そこで source_hash の出番です。以下のように書きましょう。 source_hash はTerraformステート側にのみ保存される値なので、ローカルでの更新をちゃんと検知できるようになります。

resource "aws_s3_object" "very_large_clanyan_pkg" {
  bucket = "DOC-EXAMPLE-BUCKET"
  key    = "very-large-clanyan.pkg"

  source      = "src/very-large-clanyan.pkg"
  source_hash = filemd5("src/very-large-clanyan.pkg")
}

aws_s3_bucket_object は非推奨

似たようなリソースとして aws_s3_bucket_object がありますが、こちらは廃止予定なので使わないようにしましょう。

NOTE:

The aws_s3_bucket_object resource is DEPRECATED and will be removed in a future version! Use aws_s3_object instead, where new features and fixes will be added. When replacing aws_s3_bucket_object with aws_s3_object in your configuration, on the next apply, Terraform will recreate the object. If you prefer to not have Terraform recreate the object, import the object using aws_s3_object.

Resource: aws_s3_bucket_object | Terraform Registry

おわりに

以上、 aws_s3_object を色んな方法で作成してみました。 参考になれば幸いです。

参考