用AWS部署一个无服务架构的个人网站

在这篇文章里我想介绍下怎样利用AWS(hjlouyoujuqi360com)部署一个无服务架构的个人网站。这个个人网站将具备以下特点:

  • 包含前端和后端;
  • 基本上以静态文件为主,或者主要的计算都在前端(比如React应用);
  • 与后台通过API通信,但数量非常少;
  • 后台不需要太大内存或CPU(wwwya-jucom比如一个简单的网页计数器,每次请求只需要访问一次数据库)。

服务将部署到以下域名上(这里用的都是假想的域名):

  • API服务:
  • 前端:

这里用了HTTPS,因为各大浏览器早已开始将HTTP协议标记为不安全协议了。为了保证安全,HTTPS是必要的,后面会介绍如何设置证书等。

整个网站将使用以下的AWS服务:

  • Lambda + API Gateway + S3,用于跑API服务器;
  • DynamoDB,数据存储;
  • S3,静态网站;
  • Cloudfront,分布式CDN,用作静态网站和API的前端;
  • 后台不需要太大内存或CPU(比如一个简单的网页计数器,每次请求只需要访问一次数据库)。

网站生成证书。

至于API服务器的开发部署,我们采用Python + Flaskwwwya-jucom

的组合开发服务,然后用Zappa(https://github.com/Miserlou/Zappa)作为无服务器部署工具。

设置AWS环境

首先需要设置AWS环境,以便从代码和zappa中访问AWS。需要两个步骤:

  • 创建AWS用户,用于程序访问;
  • 设置本地环境,使代码使用AWS用户。

创建AWS用户

登录到AWS中,选择“IAM”服务来管理用户。

创建一个名为“myservice-admin”的用户(或者任何你喜欢的用户名),勾选“Programmatic access”选项。

在下一步中,点击“Attach existing policies directly”按钮,然后将“AdministratorAccess”添加到该用户。

从安全的角度来说这种做法并不好。不过出于演示的目的,本文不再详述怎样找出部署无服务架构所需的权限了。

点击“Next”按钮,最后点击“Create User”按钮,myservice-admin(wwwya-jucom)

用户就建好了。注意在创建成功的那个画面上会显示Access Key ID和Secret access key两个值。务必要将这两个复制保存下来,稍后要用它们来设置本地环境。

这个画面是唯一能看到Secret access key的地方!如果你忘了复制就关闭了页面,那就只能去用户的详细画面去生成新的access key和secret了。

设置本地AWS环境

为了在本地使用AWS,我们需要创建本地环境。

首先安装awscli工具,用它来帮我们配置环境:

1$ sudo apt install awscli

安装结束后,就可以使用aws configure命令进行设置:

1$ aws configure
2AWS Access Key ID [None]: ******
3AWS Secret Access Key [None]: ******
4Default region name [None]: us-east-1
5Default output format [None]: json

这里需要输入上一步保存下来的Access Key ID和Secret Access Key值。至于区域,我用的是us-east-1。其他区域应该也可以,但如果你要像我一样使用CloudFront(wwwbeigefushicom)的话,其他区域可能会有一些麻烦。

在DynamoDB中创建表

我们的后台API要实现一个计数器。为了保存计数器的数值,我们需要使用DynamoDB。DynamoDB是AWS提供的一个键值数据库。首先我们需要在DynamoDB中建一个表,并设置好我们需要的计数器初始值。

在AWS控制台中选择DynamoDB服务,然后点击“Create Table”按钮。在“Create DynamoDB table”画面,在Table name中填写myservice-dev,Primary key字段填写id,然后点击Create Table按钮。

几秒钟之后表就建好了。选择刚刚建好的表,然后在右侧选择Items选项卡,单击Create item按钮创建一个项目,项目内容为id='counter'及counter_value=0。

创建值时需要点击左侧的加号按钮才能添加counter_value属性,而且别忘了把counter_value属性的类型设置为Number。

创建API服务

接下来我们要建立API服务。(wwwbeigefushicom)

这个API将提供一个计数器API,每次调用都会将计数器的值加一。计数器值保存在DynamoDB中。API的endpoint如下:

  • POST /counter/increase:增加计数器的值,并返回计数器值;
  • GET /counter:返回计数器值。

用Python和Flask编写API服务

首先我们要创建Python虚拟环境,并安装必要的包:

1$ mkdir myservice && cd myservice
2$ python3 -m venv .env
3$ source .env/bin/active
4(.env)$ pip install flask boto3 simplejson

Flask是Web框架,boto3是访问DynamoDB必须的包。simplejson可以解决一些JSON转换时遇到的问题。接下来创建myservice.py,内容如下:

 1import boto3
 2from flask import Flask, jsonify
 3app = Flask(__name__)
 4# Initialize dynamodb access
 5dynamodb = boto3.resource('dynamodb')
 6db = dynamodb.Table('myservice-dev')
 7@app.route('/counter', methods=['GET'])
 8def counter_get():
 9  res = db.get_item(Key={'id': 'counter'})
10  return jsonify({'counter': res['Item']['counter_value']})
11@app.route('/counter/increase', methods=['POST'])
12def counter_increase():
13  res = db.get_item(Key={'id': 'counter'})
14  value = res['Item']['counter_value'] + 1
15  res = db.update_item(
16    Key={'id': 'counter'},
17    UpdateExpression='set counter_value=:value',
18    ExpressionAttributeValues={':value': value},
19  )
20  return jsonify({'counter': value})

再创建一个run.py,以便在本地测试该服务:

1from myservice import app
2if __name__ == '__main__':
3  app.run(debug=True, host='127.0.0.1', port=8000)

运行服务:

1(.env)$ python run.py

这样就可以在命令行中测试这个服务了(再开一个终端输入下面的命令):

 1$ curl localhost:8000/counter
 2{
 3  "counter": 0
 4}
 5$ curl -X POST localhost:8000/counter/increase
 6{
 7  "counter": 1
 8}
 9$ curl -X POST localhost:8000/counter/increase
10{
11  "counter": 2
12}
13$ curl localhost:8000/counter
14{
15  "counter": 2
16}

我们可以看到计数器的值增加了,说明这个服务可以用了!

将服务部署到Lambda上

要部署API到Lambda上,可以使用Zappa包。Zappa包使得部署微服务变得极其容易。首先安装Zappa:

1(.env)$ pip install zappa

然后执行Zappa init命令初始化Zappa环境。它会问你几个问题,但基本上可以使用默认值来回答:

 1(.env)$ zappa init
 2...
 3What do you want to call this environment (default 'dev'): 
 4...
 5What do you want to call your bucket? (default 'zappa-ab7dd70x5'):
 6It looks like this is a Flask application.
 7What's the modular path to your app's function?
 8This will likely be something like 'your_module.app'.
 9We discovered: myservice.app
10Where is your app's function? (default 'myservice.app'): 
11...
12Would you like to deploy this application globally? (default 'n') [y/n/(p)rimary]:
13Okay, here's your zappa_settings.json:
14{
15    "dev": {
16        "app_function": "myservice.app",
17        "aws_region": "us-east-1",
18        "profile_name": "default",
19        "project_name": "myservice",
20        "runtime": "python3.6",
21        "s3_bucket": "zappa-ab7dd70x5"
22    }
23}
24Does this look okay? (default 'y') [y/n]: 
25...

初始化完成后,在目录下会生成一个zappa_settings.json文件。然后就可以部署服务了:

1(.env)$ zappa deploy dev
2Calling deploy for stage dev..
3...
4Deployment complete!: https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev

现在我们的服务就部署成功了。可以用下面的Curl命令测试,也可以打开浏览器测试GET的API:

1(.env)$ curl https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev/counter
2{"counter":2}
3(.env)$ curl -X POST https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev/counter/increase
4{"counter":3}
5(.env)$ curl https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev/counter
6{"counter":3}

绑定自定义域名

不过上面的API服务还有一个小问题。自动生成的API endpoint是2ks1n5nrxh.execute-api.us-east-1.amazonaws.com,很难记也不好用。不过我们可以很容易地给它绑定一个自定义域名。

我们的自定义域名是https://myservice-api.example.com。为了使用HTTPS,我们需要现申请一个证书。AWS的Certificate Manager服务提供免费的证书。生成证书之后就可以在AWS的API Gateway里自定义域名了。

申请证书

从AWS控制台切换到ACM服务(服务名称叫Certificate Manager,但敲ACM就能搜索到)。点击Request a certificate按钮,然后选择Request a public certificate选项。选择公开的证书就是免费的。

下个画面要输入证书的域名。这里我们申请了*.example.com,这样证书就能用在example.com下的所有子域名上。以后我们给前台的myfrontend.example.com添加https时就不用再申请证书了。

下一步,我们需要向AWS证明我们拥有这个域名。我这个域名是从Google Domains申请的,所以我在这里选择DNS validation。点击Review按钮然后点击Confirm and Request。

现在证书请求已经生成了,AWS会显示一个验证画面,上面写明了怎样验证该域名:

根据说明,我们需要在域名下添加一条CNAME记录。由于我的域名是从Google Domains申请的,我就打开Google Domains,找到域名example.com,然后添加上面指定的CNAME:

这里在Name栏中只添加了_2adee19a0967c7dd5014b81110387d11字符串,去掉了后面的.example.com部分,否则.example.com就重复了。

接下来要等待大约10分钟,AWS Certificate Manager就会去验证域名了。验证成功后,Status栏会显示“Issued”。

现在证书已经申请好了,我们可以继续去给API绑定域名。

为API服务绑定自定义域名

切换到API Gateway服务。从左侧的APIs一栏可以看到,Zappa已经帮我们建好了myservice-dev服务。

从左侧点击“Custom Domain Names”,然后点击右侧的Create Custom Domain Name按钮,填写必要的字段。

这里我希望API使用CloudFront服务,这样能在全世界都达到最理想的访问速度,因此我选择了Edge Optimized。如果不使用CloudFront,你可以选择Regional。

点击下面的“Add mapping”链接,然后选择myservice-dev作为Destination,再从最右边的方框中选择dev。这样做的目的是访问API时无需在URL中指定环境名称dev。Path字段留空。

点击Save按钮后,这个自定义域名绑定就建好了。实际上要等待大约40分钟左右域名绑定才能正常使用,不过我们可以利用这段时间去配置DNS。

从上面的图中可以看出,API服务的实际域名为dgt9opldriaup.cloudfront.net(因为我选择了CloudFront服务)。因此需要在DNS中添加一条CNAME,将myservice-api.example.com指向上面的CloudFront子域名dgt9opldriaup.cloudfront.net。

回到Google Domains添加这条CNAME:

该步骤完成后,等待大约40分钟,等API Gateway中的“Initializing...”字样消失后,自定义域名就可以使用了。

1(.env)$ curl https://myservice-api.example.com/counter
2{"counter":3}
3(.env)$ curl -X POST https://myservice-api.example.com/counter/increase
4{"counter":4}
5(.env)$ curl https://myservice-api.example.com/counter
6{"counter":4}

前端的静态网站

接下来我们要给这个API服务创建一个前端。作为例子,这里只创建一个非常简单的页面,它能调用/counter/increase。

前端编程

先建一个目录myfrontend:

1$ mkdir myfrontend && cd myfrontend

然后建一个简单的HTML文件index.html:

 1<html>
 2<body>
 3  <h1>Welcome to my homepage!</h1>
 4  <p>Counter: <span id="counter"></span></p>
 5  <button id="increase">Increase Counter</button>
 6  <script>
 7    const setCounter = (counter_value) => {
 8      document.querySelector('#counter').innerHTML = counter_value;
 9    };
10    const api = 'https://myservice-api.example.com';
11    fetch(api + '/counter')
12      .then(res => res.json())
13      .then(result => setCounter(result.counter));
14document.querySelector('#increase')
15      .addEventListener('click', () => {
16        fetch(api + '/counter/increase', { method: 'POST' })
17          .then(res => res.json())
18          .then(result => setCounter(result.counter));
19        }
20      );
21  </script>
22</body>
23</html>

将前端发布到S3

我们可以把前端部署到S3上。首先需要建一个桶,桶的名字就是域名。

从AWS控制台中切换到S3服务。由于我们要建立的静态网站域名为myfrontend.example.com,我们要建一个同名的桶。点击Create Bucket按钮,填入桶的名称,然后点击Next直到桶建好。

接下来要把我们的网站放到这个桶中。打开该桶,选择Properties选项卡,然后选择Static Web Hosting。在弹出的对话框中选择Use this bucket to host a website,在Index document字段中输入index.html。点击Save关闭对话框。

上面显示了“Endpoint”链接,我们稍后会用这个URL测试静态网站。

最后一件事就是让这个桶允许公开访问。我们需要添加一个桶策略来实现这一点。打开这个桶,选择Permissions选项卡,然后点击Bucket Policy按钮。

输入下面的内容作为策略,然后点击Save按钮(别忘了把myservice.example.com换成你自己的域名):

 1{
 2    "Version": "2012-10-17",
 3    "Statement": [
 4        {
 5            "Sid": "PublicReadGetObject",
 6            "Effect": "Allow",
 7            "Principal": "*",
 8            "Action": "s3:GetObject",
 9            "Resource": "arn:aws:s3:::myfrontend.example.com/*"
10        }
11    ]
12}

保存之后,我们应该可以在Bucket Policy按钮上以及Permissions选项卡上看到橙色的“public”字样,表明我们的桶是可以被公开访问的。

这样桶就建好了,但里面还是空的,现在需要把网站的内容上传到这个桶中。首先进入刚才建好的myfrontend目录中,然后输入下面的命令:

1# Make sure you are in the `myfrontend` directory...
2$ aws s3 sync . s3://myfrontend.example.com

上面的命令会把当前目录下(注意命令中的那个点 . )的所有文件都上传到S3中。

现在就完成了!在浏览器中打开下面的地址就可以看到网站内容了(地址就是前面创建桶时显示的Endpoint的URL):

http://myfrontend.example.com.s3-website-us-east-1.amazonaws.com/

嗯?貌似不太对。计数器没有显示任何值呢?

而且似乎有JavaScript错误。打开浏览器的控制台就能看到以下错误:

Failed to load https://myservice-api.example.com/counter: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://myfrontend.example.com.s3-website-us-east-1.amazonaws.com' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

显然,我们需要设置CORS header(https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)才能让这个脚本工作,因为后台API被放到了另一个域名上(myservice-api.example.com和myfront.example.com不是同一个域名)。

不过由于我们还要给前端绑定自定义域名,绑定后URL会发生变化,所以这里先放一放,等一会儿绑定好域名之后再来考虑CORS的问题。

给静态网站设置CloudFront和自定义域名

最后一步就是给前端设置CloudFront并绑定自定义域名。前面我们已经申请了*.example.com的证书,所以这一步就很容易了。

从AWS控制台中切换到CloudFront服务。点击Create Distribution按钮,然后点击Web里的Start按钮。

在“Create Distribution”画面上,我们需要填写以下信息:

  • 点击Origin domain name输入框,选择刚才的S3桶myfrontend.example.com.s3.amazonaws.com;
  • 将Viewer Protocol Policy改成Redirect HTTP to HTTPS,以强制https访问;
  • 在Alternate Domain Names输入框中输入自定义域名。这里我们输入myfrontend.example.com;
  • 向下滚动到SSL Certificate部分,选择“Custom SSL Certificate”,然后选择之前的*.example.com证书;
  • 将Default Root Object设置成index.html。

创建好distribution后,就可以在distribution列表中看到CloudFront的域名了。

上面的状态还是“In Progress”,我们可以利用这段时间去设置DNS。跟前面类似,去Google Domains里添加一个CNAME:

等到CloudFront里的distribution的状态变成Deployed之后,就可以打开浏览器访问myfrontend.example.com。应该能看到我们的静态网站了!

解决CORS问题

现在唯一的问题就是CORS了。CORS是由于前端和后台的域名不一致导致的,为了让前端能访问后台API,我们需要给后台添加CORS支持。

回到API的代码目录(myservice),激活Python环境。然后安装flask_cors包:

1$ cd myservice
2$ source .env/bin/activate
3(.env)$ pip install flask_cors

然后编辑myservice.py,添加以下几行(3和6):

1import boto3
2from flask import Flask, jsonify
3from flask_cors import CORS
4
5app = Flask(__name__)
6CORS(app, origins=['https://myfrontend.example.com'])

最后发布到AWS Lambda:

试着刷新下浏览器。现在就能看到计数器显示了正确的值。点击“Increase Counter”按钮也能增加计数器的值了。

总结

这篇文章介绍了创建一个简单的无服务器服务所需的多种AWS服务。如果你对AWS不熟悉,你可能会觉得我们用到了太多的服务,但其实绝大部分AWS服务都是一次性的,一旦设置好之后就不用再管了。以后的开发中用得上的只有zappa update和aws s3 sync两条命令而已。

而且至少,这种方法要比自己设置一台VPS、安装Web服务器再写个Jenkins脚本做持续部署要方便多了。

作为总结,下面是这篇文章的一些重点:

  • Lambda可以运行简单的服务,服务可以通过API Gateway暴露成HTTP服务;
  • 如果要用Python写无服务器服务,那么Zappa是个非常方便的工具;
  • S3桶可以用作静态网站使用;
  • 要想使用HTTPS,可以通过AWS ACM申请证书;
  • API Gateway和CloudFront都支持自定义域名。(wwwbeigefushicom)

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

Hadoop和Spark

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏娱乐心理测试

微信小程序发送模版消息(事例)

4747
来自专栏较真的前端

[译] 调试 RxJS 第2部分: 日志篇

1834
来自专栏耕耘实录

使用awk命令批量删除指定范围的账号

版权声明:本文为耕耘实录原创文章,各大自媒体平台同步更新。欢迎转载,转载请注明出处,谢谢

893
来自专栏Golang语言社区

Go语言异步服务器框架原理和实现

Go语言类库中,有两个官方的服务器框架,一个HTTP,一个是RPC。使用这个两个框架,已经能解决大部分的问题,但是,也有一些需求,这些框架是不够的,这篇文章,我...

5487
来自专栏24K纯开源

QT程序在发布的时候应注意的地方

---恢复内容开始---     我们用QT开发好的应用程序,如果要发布到其他计算机上运行怎么办呢?我们在用VC编程时,单独运行编译好的可执行文件时,经常会发现...

2695
来自专栏酷玩时刻

IDE中显示 *.properties 为中文

之前做过几个开源项目(极速开发微信公众号weixin_guide以及IJPay 让支付触手可及),Demo中有涉及到一些配置文件。有些同学下载了之后反馈说有乱码...

1012
来自专栏知晓程序

开发 | 无需后端编码,10 分钟教你实现一个朋友圈小程序

虽然目标功能的业务逻辑并不复杂,但其背后需要一套靠谱的权限控制系统,也意味着需要一个完整的后端服务系统来支持运行。

1844
来自专栏iOS 开发杂谈

CornerStone的使用

对于我们程序员来说,不管你是大神,还是小鱼小虾,进入公司之后,都用过源码管理工具,不然你就不是一个合格的程序员,现在各个公司用于源码管理工具通常有下面两种:

2871
来自专栏魏艾斯博客www.vpsss.net

网站目录/绝对路径/相对路径-零基础搭建wordpress教程

网站基本知识包括很多方面,网站结构、网页路径、域名、服务器、建站等等,我们已经写过域名和新手如何选择服务器了,正好最近遇到群友对网站基本知识有些疑问,魏艾斯博客...

3851
来自专栏即时通讯技术

SSE技术详解:一种全新的HTML5服务器推送事件技术

一般来说,Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询、Comet技术、We...

1544

扫码关注云+社区

领取腾讯云代金券