AWS Lambda の PythonでShared Library(*.so)を含む外部ライブラリを扱う方法

仕事でPython / Lambdaを触る機会があり、Shared Library(*.so) を扱うライブラリの取扱でめちゃくちゃはまったので、メモっておく。

あと最近エモい記事ばっかり書いていたので、たまにはエンジニアアピールしておく。

Python初心者なので、それxxxでできるよ的な話があれば教えてください。

TL;DR

  • Lambdaで外部ライブラリを扱うときにはZIPファイルに固めてアップロードする必要があるが、これに .so ファイルも一緒に固めておく
  • LD_LIBRARY_PATH はLambda実行時に指定できないので ctypes.cdll.LoadLibraryPython実行時に指定して読み込む
  • MacでビルドするとLambdaで動かなかったりするので、Dockerを使ってAWSAmazon Linuxなどでpip install など行う

何がやりたかったのか

Lambdaでとある形式のファイルから、画像を抜き出すという処理をPythonでやろうとしていた。Lambdaでは依存する外部ライブラリは実行したいファイルと一緒にZIPで固めてアップロードすればよいらしい(Pythonの場合)。

よく紹介されているのは以下の方法だ。

pip install -r requirements.txt -t .

しかしPythonのライブラリの中には、純粋なPythonだけでなくて、Cなどで作られたライブラリをWrapしてPythonから使えるようにしているものがある。その場合は .py ファイルだけZIPにつめても実行時に以下のようなエラーになってしまう。

ImportError: libgdcmMEXD.so.2.8: cannot open shared object file: No such file or directory

しかし、 *.so を単純にZIPに詰めれば解決するという問題ではない。Lambdaでは *.so の読み込み先を決める LD_LIBRARY_PATH をいじれないからだ。

どうするのか

Lambdaでの実行用にZIPに固める際に、以下のように必要なLibraryをlib配下にまとめてしまう。

# 依存関係を./distにインストール
pip install -r requirements.txt -t ./dist
# gdcm関連パッケージを./distにコピー
cp -r /opt/conda/lib/python3.6/site-packages/*gdcm* ./dist
mkdir -p ./dist/lib
cp -r /opt/conda/lib/libgdcm* ./dist/lib
cp -r /opt/conda/lib/libsocketxx* ./dist/lib

# 容量削減のためtestsファイルを削除
rm -rf ./dist/**/tests

# メインファイルを./distにコピー
cp ./main.py ./dist

その上で、Lambdaで実行するファイルで、明示的に lib 配下のファイルを読み込んでやればよい。

import os
import ctypes

for d, dirs, files in os.walk('lib'):
    for f in files:
        if f.endswith('.a'):
            continue
        ctypes.cdll.LoadLibrary(os.path.join(d, f))

TIPSとして、Mac環境でこれをやってもAWSのLambdaが動くLinuxの環境では動作しなかったりするので、LinuxのDockerイメージを用意してそこでZIPファイル作成や依存関係の解決を行うのがおすすめ。自分は雑に公式のAmazon Linuxは使わずに miniconda3 のイメージを使ってパッケージングしたけど問題なくLambda上でも動いた。

まとめと感想

Lambdaまともに使ったのは実は初めてだけど便利だった。Shared Library依存ののものがあると結構面倒だけど、気合でなんとなかる。 サーバーレスで自分のDockerImageをシュッと動かせるようになるとめっちゃ楽になりそうなのでGCPに期待します

参考

Using Scikit-Learn in AWS Lambda -- Serverless Code

転職の思考法を読んだ

また退職の波動が出てるとか言われそうだけど、一緒に採用に関わっている人が採用戦略を考えるときに読んでいたので買った。

このまま今の会社にいていいのか?と一度でも思ったら読む 転職の思考法

このまま今の会社にいていいのか?と一度でも思ったら読む 転職の思考法

どんな本か

