お金をかけずにサーバーの勉強をしよう

LocalStackで AWSの Lambda

2023年3月4日

メニューへ戻る

LocalStackを使って AWS Lambdaを味わってみます。

サーバーエンジニアとして就職活動を行っておりますと、必須経験に AWSや Azureといったパブリッククラウドの運用が出てくるんですが、求人ニーズに対して「実務」経験としてこれを積んでいる人はそう多くないと思いますがねぇ…。

それと個人でやるにゃ貧乏エンジニアにはパブリッククラウドは高過ぎるのと、ミスると大きく課金されたまま気づかないってのも少々怖い。

という訳で、(金銭的に)安全な AWSのエミュレーターの LocalStack(Community Edition)を使って、せめて「こういうものだよね」くらいの知識は得ておこうというものです。

LocalStackは Dockerで動いていますので、ここでの実験は DockerエンジンとLocalStackがインストールされている前提になります。

Dockerエンジンのインストールについては「Dockerエンジン インストール」に、

LocalStackのインストールについては「LocalStackを使う (AWSエミュレーター)」に書いており、ちょっと前の文書ですが、同じ手順で最新版がインストールできました。

結果として、環境は以下の通りになりました。

以前のバージョンですとエッジルーター(4566/tcp)は 127.0.0.1 にバインドされていたんですが、最近は最初から 0.0.0.0にバインドされているようです。
他のノードからアクセスするのに、一つ手間が減りました。

localstack@UbuntuServer2204-1:~$ localstack start

     __                     _______ __             __
    / /   ____  _________ _/ / ___// /_____ ______/ /__
   / /   / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
  / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
 /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|

 💻 LocalStack CLI 1.4.0

[16:52:36] starting LocalStack in Docker mode 🐳                                                                                                                                   localstack.py:138
────────────────────────────────────────────────────────────────────────── LocalStack Runtime Log (press CTRL-C to quit) ───────────────────────────────────────────────────────────────────────────
Waiting for all LocalStack services to be ready
2023-03-02 07:54:12,392 CRIT Supervisor is running as root.  Privileges were not dropped because no user is specified in the config file.  If you intend to run as root, you can set user=root in the config file to avoid this message.
2023-03-02 07:54:12,394 INFO supervisord started with pid 17
2023-03-02 07:54:13,400 INFO spawned: 'infra' with pid 22
2023-03-02 07:54:14,404 INFO success: infra entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)

LocalStack version: 1.4.1.dev
LocalStack Docker container id: 87f8413759e0
LocalStack build date: 2023-03-01
LocalStack build git hash: 71b5cfdd

2023-03-02T07:54:17.242  WARN --- [-functhread5] hypercorn.error            : ASGI Framework Lifespan error, continuing without Lifespan support
2023-03-02T07:54:17.242  WARN --- [-functhread5] hypercorn.error            : ASGI Framework Lifespan error, continuing without Lifespan support
2023-03-02T07:54:17.244  INFO --- [-functhread5] hypercorn.error            : Running on https://0.0.0.0:4566 (CTRL + C to quit)
2023-03-02T07:54:17.244  INFO --- [-functhread5] hypercorn.error            : Running on https://0.0.0.0:4566 (CTRL + C to quit)
2023-03-02T07:54:17.442  INFO --- [  MainThread] localstack.utils.bootstrap : Execution of "start_runtime_components" took 2439.33ms
Ready.

ターミナルをもう一つ開いて、LocalStackを動かしているサーバー(Ubuntu 22.04.2)に sshでログインして状態を確認しました。

