Elixir で Microsoft Azure の BLOB Storage を操作する

時は2017年、クラウドストレージやオブジェクトストレージといったものが、当たり前のように使われる時代になりました。特に、Amazon, Microsoft, Google等、パブリッククラウドサービスの事業者が提供するオブジェクトストレージサービスは、可用性・信頼性・スケーラビリティ、そして価格(TCO)の面で、オンプレに比べて大きなアドバンテージがあると言えるでしょう。

今回、私が仕事で使っている組み合わせである、Elixir と Azure BLOB Storage との組み合わせをご紹介します。


# Microsoft Azure BLOB Storage について

スケーラブルな非構造化データ用オブジェクトストレージです。詳しくは[公式](https://azure.microsoft.com/ja-jp/services/storage/blobs/)をご覧ください。

## 比較？

AWSやGCPとの比較にあたっては、一概には甲乙付けがたいのですが、Azureは他と比べてSLAが高めに設定されている点が、エンタープライズ利用を強く意識しているのかな？と思っています。

![object-storage-aws-azure-google-rightscale-100719437-large.jpg](https://qiita-image-store.s3.amazonaws.com/0/17549/44c3766b-f871-1b8b-7085-06e31f911ef8.jpeg)

([Deep dive on AWS vs. Azure vs. Google cloud storage options | Network World](https://www.networkworld.com/article/3191520/cloud-computing/deep-dive-on-aws-vs-azure-vs-google-cloud-storage-options.html) より転載)

SLA 99.95% (AWS, Google) と 99.99% (Azure) の差は、年間のサービス停止時間が最大15,768秒 か 3,154秒 かの違いです。

ま、細かいことは気にせず、先に進みましょう。


# ExAzure

[azukiapp/ex_azure: Azure wrapper for Elixir using :erlazure.](https://github.com/azukiapp/ex_azure)

Erlangで利用可能な Azure Storage Services のAPIライブラリである [erlazure](https://github.com/gullitmiranda/erlazure) の、Elixir用ラッパーです。

## インストール

[公式のREADME.md](https://github.com/azukiapp/ex_azure/blob/master/README.md) に書かれている通り・・・ではなく、そちらは書き方がちょっと古いので注意。以下に、本稿執筆時点で動作する書き方で、インストール手順を示します。

### dependencies として定義

`mix.exs` の `deps/0` 内のリストに以下のエントリを加えます。

```elixir
{:ex_azure, "~> 0.1.0"}
```

Phoenix Framework の場合、以下のようになります。

```elixir
  defp deps do
    [
      {:phoenix, "~> 1.3.0-rc"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_ecto, "~> 3.2"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:ex_azure, "~> 0.1.0"}
    ]
  end
```

### OTPアプリケーションとして登録

`mix.exs` の `application/0` 内のリスト内、`extra_applications` キーに対応する値のリストに、 `:ex_azure` を加えます。（存在しない場合はキーごと作ります。）

Phoenix Framework の場合、以下のようになります。

```elixir
  def application do
    [
      mod: {MyApp, []},
      extra_applications: [:logger, :ex_azure]
    ]
  end
```

### 認証情報を設定

`use Mix.Config` されているファイル (Phoenix Framework の場合は `config/*.exs` ) にて、account と access_key の情報が受け取れるようにします。

以下は公式の例で、それぞれの情報を環境変数から取得します。

```elixir
config :ex_azure,
    account:    System.get_env("AZURE_ACCOUNT"),
    access_key: System.get_env("AZURE_ACCESS_KEY")
```

なお、 `access_key` の取得は、Webブラウザを利用して、 [portal.azure.com](https://portal.azure.com) より、Storage accounts > ${Storage Account名} > Access Keys から行います。

## 使い方

最低限、以下の3つの処理及び実装方法を押さえておけば `:ok` でしょう。

### List

コンテナ `container_name` 内のオブジェクトに関するリストを取得します。

```elixir
{:ok, blobs} = ExAzure.request(:list_blobs, [container_name])
```

### Get

`container_name` コンテナから `object_name` オブジェクトをダウンロードします。

```elixir
{:ok, {:ok, file}} = ExAzure.request(:get_blob, [container_name, object_name])
```

### Create

`local_object_path` からデータを読み取り、 `container_name` コンテナに `object_name` という名前でオブジェクトを保存します。

```elixir
{:ok, _created} = ExAzure.request(:put_block_blob,
  [container_name, object_name, File.read!(local_object_path)])
```

戻り値の `created` は、`created` というStringが入っているだけなので、捨てて良いです。


## 便利な自作ヘルパーモジュール

`list_blobs` の戻り値は、このような構造の `Dict` になっています。

```elixir
%{body:
  [
    {
      :cloud_blob,
      'some_object_name.jpg',
      [],
      [],
      [
        last_modified: 'Sun, 29 Oct 2017 02:44:06 GMT',
        etag: '0x8D51E76ECA421B3',
        content_length: 125182,
        content_type: 'application/octet-stream',
        content_encoding: [],
        content_language: [],
        content_md5: 'PUgbs4kkHJWBpitHhGWsgw==',
        cache_control: [],
        blob_type: :block_blob,
        lease_status: :unlocked,
        lease_state: :available
      ],
      []
    },
    ...
  ]
}
```

このうち欲しい情報は限られているので、それを取得する `get_info/3` や、URL文字列を構築するための `build_url/2`、オブジェクトをダウンロードした上でディスク上への保存まで行う `download/3` などを含む、便利なモジュールを自作しました。よろしければご自由にどうぞ。

```elixir
defmodule MyApp.AzureBlob do
  use MyApp.Web, :model

  def get_info(x, container_name, acc \\ [])

  def get_info([h|t], container_name, acc) do
     acc = if elem(h, 0) == :cloud_blob and elem(h, 1) |> List.to_string() == container_name do
       acc ++ elem(h, 4)
     else
       acc
     end
     get_info(t, container_name, acc)
  end

  def get_info([], _container_name, acc) do
    acc
  end

  def build_url(object_name, container_name) when is_binary(object_name) and is_atom(container_name) do
    Application.get_env(:my_app, :object_storage)[:base_url]
      <> "/"
      <> Application.get_env(:my_app, :object_storage)[:containers][container_name]
      <> "/"
      <> object_name
  end

  def download(object_name, container_name, dirname) when is_binary(object_name) and is_binary(container_name) and is_binary(dirname) do
    {:ok, {:ok, file}} = ExAzure.request(:get_blob, [container_name, object_name])
    :ok = File.write(Path.absname(dirname <> "/" <> object_name), file)
    :ok
  end

end
```


# erlazure

[gullitmiranda/erlazure: Windows Azure Erlang bindings](https://github.com/gullitmiranda/erlazure)

ここで、`ExAzure` がラップする、Erlang の `erlazure` に関しても少し見てみましょう。
Elixir からも利用可能ですが、使い勝手の上では、 `erlazure` はプロセスとして予め明示的に立ち上げておく必要があるという点が大きく異なります。

```elixir
{:ok, pid} = :erlazure.start(account, key)
```

そして例えば `Create` 操作をする場合は、以下の要領となります。

```elixir
{ok, _created} = :erlazure.put_block_blob(pid, container_name, object_name, Binary)
```

`ExAzure` の方が若干シンプルですね。


# まとめ

Elixir で Microsoft Azure の BLOB Storage を利用する手法として、 `ExAzure` をご紹介しました。
`erlazure` のラッパーであるため、今回ご紹介したもの以外にも、様々な関数が実行できるはずです。また、今回
BLOB Storage にフォーカスしましたが、`erlazure` はそれだけでなく、[Queue Storage](https://azure.microsoft.com/ja-jp/services/storage/queues/)、[Table Storage](https://azure.microsoft.com/en-us/services/storage/tables/) にも対応しており、この点は `ExAzure` に関しても同様なのではないでしょうか。
Microsoft Azure の BLOB Storage、あるいは 各種Storage Service を利用される際は、ぜひ使ってみて下さい。