如何使用 CloudFormation 构建 VPC?

阅读数:1267 2019 年 1 月 9 日

话题:语言 & 开发云计算最佳实践

如果要为基础设施用例构建单独 VPC 模板,则可以使用 Parameters、Conditions、Mapping 和 Outputs 让现有模板更加灵活。

正文

关键要点

  • 通过使用参数和条件,CloudFormation 模板可以变得更灵活、更强大。
  • 映射可用于从查找表中根据条件选择值。
  • 输出可用于为其他栈指定特定的栈资源。
  • 团队可以使用导出的输出作为团队间的通信媒介。

在本系列文章的第一部分,我们探讨了如何使用基础设施即代码(特别是 CloudFormation)来创建和维护 AWS VPC。我们创建的 CloudFormation 模板提供了简单、可重用的组件,我们可以用它创建简单 VPC。

但是,这个模板还不够灵活。我们希望可以有一个这样的模板,可以用它为开发、测试和生产环境构建具有不同数量子网的 VPC。我们想要一些在需要进行演示或 POC 时能够用于创建公共子网的东西,或者,我们可能希望使用 NAT 实例而不是 NAT 网关。

我们可以通过使用参数、条件、映射和输出让现有的模板变得更灵活,而不是为各种情况创建单独的模板。这是本系列的第二篇文章,所以你应该已经阅读了第一篇文章,并对模板已经很熟悉了。在这篇文章中,我将从增强原始模板开始说起。

本文涉及的 CloudFormation 模板可以在GitHub上找到,读者可以下载、修改和使用它。

可变数量的子网 / 可用区域

可用区域:AWS 已经让在给定地区内利用多个可用区域(AZ)变得更容易,而且成本更低。简单地说,你可以将可用区域视为一个巨大的独立数据中心。给定地区内的 AZ 通过高速、低延迟的私有链接相互连接。它们彼此足够接近,支持同步通信,但相隔得又足够远,可以减轻自然灾害、停电等事件所带来的影响。确切地说,我们并不知道究竟相隔多远,但其实也没必要知道。

以最低的成本实现基本的高可用性是使用两个 AZ。有时候单个 AZ 更适合简单的情况,例如演示或 POC。其他时候需要三个 AZ 来获得略微改善的高可用性。所以,接下来让我们来调整模板,让它支持可变的 AZ 数量。

使用第一篇文章中的模板,在“Resources”部分上方添加以下内容。

复制代码
Parameters:
  NumberOfAZs:
    Type: Number
    AllowedValues:
    - 1
    - 2
    - 3
    Default: 2
    Description:  How many Availability Zones do you wish to utilize?

YAML 基础知识:YAML 使用双空格缩进表示层次结构(没有制表符!)。短划线“-”表示“序列”,即属于同一组的多个值。参数通常位于资源上方,但从技术上讲,它们可以被放在模板中的任何位置。

NumberOfAZs:这个条目定义了模板的输入参数。在 AWS 管理控制台中使用这个模板创建资源栈时,UI 将提示输入“NumberOfAZs”,输入框旁边是描述内容。因为我们提供了“AllowedValues”,所以输入字段将是一个下拉框,其中包含 1、2 和 3 这三个选项。如果我们不做选择,将默认使用 2。这里定义了有效的参数类型,在这里我们可以使用 Number 或 String。

我们的目标是能够使用这个模板在任意地区创建资源栈。在撰写本文时,大多数地区现在至少有三个可用区域,但有些地域则没有(蒙特利尔、孟买、北京、首尔只有两个)。在这些地区选择使用三个 AZ 将导致错误。限制模板的灵活性以便避免这种少数情况下才会发生的尴尬错误,这样值不值得取决于你。

CLI 用法:通过 AWS 命令行界面(CLI)创建资源栈时,输入参数仍然有用。如果有必要,我们可以为参数提供值,或者使用默认值。如果提供的值超出了允许的范围将会出现错误。

在指定所需的 AZ 数量之后,我们需要修改模板的其余部分,以便让 CloudFormation 构建我们想要的子网。

Coditions 部分

为了让 CloudFormation 可以构建一个、两个或三个子网,我们将定义一些可以在 Resources 部分使用的“条件”。在 Parameters 部分下方和 Resources 部分上方添加以下代码:

