Hướng dẫn tạo Serverless RESTful API với NodeJS và AWS

Bài viết này mình xin hướng dẫn cho người mới bắt đầu về cách sử dụng AWS CloudFormation và Lambda để triển khai một API RESTful đơn giản (và có Serverless).

Serverless là gì?

Thuật ngữ Serverless (a.k.a. Chức năng-as-a-Service) mô tả loại kiến trúc cho phép mã code được triển khai và chạy trên các vùng chứa tạm thời và không có trạng thái từ các nhà cung cấp bên thứ ba (ví dụ: Azure hoặc AWS).

Lợi ích của Serverless

Giảm quản lý hoạt động. Kiến trúc Serverless cho phép các nhà phát triển tập trung vào việc viết mã và không phải lo lắng về cấu hình và quản lý cơ sở hạ tầng mà mã của họ chạy. Dễ dàng mở rộng quy mô. Vì các chức năng "Không có Serverless" (a.k.a. các ứng dụng không có Serverless) là trạng thái không xác định và luôn được viện dẫn bởi một sự kiện (ví dụ: yêu cầu HTTP), bạn có thể chạy nhiều, hoặc ít, các chức năng theo nhu cầu của bạn. Tùy thuộc vào quy mô và hình dạng của lưu lượng truy cập của bạn, điều này rất hiệu quả về chi phí vì các chức năng của Serverless thường được tính cho mỗi lần gọi.

Nhược điểm của Serverless

Độ trễ cho các yêu cầu khi khởi tạo. Nếu chức năng Serverless không hoạt động (ví dụ như đã không được chạy trong một khoảng thời gian), thì việc xử lý lời gọi đầu tiên có thể cần thêm thời gian để hoàn thành vì phải khởi tạo (tức là phân bổ máy chủ lưu trữ, mã tải, v.v.). Thiếu kiểm soát hệ thống. Vì mã của bạn đang chạy trong môi trường được quản lý bởi nhà cung cấp, bạn sẽ không thể kiểm soát việc nâng cấp hệ thống hoặc phụ thuộc bên ngoài cơ sở mã của bạn.

CloudFormation là gì?

CloudFormation là một dịch vụ từ Amazon cho phép bạn xây dựng tài nguyên AWS sử dụng các mẫu (template). Mẫu là tệp cấu hình (YML hoặc JSON) để cung cấp tất cả các tài nguyên AWS của bạn như các trường hợp EC2, các bảng DynamoDB, vai trò và quyền IAM, hoặc bất cứ điều gì khác.

Trong hướng dẫn này, chúng ta sẽ tạo một API RESTful đơn giản với hai điểm cuối sau:

  1. POST /users/$ {userId}/hello
  2. GET /users/$ {userId}/hello

Bước 1. Tạo 1 project nhỏ:

Có 2 tệp mà bạn cần cho hướng dẫn này: index.js (mã NodeJS cho hàm Lambda) và stack.yml (mẫu ngăn xếp CloudFormation)

// index.js
"use strict";

var AWS = require('aws-sdk');

// Get "Hello" Dynamo table name.  Replace DEFAULT_VALUE 
// with the actual table name from your stack.
const helloDBArn = process.env['HELLO_DB'] || 'DEFAULT_VALUE';  //'Mark-HelloTable-1234567';
const helloDBArnArr = helloDBArn.split('/');
const helloTableName = helloDBArnArr[helloDBArnArr.length - 1];

// handleHttpRequest is the entry point for Lambda requests
exports.handleHttpRequest = function(request, context, done) {
  try {
    const userId = request.pathParameters.userId;
    let response = {
      headers: {},
      body: '',
      statusCode: 200
    };

    switch (request.httpMethod) {
      case 'GET': {
        console.log('GET');
        let dynamo = new AWS.DynamoDB();
        var params = {
          TableName: helloTableName,
          Key: { 'user_id' : { S: userId } },
          ProjectionExpression: 'email'
        };
        // Call DynamoDB to read the item from the table
        dynamo.getItem(params, function(err, data) {
          if (err) {
            console.log("Error", err);
            throw `Dynamo Get Error (${err})`
          } else {
            console.log("Success", data.Item.email);
            response.body = JSON.stringify(data.Item.email);
            done(null, response);
          }
        });
        break;
      }
      case 'POST': {
        console.log('POST');
        let bodyJSON = JSON.parse(request.body || '{}');
        let dynamo = new AWS.DynamoDB();
        let params = {
          TableName: helloTableName,
          Item: {
            'user_id': { S: userId },
            'email': { S: bodyJSON['email'] }
          }
        };
        dynamo.putItem(params, function(error, data) {
          if (error) throw `Dynamo Error (${error})`;
          else done(null, response);
        })
        break;
      }
    }
  } catch (e) {
    done(e, null);
  }
}
// stack.yaml
---
AWSTemplateFormatVersion: 2010-09-09

Description: API Gateway, Lambda, and Dynamo.

