如何使用CloudFormation構建 VPC?

如果要爲基礎設施用例構建單獨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

簡述:

  • 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章