AWS Lambda で任意の言語を実行する

Lambda は標準で Go や Python などが使えますが、これらの言語以外でも自分で runtime を作りさえすればあらゆる言語で使えるようになります。
Custom AWS Lambda runtimes

今まで runtime を自作するのは難しそうだなぁと思っていましたが、昨年12月のアップデートでコンテナイメージが使えるようになり、また AWS Lambda Runtime Interface Emulator もできたことで Lambda の挙動をローカルで実験するのが手軽になったので、試しに bash runtime を作ってみました。

Tutorial – Publishing a custom runtime - AWS Lambda

bash runtime

以下が Lambda で bash スクリプトを実行するための例です。簡単のためエラーハンドリングはしていません。

  • Dockerfile
  • main.sh
    • Lambda で実行するスクリプト(handler)
    • LAMBDA_TASK_ROOT に配置する
  • bootstrap
    • entrypoint から呼ばれるプログラム
    • LAMBDA_RUNTIME_DIR に配置する
    • request_id, event を GET して、handler にそれらを渡して、handler の返り値を POST するということを永遠繰り返す

Dockerfile

FROM public.ecr.aws/lambda/provided:al2

COPY bootstrap $LAMBDA_RUNTIME_DIR/bootstrap
COPY main.sh $LAMBDA_TASK_ROOT/

CMD ["main.sh"]

bootstrap

#!/bin/bash

RUNTIME_URL="http://$AWS_LAMBDA_RUNTIME_API/2018-06-01"

function post() {
    request_id=$1
    response=$2
    url="$RUNTIME_URL/runtime/invocation/$request_id/response"
    curl -X POST $url -d "$response"
}

while true
do
    HEADERS="$(mktemp)"
    EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "$RUNTIME_URL/runtime/invocation/next")
    RESPONSE=$(bash ${LAMBDA_TASK_ROOT}/${_HANDLER} $EVENT_DATA)
    REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)
    post $REQUEST_ID $RESPONSE
done

main.sh

#!/bin/bash

EVENT=$1
echo "Hello:$EVENT"

AWS Lambda execution environment - AWS Lambda

実装したのはこの図の Runtime + Function のところです。(extensions のところはまたいつか深ぼるかも)

bootstrap からわかるようにやっていることは極めて単純で curl -X GET http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/next (next invocation と呼ばれる)で必要な情報とってきて、handler でコネコネして、curl -X POST "http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$request_id/response" -d $response (Invocation response と呼ばれる)で所定の場所に結果を渡しているだけです。

この GET/POST さえしておけば Lambda としてとりあえず動きます。なので sokcet が使える普通の言語であればシュッとその言語用の runtime を作ることができます。

※ このスクリプトでは handler に event (body)しか渡していませんが、通常の Lambda では context として header も渡します。

$ aws lambda invoke --cli-binary-format raw-in-base64-out \
  --function-name lambda_container \
  --payload '"John"' /dev/stderr > /dev/null
Hello:"John"

この bash runtime では next invocation, invocation response の2つの runtime API しか使っていませんが、他にエラー時に使用する Invocation error, Initialization error があります。 エラーハンドリングもちゃんとする場合はこれらの API も使います。 AWS Lambda runtime API

entrypoint からの動きを追ってみる

AWS の公式イメージを使って、所定のプログラムを所定の場所に置くと動くことがわかりましたが、ブラックボックス感が否めないので entrypoint から動きを追ってみます。

$ docker inspect public.ecr.aws/lambda/provided:al2 | jq .[].Config.Entrypoint[]
"/lambda-entrypoint.sh"

デフォルトの entrypoint が /lambda-entrypoint.sh とわかったので中を覗いていきます。

#!/bin/sh
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.

