はじめに
皆さん、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
でコンパイルを行います。
{
"scripts": {
"clean": "rimraf dist",
"tsc": "tsc",
"build": "npm-run-all clean tsc"
},
}
tsconfig.json
の作成
以下のコマンドを実行しTypeScriptの初期設定を行います。
$ npx tsc --init
それでは、作成された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
も作成しておきましょう。
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を記載する
ファイル内にコメントを残しています。
これで大まかには理解できるかと思います。
詳しくは公式サイトを見てください。
# 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
で検証
// 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つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。
①翻訳機能を作成
// パッケージのインストール
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が理解できたところで進めていきましょう。
// パッケージのインストール
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に保存
// パッケージのインストール
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が理解できたところで進めていきましょう。
// パッケージのインストール
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も勉強してみてください!