🐏

LambdaでもCLIでも動くコマンドを作るlamblocal - fujiwara-ware 2024 day 13

2024/12/13に公開

この記事は fujiwara-ware advent calendar 2024 の13日目です。

lamblocal とは

https://github.com/fujiwara/lamblocal

lamblocal は、AWS Lambda の関数としても実行できるし、CLI としても実行できるコマンドを作るためのライブラリです。なにを言ってるんだ? と思われるかも知れませんが、それができると嬉しいことがあるのです。

Lambda の関数は、JSON のペイロード(入力)を受け取り、JSONのレスポンス(出力)を返します。実行は基本的に、Lambda のランタイム上で行われます。しかし例えば開発中に、この関数をローカルで動かしたい場合はどうすればいいでしょうか?

公式には AWS SAM CLI というものが提供されており、これを使うとローカルで Lambda 関数を実行できます。しかしDocker を使ったりしていて、ちょっと仕組みが大袈裟なのですよね…

https://github.com/aws/aws-sam-cli

Lambda関数とは要するに「JSONの入力を受け取って、JSONの出力を返すコマンド」です。これをCLIで実行する場合、「標準入力からJSONを受け取り、標準出力にJSONを出力するコマンド」として実装すればいいだけなのです。Docker を持ち出す必要はありません。

lamblocal は、このような Lambda 関数を CLI としても実行できるようにするためのライブラリです。Go 言語用です。

使い方

Go で Lambda 関数を実装する場合、以下のようなコードを書きます。実行したい関数(今回は hello)を lambda.Start に渡します。

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
)

func hello(_ context.Context, name string) (string, error) {
    return fmt.Sprintf("Hello %s", name), nil
}

func main() {
    lambda.StartWithContext(context.TODO(), hello)
}

この Lambda 関数に入力 "world" (JSON文字列) を与えると、出力 "Hello world" が返ってきます。これを lamblocal を使って、CLI としても動くようにするためには、以下のように書き換えます。

package main

import (
    "github.com/fujiwara/lamblocal"
)

func hello(_ context.Context, name string) (string, error) {
    return fmt.Sprintf("Hello %s", name), nil
}

func main() {
    lamblocal.Run(context.TODO(), hello)
}

hello 関数の実装は完全に同一です。lambda.StartWithContextlamblocal.Run に変更するだけです。

これで、このコードは Lambda 上で実行された場合には Lambda 関数として、それ以外の環境で実行された場合には CLI として動作します。

$ echo '"world"' | go run main.go 
"Hello world"

Lambda における設定値の扱い

ところで、Lambda に与える設定値は環境変数で渡します。CLIと異なりコマンドライン引数は使えません。

アプリケーションでその設定値を読む場合、例えば Go なら os.Getenv で環墓変数を読むことになります。しかしこれを素朴に書いてしまうと、ちょっとコードが大きくなったときに途端に運用がしづらいコードになります。

値が必要になった箇所で Getenv していると、設定値が空であったり意図しない値であったりした場合、実行時にその箇所で初めてエラーが発生するわけです。普段はほとんと通らないような処理にそれが埋め込まれていると、デプロイしてしばらく立ってからエラーとして発覚します。これは大変運用がしづらいため、設定の不備は初期化時に発見したいものです。

環境変数を扱えるCLIフラグパーサーによる解決

ここでは応用例として、alecthomas/kong というコマンドラインパーサーと組み合わせる例を紹介します。

https://github.com/alecthomas/kong

kongでは、コマンドライン引数をstructとして定義し、structのタグでデフォルト値やどの環境変数から値を読み取るかを定義できます。

type CLI struct {
    Foo string `help:"This is Foo." default:"foo" env:"FOO"`
}

このように定義すると、CLI.Foo の値は デフォルト値が foo、環境変数 FOO=bar が設定されていれば bar、コマンドライン引数 --foo=baz が与えられた場合には baz、となります。つまり適当なデフォルト値を設定した上で、Lambda 上で実行される場合は環境変数から、CLI として実行する場合は引数から上書きできますし、その値が何かという説明も明示的に help として記述できます。

要素の型に変換できない文字列が渡された場合はエラーになりますし、必須の値(required)、ある特定の値のみを受け付ける(enum)、複数要素を区切り文字で区切って受け付けるなどの機能もあり、設定値のバリデーションも簡単に行えます。

つまり以下のように、Lambda のハンドラー内で好き勝手に環境変数を読むコードを書かず、安全な設定値の取り扱いができるようになります。

type CLI struct {
    Foo string `help:"Foo." default:"foo" env:"FOO"`
}

func (c *CLI) Handler(ctx context.Context, _ interface{}) (string, error) {
    // c.Foo はデフォルト値、環境変数、コマンドライン引数から設定された状態になっている
    return c.Foo, nil
}

func main() {
    var c CLI
    kong.Parse(&c)
    lamblocal.Run(context.TODO(), c.Handler)
}

まとめ

lamblocal は、Lambda 関数を CLI としても実行できるようにするためのライブラリです。Lambda 関数として実装したコードを CLI としても動かせるようにすることで、開発時の効率を向上させることができます。

また応用例として、環境変数を直接扱わずに CLI フラグパーサー経由で取り扱うことで、設定値のバリデーションや初期化時のエラー検知を容易にすることができます。

Go で Lambda 関数を書く際には、ぜひ lamblocal を使ってみてください。

それでは、明日もお楽しみに!

参考資料

https://sfujiwara.hatenablog.com/entry/lamblocal

Discussion