AWSLambdaにローカルからデプロイする最も単純な方法【S3】

2022/5/21

(最終更新: 2022/5/21

はじめに

サーバレスのサービスと言えばAWS Lambdaということで、

AWS LambdaでNode.jsアプリを作ります。

AWSマネジメントコンソールでいきなりコードを書き始めることは少なく、ローカル開発環境で動作を確認してからLambda化すると思います。このときソースコードをアップロードする方法を共有します。

この記事を読むとわかること

  • AWS Lambda関数の作り方がわかります。
  • AWS Lambda関数でNode.jsのアプリを動かす方法がわかります。
  • マネジメントコンソールではなく、ローカルで作成したコードをAWS Lambdaで動かす方法がわかります。
    • npm installしたモジュールをLambdaで使う方法がわかります。
    • 環境変数の設定方法がわかります。

【参考】この記事で学んだことを使って、アプリケーションを作ってみました! →

【AWS入門】電車遅延をSMS通知してくれるアプリを作成した(TwitterAPI利用)

【AWS入門】電車遅延をSMS通知してくれるアプリを作成した(TwitterAPI利用)

AWSサービスを使ってみよう。EventBridge+Lambda+AmazonSNSを使ってアプリを作成。

https://bunsugi.com/train-delay-check-app

前提

  • AWSのアカウントがあること。
  • ローカルにNode.jsがインストールされていること。
    • 私は仮想環境(VirtualBox/ubuntu64_18)で実施しています。

全体の流れ

  1. Lambda関数を作成
  2. ローカルで関数を開発
  3. S3にアップロード
  4. S3からLambda関数へアップロード

このような流れで説明していきます。

Lambda関数を作成

Lambda関数の箱を作ります。

マネジメントコンソールから「関数の作成」 マネジメントコンソールから「関数の作成」

「一から作成」を選びます。

関数作成画面

  • ランタイム:Node.js 16.x(2022/5/21現在)
  • アーキテクチャ:x86_64
  • デフォルト実行ロールの変更: 新しいロールを作成

hello-world関数の確認・テスト

既に、簡単なアプリが記載されたindex.jsが用意されています。 index.js

「テスト」タブからテストを実行すると、結果ログが返ってきます。

テスト実行 test実行

ログ ログ

ローカルで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としてエクスポートする
index.js
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に限らず、非公開の情報はここに追加していきます。

.env
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://{バケット名}/

S3のアップロード

ローカルでインストールしたモジュールの反映方法

node_modulesフォルダをzipファイルに一緒に入れる

ことで、インストールが必要なモジュールを利用できます。

S3からLambdaへアップロード

S3のURLをメモしておきます。

S3のURL

マネジメントコンソールのLambda関数管理画面に戻ります。

アップロード元を「Amazon S3の場所」とし、

アップロード

URLを指定します。 URL指定

これでソースコードをLambdaに設定するができました。

環境変数の設定

「設定」タブから環境変数を設定します。

設定タブ

環境変数の設定

Lambda実行テスト

テストタブからテストを実行すると、ログを確認することができます。

実行テスト

欲しい値(関数の返り値)を取得することができました!!

完了です。

つまづいたところ

ここまで来るのにつまづいた3点の注意喚起です。

  • 外部モジュールの入れ方がわからない
  • イベントハンドラが無い
  • 環境変数を間違えた

外部モジュールが無い

解決策:圧縮ファイルにnode_modulesフォルダを入れる

圧縮ファイルにnode_modulesフォルダを入れない場合、外部モジュール(ここではaxiosが存在しないエラーが出ました。

CloudWatchに出力されるログ(外部モジュールが存在しない場合)
{
    "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という名前でエクスポートする。

index.js
module.exports.handler = index

この定義が無いと以下のようなエラーが出ます。

CloudWatchに出力されるログ(ハンドラが無い場合)
{
    "errorType": "Runtime.HandlerNotFound",    "errorMessage": "index.handler is undefined or not exported",
    "stack": [
        "Runtime.HandlerNotFound: index.handler is undefined or not exported",
        // 省略 //
    ]
}

環境変数の大文字小文字まちがい

環境変数の大文字小文字を間違いって入れておりました。

解決策:環境変数を大文字小文字まで正しく入れる。

環境変数がうまく設定できていない場合に出るエラーは様々ですが、このアプリの場合はTwitter APIの認証キーを取得できなかったため、axiosで「認証エラー」が出ました。

CloudWatchに出力されるログ(環境変数がうまく設定できていない)
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による定期実行手順を行ないます。

参考文献

【参考】ソースコード全文

ソースコード

index.js
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
utils/utils.js
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);
    }

};


個別連絡はこちらへ→Twitterお問い合わせ

プロフィール

プロフィールイメージ

はち子

事業会社のシステム部門に異動して4年目の会社員。システム企画/要件定義/システムアーキテクチャ等。

Twitter→@bun_sugi

過去の記事について

はてなブログに掲載の記事(主にプログラミングメモ)についてはこちらに掲載しております。(本ブログに移行中)

タグ一覧

関連記事

Copyright© 2022, エンジニアを目指す日常ブログ

お問い合わせ|プライバシーポリシー