どぶお/Pythonで遊ぼう!

Amazon S3のREST APIを使ってみる  

最近Amazon Web Services(AWS)でちょこちょこ遊んでます。今のところSimple Storage Service(S3)とGlacierぐらいですが。そこでPythonから使ってみようと思い、いろいろ試しているのでその記録。

この記事内のコード片は、パブリックドメインということにしておきますので、使用される方の責任でご自由にお使いください。

準備  

AWSを使うに当たってはアカウント作成およびS3の使用開始登録、access key、secret access keyの取得が必要になります。その作業に関しては解説サイトも多数存在するようですのでそちらを参照して下さい。

概要  

AWSへをPythonから利用するライブラリとしてはboto(http://docs.pythonboto.org/)がありますが、あらゆるサービスに対応している反面、かなり巨大なものになっています。私のようにS3ぐらいしか使わないライトユーザーはそこまでいらないので、基本を学ぶことも含めてアクセス用モジュールを作成してみました。

AWSに対するリクエスト  

AWSに対するリクエストはAWS access key、シグネチャ、タイムスタンプの組み合わせにより認証されます。このHMACのシグネチャベースの認証はTwitterなどで用いられているOAuthに似ていますね。ともかく、HTTPメソッドやヘッダの組み合わせをsecret access keyを使ってシグネチャを作成することで認証を行う仕組みです。つまり、自分とAWSしかsecret access keyを知らないため、シグネチャの検証により認証されるようです。

実装はPHPでAmazon S3のREST APIを使用 #1とAmazon S3 Developer Guideを参考にしました。Amazon S3 Developer GuideとAPI Referenceは例が豊富に載っており非常にわかりやすいです。

Pythonによる実装  

実例  

前出のAmazon S3 Developer GuideのMaking Requests using the REST APIの例をここに示します。

DELETE /puppy.jpg HTTP/1.1
User-Agent: dotnet
Host: mybucket.s3.amazonaws.com
Date: Tue, 15 Jan 2008 21:20:27 +0000
x-amz-date: Tue, 15 Jan 2008 21:20:27 +0000
Authorization: AWS AKIAIOSFODNN7EXAMPLE:k3nL7gH3+PadhTEVn5EXAMPLE

このようなリクエストを発行します。各々はとりあえず無視して認証で使われるAuthorizationヘッダを中心に実装を解説してみます。

実装  

前置きが長くなりましたが、まずはS3に対する簡単なリクエスト(Bucketの一覧取得)を実行してみます。このリクエストは使用しているリージョンに対して"/"をGETすることでXMLで書かれたBucketの一覧が取得するというものです。

シグネチャの基本と実装|  

AuthorizationヘッダはAWS Access key:Signatureという組み合わせになっており、ここのSignatureを求める手順を示します。シグネチャは必要な内容をつなぎ合わせた文字列に対してsecret access keyを使って署名することで作成することができます。署名対象のヘッダは以下です。

名称内容
HTTP-VerbHTTPメソッドGET
Content-MD5送信データのMD5のBase64エンコード文字列
検証が不要なら空文字列でよい
Content-TypeContent-Typeヘッダ。データを送信する際に付ける
GETの場合は空文字列
DateRFC1123形式のタイムスタンプ文字列(UTC)Fri, 16 Nov 2012 21:20:27 +0000
CanonicalizedAmzHeadersx-amz-で始まるヘッダ内容
CanonicalizedResourceリソース文字列/

これらを改行(\n)でつなぎ合わせてsecret access keyを使ってHMAC-SHA1による署名をします。コードは以下のようになります。
なお、ACCESS_KEYおよびSECRET_KEYはDeveloper Guide内の値を使っており、実際の認証には使えません。

#!/usr/bin/env python
from datetime import datetime
import hmac, hashlib, base64
ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
def get_authorization(method, resource):
    http_verb = method
    content_md5 = ""
    content_type = ""
    date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
    amz_headers = ""
    resource = resource
    str_to_sign = "\n".join([http_verb, content_md5, content_type, date, amz_headers, resource])
    signature = base64.b64encode(hmac.new(SECRET_KEY, str_to_sign, hashlib.sha1).digest())
    authorization = "AWS %s:%s" % (ACCESS_KEY, signature)
    return authorization
authorization_str = get_authorization("GET", "/")
print authorization_str

実行するとこんな感じ

% ./step1.py
AWS AKIAIOSFODNN7EXAMPLE:nNUgLAHZKX8fLjg7EiueU0Q71v4=

このAWSから始まる文字列をAuthorizationヘッダに使用します。

リクエストを実行する  

S3に対するリクエストはVirual hosted-styleとpath-styleがあるようですが、ここではpath-styleを使用しています。また、東京リージョン(s3-ap-northeast-1)を接続先にしています。必要であれば変更して下さい。 実際のリクエストを作成してみます。リクエストヘッダなどをいじるのでurllib2モジュールを使います。

#!/usr/bin/env python
from datetime import datetime
import hmac, hashlib, base64, urllib2

ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
BASEURL = "https://s3-ap-northeast-1.amazonaws.com"

def authorize(req, access_key, secret_key):
    http_verb = req.get_method()
    content_md5 = req.get_header("Content-md5", "")
    content_type = req.get_header("Content-type", "")
    date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
    amz_headers = ""
    resource = req.get_selector()
    str_to_sign = "\n".join([http_verb, content_md5, content_type, date, amz_headers + resource])
    signature = base64.b64encode(hmac.new(secret_key, str_to_sign, hashlib.sha1).digest())
    authorization = "AWS %s:%s" % (access_key, signature)
    req.add_header("Date", date)
    req.add_header("Authorization", authorization)

req = urllib2.Request(BASEURL + "/")
authorize(req, ACCESS_KEY, SECRET_KEY)
opener = urllib2.build_opener()
print opener.open(req).read()

ACCESS_KEYとSECRET_KEYは適切なものを指定して実行するとこんな感じです。わかりやすいように結果のXMLには改行を入れてあります。また値は適当にダミーにしてあります。

% ./step2.py
<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Owner>
    <ID>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</ID>
    <DisplayName>YourDisplayName</DisplayName>
  </Owner>
  <Buckets>
    <Bucket><Name>mybucket1</Name><CreationDate>2012-11-12T05:38:29.000Z</CreationDate></Bucket>
    <Bucket><Name>mybucket2</Name><CreationDate>2012-11-14T09:15:36.000Z</CreationDate></Bucket>
  </Buckets>
</ListAllMyBucketsResult>

これ以降  

今回はCanonicalizedAmzHeadersの処理と複雑なCanonicalizedResourceの処理は実装していませんので、詳細はDeveloper Guidなどのドキュメントを参考にして下さい。また、urllib2.RequestではHTTPメソッドにGETかPOSTしか使えない(何という仕様!)のでGET以外のリクエスト(PUT、DELETEなど)を行う場合はRequestのサブクラスを作るなどの対応が必要になります。もうちょっとましなモジュールができたらGitHubあたりで公開したいなーとか思ってます。

Glacierも操作できるようなものができたら自分的には十分満足なんですが・・・案外遠いかな(^^;

参考サイト  

コメント  

コメントなどありましたらお願いします。