复制代码
Conditions:
  BuildPublicB:         !Not [ !Equals [ !Ref NumberOfAZs, 1 ]] 
  BuildPublicC:         !Equals [ !Ref NumberOfAZs, 3 ] 

条件是布尔值(true/false)表达式,我们将在模板中使用它们。这里我们创建两个条件,一个用于指示我们是否要构建“B”子网,一个用于指示我们是否要构建“C”子网。由于“1”是 NumberOfAZs 允许的最小数量,因此我们始终都会构建“A”子网。

BuildPublicB:这个表达式检查所选的 NumberOfAZs 是否为 1 以外的值。因为 CloudFormation 中没有大于和小于内联函数,所以我们将使用!Equals 函数来引用输入参数并检查值是否与“ 1”相等。!Not 用于获得相反的结果(即 false 变 true,true 变 false)。布尔值结果将被保存在 BuildPublicB 中,然后在模板中的其他位置引用它。

BuildPublicC:这个表达式更简单,NumberOfAZs 要么是“3”(我们允许的最大值)要么不是。如果它是 true,我们将只构建 PublicSubnetC。

现在,我们已经明确定义了要创建哪些子网的条件,接下来可以用它们来创建实际的资源。

条件属性

在第一篇文章的原始模板中,我们使用以下代码创建了 PublicSubnetB:

复制代码
PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.20.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]    # Get the second AZ in the list 
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Public-B

注意下面的替换代码,特别是新的“Condition”属性:

复制代码
PublicSubnetB:
    Type: AWS::EC2::Subnet
    Condition: BuildPublicB
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.20.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]    # Get the second AZ in the list 
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Public-B

条件属性就是任意 CloudFormation 资源的可用选项。也就是说,“只有在 BuildPublicB 条件为 true 时才创建这个资源”。当它为 false 时,就会忽略资源的创建——将不会有 PublicSubnetB。

现在让我们添加第三个公共子网,但仅当 BuildPublicC 条件为 true 时:

复制代码
PublicSubnetC:
   Type: AWS::EC2::Subnet
   Condition: BuildPublicC
   Properties:
     VpcId: !Ref VPC
     CidrBlock: 10.1.30.0/24
     AvailabilityZone: !Select [ 2, !GetAZs ]    # Get the third AZ in the list 
     Tags:
     - Key: Name
       Value: !Sub ${AWS::StackName}-Public-C

你可能想知道是否有一种方法可以直接在资源上内联表达式条件,而不是使用单独的“Coditions”部分。在撰写本文时,没有。但是在编写了很多模板之后,我发现逻辑表达式的计算与用法的简单解耦其实是有好处的。毕竟,如果使用内联表达式,这些模板可能会变得非常复杂,例如你在此处看到的 AvailabilityZone 或 Tag/Value。

最后一步,为不同数量的公共子网调整子网的路由表关联。请注意以下的条件属性:

复制代码
PublicSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPublicB
    Properties:
      SubnetId: !Ref PublicSubnetB
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPublicC
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable

这里没有显示 PublicSubnetA 的关联。因为它始终存在于资源栈中,因此不需要条件属性。同样,PublicRouteTable 也是必需存在的。我们的资源栈将根据输入参数创建一个、两个或三个公共子网。接下来让我们来看看私有子网……

私有子网

我们假设要将这个模板生成的 VPC 用于一些面向公众的快速演示。在这样的 VPC 中拥有私有子网或 NAT 有点超出了实际,而且需要更长的时间来创建。让我们添加一个参数,可以用它来指定纯公共子网。在“Parameters”部分添加:

复制代码
PrivateSubnets:
    Type: String
    AllowedValues:
    - True
    - False
    Default: True
    Description: Do you want to create private subnets in addition to public subnets?

我们定义了一个输入参数来控制是否创建了任何私有子网。我希望 CloudFormation 为这样的情况提供“布尔”输入类型,但现在我们不得不是有只接受“True”或“False”的 String 类型。

让我们在 Coditions 部分中添加以下这些条件,用于计算输入值。这个会有点复杂:

复制代码
BuildPrivateSubnets: !Equals [ !Ref PrivateSubnets, True ]
  BuildPrivateA:       !Equals [ !Ref PrivateSubnets, True ]
  BuildPrivateB:       !And[!Not[!Equals[!Ref NumberOfAZs,1]],!Equals[!Ref PrivateSubnets,True]]
  BuildPrivateC:       !And[!Equals[!Ref NumberOfAZs,3],!Equals[!Ref PrivateSubnets, True]]

