logo头像
Snippet 博客主题

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

B站视频链接:https://www.bilibili.com/video/BV1Pe411L7zL/?spm_id_from=333.999.list.card_archive.click
微信公众号:自刘地
自刘地

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

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

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

image-20220918225135044

一、DependsOn属性

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

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

1
2
3
4
5
6
CreationPolicy attribute
DeletionPolicy attribute
DependsOn attribute
Metadata attribute
UpdatePolicy attribute
UpdateReplacePolicy attribute

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。

1
2
3
4
5
6
# IGW 关联VPC
MyTestAttachIgw:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
VpcId: !Ref MyTestVpc
InternetGatewayId: !Ref MyTestIgw

二、常用内部函数

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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。

完整函数名称的语法:

1
Ref: logicalName

短格式的语法:

1
!Ref logicalName

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

1
2
3
4
5
6
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有哪些属性值。

完整函数名称的语法:

1
Fn::GetAtt: [ logicalNameOfResource, attributeName ]

短格式的语法:

1
!GetAtt logicalNameOfResource.attributeName

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

1
2
3
4
5
6
Outputs:
MyTestEc2InstanceEip:
Description: MyTestEc2InstanceEip
Value: !GetAtt MyTestEc2Instance.PublicIp
Export:
Name: MyTestEc2InstanceEip

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

1
2
3
4
5
6
7
Outputs:
MyTestEc2InstanceEip:
Description: MyTestEc2InstanceEip
Value:
Fn::Getatt: [ MyTestEc2Instance, PublicIp ]
Export:
Name: MyTestEc2InstanceEip

Fn::Select 函数

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

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

1
Fn::Select: [ index, listOfObjects ] 

短格式的语法:

1
!Select [ index, listOfObjects ]

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

1
2
3
4
5
6
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函数来选择单个对象,例如这里选择返回结果中的第一个可用区。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
# 在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 函数内部使用返回字符串的任意函数。

完整函数名称的语法:

1
Fn::Base64: valueToEncode

短格式的语法:

1
!Base64 valueToEncode

Fn::Sub 函数

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

完整函数名称的语法:

1
Fn::Sub: String

短格式的语法:

1
!Sub String

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建一个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

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

变量映射

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

1
2
3
4
!Sub
- String
- Var1Name: Var1Value
Var2Name: Var2Value

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

1
2
3
4
5
6
7
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函数来获取。

1
2
3
4
5
6
7
8
9
10
11
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中选择,后续安全组和输出都可以调用这个端口信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
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时,调用参数定义的密钥对信息。

1
2
3
4
5
# 创建一个EC2实例
MyTestEc2Instance:
Type: AWS::EC2::Instance
Properties:
KeyName: !Ref myKeyPair

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

1
2
3
4
5
6
7
# 在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函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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]

1
2
3
4
5
6
7
8
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信息,模板将具有更好的灵活性。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
# 创建一个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信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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

七、参考文档