Ruby, PHP, Goで書かれたスクリプトをLambda上で実行する方法がAWS Compute Blogで紹介されました。
Scripting Languages for AWS Lambda: Running PHP, Ruby, and Go

記事が古かったり、JSONを返す方法などが難しかったので、改めて手元で試してみました。

目的

AWSのLambda上でRubyを動かす。

手順

大まかな流れは以下の通りです。
1. EC2上でLambdaの実行環境を作る
2. 1で作成したLambdaの実行環境とrubyプログラムをlambda上にデプロイする

詳細な手順を説明します。

EC2インスタンスの作成

Lambdaの実行環境を構築するためのEC2インスタンスを起動します。
作成方法がわからない場合は、こちらを参考にしてください。

EC2で作成したインスタンスにSSHアクセスする

AMIに”Amazon Linux”を選択します。
AMIには"Amazon Linux"を選択します。

インスタンスタイプとして”t2-large”を選択します。t2-largeに意味はありません。
マシンタイプは"t2-large"を選択します。

インスタンスの環境構築

インスタンスにSSH接続します。

    hoge@MyComputer:~$ ssh -i "lambda-ruby.pem" ec2-user@パブリックDNS

           __|  __|_  )
           _|  (     /   Amazon Linux AMI
          ___|\___|___|

    https://aws.amazon.com/amazon-linux-ami/2017.09-release-notes/
    6 package(s) needed for security, out of 9 available
    Run "sudo yum update" to apply all updates.
    [ec2-user@ip-172-31-38-203 ~]$

Rubyの実行環境を構築します。

    $ sudo yum update -y
    $ cd ~
    $ wget http://d6r77u77i8pq3.cloudfront.net/releases/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz .
    $ mkdir LambdaRuby
    $ tar -xvf traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz -C LambdaRuby
    $ cd LambdaRuby

Rubyのテストスクリプトを作成します。
引数をエコーするプログラムです。

    $ vi lambdaRuby.rb
#!./bin/ruby

require 'json'

# You can use this to check your Ruby version from within puts(RUBY_VERSION)

if ARGV.length > 0
    print JSON.parse( ARGV[0] )
else
    print {}
end

実行権限を付与します。

    $ chmod 755 lambdaRuby.rb

動作確認のため実行します。

    $ ./lambdaRuby.rb '{"we": "love", "using": "Lambda"}'
    {"we"=>"love", "using"=>"Lambda"}

次に、Rubyを呼び出すためのNode.jsを作成します。

    $ vi ruby.js
const exec = require('child_process').exec;

exports.handler = (event, context, callback) => {
    const cmd = './lambdaRuby.rb ' + "'" + JSON.stringify(event) + "'";
    const child = exec(cmd, {maxBuffer: 400*1024}, function(err, stdout, stderr){
        console.log("cmd:" + cmd);
        // Resolve with result of process
        var response = {
            statusCode: 200,
            headers: {
                "x-custom-header" : "my custom header value"
           },
           body: stdout
        };
        context.succeed(response);
    });

    // Log process stdout and stderr
    child.stdout.on('data', console.log);
    child.stderr.on('data', console.error);

};

この時点で、以下のフォルダ構成になっていることを確認します。

    ec2-user@ip LambdaRuby]$ ls -rlt
    total 24
    drwxrwxr-x 2 ec2-user ec2-user 4096 Jul 14  2015 info
    drwxr-xr-x 3 ec2-user ec2-user 4096 Jul 14  2015 lib
    drwxr-xr-x 2 ec2-user ec2-user 4096 Jul 14  2015 bin.real
    drwxrwxr-x 2 ec2-user ec2-user 4096 Jul 14  2015 bin
    -rwxr-xr-x 1 ec2-user ec2-user  187 Mar 10 16:17 lambdaRuby.rb
    -rw-rw-r-- 1 ec2-user ec2-user  389 Mar 10 16:34 ruby.js

圧縮します。

    $ zip -r ruby.zip ./

圧縮したファイルをクライアントマシンにコピーします。下記の例は、クライアントマシンからscpでLambdaRubyフォルダをコピーしています。

    $ scp -i ./lambda-ruby.pem ec2-user@パブリックDNS:LambdaRuby/ruby.zip ./

コピーしたruby.zipは、この後のLambda関数作成後の手順で使用します。

Lambdaのデプロイ

事前準備として、以下を参考にLambda関数を作成てください。
AWSでLambdaを使ってみる(2018年3月版)

作成後、下記手順に進みます。

関数を選択してページ下部へスクロールします。
関数を選択して下へスクロールします。

コードエントリタイプのプルダウンから”.ZIPファイルをアップロード”を選択します。
コードエントリタイプで".ZIPファイルをアップロード"を選択します。

“アップロード”ボタンをクリックして作成したruby.zipを選択したら、”ハンドラ”を”ruby.handler”に変更し、「保存」ボタンをクリックします。
作成した"ruby.zip"をアップロードしてhandlerを"ruby.handler"とします。

画面上部に戻り、「テスト」ボタンをクリックします。
画面上部の「テスト」ボタンをクリックしてテスト作成ページへ移動します。

下記のテストを作成して保存します。
テストを作成して保存します。

作成したテストを実行すると、下記のような結果が出力されます。
テストを実行すると成功します。

ログにもconsole.logの内容が出力されています。
console.logの出力結果も表示されています。

