iOSアプリでS3からPre-signed URLを取得する

# 1. 目的
S3のオブジェクトに一時的にアクセスできるURLをiOSアプリから発行します。
具体的には、ios用のAWS Amplifyを使ってS3のオブジェクトのPre-signed URL（署名付きURL）を取得します。

# 2. 対象者
 - iOSアプリでS3のpre-signed URLを取得したい人
 - AWS公式のamplify関連ドキュメントが分かりづらいと思った人

# 3. 環境
 - macOS Mojave
 - Swift5
 - Xcode10.2.1

# 4. 前提条件
node.js, npmがインストール済み

# 5. 手順
## 5-1. AWSの設定
aws-amplifyをローカルにインストール

```:コンソール
$ npm install -g @aws-amplify/cli
```

これ以降の作業はアプリのルートフォルダで行うこと。
次に、amplifyが用いるIAMユーザーを作成し、設定プロファイルを保存します。
以下のコマンドを実行してください。

```:コンソール
$ amplify configure 
```
ブラウザのAWSコンソールに飛ぶので、サインインしてください。
コンソールに戻ってEnterを押したら、リージョンを選択して作成するIAMユーザーの名前を入力します。
終えると再びブラウザに飛びます。このときIAMユーザーに権限を設定するのですが、AmplifyはS3だけでなく、CognitoやLambda、Cloudformationなど多くのサービスを使用するので、デフォルトのAdministratorAccessを設定することをオススメします。
IAMユーザー作成の最後に、ユーザー・アクセスキーID・シークレットアクセスキーが表示されるので、必ずメモしてください。この画面を閉じると二度と表示されません。

再びコンソールに戻ってEnterを押すと、以下のようにアクセスキーIDとシークレットアクセスキーの入力が求められます。最後にプロファイルの名前を設定して完了です。

```
Enter the access key of the newly created user:
? accessKeyId:  <YourAccessKeyId>
? secretAccessKey:  <YourSecretAccessKey>
This would update/create the AWS Profile in your local machine
? Profile Name:  <TypeProfileName>
```

`Successfully set up the new user.`と表示されれば完了です。

次に、Amplifyが使用するxcodeのライブラリをインストールします。
Podfileに以下のように追記してください。

```:Podfile
target 'MyApplication' do
  use_frameworks!
  #以下の3行を追記
  $awsVersion = '~> 2.10.0'
  pod 'AWSMobileClient', $awsVersion
  pod 'AWSS3', $awsVersion

  target 'MyApplicationTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'MyApplicationUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end
```
編集を終えたら、以下のコマンドを実行してAWSMobileClientとAWSS3をインストールします。

```:コンソール
$ pod install

Analyzing dependencies
Downloading dependencies
Installing AWSAuthCore (2.10.2)
Installing AWSCognitoIdentityProvider (2.10.2)
Installing AWSCognitoIdentityProviderASF (1.0.1)
Installing AWSCore (2.10.2)
Installing AWSMobileClient (2.10.2)
Installing AWSS3 (2.10.2)
Generating Pods project
Integrating client project
```

その次に、作成したamplifyプロファイルの設定をアプリに反映させます。
以下のコマンドを実行して、Xcodeプロジェクトの名前、作成する環境の名前、使っているエディター、アプリの種類を入力してください。

```:コンソール
$ amplify init

? Enter a name for the project <TypeYourProjectName>
? Enter a name for the environment <TypeYourEnvironmentName>
? Choose your default editor: <ChooseEditor>
? Choose the type of app that you're building <ChooseApp>
```

下記のように表示されれば成功です。

```:コンソール
Initialized your environment successfully.

Your project has been successfully initialized and connected to the cloud!
```

環境が構築されたら、今度はS3のストレージと連携します。
以下のコマンドを実行してください。

