AWS CloudFormation 系列--(2)常用函数及字段

AWS CloudFormation 系列--(2)常用函数及字段
B站视频

前面CloudFormation的快速入门,基本了解了如何使用这个服务来创建AWS资源。但是前面写的代码基本只能自己看看,没法分享给别人使用,比如代码里面明确写了EC2的密钥,别人账号里面肯定没有这个密钥,应该让使用者自己选择密钥信息。

所以需要创建更加灵活的CloudFormation模板,这里会介绍一些常见的字段和函数,了解了这些之后,基本上能看懂网络上大部分的CloudFormation代码。

这篇文章知识点比较多,我会先追个介绍,最后会通过一个实验把这些知识点整合起来。下面这个实验环境,和之前快速入门是一样的,但是会通过一些函数和字段,让这个模板更加灵活。

image-20220918225135044

一、DependsOn属性

前面快速入门,讲过了模板里面最重要的资源(Resources)字段。资源字段内部一共有6种属性,这里只介绍最常用的DependsOn属性。

之前创建的环境比较简单,所以没有用到这个属性。如果在两个资源之间有依赖关系时,如果你希望其中一个资源先创建,然后再创建另外一个资源,就可以用DependsOn属性。

CreationPolicy attribute
DeletionPolicy attribute
DependsOn attribute
Metadata attribute
UpdatePolicy attribute
UpdateReplacePolicy attribute

这里通过一个场景,来看DependsOn属性的用法。例如需要在CloudFormation代码里面创建EC2和数据库,并且让EC2连接上数据库。那么肯定希望先创建数据,然后再创建EC2,否则肯定连接不上。这时候就可以在EC2资源下面,加上DependsOn属性,确保创建完数据之后再创建EC2。

Resources:
  Ec2Instance:
    Type: AWS::EC2::Instance
    DependsOn: myDB
    Properties:
      ImageId: ami-003a3de8892ecbc45
  myDB:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: '5'
      DBInstanceClass: db.t2.small
      Engine: MySQL
      EngineVersion: '5.5'
      MasterUsername: MyName
      MasterUserPassword: MyPassword

这里其实隐含了一个特性,就是AWS在创建、更新、删除资源的时候默认是并行的,也就是尽可能同时创建所有资源。使用DependsOn属性可以明确的指定资源创建的先后顺序。

另外!Ref!GetAtt 这两个内部函数具有隐式的依赖关系。例如之前使用过!Ref函数,在创建IGW时,使用Ref函数调用了VPC ID信息,这样也可以确保被调用的资源先创建,也就是创建VPC之后,才会创建IGW。

# IGW 关联VPC
  MyTestAttachIgw:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref MyTestVpc
      InternetGatewayId: !Ref MyTestIgw

二、常用内部函数

下面介绍CloudFormation常见的一些内部函数。

只能在特定的模板字段下面使用内部函数,例如在resource propertiesoutputs、metadata attributes、update policy attributes这几个字段下面使用内部函数。下面是所有内部函数,这里只介绍几个常用的内部函数[参考链接1]。

Fn::Base64
Fn::Cidr
Condition functions
Fn::FindInMap
Fn::GetAtt
Fn::GetAZs
Fn::ImportValue
Fn::Join
Fn::Select
Fn::Split
Fn::Sub
Fn::Transform
Ref

Fn::Ref 函数

最常见的毫无疑问就是Ref函数,前面的模板使用过了Ref函数,一般情况下Ref函数返回资源的ID信息。有些资源会返回有重要意义的标识符,例如AWS::EC2::EIP[参考链接2]资源返回 IP 地址,AWS::EC2::Instance返回实例 ID。

完整函数名称的语法:

Ref: logicalName

短格式的语法:

!Ref logicalName

例如前面将IGW关联到VPC,就用到Ref函数,VPC ID使用了完成名称,IGW ID使用了短格式写法。一般我会使用短格式写法,写在一行更加简洁。

  MyTestAttachIgw:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: 
        Ref: MyTestVpc
      InternetGatewayId: !Ref MyTestIgw

Fn::GetAtt 函数

