使用Terraform将文件复制/上传到AWS EC2实例

28

我们有一个 cronjob 和 shell 脚本,我们希望在使用 terraform 创建实例时将它们复制或上传到 AWS EC2 实例。

我们尝试了:

  1. 文件提供程序:但是它不起作用,而且据说这个选项并不适用于所有的 terraform 版本。
      provisioner "file" {
        source      = "abc.sh"
        destination = "/home/ec2-user/basic2.sh"
      }
  1. 尝试使用数据模板文件选项
    data "template_file" "userdata_line" {
      template = <<EOF
    #!/bin/bash
    mkdir /home/ec2-user/files2
    cd /home/ec2-user/files2
    sudo touch basic2.sh
    sudo chmod 777 basic2.sh
    base64 basic.sh |base64 -d >basic2.sh
    EOF
    }

尝试了所有选项,但都没有起作用。
请问您能否帮忙或提供建议。
由于我是 terraform 的新手,所以已经苦苦挣扎很长时间。

6个回答

47
当从已安装cloud-init的AMI(这在许多官方Linux发行版中很常见)开始时,我们可以使用cloud-init的write_files模块将任意文件放置到文件系统中,只要它们足够小以适应user_data参数的限制以及所有其他cloud-init数据。
与所有cloud-init模块一样,我们使用基于YAML的cloud-init配置格式配置write_files,它以特殊的标记字符串#cloud-config开头,后跟一个YAML数据结构。因为JSON是YAML的子集,所以我们可以使用Terraform的jsonencode生成有效值[1]
locals {
  cloud_config_config = <<-END
    #cloud-config
    ${jsonencode({
      write_files = [
        {
          path        = "/etc/example.txt"
          permissions = "0644"
          owner       = "root:root"
          encoding    = "b64"
          content     = filebase64("${path.module}/example.txt")
        },
      ]
    })}
  END
}

当我们设置 encoding = "b64" 时,write_files 模块可以接受以 base64 格式表示的数据,因此我们可以与 Terraform 的 filebase64 函数一起使用,包含外部文件的内容。这里还有其他可能的方法,例如使用 Terraform 模板动态生成字符串,并使用 base64encode 将其编码为文件内容。

如果您可以在一个配置文件中表达您希望 cloud-init 做的一切,那么您可以将 local.cloud_config_config 直接分配为您的实例 user_data,并且 cloud-config 将会在系统启动时识别和处理它:

  user_data = local.cloud_config_config

如果您需要将文件创建与其他操作(如运行shell脚本)结合起来,可以使用cloud-init的multipart archive格式对多个“文件”进行编码以供cloud-init处理。Terraform有一个cloudinit提供程序,其中包含一个数据源,可轻松构建用于cloud-init的多部分存档。
data "cloudinit_config" "example" {
  gzip          = false
  base64_encode = false

  part {
    content_type = "text/cloud-config"
    filename     = "cloud-config.yaml"
    content      = local.cloud_config_config
  }

  part {
    content_type = "text/x-shellscript"
    filename     = "example.sh"
    content  = <<-EOF
      #!/bin/bash
      echo "Hello World"
    EOT
  }
}

此数据源将在cloudinit_config.example.rendered处生成单个字符串,该字符串是适用于作为cloud-inituser_data使用的多部分归档文件:

  user_data = data.cloudinit_config.example.rendered

EC2对用户数据的最大大小限制为64千字节,因此所有编码数据的总和都必须在该限制范围内。如果您需要放置一个接近或超过该限制的大文件,则最好使用其他中间系统来传输该文件,例如让Terraform将文件写入Amazon S3存储桶,并使实例中的软件使用实例配置文件凭据检索该数据。不过,对于用于系统配置的小型数据文件,则不需要这样做。
需要注意的是,从Terraform和EC2的角度来看,user_data的内容只是任意字符串。处理字符串中的任何问题必须在目标操作系统本身中进行调试,方法是读取cloud-init日志以查看它如何解释配置以及尝试执行这些操作时发生了什么。

[1]: 我们也可以潜在地使用yamlencode,但是在我写这篇文章的时候,该函数有一个警告,即其确切格式可能会在未来的Terraform版本中发生变化,而这对于user_data来说是不可取的,因为它会导致实例被替换。如果您正在未来阅读此内容,并且在yamldecode文档中不存在该警告,请考虑改用yamlencode


可以使用 Elastic Beanstalk 实例来完成这个操作吗?例如,在那里创建一个 .env 文件以避免 4K 环境限制。 - Vitim.us
很抱歉,我对Elastic Beanstalk并不是很熟悉。它似乎与其他EC2相关的部署选项有很大的不同,因此我建议您在Stack Exchange上提出一个新问题,这样更容易被熟悉Elastic Beanstalk的人看到。 - Martin Atkins
嗨,马丁,我认为“user_data = cloudinit_config.example.rendered”应该是“user_data = data.cloudinit_config.example.rendered”。或者有我不知道的不同用法吗?也许与terraform版本有关? - Ferhat
是的,你说得对Ferhat。我使用了托管资源的语法,但这是一个数据资源,所以需要data.前缀。我会编辑答案。谢谢! - Martin Atkins
@MartinAtkins 这些将按什么顺序运行?有没有一种指定顺序的方法?我正在使用您的示例传递文件并运行用户数据脚本,对于我的构建来说,它们运行的顺序很重要。 - Legion_of_boom__
很抱歉,我并不能立刻回忆起 cloud-init 在 MIME 部分中如何优先处理不同类型的数据。我记得 cloud-init 会优先处理一些操作而非其他操作,但确切的顺序是可配置的。名字以“scripts-”开头的各个模块将解析 shell 脚本,似乎默认情况下,它们都在 write-files 模块之后运行很长时间。 - Martin Atkins

