向EC2实例添加一个大小未知的安全组列表

16
我们有一个 CloudFormation 模板,它创建了一个 EC2 实例和一个安全组(以及许多其他资源),但我们需要能够将一些附加的现有安全组添加到同一 EC2 实例中。
我们面临的问题是预先存在的安全组数量不总是相同的,而我们希望有一个单一的模板来处理所有情况。
目前,我们有一个输入参数,看起来像这样:
"WebTierSgAdditional": {
  "Type": "String",
  "Default": "",
  "Description": ""
}

我们将一个逗号分隔的字符串传入该参数,其中包含预先存在的安全组,例如'sg-abc123,sg-abc456'

EC2实例的SecurityGroup标签看起来像这样:

"SecurityGroups": [
  {
    "Ref": "WebSg"
  },
  {
    "Ref": "WebTierSgAdditional"
  }
]

使用此代码时,当实例创建时,我们会在AWS控制台中收到以下错误:

必须同时使用组ID或组名称才能使用所有安全组,而不是同时使用两者

上面的“WebSg”引用是模板中其他地方正在创建的安全组之一。如果我们通过输入参数传递组名称列表而不是组ID列表,则会出现相同的错误。

当我们将输入参数的“Type”字段更改为“CommaDelimitedList”时,会收到一个错误,其中包含以下信息:

属性SecurityGroups的值必须是字符串列表类型

显然无法将一个列表与一个字符串连接起来以创建一个新列表。

当参数仅包含单个sg id时,一切都可以成功创建,但是我们需要具有添加多个sg id的功能。

我们尝试了许多不同的方法,在SecurityGroups标记内使用Fn::Join,但似乎没有任何作用。我们真正需要的是一种“Explode”函数,以从参数字符串中提取单个ID。

有人知道一种好的使这个工作的方法吗?


原因是,在EC2-classic中,您需要提供SG名称,而在VPC中的实例中,您需要提供SG ID。因此,错误“必须同时使用组ID或组名称来使用所有安全组,不能同时使用两者”很明显。换句话说,您可以将实例运行在EC2-classic或VPC中...但不能同时运行。您需要设计另一种方法,只传递SG名称或仅传递SG ID给实例。 - slayedbylucifer
我们的安全组是在VPC内创建的,因此'Ref'函数返回的是SG ID,而不是SG名称。问题似乎在于Cloudformation将我们的ID字符串参数('sg-abc123,sg-abc456')解释为SG名称,我们需要将其转换为SG ID列表。 - Jono D
6个回答

11
AWS在2017年1月引入了Fn::Split,现在这是可能的。虽然不太美观,但你基本上是使用Fn::Join将两个列表转换为字符串,然后使用Fn::Split将字符串再次转换为列表。请注意保留{和}符号。
Parameters:

    WebTierSgAdditional:
        Type: CommaDelimitedList
        Default: ''

Conditions:

    HasWebTierSgAdditional: !Not [ !Equals [ '', !Select [ 0, !Ref WebTierSgAdditional ] ] ]

Resources:

    WebSg:
        Type: AWS::EC2::SecurityGroup
        Properties:
            # ...

    Ec2Instance:
        Type: AWS::EC2::Instance
        Properties:
            # ...
            SecurityGroupIds: !Split
                  - ','
                  - !Join
                      - ','
                      -   - !Ref WebSg
                          - !If [ HasWebTierSgAdditional, !Join [ ',', !Ref WebTierSgAdditional ], !Ref 'AWS::NoValue' ]

WebTierSgAdditional 开始时是一个列表,但通过 !Join [ ',', !Ref WebTierSgAdditional ] 转换为用逗号分隔的字符串。这个字符串被包含在另一个列表中,其中包括 !Ref WebSg,它也被转换为用逗号分隔的字符串。!Split 将字符串拆分成由逗号分隔的项目组成的列表。


10

正如你所发现的那样,问题在于你需要将字符串列表作为安全组发送,尽管 CloudFormation 提供了一种方法将字符串列表合并为单个定界字符串,但它没有提供一种将定界字符串拆分为字符串列表的简单方法。

不幸的是,我知道的唯一方法是使用嵌套堆栈。 您可以利用参数类型“CommaDelimitedList”将逗号分隔的字符串拆分为字符串列表。

基本方法如下:

  1. 在 CloudFormation 模板中创建您的安全组。
  2. 使用 Fn::Join 将该安全组 ID 与您的安全组列表合并。
  3. 将该列表传递给嵌套堆栈(AWS::CloudFormation::Stack 类型的资源)。
  4. 在单独的模板中将该参数作为“CommaDelimitedList”类型进行处理。
  5. 将该参数 ref 传递到 EC2 实例声明中。

我已经使用以下两个模板进行了测试,它们是可行的:

主模板:

{
    "AWSTemplateFormatVersion": "2010-09-09",                                                                                                  
    "Parameters" : {
        "SecurityGroups" : {                                                                        
            "Description" : "A comma separated list of security groups to merge with the web security group",
            "Type" : "String"
        }
    },
    "Resources" : {
        "WebSg" : ... your web security group here,
        "Ec2Instance" : {
            "Type" : "AWS::CloudFormation::Stack",
            "Properties" : {
                "TemplateURL" : "s3 link to the other stack template",
                "Parameters" : {
                    "SecurityGroups" : { "Fn::Join" : [ ",", [ { "Ref" : "WebSg" }, { "Ref" : "SecurityGroups" } ] ] },
                }
            }
        }
    }
}

