AWS Lambda の Custom Runtimes を利用して Crystal を実行してみる #reinvent

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はい、どーも。モバイルアプリサービス部の吉田です。

この記事はAWS Lambda Custom Runtimes芸人 Advent Calendar 2018の25日目の記事です。

今回はCrystalをAWS Lambdaで動かしてみたいと思います。

Crystalとは?

Crystalという言語、あまり聞いたことがない人向けに軽く説明をしておきますと

  • Ruby風の文法
  • 型推論による、変数・メソッドの静的型付け
  • 高速なネイティブコードを生成

といった特徴があります。型推論もですが、LLVMの高速なネイティブコードを生成することができるので、非常に高速なのが売りです。

CrystalでLambdaの実装をする

今回実装した内容はこちらです。

当初はイベントループをbootstrap内で回す形にしていたのですが、Lambdaを短時間のインターバルで呼び出すと、なぜかエラーになってしまったので、実行ファイル内でイベントループをする形にしています。 また、最小限の実装しかしてないので、あまりエラーハンドリングとかは真面目にやってませんので悪しからず・・・

require "http/client"

def getContext
  endpoint = "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/next"
  res = HTTP::Client.get(endpoint)
  
  if res.status_code != 200 
    raise "Unexpected response when invoking: #{res.status_code}"
  end
    
  request_id = res.headers["Lambda-Runtime-Aws-Request-Id"]  
  context = res.body.lines[0]
  [request_id, context]
end

def sendResponse(request_id, context)
  endpoint = "http://#{ENV["AWS_LAMBDA_RUNTIME_API"]}/2018-06-01/runtime/invocation/#{request_id}/response"
  HTTP::Client.post(endpoint, body: context, headers: nil)
end

while true
  request_id, context = getContext
  sendResponse(request_id, context)
end

getContext メソッドではCrystalの組み込みHTTPクライアントを使って AWS_LAMBDA_RUNTIME_API から request_idcontext を取り出しています。

sendResponse メソッドでは、Lambda終了時のエンドポイントに対して、POSTをおこなっています。

そして最後のループで、これらの処理を回しています。

Lambdaのエントリポイントになるbootstrapは、単純に実行可能ファイルを呼び出すだけです。

#!/bin/sh
 
set -euo pipefail
 
EXEC="$LAMBDA_TASK_ROOT/$_HANDLER"
$EXEC

ビルド環境を作る

次にビルド環境を作ります。

まずビルド用のシェルスクリプト build.sh を用意します。

#!/bin/sh
crystal build main.cr --static --release --no-debug

ここで重要なのが --static オプションです。Crystalには依存関係も含め、すべてをまとめてコンパイルする機能があるので、これを利用してコンパイルします。 あえてstaticオプションを使わず、AWS Lambdaのレイヤー機能を使うこともできそうですね。

次はDockerfileですが、公式のdockerイメージがあるので、これを利用します。

FROM crystallang/crystal:latest

RUN mkdir /lambda
WORKDIR /lambda

COPY main.cr . 
COPY build.sh . 

RUN crystal version

また、利用しやすいようにdocker-compose.ymlも以下のように準備します。

version: "3"
services:
  crystal:
    build:
      context: .
    image: crystal-on-lambda
    command: ./build.sh
    volumes:
      - .:/lambda

ここまでできたらあとは

$ docker-compose up

でビルドします。ローカルにmainという実行可能ファイルができますので、これとbootstrapをzipにまとめます。

$ zip lambda.zip bootstrap main

あとはこれをAWSのコンソールからアップするだけで、Lambdaを実行できます。

実に簡単ですね!