ブログ

ryuzeeによるブログ記事。不定期更新

AzureのBlobサービスにブラウザから直接ファイルをアップロードする

こんにちは。@ryuzeeです。

ずっと趣味で作っているスライド共有アプリケーションはAWS専用なのですが、Azureにも対応させようとして色々Azureを触っています。

そこで今回は、AzureのBlobサービス(AWSのS3相当)にブラウザから直接ファイルをアップロードする方法について調査したので共有します。

下準備

Azure上のBlobの画面などで、適当なコンテナを作成してください(S3のバケット相当のものです)。アクセスポリシーはプライベートに設定します(でないとアップロードしたファイルが外界から認証なしでアクセスされてしまいます)。

必要なgemのインストール

AzureのAPIを叩くために必要なgemをインストールします。適当なディレクトリで以下のようにコマンドを実行してください。

bundle init

Gemfileが作成されるので以下の内容にします。

source "https://rubygems.org"

gem 'azure', '~> 0.7.1'
gem 'azure-contrib', git: 'https://github.com/dmichael/azure-contrib.git'
gem 'sinatra'
gem 'sinatra-contrib'

gemのうち上2つが、直接ファイルをアップロードする上で必要なgemです。azureは公式のSDKで、azure-contribは第三者が作っている拡張用のgemになります。バージョン指定しておかないと色々面倒なことになるので明示的にバージョンを指定しています。

下2つは、このあと作成するWebアプリに使うsinatraをインストールするものです。

環境変数の設定

AzureのBlobストレージをいじくる上で必要な環境変数を設定しましょう。 設定が必要なのは、Azureのストレージにアクセスするためのアクセスキー、ストレージアカウントの名前、そして実際にファイルを保存するコンテナ名です。自分の環境にあわせて変更して設定します。

export AZURE_STORAGE_ACCESS_KEY=xxxxxxxxxxxxx
export AZURE_STORAGE_ACCOUNT_NAME=openslideshare
export CONTAINER_NAME=files

CORS(Cross-Origin Resource Sharing)を設定する

Ajax経由でファイルをアップロードするので、CORSを設定します。 Azureの場合はGUIでは設定できないので、SDKを使ってコードで実施します。ファイル名は適当なもので構いませんが、今回は、set_cors.rbとしておきましょう。

require 'azure'

Azure.config.storage_account_name = ENV['AZURE_STORAGE_ACCOUNT_NAME']
Azure.config.storage_access_key = ENV['AZURE_STORAGE_ACCESS_KEY']

blob_service = Azure::Blob::BlobService.new
props = Azure::Service::StorageServiceProperties.new

props.logging = nil
props.hour_metrics = nil
props.minute_metrics = nil

rule = Azure::Service::CorsRule.new
rule.allowed_headers = ["*"]
rule.allowed_methods = ["PUT", "GET", "HEAD", "POST"]
rule.allowed_origins = ["*"]
rule.exposed_headers = ["*"]
rule.max_age_in_seconds = 1800

props.cors.cors_rules = [rule]
blob_service.set_service_properties(props)

puts blob_service.get_service_properties.inspect

ここまでできたら

bundle exec ruby set_cors.rb

としてください。

なお、CORSはストレージアカウントに対しての設定になっているようです(Container単位にできないのかは要調査)

SAS(Shared Access Signature)を取得する

SASは有効期限付きのワンタイムURLみたいなものです。発行するには以下の内容で適当なrubyスクリプトを用意して実行します。ここでは、アップロードファイル名は、hogehogeに固定されています。

require 'azure'
require 'azure-contrib'

Azure.config.storage_account_name = ENV['AZURE_STORAGE_ACCOUNT_NAME']
Azure.config.storage_access_key = ENV['AZURE_ACCESS_KEY']

def generate_url(container_name, blob_name, permissions)
  start_time = Time.now - 10
  expiration_time = Time.now + 1800
  bs = Azure::Blob::BlobService.new
  uri = bs.generate_uri Addressable::URI.escape("#{container_name}/#{blob_name}")

  signer = Azure::Contrib::Auth::SharedAccessSignature.new(uri, {
    resource:    "b",
    permissions: permissions,
    start:       start_time.utc.iso8601,
    expiry:      expiration_time.utc.iso8601
  }, Azure.config.storage_account_name)

  url = signer.sign
  url
end

puts generate_url('files', 'hogehoge', 'w')

これを実行すると以下のようなURLができ上がります。

