[Serverless][Part-2] Phần 2: Cài đặt Slackbot và lưu access_token vào DynamoDB

Spread the love
Như đã giới thiệu ở phần tổng quan
Phần này chúng ta sẽ nói về hàm install dùng để
  • Add bot vào workspaces
  • save thông tin về team cũng như token của bot vào database (ở đây là dynamoDB)
Chúng ta sẽ sử dụng các công nghệ sau
  1. node.js
  2. Serverless framework
  3. aws cli
Chúng ta đi sơ về serverless framework và cấu trúc project được tạo bởi serverless framework

Serverless Framework:

Tổng quan về serverless framework:

 Serverless framework là một framework được ưa chuộng nhất hiện nay dùng để phát triển các ứng dụng serverless, với các lợi điểm như bên dưới
  • Tăng tốc độ phát triển hệ thống. increase development speed: CLI của serverless framework giúp bạn có thể phát triển / test trên một môi trường đơn nhất. Điều đó có nghĩa là bạn có thể định nghĩa ứng dụng của mình bên trong file yaml, sau đó deploy lên nhiều provider cùng một lúc, và rollback khi có lỗi xảy ra.
  • Tránh được các thiết lập liên quan đến bên thứ 3 – Giả sử bạn phát triển một applications dựa trên nodeJs, bạn sẽ cần chạy `npm install` và sau đó zip lại toàn bộ thư mục và upload lên AWS Lambda, với serverless framework thì điều đó không còn cần thiết nữa khi bạn chỉ cần câu lệnh `sls deploy` là đủ
  • Infrastructure as Code : Không cần biết bạn sắp deploy lên AWS Lambda, Azure StepFunctions hay bất kì Cloud Provider nào khác, bạn chỉ cần định nghĩa infrastructure của mình vào trong file `serverless.yaml` và chạy câu lệnh `sls deploy` là đủ
  • Existing eco-system: serverless framework được opensource ở trên github với hàng trăm plugins cũng như được nâng cấp liên tục ( 2 tuần một lần ) do đó bạn sẽ không cần phải lo lắng khi những câu hỏi / những khó khăn bạn đang gặp phải không có ngừoi giải đáp 🙂

Cài đặt serverless framework

Trước tiên chúng ta cần cài đặt sẵn nodeJs và npm, ngoài ra thì cũng có cần cài đặt sẵn aws cli như bên dưới
  1. Cài đặt serverless framework packgages
    `npm install -g serverless`
  2. Cài đặt aws cli

Tạo một chatbot mới trong slack 

  1. Đăng nhập vào trong slack apps, tạo một applications mới như bên dưới
  2. Sau khi bấm create app, bạn sẽ chọn Bots:
  3. Sau khi chọn applications là Bots, chúng ta sẽ tạo một user-name mới cho bot
  4. Sau khi bấm nút Add Bot User thì xem như đã hoàn tất phần tạo BotUser