下記のように、curlでも取得することができます。

        $ curl -i https://tlxiy9xpmh.execute-api.ap-northeast-1.amazonaws.com/hello/hello-world -d "{'key1': "value1"}"
        HTTP/1.1 200 OK
        Content-Type: application/json
        Content-Length: 1571
        Connection: keep-alive
        Date: Sun, 11 Mar 2018 09:10:01 GMT
        x-amzn-RequestId: fa1973b8-250b-11e8-8872-eb41dcba2866
        x-custom-header: my custom header value
        X-Amzn-Trace-Id: sampled=0;root=1-5aa4f268-e4e2f0cf98e49389f3ee2fe1
        X-Cache: Miss from cloudfront
        Via: 1.1 115f36b4ab4028b3e77515694ec0e35f.cloudfront.net (CloudFront)
        X-Amz-Cf-Id: -gdCH6bEQe5GWLDwUl_Vo3XC3o5z5GPXs4sf_nXouZZtrJCdos7L7g==

        {"resource"=>"/hello-world", "path"=>"/hello-world", "httpMethod"=>"POST", "headers"=>{"Accept"=>"*/*", "CloudFront-Forwarded-Proto"=>"https", "CloudFront-Is-Desktop-Viewer"=>"true", "CloudFront-Is-Mobile-Viewer"=>"false", "CloudFront-Is-SmartTV-Viewer"=>"false", "CloudFront-Is-Tablet-Viewer"=>"false", "CloudFront-Viewer-Country"=>"JP", "Content-Type"=>"application/x-www-form-urlencoded", "Host"=>"tlxiy9xpmh.execute-api.ap-northeast-1.amazonaws.com", "User-Agent"=>"curl/7.47.0", "Via"=>"1.1 115f36b4ab4028b3e77515694ec0e35f.cloudfront.net (CloudFront)", "X-Amz-Cf-Id"=>"otElvmTpwWt6OVNs5a56ucmLvuu9PIAHhLnuhd7EG5IZSMCgPeQ1cw==", "X-Amzn-Trace-Id"=>"Root=1-5aa4f268-aa97bfd47951b89a747398e4", "X-Forwarded-For"=>"113.159.156.242, 54.182.232.71", "X-Forwarded-Port"=>"443", "X-Forwarded-Proto"=>"https"}, "queryStringParameters"=>nil, "pathParameters"=>nil, "stageVariables"=>nil, "requestContext"=>{"requestTime"=>"11/Mar/2018:09:10:00 +0000", "path"=>"/hello/hello-world", "accountId"=>"598885323478", "protocol"=>"HTTP/1.1", "resourceId"=>"xc7khs", "stage"=>"hello", "requestTimeEpoch"=>1520759400564, "requestId"=>"fa1973b8-250b-11e8-8872-eb41dcba2866", "identity"=>{"cognitoIdentityPoolId"=>nil, "accountId"=>nil, "cognitoIdentityId"=>nil, "caller"=>nil, "sourceIp"=>"113.159.156.242", "accessKey"=>nil, "cognitoAuthenticationType"=>nil, "cognitoAuthenticationProvider"=>nil, "userArn"=>nil, "userAgent"=>"curl/7.47.0", "user"=>nil}, "resourcePath"=>"/hello-world", "httpMethod"=>"POST", "apiId"=>"tlxiy9xpmh"}, "body"=>"{key1: value1}", "isBase64Encoded"=>false}

RubyGemsを使う

Rubyでは必須といえるRubyGemsを試してみます

Rubyの実行構築したインスタンス上でgemをインストールします。

    $ pwd
    /home/ec2-user/LambdaRuby
    $ ./bin/gem install faker --no-ri --no-rdoc
    Fetching: concurrent-ruby-1.0.5.gem (100%)
    Successfully installed concurrent-ruby-1.0.5
    Fetching: i18n-1.0.0.gem (100%)
    Successfully installed i18n-1.0.0
    Fetching: faker-1.8.7.gem (100%)
    Successfully installed faker-1.8.7
    3 gems installed

Gemがインストールされています。

    $ ls -lrt lib/ruby/gems/2.2.0/gems/
    total 16
    drwxrwxr-x 4 ec2-user ec2-user 4096 Jul 14  2015 bundler-1.9.9
    drwxrwxr-x 3 ec2-user ec2-user 4096 Mar 11 10:16 concurrent-ruby-1.0.5
    drwxrwxr-x 5 ec2-user ec2-user 4096 Mar 11 10:16 i18n-1.0.0
    drwxrwxr-x 3 ec2-user ec2-user 4096 Mar 11 10:16 faker-1.8.7

rubyスクリプトをGemを使うよう変更します。

    $ vi lambdaRuby.rb
#!./bin/ruby

require 'faker'

puts Faker::Name.name

動作確認

    $ ./lambdaRuby.rb
    Mathias Crist

先ほどと同様、圧縮してクライアントマシンにダウンロードしたら、デプロイしてテストを実行します。
結果は以下の通り、bodyにfakerの結果が出力されています。

RubyGems "fakcer"を使ったテスト結果です。

性能

Lambdaでは、設定したメモリに比例する CPU が割り当てられます。
以下に、fakerを使った場合の実行時間をメモリごとに計測した結果を記載します。

メモリ 実行速度(ms)
128 MB 15421.19 ms
256 MB 7410.71 ms
512 MB 3656.10 ms
1024MB 1811.51 ms
2048MB 982.29 ms
3008MB 906.83 ms

メモリを2倍にすると、実行時間が1/2になっています。
しかし、2048MBと3008MBでは実行時間に大きな差はみられませんでした。

この結果からわかるように、作成する関数ごとに、最適なメモリを設定する必要がありそうです。

まとめ

AWSのLambda上でRubyを動かすための手順を説明しました。
既存のスクリプトがRubyで書かれている場合のLambdaへの移行手段の一つとして検討いただいてもよいかと思います。