Serverless framework x CircleCI でAWS lambdaのローカル開発環境構築

Linc’well Advent Calendar10日目の記事です。

当社が展開するクリニックグループの [CLINIC FORの予約システム](https://reservation.clinicfor.life/) で、認証機能の一部にAWS Cognitoへの移行を進めております。

予約システム全体としてはモノリシックなRailsアプリケーションで開発をしてきたのですが、AWS CognitoをRailsアプリケーションの上に被すように実装する必要がり、連携をするためにAWS CognitoのTriggerをlambdaで実装する必要がでてきました。

この記事では、ローカル環境でのlambda開発、テストをどう構築したかをまとめます。

## lambda 開発環境構築

今回は、[Serverless Framework](https://github.com/serverless/serverless)を使って開発を進めます。Frameworkのinstallとprojectを作成します。

```
npm install -g serverless
serverless create --template aws-nodejs --name cognito-triggers --path cognito-triggers
cd cognito-triggers
```

## lambda の依存パッケージをインストール

RailsアプリケーションにAWS Cognitoを連携させるために、http clientとして`axios`を使います。

```
npm init
npm install axios --save 
```

## lambda のコードを書く

このは、AWS Cognitoの[サインアップ前 Lambda trigger](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html)です。AWS Cognitoは、自前のバックエンド等と連携するため、沢山のTriggerを提供しています。

ご興味あるかたは、[こちら](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html)をご覧ください。

サインアップ前に、入力されたemailが、登録済 or 未登録 か判定して、登録済であればCognitoへ登録、未登録であればCognitoへ登録しない仕様の実装です。

```javascript:pre-sign-up.js
'use strict';
const axios = require('axios'); 
  
async function exists(server, data, callback) {
  
    let url = 'https://' + server + '/api/endpoint/exists';
    const config = {
        headers: {
            "Content-Type": "application/json"
        },
        responseType: 'json'
    };

    await axios.get(url, {params: data}, config)
        .then(response => {
            console.log('response:', response.data);
            callback(null, response.data);  
        })
        .catch(function (error) {       
            callback(error);
        });
}

module.exports.exec = async (event, context, callback) => { 
    console.log('start pre authentication user:', event.userName);
    console.log(event)

    let userAttributes = event['request']['userAttributes']
    let email = userAttributes["email"];

    let rails_server_url = process.env.API_HOST_NAME; 
    await exists(rails_server_url, {
        email: email
    }, (err, response) => {
        if (err) {
            return context.fail("Connection error");
        }

        // ユーザーの 登録済 or 未登録判定
        if (response.user_exists == false) {
            // 未登録
            console.log('user not exists:', email);
            context.succeed(event);         
        }
        else {
            // 登録済
            console.log('user exists:', email);
            context.fail("user exists");    
        }
    });
};
```

## lambda をテストする

Lambdaをテストするために、下記2つをmockする必要あります。

* AWS Cognitoのcontext
* axios

これらをmockしてテストします。

### テストに必要なパッケージをインストール

テストフレームワークとして `jest`、AWS Cognitoのmockとして `aws-lambda-mock-context serverless-prune-plugin`もインストールします。

```
npm install jest aws-lambda-mock-context serverless-prune-plugin --save-dev
```

### event data の準備

加えて、AWS CognitoがTriggerで送信してくるeventも準備する必要あります。

```javascript:pre-sign-up-event-data.js
module.exports = {
testEvent: {
    "userName": "test@example.com",
    "request": {
        "userAttributes": {
            "email": "test@example.com"
        }
    }
}
};
```

###  テストコード

```javascript:pre-sign-up.test.js
'use strict'

const { exec } = require('./pre-sign-up'); 
const event = require('./pre-sign-up-event-data');

// mock
const context = require('aws-lambda-mock-context');
const axios = require('axios');
jest.mock('axios');

describe('user-migration.js.exec()', () => {
  describe('user_status: CONFIRM', () => {
      describe('user not exists', () => {
          it('return event', async () => {
              // axios mock
              axios.get.mockResolvedValue({
                  data: {user_exists: false}
              });
              const ctx = context();
              await exec(event.testEvent, ctx, function () {
              });

              // AWS Cognito の context判定
              await ctx.Promise
                  .then(result => {
                      expect(result).toBe(event.testEvent);
                  });
          });
      });

      describe('user exists', () => {
          it('through error', async () => {
              // axios mock
              axios.get.mockResolvedValue({
                  data: {user_exists: true}
              });
              const ctx = context();
              await exec(event.testEvent, ctx, function () {
              });

              // AWS Cognito の context判定
              await ctx.Promise
                  .catch(err => {
                      expect(err).not.toBeNull();
                  });
          });
      });
  });
});
```


### テスト実行
ローカル環境でテスト実行できるはずです。

```
npx jest
```

## lambda の deploy

dev, stg, prd  と deploy をしたいと考えると、

`sls deploy -v --stage dev`

こんな感じでdeployできると嬉しい。

### stage別の設定ファイルを作成する

* environments/dev.yml
* environments/stg.yml
* environments/prd.yml

のように配置する。内容は下記の通り。

```yml:environments/dev.yml
profile: your_aws_profile_name
region: aws_region_name
functionNamePrefix: function-prefix-
apiHostName: your-host-name.co.jp
```

### serverless.yml の編集

`sls deploy -v --stage dev`

から環境依存部分を解決できるように変数を展開する。

```yml:serverless.yml
service: cognito-triggers

provider:
  name: aws
  runtime: nodejs12.x

  stage: ${opt:stage}
  profile: ${self:custom.environments.${opt:stage}.profile}
  region: ${self:custom.environments.${opt:stage}.region}

  environment:
    API_HOST_NAME: ${self:custom.environments.${opt:stage}.apiHostName}

plugins:
  - serverless-prune-plugin

custom:
  prune:
    automatic: true
    number: 5
  environments:
    dev: ${file(./environments/dev.yml)}
    stg: ${file(./environments/stg.yml)}
    prd: ${file(./environments/prd.yml)}

functions:
  pre-sign-up:
    name: ${self:custom.environments.${opt:stage}.functionNamePrefix}cognito-trigger-pre-sign-up
    handler: pre-sign-up.exec
  user-migration:
    name: ${self:custom.environments.${opt:stage}.functionNamePrefix}cognito-trigger--user-migration
    handler: user-migration.exec
  post-authentication:
    name: ${self:custom.environments.${opt:stage}.functionNamePrefix}cognito-trigger--post-authentication
    handler: post-authentication.exec
  post-confirmation:
    name: ${self:custom.environments.${opt:stage}.functionNamePrefix}cognito-trigger--post-confirmation
    handler: post-confirmation.exec

```

### CIrcleCI で継続的にtest実行するための設定

詳細は割愛しますが、↓のように、install, test 実行を追加。

```yml:.circleci/config.yml
      ：
    steps:
      - run:
          name: install dependencies for lambda/cognito-triggers
          command: npm install
          working_directory: lambda/cognito-triggers
      :
      - run:
          name: run tests for lambda/cognito-triggers
          command: npm test
          working_directory: lambda/cognito-triggers
      ：
```

## まとめ

こんな感じで、ローカル環境での開発、テスト、テスト後のDeployまで一気通貫できました。さらに良いプラクティスがあればフィードバック頂けたら幸いです！