在 AWS 以 Aurora Serverless 搭建無伺服器 Web 應用程式 part4 — 建立及佈署 Serverless Framework 專案

Luyo
verybuy-dev
Published in
19 min readMay 31, 2019

上一篇中我們將 IAM 設定好,並安裝好 Serverless Framework 及 IAM credential,本文將介紹如何撰寫 Serverless Framework 的核心檔案 serverless.yml 來實現 infrastructure as code,並將專案佈署至 AWS 上。

本文參考:

初始化 Serverless 專案

建立專案

你可以建立一個目錄,底下專門放 serverless 的專案,例如:

$ mkdir serverless-projects

然後切換到該目錄底下:

$ cd serverless-projects

接著下指令來建立新專案,新專案取名為 web-api

$ sls create -t aws-nodejs -p web-api

上面這個指令中的 -t aws-nodejs 表示我們要用 “aws-nodejs” 這個 template。而 -p web-api 表示這個專案名稱叫 “web-api”,framework 會自動幫你產生一個 “web-api” 的目錄並做初始化。

其他常用的 templates 可以參考以下網址,常見的語言如 python、ruby、java、go 等等都找得到:

https://serverless.com/framework/docs/providers/aws/cli-reference/create/

跑完後會自動建立一個 web-api 的新目錄,請切換到這個目錄底下:

$ cd web-api

安裝 npm 套件

先初始化 npm:

$ npm init -y

接著安裝 serverless-offline ,這個套件是用來在 local 端跑 web server 以供開發階段的測試:

$ npm i --save-dev serverless-offline

最後,安裝 mysql2sequelize ,用來做資料庫連線以及 ORM:

$ npm i --save mysql2 sequelize

編輯 serverless.yml

專案目錄中的serverless.yml 等於是整個專案的設定檔,也是實現 infrastructure as code 的核心檔案, framework 會依據這個檔案的內容來做對應的處理。它可以設定的項目非常多,有需要時可參考此文件

請以編輯器開啟 serverless.yml ,一開始會有很多註解,請先把全部內容先清空,然後複製貼上以下的內容:

service: web-apicustom:
defaultStage: local
env: ${file(config/${self:provider.stage}/env.json)}
vpc: ${file(config/${self:provider.stage}/vpc.json)}
serverless-offline:
skipCacheInvalidation: true
stages:
- dev
- prod
provider:
name: aws
runtime: nodejs8.10
stage: ${opt:stage, self:custom.defaultStage}
region: ${self:custom.env.REGION}
timeout: 30
profile: ${opt:profile, self:custom.env.PROFILE} # 新版本已棄用此參數,請改用參數 "iam"
vpc:
securityGroupIds: ${self:custom.vpc.SECURITY_GROUP_IDS}
subnetIds: ${self:custom.vpc.SUBNET_IDS}
environment:
NODE_ENV: ${self:provider.stage}
functions:
hello:
handler: handler.hello
events:
- http:
path: /hello
method: get
cors: true
healthCheck:
handler: handler.healthCheck
events:
- http:
path: /
method: get
cors: true
create:
handler: handler.create
events:
- http:
path: notes
method: post
cors: true
getOne:
handler: handler.getOne
events:
- http:
path: notes/{id}
method: get
cors: true
getAll:
handler: handler.getAll
events:
- http:
path: notes
method: get
cors: true
update:
handler: handler.update
events:
- http:
path: notes/{id}
method: put
cors: true
destroy:
handler: handler.destroy
events:
- http:
path: notes/{id}
method: delete
cors: true
plugins:
- serverless-offline
- serverless-stage-manager

如果你覺得一頭霧水,沒關係,我們一部分一部分來簡單說明。

service

service 這部分就是用來宣告專案的名稱,framework 會依照這個名稱當做前綴,加上後面會提到的 stage 名稱當後綴,組合出一個如 web-api-dev 的名字來當做 AWS CloudFormation 的 stack 名稱。

custom

custom 這區段可以拆成兩部分來看,各有各的作用:

(1) 用來自訂義變數:

  defaultStage: local
env: ${file(config/${self:provider.stage}/env.json)}
vpc: ${file(config/${self:provider.stage}/vpc.json)}

這幾行是自訂義變數,後面的設定會用到它們。其中 ${file(...)} 表示這個設定要讀取括號內的檔名的內容,這個 file()函式支援 json 及 yml 格式,還可以直接指定讀取檔案內指定的字段,如 ${file(...):MY_VAR},當專案越來越大的時候一定會用得到。詳細說明請參考此文件

另外,檔名中的變數 ${self:provider.stage} 則會去抓自己這個 yml 檔中 provider.stage 的設定值。

(2) 用來設定 plugins 的參數:

  serverless-offline:
skipCacheInvalidation: true
stages:
- dev
- prod

這一段就是為了 serverless-offilineserverless-stage-manager這些 plugins 而設定的參數,而這些參數如何使用必須去看各 plugins 的文件。

這邊特別提一下,為什麼 stages 裡面不設定 local 這個環境呢?這是為了避免我們以後在用 sls deploy 指令的時候忘了帶 --stage 參數,那麼 stage 就會吃到預設值 “local”,你的 local 環境就跑上雲端了!安裝 serverless-stage-manager 可以用來控制 sls 指令允許的 stages 有哪些,避免打錯字的時候衍生出一些不必要的麻煩。

上述 plugins 的詳細說明請參見:
serverless-offline plugin: https://www.npmjs.com/package/serverless-offline
serverless-stage-manager: https://www.npmjs.com/package/serverless-stage-manager

provider

這區段可以拆解成以下幾段:

(1) 要用什麼樣的環境變數來建立這個專案:

  name: aws
runtime: nodejs8.10
stage: ${opt:stage, self:custom.defaultStage}
profile: ${opt:profile, self:custom.env.PROFILE}
region: ${self:custom.env.REGION}
timeout: 30

其中 ${opt:stage, self:custom.defaultStage} 表示:如果 command line 指令中有給 --stage 參數,就用這個參數值,例如指令為 $ sls deploy --stage dev,那這裡的值就會是 dev;若 command line 沒有給--stage參數,就用自己這個 yml 檔案中 custom.defaultStage 的設定值。

profile 表示我們要用哪一個 AWS IAM profile,這裡的語法同上:如果 command line 指令有給 --profile 參數就套用;若無,就會去吃自己這個 yml 檔的 custome.env.PROFILE 這個設定。

請注意,這裡為了方便,直接使用佈署用的 profile,實際上這個 profile 用不到這麼多權限,應該要另外設定才是正解。

2021更新:在較新的版本中已棄用 profile 參數,需改用參數 iam,此參數彈性高,可指定既有的 iam resource,也可直接使用 cloud formation 語法設定權限。

詳見:https://www.serverless.com/framework/docs/providers/aws/guide/iam

其他還有 stackNameapiName 等等參數可選用,可參考前面提到的官方文件。

(2) 告訴 framework 我們在 AWS 或其他 provider 那邊的設定值是什麼:

vpc:
securityGroupIds: ${self:custom.vpc.SECURITY_GROUP_IDS}
subnetIds: ${self:custom.vpc.SUBNET_IDS}

vpc 表示 AWS VPC 的網路相關設定是什麼,實際上要填入的就是 Aurora Serverless cluster 裡的設定,這部分的設定會去吃這個檔案的 custom.vpc 這組設定,而 custom.vpc 實際上是去抓 vpc.json 這個檔案,這部分後面會再解釋。

(3) 讓專案下的程式碼可以抓得到專案層級的環境變數:

environment: 
NODE_ENV: ${self:provider.stage}

一旦設定了這些變數,之後有 nodejs 裡的程式碼就可以利用 process.env 這個物件來取得環境變數,例如:

process.env.NODE_ENV

這個變數就會回傳 NODE_ENV 也就是 ${self:provider.stage}這個設定值。

若專案使用的是 python,也可用 os.environ 這個 dictionary 來取得,如:

os.environ['MY_VAR'] 

更多可供使用的變數請參見:https://serverless.com/framework/docs/providers/aws/guide/variables/

functions

這部分是用來描述 Lambda function 的名稱、觸發方式及指向等 opration 上的細節,例如:

hello:
handler: handler.hello
events:
- http:
path: /hello
method: get
cors: true

在這段宣告中,framework 會自動幫我們建立一個 web-api-dev-hello 的 Lambda function,而這個 function 對應的程式碼是 handler 的內容,也就是handler.hello 這個 method,我們可以在 handler.js 這個檔案中找到這段 Serverless Framework 幫我們自動產生的範例程式。

events 用來設定觸發這個 Lambda function 的條件,例如 http、s3、sns、sqs 等等服務都各自有對應的事件可以用來作為觸發條件。

這邊因為我們的應用程式是 Web API,所以事情類型是 http ,對應的服務就是 API Gateway 了。底下的 pathmethodcors 的設定會幫我們自動在 AWS API Gateway 中產生一個 POST /hello 的 endpoint,並開啟 cors 設定。

關於 CORS 的進階設定請參考:
https://serverless.com/blog/cors-api-gateway-survival-guide/

plugins

前面有提到 plugins 的相關設定,而這邊就是告訴 framework 我們要使用哪些 plugins, framework 會自動幫我們將相關設定加進 package.json 中。

編輯設定檔

在上一部分的 serverless.yml 中,我們可以看起 custom 區段引用了兩組設定檔:

${file(config/${self:provider.stage}/env.json)}
${file(config/${self:provider.stage}/vpc.json)}

另外我們還需要一組 db.json 的設定檔讓程式可以連線到資料庫。

因為我們總共會有 3 種環境:local、dev、prod,所以總共會有 9 個檔案。

首先請先建立各環境的目錄:

$ mkdir config
$ mkdir config/local
$ mkdir config/dev
$ mkdir config/prod

以下一一敘述每個檔案的內容。

env.json

新增以下三個檔案:

config/local/env.json
config/dev/env.json
config/prod/env.json

首先是 config/local/env.json ,內容如下:

{
"PROFILE": "",
"REGION": ""
}

因為 local 是本機測試環境,不需要用到 AWS 的設定,所以這邊的設定值留空即可。你可能會問能不能乾脆連 key 都不給,答案是可以,但跑 sls 指令 時會噴 warning,很煩,所以建議不要省略。

再來, config/dev/env.jsonconfig/prod/env.json ,內容是一樣的:

{
"PROFILE": "serverless-agent",
"REGION": "YOUR_AWS_REGION"
}

請將 REGION 改成你實際上使用的 AWS region 名稱。

vpc.json

新增以下三個檔案:

config/local/vpc.json
config/dev/vpc.json
config/prod/vpc.json

首先是 config/local/vpc.json ,內容如下:

{
"SECURITY_GROUP_IDS": [],
"SUBNET_IDS": []
}

因為 local 端用不到 AWS 的 VPC,所以這邊都留空陣列即可。

config/dev/db.jsonconfig/prod/db.json 是我們在 part1 中Aurora Serverless cluster 的設定,如下圖紅框處:

檔案內容大概會長這樣:

{
"SECURITY_GROUP_IDS": [
"sg-xxx"
],
"SUBNET_IDS": [
"subnet-xxx",
"subnet-xxx",
"subnet-xxx"
]
}

把斜體字換成你自己的 cluster 設定值就可以囉。

db.json

新增以下 3 個檔案:

config/local/db.json
config/dev/db.json
config/prod/db.json