Init serverless framework

  1. Đầu tiên thì sẽ là tạo serverless framework:
    `serverless create –template aws-nodejs –path forex_bot`
  2. Mở thư mục `forex_bot` bạn sẽ thấy xuất hiện 2 file đã được tạo sẵn bởi serverless framework, bao gồm, handler.js và serverless.yml (nào cùng thử tìm hiểu xem 2 file có gì nhé

Serverless.yml: 

Đây là file chứa toàn bộ định nghĩa về Serverless Application của bạn, trong đso có các phần lớn như
service:
  name: myService
  awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash # Optional KMS key arn which will be used for encryption for all functions
frameworkVersion: “>=1.0.0 <2.0.0"
provider:
  name: aws
  runtime: nodejs6.10
  stage: dev # Set the default stage used. Default is dev
  region: us-east-1 # Overwrite the default region used. Default is us-east-1
  profile: production # The default profile to use with this service
  memorySize: 512 # Overwrite the default memory size. Default is 1024
  timeout: 10 # The default is 6 seconds. Note: API Gateway current maximum is 30 seconds
  logRetentionInDays: 14 # Set the default RetentionInDays for a CloudWatch LogGroup
  deploymentBucket:
    name: com.serverless.${self:provider.region}.deploys # Deployment bucket name. Default is generated by the framework
    serverSideEncryption: AES256 # when using server-side encryption
  role: arn:aws:iam::XXXXXX:role/role # Overwrite the default IAM role which is used for all functions
  cfnRole: arn:aws:iam::XXXXXX:role/role # ARN of an IAM role for CloudFormation service. If specified, CloudFormation uses the role’s credentials
  versionFunctions: false # Optional function versioning
  environment: # Service wide environment variables
    serviceEnvVar: 123456789
  endpointType: regional # Optional endpoint configuration for API Gateway REST API. Default is Edge.
  apiKeys: # List of API keys to be used by your service API Gateway REST API
    – myFirstKey
    – ${opt:stage}-myFirstKey
    – ${env:MY_API_KEY} # you can hide it in a serverless variable
  apiGateway: # Optional API Gateway global config
    restApiId: xxxxxxxxxx # REST API resource ID. Default is generated by the framework
    restApiRootResourceId: xxxxxxxxxx # Root resource ID, represent as / path
    restApiResources: # List of existing resources that were created in the REST API. This is required or the stack will be conflicted
      ‘/users’: xxxxxxxxxx
      ‘/users/create’: xxxxxxxxxx
  usagePlan: # Optional usage plan configuration
    quota:
      limit: 5000
      offset: 2
      period: MONTH
    throttle:
      burstLimit: 200
      rateLimit: 100
  stackTags: # Optional CF stack tags
    key: value
  iamManagedPolicies: # Optional IAM Managed Policies, which allows to include the policies into IAM Role
    – arn:aws:iam:*****:policy/some-managed-policy
  iamRoleStatements: # IAM role statements so that services can be accessed in the AWS account
    – Effect: ‘Allow’
      Action:
        – ‘s3:ListBucket’
      Resource:
        Fn::Join:
          – ”
          – – ‘arn:aws:s3:::’
            – Ref: ServerlessDeploymentBucket
  stackPolicy: # Optional CF stack policy. The example below allows updates to all resources except deleting/replacing EC2 instances (use with caution!)
    – Effect: Allow
      Principal: “*”
      Action: “Update:*”
      Resource: “*”
    – Effect: Deny
      Principal: “*”
      Action:
        – Update:Replace
        – Update:Delete
      Condition:
        StringEquals:
          ResourceType:
            – AWS::EC2::Instance
  vpc: # Optional VPC. But if you use VPC then both subproperties (securityGroupIds and subnetIds) are required
    securityGroupIds:
      – securityGroupId1
      – securityGroupId2
    subnetIds:
      – subnetId1
      – subnetId2
  notificationArns: # List of existing Amazon SNS topics in the same region where notifications about stack events are sent.
    – ‘arn:aws:sns:us-east-1:XXXXXX:mytopic’
package: # Optional deployment packaging configuration
  include: # Specify the directories and files which should be included in the deployment package
    – src/**
    – handler.js
  exclude: # Specify the directories and files which should be excluded in the deployment package
    – .git/**
    – .travis.yml
  excludeDevDependencies: false # Config if Serverless should automatically exclude dev dependencies in the deployment package. Defaults to true
  artifact: path/to/my-artifact.zip # Own package that should be used. You must provide this file.
  individually: true # Enables individual packaging for each function. If true you must provide package for each function. Defaults to false
functions:
  usersCreate: # A Function
    handler: users.create # The file and module for this specific function.
    name: ${self:provider.stage}-lambdaName # optional, Deployed Lambda name
    description: My function # The description of your function.
    memorySize: 512 # memorySize for this specific function.
    runtime: nodejs6.10 # Runtime for this specific function. Overrides the default which is set on the provider level
    timeout: 10 # Timeout for this specific function.  Overrides the default set above.
    role: arn:aws:iam::XXXXXX:role/role # IAM role which will be used for this function
    onError: arn:aws:sns:us-east-1:XXXXXX:sns-topic # Optional SNS topic arn (Ref and Fn::ImportValue are supported as well) which will be used for the DeadLetterConfig
    awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash # Optional KMS key arn which will be used for encryption (overwrites the one defined on the service level)
    environment: # Function level environment variables
      functionEnvVar: 12345678
    tags: # Function specific tags
      foo: bar
    vpc: # Optional VPC. But if you use VPC then both subproperties (securityGroupIds and subnetIds) are required
      securityGroupIds:
        – securityGroupId1
        – securityGroupId2
      subnetIds:
        – subnetId1
        – subnetId2
    package:
      include: # Specify the directories and files which should be included in the deployment package for this specific function.
        – src/**
        – handler.js
      exclude: # Specify the directories and files which should be excluded in the deployment package for this specific function.
        – .git/**
        – .travis.yml
      artifact: path/to/my-artifact.zip # Own package that should be use for this specific function. You must provide this file.
      individually: true # Enables individual packaging for specific function. If true you must provide package for each function. Defaults to false
    events: # The Events that trigger this Function
      – http: # This creates an API Gateway HTTP endpoint which can be used to trigger this function.  Learn more in “events/apigateway”
          path: users/create # Path for this endpoint
          method: get # HTTP method for this endpoint
          cors: true # Turn on CORS for this endpoint, but don’t forget to return the right header in your response
          private: true # Requires clients to add API keys values in the `x-api-key` header of their request
          authorizer: # An AWS API Gateway custom authorizer function
            name: authorizerFunc # The name of the authorizer function (must be in this service)
            arn:  xxx:xxx:Lambda-Name # Can be used instead of name to reference a function outside of service
            resultTtlInSeconds: 0
            identitySource: method.request.header.Authorization
            identityValidationExpression: someRegex
      – s3:
          bucket: photos
          event: s3:ObjectCreated:*
          rules:
            – prefix: uploads/
            – suffix: .jpg
      – schedule:
          name: my scheduled event
          description: a description of my scheduled event’s purpose
          rate: rate(10 minutes)
          enabled: false
          input:
            key1: value1
            key2: value2
            stageParams:
              stage: dev
          inputPath: ‘$.stageVariables’
      – sns:
          topicName: aggregate
          displayName: Data aggregation pipeline
      – stream:
          arn: arn:aws:kinesis:region:XXXXXX:stream/foo
          batchSize: 100
          startingPosition: LATEST
          enabled: false
      – alexaSkill:
          appId: amzn1.ask.skill.xx-xx-xx-xx
          enabled: true
      – alexaSmartHome:
          appId: amzn1.ask.skill.xx-xx-xx-xx
          enabled: true
      – iot:
          name: myIoTEvent
          description: An IoT event
          enabled: true
          sql: “SELECT * FROM ‘some_topic'”
          sqlVersion: beta
      – cloudwatchEvent:
          event:
            source:
              – “aws.ec2”
            detail-type:
              – “EC2 Instance State-change Notification”
            detail:
              state:
                – pending
          # Note: you can either use “input” or “inputPath”
          input:
            key1: value1
            key2: value2
            stageParams:
              stage: dev
          inputPath: ‘$.stageVariables’
      – cloudwatchLog:
          logGroup: ‘/aws/lambda/hello’
          filter: ‘{$.userIdentity.type = Root}’
      – cognitoUserPool:
          pool: MyUserPool
          trigger: PreSignUp
# The “Resources” your “Functions” use.  Raw AWS CloudFormation goes in here.
resources:
  Resources:
    usersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: usersTable
        AttributeDefinitions:
          – AttributeName: email
            AttributeType: S
        KeySchema:
          – AttributeName: email
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
  # The “Outputs” that your AWS CloudFormation Stack should produce.  This allows references between services.
  Outputs:
    UsersTableArn:
      Description: The ARN for the User’s Table
      Value:
        “Fn::GetAtt”: [ usersTable, Arn ]
      Export:
        Name: ${self:service}:${opt:stage}:UsersTableArn # see Fn::ImportValue to use in other services and http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html for documentation on use.

  • Định nghĩa về service:
  • Định nghĩa về provider : ứng dụng serverless của bạn chạy với vendor AWS, GCP hay Azure v..v
  • Định nghĩa về functions: tương ứng với một lambda functions, chúng ta có thể config
    • handler:
    • runtime
    • timeout
    • vpc
    • cho hàm serverless của chúng ta
  • Định nghĩa về resources: ở đây chúng ta có thể định nghĩa các infra khác liên quan đến serverless function ( đó có thể là AWS Dynamo DB, AWS S3, hoặc IAM)
  • ĐỊnh nghĩa về output: Đây là kết quả trả về sau khi chạy stack formation từ CloudFormation
Ở đây chúng ta sẽ thử modified stack của Forex Bot như bên dưới
service: forex

frameworkVersion: "<=1.6.1"

custom:
  namespace: ${self:service}-${self:custom.stage}
  stage: ${opt:stage, self:provider.stage}

provider:
  name: aws
  runtime: nodejs4.3
  stage: dev
  region: us-east-1
  environment:
    CLIENT_ID: 'xxx'
    CLIENT_SECRET: yyy
    INSTALL_ERROR_URL: http://nech.info
    INSTALL_SUCCESS_URL: http://portfolio.serverless4everyone.info
    NAMESPACE: ${self:custom.namespace}
    TEAMS_TABLE: ${self:custom.namespace}-teams
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:GetItem
        - dynamodb:PutItem
      Resource:
        - arn:aws:dynamodb:*:*:table/${self:provider.environment.TEAMS_TABLE}

functions:
  install:
    handler: install.handler
    events:
      - http:
          path: install
          method: get

resources:
  Resources:
    TeamsDynamoDbTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.TEAMS_TABLE}
        AttributeDefinitions:
          - AttributeName: team_id
            AttributeType: S
        KeySchema:
          - AttributeName: team_id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5

Chúng ta cùng phân tích từng part của file `serverless.yml`
  • service: là phần đặt tên cho serrvice của chúng ta, ở đây là forex_bot
  • frameworkVersion: 1.6.1, do team serverless họ release mỗi 2 tuần 1 lần nên chúng ta giới hạn version để khi update thì không bị ảnh hưởng đến application hiện tại
  • custom: phần này chúng ta định nghĩa các biến số liên quan đến môi trường cũng như và stage
  • provider: ở đây chúng ta định nghĩa vendor của serverless, ví dụ như serverless function của chúng ta sẽ deploy lên vendor nào, version của nodejs dùng để chạy, region, stage ... Trong phần provider chúng ta có định nghĩa của environment các biến này khi deploy lên lambda sẽ nằm ở phần environment variables của Lambda như hình bên dưới
  • functions: ở đây chúng ta sẽ định nghĩa functions, ví dụ là function `install`, sẽ nhận events là `http` và handler của nó sẽ là install.hanler
  • resources: chúng ta định nghĩa  1 DynamoDB Table với tên là  `${self:provider.environment.TEAMS_TABLE}` (theo như định nghĩa ở phần environment thì table của chúng ta sẽ có tên là forex-dev-teams, và chúng ta provision cho table là 5 read và 5 write throughput

Đăng kí Client ID và Client Secret với Slack

Khi tạo một application mới với slack, sau khi đăng kí app, chúng ta sẽ confirm ClientID và ClientSecret như bên dưới
  • Access vào slack theo như link : https://api.slack.com/apps
  • Copy Client ID và Client Secret như bên dưới, và paste các thông số config vào file `serverless.yml`

Tạo install-handlers:

Mỗi lần forex bot của chúng ta được add vào một workspace, chúng ta cần lưu thông tin của team vào databases, (để sau này chúng ta sẽ reply đúng vào workspaces mà bot được add vào, vì vậy chúng ta cần tạo file `install.js` như bên dưới
/* eslint-disable no-console */
const qs = require('querystring');
const AWS = require('aws-sdk');
const fetch = require('node-fetch');

const dynamodb = new AWS.DynamoDB.DocumentClient();

const getCode = (event) => {
  var code = null;
  if (event.queryStringParameters && event.queryStringParameters.code) {
    code = event.queryStringParameters.code;
  }
  return code;
};

const requestToken = (code) => {
  console.log(`Requesting token with ${code}`);
  if (code === null) { return null; } // Skip if triggered without code
  const params = {
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
    code,
  };
  const url = `https://slack.com/api/oauth.access?${qs.stringify(params)}`;
  console.log(`Fetching ${url}`);
  return fetch(url)
    .then(response => response.json()) // Convert response to JSON
    .then((json) => {
      if (json.ok) return json; // Verify is valid JSON
      throw new Error('SlackAPIError');
    });
};

const saveResponse = (response) => {
  const params = {
    TableName: process.env.TEAMS_TABLE,
    Item: response,
  };
  console.log('Put', params);
  return dynamodb.put(params).promise();
};

const successResponse = callback => callback(null, {
  statusCode: 302,
  headers: { Location: process.env.INSTALL_SUCCESS_URL },
});

const errorResponse = (error, callback) => {
  console.error(error);
  return callback(null, {
    statusCode: 302,
    headers: { Location: process.env.INSTALL_ERROR_URL },
  });
};

module.exports.handler = (event, context, callback) =>
  Promise.resolve(event)
    .then(getCode) // Get code from event
    .then(requestToken) // Exchange code for token
    .then(saveResponse) // Save token to DDB
    .then(() => successResponse(callback))
    .catch(error => errorResponse(error, callback));

Flow của hàm install sẽ như bên dưới
Trong đó ở step 1, user sẽ request install application của chúng ta với slack, sau đó slack sẽ redirect lại để user approve install app, khi user ok, slack sẽ redirect sang function install (với mã access code nằm trong query-string, chúng ta sẽ sử dụng access code để gen access token và save vào trong DynamoDB
Chúng ta sẽ phân tích từng phần của file install.js
  1. Phần import library 

const qs = require('querystring');
const AWS = require('aws-sdk');
const fetch = require('node-fetch');

const dynamodb = new AWS.DynamoDB.DocumentClient();

Chúng ta import các thư viện : `querystring` để lấy thông tin querystring từ url, `aws-sdk` SDK của aws, `node-fetch`: sử dụng để request token từ Slack API và save vào DynamoDB
  1. Hàm getCode :

const getCode = (event) => {
  var code = null;
  if (event.queryStringParameters && event.queryStringParameters.code) {
    code = event.queryStringParameters.code;
  }
  return code;
};

Hàm getCode của chúng ta nhận tham số là `event` object của lambda, sau khi nhận tham số chúng ta sẽ tách lấy access code từ querystring

const requestToken = (code) => {
  console.log(`Requesting token with ${code}`);
  if (code === null) { return null; } // Skip if triggered without code
  const params = {
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
    code,
  };
  const url = `https://slack.com/api/oauth.access?${qs.stringify(params)}`;
  console.log(`Fetching ${url}`);
  return fetch(url)
    .then(response => response.json()) // Convert response to JSON
    .then((json) => {
      if (json.ok) return json; // Verify is valid JSON
      throw new Error('SlackAPIError');
    });
};

  1. Hàm requestToken :

const requestToken = (code) => {
  console.log(`Requesting token with ${code}`);
  if (code === null) { return null; } // Skip if triggered without code
  const params = {
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
    code,
  };
  const url = `https://slack.com/api/oauth.access?${qs.stringify(params)}`;
  console.log(`Fetching ${url}`);
  return fetch(url)
    .then(response => response.json()) // Convert response to JSON
    .then((json) => {
      if (json.ok) return json; // Verify is valid JSON
      throw new Error('SlackAPIError');
    });
};

Hàm này nhận tham số là access code, sau đó request lên api của slack ( truyền các parameters liên quan đến client_id, client_secret, và access code) để request được access_token
  1. Hàm saveResponse

const saveResponse = (response) => {
  const params = {
    TableName: process.env.TEAMS_TABLE,
    Item: response,
  };
  console.log('Put', params);
  return dynamodb.put(params).promise();
};

Nếu việc request access_token thành công, chúng ta sẽ save access token vào trong DynamoDB (với key là teamID) và trả về 1 promise
  1. Các hàm successResponse và errorResponse

const successResponse = callback => callback(null, {
  statusCode: 302,
  headers: { Location: process.env.INSTALL_SUCCESS_URL },
});

const errorResponse = (error, callback) => {
  console.error(error);
  return callback(null, {
    statusCode: 302,
    headers: { Location: process.env.INSTALL_ERROR_URL },
  });
};

Đúng như tên gọi của nó, successResponse sẽ redirect user về INSTALL_SUCCESS_URL và nếu error thì sẽ redirect user về INSTALL_ERROR_URL. 
  1. Xử lý chính : 
module.exports.handler = (event, context, callback) =>
  Promise.resolve(event)
    .then(getCode) // Get code từ event
    .then(requestToken) // Request token từ access_code
    .then(saveResponse) // Save token vào DynamoDB
    .then(() => successResponse(callback)) // Trả về success Response
    .catch(error => errorResponse(error, callback)); // Trả về error Response

Sau khi xử lý chạy lệnh `sls deploy`
Endpoint của API được deploy :

  1. Config redirect URL sau khi install applications
  1. Add Application vào workspaces
Sau khi config redirect URL của slack về endpoint mà serverless framework đã deploy hàm lambda của chúng ta,
Bây giờ mình cần add applications vào workspace như hình bên dưới
Chọn Slack Button
Chọn application mà bạn cần install ở dropdown, và nhớ check vào checkbox bot
Bầm vào button Add to Slack
Chọn Authorize, 
Nếu install apps thành công thì sẽ redirect về url đã config trước ở trong file serverless.yml
(Ở đây mình đang chọn là redirect về http://portfolio.serverless4everyone.info/)
Khi thực hiện gọi lện show logs ở serverless framework, ta có kết quả như bên dưới
vagrant@homestead:~/Code/forex_bot$ sls logs -f install -t
START RequestId: e02318f8-5f32-11e8-a3c2-e171ce8db398 Version: $LATEST
2018-05-24 18:14:34.870 (+09:00)        e02318f8-5f32-11e8-a3c2-e171ce8db398  R
equesting token with 41244907555.369579795397.deabe7c43ee550897171807b98c598fcf
ec9f87ed18fceaa8d46b5172747191c
2018-05-24 18:14:34.871 (+09:00)        e02318f8-5f32-11e8-a3c2-e171ce8db398  F
lient_secret=38d0a13d04d54f7a7cf836aeef3f4430&code=41244907555.369579795397.dea
be7c43ee550897171807b98c598fcfec9f87ed18fceaa8d46b5172747191c
2018-05-24 18:14:35.234 (+09:00)        e02318f8-5f32-11e8-a3c2-e171ce8db398  P
ut { TableName: 'forex-dev-teams',
  Item:
   { ok: true,
     access_token: 'xoxp-41244907555-41283925780-370467591639-6da75f78449b3dc82
509e5ca15af170b',
     scope: 'identify,bot',
     user_id: 'U178BT7NY',
     team_name: 'JAVDev',
     team_id: 'T1776SPGB',
     bot:
      { bot_user_id: 'UAURRTNKB',
        bot_access_token: 'xoxb-41244907555-368875940657-TBnzRZAD5KZwy7W3N9mlwP
A8' } } }
END RequestId: e02318f8-5f32-11e8-a3c2-e171ce8db398
REPORT RequestId: e02318f8-5f32-11e8-a3c2-e171ce8db398  Duration: 427.93 ms   B
illed Duration: 500 ms  Memory Size: 1024 MB    Max Memory Used: 40 MB

Kết:
Vậy là đã thành công , chúng ta đã install forex_bot vào workspaces, đồng thời lưu đc token vào trong DynamoDB
Phần tiếp theo, chúng ta sẽ tìm hiểu cách bot user của chúng ta nhận event (khi bot được mention bởi user chẳng hạn, và phản ứng lại event này)
Series Navigation<< [Serverless][Part-1] Mở đầu : Giới thiệu cách viết chatbot Slack bằng Serverless[Serverless][Part-3] Phần 3: Subscribe vào event bằng Slack >>

Leave a Reply

Your email address will not be published. Required fields are marked *