筆者のドヤ顔が浮かぶ偉そうになんか言ってくる系の本かと思ったけど、体裁的にはカイゼンジャーニーに近い。転職するべきか悩んでいる主人公を中心に物語として話が進み、どういう思考で転職を考えるべきかということが説明される。

  • 自分のマーケットバリューをどう捉えるか、どう高めていくか
  • これから伸びるマーケットを見つける方法
  • いいベンチャーを見つけるポイント
  • 仕事における「楽しみ」について

などなど。

以下印象に残ったフレーズ。普通に読み物として面白くて1時間くらいで一気に全部読んでしまった。

「いつでも転職できるような人間がそれでも転職しない会社。それが最強だ。」

「意思決定とは、一番情報を持っていて、一番コミットしている人間がやるべきなんだ。」

なるほどなーと思ったこと

TODO型とBEING型

人間には「何をするか」に重きをおくto do型の人間と、「どんな人でありたいか、どんな状態でありたいか」というbeing 型の人間がいる

99%の人間はbeing型らしい。自分も「本当に自分がやりたいことはなんだろう」みたいなことを考えたりしたことがあるけど、これが見つからなくても悲観する必要はないとのこと。

being型の人間が仕事を楽しむ条件としては以下が上げられていてなるほどーという感じがした。

  • マーケットバリューが高められること
  • その仕事でつく嘘を最小化すること

他のエンジニアと話していても、「人生をかけてやりたい」みたいなことって見つからねーよなーという話をよくするので、参考になった。何をやりたいかも大切だけど、自分もbeing型なのでどうありたいかをこれからは考えてみようと思う。

転職後の給与について

すでに給与が高い成熟企業と、いまの給与は低いけど今後自分のマーケットバリューが高まる企業とで悩んだら、迷わず後者を選べ

これは自分の感覚的にもそうだと思う。

本当に優れた会社には勝手に人が集まってくる

本当に優れた会社には、勝手に人が集まってくるんだ。社員が自然に評判を作り、新しい社員を呼んでくるんだよ。

メル社とかはまさにそうだよなー。社員が自然と「うちの会社すげー」ってTwitterでこんなに言ってる会社見たことない。メル社に限らず採用がうまくいっている知り合いのベンチャーはどこも、「うちの会社すごい!楽しい!」と素でTwitterで言っている。

正直、自分はまだ今の会社にそこまでの気持ちは全然持ててない。前の会社にもそこまでの感情は持てなかった。ただ前職は最近フルリモートフルフレックスが導入された辺りから、社員のポジティブな反応が増えているなとは思う。

まとめ

転職を考えている人はもちろん、採用に対して課題感を持っている採用担当の人にも参考になる本だと思います。

2018年上半期振り返り

今年も半分終わってしまった。

やったこと

レガシーシステムのリニューアル

入社してからずっとやってたシステムリニューアルがやっと(一部)リリースできた。

10年放置は言い過ぎで、このツイートに追加してるけど「10年くらい前に作られて数年放置されてた」システムが正しい。たくさんアクセスがあるタイプのシステムではないが、事業的には結構重要なシステムだったのでここをほぼトラブルなしで移行できたのは結構嬉しかった。まだ古いサーバで動いてるバッチを移行したりとかは残ってるし、明日出社して月初のバッチでおかしなことになってたりしたら辛いが・・・・・・

全体の設計もして、サーバサイドでDDDでAPI作って自分でそれを受けるSPAも書くみたいなことほぼ一人でやってたので、途中死にそうになったがなんとかここまでこれてよかった。QAの人がいる開発組織は初めてなんだけど、リリースの前に細かいところまで見てもらえたので大きな問題を起こさずに済んだと思う。

とはいえまだ全部終わったわけではなく、上記のバッチとかいろいろあと一ヶ月くらいはやることあるので頑張りたい。

Slackいれた

今の会社に入社して、とにかくツールまわりでテンション下がっていたのでいれたやつ。いれたのは正確には昨年末だけど。

suzan2go.hatenablog.com

