Serverless Typescript với AWS Lambda, API Gateway và DynamoDB trên môi trường offline - Phần 01

Function as Service

Function as Service (FaaS) là một trong 2 dịch vụ chính của nhóm dịch vụ serverless (Backend as Service (BaaS) và Function as Service (FaaS) ), ở mô hình này, bạn sẽ phải viết code ở phần backend, nhưng thay vì deploy lên server, bạn deploy dưới dạng một function. Như vậy cách này bạn sẽ chủ động hơn đối với phần backend và không cần quan tâm đến server. Function này sẽ được gọi dưới dạng RestAPI, bạn sẽ trả tiền theo số lần gọi function của mình. Dịch vụ FaaS khá nổi tiếng là AWS Lambda của Amazon. Khi công bố dịch vụ AWS Lambda nhóm phát triền Amazon đã nói "dịch vụ này cho phép các bạn chạy các đoạn code logic của mình mà không cần cung cấp hoặc phải quản lý một hoặc nhiều máy chủ. Ngoài ra, AWS API Gateway cung cấp các kết nối đầu cuối API có thể kết nối với các chức năng của Lambda function, kết nối giữa một API Gateway với một Lambda tạo ra một API endpoint mà client có thể gọi tới. Sự kết hợp này cho phép client lấy được mã truy cập (token) mà không tiết lộ ClientID và ClientSecret". Tập trung vào ứng dụng chứ không phải cơ sở hạ tầng của bạn

Typescript

Hiện tại aws lambda đã hỗ trợ các ngôn ngữ Java, Node.js, Python, C# và mới đây là Go. Trong bài này chúng ta sẽ dùng typescript để viết logic. Typescript có thể được coi là một phiên bản nâng cao của Javascript bởi việc bổ sung tùy chọn kiểu tĩnh và lớp hướng đối tượng mà điều này không có ở Javascript, ngoài ra nó sử dụng tất cả các tính năng của của ECMAScript 2015 (ES6) như classes, modules. Bản chất của TypeScript là biên dịch tạo ra các đoạn mã javascript nên ban có thê chạy bất kì ở đâu miễn ở đó có hỗ trợ biên dịch Javascript. Ngoài ra bạn có thể sử dụng trộn lẫn cú pháp của Javascript vào bên trong TypeScript, điều này giúp các lập trình viên tiếp cận TypeScript dễ dàng hơn. Các lợi ích khi phát triển dự án bằng typescript các bạn có thể tự tìm hiểu thêm.

Serverless js

serverless là một bộ công cụ giúp bạn triển khai và vận hành các ứng dụng theo mô hình serverless. Công cụ hỗ trợ nhiều nền tảng dịch vụ serverless nổi tiếng

Offline development

Việc phát triền ứng dụng theo mô hình serverless còn khá "mới", các bài viết trên trên mạng thường chưa chi tiết và chỉ dừng ở mức "Hello World!", những người muốn tìm hiểu cũng khó tiếp cận. Đặc biệt là việc phát triển dự án trên môi trường offline thường gặp nhiều vấn đề, trong đó có vấn đề mô tả được kết nối giữa API Gateway tới Lambda function. Làm sao để trigger một function bằng một lời gọi http request?

Xây dựng một ứng dụng serverless

Để thực hiện bài viết này các bạn cần phải có hiểu biết căn bản để xây dựng một ứng dụng với nodejs. Ứng dụng của chúng ta sẽ có 2 api endpoint: POST /api/v1/cats - để khởi tạo thông tin một chú mèo 😽 GET /api/v1/cats/:id - để lấy thông tin một chú mèo theo id 😻 Cơ sở dữ liệu sẽ sử dụng là DynamoDb (??)

Cấu trúc thư mục và package cần thiết Tạo thư mục ứng dụng mkdir crazy-cats & cd crazy-cats

Khởi tạo các file cơ bản yarn init -y git init

serverless: Bộ công cụ hỗ trợ quản lý ứng dụng serverless của chúng ta, deploy, quản lý ứng dụng khi triển khai nên aws. yarn global add serverless

