使用terraform从AWS S3上传多个文件

27

我想从本地设备的特定文件夹上传多个文件到AWS S3。但是我遇到了以下错误。

enter image description here

这是我的terraform代码。

resource "aws_s3_bucket" "testbucket" {
    bucket = "test-terraform-pawan-1"
    acl = "private"

    tags = {
        Name  = "test-terraform"
        Environment = "test"
    }
}

resource "aws_s3_bucket_object" "uploadfile" {
  bucket = "test-terraform-pawan-1"
  key     = "index.html"
  source = "/home/pawan/Documents/Projects/"

}

我该如何解决这个问题?

7个回答

60

从Terraform 0.12.8开始,您可以使用fileset函数获取给定路径和模式的文件列表。结合for_each,您应该能够将每个文件作为自己的aws_s3_bucket_object上传:

resource "aws_s3_bucket_object" "dist" {
  for_each = fileset("/home/pawan/Documents/Projects/", "*")

  bucket = "test-terraform-pawan-1"
  key    = each.value
  source = "/home/pawan/Documents/Projects/${each.value}"
  # etag makes the file update when it changes; see https://dev59.com/yFMI5IYBdhLWcg3wfbfN
  etag   = filemd5("/home/pawan/Documents/Projects/${each.value}")
}

请查看GitHub上terraform-providers/terraform-provider-aws: aws_s3_bucket_object: 支持目录上传#3020

注意:这不会设置像content_type这样的元数据,据我所知,Terraform没有内置的方法来推断文件的内容类型。这些元数据对于诸如从浏览器访问HTTP等事情的正确工作非常重要。如果这对您很重要,则应考虑手动指定每个文件而不是尝试自动获取文件夹中的所有内容。


1
您可以通过模块指定 content_type:https://registry.terraform.io/modules/hashicorp/dir/template/latest - Flair
@Flair,这是一个非常好的链接!它有一个专门针对S3的部分,直接回答了这个问题。我想保留我的答案,因为并不是每个人都需要content_type。你能否引用S3示例代码和链接作为单独的答案,以便更加可见? - meustrus
完成了!我甚至为 .tf.json 提供了一个更小的片段。 - Flair
请注意,在加密情况下,source_hashetag 更好。 - sdgfsdh
截至2023年,Terraform正在鼓励用户切换到aws_s3_object。"aws_s3_bucket_object资源已被弃用,并将在未来的版本中移除!"我们可以更新这个答案吗? - undefined

23
你正在尝试上传一个文件夹,而Terraform在源字段中期望的是单个文件。目前不支持将文件夹上传到S3存储桶中。
但是,你可以使用null_resource provisioner调用awscli命令,如此处建议的那样。
resource "null_resource" "remove_and_upload_to_s3" {
  provisioner "local-exec" {
    command = "aws s3 sync ${path.module}/s3Contents s3://${aws_s3_bucket.site.id}"
  }
}

3
这不会使用与运行aws_s3_bucket_object示例相同的安全凭据。 - Christian
1
它只能在第一次运行"terraform apply"时起作用。如果你已经这样做了,现在又添加/修改/删除了源文件,那么你的文件将不会再次同步到S3存储桶中。这是因为"null_resource"已经存在。所以这绝对不是解决这个问题的正确方法。 - cluxter
1
它只能在第一次运行"terraform apply"时起作用。如果你已经这样做了,现在又添加/修改/删除了源文件,你的文件将不会再次同步到S3存储桶中。这是因为"null_resource"已经存在。所以这绝对不是解决这个问题的正确方法。 - undefined

17
自2020年6月9日起,terraform内置了一种方法来推断文件的内容类型(以及其他一些属性),这对于上传到S3存储桶时可能需要的情况非常有用。
HCL格式:
module "template_files" {
  source = "hashicorp/dir/template"

  base_dir = "${path.module}/src"
  template_vars = {
    # Pass in any values that you wish to use in your templates.
    vpc_id = "vpc-abc123"
  }
}

resource "aws_s3_bucket_object" "static_files" {
  for_each = module.template_files.files

  bucket       = "example"
  key          = each.key
  content_type = each.value.content_type

  # The template_files module guarantees that only one of these two attributes
  # will be set for each file, depending on whether it is an in-memory template
  # rendering result or a static file on disk.
  source  = each.value.source_path
  content = each.value.content

  # Unless the bucket has encryption enabled, the ETag of each object is an
  # MD5 hash of that object.
  etag = each.value.digests.md5
}

JSON格式:

{
"resource": {
  "aws_s3_bucket_object": {
    "static_files": {
      "for_each": "${module.template_files.files}"
      #...
      }}}}
#...
}

来源:https://registry.terraform.io/modules/hashicorp/dir/template/latest


这应该是被接受的答案。得票最高的答案不能正确解析内容文件类型,这将在尝试从S3提供HTML文件时引发问题。 - ThangLeQuoc