半年くらい一部エンジニアにだけ展開されていたが、晴れて開発に関わる人ほぼ全員には使ってもらえることになりそう。Slackいれた当初は、元から入ってたツールとの価格差から内部のエンジニアからでさえ「コストメリットあるのか?」みたいなことを言われたりもして死にたくなったが、今では割となくてはならない存在になっている感ある。この辺はやはり外部ツールとのインテグレーションやbotが簡単に作れるとかあるけど、単純にツールとしての手触りが良いというのはあるなとは思う。ずっと当たり前に使ってたので気が付かなかったけど、emojiリアクションは偉大。

導入にあたってはいろいろ社内政治的なところも見えたりしてテンションさがったりしたが、まあここまでこれたので良しとしよう。

他にもドキュメントツールについて、他の人が音頭を取って導入を進めてくれることになって大変ありがたい。自分もいれたいと常々思っていて比較表とか作ったりしていたんだけど、Slack導入前後の色々面倒なアレで踏ん切りがつかなかったので。

採用やった

あんまり書けることがないけど、採用に関わった。放置されてた採用ツールをちゃんと運用したり、スポンサーのとりまとめしたりした。

人を評価するのって難しいが、いろんなエンジニアの人と話ができるのは結構楽しかったりする。

RubyKaigiのスポンサー準備は初めての経験で大変だったが楽しかった(小並感)

学んだこと

静的型付け言語(Kotlin)でDDDでWEB APIを書く

前職ではずっとRubyRailsだったので(ちょっとしたツールにGoを書いたりはしたけど)、Kotlin + Springでサーバーサイド、単純なMVCではなくオニオンアーキテクチャでWEB APIを書くのはとても勉強になった。

エリック・エヴァンスのドメイン駆動設計は読んだことがあったものの、実際のコードを書くところには中々紐付けられなかったので、実践ドメイン駆動設計も買って読んだ。

結果として、自分の設計能力は格段に上がった感じがしている。意識が高まってRailsのサービスクラスについて、勉強会で発表するなどした。

Vue.jsでSPAを作る

Reactは業務で書いたことがあり、SPAも某社のコードテストで書いたことがあったものの、業務でVue.js、SPAを書くのは地味に初めてだった。

SSRは必須ではなかったけど、全体的な設計の拠り所が欲しくてNuxtを選んだ。当時はまだβではあったのでリスクもあったけど、これは割と正しい判断だったんじゃないかという気がする。Vue.jsは前職であまり評判がよくなく、どうかなーという印象だったんだけど、使ってみるとかなり手触りがよかった。

サーバAPIとのつなぎこみ部分で型が欲しくてTypeScriptを選択したが、これも割といい判断だった気がする。Nuxt + TypeScriptを使う開発についても意識が高まって勉強会で発表したりした。

余談だけどこの発表資料により、某ペイ的な会社でNuxt + TypeScriptの採用が進んだという噂を観測していて嬉しい。

gRPC

色々あって結局プロダクションでは使わないことになったんだけど、途中までgRPCを本番導入する予定だったので色々勉強してた。

qiita.com

suzan2go.hatenablog.com

「フロントエンドエンジニアも知っておきたいgRPC」の方は自分史上過去最高のブクマがついて嬉しかった。なんかフロントエンドって入ってると伸びる気がしている。裾野が広いからだろうか(別に狙ってつけたわけではないけど)。

まとめと近況とか

振り返ると、この半期は普通に仕事でやって学んだことを勉強会でシュッと発表できている。今の会社に入って辛い面もあったけど、技術的な幅を広げたいという転職時に考えていた希望は達成されていることに気がついた。

今まで出会ったことがないタイプの優秀な人、特にCSに強い人が多いのでその辺りはかなり刺激になっている。自分はもともと物理系の出身で、プログラミングも研究のなかで必要だったのでやっただけ。基本的な足回りが弱いという自覚はあるのでやっていきたい。

Twitterで文句ばっかり言ってるせいか、退職の波動 がすごく出ているらしい。が、別に転職活動はしていないし、いくつかお話はぶっちゃけあったが、そういう意思決定はしていない。

