Help us understand the problem. What is going on with this article?

AWS APIGateway+lambda+S3を使ってダウンロード機能を実装する

背景

AWS使ってサーバーレスで自分用の家計簿的なwebサービスを勉強も兼ねて開発中。大分自分が欲しかった機能は実装出来てきた。
今後の為にもDynamoDBをバックアップをしたい。DynamoDB自体にバックアップ機構はあるものの、間違えてテーブル自体を削除してしまった時(構築スクリプトミスとか)の為に、CSVやjsonファイルなどでローカルPCに置いておきたい。

方法を考察

手法は色々あると思うが、大きく分けて以下の2つになる。

  • どこかサーバー側で、aws-cliなどを使ってファイル出力する
  • web画面にダウンロード機能を追加する

やっぱりここはダウンロード機能。このサービス開発は勉強も兼ねてるし。
ここで、単純にwebページからダウンロードと言ってもまたそこで細かい手法が存在する。

  1. データを取得して、クライアント側で文字列作成してダウンロード処理
  2. サーバー側で生成したファイルストリームをダイレクトレスポンスで返す
  3. S3にアップして、そのアドレスを返し、リダイレクト
  4. S3にアップ。フォルダ一覧ページも用意してそこからリンククリックダウンロード

考察ポイント

  • 1はクライアントのみで処理が完結するもの向け
  • 2,3はサイズが小さいファイル向け、4は比較的大きいファイル向け(大きいファイルは作成にも時間かかるので、ダウンロード失敗などで再実行したくない)。
  • ファイル作成する方法だと、その後処理を考えないと不要なファイルが溜まっていく。
  • 処理はLambdaでやる予定。 AWS Lambda の制限 は気を付ける。

決定した方針

  • 将来的には扱うサイズが大きくなるかもしれないが、3番目のS3にアップしてそのアドレスを返してリダイレクトを選択する。
  • 全データを出力するのでなく、一定期間で指定して出力できるようにして、そもそもデータ量を多くしないようにする。
  • 出力するS3フォルダはログインユーザー毎のフォルダ
  • S3バケットのライフサイクルポリシーを設定して一定期間過ぎたファイルは消す様にする。
  • 関係するテーブル全て(伝票情報と残額情報)が対象
  • パラメーターは開始終了日

実装

以下の技術要素が必要になる。

  • lambdaでファイル作成
  • 複数ファイルをzip圧縮
  • S3のフォルダを決定する為の認証情報をlambdaに渡す
  • lambdaからS3へファイルアップロード
  • 返却されたS3のurlを使ってダウンロード実行

lambdaでファイル作成

今、lambdaで使用している言語はPython。Pythonにはファイル出力機能はもちろんあるが、lambdaでは物理マシンの概念が無く、用意されているのは/tmpフォルダのみ。このフォルダはOS的には一時ディレクトリと呼ばれる場所。一定時間が切れたり再起動されたら消える。反対に言えば放置してもOS的に処理してくれる。
tempfileモジュールで取得したフォルダを使うべきなのか、公式ページ上でlambdaで使用できるフォルダとして指定されてる/tmp表記を直接使うべきか悩むところではあったけど、tempfileモジュールを使う事に決定。
後々圧縮するので、tempfile.TemporaryDirectoryで一時ディレクトリ作って、その中で色々する事にする。

複数ファイルをzip圧縮

ここはそんなに悩む場所ではない。zipfileモジュール使って普通に処理。

S3のフォルダを決定する為の認証情報をlambdaに渡す

最初は認証のjwtトークンをlambdaに渡して色々する必要があると思ったが、S3のフォルダ名に使用するidentityIdはそこからは取得できなさそうだった。その為、クライアント側で取得できるidentityIdを普通にクエリパラメーターでそのまま渡す事にした。

lambdaからS3へファイルアップロード

ここもそんなに悩む場所ではない。boto3モジュールで普通に出来る。

ここまでのlambda側処理は以下の感じ。try句が2重になっているが、圧縮前ファイルをcloseした状態でないとちゃんとファイルに出力されていない為。flushすればいいかもしれないが、ファイルポインタ解放の為にもcloseすべき。そして、S3アップの前に一時ディレクトリをcloseしてしまうと消えてしまう可能性があ。という事で、結果的に2重のtry句になった。

