Amplify functionで簡単DynamoDB Stream & api Multi-Auth

今回は、AWS Amplifyのfunctionカテゴリを指定して、DynamoDBの変更をトリガーに実行(DynamoDB Stream)するLambda関数を作ってみます。

# DynamoDB Stream
DynamoDB Streamを使うとイベント駆動型のアーキテクチャを構築することができます。
例えば、テーブルに特定レコードが登録、削除された場合のみメールやSlackに通知する、なんてこともできます。

DynamoDB StreamとLambdaトリガーについての詳細は[公式](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Streams.Lambda.html)をご参照あれ。

せっかくなので、とあるテーブルにデータが登録されたらLambda関数が実行され、別テーブルにデータを登録する処理を実装してみたいと思います。データ登録はAppSyncを呼び出してみます。

# 事前準備
## amplify cliのインストール

```
npm install -g @aws-amplify/cli
amplify configure
```

DynamoDB Streamは比較的新しい機能なのでバージョンの古いCliではサポートされていません。
参考に、私が動作を確認したバージョンは以下のとおりです。

```
amplify -v
4.17.2
```
なお[Amplify CLI公式](https://github.com/aws-amplify/amplify-cli)によると
Requires Node.js® version 10 or later
とのことで、Nodeは10以上をインストールしてください。

## amplifyの初期設定

```
amplify init
```
amplify-cliがインタラクティブに設定情報を聞いてくるので、回答します。
[過去記事](https://qiita.com/makishy/items/499961fa64aefae2e430)と同じなので説明は割愛。

## APIとDynamoDB作成
次にトリガーとなるDBを作成します。
色々作り方はありますが、ここではamplifyのapiカテゴリを指定して作成することにします。
もちろん、AWSコンソールから手動で作成しても良いです。

```
amplify add api
```
amplify-cliがインタラクティブに設定情報を聞いてくるので、回答していきます。
手順は[過去記事](https://qiita.com/makishy/items/675e0b3efb4624681df1)と同様です。
今回は、ToDoテーブルからデータが削除されたらDoneテーブルに内容がコピーされる、というシナリオにしてみます。（通常こういったテーブル定義はあまりしないかもしれないですが...）

```schema.graphql
type Todo @model {
  id: ID!
  name: String!
  description: String
  title: String
}

type Done @model {
  id: ID!
  todoId: String!
  name: String!
  description: String
  title: String
}

```

スキーマ定義が終わったら以下のコマンドを実行しデプロイします。

```
amplify push
```
デプロイが完了するとAWSコンソールからDynamoDBにToDoとDoneテーブルが作成されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/2b2dfa3c-2810-c088-5abe-8337441a3c5d.png)


# Lambda関数の作成
事前準備ができたので、ここから本題のamplify functionを試していきます。

## Lambda関数のデプロイ
```
amplify add function
```
他の場合と同様にインタラクティブに設定情報を聞いてくるので回答します。

```
Using service: Lambda, provided by: awscloudformation
? Provide a friendly name for your resource to be used as a label for this categor
y in the project: MyTodoTrigger
? Provide the AWS Lambda function name: MyTodoTrigger
NodeJS found for selected function configuration.
? Choose the function template that you want to use: Lambda trigger
? What event source do you want to associate with Lambda trigger? Amazon DynamoDB 
Stream
? Choose a DynamoDB event source option Use API category graphql @model backed Dyn
amoDB table(s) in the current Amplify project
Selected resource AmplifyFunction
? Choose the graphql @model(s) Todo
? Do you want to access other resources created in this project from your Lambda f
unction? No
? Do you want to edit the local lambda function now? Yes
...
? Press enter to continue 
```

以下のパスにサンプルとなるLambda関数のindex.jsが作成されます。
amplify > backend > function > {your function name} > src

```index.js
exports.handler = function(event, context) {
  //eslint-disable-line
  console.log(JSON.stringify(event, null, 2));
  event.Records.forEach(record => {
    console.log(record.eventID);
    console.log(record.eventName);
    console.log('DynamoDB Record: %j', record.dynamodb);
  });
  context.done(null, 'Successfully processed DynamoDB record'); // SUCCESS with message
};

```
後ほど修正しますが、動作を見てみるためとりあえずこのままデプロイします。
サンプルは指定したテーブルに変更があったらeventの内容をCloudWatchログに記録するコードになっています。


```
amplify push
```

なお、以下のコマンドでローカルビルド＆実行によるローカルテストも可能です。index.jsと同じディレクトリにあるevent.jsonを適宜書き換えてデプロイ前のローカルテストができます。

```
amplify function build
amplify function invoke {your function name}
```


実行して動作を確認してみましょう。
手軽に試すならAppSyncのコンソールから実行するのが良いでしょう。
今回の私の例だと、AppSyncの認証にCognitoを指定しているので任意のユーザーを作成した上で動作を確認しています。Cognitoの操作手順については本題からはずれるので省略。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/a4108dfa-8328-3c6a-323f-27053eb3a77d.png)

CloudWatchログを見て実行結果を確認します。
先程登録したデータがログに記録されていることが確認できました。正常に動作しているようですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/9db42b51-57a1-d10a-2eaa-fd3a98a79755.png)