一方で、直近前職を辞めた人たちが殆ど、数人〜十数人規模のスタートアップに行っていて、それは正直ちょっと羨ましく思っている。

(SO含め)一発当てたいのもあるし、システム・組織が小さいところから事業・会社を大きくしていく経験はどこかでやりたい。

とはいえ、上の子はまだ小さいし、奥さんも子供産んでから体調壊し気味だし、二人目もできたしーという状況で、本当に創業期に入っていくのは結構辛いだろうし、そしてそれは数年は変わらんだろうなーと思う。なので、なんかこうちょうどいいバランスでよいタイミングがあったら、挑戦したいなと漠然と考えたりしている。

SmartHRのエンジニアの入社歓迎会の練習をする会に行ってきた #saizeriya_meetup #smarthr

smarthr.connpass.com

SmartHR社主催の「エンジニアの入社歓迎会の練習をする会」に行ってきました。

会場の様子

感想

歓迎会といってもサイゼリヤで飲み放題、食べ放題みたいな感じだろうなーと思ってたら、席に風船が用意されてたり、名刺が用意されてたり、会場についたときの挨拶も「入社おめでとうございます!」だったり、入社おめでとうのTシャツが配られたり、本当に 歓迎会 でした。

あくまで本当に歓迎会で、決して強引な採用トークもなく、楽しくエンジニアリングやSmartHR社の中身について教えていただけたのが本当に素敵だったなーと思います。こういう企画がエンジニアの定例?的なところから実現されたというのもイイですね。

なんでこんなにいい会社っぽいのに採用に苦労しているかというと

  • そもそもエンジニア採用がだいぶ厳しい状況である(自分も採用に関わっているのでわかる)。
  • 会社の規模的に、ジュニアすぎるエンジニアは採用しても育成する余裕がない。
  • toBのサービスのため候補者にサービスの魅力を伝えるのが難しい。toCの会社と競合すると厳しい。

などがあるそうです。どちらかというとサービス・プロダクト指向なので、とんがった技術を使っていきたいという技術指向の方はあまりマッチしないかもともおっしゃっていました。

サイゼリヤ

仕組みはよくわかりませんが、コース?でいろいろでてきてビビりました。料理も全部美味しく、クソでかいワインボトルとか出てきて、飲みすぎました。また行きたいので、各社サイゼリヤでミートアップしてください。

まとめ

サイゼリヤ最高。SmartHR良い会社っぽい。うちもSmartHRいれてくれ(前職ではry)。

Meguro.rbでRailsのサービスクラスについてLTしてきました

speakerdeck.com

初めていったMeguro.rbでLTしてきました。本当はすぐブログ書きたかったんだけど、当日は睡眠不足でめちゃくちゃ眠く、翌日は飲み会で、土日は子供を寝かしつけると同時に寝落ちしてしまったのでこんなに遅くなってしまった。

SpringでDDD的構成をやって、なんとなくRailsのサービスクラスってこういう感じにするべきじゃね?というのを掴んだつもりだったんだけど、いざ資料にまとめてみると全然まとまらず、何が言いたいのかよくわからなくなり、なんか冗長な感じになってしまった気がする。そして資料を詰め込めすぎて、後半早口になってしまった。

LT後も反応が薄かったので、やべーこと言ってしまったかなと思ったんだけど、インターネッツにあげた資料自体への反応は悪くなくてよかった。

言いたかったこと

Twitterの反応であーそうですこれです結局言いたかったのは…となったのはこういうことでした。

Railsのサービスクラス周りの記事、とにかく手段について着目している記事が多くて、そういうものへのアンチテーゼみたいなのも裏テーマとしてありました。

サービスクラスに限らない話ですが、責務とかモデリングをよく考えた上で導入していかないと、結局コードが散らばって辛いだけになるということです。 trailblazer みたいなGemもありますが、ツールありきで考えてしまうと、結局のところあまり良い結果にはならないんじゃないかなと。

trailblacer github.com