lambda側ファイル関係処理部分(python)
# 宣言部分
import boto3
import os
import tempfile
import zipfile
# 中略
    tmpdir = tempfile.TemporaryDirectory()
    try:
        packed_full_name = os.path.join(tmpdir.name, packed_file_name)
        slip_full_name = os.path.join(tmpdir.name, slip_file_name)
        balance_full_name = os.path.join(tmpdir.name, balance_file_name)
        # 中略
        slip_file = open(slip_full_name, 'w')
        balance_file = open(balance_full_name, 'w')
        try:
            # 出力処理部分
        finally:
            slip_file.close()
            balance_file.close()

        # 出力ファイルをまとめてzip圧縮
        with zipfile.ZipFile(packed_full_name, 'w', compression=zipfile.ZIP_DEFLATED) as new_zip:
            new_zip.write(slip_full_name, arcname=slip_file_name)
            new_zip.write(balance_full_name, arcname=balance_file_name)

        # S3へアップロード。event['identityid'] がクエリパラメータで渡されたCognitoのidentityid
        s3 = boto3.resource('s3')
        bucket = s3.Bucket('myapp-userdata')
        bucket.upload_file(packed_full_name, 'cognito/myhome-account/' + event['identityid'] + '/' + packed_file_name)
    finally:
        tmpdir.cleanup()

返却されたS3のurlを使ってダウンロード実行

出力先のS3バケットが、publicならそのurlを直接使えるが、privateの場合、AccessDeniedが出てしまう。s3.getSignedUrl で一時ダウンロードURLを生成し、それを使わなければならない。生成出来たらあとはlocation.hrefに指定するだけ。

クライアント側javascriptダウンロード部分
    download: function () {
      const cognitoUser = this.$cognito.userPool.getCurrentUser()
      var that = this
      cognitoUser.getSession((err, session) => {
        if (!err && session.isValid()) {
          const itoken = session.getIdToken().getJwtToken()
          // Initialize the Amazon Cognito credentials provider
          AWS.config.region = awsconfig.Region
          AWS.config.credentials = new AWS.CognitoIdentityCredentials({
            IdentityPoolId: awsconfig.IdentityPoolId,
            Logins: {
              [PROVIDER_KEY]: itoken
            }
          })
          var identityId = AWS.config.credentials.identityId

          //・・中略・・

          that.$axios.get(that.apienv.baseendpoint + 'download?' + prmstr, config).then(
            response => {
              var s3 = new AWS.S3({
                params: { Bucket: S3_USERBACKETNAME }
              })
              var getUrlparams = {
                // バケット名
                Bucket: S3_USERBACKETNAME,
                // S3に格納済みのファイル名(ファイル名がサーバー側から返ってくる)
                Key: 'cognito/myhome-account/' + identityId + '/' + response.data,
                // 期限(秒数)
                Expires: 900
              }
              // URL発行
              s3.getSignedUrl('getObject', getUrlparams, that.execdownload)
            }
          ).catch(err => {
            that.$message({message: err, type: 'error'})
          })
        }
      })
    },
    execdownload: function (dummy, url) {
      location.href = url
    },

学んだ事

  • ファイル出力を確定する前に、そのファイルに対して別の処理してはいけない。
  • jwtから取得できる情報にはidentityId(s3などのユーザー毎キーになる)は含まれてない(?)
  • オープンでないS3バケットに上げたものは、getObjectで直接取得できない。
  • s3.getSignedUrlで一時URLを発行し、location.href すべし。

実際のソース(lambda側)

参考にさせてもらったページ

AWS Lambda の制限
Lambda + API Gateway入門。CSVやCORS
API Gateway + Lambdaでバイナリダウンロード
DynamoDBからデータをCSVにエクスポートする方法4つ
【備忘録・まとめ】AWS Lambda 開発者ガイド
【JavaScript入門】ファイルダウンロード処理を実装する方法とは?
Amazon Cognitoの認証情報を取得してみる~API Gateway+Lambda編~
【vue.js】AWS-S3へ簡単にファイルをアップロードする方法

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした