エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

RubyInline gem で C 拡張を手軽に作ってみた

f:id:Saiya:20181204042827p:plain

こんにちは、サーバーサイドエンジニアといいつつも最近は React をいじっていることが多い矢崎(id:Saiya)です。

去年あたりに自宅用に作っている Rails のアプリケーションから、libgd *1 の提供する C 言語向けの API を呼び出すコードを導入していたのですが、その際に使った RubyInline gem が便利だったので、(主に自分への備忘録を兼ねて)使う上での留意点や tips などを書いておきます *2。公式のサンプルにあるような簡単な利用例ではあまり迷うことはないのですが、多少凝った用途で使う上でハマった点もありましたため、備忘とご共有までに。

なお、この記事は エムスリーアドベントカレンダー 7 日目の記事です、他の記事もぜひご覧くださいませ。

RubyInline gem とは

C 言語のソースの文字列を inline メソッドに渡すとコンパイルされ、Ruby のメソッドとして呼び出せるという便利な gem です。 以下に引用する 公式のサンプルコード を見れば雰囲気がわかるかと思います:

require "inline"
class MyTest
  inline do |builder|
    builder.c "
      long factorial(int max) {
        int i=max, result=1;
        while (i >= 2) { result *= i--; }
        return result;
      }"
  end
end

factorial_5 = MyTest.new.factorial 5

C 拡張をしたいがために自前の gem を作ったりする必要もなく、手軽に C のコードを Ruby から呼び出すことが可能な点が大変魅力的です。性能のためだけでなく、Ruby から呼び出せないライブラリ等を使う上でも有用な選択肢であると思います。

また、Ruby アプリケーションの起動時に C のコードをコンパイルすると突飛に聞こえるかもしれませんが、Ruby 2.6 から入った JITもまさにそのような仕組みであり、MRI/CRuby においてはそのようなアプローチは自然なものなのではないかと思います。

*1:ImageMagick を使っても良かったのですが、環境構築や保守の手間を考慮し試しに使ってみました

*2:去年にやったことの背景を思い出すのに時間がかかったので、また将来に迷わないよう...

続きを読む

AWS SAM + DynamoDB Local + Go で始めるサーバレスアプリケーション開発

この記事は エムスリー Advent Calendar 2018 6日目の記事です。

こんにちは、エムスリー エンジニアリンググループの大和です。 普段は Spring Boot (Java + Kotlin) を使用したサーバサイド開発をしていますが、この度 AWS Lambda を使用したアプリケーション開発を行うことになったため、開発環境を整えるところまでを記事にしたいと思います。

今回は、例として次のシンプルな構成のアプリケーションを作成していきます。

f:id:daiwa_home:20181206120347p:plain
アプリケーション構成

AWS SAM とは

AWS SAM (Serverless Application Model) とは、AWS 上でサーバーレスアプリケーションを構築するために使用できるフレームワークです*1。 YAML もしくは JSON 形式 (正確には AWS CloudFormation のテンプレート) で構成を記述できます。 また、コマンドラインで扱うためのツールとして AWS SAM CLI が提供され、こちらを使用することでローカルで実行できます*2

似たようなフレームワークに Serverless Framework がありますが、今回は AWS のみを使用することに加え、(Node.js 以外での) ローカルでのテストが行いやすいため AWS SAM を採用しています。

続きを読む

業務時間内にPythonのコントリビューターになった顛末

これは エムスリー Advent Calendar 2018 5日目の記事です。

こんにちは、エムスリー・エンジニアリングG・基盤開発チーム小本です。

年末ということで、個人的に今年のうれしかったことである「Pythonのコントリビューターになったこと」について書きます。

続きを読む

今年修正したAndroidアプリの少し根の深いバグ・風変わりなバグ3選 2018年版

この記事はエムスリー Advent Calendar 2018 4日目の記事です。

こんにちは、Androidエンジニアでバグの原因探しが趣味の星川(id:oboenikui)です。

根の深い問題というのはどうしても発見されにくいものであり、弊社ではアプリ含め全てのサービスでUTやQAにより品質担保に万全を期しておりますが、いくつかバグを発生させてしまいました。
本記事では、今年中に発生させてしまったバグ(開発中に発生したものも含む)の中で根の深かったもの、風変わりなものを3つをピックアップして懺悔していきます。

Androidを知らない方でも読めるように、少し余談多めで紹介しますがお付き合いください。

続きを読む

AWS Lambda 新機能 Custom Runtime を作ってみた

こんにちは、プログラミング言語が大好きなエムスリーエンジニアの園田です。

この記事は エムスリーアドベントカレンダー 3日目の記事となります。

