この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
よく訓練されたアップル信者、都元です。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が非常に実用的であることが分かると思います。