AWS AmplifyのFunctionsでバッチ処理を実装する
弊社の開発合宿で久々にAWS Amplifyに触れてみました。
いまさらな感想ですが、Amplify ConsoleのUIが刷新されていたり、Amplify Admin UIが追加されていたりして、サービスの成長速度に驚きました。
さて、今回はFunctions(Lambda)を使ってDynamoDBに格納されたデータをバッチ処理する試みをしましたので、Amplifyを使う上でのノウハウの一つとして紹介したいと思います。
やりたいこと
AmplifyのAPIのバックエンドに作成されたDynamoDBに対してバッチ処理を行います。
今回は例として以下のような処理を実装しました。
- テーブル内に楽曲のタイトルが入っている
- 各楽曲のカバーアートURLをバッチ処理で取得・更新する
使用するツール,ライブラリのバージョン
$ node -v
v14.16.0
$ npm -v
7.10.0
$ amplify -v
4.48.0
下準備
サンプルアプリケーションの作成
本記事で実施する内容はフロントエンドと無関係ですが、実際にはNext.jsと組み合わせる形で使う想定なので、Next.jsのアプリケーションを作成しておきます。
# サンプルアプリケーションを作成
$ npx create-next-app --example with-typescript-eslint-jest
# サンプルアプリケーションを起動
$ cd batch-sample
$ npm run dev
Amplifyの初期化
Amplify CLIにて初期化します。
$ amplify init
? Enter a name for the project batchsample
? Initialize the project with the above configuration? No
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using react
? Source Directory Path: src
? Distribution Directory Path: out
? Build Command: npm run-script build
? Start Command: npm run-script start
Using default provider awscloudformation
? Select the authentication method you want to use: AWS profile
? Please choose the profile you want to use default
Amplify APIの作成
予めAPIのスキーマを作成しておきます。
type Song @model @auth(rules: [{ allow: public }]) {
id: ID!
type: String!
title: String!
artist: String!
coverArtUrl: String
}
上記要件に合うようなスキーマでAmplify APIを作成します。認証方法はAPI KEYとします。
$ amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: batchsample
? Choose the default authorization type for the API API key
? Enter a description for the API key: api key
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? Yes
? Provide your schema file path: schema.graphql
追加ができたら次のコマンドを実行してAPIをデプロイします。
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.
Current Environment: dev
| Category | Resource name | Operation | Provider plugin |
| -------- | ------------- | --------- | ----------------- |
| Api | batchsample | Create | awscloudformation |
? Are you sure you want to continue? Yes
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target typescript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.ts
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
? Enter the file name for the generated code src/API.ts
Amplify Functionの作成
バッチ処理を行うFunction(Lambda)を作成します。Amplify Functionは定周期トリガーを設定できるので、5分周期で起動するよう設定します。
$ amplify add function
? Select which capability you want to add: Lambda function (serverless function)
? Provide an AWS Lambda function name: spotifyapi
? Choose the runtime that you want to use: NodeJS
? Choose the function template that you want to use: Hello World
Available advanced settings:
- Resource access permissions
- Scheduled recurring invocation
- Lambda layers configuration
? Do you want to configure advanced settings? Yes
? Do you want to access other resources in this project from your Lambda function? Yes
? Select the categories you want this function to have access to. api
? Select the operations you want to permit on batchsample Query, Mutation
You can access the following resource attributes as environment variables from your Lambda function
API_BATCHSAMPLE_GRAPHQLAPIENDPOINTOUTPUT
API_BATCHSAMPLE_GRAPHQLAPIIDOUTPUT
API_BATCHSAMPLE_GRAPHQLAPIKEYOUTPUT
ENV
REGION
? Do you want to invoke this function on a recurring schedule? Yes
? At which interval should the function be invoked: Minutes
? Enter the rate in minutes: 5
? Do you want to configure Lambda layers for this function? No
? Do you want to edit the local lambda function now? No
出来上がったLambdaを確認すると、Amplify APIのエンドポイントやAPI KEYが環境変数として渡されています。これはとても助かりますね。
Amplify Functionの実装
Lambdaで実施する処理は以下の通りです。
- Amplify APIで楽曲を取得
- Spotify APIを使って、楽曲名からジャケットURLを取得
- Amplify APIで楽曲にジャケットURLを含めて更新
これを行うプログラムは以下の通りです。
/* Amplify Params - DO NOT EDIT
API_BATCHSAMPLE_GRAPHQLAPIENDPOINTOUTPUT
API_BATCHSAMPLE_GRAPHQLAPIIDOUTPUT
API_BATCHSAMPLE_GRAPHQLAPIKEYOUTPUT
ENV
REGION
Amplify Params - DO NOT EDIT */
const SpotifyWebApi = require('spotify-web-api-node')
const spotifyClient = async () => {
const client = new SpotifyWebApi({
clientId: 'xxx', // TODO: 環境変数化するとベター
clientSecret: 'xxx', // TODO: 環境変数化するとベター
redirectUri: 'http://localhost:8888',
})
client.setRefreshToken(
'xxx' // TODO: 環境変数化するとベター
)
const data = await client.refreshAccessToken()
client.setAccessToken(data.body['access_token'])
return client
}
const { GraphQLClient, gql } = require('graphql-request')
const graphqlClient = () => {
return new GraphQLClient(
process.env.API_BATCHSAMPLE_GRAPHQLAPIENDPOINTOUTPUT,
{
headers: { 'x-api-key': process.env.API_BATCHSAMPLE_GRAPHQLAPIKEYOUTPUT },
}
)
}
const query = gql`
{
listSongs {
items {
id
title
artist
}
}
}
`
const mutation = gql`
mutation UpdateSong($id: ID!, $coverArtUrl: String!) {
updateSong(input: { id: $id, coverArtUrl: $coverArtUrl }) {
id
coverArtUrl
}
}
`
exports.handler = async (event) => {
const graphql = graphqlClient()
const spotify = await spotifyClient()
// 楽曲一覧を取得
const res = await graphql.request(query)
const listSongs = await Promise.all(
res.listSongs.items.map(async (item) => {
// 楽曲情報をSpotifyから取得
const search_response = await spotify.searchTracks(item.title)
const track = search_response.body.tracks.items[0]
if (!track) return null
return {
id: item.id,
coverArtUrl: track.album.images[0].url,
spotifyUrl: track.external_urls.spotify,
}
})
)
await Promise.all(
listSongs
.filter((v) => v)
.map(async (item) => {
// カバーアートURLを更新
await graphql.request(mutation, {
id: item.id,
coverArtUrl: item.coverArtUrl,
youtubeUrl: item.spotifyUrl,
})
})
)
}
package.jsonの dependencies
に利用するパッケージの追加をしておきます。
{
"name": "spotifyapi",
"version": "2.0.0",
"description": "Lambda function generated by Amplify",
"main": "index.js",
"license": "Apache-2.0",
"dependencies": {
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"spotify-web-api-node": "^5.0.2"
}
}
Spotify APIの利用方法については次の記事を参考にしました。
動作確認
データの準備
Amplify Admin UIにログインして、サイドメニューの「GraphQL API」からAppSyncのコンソールへ移動します。
「クエリを実行」をクリックすると、GraphQLのPlaygroundを利用できます。
以下のようなmutationを実行することで、処理前の楽曲情報を追加します。
追加後のDynamoDBのSongsテーブルは次のようになりました。
デプロイ
Functionをデプロイします。
$ amplify push
しばらく待つとLambdaが実行され、DynamoDBに楽曲のカバーアートURLがセットされました。
ちなみに、デプロイをしなくてもTerminalで以下コマンドを叩くことで、手元の環境でFunctionを実行することができます。
$ amplify mock function spotifyapi
Amplifyへの期待
久々にAmplifyを触りましたが、インフラからバックエンドまでCLIですぐに作成できるのは良いですね。
一方、触っていて以下のような点で難しさを感じたので、メモしておきます。
- Amplify FunctionのトリガーがAPIかScheduleしか選択できないので、ちょっと不便
- DynamoDB Streamを使って、データを追加・更新したタイミングでFunctionが呼び出せると嬉しい
- Amplify FunctionのランタイムにRubyを追加してほしい
- Amplify APIはバックエンドがDynamoDBということもあり検索があまり得意ではない
- ElasticSearchとの連携が推奨されているが、スケーラビリティやコストで不利
- Aurora Serverlessとかと連携できると使いみちが拡がるのでは?
まとめ
AWS Amplifyを使ったシステムでバッチ処理を実装する際、参考になれば幸いです。
Discussion