1
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-07-26

はじめに

皆さん、Lambdaをご存知でしょうか?
Lambdaはサーバーレスアーキテクチャを実現する上で根幹となるサービスです。

サーバーレスアーキテクチャとは

AWSにおけるサーバーレスとは、「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」を指します。

一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。
そしてそのサーバーは、常に稼働していなければなりません。

しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか?
エンドユーザーに価値を届けることこそが使命なわけです。

ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。

サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。
特に、丸で囲っている3つがよく使われます。

スクリーンショット 2021-06-26 15.43.25.png

ということで、この3つ全てを使った翻訳アプリを作りたいと思います。

また、構成やデプロイはAWS SAMを使用します。
AWS SAMを使うことでコマンドのみで環境構築やデプロイを行えます。

アーキテクチャ

以下の2つの条件を満たしたら成功です。

①LINEで「こんにちは」と入力したら、「Hello」と返ってくる
②タイムスタンプと「こんにちは」、「Hello」がDBに保存される

arch

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の実行をしてみたいところですが、一旦後回しにします。

image.png

先に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となるので、チャネルの種類を間違えた方は修正してください。

スクリーンショット 2021-07-26 14.10.05.png

チャネルシークレットチャネルアクセストークンが必要になるのでこの2つを発行します。

スクリーンショット 2021-05-29 16.16.20.png

スクリーンショット 2021-05-29 16.17.51.png

これで必要な環境変数は取得できました。
それでは、これをSSMを使ってLambda内で使えるようにしていきましょう。

SSMパラメータストアで環境変数を設定

なぜSSMパラメータストアを使うのか?

SAMのLambda設定にも、環境変数の項目はあります。

しかし、2点問題点があります。
①Lambdaの環境変数の変更をしたいとき、Lambdaのバージョンも新規発行をしなければならない
②Lambdaのバージョンとエイリアスを紐付けて管理をするとき、もし環境変数にリリース先環境別の値をセットしていると、リリース時に手動で環境変数の変更をしなければならないケースが発生する

簡単にまとめると、「リアルタイムで反映できないし、人為的なミスのリスクもあるよ」ということです。

SSMパラメータストアで値を管理すると以下の3点のメリットがあります。
①Lambdaの環境変数の管理が不要
②Lambdaも含めた値関連情報を一元管理できる
③Lambda外部からリアルタイムに環境変数を変更制御できる

ということで、SSMパラメータストアを使用しましょう。

みんな大好きクラスメソッドの記事にやり方が書いてあります。
こちらの記事が完璧なのでこちらを見てやってみてください。

私は以下のように命名して作成しました。

スクリーンショット 2021-07-26 14.12.35.png

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を確認しましょう。

スクリーンショット 2021-07-20 16.51.25.png

Webhook URLの登録

先ほどAPI Gatewayで作成したhttpsのURLをコピーしてください。

これをLINE DevelopersのWebhookに設定します。

スクリーンショット 2021-07-14 16.30.04.png

それではSSMパラメータが正しく取得できているか確認しましょう。
CloudWatchで確認しましょう!

スクリーンショット 2021-07-20 16.54.55.png

取得できていますね!

ここからの流れはこのような感じです。
①翻訳機能を作成
②翻訳された言葉を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

最後に動作検証をしましょう。

iOS の画像.png

DynamoDBも確認しましょう。

スクリーンショット 2021-07-26 14.29.33.png

しっかり保存されていますね!

最後に

以前すべて手作業で行いましたが、SAMを使うと効率的にデプロイが行えます。
SAMテンプレートの書き方を学ぶコストは発生しますが、1度作ればそれをそのまま使えるので汎用性も高いのでおすすめです。

サーバーレスアーキテクチャを勉強する方がいましたらぜひSAMも勉強してみてください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0