Meguro.rbについて

Twitterでしか知らなかった人と何人か顔を合わせてお話することができてよかったです。 一番の進捗はこれで、割と真面目にちょっとやばい人だと思ってたら、本当にいい人でした。人柄という意味でもエンジニアという意味でも。

目黒は帰り道なので、またネタがあれば参加したいです。よろしくおねがいします。

#roppongijs でgRPC-WebについてLTしてきた

speakerdeck.com

gRPC-WebについてLTしてきた。 仕事では結局使わなかったんだけどgRPCをブラウザで使うにはということを色々と調べていたので、それについて話した。

suzan2go.hatenablog.com suzan2go.hatenablog.com

スライドでも触れたけど、公式?のgrpc/grpc-webはまだまだ色々と辛い感じなので、今後の発展に期待という感じである。 本当はTODOくらいVueかReactで作っておこうと思ったんだけど、公式のdockerイメージが全然ビルドできなくて断念。

github.com

gRPC-Webがまだ早いと思った人もいたみたいだけど、こっちのgRPC-Webはツールもこなれてるし全然問題ないんじゃなかろうか。

github.com

今回はフォントの話とか、Nodeの話とか、幅広く色んな話が聞けてよかった。相変わらずメルカリさんの会場、軽食ともすごくよかったー。次回も予定があえば参加したい。

Railsのコントローラでの不可解(に見える)なエラーハンドリングについて

Twitterでこのようなやりとりを見かけて、気になって眠れなかったので調べてみた。 長ったらしく説明しているので結論だけ知りたい方は下までスクロールしてください。

問題

手元ですぐ動かせる Rails5.1.1 / Ruby 2.5.0で試した。

# controller
class WelcomeController < ApplicationController
  def index
    raise ActiveRecord::RecordNotFound
  rescue => e
    raise 're raise'
  end
end

# config/route.rp
Rails.application.routes.draw do
  root to: 'welcome#index'
end

この状態でWEBにアクセスするとこうなる!!

f:id:suzan2go:20180420013817p:plain

!? raise re raise にならん・・・まじだ rescueされてないようにみえる。

ちなみにこれをコンソールでやるとこうなる

f:id:suzan2go:20180420013930p:plain

コンソールだとちゃんと “re raise” が例外になる?!

次にコントローラの例外をRecordNotFoundでなくしてみる

# controller
class WelcomeController < ApplicationController
  def index
    raise ActiveRecordError
  rescue => e
    raise 're raise'
  end
end

f:id:suzan2go:20180420014159p:plain

するとちゃんと re raise の方が例外として上がってくる…不可解

種明かし

rescueされてないよう見えるが、本当にされてないのだろうか。 binding.pry を仕掛けてみよう

class WelcomeController < ApplicationController
  def index
    raise ActiveRecord::RecordNotFound
  rescue => e
    binding.pry
    raise 're raise'
  end
end

f:id:suzan2go:20180420014538p:plain

あれ・・・ちゃんとrescueされてますね・・・ ということは、画面上の表示がおかしいのだろうか

lib/action_dispatch/middleware/debug_exceptions.rb @ line 68 ActionDispatch::DebugExceptions#call:

    57: def call(env)
    58:   request = ActionDispatch::Request.new env
    59:   _, headers, body = response = @app.call(env)
    60:
    61:   if headers["X-Cascade"] == "pass"
    62:     body.close if body.respond_to?(:close)
    63:     raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
    64:   end
    65:
    66:   response
    67: rescue Exception => exception
 => 68:   raise exception unless request.show_exceptions?
    69:   render_exception(request, exception)
    70: end