檔案內容格式都一樣,但請將各環境的 db 連線參數各自對號入座:

{
"database": "YOUR_DB_NAME",
"username": "YOUR_DB_USER",
"password": "YOUR_DB_PASSWORD",
"host": "YOUR_DB_HOST",
"dialect": mysql
}

config/local/db.json 的內容就是你自己本機測試用的 MySQL 連線設定,如果你還沒有的話就趕快先裝一下吧。

config/dev/db.jsonconfig/prod/db.json 就是我們在 part1 中,Aurora Serverless cluster 那邊的設定。host 填入下圖紅框內的 endpoint:

另外幾個 databaseusernamepassword 則是填入我們在 part2 中設定好的 database 名稱及使用者帳密。

註:關於設定檔目錄的架構,可能會有些不同的實作方式,若覺得這樣設計不符合 nodejs 的 best practice,歡迎提出指教喔。

修改 .gitignore

雖然我們的 project 還沒有做 git 初始化,但 Serverless Framework 很貼心地幫我們生了一個 .gitignore 檔,並且已經將一些基本不需要被追蹤的檔案加進去了。

因為剛剛我們生了一些設定檔,這些檔案中,跟 AWS 相關的設定是不應該要被 git 追蹤到的,請開啟 .gitignore 增加以下內容:

config/dev/*
config/prod/*

至於 config/local/ 這個目錄底下的檔案原則上是需要被 git 追蹤的,因為它記載著各個需要被設定的項目,方便讓其他團隊成員知道這個專案需要有哪些設定值才能正常運作。

佈署

其實到這邊,我們就已經可以將現有的資源佈署到 AWS 上了!請下指令:

$ sls deploy --stage dev

結果應該會類似這樣:

可以看到,API Gateway 的 endpoints 及 Lambda functions 都已經幫我們串好了!

接著我們可以來試著送 request 到 /hello 這個 endpoint:

$ curl https://(馬賽克).execute-api.ap-northeast-1.amazonaws.com/dev/hello

應該會回傳類似下面的訊息:

{
"message": "Go Serverless v1.0! Your function executed successfully!",
"input": {
"resource": "/hello",
"path": "/hello",
"httpMethod": "GET",
"headers": {
"Accept": "*/*",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "TW",
(以下略)

表示我們的 REST API 已經可以正常運作囉!但目前只有 /hello 這個 endpoint 是正常的,因為其他 6 個 endpoint 需要存取資料庫,將會在下一篇中實作。

你應該會發現,第一次送的 request 回傳時間會特別久,要等約莫 10 秒,這就是 serverless 界傳說中的 cold start time,也就是當 lambda function 被閒置一段時間後再啟動就會發生的現象。這算是 serverless 天生的缺陷,至於該如何處理,只要 google “cold start time” 就可以找到非常多資訊,這邊就不多討論了。

至此,我們的 serverless project 目錄檔案結構應該會長這樣:

web-api/
├── config
│ ├── dev
│ │ ├── db.json
│ │ ├── env.json
│ │ └── vpc.json
│ ├── local
│ │ ├── db.json
│ │ ├── env.json
│ │ └── vpc.json
│ └── prod
│ ├── db.json
│ ├── env.json
│ └── vpc.json
├── handler.js
├── node_modules
│ └── (略)
├── package-lock.json
├── package.json
└── serverless.yml

小結

本篇完成了 Serverless Framework 專案的初始化及 serverless.yml 的撰寫,並且順利將資源佈署至 AWS ,等於已經將骨架建立起來了,最後我們只要撰寫 application 層的程式碼就可以將 REST API 跑起來囉!

上一篇:在 AWS 以 Aurora Serverless 搭建無伺服器 Web 應用程式 part3 — 設定 IAM Credentials

下一篇:在 AWS 以 Aurora Serverless 搭建無伺服器 Web 應用程式 part5 — 實作 REST API 及測試

--

--