Fn::GetAtt 函数返回模板中资源的属性值。例如你希望获取EC2实例的公网IP地址,就可以使用!GetAtt MyTestEc2Instance.PublicIp来获取EC2的公网IP地址。具体每个资源可以获取哪些属性值,可以查阅对应资源的文档,例如查看AWS::EC2::Instance有哪些属性值。

完整函数名称的语法:

Fn::GetAtt: [ logicalNameOfResource, attributeName ]

短格式的语法:

!GetAtt logicalNameOfResource.attributeName

例如输出EC2的公网IP地址,Fn::GetAtt 函数短格式写法为:

Outputs:
  MyTestEc2InstanceEip:
    Description: MyTestEc2InstanceEip
    Value: !GetAtt MyTestEc2Instance.PublicIp
    Export:
      Name: MyTestEc2InstanceEip

下面是对应的Fn::GetAtt 函数的完整写法。

Outputs:
  MyTestEc2InstanceEip:
    Description: MyTestEc2InstanceEip
    Value: 
      Fn::Getatt: [ MyTestEc2Instance, PublicIp ]
    Export:
      Name: MyTestEc2InstanceEip

Fn::Select 函数

Fn::Select函数通过索引,来获取列表中的某个对象。一般结合函数Fn::GetAZs使用。

完整函数名称的语法,index指定索引序号,listOfObjects表示对象的列表。

Fn::Select: [ index, listOfObjects ] 

短格式的语法:

!Select [ index, listOfObjects ]

为了更好理解这个函数的用法,这里通过一个伪代码来进行解释。例如变量Ec2TypeList是一个列表,想要设置EC2实例类型为t2.micro,通过Select函数,选择列表的第一个索引,即0号索引即可。

Ec2TypeList = [ t2.micro, t2.small, t2.large ]

  MyTestEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Select [ 0, Ec2TypeList ]

Fn::GetAZs 函数

Fn::GetAZs函数返回一个数组,该数组按字母顺序列出指定区域的可用区。

前面模板创建子网时,在模板里面硬编码了可用区信息AvailabilityZone: cn-northwest-1a,那么这个代码在宁夏以外的其他区域使用就会报错,因为其他区域没有cn-northwest-1a这个可用区。

下面案例中,AWS::Region伪参数[参考链接3]。伪参数是 AWS CloudFormation 内建的变量,调用AWS::Region会返回代码运行的区域值,即cn-northwest-1

进一步通过Fn::GetAZs函数来指定获取可用区信息,因为返回的结果是一个列表,所以可以用Select函数来选择单个对象,例如这里选择返回结果中的第一个可用区。

AvailabilityZone: !Select 
  - 0
  - Fn::GetAZs: !Ref 'AWS::Region'

这样前面子网的可用区信息就不用硬编码了,换成更加灵活的方式指定可用区,这里指定为CloudFormation运行区域的第一个可用区。

# 在VPC内创建子网
  MyTestVpcSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyTestVpc
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: 
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      Tags:
      - Key: Name
        Value: my-test-vpc-public-subnet

Fn::Base64 函数

内部函数Fn::Base64返回输入字符串的 Base64 表示方法。该函数通常用于通过 UserData 属性将编码的数据传递给 Amazon EC2 实例。你可以在 Fn::Base64 函数内部使用返回字符串的任意函数。

完整函数名称的语法:

Fn::Base64: valueToEncode

短格式的语法:

!Base64 valueToEncode

Fn::Sub 函数

内部函数 Fn::Sub 将输入字符串中的变量替换为你指定的值。 Fn::Sub 主要有两个使用场景,一个是拼接字符串,另外就是结合Fn::Base64函数,将数据传递到EC2的 UserData 里面。

完整函数名称的语法:

Fn::Sub: String

短格式的语法:

!Sub String

例如下面这个案例,通过UserData来安装apache服务,修改http端口,并启动服务。

函数Fn::Base64:表示将后面的字符串编码转换为Base64格式。

函数!Sub将输入字符串中的变量替换为你指定的值。

字符|(管道符号)表示一种文本风格(Literal Style),它可以让你输入正常的文本,而不用使用\n来表示换行。

