本章使用CloudFormation來實現(xiàn)自定義域名并且使用AWS的SSL證書饱狂。同時利用AWS的CloudFront(AWS的CDN服務(wù))讓API網(wǎng)關(guān)實現(xiàn)多點接入过咬,讓用戶在最近的CDN節(jié)點連接,從而加快用戶和API之間的通訊速度查乒。
- 配置CloudFront
- 配置自定義域名
- 關(guān)聯(lián)SSL證書
工程說明
- 工程目錄結(jié)構(gòu)及java,build.gradle文件與AWS Lambda教程-自動部署-5 差不多唯一區(qū)別是build.gradle中將us-east-2改成us-east-1魂毁,本章主要是 cloudformation.tempalte 增加幾段json塊僚祷。
- 這邊使用自定義域名尺锚,并且使用ACM證書(AWS Certificate Manager (ACM) )目前支持的地區(qū)僅us-east-1(弗吉尼亞北部)
cloudformation.tempalte中AWS組件關(guān)聯(lián)關(guān)系,完整配置筆記的最后部分讶凉。
1. 配置CloudFront(CDN)
CloudFront都是例行公事的配置染乌,這里主要有HTTP的版本,原站信息懂讯,緩存方式荷憋,是否支持壓縮,被允許的HttpMethod類型褐望,具體Forward信息等勒庄,詳細看以下配置及具體備注串前。
"CloudformationDistribution": {
//CDN配置分發(fā),它告知CloudFront 從何處傳輸內(nèi)容实蔽,并如何跟蹤和管理內(nèi)容傳輸?shù)脑敿毿畔ⅰ? "Type": "AWS::CloudFront::Distribution",
"Properties": {
"Enabled": "true", //啟用該資源
"HttpVersion": "http2", //支持版本
//此分配的源信息的復雜類型荡碾。用于描述CloudFront從中獲取文件的S3 存儲桶、
//HTTP服務(wù)器局装、或其他服務(wù)器玩荠。
"Origins": [
{
"DomainName": { //允許開發(fā)者使用內(nèi)建函數(shù)Fn::Sub和其他資源變量合成域名
"Fn::Sub": "${RestApi}.execute-api.${AWS::Region}.amazonaws.com"
},
"OriginPath": "/production", //源中的目錄請求內(nèi)容
"Id": "APIGATEWAY", //源或源組的唯一標識符。
"CustomOriginConfig": { //配置為網(wǎng)站終端節(jié)點的自定義源或S3存儲桶
"OriginProtocolPolicy": "https-only" //要應(yīng)用至源的源協(xié)議策略
}
}
],
//描述緩存行為
"DefaultCacheBehavior": {
//當請求使用默認緩存行為時贼邓,CloudFront將請求路由到的源的ID值
"TargetOriginId": "APIGATEWAY",
"Compress": true, //自動壓縮此緩存行為的某些文件
"AllowedMethods": [ //被允許的方法
"DELETE",
"GET",
"HEAD",
"OPTIONS",
"PATCH",
"POST",
"PUT"
],
"ForwardedValues": { //處理查詢字符串阶冈、Cookie 和 HTTP 標頭
//如果為 QueryString 指定 true,并且沒有為 QueryStringCacheKeys
//指定任何值塑径,CloudFront 會將所有查詢字符串參數(shù)轉(zhuǎn)發(fā)到來源女坑,
//并基于所有查詢字符串參數(shù)進行緩存。根據(jù)擁有的查詢字符串參數(shù)的個數(shù)和值统舀,
//這可能對性能產(chǎn)生不利影響匆骗,因為 CloudFront 必須將更多的請求轉(zhuǎn)發(fā)到源。
"QueryString": "true",
"Cookies": { //Cookie 轉(zhuǎn)發(fā)到源
"Forward": "none" //指定希望將哪些 Cookie 轉(zhuǎn)發(fā)到此緩存行為的來源
},
//轉(zhuǎn)發(fā)到此緩存行為的源的 Headers(如有)誉简。對于您指定的標頭碉就,
//CloudFront 還將緩存基于查看器請求中的標頭值的指定對象的各個版本。
"Headers": [
"Accept",
"Content-Type",
"Authorization"
]
},
"DefaultTTL": 0, //TTL值
"MaxTTL": 0, //保留的最長時間
"MinTTL": 0, //如配置為將所有標頭轉(zhuǎn)發(fā)到源則必須為MinTTL指定0闷串。
//當請求與 TargetOriginId 中的路徑模式匹配時瓮钥,
//查看器可用于訪問 PathPattern 指定的來源中的文件的協(xié)議
//redirect-to-https如果查看器提交HTTP請求,則CloudFront將向查看器返回
//HTTP 狀態(tài)代碼 301(永久移動)以及 HTTPS URL烹吵。然后碉熄,查看器會使用新的
//URL 重新提交請求。
"ViewerProtocolPolicy": "redirect-to-https"
}
}
}
}
以上配置好肋拔,我們可以部署(./gradlew deploy), 可以登陸CloudFront的控制臺
锈津,獲取域名(d3se3kgs51ey11.cloudfront.net),可以dig測試下凉蜂,我們的域名是否已經(jīng)全球解析琼梆。第一個是國內(nèi)dig的結(jié)果,域名解析到日本IP窿吩。第二個代理到美國域名解析的是美國IP茎杂。可以看出域名已經(jīng)成功解析到各個區(qū)域爆存。我們訪問:https://d3se3kgs51ey11.cloudfront.net/test?value=hello+world蛉顽,(d3se3kgs51ey11.cloudfront.net替換成你自己在CloudFront中域名)會相對會快一點點蝗砾。
dig域名的結(jié)果:
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內(nèi)容的復雜度曾棕,同時該域名沒有配置郵箱,也不方面在申請SSL證書時通過郵件認證菜循。
1)配置域名NS記錄
進入AWS Route 53 管理頁面https://console.aws.amazon.com/route53/home翘地,點擊左側(cè)菜單“托管區(qū)域”。
重點:這邊我們需要保存下癌幕, 托管區(qū)域:Z09377931HZWDHZB7ST9N衙耕,在后續(xù)配置中需要使用到。
點擊域名勺远,可以看到該域名的NS橙喘,SOA記錄。
在自己的域名解析管理中增加NS記錄
配置生效后胶逢,我們?yōu)楸U舷乱徊巾樌瓿商梗炞C下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中創(chuàng)建記錄” 這時會自動創(chuàng)建一個NS記錄,等待一會兒域名狀態(tài)從“等待審核” 變成 “已頒發(fā)”半沽。
成功之后身诺,我們需要記錄下ACM的ARN記錄,如圖:
接下來我們需要繼續(xù)在cloudformation.template文件添加配置域名的A記錄抄囚。
"DNSRecord": {
//可選注釋霉赡、要更改的托管區(qū)域的名稱和 ID,以及要創(chuàng)建的記錄的值
"Type": "AWS::Route53::RecordSetGroup",
"Properties": {
"Comment": "Z09377931HZWDHZB7ST9N在Route53上創(chuàng)建托管區(qū)域",
////要在其中創(chuàng)建記錄的托管區(qū)域的ID幔托,在“配置域名NS記錄”中重點說明過
"HostedZoneId": "Z09377931HZWDHZB7ST9N",
"RecordSets": [ //一條記錄的信息
{
"Name": {
"Ref": "DomainName"
},
"Type": "A", //DNS記錄類型
//僅限別名記錄:有關(guān)您要將流量路由到的 AWS 資源
//例如 CloudFront 分配或 Amazon S3 存儲桶的信息穴亏。
"AliasTarget": { //CloudFront 分配
//CloudFront分配,指定Z2FDTNDATAQYW2。
//在創(chuàng)建將流量路由到CloudFront 分配的別名記錄時重挑,它始終是托管區(qū)域ID嗓化。
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": {
"Fn::GetAtt": [
"CloudformationDistribution",
"DomainName"
]
}
}
}
]
}
}
Alias(別名)是Route53提供的強大功能之一,相比CName記錄谬哀,別名記錄可以直接指向AWS資源刺覆,例如ELB,CloudFront史煎。別名在DNS中沒有對應(yīng)的概念谦屑,使用別名免費驳糯,而CName是付費服務(wù)。另外一個有點是Alias比CName少一步獲取最終IP地址氢橙,減少解析的負擔酝枢,繼續(xù)發(fā)布工程(./gradlew deploy)發(fā)布成功后,我dig配置的域名可以發(fā)現(xiàn)解析的A記錄多很多悍手,到此自定義域名配置成功帘睦。但是使用http訪問http://serverless.kkkkkk.com//test?value=hello+world,將放回403信息坦康,提醒:The request could not be satisfied. 接下來我們將繼續(xù)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. 關(guān)聯(lián)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"
},
執(zhí)行
~/.gradlew deploy
發(fā)布成功后赤兴,我們訪問API就變成:https://serverless.kkkkkk.com/test?value=hello+world
現(xiàn)在這個API使用自定義域名且配置SSL,但是還沒有權(quán)限控制隧哮,后續(xù)我們需要對該方法進行權(quán)限控制桶良。
發(fā)布異常一
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數(shù)名名的正確寫法AcmCertificateArn,這說明CloudFormtion嚴格區(qū)分大小寫沮翔。
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上創(chuàng)建托管區(qū)域",
"HostedZoneId": "Z09377931HZWDHZB7ST9N",
"RecordSets": [
{
"Name": {
"Ref": "DomainName"
},
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": {
"Fn::GetAtt": [
"CloudformationDistribution",
"DomainName"
]
}
}
}
]
}
}
}
}