Crystal で AWS API を叩く

Posted on
qiita AWS crystal

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

この記事は Crystal Advent Calendar 2015 の16日目の記事です :exclamation:

昨日の記事は @pine613 さんの 『 東京 Crystal 勉強会 #3 in 恵比寿 』 を開催します!!!! でした。1/22 に第三回を開催とのことでぜひ参加したいのですが、3日後の 1/25 が修論提出日だったりします。善処できるよう努力します。

さて、今回は Crystal で AWS の API を叩いてみた、というお話です。

Crystal から AWS を触りたい

普段 AWS なインフラやってる人間なので、Crystal で AWS を操作できないものかと考えました。やっぱネイティブバイナリ吐けるっていうのはツール作る人間にとって魅力的なんですよ…

Ruby, Python, JavaScript, Java, Go, … といったいわゆるメジャーな言語から AWS を操作する場合は、AWS が公式に提供している SDK を使うのが一般的です。提供言語の一覧は以下のページに有ります。

AWS のツール | アマゾン ウェブ サービス(AWS 日本語)

ですが、ご覧のとおり Erlang/Elixir や Crystal といったマイナーな言語についてはまだ SDK は提供されていません。Erlang/Elixir は一部サービスに対応した非公式 SDK gleber/erlcloud があります。が、俺たちの Crystal には非公式 SDK すらありません。

それどころが GitHub で「Crystal の AWS プロダクト aws language:crystal」を検索しても

スクリーンショット 2015-12-16 17.12.56.png

ご覧のとおりまっさらです… Issue では basmoura/c3p0 というリポジトリがヒットしますが (S3 クライアント?)、実装は空っぽも同然です。

というわけで土台を作った

むしろまっさらということはこの領域のパイオニアになれる!ということで自分で AWS API クライアントを作ってみることにしました。まだクライアント「本体」までは作れていないのですが…、クライアントを作るための「土台」をまず作りました。

AWS API を叩くためには、HTTP ヘッダに特別な認証情報 (AWS Signature Version 4) を含める必要があります。この認証情報を生成するライブラリを作りました。

(上の検索結果にも出ていますが) dtan4/aws4_signer.cr です。

使い方

shard.yml の dependencies に以下を追加してインストールします。

dependencies:
  aws4_signer:
    github: dtan4/aws4_signer.cr
    branch: master
$ crystal deps

例えば S3 のバケット情報を取得するようなコードだと、以下のようになります。

require "aws4_signer"
require "http/client"
require "uri"

bucket = "your-bucket"

signer = Aws4Signer.new(ENV["AWS_ACCESS_KEY_ID"], ENV["AWS_SECRET_ACCESS_KEY"], "ap-northeast-1", "s3")
uri = URI.parse("https://s3-ap-northeast-1.amazonaws.com/#{bucket}")

HTTP::Client.new(uri.host.to_s) do |client|
  headers = signer.sign_headers("GET", uri) ## ここでヘッダに認証情報を付与
  response = client.get(uri.path.to_s, headers)
  puts response.body
end

この response.body は XML なので、適当にパースして煮るなり焼くなりします。そこまで面倒を見るツールは今後作っていきたいです。

実装

aws4_signer.cr の実装にあたっては、sora_h さんの sorah/aws4_signer を全面的に参考にしました。これは Ruby で書かれた同様のライブラリなのですが、Crystal は Ruby そっくりのアレということなので移植はしやすかったです。

Ruby と Crystal の細かい違い

  • 文字列 (String) はダブルクォートで囲む、シングルクォートは Char のみ使える
  • 空の Array や Hash を作るときは型宣言が必要
    • [] of String
    • {} of Symbol => String?
  • ちゃんと型考えないと、テスト時 crystal spec にもコンパイルでコケる
    • けっこう助けられた
  • やっぱり Ruby にはあって Crystal にはないモジュールはある
    • でも同等のものはある。Digest に対する OpenSSL::DigestNet::HTTP に対する HTTP とか。

などなど、普通にコピペするだけで動くわけではないです。

テストフレームワーク

最初は標準ライブラリの Spec を使おうとしたのですが、let に値するものが無くて厳しかったので、ysbaddaden/minitest.cr を使うことにしました。Ruby の minitest と同じく RSpec 風の記法 (let も!) で書くことができます。実際 sorah/aws4_signer も minitest を使っていたので、結果的にテストコードの移植が楽になりました。

自動テスト(失敗)

guardguard-shell を使って、ファイルの保存にフックして自動で crystal spec を走らせるようにしました。Crystal で Guard 同等のものがなかったので、仕方なくリポジトリに Gemfile を置くことにしました…

Guardfile はこれです。

guard :shell do
  watch(/(.*).cr/) { |m| `crystal spec` }
end

一見よさそうでしたが、テストがオールグリーンになるとそれ以降自動実行が行われなくなってしまいました。ので、最後のほうは手動で crystal spec していました…

自分の実装が悪いのか言語 / ライブラリのバグなのか…?

ということがたまにありました。上のサンプルコードで #to_s を2箇所で呼んでますが、これを外すとコンパイルエラーになります。Crystal 本体のコードも読んだのですがそこでも型宣言が無くハマりました。

まあ自分が Crystal に慣れてなかったのでもっとよい書き方があるのかもしれません。Crystal も発展途上なので、これを機に自分でより内部を探って見るようにします。

AWS SDK for Crystal?

AWS の API なんて全サービス考えるとそれこそ膨大な数があるので、公式 SDK は API Schema からクライアントのコードを生成するようにしています。例えば AWS SDK for Go は Go のコードを機械的に生成していますし、JavaScript や Ruby なんかだと JSON の API Schema からメソッドを動的定義しています。

Crystal でも macro を使えば後者の方法で実装できるとは思います。だとしたら実装がシンプルになる(はず)なので意外と簡単にできる…?

おわりに

というわけで、AWS API の認証情報ヘッダを生成するためのライブラリ dtan4/aws4_signer.cr を作ったのでした。もし AWS 系のクライアントを作りたい、という方がいたらぜひお使いください。自分でも今回の実装で Crystal 慣れてきたかな?という感じなので、色々作ってみようとは思います。

Crystal は他言語に比べてまだエコシステムが弱いです。しかしそれは、今からでもパイオニアになれる可能性が十分にあるということです。Ruby のライブラリを移植してみるとかでもいけます。よりよいエコシステムを作っていきましょう :exclamation:

Crystal Advent Calendar 2015、明日は @kubo39 さんです。よろしくお願いします!