Canvas 1 Layer 1

AWS Lambdaで Ruby 2.6 カスタムランタイムの作り方

はじめに

AWS Lambdaで公式サポートしていない言語やバージョンを使用したい場合、カスタムランタイムCustom Runtimeを使用することができます。カスタムランタイムは、使用したい環境を直接ビルドしてLambda Functionを実行する機能です。

今回のre:Invent2018ではプログラミング言語Rubyの公式サポートを発表しましたが、現在サポートしているバージョンは2.5のみです。2018年のクリスマスにリリースされた2.6はまだ正式にサポートしていません。この記事では、Rubyの2.6.0バージョンのLambda カスタムランタイムを直接作って見ます。

カスタムランタイムの構造

カスタムランタイムは、Lambda Functionが呼び出されると/opt/var/taskパスのいずれかで実行可能なbootstrapファイルを探し実行します。bootstrapはカスタムランタイムがやるべき事を実行して、プログラムを終了します。

カスタムランタイムがやるべき事は、次のとおりです

AWS公式チュートリアルで実装されているbootstrapサンプルは、次のとおりです。このbootstrapはシェルスクリプトを実行するカスタムランタイムであり、Lambdaを実行するためにカスタムターンランタイムがすべきことをやっています。

#!/bin/sh

set -euo pipefail

source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

while true
do
  HEADERS="$(mktemp)"
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done

ラムダ実行中に設定される環境変数の詳細については、以下の資料で確認することができます。

上の例は、単にシェルスクリプトを実行するランタイムなので簡単に実装されています。ただし他のランタイムを実装する場合では各言語ごとにこれらの機能を直接実装する必要があります。言語ごとに直接実装せずに、上のシェルスクリプトの例を用いて自分のランタイムを実行することも不可能ではありません。しかしエラーの追跡のように言語別に特化した機能を使用するには言語ごとにカスタムランタイムを実装することをお勧めします。

Ruby 2.6.0 カスタムランタイムの実装

カスタムランタイムの動作を簡単に説明しました。ここからはランタイムを直接作ってみましょう。Ruby 2.6.0のカスタムランタイムを実装しLamada Functionで実行してみましょう。

公式Rubyランタイムのbootstrapコードを抽出する

bootstrapを直接作ることは面倒な作業です。イベントとコンテキストデータを取得して、関数を実行し結果やエラーを処理するコードを全部作る必要があります。

AWS Re:Invent 2018でカスタムランタイムを発表しながら“Ruby公式サポートはカスタムランタイムを利用している”と、話した事を思い出しました。だとすると、Ruby 2.5.0 ランタイムの実行時にbootstrapのコードにアクセスできるかもしれません。色々調べた結果公式ランタイムのbootstrapコードのパスを知ることができました。そしてそのファイルをダウンロードしました。

Lambdaでルビー2.5ランタイムを選択して、次のコードを実行するとファイルをダウンロードできまるリンクが出力されます。

bootstrapファイルをダウンロードすれば、ファイルと一緒にルビーで書かれたLambda実行コードを取得できます。以下はbootstrapファイルの一部です。

#!/usr/bin/env bash
... 생략
/var/runtime/lib/runtime.rb

環境変数の設定し、Rubyファイルを実行するスクリプトであることがわかります。ただしこのbootstrapファイルをそのまま使用することができず、ファイルのパスなどを状況に合わせて変更する必要があります。変更の詳細については、後から説明します。

AWS Lambda実行環境でRubyの2.6.0ビルドする

LambdaでRuby2.6.0を使用するには、まずRubyインタプリタを準備する必要があります。Lambdaが実行される環境と同じ環境でRuby 2.6.0をビルドしその結果をカスタムランタイムにコピーしなければ、新しいバージョンのRubyが正常に動作しません。Lambdaが実行されている環境はAmazon Linuxで、AMIはamzn-ami-hvm-2017.03.1.20170812-x86_64-gp2です。この情報は今後変更されるかも知れません。公式文書で最新の環境を確認してください。

Amazon LinuxのDockerイメージを使用すると、EC2なしでRuby 2.6.0をビルドすることができます。ここではlambci/lambda-base:buildイメージでコンテナを実行し、Ruby 2.6.0をインストールして見ます。

$ docker run -it --rm -v $(pwd):/var/task lambci/lambda-base:build /bin/bash
bash# yum install -y git bzip2 openssl-devel libyaml-devel libffi-devel \
        readline-devel zlib-devel gdbm-devel ncurses-devel \
        gcc gcc-c++ autoconf automake libtool bison
bash# git clone https://github.com/rbenv/ruby-build.git
bash# PREFIX=/usr/local ./ruby-build/install.sh
bash# ruby-build 2.6.0 /var/task/ruby

Rubyを/var/task/rubyパスにインストールしました。このパスはLambada FunctionでのRubyのパスと同じ位置であるべきです。インストール時にパスと実際のパスが異なる場合、Rubyを実行すると以下のようなエラーが発生します。

Traceback (most recent call last):
    1: from <internal:gem_prelude>:2:in `<internal:gem_prelude>'
<internal:gem_prelude>:2:in `require': cannot load such file -- rubygems.rb (LoadError)

