2020年10月6日 星期二

AWS Lambda Function - Express Lambda With RDS and Wrong Pattern Correction

本篇文章紀錄,從現有的 Express Proejct,在搬上 AWS Lambda 時解決 MySQL 連接所造成的問題。


首先,預備上 Lambda 的專案有以下功能:

  • 原本就自帶 Express
  • 使用 mysql client 初始化長連接
上雲的步驟會簡易帶過,詳情可以自行參閱 Lambda API 首頁 ,本地端測試有用到 serverless-offline。

然後,本上雲計畫中,所需要使用到的資源:
  • RDS (MySQL 8)
  • Lambda Function
  • API Gateway
  • CloudFormation

基本上, Troubleshooting 可以由簡單的幾個方式判斷:
  1. 如果 serverless-offline 出錯,那就代表你本身的程式碼有問題,必須注意 serverless 是無狀態的,所以是每次執行 api 都會從 main.js 執行,然後執行到 module.export 的那些 function。
  2. 如果 serverless-offline 正常,那表示可能是 VPC / Security Group 設定錯誤,這會在最下面討論。

以下,使用別人專案的例子作為示範,程式寫法並非筆者本人的風格,勿混淆使用,僅作為常見錯誤更正及為了符合推上 Lambda Function 平台所做的修正。


Express 修正為 Serverless Express



需要先安裝 npm i serverless-http

然後,在 main.js 中的改動如下:
const express = require("express");
const mysql = require('mysql');
const https = require('https');
const bodyParser = require('body-parser');
const cors = require("cors");
const fs = require('fs');
const path = require('path');
const morgan = require('morgan');
var dov = require('dotenv').config();

// AWS Lambda serveless
const sls = require('serverless-http')

// Use connection pool to handle each query
// https://stackoverflow.com/questions/54888061/configuration-mysql-node-js-error-can-not-enqueue-query-after-fatal-error
var db = mysql.createPool({
    host: host, //RDS Endpoint
    port: port, //MySQL: 3306
    user: "admin", 
    password: passowrd,
    database: default_db_name
});

/*
var db = mysql.createConnection({
    host: host, //RDS Endpoint
    port: port, //MySQL: 3306
    user: "admin", 
    password: passowrd,
    database: default_db_name
});
*/


// you're using connection pool, no need to connect on initialization
// connect to database
/*db.connect((err) => {
    if (err) {
      console.log(err)
        throw err;
    }
    console.log('Connected to database');
});*/


global.db = db;

app = express();
app.use(morgan('dev'));
app.use(cors({
  origin:["*"],
  "methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
  "preflightContinue": false,
  "allowedHeaders": ['Content-Type', 'Authorization'],
  "optionsSuccessStatus": 204
}));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ 
    extended: true
}));


//Router
app.use('/', require('./myRouter'));

// no need to specific listen
//app.listen(8080)

// serveless export usage
module.exports.server = sls(app)

注意,在此專案中,有把 db pool 丟到 global.db 這個全域變數中 (見上方程式碼)。


並在 myRouter.js 中的程式碼做以下調整:
exports.getXXXData = (req,res) => {

  //First to activate and get connection from pool
  db.getConnection(function(err, connection) {
    //Callback connection object can be use to query
    connection.query(`SELECT * FROM XXX`,(err, result) => {
      //When done by query, release connection immediately
      connection.release();
      
      if (!!err){
        res.send({setting: err.message});
        return;
      }

      res.send({setting: result});
    });
  });
};

/*exports.getXXXData = (req,res) => {
  db.query(`SELECT * FROM XXX`, (err, result) => {
    if (!!err){
      res.send({setting: err.message});
      return;
    }
    res.send({setting: result});
  });
};*/


新增 AWS Lambda Config 檔 (serverless.yml)


另外,需要在專案中新增 serverless.yml 檔案,最後才能使用指令自動化佈署。

service: Your Service Name

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev  # api gateway stage
  region: us-east-2 #Ohio
  memorySize: 512
functions:
  app:
    handler: app.server # app.js exported module: `module.exports.server = sls(app)`, name is `server`
    events:
      - http:
            path: / 
            method: ANY 
            cors: true
      - http: # all routes get proxied to the Express router 
            path: /{proxy+} 
            method: ANY 
            cors: true
      - http:
            path: getXXXData
            method: post
            cors: true
plugins:
  - serverless-offline



離線測試與佈署


需要安裝的工具:

npm install serverless --save

npm install serverless-http --save

npm install serverless-mysql --save

npm install serverless-offline --save


記得在執行的目錄下,賦予所有檔案權限,否則離線測試可能會沒有權限讀取 app.js 檔案:

sudo chmod -R 777 ./


然後,開啟離線測試指令

SLS_DEBUG=* sls offline start

此時就會看到離線測試的 API 清單:


確認正常後,使用 deploy 指令就可以佈署到雲端 AWS Lambda。

SLS_DEBUG=* sls deploy



Lambda Function 上雲後,連上 RDS 必要步驟


RDS 開放白名單或暫時公開全域


開放指定或全域的 Traffic 或 TCP ,才能讓外部連線進來,先到 RDS 選擇到 Security Group,並且把 Inbound, Outbound 設定 All Traffic / 0.0.0.0/0 以及 All Traffic / ::/0。




詳情說明,可以看 Stackoverflow 上其中一篇討論: AWS: can't connect to RDS database from my machine

總之, RDS 的 Security Group 必須要開放 Inbound, Outbound Rule,而且完全對外的話,一定要有:

  • 0.0.0.0/0
  • ::/0


Lambda 開啟 VPC 權限及連線 Policy


對 Lambda 來說,Lambda 本身無法訪問 RDS ,是受到 VPC 跟 Policy 限制,因此,需要打開 Security Group Inbound, Outbound Rule 以及必須讓 Lambda 擁有 RDS 權限 (AWSLambdaVPCAccessExecutionRole)。 則有以下必須確認的步驟:


  1. Lambda 以及 RDS 必須共用同一個 VPC (需自行確認)
  2. Lambda Security Group 有開通 Inbound, Outbound Rule
  3. Lambda Policy 套用到 AWSLambdaVPCAccessExecutionRole


首先,到 Lambda 上,選到目標的 Application,然後到 Permissions 進入 Execution role:


然後,在 Policy 頁面增加 AWSLambdaVPCAccessExecutionRole 這個權限:


然後,確認 Lambda 的 VPC 名稱後,到 EC2/VPC 底下,更改 Inbound, Outbound Rule:



詳情說明,可以看 Stackoverflow 上其中一篇討論: Error: connect ETIMEDOUT rds lambda


完成後,到 API Gateway 就可以正常測試:




Reference:

https://stackoverflow.com/questions/35880022/error-connect-etimedout-rds-lambda
https://stackoverflow.com/questions/37212945/aws-cant-connect-to-rds-database-from-my-machine
https://docs.aws.amazon.com/lambda/latest/dg/configuration-database.html
https://medium.com/@hk_it_er/create-lambda-and-api-gateway-nodejs-aws-serverless-to-rds-mysql-6a75243e61cc
https://github.com/awsdocs/aws-lambda-developer-guide/blob/master/sample-apps/rds-mysql/dbadmin/index.js
https://www.youtube.com/watch?v=cGYknt3xIvM
https://redstapler.co/aws-lambda-tutorial-rds-mysql/
https://aws.amazon.com/blogs/compute/using-amazon-rds-proxy-with-aws-lambda/
https://docs.aws.amazon.com/zh_tw/lambda/latest/dg/configuration-vpc.html

沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014