【Node.js】JTW生成処理をSAMで作る

[前回の続きです。](https://qiita.com/Shu3/items/edacbfbea4b1b6aaf877)
Zoom会議をSlackをお知らせするアプリにてZoom APIに必要なJWTを生成する処理をSAMで作成しました。（下図の赤枠）
時間ができたらやりたいと思っていましたが、思ったよりすぐやっちゃいました。さすがStayHome週間（笑）
※本記事ではSAM,SAM-CLIなどの詳述は割愛します。ご了承ください。
<img width="637" alt="スクリーンショット 2020-05-06 11.28.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480978/c2048043-cd15-269a-c509-a580ad3213b4.png">

# SAMを始める

[公式](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-getting-started-hello-world.html)にもありますが、まず↓のような前提があるので、それぞれやっておきます。

1. AWSアカウントの作成
2. IAMの設定
3. [Docker](https://hub.docker.com/)のインストール（ローカル実行に必要）
4. [Homebrew](https://brew.sh/index_ja.html)のインストール（Linux,mac）
5. [SAM CLI](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)のインストール

# SAMテンプレートの取得

まずは、SAMテンプレートをDLしてきます。

```bash
sam init
```

これを実行します。今回ランタイムはNode.js 12.Xを選択しました。
テンプレートを取得すると以下のような感じのフォルダ構成で展開されるので、適宜編集しながら、処理を実装します。

```
sam-app/
   ├── README.md
   ├── events/
   │   └── event.json
   ├── hello_world/          
   │   └── app.js            #Contains your AWS Lambda handler logic.
   ├── template.yaml         #Contains the AWS SAM template defining your application's AWS resources.
   └── tests/
       └── unit/
           └── test-handler.js
```

# JWT生成処理

↓のようなコードになります。

```app.js
const jwt = require('jsonwebtoken')

let response;

const apiKey = process.env.API_KEY;
const secretKey = process.env.SECRET_KEY;

/**
 *
 * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
 * @param {Object} event - API Gateway Lambda Proxy Input Format
 *
 * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html 
 * @param {Object} context
 *
 * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 * @returns {Object} object - API Gateway Lambda Proxy Output Format
 * 
 */
exports.lambdaHandler = async (event, context) => {
    return this.generateJWT(apiKey, secretKey)
};

exports.generateJWT = function (apiKey, secretKey) {
    try {
        // const ret = await axios(url);
        expire = Math.floor(Date.now()) + (60000 * 15);
        const token = jwt.sign({
            iss: apiKey,
            exp: expire
        }, secretKey, {
            algorithm: 'HS256'
        });
        response = {
            'statusCode': 200,
            'jwt': token
        }
    } catch (err) {
        console.log(err);
        response = {
            'statusCode': 500,
            'jwt': null
        }
    }
    return response
};
```

[node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken)をいう[Auth0](https://github.com/auth0)のライブラリを用いてJWT生成処理を実装しています。

```bash
npm install jsonwebtoken
```

`jwt.sign({iss: apiKey,exp: expire}, secretKey, {algorithm: 'HS256'});`がJWT生成部分になるワケですが、`iss`にZoomのAPIキー、`exp`にはトークンがエクスパイアするまでの時間を入れます。エクスパイアする時間はエポックミリ秒で指定します。今回は、15分後を指定しています。

そして、生成したJWTを返すワケです。

# SAMのデプロイ

## ローカル実行

ローカルテストにあたり擬似的に環境変数を与えてやる必要があります。
ざっくりとした理解ですが、ローカルにLambda環境を模したコンテナを建てるので、そこではローカルホストの環境変数は使えないワケですね。

そのため、以下のような環境変数用の設定ファイルを用意しました。

```env.json
{
    "GenerateJWT": {
      "ApiKey": "XXX",
      "SecretKey": "XXX"
    }
}
```

そして、template.yamlに↓を追加しましょう。

```yaml
Resources:
  GenerateJWT:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: generate-jwt/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      #ここから
      Environment:
        Variables:
          API_KEY: !Ref ApiKey
          SECRET_KEY: !Ref SecretKey
      #ここまで  
```

では、ローカルでテストしてみましょう！

まずは

```bash
sam build
```

ビルドして

```bash
sam local invoke --env-vars generate-jwt/env.json
```

`generate-jwt/env.json`を環境変数をして渡しています。
このやり方は以下のブログを参考にさせていただきました。

[**AWS SAM Local と LocalStack を使って ローカルでAWS Lambdaのコードを動かす**](https://dev.classmethod.jp/articles/sam-local-with-localstack/)

## デプロイ

デプロイは簡単です。

```bash
sam deploy --guided
```

これだけ。あとはガイドに従いながら、質問に答えていくだけです。SAM-CLI素晴らしい。
ガイド内容はStack名は？とかリージョンは？とか。↓のように回答していきます。

```terminal
Deploying with following values
===============================
Stack name                 : generateJWT
Region                     : ap-northeast-1
Confirm changeset          : True
Deployment s3 bucket       : 
Capabilities               : ["CAPABILITY_IAM"]
Parameter overrides        : {'ApiKey': '', 'SecretKey': ''}
```

このタイミングで今回の`template.yaml`を載せておきます。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  GenerateJWT
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  GenerateJWT:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: generate-jwt/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      Environment:
        Variables:
          API_KEY: !Ref ApiKey
          SECRET_KEY: !Ref SecretKey
      #Events:
      #  GetJWT:
      #    Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
      #    Properties:
      #      Path: /jwt
      #      Method: get

Parameters:
  ApiKey:
    Type: String
  SecretKey:
    Type: String

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  #GenerateJWTdApi:
    #Description: "API Gateway endpoint URL for Prod stage for GenerateJWT function"
    #Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/jwt/"
  GenerateJWT:
    Description: "GenerateJWT Lambda Function ARN"
    Value: !GetAtt GenerateJWT.Arn
  GenerateJWTIamRole:
    Description: "Implicit IAM Role created for GenerateJWT function"
    Value: !GetAtt GenerateJWTRole.Arn
```

API Gatewayもデフォルトのテンプレートでは作成されるのですが、今回Step Functionsから呼び出すだけなこともあり、外しています。
JWTを返すAPIなんてデプロイしたくないし。

無事デプロイすると以下のようにターミナルに出ます。※更新の際のデプロイ結果です。

```terminal
CloudFormation events from changeset
---------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                      ResourceType                        LogicalResourceId                   ResourceStatusReason              
---------------------------------------------------------------------------------------------------------------------------------------------
UPDATE_IN_PROGRESS                  AWS::Lambda::Function               GenerateJWT                         -                                 
UPDATE_COMPLETE                     AWS::Lambda::Function               GenerateJWT                         -                                 
UPDATE_COMPLETE_CLEANUP_IN_PROGRE   AWS::CloudFormation::Stack          generateJWT                         -                                 
SS                                                                                                                                            
UPDATE_COMPLETE                     AWS::CloudFormation::Stack          generateJWT                         -                                 
---------------------------------------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
----------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                      
----------------------------------------------------------------------------------------------------------------------------------------------
Key                 GenerateJWT                                                                                                              
Description         GenerateJWT Lambda Function ARN                                                                                          
Value                                             

Key                 GenerateJWTIamRole                                                                                                       
Description         Implicit IAM Role created for GenerateJWT function                                                                       
Value                                                     
----------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - generateJWT in ap-northeast-1
```

ところで、`Parameters`を定義している`ApiKey`と`SecretKey`の値がガイドの中で聞かれるのですが、ここでローカルの環境変数としてキー情報を渡してやりたかったのに、上手くできませんでした。。。環境変数はこの中では使えないのかな。。。？

# Step Functionsで呼び出す

Step Functionsも今回のLambdaに合わせて下図のように変更しています。JWT処理に失敗すればLambdaが500のステータスコードを返すので、そしたらエラー通知用のSNSトピックにプッシュする感じです。
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480978/2620e991-60a6-7886-e3d4-e713d08c24ae.png width=30%>

# CloudWatch Eventでサイクル実行

日次で毎朝9時に実行したければ、以下のようにcronを定義します。

```cron
0 0 * * ? *
```

GMTなので、9時間戻し毎日0時を指定します。
今回は朝9時まで待ちきれず12:55に実行させました。お昼休み開ける前にZoom会議を作る！みたいなイメージですかね（笑）

# 結果は？

<img width="332" alt="スクリーンショット 2020-05-06 14.38.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/480978/de38f12e-e14d-17ea-bb4b-9d352abe23f0.png">

無事成功しました。
ちなみに「密！」は自作です（笑）密を作らず、人と会うときはオンラインで。