やりたい事
AndroidやiOSなどのモバイルアプリケーションからデータをクラウド上にアップロードしたい場合があるかと思います。以下のような要件をみたすアーキテクチャーを実現したいと考えています。
- データをアップロードするS3のバケットは公開されいない。保存されたデータも公開されない。
- IAMのアクセスキーをアプリケーションにハードコーディングするといった、セキュリティ上問題がある事は行わない。
上記を実現するためにCognitoを用いて、Androidアプリケーションに一時的なクレデンシャルを与えて、許可されたS3のバケットにファイルをアップデートする仕組みを作ります。
S3バケットの準備
データアップロード様のS3バケットを準備します。このバケットは、中に格納されるオブジェクトが謝って公開されない様に、バケットのパブリックアクセスコントロールリスト(ACL)を全て設定した状態で作成します。
上記の設定をして置くと、バケットの中のオブジェクトを公開しようとしても、下記の様にエラーになります。バケットのACLの方がオブジェクトのACLより優先されるという事かと思います。
アプリケーションからのデータは一定期間だけ保存すれば良い場合は、バケットに対してライフサイクルを設定する事ができます。
この例では、90日以上経過したデータは削除する設定にしています。ライフサイクルのルールは最大1000個まで設定可能な模様です。
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/intro-lifecycle-rules.html
Cognitoの設定
CognitoのIDプールを作成します。Cognitoは通常、FacebookやGoogleなどのユーザー認証を簡単に実現する事ができますが、今回はアプリケーションでログインさせるのではなく、認証なしでアプリケーションにS3の権限を与えます。よって認証プロバイダーは設定しません。
次にロールの設定で、先ほど作成したS3のバケットに権限を与えます。unauthenticated identitiesに対してロールを作成し、以下の様な設定を行います。
以下のポリシーでは、my-mobile-app-dataバケット以下のオブジェクトに対して全ての権限が与えられています。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"mobileanalytics:PutEvents",
"cognito-sync:*"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*Object"
],
"Resource": [
"arn:aws:s3:::my-mobile-app-data/*"
]
}
]
}
IDプールを作成すると、以下の様にAndroidで実装するためのサンプルを表示してくれます。
Androidアプリの実装
AWSのSDKを利用しますので、project直下のbuild.gradleにクラスパスを追加します。
dependencies {
classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.+'
次に必要となる、ライブラリをapp/build.gradleに設定します。
dependencies {
implementation 'com.amazonaws:aws-android-sdk-core:2.+'
implementation 'com.amazonaws:aws-android-sdk-cognito:2.+'
implementation 'com.amazonaws:aws-android-sdk-s3:2.+'
この例では/sdcard直下のtest.jpegをアップロードしますので、インターネット関連のパーミッションに加えて、外部ストレージアクセスのパーミッションを設定します。パーミッションだけではうまく行かない場合があるので、Androidの設定画面のストレージから作成したアプリにアクセス権を与えて下さい。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
以下、簡単にプログラムを書きました。シンプルにするためにMain ActivityのonCreateの中でAsyncTaskを作って、その中でS3のバケットにアップデートします。Cognitoで一時的なクレデンシャルを取得して、その後にS3 Clientを作成してファイルをアップロードしています。 以下のコードでS3のバケットにアップロードされる事を確認しました。
private static class FileUploadTask extends AsyncTask <Integer, Integer, Integer> {
private static final String BUCKET = "my-mobile-app-data";
private static final String TEST_FILE = Environment.getExternalStorageDirectory().getPath() + "/test.jpeg";
private final Context mContext;
private FileUploadTask(Context context) {
mContext = context;
}
@Override
protected Integer doInBackground(Integer... value) {
CognitoCachingCredentialsProvider credentialsProvider = new CognitoCachingCredentialsProvider(
mContext,
<ID Pool>, // ID プールの ID
Regions.AP_NORTHEAST_1 // リージョン
);
AmazonS3Client s3 = new AmazonS3Client(credentialsProvider, Region.getRegion(Regions.AP_NORTHEAST_1));
TransferUtility transferUtility = TransferUtility.builder().s3Client(s3).context(mContext).build();
File file = new File(TEST_FILE);
TransferObserver transferObserver = transferUtility.upload(BUCKET, "test.jpeg", file);
transferObserver.setTransferListener(new TransferListener() {
@Override
public void onStateChanged(int id, TransferState state) {
}
@Override
public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
}
@Override
public void onError(int id, Exception ex) {
}
});
return 0;
}
@Override
protected void onPostExecute(Integer result) {
}
}