if [ $# -ne 1 ]; then
  echo "entrypoint requires the handler name to be the first argument" 1>&2
  exit 142
fi
export _HANDLER="$1"

RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
else
  exec $RUNTIME_ENTRYPOINT
fi

第1引数に handler 名をとり、/var/runtime/bootstrap を実行しています。 本物の Lambda ではAWS_LAMBDA_RUNTIME_API に値が入っているので /var/runtime/bootstrap を実行し、値が入っていなかった場合(ローカルで実行した場合)は aws-lambda-rie (runtime interface emulator) を使ってエミュレーションしています。

/var/runtime/bootstrap 以降は書いたプログラムのままなので、どうやら内部的に複雑なことはしていないようです。

alpine base でイメージを作る

bootstrap が動けばいいだけなら alpine linux をもとにしたイメージでも動くのでは?と思ったので試しに作ってみました。

Dockerfile

FROM alpine:3.13.0

RUN apk add curl

ENV LAMBDA_RUNTIME_DIR=/var/runtime \
    LAMBDA_TASK_ROOT=/var/task

COPY bootstrap /$LAMBDA_RUNTIME_DIR/bootstrap
COPY main.sh $LAMBDA_TASK_ROOT/

WORKDIR $LAMBDA_TASK_ROOT

ENTRYPOINT ["/var/runtime/bootstrap"]
CMD ["main.sh"]

main.sh

#!/bin/sh

EVENT=$1
echo "HelloWorld:$EVENT"

bootstrap

#!/bin/sh

_HANDLER=$1

RUNTIME_URL="http://$AWS_LAMBDA_RUNTIME_API/2018-06-01"

function post() {
    request_id=$1
    response=$2
    url="$RUNTIME_URL/runtime/invocation/$request_id/response"
    echo post url: $url
    curl -X POST $url -d "$response"
}

while true
do
    HEADERS="$(mktemp)"
    EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "$RUNTIME_URL/runtime/invocation/next")
    RESPONSE=$(sh ${LAMBDA_TASK_ROOT}/${_HANDLER} $EVENT_DATA)
    REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)
    post $REQUEST_ID $RESPONSE
done

image サイズは公式のイメージをもとにした場合は 303 MB でしたが、alpine を使った場合は 9.69 MB まで減りました。

rie 使ってまずはローカルで動くか試してみます。

$ curl -Lo aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie
$ chmod +x aws-lambda-rie
$ docker build -t lambda_alpine .
$ docker run --rm -p 9000:8080 \
    -v $(pwd)/aws-lambda-rie:/aws-lambda-rie \
    --entrypoint="/aws-lambda-rie" \
    lambda_alpine /var/runtime/bootstrap main.sh
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '"John"'
HelloWorld:John

ひとまず、ローカルで実験する限りは Lambda っぽく動いてくれました。

続いて、イメージを ECR にあげて、本物の Lambda で動かしてみます。

$ docker push ...
$ aws lambda update-function-code ...
$ aws lambda invoke --cli-binary-format raw-in-base64-out --function-name lambda_container --payload '"John"' /dev/stderr > /dev/null
HelloWorld:"John"

無事にほしい結果が返ってきました。runtime API の扱いさえちゃんとしていれば Lambda 用にソフトウェア追加する必要はどうやらないようです。 これなら bootstrap を一つ書けば既存の docker image を Lambda 用に編集するというのも容易にできそうですね。

Lambda の副作用問題

副作用のあるプログラムをLambda で動かすともろにその影響を受けるというのは AWS Lambda を使っている人には有名ですが、bootstrap の中で同じ関数を繰り返し実行していることを考えれば至極当然だとわかりますね。

# main.py
x = []
n = 0

def handler(event, context):
    global n
    x.append(n)
    n += 1
    return str(x)
$ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null
"[0]"
$ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null
"[0, 1]"
$ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null
"[0, 1, 2]"
$ aws lambda invoke --function-name lambda_container /dev/stderr > /dev/null
"[0, 1, 2, 3]"

sops で localstack の KMS key を使う

sops を使っていて、これ localstack の KMS key を使うことできないかなぁとふと思ってみたので試してみました。

endpont-url を受け取れるように修正すれば一応動かせました。

github.com

特に実用性はないのですけど、備忘録的に残しておきます。 localstack を使ったローカルテストをするときには役に立つかも?

動作確認環境

  • ArchLinux
  • awscli v2.1.13
  • localstack v0.12.4

修正点

対した変更はしていませんが、endpoint-url を環境変数から吸えるようにしました。

   config := aws.Config{
        Region:   aws.String(matches[1]),
        Endpoint: aws.String(os.Getenv("AWS_ENDPOINT_URL")),
    }