```:コンソール

$ amplify add storage

# 動画や画像、音声を扱う場合は"Content"を選択
? Please select from one of the below mentioned services Content (Images, audio, video, etc.)
# S3にアクセスする際の権限の設定をするかどうか聞かれるので"Yes"を選択
? You need to add auth (Amazon Cognito) to your project in order to add storage for user files. Do you want to add auth now? Yes
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 # デフォルトの権限・セキュリティ設定を用いるか聞かれるので"Default configuration"を選択
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections. 
 # Cognito Userがサインインに用いる媒体を聞かれるので"Email"を選択
 How do you want users to be able to sign in? Email
 # 詳細な設定をするか聞かれるので"No, I am done."を選択
 Do you want to configure advanced settings? No, I am done.
Successfully added auth resource
# リソースに対して適当なニックネームを付ける
? Please provide a friendly name for your resource that will be used to label this category in the project: 08071219frie
# 新規作成するバケットの名前を付ける
? Please provide bucket name: shion-test-08071219
# バケットにアクセスできる権限を設定。pre-signed URLを発行するにはguestを含める必要がある。
? Who should have access: Auth and guest users
# Authユーザー（AWSのアカウントを持ったユーザー）への権限を付与。後から変更可能
? What kind of access do you want for Authenticated users? create/update, read, delete
# Unauthユーザー（AWSのアカウントを持たないユーザー）への権限を付与。後から変更可能
? What kind of access do you want for Guest users? create/update, read, delete
# Lambdaと連携するか聞かれるので"No"を選択
? Do you want to add a Lambda Trigger for your S3 Bucket? No
```

これでバックエンドの設定が完了しました。
これをクラウドに反映させるために以下のコマンドを実行します。

```:コンソール
$ amplify push

Current Environment: <Your Env Name>

| Category | Resource name      | Operation | Provider plugin   |
| -------- | ------------------ | --------- | ----------------- |
| Auth     | <YourResourceName> | Create    | awscloudformation |
| Storage  | <YourFriendlyName> | Create    | awscloudformation |
# 続行するか聞かれるので"Yes"を入力
? Are you sure you want to continue? Yes
```

`✔ All resources are updated in the cloud`と表示されれば完了です。

AWSの設定の最後に、先ほど設定したUnauthRoleの権限を変更します。このUnauthRoleというのはAmplifyがpre-signed URLを取得する際に使われるIAMのロールで、先ほどpushした際に作成されています。

まずは、ブラウザからAWSのIAMコンソールを開き、ロール一覧画面を表示します。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287853/7273ff71-ceb6-5fb9-1194-8e247a02d48a.png)

unauthで検索すると出てくるロールをクリックし、ロール詳細画面に移動します。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287853/3e08db0c-8d0f-b1a2-e15b-b4e9708a269c.png)

インラインポリシーの追加をクリックし、JSONエディタでポリシーを編集します。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287853/8e25a5ad-58f3-02c6-a19e-fca92962d088.png)

以下のように設定すると、指定したバケット内のオブジェクトに対して一覧の取得とダウンロードが可能になります。

```json:JSONエディタ
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Resource": [
                "arn:aws:s3:::<your-bucket-name>",
                "arn:aws:s3:::<your-bucket-name>/*"
            ]
        }
    ]
}
```

入力を終えたら「ポリシーの確認」に進み、ポリシーに名前をつけて権限を確認します。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287853/bd7053d1-3297-62b8-3ed2-7a253e480b9d.png)

「ポリシーの作成」をクリックしたら設定完了です。
これでAWS側の設定は終わりになります。


## 5-2. Xcodeの設定
プロジェクトにapmlifyの設定をインポートします。

xcworkspaceファイルを開き、Navigation Barを右クリックして"Add Files to <Your Project Name>"を選びます。
`amplify init`した際に"awsconfiguration.json"というファイルが生成されているので、追加します。
下の画像のように、Info.plistと同じ階層に追加されれば成功です。
![スクリーンショット 2019-08-09 18.10.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287853/a47dab80-26dd-a60b-fc24-5468c445f7dd.png)

ここで、pre-signed URLを取得する先を先ほど作成したバケットではなく、すでに作ってあるバケットを使いたい場合は以下のように編集してください。

