AWS CloudFormation 系列--(2)常用函数及字段
前面CloudFormation的快速入门,基本了解了如何使用这个服务来创建AWS资源。但是前面写的代码基本只能自己看看,没法分享给别人使用,比如代码里面明确写了EC2的密钥,别人账号里面肯定没有这个密钥,应该让使用者自己选择密钥信息。
所以需要创建更加灵活的CloudFormation模板,这里会介绍一些常见的字段和函数,了解了这些之后,基本上能看懂网络上大部分的CloudFormation代码。
这篇文章知识点比较多,我会先追个介绍,最后会通过一个实验把这些知识点整合起来。下面这个实验环境,和之前快速入门是一样的,但是会通过一些函数和字段,让这个模板更加灵活。
一、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 properties、outputs、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
在创建堆栈时,需要填写参数信息。
四、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
可以通过【参数】查看用户堆栈的输入
通过【输出】查看Outputs
打开Web服务器的URL
七、参考文档
- [1] 内置函数参考: https://docs.aws.amazon.com/zh_cn/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html
- [2] AWS::EC2::EIP: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-eip.html
- [3] AWS CloudFormation 伪参数: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html
- [4] AWS Linux AMI 虚拟化类型: https://docs.aws.amazon.com/zh_cn/AWSEC2/latest/UserGuide/virtualization_types.html