ex_awsを使ってElixirからAWSのAPIを呼び出す
この記事はElixir Advent Calendar 201613日目の記事です。
RubyやJava、Goなどの言語でAWSを利用する時には公式SDKを使いAWSの各APIを呼び出すことが多いと思います。
Elixirにも公式ではないですがex_awsというライブラリがあり、多くのAWSのAPIをサポートしています。開発も非常に活発です。
ex_awsの公式ページ
ex_awsのGitHubリポジトリ
今回は、ex_awsを使ってAWSの以下APIを呼び出してみます。
- S3
- Dynamodb
- SNS + SQS
- Kinesis Streams
- KMS
依存ライブラリの設定
まずはプロジェクトの設定です。
今回はPhoenixのプロジェクトでex_aws
を使ってみます(Phoenixは今回の内容に全く関係ないですが...)。
mixファイルに以下の定義を追加します。
defp deps do ... {:ex_aws, git: "https://github.com/CargoSense/ex_aws.git", branch: "master" }, {:configparser_ex, "~> 0.2.1", optional: true}, {:poison, "~> 2.0"}, {:secure_random, "~> 0.5"}
ex_aws
は日々機能追加が行われているので、今回は最新のmasterを使います。
configparser_ex
は設定ファイルのparse処理で使用します。
poison
はエンコード、デコード処理をするために使用します。
secure_random
はランダムなハッシュ値を生成するために使用します。
もし自分でライブラリの中をデバッグしたい場合は、ex_awsをローカルにチェックアウトしてから以下のように定義を変更してください。
{:ex_aws, path: "/path/to/ex_aws"} # チェックアウトしたディレクトリを指定します。
Credentialの設定
設定ファイル(configディレクトリ下のファイル)にAWSのアクセスキー情報を設定します。
以下のように設定することで、環境変数の値を読んで実行するようになります。
config :ex_aws, debug_requests: true, access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role], secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role], region: "ap-northeast-1"
awscliのように/$HOME/.aws/credentials
に設定した情報を使いプロファイル名を指定することもできます。
以下のように設定します。
config :ex_aws, debug_requests: true, access_key_id: [{:awscli, "profile-name", 30}, :instance_role], secret_access_key: [{:awscli, "profile-name", 30}, :instance_role], region: "ap-northeast-1"
profile-name
のところに、使用するアカウントのプロファイル名を指定します。
S3
設定は終わりましたのでAWSのAPIを呼び出していきましょう。
最初はS3からです。S3のバケットにあるJSONファイルを取得して呼び出し元に返却します。
JSONの値は何でも良いのですが、ここでは以下のような配列型のデータ構造とします。
{ "contents": [ { "id": 100, "title": "test content 100", "message": "test message 100", "start_at": "20161215", "end_at": "20170101" }, { "id": 101, "title": "test content 101", "message": "test message 101", "start_at": "20161115", "end_at": "20170101" }, ... ] }
バケット名とキー名を指定してget_object
APIを呼び出します。
contents = ExAws.S3.get_object("test-bucket", "contents.json")
戻り値はJSONなのでMap型にデコードしてから、Contentの配列を取得します。
また、配列の中身はただのMap型なので、Elixirの構造体に変換します。
alias ExAws.S3 def index(conn, _params) do body = S3.get_object("test-bucket", "content.json") |> ExAws.request! |> Map.get(:body) |> Poison.decode! contents = body |> Map.get("contents") |> Enum.map(fn(content) -> map_to_struct(content, Content) end) render(conn, "index.json", contents: contents) end # MapからStructに変換するヘルパー関数 defp map_to_struct(map, module) do module.__struct__ |> Map.from_struct |> Enum.reduce(%{}, fn({k, v}, acc) -> Map.put(acc, k, Map.get(map, to_string(k), v)) end) |> Map.put(:__struct__, module) end
Content構造体もJSONに記述した内容に合わせて定義しておきます。
defmodule Content do defstruct [:id, :message, :title, :start_at, :end_at] end
APIを試してみます。S3のJSONの内容を取得します。
$curl -H "Accept: application/json" -H 'Content-type:application/json' -X GET http://localhost:4000/api/contents {"data": [{"title":"test content 100","start_at":"20161215","message":"test message 100","id":100,"end_at":"20170101"}, {"title":"test content 101","start_at":"20161115","message":"test message 101","id":101,"end_at":"20170101"}, {"title":"test content 102","start_at":"20161201","message":"test message 102","id":102,"end_at":"20170115"}, {"title":"test content 103","start_at":"20161230","message":"test message 103","id":103,"end_at":"20170131"}, {"title":"test content 104","start_at":"20161215","message":"test message 104","id":104,"end_at":"20170131"}]}%
Dynamodb
続いてDynamodbのAPIを呼び出してみましょう。先ほどのJSONと同じデータ構造のテーブルを事前に作成してあります。
get_item
APIを呼び出してテーブルのレコードを取得します。
alias ExAws.Dynamo def show(conn, %{"id" => id}) do content = Dynamo.get_item("contents", %{id: String.to_integer(id)}) |> ExAws.request! |> Dynamo.decode_item(as: Content) render(conn, "show.json", content: content) end
APIを試してみます。Dynamodbのレコードを取得します。
$ curl -H "Accept: application/json" -H 'Content-type:application/json' -X GET http://localhost:4000/api/contents/103 {"data":{"title":"test content 103","start_at":"20161230","message":"test message 103","id":103,"end_at":"20170131"}}%
ちなみに更新の場合はこうです
Dynamo.update_item("contents", %{id: id}, expression_attribute_values: %{m: "test updated message"}, update_expression: "set message = :m") |> ExAws.request!
削除の場合はこう
Dynamo.delete_item("contents", %{id: 103}) |> ExAws.request!
SNS + SQS
SNSトピックへのpublishと、トピックをsubscribeしているSQSからのメッセージ取得を試します。
まずはSNSトピックへのpublishです。
publish_opts = %{topic_arn: "arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:test-topic"} message = Poison.encode!(%{message: "ExAws Rocks!"}) SNS.publish(message, publish_opts) |> ExAws.request!
続いてSQSからメッセージを取得します。
response = SQS.receive_message("test-queue") |> ExAws.request!
最後に、取得したメッセージを削除します。
receipt_handle = response[:body][:messages] |> Enum.at(0) |> Map.get(:receipt_handle) SQS.delete_message("test-queue", receipt_handle) |> ExAws.request!
Kinesis Streams
Streamへのレコードの追加と取得を行います。まずは1レコードのみ追加してみます。
レコードはPoison
ライブラリを使用してエンコードしてから追加します。
alias ExAws.Kinesis def put_records(conn, _params) do record = Poison.encode!(%{language: "Elixir", framework: "Phoenix"}) Kinesis.put_record("test-stream", SecureRandom.hex(16), record) |> ExAws.request! send_resp(conn, :ok, "") end
続いて、put-records
APIを呼び出して複数レコードを追加します。
複数シャードにうまく分散されるようにパーティションキーの値をランダムにします。
def put_records(conn, _params) do records = [%{data: "Hello Elixir", partition_key: SecureRandom.hex(16)}, %{data: "Hello Clojure", partition_key: SecureRandom.hex(16)}, %{data: "Hello Rust", partition_key: SecureRandom.hex(16)}, %{data: "Hello Go", partition_key: SecureRandom.hex(16)}] Kinesis.put_records("test-stream", records) |> ExAws.request! send_resp(conn, :ok, "") end
レコードのputはできたので、次はStreamからgetしましょう。
シャードイテレータを取得してから、その値をパラメータにしてレコードを取得します。
def get_records(conn, _params) do shard_iterator = Kinesis.get_shard_iterator("test-stream", "shardId-000000000001", :trim_horizon) |> ExAws.request! |> Map.get("ShardIterator") Kinesis.get_records(shard_iterator) |> ExAws.request send_resp(conn, :ok, "") end
KMS
KMSを使ってデータ暗号化と復号化を試します。事前にカスタマーマスターキーは作成していることとします。
暗号化
まずはデータキーを作成します。
# カスタマーマスターキーを指定する encrypted_response = ExAws.KMS.generate_data_key("xxxx-xxxx-xxxx-xxxx-xxxx") |> ExAws.request!
結果
%{"CiphertextBlob" => "AQEDAHhOO/6KtZ8+9x3gvTYAZV9p1b1x31e/xeaj7M0rwtsTCgAAAH4wfAYJKoZIhvcNAQcGoG8wbQIBADBoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDLu2ma0AN/dEOogKlAIBEIA7rWb8UC218CaEsO1D/Qca3OH7Gbj/A2R2zX4U1++QFiyfeEZQBj9tBEvl+RsqNUG8wLtW4XO0y9yvLEU=", "KeyId" => "arn:aws:kms:ap-northeast-1:xxxxxxxx:key/xxxx-xxxx-xxxx-xxxx-xxxx", "Plaintext" => "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
次に、作成したデータキーを使ってデータを暗号化します。
echo Hello Elixir |openssl enc -e -aes-256-ecb -pass pass:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > encrypted_data.txt
復号化
暗号化できたので、平文のデータキーは削除していることとします。
続いて復号化を行います。
ます、暗号化済みのデータキーを復号化します。
decrypted_response = ExAws.KMS.decrypt(encrypted_response["CiphertextBlob"]) |> ExAws.request!
結果
%{"KeyId" => "arn:aws:kms:ap-northeast-1:xxxxxxxx:key/xxxx-xxxx-xxxx-xxxx-xxxx", "Plaintext" => "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}}
復号化したデータキーでデータを復号化します。
openssl enc -d -aes-256-ecb -pass pass:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -in encrypted_data.txt
結果
Hello Elixir
まとめ
ex_aws
はドキュメント、テストコードもしっかりしていて開発も非常に活発です。
私も数ヶ月前から使い始めましたが、各APIも直感的に使えてわかり易いです。
今回試したのは S3, Dynamodb, SNS, SQS, Kinesis, KMSだけですが、ex_aws
は他にもDynamoStreams, EC2, Lambda, RDSをサポートしています。
また、現在開発中のもの(SES, STS, Route53)もあります、近いうちにmasterにマージされるかもしれません。
今回のre:Inventでは多くの魅力的なサービスが発表されました!これらのAPIも今後ex_awsに追加されて、さらに機能が充実してきそうです。
Elixir Advent Calendar 2016
12日目の記事: Elixir のコードレビューで見たやつ
14日目の記事: edeliverとconsulを使ったphoenix applicationのdeploy