# 创建一个EC2实例
  MyTestEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-003a3de8892ecbc45
      KeyName: CloudFormation-Test-Key
      InstanceType: t3.small
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - Ref: MyTestVpcSg
          SubnetId:  !Ref MyTestVpcSubnet
      UserData:
        Fn::Base64:
          !Sub |
          #!/bin/bash
          yum update -y
          yum install -y httpd
          sed -i.bak 's/Listen 80/Listen 8443/g' /etc/httpd/conf/httpd.conf
          systemctl start httpd.service
          systemctl enable httpd.service

函数 Fn::Sub 也经常用来做文本替换操作,有两种使用场景,一种是直接调用已有变量,另外一种是指定变量映射。

直接调用已有变量

以下示例使用AWS::Region这个伪参数,以及 VPC 资源逻辑 ID 产生了一个字符串。替换的结果格式为arn:aws:ec2:cn-northwest-1:vpc/vpc-0e2bd48e14bb15086

使用${}替换变量,其实就是直接调用变量。以下示例使用AWS::Region这个伪参数,以及 VPC 资源逻辑 ID 产生了一个字符串。替换的结果格式为arn:aws:ec2:cn-northwest-1:vpc/vpc-0e2bd48e14bb15086

!Sub 'arn:aws:ec2:${AWS::Region}:vpc/${MyTestVpc}'

变量映射

变量映射的短格式语法如下,后面的变量往前面的字符串传递参数。

!Sub
  - String
  - Var1Name: Var1Value
    Var2Name: Var2Value

例如,使用GetAtt 函数获取的结果,来替换变量 ${MyTestEc2InstanceEip} 的值。注意${}内部只能填写变量,不能嵌套其他函数,例如不能写成${!GetAtt MyTestEc2Instance.PublicIp}

  WebServerSubUrl:
    Description: WebServerSubUrl
    Value: !Sub
      - "http://${MyTestEc2InstanceEip}:${webServerPort}"
      - {MyTestEc2InstanceEip: !GetAtt MyTestEc2Instance.PublicIp}
    Export:
      Name: WebServerSubUrl

Fn::FindInMap 函数

内置函数Fn::FindInMap返回与Mappings部分声明的双层映射中的键对应的值。通过下面的例子看下什么是双层映射中的键对应的值:

ToplevelKey1是第一层映射的键,SecondLevelKey1是第二层映射的键。想要获取Vaule-ABC这个值,就可以利用FindInMap函数来获取。

Mappings:
 logicalResource:
   ToplevelKey1:
     SecondLevelKey1: Vaule-ABC
     SecondLevelKey2: Vaule-123
   ToplevelKey2:
     SecondLevelKey1: Vaule-XYZ
     SecondLevelKey2: Vaule-456

!FindInMap [logicalResource, ToplevelKey1, SecondLevelKey1]
Vaule-ABC

上面就是一些常见的函数,还有一些函数没有介绍,后续有时间会在单独介绍。

三、Parameters(参数)

Parameters是模板里面的可选字段。利用参数,你能够在每次创建或更新堆栈时将自定义值输入模板。

例如可以让用户自己选择EC2的密钥对,密钥对可以设置默认值。设置Web服务器的端口,端口值只能在AllowedValues中选择,后续安全组和输出都可以调用这个端口信息。

Parameters:
  myKeyPair:
    Description: Amazon EC2 Key Pair
    Type: AWS::EC2::KeyPair::KeyName
    Default: MyCN-CloudFormation-Test-Key
  webServerPort:
    Description: Apache Http Server Port
    Type: String
    Default: 8443
    AllowedValues:
      - 8443
      - 8888
      - 8088

在创建EC2时,调用参数定义的密钥对信息。

# 创建一个EC2实例
  MyTestEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref myKeyPair 

在安全组放行端口时,可以使用!Ref或者!Sub函数调用参数里面定义的Web服务器端口。

# 在VPC内创建一个安全组
  MyTestVpcSg:
...
      - IpProtocol: tcp
        FromPort: !Ref webServerPort  # !Sub ${webServerPort}
        ToPort: !Ref webServerPort	# !Sub ${webServerPort}
        CidrIp: 0.0.0.0/0

在创建堆栈时,需要填写参数信息。

image-20220401150442255