2
我的目标是使其具有动态性,因此每当我在目录中创建一个文件夹时,terraform会自动将该新文件夹及其内容上传到具有相同键结构的S3存储桶中。
以下是我的做法。
首先,您需要获取一个本地变量,其中包含每个文件夹及其下的文件列表。然后,我们可以循环遍历该列表,将源上传到S3存储桶中。
例如:我有一个名为“Directories”的文件夹,其中包含2个子文件夹“Folder1”和“Folder2”,每个文件夹都有自己的文件。
- Directories
  - Folder1
    * test_file_1.txt
    * test_file_2.txt
  - Folder2
    * test_file_3.txt

步骤1:获取本地变量。
locals{
  folder_files = flatten([for d in flatten(fileset("${path.module}/Directories/*", "*")) : trim( d, "../") ])
}

输出结果如下:
folder_files = [
  "Folder1/test_file_1.txt",
  "Folder1/test_file_2.txt",
  "Folder2/test_file_3.txt",
]

步骤2:动态上传S3对象。
resource "aws_s3_object" "this" {
  for_each = { for idx, file in local.folder_files : idx => file }

  bucket       = aws_s3_bucket.this.bucket
  key          = "/Directories/${each.value}"
  source       = "${path.module}/Directories/${each.value}"
  etag = "${path.module}/Directories/${each.value}"
}

这段代码循环遍历本地变量。
因此,在您的S3存储桶中,您将以相同的结构上传本地目录及其子目录和文件。
Directory
  - Folder1
    - test_file_1.txt
    - test_file_2.txt
  - Folder2
    - test_file_3.txt

0
适用于 Terraform v1.6 < 的工作,无需使用 dir 模板。
resource "aws_s3_bucket_object" "files" {
  bucket = aws_s3_bucket.buc.bucket

  for_each = fileset("C:\\Users\\terraform-aws\\", "**")
  key=each.key
  
  source = "C:\\Users\\terraform-aws\\${each.value}"
  content_type = each.value
  etag = filemd5("C:\\Users\\terraform-aws\\${each.value}")

}

通过使用content_type,您可以上传静态网站的HTML文件。

0
只是为了扩展之前的回答,我尝试使用aws_s3_object,但发现对于我的用例来说并不是一个好的方法:构建前端文件并将其上传到位于cloudfront后面的S3。为了完成这个任务,我需要完成以下三件事情:
  1. 使用更新的.env文件构建前端文件,该文件将包含api的端点 -- (1)我在这里没有使用自定义域,所以我使用的是弹性负载均衡器生成的DNS名称,而在ELB创建之前我是不知道它的。
  2. 将所有构建文件上传到S3,确保正确的content-type,否则,(2)它将自动上传为八进制流或类似的内容,这将导致当您访问网站时,您将下载HTML文件而不是浏览器渲染它。
  3. 使cloudfront缓存失效。
aws_s3_object在这种情况下存在以下问题:
  1. aws_s3_object没有指定正确的内容类型,导致了我上面提到的副作用,标记为2。要解决这个限制,你需要使用类似于terraform-template-dir的东西。
  2. 使用terraform-template-dir需要在运行terraform apply之前确保文件已经存在!因为如果它们不存在,并且在terraform apply期间生成,terraform-template-dir将不会捕获任何内容,这意味着你必须在运行terraform apply之前运行yarn build,但是如果你的yarn build依赖于在terraform中生成的.env文件,你将不得不再次运行build。
  3. 无论如何,你仍然需要使cloudfront缓存失效,有现有的模块可以用于此。

我认为最好的解决方案是这样的:

resource "null_resource" "frontend_files" {
  triggers = {
    always_run = timestamp()
  }

  // if we have a local .env file, we want to make sure we keep it before creating a new one with different contents
  provisioner "local-exec" {
    command = <<EOF
      [[ ! -f ../apps/web/.env ]] || mv ../apps/web/.env ../apps/web/.env.backup
      touch ../apps/web/.env
      echo "VITE_TRPC_ENDPOINT=http://${aws_lb.ecs_lb.dns_name}/trpc" >> ../apps/web/.env
      yarn --cwd ../apps/web build
      aws s3 sync ../apps/web/dist s3://${aws_s3_bucket.frontend_s3_bucket.id}
      aws cloudfront create-invalidation --distribution-id ${aws_cloudfront_distribution.frontend_cf.id} --paths "/*"
      [[ ! -f ../apps/web/.env.backup ]] || mv ../apps/web/.env.backup ../apps/web/.env
    EOF
  }
}

另一个有意义的解决方案是将此构建过程从Terraform中分离出来,放到类似于Bash脚本的东西中,在Terraform构建完成后运行。不过,缺点是aws cloudfront create-invalidation需要你的CloudFront ID,而你在运行terraform apply之前是不知道这个ID的。为了解决这个问题,你可以输出一个包含CloudFront ID的文件,并在Bash脚本中读取该文件的内容。

0
这对我来说很有效,只上传修改过的文件。
resource "aws_s3_object" "files_upload" {
  for_each = fileset("${path.root}/dir_upload", "**/*.*")
  bucket      = "bucket-name"
  key         = "key-prefix/${each.value}"
  source      = "${path.root}/dir_upload/${each.value}"
  source_hash = filemd5("${path.root}/dir_upload/${each.value}")
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接