【问题标题】:AWS Cloudformation: How to reuse bash script placed in user-data parameter when creating EC2?AWS Cloudformation:如何在创建 EC2 时重用放置在用户数据参数中的 bash 脚本?
【发布时间】:2017-05-16 22:38:19
【问题描述】:

在 Cloudformation 中,我有两个堆栈(一个嵌套)。

嵌套堆栈“ec2-setup”:

{
  "AWSTemplateFormatVersion" : "2010-09-09",

  "Parameters" : {
    // (...) some parameters here

    "userData" : {
      "Description" : "user data to be passed to instance",
      "Type" : "String",
      "Default": ""
    }

  },

  "Resources" : {

    "EC2Instance" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "UserData" : { "Ref" : "userData" },
        // (...) some other properties here
       }
    }

  },
  // (...)
}

现在在我的主模板中,我想引用上面介绍的嵌套模板并使用 userData 参数传递一个 bash 脚本。此外,我不想内联用户数据脚本的内容,因为我想为少数几个 ec2 实例重用它(所以我不想每次在我的主模板中声明 ec2 实例时都复制脚本)。

我试图通过将脚本的内容设置为参数的默认值来实现这一点:

{
  "AWSTemplateFormatVersion": "2010-09-09",

  "Parameters" : {
    "myUserData": {
      "Type": "String",
      "Default" : { "Fn::Base64" : { "Fn::Join" : ["", [
        "#!/bin/bash \n",
        "yum update -y \n",

        "# Install the files and packages from the metadata\n",
        "echo 'tralala' > /tmp/hahaha"
      ]]}}
    }
  },
(...)

    "myEc2": {
      "Type": "AWS::CloudFormation::Stack",
      "Properties": {
        "TemplateURL": "s3://path/to/ec2-setup.json",
        "TimeoutInMinutes": "10",
        "Parameters": {
          // (...)
          "userData" : { "Ref" : "myUserData" }
        }

但在尝试启动堆栈时出现以下错误:

"模板验证错误:模板格式错误:Every Default 成员必须是字符串。”

该错误似乎是由于声明 { Fn::Base64 (...) } 是一个对象而不是字符串(尽管它导致返回 base64 编码的字符串)这一事实引起的。

一切正常,如果我在调用嵌套模板时将脚本直接粘贴到参数部分(作为内联脚本)(而不是将字符串设置为参数):

"myEc2": {
  "Type": "AWS::CloudFormation::Stack",
  "Properties": {
    "TemplateURL": "s3://path/to/ec2-setup.json",
    "TimeoutInMinutes": "10",
    "Parameters": {
      // (...)
      "userData" : { "Fn::Base64" : { "Fn::Join" : ["", [
        "#!/bin/bash \n",
        "yum update -y \n",

        "# Install the files and packages from the metadata\n",
        "echo 'tralala' > /tmp/hahaha"
        ]]}}
    }

但我想将userData 脚本的内容保留在参数/变量中以便能够重用它。

有没有机会重复使用这样的 bash 脚本而无需每次都复制/粘贴?

【问题讨论】:

标签: linux amazon-web-services amazon-ec2 amazon-cloudformation


【解决方案1】:

以下是一些关于如何在通过 CloudFormation 定义的多个 EC2 实例的用户数据中重用 bash 脚本的选项:

1。设置默认参数为字符串

您最初尝试的解决方案应该可以工作,只需稍作调整:您必须将默认参数声明为字符串,如下所示(使用 YAML 而不是 JSON 可以/更轻松地声明多行字符串内联):

  AWSTemplateFormatVersion: "2010-09-09"
  Parameters:
    myUserData:
      Type: String
      Default: |
        #!/bin/bash
        yum update -y
        # Install the files and packages from the metadata
        echo 'tralala' > /tmp/hahaha
(...)
  Resources:
    myEc2:
      Type: AWS::CloudFormation::Stack
      Properties
        TemplateURL: "s3://path/to/ec2-setup.yml"
        TimeoutInMinutes: 10
        Parameters:
          # (...)
          userData: !Ref myUserData

然后,在您的嵌套堆栈中,应用任何所需的 intrinsic functionsFn::Base64,以及 Fn::Sub,如果您需要在用户数据中应用任何 RefFn::GetAtt 函数,这将非常有用脚本)在 EC2 实例的资源属性中:

  AWSTemplateFormatVersion: "2010-09-09"
  Parameters:
    # (...) some parameters here
    userData:
      Description: user data to be passed to instance
      Type: String
      Default: ""    
  Resources:
    EC2Instance:
      Type: AWS::EC2::Instance
      Properties:
        UserData:
          "Fn::Base64":
            "Fn::Sub": !Ref userData
        # (...) some other properties here
  # (...)

2。将脚本上传到 S3

您可以将单个 Bash 脚本上传到 S3 存储桶,然后通过在模板中的每个 EC2 实例中添加一个最小用户数据脚本来调用该脚本:

  AWSTemplateFormatVersion: "2010-09-09"
  Parameters:
    # (...) some parameters here
    ScriptBucket:
      Description: S3 bucket containing user-data script
      Type: String
    ScriptKey:
      Description: S3 object key containing user-data script
      Type: String
  Resources:
    EC2Instance:
      Type: AWS::EC2::Instance
      Properties:
        UserData:
          "Fn::Base64":
            "Fn::Sub": |
              #!/bin/bash
              aws s3 cp s3://${ScriptBucket}/${ScriptKey} - | bash -s
        # (...) some other properties here
  # (...)

3。使用预处理器从单一来源内联脚本

最后,您可以使用troposphere 或您自己的模板预处理器工具从更紧凑/更具表现力的源文件中“生成”详细的 CloudFormation 可执行模板。这种方法将允许您消除源文件中的重复 - 尽管模板将包含“重复”用户数据脚本,但这只会出现在生成的模板中,因此不会造成问题。

【讨论】:

  • 精彩的答案!解决方案 1 似乎正是我正在寻找的,因为这将允许我将 bash 脚本保存在与主模板相同的文件中(我现在更喜欢)。不过其他的也不错。我今天将测试这种方法。
  • 补充解决方案1的答案。在json中可以将脚本作为默认值,但是所有内容都需要写在一行中,并使用“\n”字符作为分隔符:"Default" : "#!/bin/bash \n yum update -y \n # Install the files and packages from the metadata\n echo 'tralala' > /tmp/hahaha"。但是 YAML multilne 仍然更容易阅读和维护:)
  • 方法 (1) 在验证时给我Template error: One or more Fn::Sub intrinsic functions don't specify expected arguments.
  • 显然你不能使用Ref作为Sub的第一个参数:stackoverflow.com/questions/44547004/…
  • 我找不到逻辑“或”'|'的参考在本节中执行:默认值:|就在 bash 脚本之前。那有什么作用?
【解决方案2】:

您必须在模板之外查看,才能为多个模板提供相同的用户数据。这里的一种常见方法是进一步抽象您的模板,或“模板化模板”。使用相同的方法来创建两个模板,您将保持它们都干燥。

我是 cloudformation 的忠实粉丝,我用它来创建我的大部分资源,尤其是用于生产绑定的用途。但尽管它很强大,但它并不是完全的交钥匙。除了创建模板之外,您还必须调用 coudformation API 来创建堆栈,并提供堆栈名称和参数。因此,围绕使用 cloudformation 的自动化是完整解决方案的必要部分。这种自动化可以是简单的(例如 bash 脚本)或复杂的。我已经开始使用 ansible 的 cloudformation 模块来自动化“围绕”模板,无论是使用 Jinja 为模板创建模板,还是只是为同一个可重用模板提供不同的参数集,或者在创建堆栈之前进行发现;任何必要的辅助操作。出于这个目的,有些人真的很喜欢对流层——如果你是一个 pythonic 思想家,你可能会发现它很合适。一旦您拥有处理堆栈创建的任何类型的自动化,您会发现很容易添加步骤以使模板本身更具动态性,或从可重用组件组装多个堆栈。

在工作中,我们经常使用 cloudformation,并且最近更倾向于使用组合方法,在这种方法中,我们定义了我们使用的模板的共享组件,然后从组件中组合实际的模板。

另一种选择是合并两个堆栈,使用条件来控制在从模板创建的任何特定堆栈中包含已定义资源。这在简单的情况下可以正常工作,但所有这些条件的组合复杂性往往会使从长远来看这是一个困难的解决方案,除非差异真的很简单。

【讨论】:

  • 其实我很担心创建额外的模板级别可能是纯 cloudformation 的唯一方法。不过,感谢您的宝贵意见!您的回答恰好涉及我目前正在查看的内容-我已经创建了类似于您所描述的我认为的“原子”组件模板(ec2 只是其中之一,另一个是 vpc 等)。感谢您指出外部工具,我会看看它们。另一方面,我希望只定义模板......而不是将它们上传到 s3...... 而不是只调用 create stack(通过 webconsole 或 cli)而不使用额外的工具
  • 您也可以将您的 bash 脚本与一个参数一起传递,该参数已经经过 base64 编码。但在这种情况下,您将无法在用户数据中使用任何 Ref 伪参数。这样你就可以保持“纯粹的云形成”。
【解决方案3】:

实际上,我找到了比前面提到的更多的解决方案。这个解决方案一方面有点“hackish”,但另一方面我发现它对“bash script”用例(以及其他参数)非常有用。

这个想法是创建一个额外的堆栈 - “parameters stack” - 它将输出值。由于堆栈的输出不限于字符串(因为它是默认值),我们可以将整个 base64 编码脚本定义为堆栈的单个输出。

缺点是每个栈都需要定义至少一个资源,所以我们的参数栈也需要定义至少一个资源。这个问题的解决方案是要么在另一个已经定义了现有资源的模板中定义参数,要么创建一个永远不会创建的“假资源”,因为条件永远不会被满足。

在这里,我提出了使用假资源的解决方案。首先我们创建新的 paramaters-stack.json 如下:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "Outputs/returns parameter values",


  "Conditions" : {
    "alwaysFalseCondition" : {"Fn::Equals" : ["aaaaaaaaaa", "bbbbbbbbbb"]}
  },

  "Resources": {
    "FakeResource" : {
      "Type" : "AWS::EC2::EIPAssociation",
      "Condition" : "alwaysFalseCondition",
      "Properties" : {
        "AllocationId" :  { "Ref": "AWS::NoValue" },
        "NetworkInterfaceId" : { "Ref": "AWS::NoValue" }
      }
    }
  },

  "Outputs": {
    "ec2InitScript": {
      "Value":
      { "Fn::Base64" : { "Fn::Join" : ["", [
        "#!/bin/bash \n",
        "yum update -y \n",

        "# Install the files and packages from the metadata\n",
        "echo 'tralala' > /tmp/hahaha"
      ]]}}

    }
  }
}

现在在主模板中,我们首先声明我们的参数堆栈,然后我们引用该参数堆栈的输出:

{
  "AWSTemplateFormatVersion": "2010-09-09",

   "Resources": { 

    "myParameters": {
      "Type": "AWS::CloudFormation::Stack",
      "Properties": {
        "TemplateURL": "s3://path/to/paramaters-stack.json",
        "TimeoutInMinutes": "10"
      }
    },

    "myEc2": {
      "Type": "AWS::CloudFormation::Stack",
      "Properties": {
        "TemplateURL": "s3://path/to/ec2-setup.json",
        "TimeoutInMinutes": "10",
        "Parameters": {
          // (...)
          "userData" : {"Fn::GetAtt": [ "myParameters", "Outputs.ec2InitScript" ]}
        }
     }
  }
}

请注意,在一个堆栈文件中最多可以创建 60 个输出,因此可以使用此技术为每个堆栈文件定义 60 个变量/参数。

【讨论】:

    猜你喜欢
    • 2017-11-19
    • 2017-01-15
    • 1970-01-01
    • 1970-01-01
    • 2018-02-22
    • 1970-01-01
    • 2019-06-24
    • 1970-01-01
    • 2019-09-22
    相关资源
    最近更新 更多