http://openslideshare.blob.core.windows.net/files/hogegoge?se=2016-02-06T03%3A28%3A42Z&sig=%2FFr80o0c7W1%2BITXqg7DCcfnx2sL4zhq1op6nJOY%2Flsc%3D&sp=rw&sr=b&st=2016-02-06T02%3A58%3A32Z

このURLに対してファイルをPUTすればOKです。以下のようにして試してみましょう。

curl -X PUT "http://openslideshare.blob.core.windows.net/files/hogehoge?se=2016-02-06T04%3A43%3A09Z&sig=HewO2DhvwKASDg6LCF0GNhvATQA0Lfl3J0nPsKNCJ9k%3D&sp=w&sr=b&st=2016-02-06T04%3A12%3A59Z" -F "file=~/Desktop/hogehoge;type=application-octetstream"

これでAzureのBlobの指定したコンテナ内にファイルができていればOKです!

注意点

注意点としてはSASのURLを取得する時点でBlobに保存する際のファイル名が必要になる点で、ユーザーがアップロードしたファイルの名前を元にしたい場合は、JavaScriptなどとの組み合わせで先にファイル名取得→SASのURLを発行という流れを経てからファイルのアップロードに進まないといけないことになります。

アップロードフォームを作ってみる

ここまで来たら後はWebアプリでアップロードできるようにしましょう。前述の通り、Sinatraを使ってWebアプリを作ってみます。これからカレントディレクトリにファイルを作っていきましょう。

app.rb (メイン部分)

# -*- coding: utf-8 -*-

require 'sinatra'
require 'sinatra/reloader'
require 'azure'
require 'azure-contrib'
require 'uri'
require 'json'
Azure.config.storage_account_name = ENV['AZURE_STORAGE_ACCOUNT_NAME']
Azure.config.storage_access_key = ENV['AZURE_STORAGE_ACCESS_KEY']

get '/' do
  erb :form
end

post '/sas' do
  filename = params[:filename]
  url = generate_url(ENV['CONTAINER_NAME'], filename, 'w')
  puts url
  content_type :json
  data = { url: url }
  JSON.dump(data)
end

def generate_url(container_name, blob_name, permissions)
  start_time = Time.now - 10
  expiration_time = Time.now + 1800
  bs = Azure::Blob::BlobService.new
  uri = bs.generate_uri Addressable::URI.escape("#{container_name}/#{blob_name}")

  signer = Azure::Contrib::Auth::SharedAccessSignature.new(uri, {
    resource:    "b",
    permissions: permissions,
    start:       start_time.utc.iso8601,
    expiry:      expiration_time.utc.iso8601
  }, Azure.config.storage_account_name)

  url = signer.sign
  url
end

views/form.erb (View部分)

<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
</head>
<body>

<div>
  <form id="upload-form" method="post" enctype="multipart/form-data">
    <div>
      <label class="control-label col-sm-2"><span>File</span></label>
      <div>
        <input type="file" name="file" id="file" />
    </div>
  </div>
</div>

<span id="progress">0%</span>

<script type="text/javascript">
$(document).ready(function() {
  $('#file').on("change", function(event) {
    var file = this.files[0];
    if(file != null) {
      console.log(file.name);
    } else {
      return;
    }
    event.preventDefault();

    url = '';
    var formDataSAS = new FormData();
    formDataSAS.append('filename', file.name);

    $.ajax({
      type: 'POST',
      url: '/sas',
      dataType: 'json',
      data: formDataSAS,
      async: false,
      cache: false,
      contentType: false,
      processData: false
    }).done(function( data, textStatus, jqXHR ) {
      console.debug(data.url);
      url = data.url;
    }).fail(function( jqXHR, textStatus, errorThrown ) {
      console.log('Fail...');
    });

    if (url == '') {
      alert('Could not get SAS url...')
      return false;
    }

    var formData = new FormData();
    var form = $('#upload-form');
    $(form.serializeArray()).each(function(i, v) {
      if(v.name != "file") {
        formData.append(v.name, v.value);
      }
    });
    formData.append("file", $("#file").prop("files")[0]);

    $.ajax({
      url: url,
      type: 'PUT',
      contentType: 'application/octet-stream',
      data: formData,
      async: true,
      crossDomain: true,
      xhr: function() {
        xhr = $.ajaxSettings.xhr();
        xhr.upload.addEventListener("progress", function(evt) {
          if (evt.lengthComputable) {
            var percentComplete = evt.loaded / evt.total;
            var p = Math.round(percentComplete * 100);
            $("#progress").html(p + "%");
          }
        }, false);
        return xhr;
      },
      statusCode: {
        201: function(){
          console.log("201:OK");
        },
        403: function(){
          console.log("403:Forbidden");
        },
        404: function(){
          console.log("404:NOT Found");
        },
        405: function(){
          console.log("405:Authentication Error");
        }
      },
      cache: false,
      contentType: false,
      processData: false
    }).done(function( data, textStatus, jqXHR ) {
      alert('Success');
    }) .fail(function( jqXHR, textStatus, errorThrown ) {
      alert('Fail...');
    });
    return false;
  });
});
</script>
</body>
</html>