四、Outputs(输出)

Outputs是模板里面的可选参数,你可以输出指定的值,这些值可以导入到其他堆栈中、可以从响应中获取这些值、或者在AWS CloudFormation 控制台中查看。

这里输出了EC2的公网IP地址信息。还通过字符串替换或拼接的方式输出了EC2的URL信息,可以用!Sub!Join两种方式输出Web服务器的URL,!Join函数这里简单演示一下使用案例,Join函数类似于字符串拼接,我个人更习惯使用!Sub函数。

Outputs:
  MyTestEc2InstanceEip:
    Description: MyTestEc2InstanceEip
    Value: !GetAtt MyTestEc2Instance.PublicIp
    Export:
      Name: MyTestEc2InstanceEip
  WebServerSubUrl:
    Description: WebServerUrl
    Value: !Sub
      - "http://${MyTestEc2InstanceEip}:${webServerPort}"
      - {MyTestEc2InstanceEip: !GetAtt MyTestEc2Instance.PublicIp}
    Export:
      Name: WebServerSubUrl
  WebServerJoinUrl:
    Description: WebServerUrlTest
    Value: !Join
      - ''
      - - 'http://'
        - !GetAtt
          - MyTestEc2Instance
          - PublicIp
        - ':'
        - !Sub ${webServerPort}
    Export:
      Name: WebServerJoinUrl

五、Mappings(映像)

Mappings是模板里面的可选字段,包含一些键值对,通过Fn::FindInMap函数可以获取映射中值的信息。

Mappings字段在网上其它人分享的模板里面经常会看到。例如,因为AMI ID每个区域都是不一样的,所以想要模板内部的EC2实例能在多个区域运行,需要提前指定不同区域的AMI ID信息,然后就可以利用Mappings定义好不同区域的AMI ID信息,然后在EC2资源利用Fn::FindInMap调用。

这里定义了中国北京和中国宁夏区域的Amazon Linux AMI ID。其中HVM64表示全虚拟化x86架构64位系统,HVMG2表示Arm架构Graviton2处理器。[参考链接4]

Mappings:
  RegionMap:
    cn-north-1:
      HVM64: ami-0a01c170bf8ccec5b
      HVMG2: ami-0491098e85ea4b162
    cn-northwest-1:
      HVM64: ami-003a3de8892ecbc45
      HVMG2: ami-026d9fc0529d10475

在指定EC2镜像时,可以使用!FindInMap函数进行调用。其中!Ref "AWS::Region"是伪参数,可以获取创建堆栈的AWS区域。这里就实现了在哪个区域部署时,就传递对应区域的AMI ID信息,模板将具有更好的灵活性。

# 创建一个EC2实例
  MyTestEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", HVM64]

另外一个常见使用Mappings的场景,是根据测试环境或者生产环境自动选择EC2的实例类型,或者其他参数。

Mappings定义测试环境和生产环境的实例类型,并提供参数让用户选择环境类型。

Mappings:

  EnvironmentType:
    dev:
      instanceType: t3.micro
      name: dev
    prod:
      instanceType: t3.small
      name: prod
      
Parameters:
  Environment:
    Type: String
    AllowedValues:
      - dev
      - prod
    Default: dev

EC2实例调用用户选择的环境类型,来决定启动的实例类型。例如如果选择的dev环境,那么实例类型就会通过FindInMap函数获取到t3.micro

# 创建一个EC2实例
  MyTestEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", HVM64]
      KeyName: !Ref myKeyPair
      InstanceType: !FindInMap [EnvironmentType, !Ref Environment, instanceType]

六、创建一个更加灵活的模板

现在把上面的参数和函数都整合起来。

模板添加了Parameters字段、让用户选择EC2的Key、指定Web服务器的端口、在安全组内调用Web端口的参数、在UserData的配置文件调用Web端口信息、添加了Outputs字段,输出EC2的公网IP地址,输出Web服务器的URL信息。

AWSTemplateFormatVersion: "2010-09-09"
Description: "CloudFormation learning template from Liu Qianglong."