先日、AWS 最大の年間イベントである re:Invent 2018 でラスベガスに渡航していました。

f:id:ryoheisonoda:20181130113248j:plain

基調講演で発表された数々の新サービス、日本時間29日の基調講演で AI・機械学習、内部統制・コンプライアンス、ブロックチェーンと新サービスの発表が相次ぎました。
日本時間30日の基調講演ではサーバレス関連の新サービスや機能追加が続々と発表されました。アプリケーションエンジニアとしてはこちらのほうが胸アツでしたね。

その中でも個人的に特にインパクトが大きかったのが、 AWS Lambda Function の Ruby 対応 でした。
次に気になったのが、 AWS Lambda Custom Runtime です。要はどんな言語ランタイムでも Lambda で実行できるようになったということですね。

ローンチ時ですでに AWS から C++ と Rust の Custom Runtime が提供されており、パートナー企業からも以下のランタイムが提供されているということでした。

  • Elixir
  • Erlang
  • N|Solid
  • COBOL
  • PHP

Elixir は再三このブログでも取り上げていますが、弊社でもプロダクション利用しており、「おっ」と思いました。
ところが、ないんです。あの言語が。私の推しラン(推しLanguage)がないのです。

そう、 Nim 言語 がないのです。

というわけで、 Nim の Lambda Runtime を実装してみました。
本記事は Nim アドベントカレンダー 3日目の記事でもあります。

Lambda Custom Runtime のインタフェースを理解する

公式ドキュメントはこちらです。

Custom Runtime では以下の順序で処理を実行するように実装します。

  1. 初期化処理
  2. イベントループ

シンプルですね。イベントループが従来の Lambda Handler にあたります。

エントリポイント

Custom Runtime では、インスタンス作成時に Lambda の実行ディレクトリ(環境変数 LAMBDA_TASK_ROOT)の直下にある、実行可能な bootstrap というファイル(ファイル名固定、拡張子なし)を実行します。
この bootstrap の中に上記の初期化処理とイベントループが実装されていれば、どんな言語であれ Lambda 化が可能という仕組みです。チュートリアルではシェルでの実装例が示されています。

初期化処理

Lambda インスタンスの実行時に1回だけ実行される処理です。通常は環境変数の読み込みなどを行います。
Nim はコンパイルされてシングルバイナリになるため、初期化処理はシェルで実装し、バイナリを実行するだけとします。

イベントループ(Lambda Handler)

イベントループは Lambda の InvokeFunction を処理するための処理です。
Runtimes API に対して HTTP リクエストを実行することで、Lambda の実行環境からイベントデータの取得や結果のレスポンスを返すことを役割とします。

イベントデータの取得

Runtimes API の next エンドポイントに GET リクエストを実行することで、イベントキューのリクエストを取得することができます。

curl -sSL "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next"

AWS_LAMBDA_RUNTIME_APIbootstrap 実行プロセスの環境変数に設定された Runtime ごと(?)に一意になるエンドポイントだと思われます。

この GET リクエストのレスポンスボディが InvokeFunction に渡されたイベントデータとなっています。

イベントデータの処理とエラー処理

取得したイベントデータを入力値としてビジネスロジックを実行します。正常に終了した場合は success エンドポイントにレスポンスを POST します。

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

REQUEST_ID は、 next エンドポイント実行時のレスポンスヘッダに含まれています。

処理中にエラーが発生した場合は、 invocation error エンドポイントにエラーデータを POST します。

Nim での実装

エントリポイントの作成

エントリポイントとなる bootstrap を実装します。こちらは Nim ではなく sh で実装します。チュートリアルにあるサンプルを参考に実装します。

#!/bin/sh

set -euo pipefail

# $_HANDLER には Lambda のハンドラー設定値が格納されています。
# Node.jsとかでおなじみの `index.handler` とかですね。
# Nim はシングルバイナリなので、ハンドラーにはバイナリファイル名を指定してあるものとします。
EXEC="$LAMBDA_TASK_ROOT/$_HANDLER"

# 実行可能バイナリがなければ初期化エラーのエンドポイントにエラーをPOSTします
# リクエストボディの形式に決まりはありません
if [ ! -x "$EXEC" ]; then
    ERROR="{\"errorMessage\" : \"$_HANDLER is not found.\", \"errorType\" : \"HandlerNotFoundException\"}"
    curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/init/error"  -d "$ERROR"
    exit 1
fi

# イベントループはバイナリの中で実装してあるものとして実行します
$EXEC

忘れずに実行権限を付与しておきます。

chmod +x bootstrap

イベントループ(Lambda Handler)の実装

次に、イベントループを Nim で実装します。今回は単純な echo サーバを実装してみます。

import json, httpclient, os, strutils

let
  # Runtimes API のエンドポイント Prefix
  runtimeApiEndpointPrefix = "http://" & getEnv("AWS_LAMBDA_RUNTIME_API") & "/2018-06-01/runtime/invocation/"
  # next API のエンドポイント
  nextEndpoint = runtimeApiEndpointPrefix & "next"
  # HttpClient インスタンス
  client = newHttpClient()

type
  # Lambda のお作法として、Contextを構造体とする
  InvocationContext* = object of RootObj
    requestId*: string
    deadline*: string
    functionArn*: string
    traceId*: string
    eventData*: JsonNode

# next エンドポイントからイベントの Context を取得する
proc getInvocationContext(client: HttpClient): InvocationContext =
  let nextResponse = client.request(nextEndpoint, httpMethod = HttpGet)
  return InvocationContext(
    requestId: nextResponse.headers["Lambda-Runtime-Aws-Request-Id"],
    deadline: nextResponse.headers["Lambda-Runtime-Deadline-Ms"],
    functionArn: nextResponse.headers["Lambda-Runtime-Invoked-Function-Arn"],
    traceId: nextResponse.headers["Lambda-Runtime-Trace-Id"],
    eventData: parseJson(nextResponse.bodyStream)
  )

# Context を JSON 文字列に無理やり変換
proc toJson(context: InvocationContext): string =
  let jsonNode = %*{
    "requestId": context.requestId,
    "deadline": context.deadline.parseInt(),
    "functionArn": context.functionArn,
    "traceId": context.traceId,
    "event": context.eventData
  }
  return $jsonNode

# echo レスポンスを Lambda に送信する
proc echoResponse(client: HttpClient, context: InvocationContext): void =
  discard client.request(
      runtimeApiEndpointPrefix & context.requestId & "/response",
      httpMethod = HttpPost,
      body = context.toJson())

# エラー処理
proc handleInvocationError(client: HttpClient, context: InvocationContext, e:ref Exception, message: string): void =
  let errorData = %*{ "errorMessage": message, "errorType": repr(e) }
  discard client.request(
    runtimeApiEndpointPrefix & context.requestId & "/error",
    httpMethod = HttpPost,
    body = $errorData)

# イベントループ
while true:
  # GET リクエストでイベントデータを取得
  let context = client.getInvocationContext()
  try:
    # POST リクエストでレスポンスデータを登録
    client.echoResponse(context)
  except:
    let
      e = getCurrentException()
      msg = getCurrentExceptionMsg()
    # POST リクエストでエラーデータを登録
    handleInvocationError(client, context, e, msg)

上記を aws_lambda_nim_example.nim というファイル名で保存します。

ビルド用のシェルを作成

build.sh という名前で作成しました。nim をコンパイルして bootstrap と一緒に zip に固めているだけです。

#!/bin/sh
MAIN=$1
~/.nimble/bin/nim c -d:release $MAIN.nim
zip $MAIN.zip bootstrap $MAIN

ビルド用 Dockerfile

現在のLambda実行環境であるAmazonLinux 2017.03.1.20170812 の Docker イメージでビルド環境を構築します。

FROM amazonlinux:2017.03.1.20170812

RUN yum install -y xz gcc

RUN curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh \
    && chmod +x init.sh \
    && ./init.sh -y \
    && rm -f init.sh

RUN mkdir /work
WORKDIR /work

RUN yum install -y zip
COPY build.sh /build.sh

docker-compose.yml を作成します。

version: "3"
services:
  nim:
    build:
      context: .
    image: aws-lambda-nim-builder
    command: /build.sh aws_lambda_nim_example
    volumes:
      - .:/work

最終的なファイル構成は以下のようになりました。

.
├── Dockerfile
├── aws_lambda_nim_example.nim
├── bootstrap
├── build.sh
└── docker-compose.yml

ビルド実行

docker-compose を利用して nim をコンパイルします。

docker-compose build
docker-compose up

コンパイル実行後のファイルは以下のようになりました。 aws_lambda_nim_example.zip が Lambda にアップロードするファイルとなります。

.
├── Dockerfile
├── aws_lambda_nim_example
├── aws_lambda_nim_example.nim
├── aws_lambda_nim_example.zip
├── bootstrap
├── build.sh
└── docker-compose.yml

Lambda Function の作成

Lambda 実行用のロールはすでにあるものとします。 handler にバイナリファイル名を指定して作成します。

aws lambda create-function \
  --function-name "aws-lambda-nim-example" \
  --zip-file "fileb://aws_lambda_nim_example.zip" \
  --handler "aws_lambda_nim_example" \
  --runtime provided \
  --role arn:aws:iam::999999999999:role/lambda_basic_execution