アプリケーションの起動

ここまでできたらSinatraのアプリケーションを起動しましょう。以下のようにします。

bundle exec ruby app.rb

以下のような感じでログが出力されます。4567番ポートでListenしているのが分かります。

I, [2016-02-06T20:13:12.143616 #19922]  INFO -- : Celluloid 0.17.3 is running in BACKPORTED mode. [ http://git.io/vJf3J ]
[2016-02-06 20:13:12] INFO  WEBrick 1.3.1
[2016-02-06 20:13:12] INFO  ruby 2.2.3 (2015-08-18) [x86_64-darwin14]
== Sinatra (v1.4.7) has taken the stage on 4567 for development with backup from WEBrick
[2016-02-06 20:13:12] INFO  WEBrick::HTTPServer#start: pid=19922 port=4567

ブラウザでのアクセス

http://localhost:4567 にアクセスすると以下のような(無味乾燥な)画面が表示されますので適当なファイルを指定してみてください。

アップロードが終わったらAzure側の画面を確認して、BLOBが生成されているかを確認しましょう。

まとめ

AWSでもAzureでも、大きなファイルをごにょごにょする際に、一端仮想マシン側でPOSTで受け付けるようなことをしてしまうと、仮想マシンが止まった場合に問題が起こりやすく、仮想マシンの負荷が増えたりまた転送料金が高くなったりします。したがってこのような仕掛けを使ってファイルを直接ストレージに配置することはベスト・プラクティスの1つになります。 ぜひ試してみてください。

課題

ファイルサイズが64MBを超えると、以下のエラーが出力されます。

413 (The request body is too large and exceeds the maximum permissible limit.)

これについては、こちらのドキュメントに記載があります。

The maximum size for a block blob created via Put Blob is 64 MB. If your blob is larger than 64 MB, you must upload it as a set of blocks. For more information, see the Put Block and Put Block List operations. It's not necessary to also call Put Blob if you upload the blob as a set of blocks.

ということで、単一ブロックではなく、複数ブロックに分割してアップロードが必要なようです。

アジャイルコーチングやトレーニングを提供しています

株式会社アトラクタでは、アジャイル開発に取り組むチーム向けのコーチングや、認定スクラムマスター研修などの各種トレーニングを提供しています。ぜひお気軽にご相談ください。

詳細はこちら
  • スクラム実践者が知るべき97のこと
  • 著者/訳者:Gunther Verheyen / 吉羽龍太郎 原田騎郎 永瀬美穂
  • 出版社:オライリージャパン(2021-03-23)
  • 定価:¥ 2,640
  • スクラムはアジャイル開発のフレームワークですが、その実装は組織やチームのレベルに応じてさまざまです。本書はスクラムの実践において、さまざまな課題に対処してきた実践者が自らの経験や考え方を語るエッセイ集です。日本語書き下ろしコラムを追加で10本収録
  • プロダクトマネジメント ―ビルドトラップを避け顧客に価値を届ける
  • 著者/訳者:Melissa Perri / 吉羽龍太郎
  • 出版社:オライリージャパン(2020-10-26)
  • 定価:¥ 2,640
  • プロダクト開発を作った機能の数やベロシティなどのアウトプットで計測すると、ビルドトラップと呼ばれる失敗に繋がります。本書ではいかにしてビルドトラップを避けて顧客に価値を届けるかを解説しています。
  • SCRUM BOOT CAMP THE BOOK 【増補改訂版】
  • 著者/訳者:西村直人 永瀬美穂 吉羽龍太郎
  • 出版社:翔泳社(2020-05-20)
  • 定価:¥ 2,640
  • スクラム初心者に向けて基本的な考え方の解説から始まり、プロジェクトでの実際の進め方やよく起こる問題への対応法まで幅広く解説。マンガと文章のセットでスクラムを短期間で理解できます。スクラムの概要を正しく理解したい人、もう一度おさらいしたい人にオススメ。
  • みんなでアジャイル ―変化に対応できる顧客中心組織のつくりかた
  • 著者/訳者:Matt LeMay / 吉羽龍太郎、永瀬美穂、原田騎郎、有野雅士
  • 出版社:オライリージャパン(2020-3-19)
  • 定価:¥ 2,640
  • アジャイルで本当の意味での成果を出すには、開発チームだけでアジャイルに取り組むのではなく、組織全体がアジャイルになる必要があります。本書にはどうやってそれを実現するかのヒントが満載です
  • レガシーコードからの脱却 ―ソフトウェアの寿命を延ばし価値を高める9つのプラクティス
  • 著者/訳者:David Scott Bernstein / 吉羽龍太郎、永瀬美穂、原田騎郎、有野雅士
  • 出版社:オライリージャパン( 2019-9-18 )
  • 定価:¥ 3,132
  • レガシーコードになってから慌てるのではなく、日々レガシーコードを作らないようにするにはどうするか。その観点で、主にエクストリームプログラミングに由来する9つのプラクティスとその背後にある原則をわかりやすく説明しています。
  • Effective DevOps ―4本柱による持続可能な組織文化の育て方
  • 著者/訳者:Jennifer Davis、Ryn Daniels / 吉羽 龍太郎、長尾高弘
  • 出版社:オライリージャパン( 2018-3-24 )
  • 定価:¥ 3,888
  • 主にDevOpsの文化的な事柄に着目し、異なるゴールを持つチームが親和性を高め、矛盾する目標のバランスを取りながら最大限の力を発揮する方法を解説します
  • ジョイ・インク 役職も部署もない全員主役のマネジメント
  • 著者/訳者:リチャード・シェリダン / 原田騎郎, 安井力, 吉羽龍太郎, 永瀬美穂, 川口恭伸
  • 出版社:翔泳社( 2016-12-20 )
  • 定価:¥ 1,944
  • 米国で何度も働きやすい職場として表彰を受けているメンローの創業者かつCEOであるリチャード・シェリダン氏が、職場に喜びをもたらす知恵や経営手法、より良い製品の作り方などを惜しみなく紹介しています
  • アジャイルコーチの道具箱 – 見える化の実例集
  • 著者/訳者:Jimmy Janlén / 原田騎郎, 吉羽龍太郎, 川口恭伸, 高江洲睦, 佐藤竜也
  • 出版社:Leanpub( 2016-04-12 )
  • 定価:$14.99
  • この本は、チームの協調とコミュニケーションを改善したり、行動を変えるための見える化の実例を集めたものです。96個(+2)の見える化の方法をそれぞれ1ページでイラストとともに解説しています。アジャイル開発かどうかに関係なくすぐに使えるカタログ集です
  • カンバン仕事術 ―チームではじめる見える化と改善
  • 著者/訳者:原田騎郎 安井力 吉羽龍太郎 角征典 高木正弘
  • 出版社:オライリージャパン( 2016-03-25 )
  • 定価:¥ 2,138
  • チームの仕事や課題を見える化する手法「カンバン」について、その導入から実践までを図とともにわかりやすく解説した書籍。カンバンの原則などの入門的な事柄から、サービスクラス、プロセスの改善など、一歩進んだ応用的な話題までを網羅的に解説します。
  • Software in 30 Days スクラムによるアジャイルな組織変革“成功"ガイド
  • 著者/訳者:Ken Schwaber、Jeff Sutherland著、角征典、吉羽龍太郎、原田騎郎、川口恭伸訳
  • 出版社:アスキー・メディアワークス( 2013-03-08 )
  • 定価:¥ 1,680
  • スクラムの父であるジェフ・サザーランドとケン・シュエイバーによる著者の日本語版。ビジネス層、マネジメント層向けにソフトウェア開発プロセス変革の必要性やアジャイル型開発プロセスの優位性について説明
  • How to Change the World 〜チェンジ・マネジメント3.0〜
  • 著者/訳者:Jurgen Appelo, 前川哲次(翻訳), 川口恭伸(翻訳), 吉羽龍太郎(翻訳)
  • 出版社:達人出版会
  • 定価:500円
  • どうすれば自分たちの組織を変えられるだろう?それには、組織に変革を起こすチェンジ・マネジメントを学習することだ。アジャイルな組織でのマネージャーの役割を説いた『Management 3.0』の著者がコンパクトにまとめた変化のためのガイドブック