BuildPrivateSubnets:这是一个直接用于表达输入参数的简单条件。有时候,我们会根据是否存在私有子网(即 NAT)来构建一些东西。

BuildPrivateA:“BuildPrivateSubnets”的同义词,不是绝对必需的,但它的代码看起来非常干净。但有点遗憾的是,我们无法在一个条件中引用另一个条件。

BuildPrivateB:这里的逻辑是“如果我们想要使用多个 AZ 并且想要构建私有子网,那么就构建 PrivateSubnetB”。

BuildPrivateC:这里的逻辑是“如果我们想要使用三个 AZ,并且想要构建私有子网,那么就构建 PrivateSubnetC”。

现在,我们可以将私有子网定义从原始模板转换为使用条件属性,如下所示:

复制代码
PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Condition: BuildPrivateA
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.50.0/24
      AvailabilityZone: !Select [ 0, !GetAZs ]    # Get the first AZ in the list 
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Private-A
  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Condition: BuildPrivateB
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.60.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]    # Get the second AZ in the list 
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Private-B
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Condition: BuildPrivateC
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.70.0/24
      AvailabilityZone: !Select [ 2, !GetAZs ]    # Get the third AZ in the list 
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-Private-C

我们所做的修改就是在原始模板中添加了条件属性。另外,我们还添加了 PrivateSubnetC,它可以很容易地从 PrivateSubnetA 和 PrivateSubnetB 的定义中克隆出来。

子网路由表关联也需要修改。如果没有子网,则不需要子网关联:

复制代码
PrivateSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPrivateA
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTable
  PrivateSubnetBRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPrivateB
    Properties:
      SubnetId: !Ref PrivateSubnetB
      RouteTableId: !Ref PrivateRouteTable
  PrivateSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Condition: BuildPrivateC
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTable

NAT 网关

我们的模板现在可以有条件地创建私有子网,因此我们需要相应地调整 NAT 网关和路由表条目。首先是 NAT 网关,如果我们不构建私有子网,就没有理由创建它或与其关联的弹性 IP 地址:

复制代码
# A NAT Gateway will be built and used if the user selected Private subnets and a Gateway instead of an EC2 instance. 
 NATGateway:
   Type: AWS::EC2::NatGateway
   Condition: BuildPrivateSubnets
   Properties:
     AllocationId: !GetAtt ElasticIPAddress.AllocationId
     SubnetId: !Ref PublicSubnetA
     Tags:
     - Key: Name
       Value: !Sub NAT-${AWS::StackName}
 ElasticIPAddress:
   Type: AWS::EC2::EIP
   Condition: BuildPrivateSubnets
   Properties:
     Domain: VPC

原始模板的唯一变化是条件属性,我们希望只在选择了要构建私有子网时才构建这些东西。

接下来,条件表明我们可能不需要私有路由表或路由表的条目:

复制代码
# Here is a private route table:
 PrivateRouteTable:
   Type: AWS::EC2::RouteTable
   Condition: BuildPrivateSubnets
   Properties:
     VpcId: !Ref VPC
     Tags:
     - Key: Name
       Value: Private
 PrivateRoute1:            # Private route table can access web via NAT (created below)
   Type: AWS::EC2::Route
   Condition: BuildPrivateSubnets
   Properties:
     RouteTableId: !Ref PrivateRouteTable
     DestinationCidrBlock: 0.0.0.0/0
     # Route traffic through the NAT Gateway:
     NatGatewayId: !Ref NATGateway

这个时候,当“BuildPrivateSubnets”为 false 时,我们将忽略任何私有子网、路由表或 NAT 的创建。我们的模板能够根据参数输入创建一到六个子网。相当灵活,而且要实现这一点不需要太多的工作量。

可选的 NAT 类型

为了进一步提高灵活性,我们让模板支持 NAT 网关。内建的托管服务非常适合用于生产环境,但用于 POC 可能就有点贵了。在出现 NAT 网关之前,我们通过常规的 EC2 实例来提供 NAT 支持,不管哪种方式都有其优缺点。所以,出于实验的目的,我们添加一个参数,以便提供这种选择:

