先日、Node.jsとTypeScriptで天気予報アプリを作成しました。
この記事内では、Glitch
でデプロイをしています。
まぁ無料なわけで色々問題があります。
・プロジェクトは、利用されていないときは5分でスリープ状態になる
・4000件/1hのリクエスト制限がある(Error: 429 too many requests)
ということで、AWSのLambdaを使ってデプロイすることにします。
それではアーキテクチャに関してみていきましょう。
サーバーレスアーキテクチャとは
AWSにおけるサーバーレスとは、**「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」**を指します。
一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。
そしてそのサーバーは、常に稼働していなければなりません。
しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか?
エンドユーザーに価値を届けることこそが使命なわけです。
ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。
サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。
特に、丸で囲っている3つがよく使われます。
今回は、Lambda
, API Gateway
, S3
の3つを使ってアプリを作成していきます。
アーキテクチャ
追記
AWSのEC2を使ってデプロイした記事もあります。
サーバーレスよりもEC2に興味があるぞという方はこちらの記事もどうぞ。
どのようなアプリか
皆さんは、今日の気温を聞いて、**「快適に過ごすために今日のファッションをこうしよう」**ってパッと思いつきますか?
私は、最高気温、最低気温を聞いてもそれがどのくらい暑いのか、寒いのかがピンと来ず、洋服のチョイスを外したことがしばしばあります。
こんな思いを2度としないために今回このアプリを作りました。
line-bot-sdk-nodejs
の型定義で多少躓きましたが、TypeScript初心者でもそこまで時間かからずにできるかと思います。
なので、TypeScriptを勉強中の方はぜひ取り組んでみてください。
アプリの流れ
アプリの流れは大まかに以下の4つのステップで成り立っています。
・①クライアントが現在地を送る
・②OpenWeatherから天気予報を取得
・③データの整形
・④クライアントに送る
GitHub
完成形のコードは以下となります。
では実際に作成していきましょう!
AWSの環境構築を行う
上記で説明した通り、今回はAWSの3つのサービスを使用します。
その3つのサービスの環境を構築しましょう。
Lambda
まずは関数を作成しましょう。
では設定を行います。
これで終了です。
API Gateway
APIを作成しましょう。
REST API
を選択します。
適当にAPI名をつけて作成しましょう。
作成できたらメソッドを作成しましょう。
メソッドはPOST
になります。
Lambda関数を呼び出せるようにします。
最後にデプロイをしてURLを発行します。
URLができました。
このURLを叩くと、Lambda関数が実行されることになります。
S3
バケットの作成を行います。
これで完了です。
LINE Developers
にアカウントを作成する
LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。
その後諸々入力してもらったら以下のように作成できるかと思います。
注意事項としては、今回Messaging API
となるので、チャネルの種類を間違えた方は修正してください。
チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。
ではこの2つをLambdaの環境変数
に入力します。
package.json
の作成
以下のコマンドを入力してください。
これで、package.json
の作成が完了します。
$ npm init -y
必要なパッケージのインストール
dependencies
dependencies
はすべてのステージで使用するパッケージです。
今回使用するパッケージは以下の4つです。
・@line/bot-sdk
・dotenv
・axios
以下のコマンドを入力してください。
これで全てのパッケージがインストールされます。
$ npm install @line/bot-sdk dotenv axios --save
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 dev
が開発環境の立ち上げに使います。
npm run start
が本番環境用です。
{
"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
も作成しておきましょう。
_.drawio
node_modules
package-lock.json
dist
Webhook URLの登録
先ほどAPI Gateway
で作成したhttpsのURLをコピーしてください。
これをLINE DevelopersのWebhookに設定します。
これで初期設定は完了です。
ここからの流れはこのような感じです。
①「今日の洋服は?」というメッセージを受け取る
②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る
③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る
④「位置情報メッセージ」を受け取る
⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する
⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る
では作っていきましょう!
またこれら全てのコードをapi/src/index.ts
に書くとコードが肥大化し可読性が落ちます。
なのでCommon
ディレクトリに関数に切り分けて作成していきます。
またここからはLINEBotのオリジナルの型が頻出します。
1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。
①「今日の洋服は?」というメッセージを受け取る
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';
// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.CHANNEL_SECRET || '',
};
// インスタンス化
const client: Client = new Client(clientConfig);
// 実行
exports.handler = async (event: any, context: any) => {
const body: any = JSON.parse(event.body);
const response: WebhookEvent = body.events[0];
try {
await actionButtonOrErrorMessage(response);
} catch (err) {
console.log(err);
}
};
// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
try {
if (event.type !== 'message' || event.message.type !== 'text') {
return;
}
const { replyToken } = event;
const { text } = event.message;
if (text === '今日の洋服は?') {
// 「今日の洋服は?」というメッセージを受け取る
} else {
// 「今日の洋服は?」以外のメッセージを受け取る
}
} catch (err) {
console.log(err);
}
};
②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る
// パッケージを読み込む
import { TemplateMessage } from '@line/bot-sdk';
export const buttonMessageTemplate = (): Promise<TemplateMessage> => {
return new Promise((resolve, reject) => {
const params: TemplateMessage = {
type: 'template',
altText: 'This is a buttons template',
template: {
type: 'buttons',
text: '今日はどんな洋服にしようかな',
actions: [
{
type: 'uri',
label: '現在地を送る',
uri: 'https://line.me/R/nv/location/',
},
],
},
};
resolve(params);
});
};
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';
// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';
// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.CHANNEL_SECRET || '',
};
// インスタンス化
const client: Client = new Client(clientConfig);
// 実行
exports.handler = async (event: any, context: any) => {
const body: any = JSON.parse(event.body);
const response: WebhookEvent = body.events[0];
try {
await actionButtonOrErrorMessage(response);
} catch (err) {
console.log(err);
}
};
// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
try {
if (event.type !== 'message' || event.message.type !== 'text') {
return;
}
const { replyToken } = event;
const { text } = event.message;
if (text === '今日の洋服は?') {
const buttonMessage = await buttonMessageTemplate();
await client.replyMessage(replyToken, buttonMessage);
} else {
// 「今日の洋服は?」以外のメッセージを受け取る
}
} catch (err) {
console.log(err);
}
};
ボタンメッセージのJSON作成に関しては公式サイトを参考にしましょう。
③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る
// パッケージを読み込む
import { TextMessage } from '@line/bot-sdk';
export const errorMessageTemplate = (): Promise<TextMessage> => {
return new Promise((resolve, reject) => {
const params: TextMessage = {
type: 'text',
text: 'ごめんなさい、このメッセージは対応していません。',
};
resolve(params);
});
};
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';
// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';
import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate';
// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.CHANNEL_SECRET || '',
};
// インスタンス化
const client: Client = new Client(clientConfig);
// 実行
exports.handler = async (event: any, context: any) => {
const body: any = JSON.parse(event.body);
const response: WebhookEvent = body.events[0];
try {
await actionButtonOrErrorMessage(response);
} catch (err) {
console.log(err);
}
};
// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
try {
if (event.type !== 'message' || event.message.type !== 'text') {
return;
}
const { replyToken } = event;
const { text } = event.message;
if (text === '今日の洋服は?') {
const buttonMessage = await buttonMessageTemplate();
await client.replyMessage(replyToken, buttonMessage);
} else {
const errorMessage = await errorMessageTemplate();
await client.replyMessage(replyToken, errorMessage);
}
} catch (err) {
console.log(err);
}
};
テキストメッセージのJSON作成に関しては公式サイトを参考にしましょう。
④「位置情報メッセージ」を受け取る
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';
// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';
import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate';
import { flexMessageTemplate } from './Common/WeatherForecastMessage/FlexMessageTemplate';
// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.CHANNEL_SECRET || '',
};
// インスタンス化
const client: Client = new Client(clientConfig);
// 実行
exports.handler = async (event: any, context: any) => {
const body: any = JSON.parse(event.body);
const response: WebhookEvent = body.events[0];
try {
await actionButtonOrErrorMessage(response);
await actionFlexMessage(response);
} catch (err) {
console.log(err);
}
};
// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
try {
if (event.type !== 'message' || event.message.type !== 'text') {
return;
}
const { replyToken } = event;
const { text } = event.message;
if (text === '今日の洋服は?') {
const buttonMessage = await buttonMessageTemplate();
await client.replyMessage(replyToken, buttonMessage);
} else {
const errorMessage = await errorMessageTemplate();
await client.replyMessage(replyToken, errorMessage);
}
} catch (err) {
console.log(err);
}
};
const actionFlexMessage = async (event: WebhookEvent) => {
try {
if (event.type !== 'message' || event.message.type !== 'location') {
return;
}
// 「位置情報メッセージ」を受け取る
} catch (err) {
console.log(err);
}
};
⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する
Flex Messageの作成方法に関してファイル名も出しながら説明します。
【ファイル名】GetWeatherForecast.ts
天気予報を取得します。
まずはOpenWeatherで天気予報を取得するために必要な情報が3つあります。
①API
②経度
③緯度
それではこの3つを取得していきましょう。
①API
以下にアクセスしてください。
アカウントを作成し、APIキーを発行してください。
発行できたらこのAPIをLambdaの環境変数
に入力します。
②経度、③緯度
これら2つは、eventから取得できます。
ということで作っていきましょう。
// Load the package
import { WebhookEvent } from '@line/bot-sdk';
import axios, { AxiosResponse } from 'axios';
export const getWeatherForecastData = async (event: WebhookEvent): Promise<any> => {
return new Promise(async (resolve, reject) => {
try {
if (event.type !== 'message' || event.message.type !== 'location') {
return;
}
// Get latitude and longitude
const latitude: number = event.message.latitude;
const longitude: number = event.message.longitude;
// OpenWeatherAPI
const openWeatherAPI: string | undefined = process.env.WEATHER_API || '';
// OpenWeatherURL
const openWeatherURL: string = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&units=metric&lang=ja&appid=${openWeatherAPI}`;
const weatherData: AxiosResponse<any> = await axios.get(openWeatherURL);
resolve(weatherData);
} catch (err) {
reject(err);
}
});
};
【ファイル名】FormatWeatherForecast.ts
取得した天気予報のデータの整形を行う。
こちらでは、const weather
とconst weatherArray
の2つで型定義ファイルを作成する必要があります。
ということで作成しましょう。
export type WeatherType = {
dt: number;
sunrise: number;
sunset: number;
moonrise: number;
moonset: number;
moon_phase: number;
temp: {
day: number;
min: number;
max: number;
night: number;
eve: number;
morn: number;
};
feels_like: {
day: number;
night: number;
eve: number;
morn: number;
};
pressure: number;
humidity: number;
dew_point: number;
wind_speed: number;
wind_deg: number;
wind_gust: number;
weather: [
{
id: number;
main: string;
description: string;
icon: string;
}
];
clouds: number;
pop: number;
rain: number;
uvi: number;
};
export type WeatherArrayType = {
today: string;
imageURL: string;
weatherForecast: string;
mornTemperature: number;
dayTemperature: number;
eveTemperature: number;
nightTemperature: number;
fashionAdvice: string;
};
作成した型定義を使ってファイルを完成させます。
// Load the package
import { WebhookEvent } from '@line/bot-sdk';
import { AxiosResponse } from 'axios';
// Load the module
import { getWeatherForecastData } from './GetWeatherForecast';
// types
import { WeatherType, WeatherArrayType } from './types/FormatWeatherForecast.type';
export const formatWeatherForecastData = async (event: WebhookEvent): Promise<WeatherArrayType> => {
return new Promise(async (resolve, reject) => {
// Get the getWeatherForecastData
const weathers: AxiosResponse<any> = await getWeatherForecastData(event);
// Util
const weather: WeatherType = weathers.data.daily[0];
// Five required data
// 1) Today's date
const UNIXToday: number = weather.dt;
const convertUNIXToday: Date = new Date(UNIXToday * 1000);
const today: string = convertUNIXToday.toLocaleDateString('ja-JP');
// 2) Weather forecast
const weatherForecast: string = weather.weather[0].description;
// 3) Temperature (morning, daytime, evening, night)
const mornTemperature: number = weather.feels_like.morn;
const dayTemperature: number = weather.feels_like.day;
const eveTemperature: number = weather.feels_like.eve;
const nightTemperature: number = weather.feels_like.night;
// Bifurcate your clothing by maximum temperature
const maximumTemperature: number = Math.max(
mornTemperature,
dayTemperature,
eveTemperature,
nightTemperature
);
// 4) Fashion Advice
let fashionAdvice: string = '';
// 5) Fashion Image
let imageURL: string = '';
if (maximumTemperature >= 26) {
fashionAdvice =
'暑い!半袖が活躍する時期です。少し歩くだけで汗ばむ気温なので半袖1枚で大丈夫です。ハットや日焼け止めなどの対策もしましょう';
imageURL =
'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/60aa3c44153071e6df530eb7_71.png';
} else if (maximumTemperature >= 21) {
fashionAdvice =
'半袖と長袖の分かれ目の気温です。日差しのある日は半袖を、曇りや雨で日差しがない日は長袖がおすすめです。この気温では、半袖の上にライトアウターなどを着ていつでも脱げるようにしておくといいですね!';
imageURL =
'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e58a5923ad81f73ac747_10.png';
} else if (maximumTemperature >= 16) {
fashionAdvice =
'レイヤードスタイルが楽しめる気温です。ちょっと肌寒いかな?というくらいの過ごしやすい時期なので目一杯ファッションを楽しみましょう!日中と朝晩で気温差が激しいので羽織ものを持つことを前提としたコーディネートがおすすめです。';
imageURL =
'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6087da411a3ce013f3ddcd42_66.png';
} else if (maximumTemperature >= 12) {
fashionAdvice =
'じわじわと寒さを感じる気温です。ライトアウターやニットやパーカーなどが活躍します。この時期は急に暑さをぶり返すことも多いのでこのLINEで毎日天気を確認してくださいね!';
imageURL =
'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e498e7d26507413fd853_4.png';
} else if (maximumTemperature >= 7) {
fashionAdvice =
'そろそろ冬本番です。冬服の上にアウターを羽織ってちょうどいいくらいです。ただし室内は暖房が効いていることが多いので脱ぎ着しやすいコーディネートがおすすめです!';
imageURL =
'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e4de7156326ff560b1a1_6.png';
} else {
fashionAdvice =
'凍えるほどの寒さです。しっかり厚着して、マフラーや手袋、ニット帽などの冬小物もうまく使って防寒対策をしましょう!';
imageURL =
'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056ebd3ea0ff76dfc900633_48.png';
}
// Make an array of the above required items.
const weatherArray: WeatherArrayType = {
today,
imageURL,
weatherForecast,
mornTemperature,
dayTemperature,
eveTemperature,
nightTemperature,
fashionAdvice,
};
resolve(weatherArray);
});
};
【ファイル名】FlexMessageTemplate
整形したデータを取得して Flex Messageのテンプレートを作成する。
// Load the package
import { WebhookEvent, FlexMessage } from '@line/bot-sdk';
// Load the module
import { formatWeatherForecastData } from './FormatWeatherForecast';
export const flexMessageTemplate = async (event: WebhookEvent): Promise<FlexMessage> => {
return new Promise(async (resolve, reject) => {
const data = await formatWeatherForecastData(event);
resolve({
type: 'flex',
altText: '天気予報です',
contents: {
type: 'bubble',
header: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: data.today,
color: '#FFFFFF',
align: 'center',
weight: 'bold',
},
],
},
hero: {
type: 'image',
url: data.imageURL,
size: 'full',
},
body: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: `天気は、「${data.weatherForecast}」です`,
weight: 'bold',
align: 'center',
},
{
type: 'text',
text: '■体感気温',
margin: 'lg',
},
{
type: 'text',
text: `朝:${data.mornTemperature}℃`,
margin: 'sm',
size: 'sm',
color: '#C8BD16',
},
{
type: 'text',
text: `日中:${data.dayTemperature}℃`,
margin: 'sm',
size: 'sm',
color: '#789BC0',
},
{
type: 'text',
text: `夕方:${data.eveTemperature}℃`,
margin: 'sm',
size: 'sm',
color: '#091C43',
},
{
type: 'text',
text: `夜:${data.nightTemperature}℃`,
margin: 'sm',
size: 'sm',
color: '#004032',
},
{
type: 'separator',
margin: 'xl',
},
{
type: 'text',
text: '■洋服アドバイス',
margin: 'xl',
},
{
type: 'text',
text: data.fashionAdvice,
margin: 'sm',
wrap: true,
size: 'xs',
},
],
},
styles: {
header: {
backgroundColor: '#00B900',
},
hero: {
separator: false,
},
},
},
});
});
};
⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る
// パッケージのインストール
import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk';
// モジュールを読み込む
import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate';
import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate';
import { flexMessageTemplate } from './Common/WeatherForecastMessage/FlexMessageTemplate';
// アクセストークンとチャンネルシークレットをenvから読み込む
const clientConfig: ClientConfig = {
channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: process.env.CHANNEL_SECRET || '',
};
// インスタンス化
const client: Client = new Client(clientConfig);
// 実行
exports.handler = async (event: any, context: any) => {
const body: any = JSON.parse(event.body);
const response: WebhookEvent = body.events[0];
try {
await actionButtonOrErrorMessage(response);
await actionFlexMessage(response);
} catch (err) {
console.log(err);
}
};
// ボタンメッセージもしくはエラーメッセージを送る関数
const actionButtonOrErrorMessage = async (event: WebhookEvent) => {
try {
if (event.type !== 'message' || event.message.type !== 'text') {
return;
}
const { replyToken } = event;
const { text } = event.message;
if (text === '今日の洋服は?') {
const buttonMessage = await buttonMessageTemplate();
await client.replyMessage(replyToken, buttonMessage);
} else {
const errorMessage = await errorMessageTemplate();
await client.replyMessage(replyToken, errorMessage);
}
} catch (err) {
console.log(err);
}
};
const actionFlexMessage = async (event: WebhookEvent) => {
try {
if (event.type !== 'message' || event.message.type !== 'location') {
return;
}
const { replyToken } = event;
const message = await flexMessageTemplate(event);
await client.replyMessage(replyToken, message);
} catch (err) {
console.log(err);
}
};
これで完成です!
npm run build
でコンパイルしましょう。
$ npm run build
S3にアップロードしましょう
コンパイルするとdist
ディレクトリに2つのファイルと1つのフォルダがあるかと思います。
ファイル | フォルダ |
---|---|
index.js | Common |
index.js.map |
これら3つとnode_modules
でzipを作成しましょう。
私は__dist.zip
という名前で作成しました。
ちなみになぜnode_modulesが必要になるかなのですが、
現在Lambda関数内でLINEのパッケージが使える状況にありません。
なのでLambda関数内で使えるようにするために、node_modules
をLambdaに渡す必要があるのです。
アップロードはめちゃくちゃ簡単です。
ただドラッグアンドドロップするだけです。
アップロードが完了するとオブジェクトURLが設定されるのでこれをLambdaにアップロードします。
これで完成です!
最後に
今回はすべて手作業で行いました。
所々デバッグしながら進めていきたいですが、
そのためには毎回S3にアップしてそれをLambdaでもアップするという作業が必要になります。
こんなのめんどくさいですよねw
ということで、そんな人のためにSAMというものがあります。
SAMを使えば、ローカルでデバッグやテストができるようになるとともに、
コマンド1つでデプロイすることが可能になります。
次回は今回作ったアプリをSAMを使って作りたいと思います。