Resources:
  # Policy required for all lambda function roles.
  BaseLambdaExecutionPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: Base permissions needed by all lambda functions.
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
              - ec2:CreateNetworkInterface
              - ec2:DescribeNetworkInterfaces
              - ec2:DeleteNetworkInterface
            Resource: "*"

  HelloTable:
    Type: AWS::DynamoDB::Table
    Properties:
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
      AttributeDefinitions:
        - AttributeName: user_id
          AttributeType: S
      KeySchema:
        - AttributeName: user_id
          KeyType: HASH

  # FIXME How to hook up custom domain?
  MyApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub "${AWS::StackName}-MyApiGateway"
      Description: A description
      FailOnWarnings: true
      Body:
        swagger: 2.0
        info:
          description: |
            The account API.
          version: 1.0
        basePath: /
        schemes:
          - https
        consumes:
          - application/json
        produces:
          - application/json
        paths:
          /users/{userId}/hello:
            get:
              description: TBD
              x-amazon-apigateway-integration:
                uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloLambda.Arn}/invocations"
                credentials: !GetAtt MyApiGatewayRole.Arn
                passthroughBehavior: when_no_match
                httpMethod: POST
                type: aws_proxy
              operationId: getHello
              parameters:
                - name: userId
                  in: path
                  description: TBD
                  required: true
                  type: string
                  format: uuid
            post:
              description: TBD
              x-amazon-apigateway-integration:
                uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloLambda.Arn}/invocations"
                credentials: !GetAtt MyApiGatewayRole.Arn
                passthroughBehavior: when_no_match
                httpMethod: POST
                type: aws_proxy
              operationId: postHello
              parameters:
                - name: userId
                  in: path
                  description: TBD
                  required: true
                  type: string
                  format: uuid
                - name: body
                  in: body
                  description: TBD
                  required: true
                  schema:
                    type: object
                    required:
                    - email
                    properties:
                      email:
                        type: string

  MyApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref MyApiGateway
      StageName: prod

  MyApiGatewayRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: apigateway.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: InvokeLambda
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource:
                  - !GetAtt HelloLambda.Arn

  HelloLambda:
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt HelloLambdaRole.Arn  # TODO
      Handler: index.handleHttpRequest
      Runtime: nodejs6.10
      Environment:
        Variables:
          HELLO_DB: !Sub "arn:aws:dynamodb:${AWS::Region}:*:table/${HelloTable}"
      Code:
        ZipFile: |
          exports.handlers = function(event, context) {}
  HelloLambdaRole:  # -> AppAPIRole
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Ref BaseLambdaExecutionPolicy
      Policies:
        - PolicyName: getHello
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:GetItem
                Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:*:table/${HelloTable}"
        - PolicyName: putHello
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:*:table/${HelloTable}"
Bước 2. Trong tập tin cấu hình CloudFormation

Chú ý đến stack.yml, nó là tập tin cấu hình sẽ được sử dụng bởi CloudFormation để tạo ra tất cả mọi thứ ứng dụng của chúng ta sẽ yêu cầu.

Dưới đây là sơ đồ chi tiết về tất cả các tài nguyên AWS của chúng ta trong stack.yml sẽ cần phải tạo. Tên được sử dụng trong YML nằm trong các ô màu đỏ.

Bước 3. Tạo Cloudformation Stack

Sau khi kiểm tra YML, hãy tới https://console.aws.amazon.com/cloudformation và nhấp vào nút Create Stack. Chọn Tải lên mẫu lên Amazon S3 và tải tệp stack.yml lên.

Trên màn hình tiếp theo, bạn sẽ được yêu cầu chọn một tên Stack (có thể là bất cứ thứ gì). Sau đó, nhấp vào Tiếp theo và chọn Tôi thừa nhận rằng AWS CloudFormation có thể tạo tài nguyên IAM và nhấp vào Tiếp theo một lần nữa.

Tại thời điểm này, stack của bạn đang được tạo ra. Đợi một phút trên trang Stacks cho đến khi trạng thái ngăn xếp của bạn trở thành CREATE_COMPLETE.

Bước 4. Sao chép và dán mã vào Lambda

Một khi ngăn xếp của bạn đã hoàn tất, hãy đi tìm Lambda mới của stack ở đây: https://console.aws.amazon.com/lambda. Tên hàm Lambda của bạn phải giống với $ {StackName} -HelloLambda-XXXX

Bước 5. Tìm API Gateway của bạn và kiểm tra nếu nó hoạt động

Tìm API Gateway được tạo bởi mẫu CloudFormation của bạn tại đây: https://console.aws.amazon.com/apigateway. Tên Gateway API của bạn phải giống với $ {StackName} -MyApiGateway.

Sau khi bạn tìm thấy Cổng API của mình, chúng ta có thể kiểm tra xem mọi thứ đã được nối bằng cách chọn tùy chọn POST bên dưới / người dùng và sau đó nhấp vào TEST.

Trên trang Test page, thiết lập UserId là 123, và thiết lập Request Body theo sau và nhấp vào Test. Nếu mọi thứ đã hoạt động, Trạng thái phải là 200 mà không có dữ liệu

Sau khi kiểm tra điểm cuối POST, bạn có thể kiểm tra xem liệu dữ liệu của bạn đã được lưu bằng cách vào /hello GET Test page và thử một yêu cầu (nhớ thiết lập userId là 123). Kết quả => xem phần trên.

Bước 6. Deploy API Gateway

Bây giờ bạn đã xác minh rằng API Gateway của bạn, Lambda và DynamoDB được kết nối, bạn có thể deploy API Gateway của mình để có thể truy cập nó từ internet.

Để triển khai API của bạn, hãy chọn Actions menu và chọn Deploy. Khi cửa sổ bật lên xác nhận xuất hiện, hãy thiết lập giai đoạn triển khai và sau đó nhấp vào Deploy.

Bước 7. Gửi yêu cầu tới API của bạn

Khi bạn đã triển khai API của mình, bạn sẽ được chuyển tiếp tới trang Stages. Ở đây bạn sẽ tìm thấy tên miền cho API Gateway của mình trong khu vực đánh dấu màu xanh lam bên cạnh Invoke URL.

Sử dụng URL từ ảnh chụp màn hình ở trên, bạn có thể gửi yêu cầu GET /users/123/hello trong trình duyệt web của bạn như dưới đây.

Và đó là nó. Bây giờ bạn đã có một API RESTful Serverless có khả năng mở rộng, reliabe....