AWS Cloudformation:如何在创建 EC2 时重用放置在用户数据参数中的 bash 脚本?
AWS Cloudformation: How to reuse bash script placed in user-data parameter when creating EC2?
在 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" }
}
但是我在尝试启动堆栈时遇到以下错误:
"Template validation error: Template format error: Every Default
member must be a string."
错误似乎是由于声明 { 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
脚本的内容保留在 parameter/variable 中以便能够重用它。
是否有机会重用这样的 bash 脚本而不需要每次都 copy/paste 它?
您必须在 模板之外查看才能向多个模板提供相同的用户数据。这里的一种常见方法是进一步抽象您的模板,或 "template the template"。使用相同的方法创建两个模板,您将让它们保持干燥。
我是 cloudformation 的超级粉丝,我用它来创建我所有的资源,尤其是生产相关的用途。但是,尽管它很强大,但它并不是完全交钥匙的。除了创建模板之外,您还必须调用 coudformation API 来创建堆栈,并提供堆栈名称和参数。因此,围绕使用 cloudformation 的自动化是完整解决方案的必要部分。这种自动化可以是简单的(例如 bash 脚本)或复杂的。我已经开始使用 ansible 的 cloudformation 模块来自动化 "around" 模板,无论是使用 Jinja 为模板创建模板,还是只是为同一个可重用模板提供不同的参数集,或者在堆栈之前进行发现创建;任何必要的辅助操作。出于这个目的,有些人真的很喜欢对流层——如果你是一个 pythonic 思想家,你可能会发现它很合适。一旦您使用任何类型的自动化处理堆栈创建,您会发现很容易添加步骤以使模板本身更加动态,或者 assemble 来自可重用组件的多个堆栈。
在工作中,我们经常使用 cloudformation,并且最近倾向于使用组合方法,在这种方法中,我们定义我们使用的模板的共享组件,然后从组件组合实际模板。
另一种选择是合并两个堆栈,使用条件来控制定义的资源包含在从模板创建的任何特定堆栈中。这在简单的情况下工作正常,但所有这些条件的组合复杂性往往使它在长 运行 中成为一个困难的解决方案,除非差异真的很简单。
这里有一些关于如何在用户数据中为通过 CloudFormation 定义的多个 EC2 实例重用 bash 脚本的选项:
1。将默认参数设置为字符串
您最初尝试的解决方案应该可以工作,稍作调整:您必须将默认参数声明为字符串,如下所示(使用 YAML 而不是 JSON 使其 possible/easier 声明多-line 字符串内联):
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
然后,在您的嵌套堆栈中,在 EC2 实例的资源属性中应用任何必需的 intrinsic functions (Fn::Base64
, as well as Fn::Sub
which is quite helpful if you need to apply any Ref
or Fn::GetAtt
用户数据脚本中的函数):
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 或您自己的模板预处理器工具来 'generate' 来自更多 compact/expressive 源文件的详细 CloudFormation 可执行模板。这种方法将允许您消除源文件中的重复 - 虽然模板将包含 'duplicate' 用户数据脚本,但这只会出现在生成的模板中,因此应该不会造成问题。
实际上我找到了比上面提到的更多的解决方案。这个解决方案一方面有点 "hackish",但另一方面我发现它对 "bash script" 用例(以及其他参数)非常有用。
想法是创建一个额外的堆栈 - “parameters stack” - 它将输出值。由于堆栈的输出不限于字符串(因为它是默认值),我们可以将整个 base64 编码脚本定义为堆栈的单个输出。
缺点是每个栈都需要定义至少一种资源,所以我们的参数栈也需要定义至少一种资源。这个问题的解决方案是要么在另一个已经定义了现有资源的模板中定义参数,要么创建一个 "fake resource",它永远不会被创建,因为条件永远不会被满足。
这里我给出了假资源的解决方案。首先我们创建我们的新参数-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 variables/paramaters。
在 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" }
}
但是我在尝试启动堆栈时遇到以下错误:
"Template validation error: Template format error: Every Default member must be a string."
错误似乎是由于声明 { 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
脚本的内容保留在 parameter/variable 中以便能够重用它。
是否有机会重用这样的 bash 脚本而不需要每次都 copy/paste 它?
您必须在 模板之外查看才能向多个模板提供相同的用户数据。这里的一种常见方法是进一步抽象您的模板,或 "template the template"。使用相同的方法创建两个模板,您将让它们保持干燥。
我是 cloudformation 的超级粉丝,我用它来创建我所有的资源,尤其是生产相关的用途。但是,尽管它很强大,但它并不是完全交钥匙的。除了创建模板之外,您还必须调用 coudformation API 来创建堆栈,并提供堆栈名称和参数。因此,围绕使用 cloudformation 的自动化是完整解决方案的必要部分。这种自动化可以是简单的(例如 bash 脚本)或复杂的。我已经开始使用 ansible 的 cloudformation 模块来自动化 "around" 模板,无论是使用 Jinja 为模板创建模板,还是只是为同一个可重用模板提供不同的参数集,或者在堆栈之前进行发现创建;任何必要的辅助操作。出于这个目的,有些人真的很喜欢对流层——如果你是一个 pythonic 思想家,你可能会发现它很合适。一旦您使用任何类型的自动化处理堆栈创建,您会发现很容易添加步骤以使模板本身更加动态,或者 assemble 来自可重用组件的多个堆栈。
在工作中,我们经常使用 cloudformation,并且最近倾向于使用组合方法,在这种方法中,我们定义我们使用的模板的共享组件,然后从组件组合实际模板。
另一种选择是合并两个堆栈,使用条件来控制定义的资源包含在从模板创建的任何特定堆栈中。这在简单的情况下工作正常,但所有这些条件的组合复杂性往往使它在长 运行 中成为一个困难的解决方案,除非差异真的很简单。
这里有一些关于如何在用户数据中为通过 CloudFormation 定义的多个 EC2 实例重用 bash 脚本的选项:
1。将默认参数设置为字符串
您最初尝试的解决方案应该可以工作,稍作调整:您必须将默认参数声明为字符串,如下所示(使用 YAML 而不是 JSON 使其 possible/easier 声明多-line 字符串内联):
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
然后,在您的嵌套堆栈中,在 EC2 实例的资源属性中应用任何必需的 intrinsic functions (Fn::Base64
, as well as Fn::Sub
which is quite helpful if you need to apply any Ref
or Fn::GetAtt
用户数据脚本中的函数):
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 或您自己的模板预处理器工具来 'generate' 来自更多 compact/expressive 源文件的详细 CloudFormation 可执行模板。这种方法将允许您消除源文件中的重复 - 虽然模板将包含 'duplicate' 用户数据脚本,但这只会出现在生成的模板中,因此应该不会造成问题。
实际上我找到了比上面提到的更多的解决方案。这个解决方案一方面有点 "hackish",但另一方面我发现它对 "bash script" 用例(以及其他参数)非常有用。
想法是创建一个额外的堆栈 - “parameters stack” - 它将输出值。由于堆栈的输出不限于字符串(因为它是默认值),我们可以将整个 base64 编码脚本定义为堆栈的单个输出。
缺点是每个栈都需要定义至少一种资源,所以我们的参数栈也需要定义至少一种资源。这个问题的解决方案是要么在另一个已经定义了现有资源的模板中定义参数,要么创建一个 "fake resource",它永远不会被创建,因为条件永远不会被满足。
这里我给出了假资源的解决方案。首先我们创建我们的新参数-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 variables/paramaters。