localstack@UbuntuServer2204-1:~$ localstack status services
┏━━━━━━━━━━━━━┳━━━━━━━┓
┃ Service                  ┃ Status       ┃
┡━━━━━━━━━━━━━╇━━━━━━━┩
│ acm                      │ ✔ available  │
│ apigateway               │ ✔ available  │
│ cloudformation           │ ✔ available  │
│ cloudwatch               │ ✔ available  │
│ config                   │ ✔ available  │
│ dynamodb                 │ ✔ available  │
│ dynamodbstreams          │ ✔ available  │
│ ec2                      │ ✔ available  │
│ es                       │ ✔ available  │
│ events                   │ ✔ available  │
│ firehose                 │ ✔ available  │
│ iam                      │ ✔ available  │
│ kinesis                  │ ✔ available  │
│ kms                      │ ✔ available  │
│ lambda                   │ ✔ available  │
│ logs                     │ ✔ available  │
│ opensearch               │ ✔ available  │
│ redshift                 │ ✔ available  │
│ resource-groups          │ ✔ available  │
│ resourcegroupstaggingapi │ ✔ available  │
│ route53                  │ ✔ available  │
│ route53resolver          │ ✔ available  │
│ s3                       │ ✔ available  │
│ s3control                │ ✔ available  │
│ secretsmanager           │ ✔ available  │
│ ses                      │ ✔ available  │
│ sns                      │ ✔ available  │
│ sqs                      │ ✔ available  │
│ ssm                      │ ✔ available  │
│ stepfunctions            │ ✔ available  │
│ sts                      │ ✔ available  │
│ support                  │ ✔ available  │
│ swf                      │ ✔ available  │
│ transcribe               │ ✔ available  │
└─────────────┴───────┘

良さげです。

この後 aws cliと、ラッパーの awslocalコマンドをインストールしました。

これで環境は良いでしょう。


件の Lambdaですが、FaaSと呼ばれる AWSのサービスで、簡単に言えば 特定のイベントをトリガに自作プログラムを実行してくれる環境ですね。
1回実行でいくらという料金体系ですけど、100万回/月までタダではなかったか。

私は当初ランバダかと思っていたんですが、正しくはラムダです。

ランバダはさておき、どこから手を付ければ良いのか分からないので、まずは公式のドキュメントでやってみましょう。
AWS CLI での Lambda の使用

公式のドキュメントでは、JavaScriptのプログラムを例にしていますね。
中の人はコンテナで Node.jsを動かしているってところでしょうか。

2023年3月3日時点で他の開発言語として Python・Ruby・Java・Go・C#・Powershellがラインナップされていました。
マイクロソフト独自の C#と Powershellまで含まれているのは意外でしたね。


では上記のドキュメントに沿って進めていきます。

本物の AWSの場合、まずはセキュリティとして IAMで実行ロールを作らいないとアクセスができませんが、LocalStackの Community Editionではデフォルトでセキュリティが無効になっているようです。(裏は取っていませんが)

ですので、実行ロール自体は Lambdaへのプログラム登録時に必要ながらも、中身は確認されてないようで、適当なものを入れておけば良いようなので、ここは割愛します。

結果、JavaScriptのプログラムを作るところから(上のドキュメントの「関数を作成する」という段落から)スタートですね。

ただ、その前に Lambdaで JavaScriptを実行させるときのお作法を少しおさらいしましょう。
Node.js による Lambda 関数の構築

ここには Lambdaコンソール(AWSの Web画面のことでしょう)での操作方法が書いてあるのですが、推察するにそこで自動的に作られるのが[index.js]または[index.js]という名前のファイルのようです。

そしてこの記述

index.js または index.mjs ファイルは、イベントオブジェクトとコンテキストオブジェクトを取得する handler という名前の関数をエクスポートします。これは、関数が呼び出されるときに Lambda が呼び出すハンドラー関数です。Node.js 関数のランタイムは、Lambda から呼び出しイベントを取得し、ハンドラに渡します。関数設定で、ハンドラ値は index.handler です。

とありますので、上のドキュメントのソース例とも合致します。

後述する aws lambda コマンドのオプションからは、ここで呼び出される関数名は指定できそうな気もしますが、とりあえず[handler]という関数をエクスポートするように書くのが基本と思われます。

ドキュメントオリジナルのままだと著作権に引っかかるかも知れないので、もっとシンプルに Hello Worldにしておきました。