嵌套模板(通过上面的“TemplateURL”链接):

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Parameters" : {
        "SecurityGroups" : {
            "Description" : "The Security Groups to launch the instance with",
            "Type" : "CommaDelimitedList"
        },
    }
    "Resources" : {
        "Ec2Instance" : {
            "Type" : "AWS::EC2::Instance",
            "Properties" : {                                     
                ... etc           
                "SecurityGroupIds" : { "Ref" : "SecurityGroups" }
            }
        }
    }
}

我很想知道是否有更好的方法来做这件事。正如你所提到的,它确实需要一个“explode”函数。


1
叹气,2015年还没有更好的方法来做这件事吗? - bdrx
1
我讨厌自己像这样实现它,但感谢您的答案。这种解决方法从未发生在我脑海中。 - Chris B
1
请看我的答案,了解如何在不使用子堆栈的情况下完成此操作。 - Eric Woodruff

3

我得到了支持团队提供的另一种解决方案,我认为更好。

基本上,只需通过索引从列表中获取每个安全组,然后在末尾添加内部安全组即可。

"SecurityGroupList": {
    "Description": "List of existing security groups",
    "Type": "CommaDelimitedList"
},
...
"InternalSecurityGroup": {
    "Type": "AWS::EC2::SecurityGroup"
},
...
"SecurityGroupIds": [
    {
        "Fn::Select": [
            "0",
            {
                "Ref": "SecurityGroupList"
            }
        ]
    },
    {
        "Fn::Select": [
            "1",
            {
                "Ref": "SecurityGroupList"
            }
        ]
    },
    {
        "Fn::Select": [
            "2",
            {
                "Ref": "SecurityGroupList"
            }
        ]
    },
    {
        "Ref": "InternalSecurityGroup"
    }
],

希望这能对其他人有所帮助。

11
这其实不是一个坏的解决方案,但是如何处理列表中可变数量的SecurityGroups? - M. Glatki

1
一种更简单的两行解决方案如下:
假设您有一个常见的或参数中的AppSecurityGroups,则可以提供以下内容:
"AppSecurityGroups" : "sg-xxx,sg-yyyy,sg-zzz"

在您的CFT中,作为参数将第二个安全组(服务特定)ServiceSecurityGroup的值设置为sg-aaa

编写一个条件来检查服务安全组是否为None。

"Conditions": {
  "IsEmptySSG": {
     "Fn::Equals": [
       {"Ref": "ServiceSecurityGroup"}, 
       "None"
     ]
  }
}

然后根据条件合并安全组,条件是ServiceSecurityGroup不为空。

"SecurityGroups" : {
  "Fn::If" : [
    "IsEmptySSG",
    {"Fn::Split" : [ "," , {"Ref" : "AppSecurityGroups"} ]},
    {"Fn::Split" : [ "," , 
        { "Fn::Join" : [ ",", [{"Ref" : "AppSecurityGroups"}, { "Ref" : "ServiceSecurityGroup" }]]}
      ]
    }
  ]
}

这将动态地将两个安全组CSV列表追加到一个列表中,以供您的AWS资源使用。
我已经测试并使用它解决了类似的问题,效果非常好。

我对这个解决方案唯一的问题是您需要使用字符串参数类型来添加安全组,并以逗号分隔的形式放置安全组IDs,而不是使用ListAWS::EC2::SecurityGroup::Id,这样会更清洁。 - CarlR

1

在@alanthing的方法指导下,我用JSON格式提出了这个解决方案。只需确保参数不为空,因为在我的情况下我没有检查。还要注意模板是不完整的,只显示相关部分。

如果有用的话,就在这里发布它。

  "Parameters": {
    "SecurityGroups": {
      "Type": "List<AWS::EC2::SecurityGroup::Id>",
      "Description": "List of security groups"
    }
  },
  "Resources": {
    "EC2Instance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "SecurityGroupIds": {
          "Fn::Split": [
            ",",
            {
              "Fn::Sub": [
                "${SGIdsByParam},${SGByLogicalId}",
                {
                  "SGIdsByParam": {
                    "Fn::Join": [",", {
                      "Ref": "SecurityGroups"
                    }]
                  },
                  "SGByLogicalId": {
                    "Fn::GetAtt": ["InstanceSecurityGroup", "GroupId"]
                  }
                }
              ]
            }
          ]
        }
      }

1
你实际上可以在lambda中编写必要的转换函数,而不需要创建子堆栈,以获得所需的List-String。
例如:http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/walkthrough-custom-resources-lambda-lookup-amiids.html 实际上,这个函数除了接收列表并将其返回之外,不需要做任何事情。我有一个类似的场景,其中我有一个List-AWS :: EC2 :: Subnet :: Id,并且需要将其传递给需要List-String的资源。
过程:
定义LambdaExecutionRole。
定义类型为AWS :: Lambda :: Function的ListToStringListFunction,添加代码,在cfnresponse数据中返回{'Result':event['ResourceProperties']['List']}。
定义自定义资源MyList类型Custom :: MyList,将GetAtt的ServiceToken赋值为ListToStringListFunction Arn。还将List作为属性传递,该属性引用原始列表。
使用Fn :: GetAtt(MyList,Result)引用MyList。

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