typescript: gói biên dịch ngôn ngữ typescript có thể đọc được theo tiêu chuẩn của javascript webpack: công cụ hỗ trợ tạo build-tool ts-load: "plugin" của webpack, biên dịch các file typescript thành ngôn ngữ javascript serverless-webpack: plugin của serverless js yarn add -D typescript webpack ts-loader serverless-webpack

các serverless plugin để phát triển ở môi trường offline yarn add -D serverless-offline serverless-dynamodb-local dynamodb chúng ta có thể cài đặt local bằng gói cài đặt từ aws, hoặc dùng docker.

Các file "Hello world!" serverless touch serverless.yml && mkdir src && cd src && touch handler.ts Cấu hình webpack tạo file webpack.config.js với nội dung

var path = require('path');

module.exports = {
  entry: './src/handler.ts',
  target: 'node',
  stats: {warnings: false},
  module: {
    exprContextCritical: false,
    loaders: [
      {
        test: /\.ts(x?)$/,
        loader: 'ts-loader',
        exclude: [/node_modules/, /\.(spec|e2e)\.ts$/]
      },
    ]
  },

  resolve: {
    extensions: ['.ts', '.js', '.tsx', '.jsx']
  },

  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, 'dist'),
    filename: 'handler.js'
  },

  externals: {
    'aws-sdk': true
  },
};

Tạo file cấu hình cho typescript tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "noEmitOnError": true,
    "noImplicitAny": false,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "sourceMap": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

Định nghĩa dịch vụ serverless serverless.yml

service: crazy-cats
provider:
  name: aws
  runtime: nodejs6.10
  stage: dev

plugins:
  - serverless-webpack
  - serverless-dynamodb-local
  - serverless-offline

custom:
  dynamodb:
    start:
      port: 8000
      inMemory: true
      migrate: true
      seed: true
      # Uncomment only if you already have a DynamoDB running locally
      # noStart: true

functions:
  createCat:
    handler: handler.createCat
    events:
     - http:
         path: cats
         method: POST
  findCatById:
   handler: handler.findCatById
   events:
    - http:
        path: cats/:id
        method: GET

resources:
  Resources:
    usersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: cats
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

Để cài đặt dynamodb local cho project, chúng ta quay lại thư mục gốc của dự án, gõ lệnh sls dynamodb install

Nội dung file /src/handler.js

export function createCat(event, context, callback) {
  let response = {
    statusCode: 200,
    headers: {
      "x-custom-header" : "my custom header value"
    },
    body: JSON.stringify({message: "Cat created!"})
  };
  callback(null, response);
}

export function findCatById(event, context, callback) {
  let response = {
    statusCode: 200,
    headers: {
      "x-custom-header" : "my custom header value"
    },
    body: JSON.stringify({cats: []})
  };
  callback(null, response);
}

Để chạy ứng dụng chúng ta sử dụng lệnh sls offline start --location=./dist/service

output log

Serverless: Bundling with Webpack...
Hash: ddd335cb56f5819b1c75
Version: webpack 3.0.0
Time: 8808ms
     Asset     Size  Chunks             Chunk Names
handler.js  3.24 kB       0  [emitted]  main
   [0] ./src/handler.ts 682 bytes {0} [built]
Serverless: Watching for changes...
Dynamodb Local Started, Visit: http://localhost:8000/shell
Serverless: DynamoDB - created table cats
Serverless: Starting Offline: dev/us-east-1.

Serverless: Routes for createCat:
Serverless: POST /api/v1/cats

Serverless: Routes for findCatById:
Serverless: GET /api/v1/cats/{id}

Serverless: Offline listening on http://localhost:3000

Chúng ta có thể dùng Postman để gọi vào một trong các api tương ứng.