复制代码
NATType:
    Type: String
    AllowedValues:
    - "EC2 NAT Instance"
    - "NAT Gateway"
    Default:  "NAT Gateway"
    Description:  What type of NAT to use for private instances to communicate with the internet.  A single EC2 instance can be used as a NAT, or you can use the AWS NAT Gateway (managed, scalable, more expensive).  This setting will be IGNORED if you do not build private subnets.

并在 Conditions 部分添加以下内容:

复制代码
BuildNATGateway:  !And[!Equals[!Ref PrivateSubnets,True],!Equals[!Ref NATType, "NAT Gateway"]]
  BuildNATInstance: !And[!Equals[!Ref PrivateSubnets,True],!Equals[!Ref NATType, "EC2 NAT Instance"]]

第一行的意思是“如果我们要构建私有子网和 NAT 网关,那么就构建 NAT 网关”。第二行的意思是“如果我们要构建私有子网,并选择使用 EC2 实例,那么就构建一个 EC2 实例作为 NAT”。

我们之前描述的 NAT 网关 / 弹性 IP 地址条件也需要做出调整,我们现在想要根据 BuildNATGateway 条件控制它们的创建:

复制代码
# A NAT Gateway will be built and used if the user selected Private subnets and a Gateway instead of an EC2 instance. 
 NATGateway:
   Type: AWS::EC2::NatGateway
   Condition: BuildNATGateway
   Properties:
     AllocationId: !GetAtt ElasticIPAddress.AllocationId
     SubnetId: !Ref PublicSubnetA
     Tags:
     - Key: Name
       Value: !Sub NAT-${AWS::StackName}
 ElasticIPAddress:
   Type: AWS::EC2::EIP
   Condition: BuildNATGateway
   Properties:
     Domain: VPC

基于 EC2 的 NAT 实例需要一些新的构造。首先,EC2 实例需要 AMI,但不同地区的 AMI ID 值是不一样的。为了可以在任意地区使用这个模板,我们在 Condition 部分之前将以下的 Mappings 部分添加到模板中(从技术上说,这些部分的放置顺序是随意的,有些人喜欢把它放在底部附近):

复制代码
Mappings:
 #  This is the Amazon Linux 2 AMI.  Adjust these values as needed, they can change a few times per year:
 AmazonLinuxAMI:
   us-east-1:
     AMI: ami-04681a1dbd79675a5    # N Virginia
   us-east-2:
     AMI: ami-0cf31d971a3ca20d6    # Ohio     
   us-west-1:
     AMI: ami-0782017a917e973e7    # N California
   us-west-2:
     AMI: ami-6cd6f714             # Oregon
   eu-west-1:
     AMI: ami-0bdb1d6c15a40392c    # Ireland
   eu-central-1:
     AMI: ami-0f5dbc86dd9cbf7a8    # Frankfurt
   sa-east-1:
     AMI: ami-0ad7b0031d41ed4b9    # Sao Paulo
   ap-southeast-1:
     AMI: ami-01da99628f381e50a    # Singapore
   ap-southeast-2:
     AMI: ami-00e17d1165b9dd3ec    # Sydney
   ap-northeast-1:
     AMI: ami-08847abae18baa040    # Tokyo

这个 Mapping 部分定义了 Amazon Linux 2 OS 的 AMI ID 值。ID 值根据资源栈所在地区的不同而不同。稍后我们将看到在定义 EC2 实例资源时如何使用这个映射表。但在继续往下介绍之前,有一些要点需要提及:1)注释是你的好朋友;2)我没有为每个地区都提供值;3)这些值只是截止撰写本文时的值,EC2 团队将会时不时发布改进过的新 AMI 版本,你或许可以使用它们。

找到这些值并不难,我通过使用 AWS 管理控制台的 EC2 实例创建向导就可以找到它们。在 AMI 选择页面,我利用区域选择来获得所有集合的值。当然,还有更多高级技术可以取代映射表(例如由 Lambda 函数支持的参数存储查找或 CloudFormation 自定义资源),但我不想在这篇文章中过多地介绍它们。

接下来,我们的 EC2 实例需要一个安全组:

复制代码
# A security group for our NAT.  Ingress from the VPC IPs only.  Egress is TCP & UDP only:
  NATSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Condition: BuildNATInstance
    DependsOn: AttachGateway
    Properties:
      GroupName: !Sub NATSecurityGroup-${AWS::StackName}
      GroupDescription: Enable internal access to the NAT device
      VpcId: !Ref VPC
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: '0'
        ToPort: '1024'
        CidrIp: !GetAtt VPC.CidrBlock
      SecurityGroupEgress:
      - IpProtocol: tcp
        FromPort: '0'
        ToPort: '65535'
        CidrIp: 0.0.0.0/0
      - IpProtocol: udp
        FromPort: '0'
        ToPort: '65535'
        CidrIp: 0.0.0.0/0

为了保持这篇文章的简短,我不打算解释所有复杂的东西,所以这里给出了摘要:

  • 仅在 BuildNATInstance 的条件为 true 时创建它。
  • 在 Internet Gateway 连接到 VPC 之前不会尝试创建它(请参阅上一篇文章)。
  • 安全组的名称以 CloudFormation 资源栈的名称为基础(请参阅上一篇文章)。
  • 安全组仅允许来自 VPC 内部地址范围内的入站流量。只有 VPC 内的私有 IP 才能向 NAT 发送流量。
  • 出站流量可以基于 TCP 或 UDP,并且基本上可以流向任何地方。
  • 有关安全组的完整信息,请参阅AWS::EC2::SecurityGroup

接下来是 EC2 实例:

复制代码
# A NAT Instance will be used if the user selected Private subnets and EC2-based NAT.     
  NATInstance:
    Type: AWS::EC2::Instance
    Condition: BuildNATInstance
    DependsOn: PublicRoute1                           # Must have route to IGW established.
    Properties:
      ImageId: !FindInMap [ AmazonLinuxAMI, !Ref "AWS::Region", AMI]  # lookup from AMI map
      InstanceType: t2.small                          # Any instance type is fine
      NetworkInterfaces:
      - DeviceIndex: '0'
        SubnetId: !Ref PublicSubnetA                  # Any public subnet is fine
        AssociatePublicIpAddress: true                # We will need a public IP address
        GroupSet: [!Ref NATSecurityGroup]             # Plug in the security group
      SourceDestCheck: false  # NATs don't work if EC2 matches source with destinations.
      Tags:
      - Key: Name
        Value: !Sub NAT-${AWS::StackName}
      UserData:      #  This code is NAT code.  Last line signals completion:
        Fn::Base64: !Sub |
          #!/bin/bash
          yum -y update
          yum install -y aws-cfn-bootstrap
          echo 1 > /proc/sys/net/ipv4/ip_forward
          echo 0 > /proc/sys/net/ipv4/conf/eth0/send_redirects
          /sbin/iptables -t nat -A POSTROUTING -o eth0 -s 0.0.0.0/0 -j MASQUERADE
          /sbin/iptables-save > /etc/sysconfig/iptables
          mkdir -p /etc/sysctl.d/
          cat << NatConfFileMarker > /etc/sysctl.d/nat.conf
          net.ipv4.ip_forward = 1
          net.ipv4.conf.eth0.send_redirects = 0
          NatConfFileMarker
          /opt/aws/bin/cfn-signal -e 0 --resource NATInstance --stack ${AWS::StackName} --region ${AWS::Region}
    # This NATInstance is only complete when you get 1 signal back within 5 minutes'.
    CreationPolicy:
      ResourceSignal:
        Count: 1
        Timeout: PT5M
{1}