Mappings:
  RegionMap:
    cn-north-1:
      HVM64: ami-0a01c170bf8ccec5b
      HVMG2: ami-0491098e85ea4b162
    cn-northwest-1:
      HVM64: ami-003a3de8892ecbc45
      HVMG2: ami-026d9fc0529d10475
  EnvironmentType:
    dev:
      instanceType: t3.micro
      name: dev
    prod:
      instanceType: t3.small
      name: prod

Parameters:
  Environment:
    Type: String
    AllowedValues:
      - dev
      - prod
    Default: dev
  myKeyPair:
    Description: Amazon EC2 Key Pair
    Type: AWS::EC2::KeyPair::KeyName
    Default: CloudFormation-Test-Key
  webServerPort:
    Description: Apache Http Server Port
    Type: String
    Default: 8443
    AllowedValues:
      - 8443
      - 8888
      - 8088
      
Resources:
# 创建一个VPC
  MyTestVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      Tags:
       - Key: Name
         Value: MyTestVpc

# 创建IGW并且关联到VPC
  MyTestIgw:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
        - Key: Name
          Value: my-test-igw

  MyTestAttachIgw:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref MyTestVpc
      InternetGatewayId: !Ref MyTestIgw

# 在VPC内创建子网
  MyTestVpcSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyTestVpc
      CidrBlock: 10.0.0.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      Tags:
      - Key: Name
        Value: my-test-vpc-public-subnet

# VPC内创建路由表并关联到子网,路由表设置默认路由指向IGW
  MyTestPublicRouteTable:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref MyTestVpc
      Tags:
        - Key: Name
          Value: my-test-public-route-table

  MyTestRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref MyTestPublicRouteTable
      SubnetId: !Ref MyTestVpcSubnet

  MyTestInternetRoute:
    Type: "AWS::EC2::Route"
    Properties:
     RouteTableId: !Ref MyTestPublicRouteTable
     DestinationCidrBlock: 0.0.0.0/0
     GatewayId: !Ref MyTestIgw

# 在VPC内创建一个安全组
  MyTestVpcSg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SG to test ping
      VpcId:
        Ref: MyTestVpc
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: 0.0.0.0/0
      - IpProtocol: icmp
        FromPort: -1
        ToPort: -1
        CidrIp: 0.0.0.0/0
      - IpProtocol: tcp
        FromPort: !Ref webServerPort  # !Sub ${webServerPort}
        ToPort: !Ref webServerPort
        CidrIp: 0.0.0.0/0

# 创建一个EC2实例
  MyTestEc2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", HVM64]
      KeyName: !Ref myKeyPair     # MyCN-CloudFormation-Test-Key
      InstanceType: !FindInMap [EnvironmentType, !Ref Environment, instanceType]
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - Ref: MyTestVpcSg
          SubnetId:  !Ref MyTestVpcSubnet
      UserData:
        Fn::Base64:
          !Sub |
          #!/bin/bash
          yum update -y
          yum install -y httpd
          sed -i.bak 's/Listen 80/Listen ${webServerPort}/g' /etc/httpd/conf/httpd.conf
          echo "<h2>Hello World from $(hostname -f)</h2>" > /var/www/html/index.html
          systemctl start httpd.service
          systemctl enable httpd.service

Outputs:
  MyTestEc2InstanceEip:
    Description: MyTestEc2InstanceEip
    Value: !GetAtt MyTestEc2Instance.PublicIp
    Export:
      Name: MyTestEc2InstanceEip
  WebServerSubUrl:
    Description: WebServerSubUrl
    Value: !Sub
      - "http://${MyTestEc2InstanceEip}:${webServerPort}"
      - {MyTestEc2InstanceEip: !GetAtt MyTestEc2Instance.PublicIp}
    Export:
      Name: WebServerSubUrl
  WebServerJoinUrl:
    Description: WebServerJoinUrl
    Value: !Join
      - ''
      - - 'http://'
        - !GetAtt
          - MyTestEc2Instance
          - PublicIp
        - ':'
        - !Sub ${webServerPort}
    Export:
      Name: WebServerJoinUrl

可以通过【参数】查看用户堆栈的输入

image-20220401192853510

通过【输出】查看Outputs

image-20220401192903030

打开Web服务器的URL

image-20220401153720795

七、参考文档