[1] pry(#<ActionDispatch::DebugExceptions>)> exception
=> #<RuntimeError: re raise>

render_exception というエラーページを生成しているところまでちゃんと re raise のエラーが渡ってきていますね。

では render_exception の中身を見ていきましょう

# actionpack-5.1.1/lib/action_dispatch/middleware/debug_exceptions.rb

      def render_exception(request, exception)
        backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
        wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
        log_error(request, wrapper)

        if request.get_header("action_dispatch.show_detailed_exceptions")
          content_type = request.formats.first

          if api_request?(content_type)
            render_for_api_request(content_type, wrapper)
          else
            render_for_browser_request(request, wrapper)
          end
        else
          raise exception
        end
      end

ExceptionWrapper.new が怪しそう。さらにこのコードを追ってみます。

# actionpack-5.1.1/lib/action_dispatch/middleware/exception_wrapper.rb

module ActionDispatch
  class ExceptionWrapper
  # 〜〜〜省略〜〜〜〜
    @@rescue_responses.merge!(
      "ActionController::RoutingError"               => :not_found,
      "AbstractController::ActionNotFound"           => :not_found,
      "ActionController::MethodNotAllowed"           => :method_not_allowed,
      "ActionController::UnknownHttpMethod"          => :method_not_allowed,
      "ActionController::NotImplemented"             => :not_implemented,
      "ActionController::UnknownFormat"              => :not_acceptable,
      "ActionController::InvalidAuthenticityToken"   => :unprocessable_entity,
      "ActionController::InvalidCrossOriginRequest"  => :unprocessable_entity,
      "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
      "ActionController::BadRequest"                 => :bad_request,
      "ActionController::ParameterMissing"           => :bad_request,
      "Rack::QueryParser::ParameterTypeError"        => :bad_request,
      "Rack::QueryParser::InvalidParameterError"     => :bad_request
    )
  # 〜〜〜省略〜〜〜〜

  def initialize(backtrace_cleaner, exception)
    @backtrace_cleaner = backtrace_cleaner
    @exception = original_exception(exception)

    expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
  end

  # 〜〜〜省略〜〜〜〜

  private

      def original_exception(exception)
        if @@rescue_responses.has_key?(exception.cause.class.name)
          exception.cause
        else
          exception
        end
      end

  # 〜〜〜省略〜〜〜〜

end

読んでいくと以下のような動きになっていることがわかります。

  1. initializeoriginal_exception というのを呼ぶ、
  2. original_exception ではexception.cause で元々発生していた例外を見に行く
  3. それが rescue_responses に一致する場合には、それをExceptionWrapperexception にセットする

docs.ruby-lang.org

でも、ここには ActiveRecord::NotFound に対応するものはありません。 実はここではなくて、ActiveRecodの方で拡張が行われているようです。(コードちゃんとおったわけではないけど多分これ)

# activerecord-5.1.1/lib/active_record/railtie.rb

module ActiveRecord
  # = Active Record Railtie
  class Railtie < Rails::Railtie # :nodoc:

  # 〜〜〜省略〜〜〜〜

    config.action_dispatch.rescue_responses.merge!(
      "ActiveRecord::RecordNotFound"   => :not_found,
      "ActiveRecord::StaleObjectError" => :conflict,
      "ActiveRecord::RecordInvalid"    => :unprocessable_entity,
      "ActiveRecord::RecordNotSaved"   => :unprocessable_entity
    )

  # 〜〜〜省略〜〜〜〜
end

ここまで来てやっとわかりました。例外のその前に発生していた例外が、ActiveRecord::RecordNotFound の場合には、画面上に発生した例外そのもの(今回でいうと re raise)ではなく、元々の例外であるActiveRecord::RecordNotFound が表示される。という挙動になっているようです。

 結論

実際にTweetされてる方の環境を確認したわけではないので完全なる推測ですが、以下のような状況なのではないでしょうか

  • エラーハンドリングはきちんと行われている
  • しかし特定の例外の場合には、rescue節の例外ではなく、その前に発生していた元々の例外の情報が画面に表示される
  • 上記の挙動によりActiveRecord::RecordNotFoundrescue されていないような不可解な挙動に見えた

ちなみにこの挙動はRails3.2.0RCからあるもののようです。

Add an ExceptionWrapper that wraps an exception and provide convenien… · rails/rails@0b677b1 · GitHub

追記