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

はじめに

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

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

44BITS 소식과 클라우드 뉴스를 전해드립니다. 지금 5,000명 이상의 구독자와 함께 하고 있습니다 📮

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

カスタムランタイムは、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に公開しておきました。

44BITS 로고

아마존 웹 서비스(AWS, Amazon Web Serivce)란?

🏷️ 키워드, 2020-01-20 - 아마존 웹 서비스는 아마존의 자회사로 같은 이름으로 퍼블릭 클라우드 컴퓨팅 서비스를 제공하고 있습니다. 대표적인 서비스로는 컴퓨팅 자원을 제공하는 EC2, 오브젝트 스토리지 S3, 프라이빗 클라우드 VPC, 권한 제어 IAM, 컨테이너 오케스트레이션 ECS, EKS 등이 있습니다.

도커(Docker), 쿠버네티스(Kubernetes) 통합 도커 데스크톱을 스테이블 채널에 릴리즈

🗞 새소식, 2018-08-13 - 2018년 7월 25일 도커(Docker)에서는 쿠버네티스(Kubernetes) 통합 도커 데스크탑을 스테이블 채널로 릴리즈하였습니다.

젯브레인 IDE로 쾌적한 테라폼(Terraform) 코딩 환경 구축

記事, 2019-02-26 - 테라폼(Terraform)은 클라우드 시대에 각광받고 있는 인프라스트럭처 관리 도구입니다. 대다수 에디터들이 코드 하이라이팅이나 자동 완성 등을 지원하지만, 아직까지 인텔리J(IntelliJ) 만큼 강력한 지원 기능을 본 적은 없습니다. 어떤 기능인지 둘러보실까요?

그라파이트(Graphite)와 그라파나(Grafana)로 메트릭스 모니터링 시스템 구축하기

記事, 2014-07-25 - 그라파이트(Graphite)는 파이썬(Python) 기반의 메티릭스 수집 및 모니터링 도구입니다. 그라파이트는 다수의 모듈로 구성되어있어서 처음 접하면 구조를 이해하기가 어려울 수 있습니다. 이 글에서는 그라파이트의 아키텍처를 소개하고 도커를 사용해 각 모듈을 설치하고 연동하는 법을 소개합니다. 또한 그라파나(Grafana)를 사용해 그라파이트의 대시보드를 만들어봅니다.