Amazon S3をprivateなモバイルアプリケーション・バックエンドストレージとして利用する方法
よく訓練されたアップル信者、都元です。mBaaS、流行ってますね! AWSもmBaaSへの取り組みを強めています。
AWSでmBaaS
さて mobile backend as a Service、略してmBaaS(えむばーす)ですが、要するにモバイルアプリケーションに対するバックエンドをサーバに独自実装するのではなく、サービスとして提供されているもののことを言います。
複数のモバイルデバイス間で共通のデータ領域を持ちたい場合や、モバイルデバイス内で処理させるには適切で無い *1処理を行いたい場合、旧来は、そのアプリケーション用のバックエンドサーバを用意し、サーバとモバイルアプリが通信を行って問題解決をしていました。
しかし、多くの一般的なモバイルアプリにおいて、バックエンドサーバの役割には共通点が多いのが一般的です。例えばベタなところで「デバイス間の写真共有アプリ」をイメージした場合、旧来は、写真の保存場所としてサーバを用意していたはずです。その上で、サーバのローカルディスク上に写真を保存します。もしくは、AWSを使っているのであれば、アップロードを受け付けたEC2は、その写真をS3に保存する、というアーキテクチャが良いですね。
ただ、これだけのために *224/365でEC2サーバを運用する必要がありました。たとえアクセスが少ない夜間でも、最低1台できれば2台のEC2が必要です。EC2はAWSの中では比較的高価なリソースであるため、コストの大部分はEC2が占めてしまいます。
そうではなく、画像(等のファイル)の保存であれば、モバイルアプリは「自前のサーバ」にアップロードするのではなく、「S3に直接」アップロードしてしまえばいいではないか。というのが、AWSをmBaaSとして利用する時の考え方です。仮に別途EC2サーバがあるにせよ、もしコレが可能なのであれば、わざわざEC2を経由させて、貴重なコンピューティングリソースを使いながらアップロードする必要が無いことも多いはずです。
CognitoとSTS
ただ、(脚注でも触れましたが)旧来構成のEC2が果たしていた大きな役割が、認証認可です。仮にS3のバケットに対して誰が書き込み(PutObject)してもよく、誰が読み込み(GetObject)してもよく、さらに、Put/Getしたのは誰なのかは判別できなくてよい、というのであれば簡単です。しかし、そんなゆるふわな要件はあまり聞いたことが無いですね。
というのを解決するのがCognitoとSTSです。詳しくは下記を御覧ください。
- Amazon Cognitoによる認証はSTSのweb identity federationとどう違うのか!?
- [AWS][Android] Amazon Cognito のモバイルユーザー認証を使って、AndroidからAWSリソースを利用してみた。
- [AWS][iOS] Amazon Cognito のモバイルユーザー認証 & データ同期 を iOS で使ってみた
上記のエントリーでは、Web Identity(や未認証ゲスト)を判定して、AWSの一時キーを得る方法をご紹介しています。これによって「認証」は実現できました。
IAM Policyによるアクセス制御
次は「認可」ですね。
要件
仮の要件として、このアプリはCognitoによる認証を行う前提で、各ユーザはそれぞれpublic領域とprivate領域として自分専用の領域を持ち、それぞれのアクセス権が下記のようになっている、としてみましょう。
public領域 | private領域 | ||
---|---|---|---|
オーナーが | オブジェクト一覧を取得できる? (ListBucket) | Yes | Yes |
オブジェクトを読める? (GetObject) | Yes | Yes | |
オブジェクトを書ける? (PutObject/DeleteObject) | Yes | Yes | |
オーナー以外が | オブジェクト一覧を取得できる? (ListBucket) | Yes | No |
オブジェクトを読める? (GetObject) | Yes | No | |
オブジェクトを書ける? (PutObject/DeleteObject) | No | No |
で、ディレクトリ構成はこんな感じにしました。
example-bucket ├ public │ ├ us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa │ ├ us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb │ │ ... │ └ userX └ private ├ us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa ├ us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb │ ... └ userX
IAMポリシーの実装
以上に沿ったIAMポリシーを書くとこんな感じです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPublicHomeListingByAllUsers", "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::example-bucket" ], "Condition": { "StringLike": { "s3:prefix": [ "public/*/*" ]}} }, { "Sid": "AllowPrivateHomeListingByOwnerUsers", "Effect": "Allow", "Action": [ "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::example-bucket" ], "Condition": { "StringLike": { "s3:prefix": [ "private/${cognito-identity.amazonaws.com:sub}/*" ]}} }, { "Sid": "AllowPublicHomeObjectReadingByAllUsers", "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::example-bucket/public", "arn:aws:s3:::example-bucket/public/*" ] }, { "Sid": "AllowPrivateHomeObjectReadingByOwnerUsers", "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::example-bucket/private/${cognito-identity.amazonaws.com:sub}", "arn:aws:s3:::example-bucket/private/${cognito-identity.amazonaws.com:sub}/*" ] }, { "Sid": "AllowPublicHomeObjectWritingByOwnerUsers", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::example-bucket/public/${cognito-identity.amazonaws.com:sub}", "arn:aws:s3:::example-bucket/public/${cognito-identity.amazonaws.com:sub}/*" ] }, { "Sid": "AllowPrivateHomeObjectWritingByOwnerUsers", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::example-bucket/private/${cognito-identity.amazonaws.com:sub}", "arn:aws:s3:::example-bucket/private/${cognito-identity.amazonaws.com:sub}/*" ] } ] }
ちょっと複雑で長いポリシーかと思いますが、順に説明していきます。
AllowPublicHomeListingByAllUsers
まず、誰でも/public以下のオブジェクト一覧は見て構わないですね。従って、このステートメントで許可を行います。
ちなみに、このステートメントだと「ユーザの一覧」は取得できません。つまり、明示的にIdentityIDを指定しなければオブジェクト一覧を出すこともできません。もしユーザ一覧を取得できるべきであれば、
"Condition": { "StringLike": { "s3:prefix": [ "public/*/*" ]}}
を
"Condition": { "StringLike": { "s3:prefix": [ "public/*" ]}}
に変更することで、ユーザ一覧を取れるようにできます。
AllowPrivateHomeListingByOwnerUsers
次に、/private以下については、自分のIdentityIDのフォルダだけ、一覧できるようにしたのがこのポリシーです。S3に対するobject listing要求において、prefixパラメータがprivate/${cognito-identity.amazonaws.com:sub}/*のパターンにマッチしたら許可、という記述です。
${cognito-identity.amazonaws.com:sub}の部分は、CognitoのIdentityIDに置き換わります。
AllowPublicHomeObjectReadingByAllUsers
public/以下のオブジェクトは、誰でもダウンロードできる、という比較的明快なステートメントです。
AllowPrivateHomeObjectReadingByOwnerUsers
これも比較的明快ですね。private/${cognito-identity.amazonaws.com:sub}/以下のオブジェクトはオーナーのみがダウンロードできます。
AllowPublicHomeObjectWritingByOwnerUsersとAllowPrivateHomeObjectWritingByOwnerUsers
ここまで理解できればあとは特に説明する必要も無いと思います。
このポリシーの設定先
これを、通常はCognitoのAuth Roleに設定しておきます。が、今回は検証を楽に *3するために、Unauth Roleの方に設定してみます。
検証!
まず、Amazon Cognitoによる認証はSTSのweb identity federationとどう違うのか!?で挙げたサンプルコードを2回実行して、IdentityIDを2つ発行、そしてそれぞれのtemporary credentialsを入手します。
shellを2つ立ち上げ、それぞれにAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN環境変数としてキーを設定します。ここでは1つ目のIdentityIDをus-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa、もう一つをus-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbbとします。
s3上には先に挙げたディレクトリ構造を作っておき、適当なコンテンツを上げておいてください。
まずはpublic直下のリスティング
aaaa$ aws s3 ls s3://example-bucket/public/ A client error (AccessDenied) occurred when calling the ListObjects operation: Access Denied
bbbb$ aws s3 ls s3://example-bucket/public/ A client error (AccessDenied) occurred when calling the ListObjects operation: Access Denied
上記の通り、どちらからもアクセスできませんでした。
次にユーザを明示したpublicのリスティング
aaaa$ aws s3 ls s3://example-bucket/public/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ 2014-10-31 16:21:33 0 2014-10-31 16:21:36 4 foo.txt aaaa$ aws s3 ls s3://example-bucket/public/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ 2014-10-31 16:21:19 0 2014-10-31 16:21:24 4 foo.txt
bbbb$ aws s3 ls s3://example-bucket/public/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ 2014-10-31 16:21:33 0 2014-10-31 16:21:36 4 foo.txt bbbb$ aws s3 ls s3://example-bucket/public/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ 2014-10-31 16:21:19 0 2014-10-31 16:21:24 4 foo.txt
相互にpublicの中身が見えていますね。
publicへの書き込み
aaaa$ echo bar >bar.txt aaaa$ aws s3 cp bar.txt s3://example-bucket/public/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ upload: ./bar.txt to s3://example-bucket/public/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/bar.txt aaaa$ aws s3 cp bar.txt s3://example-bucket/public/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ upload failed: ./bar.txt to s3://example-bucket/public/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/bar.txt A client error (AccessDenied) occurred when calling the PutObject operation: Access Denied
bbbb$ echo bar >bar.txt bbbb$ aws s3 cp bar.txt s3://example-bucket/public/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ upload failed: ./bar.txt to s3://example-bucket/public/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/bar.txt A client error (AccessDenied) occurred when calling the PutObject operation: Access Denied bbbb$ aws s3 cp bar.txt s3://example-bucket/public/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ upload: ./bar.txt to s3://example-bucket/public/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/bar.txt
それぞれ、publicであっても、自分のフォルダ以下でなければ書き込みはできませんでした。
private直下のリスティング
aaaa$ aws s3 ls s3://example-bucket/private/ A client error (AccessDenied) occurred when calling the ListObjects operation: Access Denied
bbbb$ aws s3 ls s3://example-bucket/private/ A client error (AccessDenied) occurred when calling the ListObjects operation: Access Denied
もちろん、どちらからもアクセスできませんでした。
次にユーザを明示したprivateのリスティング
aaaa$ aws s3 ls s3://example-bucket/private/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ 2014-10-31 16:20:59 0 2014-10-31 16:21:03 4 foo.txt aaaa$ aws s3 ls s3://example-bucket/private/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ A client error (AccessDenied) occurred when calling the ListObjects operation: Access Denied
bbbb$ aws s3 ls s3://example-bucket/private/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ A client error (AccessDenied) occurred when calling the ListObjects operation: Access Denied bbbb$ aws s3 ls s3://example-bucket/private/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ 2014-10-31 16:20:41 0 2014-10-31 16:20:51 4 foo.txt
自分のフォルダでなければリスティングはできません。
privateへの書き込み
aaaa$ echo bar >bar.txt aaaa$ aws s3 cp bar.txt s3://example-bucket/private/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ upload: ./bar.txt to s3://example-bucket/private/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/bar.txt aaaa$ aws s3 cp bar.txt s3://example-bucket/private/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ upload failed: ./bar.txt to s3://example-bucket/private/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/bar.txt A client error (AccessDenied) occurred when calling the PutObject operation: Access Denied
bbbb$ echo bar >bar.txt bbbb$ aws s3 cp bar.txt s3://example-bucket/private/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/ upload failed: ./bar.txt to s3://example-bucket/private/us-east-1:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/bar.txt A client error (AccessDenied) occurred when calling the PutObject operation: Access Denied bbbb$ aws s3 cp bar.txt s3://example-bucket/private/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/ upload: ./bar.txt to s3://example-bucket/private/us-east-1:bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/bar.txt
それぞれ、publicと同様、自分のフォルダ以下でなければ書き込みはできませんでした。
まとめ
同じような記述とコマンドが続き、認識が大変だったかもしれません。しかし、以上のような認証認可の仕組みが揃っていますので、モバイルから利用するファイルストレージとして、S3が非常に実用的であることが分かると思います。