Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9328

【AWS Lambda×LINE Messaging API】AWS SAMで翻訳アプリを作ろう

$
0
0
はじめに 皆さん、Lambdaをご存知でしょうか? Lambdaはサーバーレスアーキテクチャを実現する上で根幹となるサービスです。 サーバーレスアーキテクチャとは AWSにおけるサーバーレスとは、「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」を指します。 一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。 そしてそのサーバーは、常に稼働していなければなりません。 しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか? エンドユーザーに価値を届けることこそが使命なわけです。 ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。 サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。 特に、丸で囲っている3つがよく使われます。 ということで、この3つ全てを使った翻訳アプリを作りたいと思います。 また、構成やデプロイはAWS SAMを使用します。 AWS SAMを使うことでコマンドのみで環境構築やデプロイを行えます。 アーキテクチャ 以下の2つの条件を満たしたら成功です。 ①LINEで「こんにちは」と入力したら、「Hello」と返ってくる ②タイムスタンプと「こんにちは」、「Hello」がDBに保存される GitHub 完成形のコードは以下となります。 ハンズオン 前提 初めてAWSを使う方に対しての注意です。 ルートユーザーで行うのはよろしくないので、全ての権限を与えたAdministratorユーザーを作っておいてください。 公式サイトはこちらです。 文章は辛いよって方は、初学者のハンズオン動画があるのでこちらからどうぞ。 sam initを実行する ゼロから書いていってもいいのですが、初めての方はまずはsam initを使いましょう。 以下のように選択していってください。 ターミナル $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 What package type would you like to use? 1 - Zip (artifact is a zip uploaded to S3) 2 - Image (artifact is an image uploaded to an ECR image repository) Package type: 1 Which runtime would you like to use? 1 - nodejs14.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs12.x 8 - nodejs10.x 9 - python3.7 10 - python3.6 11 - python2.7 12 - ruby2.5 13 - java8.al2 14 - java8 15 - dotnetcore2.1 Runtime: 7 Project name [sam-app]: Translate AWS quick start application templates: 1 - Hello World Example 2 - Step Functions Sample App (Stock Trader) 3 - Quick Start: From Scratch 4 - Quick Start: Scheduled Events 5 - Quick Start: S3 6 - Quick Start: SNS 7 - Quick Start: SQS 8 - Quick Start: App Backend using TypeScript 9 - Quick Start: Web Backend Template selection: 1 ここまでできれば作成されます。 このような構成になっていればOKです。 .Translate ├── events/ │ ├── event.json ├── hello-world/ │ ├── tests │ │ └── integration │ │ │ └── test-api-gateway.js │ │ └── unit │ │ │ └── test-handler.js │ ├── .npmignore │ ├── app.js │ ├── package.json ├── .gitignore ├── README.md ├── template.yaml 必要ないファイルなどがあるのでそれを削除していきましょう。 .Translate ├── hello-world/ │ ├── app.js ├── .gitignore ├── README.md ├── template.yaml また、ディレクトリ名やファイル名を変えましょう。 .Translate ├── api/ │ ├── index.js ├── .gitignore ├── README.md ├── template.yaml 次は、template.yamlを修正して、SAMの実行をしてみたいところですが、一旦後回しにします。 先にTypeScriptなどのパッケージを入れ、ディレクトリ構造を明確にした後の方が理解しやすいので。。 ということでパッケージを入れていきましょう。 package.jsonの作成 以下のコマンドを入力してください。 これで、package.jsonの作成が完了します。 ターミナル $ npm init -y 必要なパッケージのインストール dependencies dependenciesはすべてのステージで使用するパッケージです。 今回使用するパッケージは以下の4つです。 ・@line/bot-sdk ・aws-sdk 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install @line/bot-sdk aws-sdk --save ちなみに、Lambdaでは元よりaws-sdkが使えるようなのでなくても問題ないです。 インストールしなければその分容量が軽くなるので、レスポンスは早くなります。 devDependencies devDependenciesはコーディングステージのみで使用するパッケージです。 今回使用するパッケージは以下の5つです。 ・typescript ・@types/node ・ts-node ・rimraf ・npm-run-all 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install -D typescript @types/node ts-node rimraf npm-run-all package.jsonにコマンドの設定を行う npm run buildでコンパイルを行います。 package.json { "scripts": { "clean": "rimraf dist", "tsc": "tsc", "build": "npm-run-all clean tsc" }, } tsconfig.jsonの作成 以下のコマンドを実行しTypeScriptの初期設定を行います。 ターミナル $ npx tsc --init それでは、作成されたtsconfig.jsonの上書きをしていきます。 tsconfig.json { "compilerOptions": { "target": "ES2018", "module": "commonjs", "sourceMap": true, "outDir": "./api/dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["api/src/*"] } 簡単にまとめると、 api/srcディレクトリ以下を対象として、それらをapi/distディレクトリにES2018の書き方でビルドされるという設定です。 tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。 また、この辺りで必要ないディレクトリはGithubにpushしたくないので、.gitignoreも作成しておきましょう。 .gitignore node_modules package-lock.json .aws-sam samconfig.toml dist 最終的にはこのようなディレクトリ構成にしましょう。 .WeatherFashion ├── api/ │ ├── dist(コンパイル後) │ │ └── node_modules(コピーする) │ │ └── package.json(コピーする) │ ├── src(コンパイル前) │ │ └── index.ts ├── node_modules(コピー元) ├── .gitignore ├── package.json(コピー元) ├── package-lock.json ├── README.md ├── template.yaml ├── tsconfig.json やるべきことは以下の2つです。 ①distディレクトリを作成する ②distディレクトリに、node_modules, package.jsonをコピーする 次に、template.yamlを書いていきましょう。 SAM Templateを記載する ファイル内にコメントを残しています。 これで大まかには理解できるかと思います。 詳しくは公式サイトを見てください。 template.yaml # AWS CloudFormationテンプレートのバージョン AWSTemplateFormatVersion: '2010-09-09' # CloudFormationではなくSAMを使うと明記する Transform: AWS::Serverless-2016-10-31 # CloudFormationのスタックの説明文(重要ではないので適当でOK) Description: > Translate Globals: # Lambda関数のタイムアウト値(3秒に設定) Function: Timeout: 3 Resources: # API Gateway TranslateAPI: # Typeを指定する(今回はAPI Gateway) Type: AWS::Serverless::Api Properties: # ステージ名(APIのURLの最後にこのステージ名が付与されます) StageName: v1 # DynamoDB TranslateDynamoDB: # Typeを指定する(今回はDynamoDB) Type: AWS::Serverless::SimpleTable Properties: # テーブルの名前 TableName: translations # プライマリキーの設定(名前とプライマリキーのタイプ) PrimaryKey: Name: TimeStamp Type: String # プロビジョニングされたキャパシティの設定(今回の要件では最小の1でOK) ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 # Lambda TranslateFunction: # Typeを指定する(今回はLambda) Type: AWS::Serverless::Function Properties: # 関数が格納されているディレクトリ(今回はコンパイル後なので、distディレクトリを使用する) CodeUri: api/dist # ファイル名と関数名(今回はファイル名がindex.js、関数名がexports.handlerなので、index.handlerとなります) Handler: index.handler # どの言語とどのバージョンを使用するか Runtime: nodejs12.x # ポリシーを付与する(今回はLambdaの権限とSSMの読み取り権限とDynamoDBのフルアクセス権限とAmazon translateのフルアクセス権限を付与) Policies: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess - arn:aws:iam::aws:policy/TranslateFullAccess # この関数をトリガーするイベントを指定します Events: # API Gateway TranslateAPI: Type: Api Properties: # どのAPIを使用するか(!Refは値の参照に使用します) RestApiId: !Ref TranslateAPI # URL Path: / # POSTメソッド Method: post Outputs: TranslateAPI: Description: 'API Gateway' # URLを作成(!Subは${}で値を指定することができます) Value: !Sub 'https://${TranslateAPI}.execute-api.${AWS::Region}.amazonaws.com/v1' TranslateFunction: Description: 'Lambda' # ロールの値を返す Value: !GetAtt TranslateFunction.Arn TranslateFunctionIamRole: Description: 'IAM Role' # ロールの値を返す Value: !GetAtt TranslateFunctionRole.Arn LINE Developersにアカウントを作成する LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。 その後諸々入力してもらったら以下のように作成できるかと思います。 注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。 チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。 これで必要な環境変数は取得できました。 それでは、これをSSMを使ってLambda内で使えるようにしていきましょう。 SSMパラメータストアで環境変数を設定 なぜSSMパラメータストアを使うのか? SAMのLambda設定にも、環境変数の項目はあります。 しかし、2点問題点があります。 ①Lambdaの環境変数の変更をしたいとき、Lambdaのバージョンも新規発行をしなければならない ②Lambdaのバージョンとエイリアスを紐付けて管理をするとき、もし環境変数にリリース先環境別の値をセットしていると、リリース時に手動で環境変数の変更をしなければならないケースが発生する 簡単にまとめると、「リアルタイムで反映できないし、人為的なミスのリスクもあるよ」ということです。 SSMパラメータストアで値を管理すると以下の3点のメリットがあります。 ①Lambdaの環境変数の管理が不要 ②Lambdaも含めた値関連情報を一元管理できる ③Lambda外部からリアルタイムに環境変数を変更制御できる ということで、SSMパラメータストアを使用しましょう。 みんな大好きクラスメソッドの記事にやり方が書いてあります。 こちらの記事が完璧なのでこちらを見てやってみてください。 私は以下のように命名して作成しました。 SSMパラメータが取得できているかconsole.logで検証 api/src/index.ts // import import aws from 'aws-sdk'; // SSM const ssm = new aws.SSM(); exports.handler = async (event: any, context: any) => { const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; console.log('channelAccessToken: ' + channelAccessToken); }; これをコンパイルしてデプロイしていきましょう。 ターミナル // コンパイル $ npm run build // ビルド $ sam build // デプロイ $ sam deploy --guided Configuring SAM deploy ====================== Looking for samconfig.toml : Not found Setting default arguments for 'sam deploy' ========================================= // CloudFormation スタック名の指定 Stack Name [sam-app]: Translate // リージョンの指定 AWS Region [us-east-1]: ap-northeast-1 // デプロイ前にCloudformationの変更セットを確認するか #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y // SAM CLI に IAM ロールの作成を許可するか(CAPABILITY_IAM) #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y // API イベントタイプの関数に認証が含まれていない場合、警告される HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y // この設定を samconfig.toml として保存するか Save arguments to samconfig.toml [Y/n]: y これでデプロイが完了します。 では、API GatewayのURLを確認しましょう。 Webhook URLの登録 先ほどAPI Gatewayで作成したhttpsのURLをコピーしてください。 これをLINE DevelopersのWebhookに設定します。 それではSSMパラメータが正しく取得できているか確認しましょう。 CloudWatchで確認しましょう! 取得できていますね! ここからの流れはこのような感じです。 ①翻訳機能を作成 ②翻訳された言葉をDBに保存 今回は、翻訳する部分、DBにデータを登録する部分と様々な機能があるため動作ごとにファイルを切り分けてあげましょう。 以下のように作っていきます。 . ├── api/ │ ├── src/ │ │ ├── Common/ │ │ └── getTranslate.ts │ │ └── putDynamoDB.ts │ └── index.ts またここからはLINEBotのオリジナルの型が頻出します。 1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。 ①翻訳機能を作成 api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent, TextMessage } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールのインストール import { getTranslate } from './Common/getTranslate'; // SSM const ssm = new aws.SSM(); const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_TRANSLATE_CHANNEL_SECRET = { Name: 'LINE_TRANSLATE_CHANNEL_SECRET', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { try { // SSM (.env) const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_TRANSLATE_CHANNEL_SECRET).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; // client const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // JSONとして解析して値やオブジェクトを構築する const body: any = JSON.parse(event.body); // LINE Eventを取得 const response: WebhookEvent = body.events[0]; // 送られるメッセージがテキスト以外の場合 if (response.type !== 'message' || response.message.type !== 'text') { return; } // 翻訳を行うために必要な情報 const input_text: string = response.message.text; const sourceLang: string = 'ja'; const targetLang: string = 'en'; const res: any = await getTranslate(input_text, sourceLang, targetLang); const output_text: string = res.TranslatedText; // メッセージ送信のために必要な情報 const replyToken = response.replyToken; const post: TextMessage = { type: 'text', text: output_text, }; // メッセージの送信 await client.replyMessage(replyToken, post); } catch (err) { console.log(err); } }; では次に、getTranslate.tsを作っていきましょう。 コードだけ書いても訳がわからないと思うので、リファレンスを見ましょう。 入力されたテキストをソース言語からターゲット言語に変換する、translateTextを使います。 必須項目は以下の3つで、SourceLanguageCodeに元の言語コード、TargetLanguageCodeに変換先の言語コード、Textに変換するテキストを入れればいいことがわかります。 SourceLanguageCode: 'STRING_VALUE', /* required */ TargetLanguageCode: 'STRING_VALUE', /* required */ Text: 'STRING_VALUE', /* required */ そのあとはこのデータを実行するだけです。 translate.translateText(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); APIが理解できたところで進めていきましょう。 api/src/Common/getTranslate.ts // パッケージのインストール import aws from 'aws-sdk'; // 必要なAWSサービス const translate = new aws.Translate(); export const getTranslate = (input: string, inLang: string, outLang: string) => { return new Promise((resolve, reject) => { // 必要なデータ const params = { Text: input, SourceLanguageCode: inLang, TargetLanguageCode: outLang, }; // 翻訳を行う translate.translateText(params, (err, data) => { if (err) { console.log(err); reject(); } else { resolve(data); } }); }); }; ②翻訳された言葉をDBに保存 api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent, TextMessage } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールのインストール import { getTranslate } from './Common/getTranslate'; import { putDynamoDB } from './Common/putDynamoDB'; // SSM const ssm = new aws.SSM(); const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_TRANSLATE_CHANNEL_SECRET = { Name: 'LINE_TRANSLATE_CHANNEL_SECRET', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { try { // SSM (.env) const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_TRANSLATE_CHANNEL_SECRET).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; // client const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // JSONとして解析して値やオブジェクトを構築する const body: any = JSON.parse(event.body); // LINE Eventを取得 const response: WebhookEvent = body.events[0]; // 送られるメッセージがテキスト以外の場合 if (response.type !== 'message' || response.message.type !== 'text') { return; } // 翻訳を行うために必要な情報 const input_text: string = response.message.text; const sourceLang: string = 'ja'; const targetLang: string = 'en'; const res: any = await getTranslate(input_text, sourceLang, targetLang); const output_text: string = res.TranslatedText; // メッセージ送信のために必要な情報 const replyToken = response.replyToken; const post: TextMessage = { type: 'text', text: output_text, }; // メッセージの送信 await client.replyMessage(replyToken, post); // DB-タイムスタンプ const date = new Date(); const Y = date.getFullYear(); const M = ('00' + (date.getMonth() + 1)).slice(-2); const D = ('00' + date.getDate()).slice(-2); const h = ('00' + (date.getHours() + 9)).slice(-2); const m = ('00' + date.getMinutes()).slice(-2); const s = ('00' + date.getSeconds()).slice(-2); const dayTime = Y + M + D + h + m + s; // DynamoDB保存 await putDynamoDB(dayTime, input_text, output_text); } catch (err) { console.log(err); } }; 次に、putDynamoDB.tsを作ります。 コードだけ書いても訳がわからないと思うので、リファレンスを見ましょう。 アイテム(レコード)を作成したいので、putItemを使います。 必須項目は以下の3つで、Itemにデータ、ReturnConsumedCapacityに集計、TableNameにテーブルの名前を入れればいいことがわかります。 var params = { Item: { "AlbumTitle": { S: "Somewhat Famous" }, "Artist": { S: "No One You Know" }, "SongTitle": { S: "Call Me Today" } }, ReturnConsumedCapacity: "TOTAL", TableName: "Music" }; そのあとはこのデータを実行するだけです。 dynamodb.putItem(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); APIが理解できたところで進めていきましょう。 api/src/Common/putDynamoDB.ts // パッケージのインストール import aws from 'aws-sdk'; // 必要なAWSサービス const dynamodb = new aws.DynamoDB(); export const putDynamoDB = (dayTime: string, input: string, output: string) => { return new Promise((resolve, reject) => { const params = { Item: { TimeStamp: { S: dayTime, }, InputText: { S: input, }, OutputText: { S: output, }, }, ReturnConsumedCapacity: 'TOTAL', TableName: 'translations', }; dynamodb.putItem(params, (err, data) => { if (err) { console.log(err); reject(err); } else { resolve(data); } }); }); }; これで完成です! では、デプロイしていきましょう。 デプロイ まずは、npm run buildでコンパイルしましょう。 ターミナル $ npm run build コンパイルされた後は、ビルドしてデプロイしていきましょう。 ターミナル // ビルド $ sam build // デプロイ $ sam deploy --guided 最後に動作検証をしましょう。 DynamoDBも確認しましょう。 しっかり保存されていますね! 最後に 以前すべて手作業で行いましたが、SAMを使うと効率的にデプロイが行えます。 SAMテンプレートの書き方を学ぶコストは発生しますが、1度作ればそれをそのまま使えるので汎用性も高いのでおすすめです。 サーバーレスアーキテクチャを勉強する方がいましたらぜひSAMも勉強してみてください!

Viewing all articles
Browse latest Browse all 9328

Trending Articles