## APIとLambda関数の変更
### API更新
今回は１つのAPIでWebやアプリなどのクライアントからの呼び出しとバックエンドからの呼び出しを可能にするため複数認証を設定します。

```
amplify update api
```

```
? Please select from one of the below mentioned services: GraphQL
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API Yes, I want to
 make some additional changes.
? Configure additional auth types? Yes
? Choose the additional authorization types you want to configure for the API A
PI key
API key configuration
? Enter a description for the API key: myApiKey
? After how many days from now the API key should expire (1-365): 7
? Configure conflict detection? No
```

スキーマを以下のように変更します。
DoneテーブルのproviderにCognito、APIKey認証を設定します。operationも適当に（あまりこういった設定するケースはない気がしますが、今回はお試しということで）

```
type Todo @model {
  id: ID!
  name: String!
  description: String
  title: String
}

type Done 
  @model
  @auth(
    rules: [
      { allow: private, provider: userPools, operations: [create, update, read, delete] },
      { allow: public, provider: apiKey, operations: [create, read] }
    ]
  )
{
  id: ID!
  todoId: String!
  name: String!
  description: String
  title: String
}
```


### Lambda関数更新

index.jsを修正しDynamoDBテーブル更新をトリガーに別テーブルを更新する処理を加えてみます。

Lambda関数内部で事前に作成したapi(MyTodoTrigger)を呼び出し、Doneテーブルを更新します。

```
amplify update function
```

```
Using service: Lambda, provided by: awscloudformation
? Please select the Lambda Function you would want to update MyTodoTrigger
? Do you want to update permissions granted to this Lambda function to perform o
n other resources in your project? Yes
? Select the category api
Api category has a resource called AmplifyFunction
? Select the operations you want to permit for AmplifyFunction create, read, upd
ate, delete

You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION
var apiAmplifyFunctionGraphQLAPIIdOutput = process.env.API_AMPLIFYFUNCTION_GRAPHQLAPIIDOUTPUT
var apiAmplifyFunctionGraphQLAPIEndpointOutput = process.env.API_AMPLIFYFUNCTION_GRAPHQLAPIENDPOINTOUTPUT

? Do you want to edit the local lambda function now? Yes
```

次にindex.jsを変更します。

```index.js

require('isomorphic-fetch')
const gql = require('graphql-tag')
const AWSAppSyncClient = require('aws-appsync').default
const { AUTH_TYPE } = require('aws-appsync')
const mutation = gql(
  `mutation createDone($todoId: String!, $name: String!, $description: String, $title: String){
    createDone(input:{
      todoId: $todoId,
      name: $name,
      description: $description,
      title: $title
  }){
    id
  }
}`)

exports.handler = async (event, context, callback) => {

  const client = new AWSAppSyncClient({
    url: process.env['API_AMPLIFYFUNCTION_GRAPHQLAPIENDPOINTOUTPUT'],
    region: process.env['REGION'],
    auth: {
      type: AUTH_TYPE.API_KEY,
      apiKey: process.env['API_KEY']
    },
    disableOffline: true
  })

  event.Records.map((record) => {
    if (record.eventName !== 'REMOVE') {
      return
    }
    const todoId = record.dynamodb.OldImage.id.S
    const name = record.dynamodb.OldImage.name.S
    const description = record.dynamodb.OldImage.description.S
    const title = record.dynamodb.OldImage.title.S

    client.mutate(
      {
        mutation: mutation,
        variables: {
          "todoId": todoId,
          "name": name,
          "description": description,
          "title": title
        }
      }
    ).then((result) => {
      if (result) {
        console.log('result', result)
      } else {
        console.log('error. result is null')
      }
    }).catch(console.error)

    callback(null, 'End process')
  }
  )
}


```

ここまで来たら準備が整ったのでデプロイしましょう。

```
amplify push
```

次に、Lambdaの環境変数に「API_KEY」を設定します。
amplify update functionの設定により追加されたAPIを追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415206/ed88ef9b-b220-dcd2-318d-a6173d741cf6.png)


最後に、動作確認をします。
以下のようなmutationを発行して動作を見てみます。
AppSyncのコンソールなどから実行すると簡単です。

```
mutation create{
  createTodo(input: {
    name: "朝はやく起きる"
    description: "朝にはやくおきます"
    title: "朝起き"
  }){
    id
  }
}
```

```
mutation delete{
  deleteTodo(input: {
    id: <上記で作成したTodoのid>
  }){
    id
  }
}

```

DynamoDBのDoneテーブルを見て項目が追加されていればOKです。
やったー！


# まとめ

Amplify functionのLambda Triggerを利用することで簡単にLambdaをDynamoDB Streamをトリガーに動作させることができました。また、合わせてAmplify apiのMulti-Authについてもついでに導入してみました。
Amplifyを利用することで複雑なイベント駆動アーキテクチャでも非常に簡単に構築できそうですね。

Amplifyは続々と新しいカテゴリが追加されているので今後も色々とキャッチアップしていこうと思いますー。