この環境では[localstack]ユーザーにしか[awslocal]コマンドを使えるようにしていないので、LocalStackを実行しているこのユーザーでやりますが、他のユーザーでも構いません。

localstack@UbuntuServer2204-1:~/work/lambda$ ls -l hello.js 
-rw-rw-r-- 1 localstack localstack 117  3月  3 14:41 hello.js

localstack@UbuntuServer2204-1:~/work/lambda$ cat hello.js
exports.handler = async function(event, context) {
  console.log("Hello World!\n")
  return context.logStreamName
}

ピンク色の所が JavaScriptです。

ソースファイルができたので zip圧縮します。
Ubuntu Server にはデフォルト状態では zipコマンドがありませんので、インストールしておいて下さい。

ドキュメントの例では[function.zip]というファイル名にしてありましたが、これも指定できると思うんですよね。
あえて違う名前[hello.zip]にしてみます。

localstack@UbuntuServer2204-1:~/work/lambda$ zip hello.zip hello.js
  adding: hello.js (deflated 10%)

localstack@UbuntuServer2204-1:~/work/lambda$ ls -l hello*
-rw-rw-r-- 1 localstack localstack 117  3月  3 14:41 hello.js
-rw-rw-r-- 1 localstack localstack 271  3月  3 14:45 hello.zip

圧縮ファイルができました。

いよいよ LocalStackの Lambdaに登録をします。
コマンドリファレンスはこちら。
create-function

--function-nameこの関数につける名前
--zip-file圧縮ファイル名の前に[fileb://]と書きましょう。
(blobの意味らしい)
--handlerLambdaが実行するハンドラ名
zip圧縮ファイルでのデプロイに必須とあります
--runtimeプログラムの実行環境
--roleARN(Amazon Resource Name)で指定するロール
この関数を登録するユーザーのイメージでしょうか
ここではドキュメントの例のものをそのまま入れています

オプションが長いので、途中で改行していて、実際に入力しているのが緑色のところ。

localstack@UbuntuServer2204-1:~/work/lambda$ awslocal lambda create-function \
> --function-name my-function \
> --zip-file fileb://hello.zip \
> --handler hello.handler \
> --runtime nodejs18.x \
> --role arn:aws:iam::123456789012:role/lambda-ex

{
    "FunctionName": "my-function",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:my-function",
    "Runtime": "nodejs18.x",
    "Role": "arn:aws:iam::123456789012:role/lambda-ex",
    "Handler": "hello.handler",
    "CodeSize": 271,
    "Description": "",
    "Timeout": 3,
    "LastModified": "2023-03-03T05:55:12.222+0000",
    "CodeSha256": "V8+/+6n7vLsMCfq7yoBji39b/nM+N5evnTDekzCBEAM=",
    "Version": "$LATEST",
    "VpcConfig": {},
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "24febc2c-c73e-4293-aa81-f1684be15235",
    "State": "Active",
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ]
}

登録できました。

コマンド実行の結果、画面には JSON形式の出力がありました。
ピンク色の箇所が、コマンドで入れたものと対応する箇所ですね。


では、手動実行してみましょう。

localstack@UbuntuServer2204-1:~/work/lambda$ awslocal lambda invoke --function-name my-function out --log-type Tail

{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "LogResult": "",
    "ExecutedVersion": "$LATEST"
}

何かエラーになってるように見えます。

LocalStack側に出たログを見てみるとこんなのが。

docker.errors.NotFound: 404 Client Error for http+docker://localhost/v1.42/images/create?tag=nodejs18.x&fromImage=lambci%2Flambda: Not Found ("manifest for lambci/lambda:nodejs18.x not found: manifest unknown: manifest unknown")

Node.js 18にはまだ対応してないのでしょうか。(本物は対応済です)

「Localstack Nodejs18」でググりますと、GitHubにこんなのが。
feature request: Support nodejs18.x Lambda runtime #7209
これが2022年11月20日に上がってます。