Rubyをインストールした結果は、rubyフォルダに保存されます。

カスタムランタイムの作成

bootstrapファイルと一緒に抽出したlibフォルダとDockerでビルドしたrubyを一つののフォルダに追加します。

ここにカスタムランタイムで実行するLambda関数ファイルをlambda_function.rbに作成ます。Ruby2.6.0であることを確認するために、Rubyの現在の実行バージョンを出力し2.6.0に新たに入ったEndless Rangeを試して見ます。

require 'json'
def lambda_handler(event:, context:)
  r = {
    version: "Current lambda ruby versions is #{RUBY_VERSION}",
    endless: [0, 1, 2][0..]
  }
  { statusCode: 200, body: JSON.generate(r) }
end
.
├─ bootstrap
├─ lib
├─ ruby
└─ lambda_function.rb

bootstrapファイルから/var/runtime/var/task/rubyに変更します。bootstrapファイルは、次のとおりです。

#!/usr/bin/env bash

if [ -z "$GEM_HOME" ]; then
  export GEM_HOME=/var/task/vendor/bundle/ruby/2.6.0
fi

if [ -z "$GEM_PATH" ]; then
  export GEM_PATH=/var/task/vendor/bundle/ruby/2.6.0:/var/task/ruby/gems/2.6.0:/var/task/ruby/lib/ruby/gems/2.6.0
fi

if [ -z "$AWS_EXECUTION_ENV" ]; then
  export AWS_EXECUTION_ENV=AWS_Lambda_custom_ruby2.6
fi

if [ -z "$RUBYLIB" ]; then
  export RUBYLIB=/var/task:/var/task/ruby/lib
else
  export RUBYLIB=/var/task:/var/task/ruby/lib:$RUBYLIB
fi

export PATH=/var/task/ruby/bin:$PATH
/var/task/lib/runtime.rb

フォルダを一つのファイルに圧縮します。

$ zip -r ruby_260.zip bootstrap lib ruby lambda_function.rb

ruby_260.zipで圧縮されたファイルをLambda関数として登録し、実行してみます。roleは、自分のアカウントに合わせて変更します。

$ aws lambda create-function \
    --function-name "aws-lambda-ruby-260" \
    --zip-file "fileb://ruby_260.zip" \
    --handler "lambda_function.lambda_handler" \
    --runtime provided \
    --role arn:aws:iam::xxxxxx:role/lambda_basic_execution

$ aws lambda invoke \
    --function-name "aws-lambda-ruby-260" \
    --payload '{"text":"Hello"}' \
    response.txt

$ cat response.txt
{"statusCode":200,"body":"{\"version\":\"Current lambda ruby versions is 2.6.0\",\"endless\":[0,1,2]}"}

Ruby2.6.0カスタムランタイムでLambda Functionの実行に成功しました。

カスタムランタイムを含むLambda Layer

ここまでRuby2.6.0を実行するカスタムランタイムを作りました。しかし簡単なRubyファイルにもかかわらず圧縮したファイルが43MBになります。Lambda関数には圧縮後の50MBの制限をあります。したがってわずか7MBしか残っていません。またRuby2.6.0を使用するたびに、同じ作業を繰り返す必要があります。この問題を解決するためにRuby2.6.0カスタムランタイムをLambda Layerで作って見ます。

Lambda Layerを使用すると、アップロードした圧縮ファイルが/var/taskではなく、/optに解凍されます。したがって先にビルドしたRubyビルド結果をそのまま使用することは出来ません。/opt/rubyにRubyをインストールし直します。Docker イメージを使用して/opt/rubyにRuby2.6.0をインストールしてその結果をコピーします。同様bootstrapファイルでも/var/task/ruby/opt/rubyに変更します。

以下のようなフォルダ構造を確認してzipファイルで圧縮します。

.
├─ bootstrap
├─ lib
└─ ruby
$ zip -r ruby_260_layer.zip bootstrap lib ruby

AWS CLIを利用してLambda Layerを作ります。

$ aws lambda publish-layer-version \
    --layer-name ruby_260 \
    --description "Ruby 2.6.0" \
    --compatible-runtimes provided \
    --license-info MIT \
    --zip-file fileb://ruby_260_layer.zip

Ruby2.6.0カスタムランタイムを含むLambda Layerが生成されました。今からはこのLayerを使ってRuby 2.6.0を実行することができます。

おわりに

ここまでLambdaに新しく追加されたカスタムランタイムやLayerを使って見ました。Python、Node.js、Java Javaのような公式サポートしている言語でも、最新のバージョンを使用したい場合同じ方法を使うことができます。もちろんサポートしていない言語もカスタムランタイムを使えば実行することができます。

そしてLambda Layerは他のアカウントと共有することができます。Ruby2.6.0カスタムランタイムは次のARNで誰でも利用できるように共有して置きました。<region>を自分のリージョンの書き換えてください。

arn:aws:lambda:<region>:350831304703:layer:ruby-260:1

東京リージョンのARNは次のとおりです。

arn:aws:lambda:ap-northeast-1:350831304703:layer:ruby-260:1

この記事の全てのコードはGitHubの seapy/aws-lambda-custom-runtime-builder-for-rubyに公開しておきました。