本章使用CloudFormation来实现自定义域名并且使用AWS的SSL证书。同时利用AWS的CloudFront(AWS的CDN服务)让API网关实现多点接入,让用户在最近的CDN节点连接,从而加快用户和API之间的通讯速度。
- 配置CloudFront
- 配置自定义域名
- 关联SSL证书
工程说明
- 工程目录结构及java,build.gradle文件与AWS Lambda教程-自动部署-5 差不多唯一区别是build.gradle中将us-east-2改成us-east-1,本章主要是 cloudformation.tempalte 增加几段json块。
- 这边使用自定义域名,并且使用ACM证书(AWS Certificate Manager (ACM) )目前支持的地区仅us-east-1(弗吉尼亚北部)
cloudformation.tempalte中AWS组件关联关系,完整配置笔记的最后部分。
1. 配置CloudFront(CDN)
CloudFront都是例行公事的配置,这里主要有HTTP的版本,原站信息,缓存方式,是否支持压缩,被允许的HttpMethod类型,具体Forward信息等,详细看以下配置及具体备注。
"CloudformationDistribution": {
//CDN配置分发,它告知CloudFront 从何处传输内容,并如何跟踪和管理内容传输的详细信息。
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"Enabled": "true", //启用该资源
"HttpVersion": "http2", //支持版本
//此分配的源信息的复杂类型。用于描述CloudFront从中获取文件的S3 存储桶、
//HTTP服务器、或其他服务器。
"Origins": [
{
"DomainName": { //允许开发者使用内建函数Fn::Sub和其他资源变量合成域名
"Fn::Sub": "${RestApi}.execute-api.${AWS::Region}.amazonaws.com"
},
"OriginPath": "/production", //源中的目录请求内容
"Id": "APIGATEWAY", //源或源组的唯一标识符。
"CustomOriginConfig": { //配置为网站终端节点的自定义源或S3存储桶
"OriginProtocolPolicy": "https-only" //要应用至源的源协议策略
}
}
],
//描述缓存行为
"DefaultCacheBehavior": {
//当请求使用默认缓存行为时,CloudFront将请求路由到的源的ID值
"TargetOriginId": "APIGATEWAY",
"Compress": true, //自动压缩此缓存行为的某些文件
"AllowedMethods": [ //被允许的方法
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT"
],
"ForwardedValues": { //处理查询字符串、Cookie 和 HTTP 标头
//如果为 QueryString 指定 true,并且没有为 QueryStringCacheKeys
//指定任何值,CloudFront 会将所有查询字符串参数转发到来源,
//并基于所有查询字符串参数进行缓存。根据拥有的查询字符串参数的个数和值,
//这可能对性能产生不利影响,因为 CloudFront 必须将更多的请求转发到源。
"QueryString": "true",
"Cookies": { //Cookie 转发到源
"Forward": "none" //指定希望将哪些 Cookie 转发到此缓存行为的来源
},
//转发到此缓存行为的源的 Headers(如有)。对于您指定的标头,
//CloudFront 还将缓存基于查看器请求中的标头值的指定对象的各个版本。
"Headers": [
"Accept",
"Content-Type",
"Authorization"
]
},
"DefaultTTL": 0, //TTL值
"MaxTTL": 0, //保留的最长时间
"MinTTL": 0, //如配置为将所有标头转发到源则必须为MinTTL指定0。
//当请求与 TargetOriginId 中的路径模式匹配时,
//查看器可用于访问 PathPattern 指定的来源中的文件的协议
//redirect-to-https如果查看器提交HTTP请求,则CloudFront将向查看器返回
//HTTP 状态代码 301(永久移动)以及 HTTPS URL。然后,查看器会使用新的
//URL 重新提交请求。
"ViewerProtocolPolicy": "redirect-to-https"
}
}
}
}
以上配置好,我们可以部署(./gradlew deploy), 可以登陆CloudFront的控制台
,获取域名(d3se3kgs51ey11.cloudfront.net),可以dig测试下,我们的域名是否已经全球解析。第一个是国内dig的结果,域名解析到日本IP。第二个代理到美国域名解析的是美国IP。可以看出域名已经成功解析到各个区域。我们访问:https://d3se3kgs51ey11.cloudfront.net/test?value=hello+world,(d3se3kgs51ey11.cloudfront.net替换成你自己在CloudFront中域名)会相对会快一点点。
dig域名的结果:
dig d3se3kgs51ey11.cloudfront.net
;; ANSWER SECTION:
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.24
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.84
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.145
d3se3kgs51ey5f.cloudfront.net. 36 IN A 13.225.157.197
dig d3se3kgs51ey11.cloudfront.net
;; ANSWER SECTION:
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.123
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.231
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.49
d3se3kgs51ey5f.cloudfront.net. 300 IN A 13.227.53.61
2. 配置自定义域名
CloudFront配置成功后,我需要手动配置自定义域名的NS记录和SSL证书的认证。这些也可以通过CloudFormation模版自动化,不过这些操作都是一次性的,所有就不增加CloudFormation内容的复杂度,同时该域名没有配置邮箱,也不方面在申请SSL证书时通过邮件认证。
1)配置域名NS记录
进入AWS Route 53 管理页面https://console.aws.amazon.com/route53/home,点击左侧菜单“托管区域”。
重点:这边我们需要保存下, 托管区域:Z09377931HZWDHZB7ST9N,在后续配置中需要使用到。
点击域名,可以看到该域名的NS,SOA记录。
在自己的域名解析管理中增加NS记录
配置生效后,我们为保障下一步顺利完成,验证下NS记录是否生效。
//dig ns 确认解析出来的为刚才配置的ns记录
dig ns serverless.kkkkkk.com
2)申请SSL证书并通过DNS认证
进入AWS Certificate Manger 页面 https://us-east-2.console.aws.amazon.com/acm/home ,点击“请求证书” ,按照提示
步骤 1: 添加域名
步骤 2:选择验证方法 (选择DNS验证)
步骤 3: 添加标签 (可以不操作)
步骤 4:审核并请求
步骤 5:验证
按步骤操作完成,回到“证书管理”页面,查看申请证书的域名,点击“在Route 53中创建记录” 这时会自动创建一个NS记录,等待一会儿域名状态从“等待审核” 变成 “已颁发”。
成功之后,我们需要记录下ACM的ARN记录,如图:
接下来我们需要继续在cloudformation.template文件添加配置域名的A记录。
"DNSRecord": {
//可选注释、要更改的托管区域的名称和 ID,以及要创建的记录的值
"Type": "AWS::Route53::RecordSetGroup",
"Properties": {
"Comment": "Z09377931HZWDHZB7ST9N在Route53上创建托管区域",
////要在其中创建记录的托管区域的ID,在“配置域名NS记录”中重点说明过
"HostedZoneId": "Z09377931HZWDHZB7ST9N",
"RecordSets": [ //一条记录的信息
{
"Name": {
"Ref": "DomainName"
},
"Type": "A", //DNS记录类型
//仅限别名记录:有关您要将流量路由到的 AWS 资源
//例如 CloudFront 分配或 Amazon S3 存储桶的信息。
"AliasTarget": { //CloudFront 分配
//CloudFront分配,指定Z2FDTNDATAQYW2。
//在创建将流量路由到CloudFront 分配的别名记录时,它始终是托管区域ID。
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": {
"Fn::GetAtt": [
"CloudformationDistribution",
"DomainName"
]
}
}
}
]
}
}
Alias(别名)是Route53提供的强大功能之一,相比CName记录,别名记录可以直接指向AWS资源,例如ELB,CloudFront。别名在DNS中没有对应的概念,使用别名免费,而CName是付费服务。另外一个有点是Alias比CName少一步获取最终IP地址,减少解析的负担,继续发布工程(./gradlew deploy)发布成功后,我dig配置的域名可以发现解析的A记录多很多,到此自定义域名配置成功。但是使用http访问http://serverless.kkkkkk.com//test?value=hello+world,将放回403信息,提醒:The request could not be satisfied. 接下来我们将继续SSL证书配置。
dig serverlessbook.kkkkkk.com
; <<>> DiG 9.10.6 <<>> serverlessbook.kkkkkk.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29231
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0;; QUESTION SECTION:
;serverlessbook.kkkkkk.com. IN A;; ANSWER SECTION:
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.197
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.84
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.24
serverlessbook.kkkkkk.com. 59 IN A 13.225.157.145;; Query time: 2468 msec
;; SERVER: 114.114.114.114#53(114.114.114.114)
;; WHEN: Sun Jun 07 15:33:07 CST 2020
;; MSG SIZE rcvd: 107
3. 关联SSL证书
这里只需要在 AWS::CloudFront::Distribution 的 Properties 增加:
"Aliases": [ //分配的 CNAME(备用域名)
{
"Ref": "DomainName"
}
],
"ViewerCertificate": { // SSL/TLS 配置
//指定 ACM 证书 ARN,必须指定 MinimumProtocolVersion 和 SslSupportMethod 的值。
//使用 Aliases(备用域名或 CNAME),请指定分配接受来自哪些查看器的 HTTPS 连接.
//分为sni-only(免费,到部分浏览器都支持,推荐) 和 vip(付费且需要单独申请)
"SslSupportMethod": "sni-only",
//SSL证书申请中的ARN
"AcmCertificateArn": "arn:aws:acm:us-east-1:083845954160:certificate/0cc193a9-9489-47ce-b7b3-8213a4c434d1"
},
执行
~/.gradlew deploy
发布成功后,我们访问API就变成:https://serverless.kkkkkk.com/test?value=hello+world
现在这个API使用自定义域名且配置SSL,但是还没有权限控制,后续我们需要对该方法进行权限控制。
发布异常一
FAILURE: Build failed with an exception.
- What went wrong:
Execution failed for task ':awsCfnWaitStackComplete'.
Status of stack serverlessbook is UPDATE_ROLLBACK_COMPLETE. It seems to be failed.
查看CloudFormation
]的“事件”提示具体的错误信息:
Property validation failure: [Encountered unsupported properties in {/DistributionConfig/ViewerCertificate}: [ACMCertificateArn]]
这个原因是ACMCertificateArn数名名的正确写法AcmCertificateArn,这说明CloudFormtion严格区分大小写。
cloudformation.tempalte 完整配置
{
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": {
"DeploymentBucket": {
"Type": "String",
"Description": "S3 bucket name where built artifacts are deployed"
},
"ProjectVersion": {
"Type": "String",
"Description": "Project Version"
},
"DeploymentTime": {
"Type": "String",
"Description": "It is a timestamp value which shows the deployment time. Used to rotate sources."
},
"DomainName": {
"Type": "String",
"Description": "Domain Name to serve the application"
}
},
"Resources": {
"DeploymentLambdaRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"Path": "/",
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
],
"Policies": [
{
"PolicyName": "LambdaExecutionPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:PublishVersion",
"apigateway:POST"
],
"Resource": [
"*"
]
}
]
}
}
]
}
},
"DeploymentLambda": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Role": {
"Fn::GetAtt": [
"DeploymentLambdaRole",
"Arn"
]
},
"Handler": "serverless.handler",
"Runtime": "nodejs12.x",
"Code": {
"S3Bucket": {
"Fn::Sub": "serverless-arch-${AWS::Region}"
},
"S3Key": "serverless.zip"
}
}
},
"ApiGatewayCloudwatchRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"apigateway.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
},
"Path": "/",
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
]
}
},
"ApiGatewayAccount": {
"Type": "AWS::ApiGateway::Account",
"Properties": {
"CloudWatchRoleArn": {
"Fn::GetAtt": [
"ApiGatewayCloudwatchRole",
"Arn"
]
}
}
},
"RestApi": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Name": {
"Ref": "AWS::StackName"
}
}
},
"LambdaExecutionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"Path": "/",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
]
}
},
"LambdaCustomPolicy": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "LambdaCustomPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBuckets"
],
"Resource": "*"
}
]
},
"Roles": [
{
"Ref": "LambdaExecutionRole"
}
]
}
},
"TestLambda": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Handler": "com.serverlessbook.lambda.test.Handler",
"Runtime": "java8",
"Timeout": "300",
"MemorySize": "1024",
"Description": "Test lambda",
"Role": {
"Fn::GetAtt": [
"LambdaExecutionRole",
"Arn"
]
},
"Code": {
"S3Bucket": {
"Ref": "DeploymentBucket"
},
"S3Key": {
"Fn::Sub": "artifacts/lambda-test/${ProjectVersion}/${DeploymentTime}.jar"
}
}
}
},
"TestResource": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"PathPart": "test",
"RestApiId": {
"Ref": "RestApi"
},
"ParentId": {
"Fn::GetAtt": [
"RestApi",
"RootResourceId"
]
}
}
},
"TestGetMethod": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "GET",
"RestApiId": {
"Ref": "RestApi"
},
"ResourceId": {
"Ref": "TestResource"
},
"AuthorizationType": "NONE",
"RequestParameters": {
"method.request.querystring.value": "True",
"method.request.header.Accept": "True"
},
"MethodResponses": [
{
"StatusCode": "200"
}
],
"Integration": {
"Type": "AWS",
"Uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${TestLambda.Arn}/invocations"
},
"IntegrationHttpMethod": "POST",
"RequestParameters": {
"integration.request.querystring.value": "method.request.querystring.value",
"integration.request.header.Accept": "method.request.header.Accept"
},
"RequestTemplates": {
"application/json": "{\"value\":\"$input.params('value')\"}"
},
"PassthroughBehavior": "NEVER",
"IntegrationResponses": [
{
"SelectionPattern": ".*",
"StatusCode": "200"
}
]
}
}
},
"TestLambdaPermission": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Ref": "TestLambda"
},
"Principal": "apigateway.amazonaws.com",
"SourceArn": {
"Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*"
}
}
},
"ApiDeployment": {
"DependsOn": [
"TestGetMethod"
],
"Type": "Custom::ApiDeployment",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"DeploymentLambda",
"Arn"
]
},
"RestApiId": {
"Ref": "RestApi"
},
"StageName": "production",
"DeploymentTime": {
"Ref": "DeploymentTime"
}
}
},
"CloudformationDistribution": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Aliases": [
{
"Ref": "DomainName"
}
],
"ViewerCertificate": {
"SslSupportMethod": "sni-only",
"AcmCertificateArn": "arn:aws:acm:us-east-1:083845954160:certificate/0cc193a9-9489-47ce-b7b3-8213a4c434d1"
},
"Enabled": "true",
"HttpVersion": "http2",
"Origins": [
{
"DomainName": {
"Fn::Sub": "${RestApi}.execute-api.${AWS::Region}.amazonaws.com"
},
"OriginPath": "/production",
"Id": "APIGATEWAY",
"CustomOriginConfig": {
"OriginProtocolPolicy": "https-only"
}
}
],
"DefaultCacheBehavior": {
"TargetOriginId": "APIGATEWAY",
"Compress": true,
"AllowedMethods": [
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT"
],
"ForwardedValues": {
"QueryString": "true",
"Cookies": {
"Forward": "none"
},
"Headers": [
"Accept",
"Content-Type",
"Authorization"
]
},
"DefaultTTL": 0,
"MaxTTL": 0,
"MinTTL": 0,
"ViewerProtocolPolicy": "redirect-to-https"
}
}
}
},
"DNSRecord": {
"Type": "AWS::Route53::RecordSetGroup",
"Properties": {
"Comment": "Z09377931HZWDHZB7ST9N在Route53上创建托管区域",
"HostedZoneId": "Z09377931HZWDHZB7ST9N",
"RecordSets": [
{
"Name": {
"Ref": "DomainName"
},
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": {
"Fn::GetAtt": [
"CloudformationDistribution",
"DomainName"
]
}
}
}
]
}
}
}
}