Giờ chúng ta sẽ thực hiện phần logic của ứng dụng, trước tiên để làm việc với Dynamodb chúng ta sẽ dùng DocumentClient của gói aws-sdk yarn add aws-sdk -S Trước tiên là api POST /api/v1/cats Client sẽ gửi 2 thông tin của một chú mèo bao gồm namemood giống như thế này:

{
	"name": "Pikachu",
	"mood": "crazy"
}

Parmameter đầu tiên của 1 Lambda function nếu được gọi bởi API Gateway sẽ có kiểu:

{
    body: string | null;
    headers: { [name: string]: string };
    httpMethod: string;
    isBase64Encoded: boolean;
    path: string;
    pathParameters: { [name: string]: string } | null;
    queryStringParameters: { [name: string]: string } | null;
    stageVariables: { [name: string]: string } | null;
    requestContext: APIGatewayEventRequestContext;
    resource: string;
}

function create

export function createCat(event, context, callback) {
  let {name, mood} = parseBody(event.body);
  const params = {
    Item: {
      id: uuid.v4(),
      mood: mood,
      name: name,
    },
    TableName: 'cats',
  };
  let response = {
    statusCode: 200,
    headers: {
      'x-custom-header' : 'my custom header value'
    },
    body: ''
  };
  dynamoDb.put(params, (err) => {
    if (err) {
      response.statusCode = 500;
      response.body = JSON.stringify({message: err.toString()})
    } else {
      response.body = JSON.stringify(params.Item);
    }
    callback(null, response);
  });
}

function find by id

export function findCatById(event, context, callback) {
  let id = event.pathParameters.id;
  const params = {
    Key: {
      id,
    },
    TableName: 'cats',
  };
  let response = {
    statusCode: 200,
    headers: {
      'x-custom-header' : 'my custom header value'
    },
    body: ''
  };
  dynamoDb.get(params, (err, data) => {
    if (err) {
      response.statusCode = 500;
      response.body = JSON.stringify({message: err.toString()})
    } else {
      response.body = JSON.stringify(data.Item);
    }
    callback(null, response);
  });
}

Nội dung của cả file src/handler.ts

import * as AWS from 'aws-sdk';
import * as uuid from 'uuid';
let dynamoDb = null;

if (process.env.IS_OFFLINE === 'true') {
  dynamoDb = new AWS.DynamoDB.DocumentClient({
    endpoint: process.env.DYNAMODB_ENDPOINT || 'http://localhost:8000',
    region: 'localhost',
  });
} else {
  dynamoDb = new AWS.DynamoDB.DocumentClient();
}

function parseBody(body: string): any {
  try {
    return JSON.parse(body);
  } catch {
    return {};
  }
}

export function createCat(event, context, callback) {
  let {name, mood} = parseBody(event.body);
  const params = {
    Item: {
      id: uuid.v4(),
      mood: mood,
      name: name,
    },
    TableName: 'cats',
  };
  let response = {
    statusCode: 200,
    headers: {
      'x-custom-header' : 'my custom header value'
    },
    body: ''
  };
  dynamoDb.put(params, (err) => {
    if (err) {
      response.statusCode = 500;
      response.body = JSON.stringify({message: err.toString()})
    } else {
      response.body = JSON.stringify(params.Item);
    }
    callback(null, response);
  });
}

export function findCatById(event, context, callback) {
  let id = event.pathParameters.id;
  const params = {
    Key: {
      id,
    },
    TableName: 'cats',
  };
  let response = {
    statusCode: 200,
    headers: {
      'x-custom-header' : 'my custom header value'
    },
    body: ''
  };
  dynamoDb.get(params, (err, data) => {
    if (err) {
      response.statusCode = 500;
      response.body = JSON.stringify({message: err.toString()})
    } else {
      response.body = JSON.stringify(data.Item);
    }
    callback(null, response);
  });
}

Giờ bạn chạy lại lệnh sls offline start --location=./dist/service để xem những thay đổi!

Bài viết này mình sẽ dừng ở đây, ở phần tiếp theo mình sẽ đi sâu vào việc chuyển đổi một ứng dụng express sang dạng serverless.