简述:

  • Condition:只在 BuildNATInstance 为 true 时才会创建这个实例。
  • DependsOn:在 PublicRoute1 构建完成之前,我们不会尝试创建它,也就是说我们必须连接到互联网。这对于在 UserData 中正常运行“yum”命令来说至关重要。
  • ImageID:要使用的 AMI(来自之前创建的映射表)。“AWS::Region”是一个伪参数,它会告诉我们正在创建的资源栈所在的地区。实质上,我们要求 CloudFormation 在映射表中查找地区并使用生成的 AMI。
  • SubnetId:我们将这个 EC2 NAT 实例放在公共子网中。在单个公共子网中使用单个 NAT 实例显得有点简陋,但本文的主要目的是演示基本的灵活性,并不是要介绍详尽的最佳实践。
  • AssociatePublicIPAddress:NAT 用来与公共网络上的各方发起通信的公共 IP 地址。
  • GroupSet:NAT 与之前定义的安全组相关联。这个参数需要一个安全组列表,而不是单个值,所以添加了方括号“[]”,将单个值强制转换为列表结构。
  • SourceDestCheck:让 EC2 忽略通常的流量检查,通常情况下,它需要确保 EC2 实例是它接收到的流量的源或目的地,但这对于 NAT 来说不适用。简单的说我们必须这么做才能让 NAT 生效。请参阅源和目的地检查获得更深入的解释。
  • UserData:提供 NAT 功能的 Linux 脚本,不过这里不详细介绍这个脚本的相关内容了。!Sub内部函数会查找和替换 {AWS::StackName})。脚本的最后一行cfn-signal是一个 CloudFormation 函数,它会在脚本执行完成时向 CloudFormation 栈发出信号。
  • Fn::Base64:!Sub:UserData 脚本必须经过 Base64 编码,这可以通过Fn::Base64内部函数来完成。通常我会使用!Base64 快捷语法,也会使用!Sub 函数来替换脚本中的占位符。在!Base64 中使用!Sub 是有效的 Cloud Formation,但却是无效的 YAML,所以我们必须使用外部函数的完整函数名。
  • CreationPolicy:通常,CloudFormation 会在底层服务表示资源已创建完成之后才会认为资源创建完成。对于 EC2 实例来说,就是要等到操作系统开始启动。不过,为了让 EC2 NAT 实例可供资源栈中的任何组件使用,UserData 脚本需要先执行完毕。CreationPolicy 就像是在说:“在我们收到信号(来自 cfn-signal 命令)之前,这个资源还不算创建完成”。

好多东西啊!但 CloudFormation 或基础设施即代码的美妙之处在于,这些事情我只需要做一次。有关所有这些设置的完整细节,请参阅AWS::EC2::Instance

最后,我们需要调整之前构建的 PrivateRoute。我们需要将出站流量路由到 NAT 网关或 NAT 实例,具体取决于创建的是哪个:

复制代码
PrivateRoute1:            # Private route table can access web via NAT (created below)
   Type: AWS::EC2::Route
   Condition: BuildPrivateSubnets
   Properties:
     RouteTableId: !Ref PrivateRouteTable
     DestinationCidrBlock: 0.0.0.0/0
     # If we are using a NAT Instance, route traffic through the NAT Instance:
     InstanceId:   !If [ BuildNATInstance, !Ref NATInstance, !Ref "AWS::NoValue" ]
     # Otherwise if we are using a NAT Gateway, route traffic through the NAT Gateway:
     NatGatewayId: !If [ BuildNATGateway, !Ref NATGateway, !Ref "AWS::NoValue" ]

请注意 InstanceId 和 NatGatewayId 这两个属性,根据AWS::EC2::Route文档,它们是互斥的。当我们将流量路由到 EC2 实例是会用到InstanceId。如果我们选择了 BuildNATInstance,那么!If内部函数只会将该值设置为 NATInstance。AWS::NoValue所做的事情比它看起来的要多,它不只是表示没有设置值,而且 CloudFormation 知道这意味着根本不需要设置这个属性。如果我们选择了 BuildNATGateway,NatGatewayId的镜像逻辑会将值设置为 NATGateway。由于条件是互斥的,因此只有一个会设置成功,出站流量将使用 NATInstance 或 NATGateway,最终取决于我们的原始输入决策。

可选的模板元数据

我们可以对修改后的模板进行一些装饰性的调整。我们希望能够控制参数的输入顺序,为此,在“Parameters”部分之前添加“Metadata”部分(尽管有些人喜欢将其放在模板的底部):

复制代码
Metadata:
 # Control the UI display when running this template from the AWS Management Console:
 AWS::CloudFormation::Interface:
   ParameterGroups:
     - Label:
         default: "Network Configuration"
       Parameters:
         - NumberOfAZs
         - PrivateSubnets
         - NATType

现在,在 AWS 管理控制台中使用这个模板创建资源栈时,参数页将提示操作员进行“网络配置”,并按所需顺序显示参数。当在使用 CLI 时,这部分不会有任何影响。

Outputs

我们创建的模板是一个很好的通用模板,可以作为其他需要使用 VPC 的 CloudFormation 资源栈的起点。我们希望能够轻松地将其作为其他资源栈的输入。这对于大多数 IT 组织来说尤为重要,在这些组织中,团队之间承担着细分的责任,负责管理网络资源的团队和负责构建使用网络的资源的团队是不同的团队。要让资源栈提供输出值,可以通过在模板中创建 Outputs 部分来实现:

复制代码
Outputs:
 VPC:
   Description: VPC of the base network
   Value: !Ref VPC
   Export:
     Name: !Sub ${AWS::StackName}-VPC
 PublicSubnetA:
   Description: First Public Subnet
   Value: !Ref PublicSubnetA
   Export:
     Name: !Sub ${AWS::StackName}-PublicSubnetA
 PublicSubnetB:
   Description: Second Public Subnet
   Condition: BuildPublicB
   Value: !Ref PublicSubnetB
   Export:
     Name: !Sub ${AWS::StackName}-PublicSubnetB
 PublicSubnetC:
   Description: Third Public Subnet
   Condition: BuildPublicC
   Value: !Ref PublicSubnetC
   Export:
     Name: !Sub ${AWS::StackName}-PublicSubnetC
 PrivateSubnetA:
   Condition: BuildPrivateSubnets
   Description: First Private Subnet
   Value: !Ref PrivateSubnetA
   Export:
     Name: !Sub ${AWS::StackName}-PrivateSubnetA
 PrivateSubnetB:
   Condition: BuildPrivateB
   Description: Second Private Subnet
   Value: !Ref PrivateSubnetB
   Export:
     Name: !Sub ${AWS::StackName}-PrivateSubnetB
 PrivateSubnetC:
   Condition: BuildPrivateC
   Description: Third Private Subnet
   Value: !Ref PrivateSubnetC
   Export:
     Name: !Sub ${AWS::StackName}-PrivateSubnetC

基本上,在资源栈创建完成后,这些输出条目会在 AWS 管理控制台 /CLI JSON 输出上显示相关的值。请注意,包含条件属性是为了仅触发实际创建的资源的值。

需要注意的部分是 Export/Name。它生成了一个区域范围的名称,另一个资源栈可以通过这个名称引用该资源。以 PublicSubnetA 为例,并假设资源栈名称为“my-network”,导出的值为“my-network-PublicSubnetA”。另一个资源栈可以使用“!ImportValue my-network-PublicSubnetA”来引用该资源,就像在资源栈中使用“!Ref”一样容易。通常,初始(基础)资源栈会被作为输入参数,因此,资源栈名称部分可以是动态的,例如:

复制代码
Fn::ImportValue: !Sub ${BaseStack}-PublicSubnetA

其中“BaseStack”是另一个资源栈的输入参数。!ImportValue 里的!Sub 是无效的 YAML,所以我们必须使用“长格式”的函数名称Fn::ImportValue

这里的的 Export/Name/!ImportValue 技术在多团队环境中很常见。每个团队通常需要引用其他团队生成的资源栈中的资源,并生成被其他资源栈引用的资源。导出的名称成为团队间的可靠通信点。此外,CloudFormation 会跟踪这些资源栈间的引用,以防止因为删除或更新资源栈导致另一个资源栈中的相关资源无效。

总结

除了添加 EC2 NAT 实例选项和输出部分外,我们只修改了原始模板中的少量行。参数和条件为我们的模板带来了更强悍的功能。我们现在可以创建具有一到六个子网的 VPC,包含各种可能的排列组合。我们创建的资源栈可以被其他资源栈引用。更令人惊奇的是,我们可以使用这个模板修改生成的资源栈,为其添加或删除子网,例如在 POC 时进行试用性部署。你可以扩展这项技术,让这个模板变得更加复杂,例如创建只包含私有子网的 VPC。

关于作者

Ken Krueger的专业使命宣言是“通过应用现代技术来指导组织和个人实现商业成功”。他拥有超过 30 年软件从业经验,做过软件开发者、项目负责人、项目经理、Scrum Master,以及大型机、客户端 - 服务器、Web 讲师。他在 Java、Spring、SQL、Web 开发、云计算和相关技术方面有着丰富的经验。他的行业经验包括电信、金融、房地产、零售、电力、航运、酒店和软件开发。他拥有南佛罗里达大学 MIS 学位、罗林斯学院 Crummer 商学院 MBA 学位,以及 Scrum Master、PMP、AWS 和 Java 认证。

查看英文原文https://www.infoq.com/articles/aws-vpc-cloudformation-part2