20

我只是为此使用了provisioner "file",没有问题...
但是你必须提供连接:

resource "aws_instance" "foo" {
...
  provisioner "file" {
    source      = "~/foobar"
    destination = "~/foobar"

    connection {
      type        = "ssh"
      user        = "ubuntu"
      private_key = "${file("~/Downloads/AWS_keys/test.pem")}"
      host        = "${self.public_dns}"
    }
  }
...
}

以下是一些代码示例:
https://github.com/heldersepu/hs-scripts/blob/master/TerraForm/ec2_ubuntu.tf#L21


我使用这种方法遇到的问题是,在用户数据中无法使用该文件,因为这是在启动后发生的。 - openCivilisation
1
@openCivilisation 是的,任何你需要在操作系统中存在的东西都应该打包到你的 AMI 中,可以考虑使用 Packer 来实现。 - Helder Sepulveda

7

在公司域中,所有的选项都无法正常工作。但最终我们能够通过s3存储桶复制/下载文件。

创建 s3.tf 文件以上传这些文件 basic2.sh。

resource "aws_s3_bucket" "demo-s3" {

  bucket = "acom-demo-s3i-<bucketID>-us-east-1"
  acl    = "private"


  tags {
    Name = "acom-demo-s3i-<bucketID>-us-east-1"
    StackId = "demo-s3"
  }
}

resource "aws_s3_bucket_policy" "s3_policy" {

  bucket = "${aws_s3_bucket.demo-s3.id}"

  policy = <<EOF
{
    "Version": "2009-10-17",
    "Statement": [
            {
            "Sid": "Only allow specific role",
            "Effect": "allow",
            "Principal":{ "AWS": ["arn:aws:iam::<bucketID>:role/demo-s3i"]},
            "Action":  "s3:*",
            "Resource": [
          "arn:aws:s3:::acom-demo-s3i-<bucketID>-us-east-1",
          "arn:aws:s3:::acom-demo-s3i-<bucketID>-us-east-1/*"
            ]

        }
    ]
}
EOF
}


resource "aws_s3_bucket_object" "object" {
  bucket = "acom-demo-s3i-<bucketID>-us-east-1"
  key    = "scripts/basic2.sh"
  source = "scripts/basic2.sh"
  etag = "${filemd5("scripts/basic2.sh")}"
}

然后在另一个tpl文件中宣布文件下载部分。

 aws s3 cp s3://acom-demo-s3i-<bucketID>-us-east-1/scripts/basic2.sh /home/ec2-user/basic2.sh

我的问题是,AWS CLI是否可在实例上使用,而无需进行配置?例如,即使没有将AWS CLI添加到实例中,aws s3 cp命令是否有效? - blamb

6

这里有一个更简单的例子,展示如何按照@martin-atkins所述使用write_filescloud-init

templates/cloud-init.yml.tpl 的内容为:

#cloud-config
package_update: true
package_upgrade: true

packages:
  - ansible

write_files:
  - content: |
      ${base64encode("${ansible_playbook}")}
    encoding: b64
    owner: root:root
    path: /opt/ansible-playbook.yml
    permissions: '0750'

runcmd:
 - ansible-playbook /opt/ansible-playbook.yml

main.tf文件内容:

data "template_file" "instance_startup_script" {
  template = file(format("%s/templates/templates/cloud-init.yml.tpl", path.module))

  vars = {
    ansible_playbook = templatefile("${path.module}/templates/ansible-playbook.yml.tpl", {
      playbookvar = var.play_book_var
    })
    
    cloudinitvar = var.cloud_init_var
  }
}

在 cloud-init 和 ansible-playbook 模板中,都可以使用变量插值。


2

您需要使用文件提供程序以连接详细信息到EC2实例。示例配置如下:

provisioner "file" {
  source      = "${path.module}/files/script.sh"
  destination = "/tmp/script.sh"

  connection {
    type     = "ssh"
    user     = "root"
    password = "${var.root_password}"
    host     = "${var.host}"
  }
}

您可以使用用户名/密码、私钥甚至跳板主机进行连接。了解更多详情,请访问https://www.terraform.io/docs/provisioners/connection.html


1
这对我有用:

这对我有用:

resource "aws_instance" "myapp-server" {
  ami = data.aws_ami.ubuntu.id
  instance_type = xx
  subnet_id =  xx
  vpc_security_group_ids = xx
  availability_zone=xx
  associate_public_ip_address = true  
  key_name = xx  
  user_data = file(xx)

  connection {
    type     = "ssh"
    host     =  self.public_ip
    user     = "ubuntu"
    private_key     = file(xx) 
  }
 
   provisioner "file" {
    source      = "source-file"
    destination = "dest-file"
  }

} 

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