Lambda 関数が作成されました。

{
    "FunctionName": "aws-lambda-nim-example",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:999999999999:function:aws-lambda-nim-example",
    "Runtime": "provided",
    "Role": "arn:aws:iam::999999999999:role/lambda_basic_execution",
    "Handler": "aws_lambda_nim_example",
    "CodeSize": 102317,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2018-11-30T01:12:38.342+0000",
    "CodeSha256": "oSjewil21oL7C1w+gSJ2NthQv2m6ONETSb5RtVtup4c=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "ddbf1688-345f-460a-b7d0-e0662b9d0f2c"
}

実際に実行してみます。response.txt にレスポンスの中身を出力します。

aws lambda invoke \
  --function-name "aws-lambda-nim-example" \
  --payload '{"text":"Hello"}' \
  response.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

レスポンスの中身を cat response.txt | jq . で見てみます。

{
  "requestId": "3db41196-f442-11e8-a8b4-9d64ba70d9be",
  "deadline": 1543542600555,
  "functionArn": "arn:aws:lambda:ap-northeast-1:999999999999:function:aws-lambda-nim-example",
  "traceId": "Root=1-5c009745-36997a167a83da36a3c93059;Parent=62cea21b272ebe72;Sampled=0",
  "event": {
    "text": "Hello"
  }
}

event にちゃんとリクエスト時のデータが格納されていますね!!

まとめ

AWS Lambda で Nim のようなマイナー 神 言語でも実行できることが確認できました!!

今回はシングルバイナリにコンパイルされる言語だったためシンプルでしたが、Runtime と実行コードが別になる言語(PHPなど)では Runtime だけを含めたレイヤ(新機能のLambda Layer)を作成するのがベストプラクティスとなります。

ただ、こういった足回りのコードを運用する必要があるため、よっぽどのことがない限りはサポートされている言語で実装することをおすすめします。

エンジニア募集!!

 エムスリーでは、共に医療 × テクノロジーの未来を切り拓いてくれる仲間を募集中です! AWS 以外にも GCP や Firebase などのクラウドも活用しています!興味がある方はカジュアル面談やTechtalkにおこしください!!

 jobs.m3.com

GitLab Runner の autoscaling

12月1日は映画の日で1,000円で映画が見れるということだったので話題の「ボヘミアン・ラプソディ」を見てきました。おもしろかったですまる。近頃、映画やドラマを見ては泣いている SRE の寺岡です。

この投稿はエムスリーアドベントカレンダー2日目の記事です。また、 GitLab Advent Calendar 2018 の14日目の記事でもあります。そして14日は私の誕生日ですね、ありがとうございます。

それでは本題へ

現在のエムスリーの GitLab Runner の状況

エムスリーではソースコード管理に GitLab を使用しており GitLab Runner も CI に使っています。この Runner を EC2 の spotfleet で毎朝起動させるという記事を以前 Qiita に投稿しています。平日の朝、固定されて数の Runner が EC2 で起動して、指定した時間だけ稼働するようになっています。(Runner の用途によって状況は異なります)

qiita.com

最近、新たな用途の Runner を追加しようとしたときに register コマンドのオプションに autoscale に関する項目があることに気づきました、そして spotfleet も使えるようなので切り替えることでより効率的な運用ができるのではないかと試してみました。

続きを読む

ニューラル文章要約モデルの紹介

こんにちは!クリスマスに何をしようか悩んでいるエンジニアリンググループの 鹿山です。

この記事はエムスリーアドベントカレンダー1日目の記事となります。今後の記事もお楽しみに!

今回トップバッターとして、 最近のニューラル文章要約モデルについて、その概要と代表的な論文をご紹介させていただきます。

目次

  • 系列変換 (Sequence to Sequence) モデル
  • 文章要約モデル分類
  • 要約モデルの学習に用いるデータセット
  • 文章要約モデル
    • 単一ドキュメントからの抽出要約
    • 単一ドキュメントからの抽象要約
    • 単一ドキュメントからの抽出 x 抽象要約
  • 参考
  • まとめ
  • We are hiring !

系列変換 (Sequence to Sequence) モデル

まず、要約モデルの基礎となる系列変換モデルについて簡単に紹介させてください。 近年研究が盛んになっているニューラル翻訳モデル・要約モデルはほぼ、系列変換(Sequence-to-Sequence) モデルが元になっています。 このモデルは系列を入力として系列を出力する機構になっています。

f:id:kayamin:20181129223134p:plain
Sequence to Sequence モデル概要

続きを読む