【Swift/AWS】
iOSアプリからAWS S3にZipファイルをアップロード

記事イメージ

お疲れ様です。堺です。

Swiftで実装したiOSアプリで、アプリ内のファイルをAWSのS3のストレージ内に送信するタスクがあったので、実装方法をまとめて共有したいと思います。

この記事の目標:iOSアプリからS3にファイルをアップロードする処理を実装する

本記事は、この実装の全体図に基づいて解説していきます。
本記事は、この実装の全体図に基づいて解説していきます

実装の全体図についての解説

今回のタスクでは、クライアント(iOS)に対して、S3にアップロードする権限を付与する必要があります。

そこで今回は、IAMアカウントなしでも時間制限で権限付与可能なS3 Presinged URLを利用します。このURLに対してHttp PUTリクエストを投げることで、S3にアップロードすることができます。

全体の流れとしては、LambdaがURLを取得しiOSに返却し、そのURLを使ってアップロード処理を行います。

実装

iOSからAPIを呼び出す処理にはAlamofireを使用します。尚、Alamofireはバージョン5.Xを使用します。4.X以前とは書き方が異なるのでお気を付けください。

Alamofireのインストールから始めます。

Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'app_name' do
  # Comment the next line if you don't want to use dynamic frameworks

  use_frameworks!
  pod 'Alamofire'

  # Pods for app_name

end

ターミナル
$ pod install

全体図①の実装(iOS側がS3 Presinged URLをリクエスト)

AlamofireでAPI Gatewayをコールします。

このコードでは、valueAsDict["Url"]にAWSから返ってきたS3 Presinged URLが格納されています。

レスポンスのJsonはこの後の工程で、このコードにマッチするように実装します。

Swift
import Foundation
import Alamofire

let LAMBDA_API_URL_STRING = "https://XXXXXXXX.execute-api.ap-northeast-1.amazonaws.com/XXXXXXX/XXXXXXXXXXXX"
let SECURITY_KEY = "XXXXXXXXXXXXX"

func getUrl(
    completionOk : @escaping (_ s3_presigned_url: String) -> Void,
    completionNg : @escaping () -> Void) {

    AF.request(LAMBDA_API_URL_STRING,
               method: .post,
               parameters: ["file_name":"files.zip"], //これから送信するファイル名(AWS側のパス)を予め通知します。Lambda側でURL発行に必要です。
               encoding: JSONEncoding.default,
               headers: ["x-api-key" : SECURITY_KEY])
         .responseJSON(queue: DispatchQueue.global(qos: .utility)) { response in
        switch response.result {
         case .success(let value):
            if let valueAsDict = value as? Dictionary<String, Any> {
                completionOk(valueAsDict["Url"]) //URLを取得
            } else {
                completionNg()
            }
         case .failure(let error):
            completionNg()
        }
    }
}

全体図③の実装(LambdaがS3 Presinged URLを取得してクライアントに返す)

LambdaランタイムはPython3.9を使用します。

BUCKET_NAMEはS3コンソールの一覧に表示されている名前です。

Python
AWS_S3_BUCKET_NAME = 'XXXXXXXXXXXXXXX'

def lambda_handler(event, context):
    inputs = json.loads(event['body'])    

    s3 = boto3.client(service_name='s3')

    presigned_url = s3.generate_presigned_url(
        ClientMethod='put_object',
        Params={
            'Bucket': AWS_S3_BUCKET_NAME
            'Key': inputs['file_name'], #アップロードするファイルパスをあらかじめ指定します。
        },
        ExpiresIn=300, #URLの有効時間を秒単位で指定。アプリ側でURLを受け取って即アップロード処理するなら、数秒でもOKかと。
        HttpMethod='PUT'
    )
    
    response_body = {"Url":presigned_url}

    return {
        'statusCode': 200,
        'body': json.dumps(response_body)
    }

また、Lambdaの実行ロールにS3にアクセスする権限を付与します。

ここでは簡単のためFull Accessを付与します。

コンソール>関数>設定>アクセス権限>実行ロール>ロール名 から操作します。
コンソール>関数>設定>アクセス権限>実行ロール>ロール名 から操作します

全体図⑤の実装(iOSからS3 Presinged URLを使用してアップロード)

取得したURLに対してPUTリクエストを投げ、ファイルをアップロードします。

全体図①で実装したgetUrlのクロージャに実装します。

Swift
self.getUrl(
    //zipFilePathUrl変数に、あらかじめURLクラスでZipファイルのストレージ内パスを用意してください。
    //SSZipArchiveなどのライブラリでiOSストレージ内にZipファイルを生成できます。
    completionOk: {s3PresignedUrl in
        guard let zipData = try? Data(contentsOf: zipFilePathUrl) else { fatalError("load failed.") }

        let url = URL(string: s3PresignedUrl)!
        let urlRequest = try! URLRequest(
            url: url,
            method: .put,
        )

        AF.upload(zipData, with: urlRequest).response {response in
            if let _ = response.error {
                print('error!')
            } else {
                print('succeed!')
           }
        }
    }, completionNg: {})

セキュリティ等の観点での注意点

S3課金死対策

S3のファイルに上限を設けられないので(たぶん)、このコードを実装したアプリの一般公開は課金死リスクがあると考えています。

対策の一つとして考えられるのが、URLを発行する前にバケット内のサイズ上限を計算し、上限を超えているようであればURL発行を中断するという方向性です。

以下のコードでは、バケット内のファイルサイズの合計を取得して、12,582,912KB(12GB)以上であれば処理を終了するコードです。

尚、list_objects_v2の戻り値については公式ドキュメントの該当箇所をご参照ください。

Python
s3 = boto3.client(service_name='s3')
files = s3.list_objects_v2(Bucket=AWS_S3_BUCKET_NAME)
all_files_size = sum([int(file['Size']/1000) for file in files['Contents']])
if all_files_size >= 12582912:
    return

今回は以上です。ありがとうございました。

記事筆者へのお問い合わせ、仕事のご依頼

当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。

この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。

皆様のご投稿をお待ちしております。

記事筆者へ問い合わせする

※ご相談は無料でお受けいたします。
IT活用経営を実現する - 堺財経電算合同会社