ここに書いてあるに、[ PROVIDER_OVERRIDE_LAMBDA=asf ]と環境変数設定をしてやると、Lambdaのプロバイダ(≒プログラムのことかコンテナか)が新しいもので動くようです。

やってみます。
現在動いている LocalStackをCtrl+cで止めて、上の環境変数を有効にしてから再開します。

localstack@UbuntuServer2204-1:~/work/lambda$ PROVIDER_OVERRIDE_LAMBDA=asf localstack start

     __                     _______ __             __
    / /   ____  _________ _/ / ___// /_____ ______/ /__
   / /   / __ \/ ___/ __ `/ /\__ \/ __/ __ `/ ___/ //_/
  / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,<
 /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_|

多分新しいコンテナをダウンロードしてるのだと思いますが、Lambdaファンクションを使えるようになるまでちょっと時間がかかりました。

実行します。

localstack@UbuntuServer2204-1:~/work/lambda$ awslocal lambda invoke --function-name my-function out --log-type Tail
{
    "StatusCode": 200,
    "LogResult": "U1RBUlQgUmVxdWVzdElkOiAyYWU1Y2U0Yy0xNTNlLTQzYTgtYTI3My05YjVhMzYyMmFhNzYgVmVyc2lvbjogJExBVEVTVAoyMDIzLTAzLTA0VDAzOjUxOjQ1LjAzNloJMmFlNWNlNGMtMTUzZS00M2E4LWEyNzMtOWI1YTM2MjJhYTc2CUlORk8JSGVsbG8gV29ybGQhDQpFTkQgUmVxdWVzdElkOiAyYWU1Y2U0Yy0xNTNlLTQzYTgtYTI3My05YjVhMzYyMmFhNzYKUkVQT1JUIFJlcXVlc3RJZDogMmFlNWNlNGMtMTUzZS00M2E4LWEyNzMtOWI1YTM2MjJhYTc2CUR1cmF0aW9uOiA2LjY5IG1zCUJpbGxlZCBEdXJhdGlvbjogNyBtcwlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTI4IE1CCQo=",
    "ExecutedVersion": "$LATEST"
}

今度は[LogResult]に何やら暗号的なものが出てきました。

上の手順に戻ると、base64でエンコードされたログ(最大4k)になってるって書いてあります。

同じ関数をもう一度実行して、[LogResult]の中身をテキストアウトして、 base64コマンドでデコードしています。

localstack@UbuntuServer2204-1:~/work/lambda$ awslocal lambda invoke --function-name my-function out --log-type Tail --query 'LogResult' --output text |  base64 -d
START RequestId: 646df426-6fc2-4a09-bdb1-8846882312fd Version: $LATEST
2023-03-04T03:55:46.500Z        646df426-6fc2-4a09-bdb1-8846882312fd    INFO    Hello World!
END RequestId: 646df426-6fc2-4a09-bdb1-8846882312fd
REPORT RequestId: 646df426-6fc2-4a09-bdb1-8846882312fd  Duration: 3.40 ms       Billed Duration: 4 ms   Memory Size: 128 MB     Max Memory Used: 128 MB

「Hello World」が書いてありました。

これで終わりです。


LocalStackにて Lambdaで JavaScriptのコードを実行してみました。

「簡単〜」と言われていますが、未経験者がやるには前提知識が結構必要ですね。
単に自分の PCで開発したプログラムを実行してるのとは違います。

ここでの実験ではコンソールに出力したものを拾っていますが、実際にはこういうことはしないと思います。
(あれば)入力と出力は S3など AWSの他サービスと連携するのが一般かと。

ただ、連携という話になると別に Lambda上じゃないと連携できないというのではなく、他サービス用の APIを実行するってことになるんでしょう。

そうなると自分の PC上の Node.jsで作ってテストしてから、Lambdaで実行って手順になるんでしょうね。


LocalStack…良いですね。
散々コチラでいじってから AWSでちょっと課金、に出来ると良いな〜と思いました。


LocalStackとこういう本でガッツリ勉強。
でも日本語の新しい Lambda本ってこれだけ?