用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')
 [email protected]('/counter', methods=['GET'])
 8def counter_get():
 9  res = db.get_item(Key={'id': 'counter'})
10  return jsonify({'counter': res['Item']['counter_value']})
[email protected]('/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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章