sops/keysource.go at localstack · goropikari/sops · GitHub

$ git clone -b localstack --depth 1 https://github.com/goropikari/sops.git
$ docker build -t sops -f Dockerfile-localstack .
$ id=$(docker create sops)
$ docker cp $id:/go/bin/sops sops-localstack
$ docker rm -v $id

$ docker run --rm -p 4566:4566 localstack/localstack:0.12.4
$ export AWS_ENDPOINT_URL=http://localhost:4566
$ alias awsl="aws --endpoint-url $AWS_ENDPOINT_URL"
$ keyarn=$(awsl kms create-key --query KeyMetadata.Arn --output text)
$ export SOPS_KMS_ARN=$keyarn
$ ./sops-localstack foo.json
$ ./sops-localstack -d foo.json
{
        "hello": "localstack"
}

参考

boot2docker - Docker - how can I copy a file from an image to a host? - Stack Overflow

2020年 振り返り

勉強記録を見直すと2020年は低レイヤの勉強と雰囲気で使っていたものをちゃんと学び直した1年だったようだ。

読んだ・受講した

途中まで読んだ

現在取り組んでいる

AWS Lambda をコンテナイメージで動かすときの Terraform の書き方

AWS Lambda がコンテナイメージをサポートするようになりましたが、Terraform で Lambda のリソースを定義するのにてこづったので書き方を残しておきます。

aws.amazon.com

動作確認時のソフトウェア

  • awscli 2.1.11
  • terraform 0.14.2
  • terraform-provider-aws 3.21.0

Terraform の書き方

Lambda のリソースを作るときに ECR にイメージがすでにある必要があるので、Lambda を作る前に ECR のリポジトリと Lambda で使う IAM を作っておきます。 ECR リポジトリができたら Docker image を push しておきます。

resource "aws_lambda_function" "lambda_container" {
  function_name = "lambda_container"
  role          = aws_iam_role.lambda_container.arn
  package_type  = "Image"
  image_uri     = "${aws_ecr_repository.lambda_containter.repository_url}:latest"
  timeout       = 60

  lifecycle {
    ignore_changes = [image_uri]
  }
}

resource "aws_iam_role" "lambda_container" {
...
}

resource "aws_ecr_repository" "lambda_containter" {
...
}

Terraform で Lambda を作るときは package_type = "Image" にし、image_uri に ECR にイメージ URI をタグを含めて指定します。 Lambda 関数を update するとそのたびに Terraform で差分が出てしまうので ignore_changesimage_uri を入れておくと良いです。

Terraform の公式ドキュメントを見ると handler が Required になっていますが、package_typeImage にした場合は不要です。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function

実際に使用したコード github.com

RubyでつくるRuby を読んだ

RubyでつくるRuby ゼロから学びなおすプログラミング言語入門

RubyでつくるRuby ゼロから学びなおすプログラミング言語入門

  • 作者:遠藤 侑介
  • 発売日: 2017/03/31
  • メディア: 単行本(ソフトカバー)

nand2tetris が終わってヘビーな本を読むのはちょっと時間をおきたかったので、電子積ん読していた「RubyでつくるRuby」を読みました。 内容的にも難しいことはなく、実装時間含めても5時間ほどで読み終わりました。

この本では字句解析・構文解析は著者が用意したライブラリを使用するのでメインは Parse Tree をどう評価するかという内容となっていました。 大元の Ruby の機能を結構使うので想像以上にさっくり作ることができます。

初めて言語処理系を書くという方でも読みやすいとは思いますが、1冊目としては「Rubyで作る奇妙なプログラミング言語」のほうが個人的にはおすすめです。作るのは brainf*ck や whitespace などの実用性皆無な言語ばかりですが、字句解析や構文解析もちゃんとやるのでブラックボックス感が少ないです。 「RubyでつくるRuby」は字句解析とかの知識をつけてから読むとより楽しめる内容だと思いました。

Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~

Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~

  • 作者:原 悠
  • 発売日: 2008/12/20
  • メディア: 単行本(ソフトカバー)