はじめに
サーバレスのサービスと言えばAWS Lambdaということで、
AWS LambdaでNode.jsアプリを作ります。
AWSマネジメントコンソールでいきなりコードを書き始めることは少なく、ローカル開発環境で動作を確認してからLambda化すると思います。このときソースコードをアップロードする方法を共有します。
この記事を読むとわかること
- AWS Lambda関数の作り方がわかります。
- AWS Lambda関数でNode.jsのアプリを動かす方法がわかります。
- マネジメントコンソールではなく、ローカルで作成したコードをAWS Lambdaで動かす方法がわかります。
npm install
したモジュールをLambdaで使う方法がわかります。- 環境変数の設定方法がわかります。
【参考】この記事で学んだことを使って、アプリケーションを作ってみました! →
前提
- AWSのアカウントがあること。
- ローカルにNode.jsがインストールされていること。
- 私は仮想環境(VirtualBox/ubuntu64_18)で実施しています。
全体の流れ
- Lambda関数を作成
- ローカルで関数を開発
- S3にアップロード
- S3からLambda関数へアップロード
このような流れで説明していきます。
Lambda関数を作成
Lambda関数の箱を作ります。
「一から作成」を選びます。
- ランタイム:Node.js 16.x(2022/5/21現在)
- アーキテクチャ:x86_64
- デフォルト実行ロールの変更: 新しいロールを作成
hello-world関数の確認・テスト
既に、簡単なアプリが記載されたindex.js
が用意されています。
「テスト」タブからテストを実行すると、結果ログが返ってきます。
ローカルでNode.jsアプリを作る
初期設定
ローカルでいつも通りNode.jsアプリを作成します。
$ npm init -y
$ touch index.js
$ npm install
GitHubとも連携しておきます。
$ git init
$ git remote add origin git@github.com:<ユーザ名>/<リポジトリ名>.git
$ git branch -m main
$ git add .
$ git commit -m "initial commit"
$ git push origin main
Node.jsアプリ作成
アプリを作成します。twitterのAPIを呼び出して、「直近30分」と「昨日の同じ時間」に特定の言葉が呼び出された数を取得し、数を比較する関数です。
詳細は別記事へ。
フォルダ構成
フォルダ構成は以下のようになります。
twitter-app
├── .env // 環境変数を設定する場合(後述)
├── .git
│ └── ・・・
├── .gitignore
├── index.js // メインファイル
├── node_modules // ※Node.js特有
│ └── ・・・
├── package-lock.json // ※Node.js特有
├── package.json // ※Node.js特有
└── utils // index.jsから呼び出される関数を記載
└── utils.js
ソースコード
全文は最下部に記載します。
ポイントは以下。
return
で値を返却する関数index
を作成index
関数を、module.exports.handler=index
としてエクスポートする
const index = async () => {
// 省略 //
// ログ出力
console.log(`今日のカウント:${todayCount}`)
console.log(`昨日のカウント:${yesterdayCount}`)
if (yesterdayCount !== 0) {
const growthRate = todayCount / yesterdayCount;
// true or falseを返却
return growthRate > 10;
} else {
// true or falseを返却
return todayCount > 100;
}
};
// lambdaのハンドラ関数としてエクスポート
module.exports.handler = index
モジュールインストール
必要なモジュールをインストールします。
ここでインストールしたnode_modules
フォルダもS3アップロード対象となります。
今回、2つのモジュールを利用します。
- axios:APIの呼び出しに利用します。
- dotenv:環境変数の呼び出しに使用します。
開発環境でインストールしておきます。
$ npm install axios dotenv
環境変数の設定
twitter APIにはAPIアクセスキーが必要ですが、秘密情報はGitHub等にpushしてはいけないので、環境変数化します。
TwitterのAPIに限らず、非公開の情報はここに追加していきます。
TOKEN=Twitterから払いだされたAPIキー
※.gitignore
に.env
が含まれることを忘れずに確認する。
ローカルでお試し実行
index.js
の最下部に
index()
を一時的に追加し、ローカルで実行してみます。
$ node index.js
今日のカウント:22 # console.logでログが出力された
昨日のカウント:14
確認用のconsole.log
が問題なく実行されていることがわかります。
開発環境で作成した関数をLambdaで実行する
S3へコードをアップロード
以下の5つのファイル/フォルダをzip化します。
- index.js
- node_modules
- package-lock.json
- package.json
- utils
zip化は右クリックでもよいですが、面倒なのでコマンドを用意しておきます。
$ zip -r package.zip index.js node_modules/ package-lock.json package.json utils/
# 構文:zip -r package.zip [ファイル名] [ファイル名] [ファイル名]
zip化できたら、S3に適当なフォルダを作成してアップロードします。
ドラッグ&ドロップでよいですが、AWS CLIをインストールすればコマンドでアップロード可能です。
$ aws s3 cp package.zip s3://{バケット名}/
ローカルでインストールしたモジュールの反映方法
node_modulesフォルダをzipファイルに一緒に入れる
ことで、インストールが必要なモジュールを利用できます。S3からLambdaへアップロード
S3のURLをメモしておきます。
マネジメントコンソールのLambda関数管理画面に戻ります。
アップロード元を「Amazon S3の場所」とし、
これでソースコードをLambdaに設定するができました。
環境変数の設定
「設定」タブから環境変数を設定します。
Lambda実行テスト
テストタブからテストを実行すると、ログを確認することができます。
欲しい値(関数の返り値)を取得することができました!!
完了です。
つまづいたところ
ここまで来るのにつまづいた3点の注意喚起です。
- 外部モジュールの入れ方がわからない
- イベントハンドラが無い
- 環境変数を間違えた
外部モジュールが無い
解決策:圧縮ファイルにnode_modules
フォルダを入れる
圧縮ファイルにnode_modules
フォルダを入れない場合、外部モジュール(ここではaxios
が存在しないエラーが出ました。
{
"errorType": "Runtime.ImportModuleError",
"errorMessage": "Error: Cannot find module 'axios'\nRequire stack:\n- /var/task/utils/utils.js\n- /var/task/index.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js", "stack": [
"Runtime.ImportModuleError: Error: Cannot find module 'axios'",
"Require stack:",
// 省略 //
]
}
イベントハンドラが無い
Lambdaは、アップロードされたソースコードの中からhandler
という関数を探して実行してくれます。
正しい名前で、関数としてエクスポートしないとエラーが発生します。
解決策:index.js
で関数を定義し、handler
という名前でエクスポートする。
module.exports.handler = index
この定義が無いと以下のようなエラーが出ます。
{
"errorType": "Runtime.HandlerNotFound", "errorMessage": "index.handler is undefined or not exported",
"stack": [
"Runtime.HandlerNotFound: index.handler is undefined or not exported",
// 省略 //
]
}
環境変数の大文字小文字まちがい
環境変数の大文字小文字を間違いって入れておりました。
解決策:環境変数を大文字小文字まで正しく入れる。
環境変数がうまく設定できていない場合に出るエラーは様々ですが、このアプリの場合はTwitter APIの認証キーを取得できなかったため、axios
で「認証エラー」が出ました。
INFO [AxiosError: Request failed with status code 401] {
code: 'ERR_BAD_REQUEST', config: {
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
},
// 省略 //
headers: {
Accept: 'application/json, text/plain, */*',
Authorization: 'Bearer undefined', 'User-Agent': 'axios/0.27.2'
},
method: 'get',
url: 'https://api.twitter.com/2/tweets/counts/recent?query=%E6%B1%A0%E8%A2%8B%E7%B7%9A&start_time=2022-05-15T00:13:13.760Z&end_time=2022-05-15T00:43:13.760Z&granularity=day',
data: undefined
},
// 省略 //
data: {
title: 'Unauthorized',
type: 'about:blank',
status: 401, detail: 'Unauthorized'
}
}
}
まとめ
ソースコードをS3にアップロードしてLambda関数を実行する方法を試しました。
- 環境変数の設定方法
- 外部モジュールのインストール方法
等も示しました。
関連記事
今のままでは手動実行しかできないので、Lambda関数を実行するトリガーを設定する必要があります。
トリガーにも様々ありますが、EventBridgeによる定期実行手順を行ないます。
参考文献
【参考】ソースコード全文
ソースコード
const { setDateTime, getTweetCount } = require("./utils/utils");
const index = async () => {
const now = new Date();
const now1mAgo = setDateTime(now, 0, 1);
const yesterday = setDateTime(now, 1, 1);
const now30mAgo = setDateTime(now, 0, 31);
const yesterday30mAgo = setDateTime(now, 1, 31);
const todayData = await getTweetCount(
now30mAgo.toISOString(),
now1mAgo.toISOString(),
"東横線"
);
const yesterdayData = await getTweetCount(
yesterday30mAgo.toISOString(),
yesterday.toISOString(),
"東横線"
);
const todayCount = todayData.data[0].tweet_count;
const yesterdayCount = yesterdayData.data[0].tweet_count;
console.log(`今日のカウント:${todayCount}`)
console.log(`昨日のカウント:${yesterdayCount}`)
if (yesterdayCount !== 0) {
const growthRate = todayCount / yesterdayCount;
return growthRate > 10;
} else {
return todayCount > 100;
}
};
module.exports.handler = index
const axios = require("axios");
require("dotenv").config();
module.exports.setDateTime = (datetime, day = 0, minute = 0) => {
return new Date(
datetime.getFullYear(),
datetime.getMonth(),
datetime.getDate() - day,
datetime.getHours(),
datetime.getMinutes() - minute,
datetime.getSeconds(),
datetime.getMilliseconds()
);
};
module.exports.getTweetCount = async (startTime, endTime, query) => {
const url = "https://api.twitter.com/2/tweets/counts/recent";
const granularity = "day";
const token = process.env.TOKEN;
const req = encodeURI(
`${url}?query=${query}&start_time=${startTime}&end_time=${endTime}&granularity=${granularity}`
);
try {
const res = await axios.get(req, { headers: { Authorization: `Bearer ${token}` } });
return res.data;
} catch (error) {
console.log(error);
}
};