```json:awsconfiguration.json
    "S3TransferUtility": {
        "Default": {
            "Bucket": "<Your-Existing-Bucket>",
            "Region": "<Your-Bucket-Region>"
        }
    }
```

次に、AppDelegate.swiftファイルを編集します。
下の図のように書き換えてください。

```swift:AppDelegate.swift
import UIKit
import AWSS3
import AWSMobileClient

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    func application(_ application: UIApplication,
                     handleEventsForBackgroundURLSession identifier: String,
                     completionHandler: @escaping () -> Void) {
        
        AWSMobileClient.sharedInstance().initialize { (userState, error) in
            guard error == nil else {
                print("Error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
                return
            }
            print("AWSMobileClient initialized.")
        }
        
        //provide the completionHandler to the TransferUtility to support background transfers.
        AWSS3TransferUtility.interceptApplication(application,
                                                  handleEventsForBackgroundURLSession: identifier,
                                                  completionHandler: completionHandler)
    }
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }
    
}
```

ここから、実際にpre-signed URLを発行するコードを書いていきます。

最初に、AWSにアクセスする認証を以下のように行います。

```swift:ViewController.swift
// "regionType"は".USEAST1"のように指定するバケットの地域を書いてください
// identityPoolIdは、awsconfiguration.jsonのCredentialsProvider > CognitoIdentity > Default > PoolIdを使用してください
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: <Your-Bucket-Region>, identityPoolId: <YourIdentityPoolId>)
let configuration = AWSServiceConfiguration(region:<Your-Bucket-Region>, credentialsProvider:credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = configuration
```

それから、pre-signed URLの取得は以下のように行います。
まずはインスタンスを作成し、取得するpre-signed URLのプロパティを設定します。

```swift:ViewController.swift
let getPreSignedURLRequest = AWSS3GetPreSignedURLRequest()
getPreSignedURLRequest.bucket = "<Your-Bucket-Name>" //バケット名を入力
getPreSignedURLRequest.key = "<Object-Key>" //pre-signed URLを取得したいオブジェクトのS3上のパスを入力
getPreSignedURLRequest.httpMethod = .GET
getPreSignedURLRequest.expires = Date(timeIntervalSinceNow: 60)  // URLの有効期限を秒単位で設定

```

次にURLを取得します。

```swift:ViewController.swift
AWSS3PreSignedURLBuilder.default().getPreSignedURL(getPreSignedURLRequest).continueWith { (task:AWSTask<NSURL>) -> Any? in
    if let error = task.error as? NSError {
        print("Error: \(error)")
        return nil
    }
    let presignedURL = task.result
    print(presignedURL!)
    // 処理を追加
    return nil
}

```

これでコードは完成です。
全体図は以下のようになります。

```swift:ViewController.swift
import UIKit
import AWSS3

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let credentialsProvider = AWSCognitoCredentialsProvider(regionType: <Your-Bucket-Region>, identityPoolId: <YourIdentityPoolId>)
        let configuration = AWSServiceConfiguration(region:<Your-Bucket-Region>, credentialsProvider:credentialsProvider)
        AWSServiceManager.default().defaultServiceConfiguration = configuration

        AWSS3PreSignedURLBuilder.default().getPreSignedURL(getPreSignedURLRequest).continueWith { (task:AWSTask<NSURL>) -> Any? in
            if let error = task.error as? NSError {
                print("Error: \(error)")
                return nil
            }
            let presignedURL = task.result
            print(presignedURL!)
            // 処理を追加
            return nil
        }
    }
}
```

以上で全ての準備が整いました。
実際に動かしてみてください！

# 6. 参考
 - [The Amazon S3 Background Transfer Sample](https://github.com/awslabs/aws-sdk-ios-samples/tree/master/S3TransferUtility-Sample/Swift)
 - [Amplify Framework / iOS SDK / Storage](https://aws-amplify.github.io/docs/ios/storage)