AWSマルチアカウントのS3間のcopyのメモ

AWSアカウントAにIAMユーザーA1,
AWSアカウントBにIAMユーザーB1がいるとする。

アカウントAのS3からアカウントBのS3にオブジェクトのコピーを行うには、通常、ユーザーA1としてアカウントAのS3からオブジェクトをgetし、ユーザーB1としてアカウントBのS3にputする方法が考えられる。
 
そうではなく、S3からS3に直接オブジェクトをcopyしたい。
 
そこで、コピー元の対象bucketのバケットポリシーに以下を追加する。
 

{
   "Version": "2012-10-17",
   "Statement" : {
      "Effect":"Allow",
      "Sid":"ReadAccess",
      "Principal" : {
          "AWS":["arn:aws:iam::アカウントBのユーザーB1"]
      },
      "Action":"s3:GetObject",
      "Resource":"arn:aws:s3:::コピー元対象bucket/*"
   }
}

 
cliでのアクセスは以下の通り。クライアントのコンピュータを介することなく複数のprofileを跨いだcopyやsyncができる。

 

aws s3 cp --profile user_A1 s3://A-bucket/object.png --profile user_B1 s3://B-bucket/

Read More

mozjpeg3.1 を Amazon Linux用にフルビルドするメモ

 
ローカルのMac上で開発してると、一部のnpmモジュールはインストール時に自動でMac用のバイナリ生成のコードをインストールしてパスも通してくれちゃうので、そのまま同梱するとAWS Lambda上では当然動かない。
今回は画像の圧縮でよく使われる mozjpegpngquant がそれに当たった。
Lambdaをコールするたび、いちいちコンテナ上でインストールするのも微妙なので出来ればあらかじめ全部入りの実行ファイルをmakeしておく。
地味にいろいろ迷ったので作業メモする。
 
 
Amazon LinuxのDockerImageかubuntuのImage あたりをpullしてきて,docker-machine上で作業。
 

# 基本
apt-get update
apt-get install -y gcc make wget dpkg 

# mozjpeg用
apt-get install -y yasm nasm

wget https://github.com/mozilla/mozjpeg/releases/download/v3.1/mozjpeg-3.1-release-source.tar.gz
tar -xf mozjpeg-3.1-release-source.tar.gz
cd mozjpeg
./configure --disable-shared --enable-static 
make
make deb

 
ここ大事。 普通にやると多分実行時に symbol jpeg_float_quality_scaling, version LIBJPEG_6.2 not defined in file libjpeg.so.62 with link time reference とかで怒られる。

./configure --disable-shared --enable-static

 
これで可逆圧縮用の jpegtran や非可逆圧縮の cjpg が出来ている。
これらを同梱して既存のモジュールにエイリアス貼ったり愚直に置き換えるなりして Lambda上でも使える。
どうしてもLambda上で動いて欲しいものは1ファイルで実行できる形に持っていきたい。

Read More

ブラウザからS3に巨大なファイルを低メモリで送りつけるアレ

ブラウザから8GB以上の巨大なファイルや多量のファイルをS3に送りつけるには、MultiPart Uploadの機能を使っていく必要がある。
具体的には、データを5MB以上のchunkに分け、分割して送りつけることになる。
データが8GBいかないケースでも、ファイル読み込み量を分割できる機構が使えるので、メモリにも優しい。
それなりに使う場面はありそう。

1.とりあえずドキュメント通り実装して徳を積む

マルチパートアップロードの概要
Multipart Upload API を使用したオブジェクトのアップロード

(1) S3のCORS設定

  • ExposeHeader => ETag を指定する。
  • AllowHeader
<AllowedHeader>Authorization</AllowedHeader>
<AllowedHeader>Content-Type</AllowedHeader>
<AllowedHeader>User-Agent</AllowedHeader>
<AllowedHeader>x-amz-date</AllowedHeader>
<AllowedHeader>x-amz-user-agent</AllowedHeader>
<AllowedHeader>x-amz-storage-class</AllowedHeader>
<AllowedHeader>x-amz-acl</AllowedHeader>

この辺を設定する。PUT権とかはあとで動的に付与する。

(2) ブラウザのfile api

ブラウザのfile api でファイル名やbyte数取得。
 
この段階ではFileReaderによるメモリ上への展開は行わないこと。
 
余裕があればWebWorkerで別スレッド立ててUIスレッドへの影響範囲を小さくする。
 

(3) サーバーから署名付きURL取得

ブラウザからのsignedURL発行要求を認可,返却。
S3のbucketやオブジェクトに対し、PUTやHEADリクエストが通るようにする。
 
この辺までは割といつも通り。
 

(4) S3::createMultipartUpload

S3の該当keyに対して createMultipartUpload 要求。uploadIdを取得。
要するに「今から分割ファイルでアップロードするぜ?」という宣言みたいなの。
このuploadIdは今後のリクエストすべてに同梱することとなる。
 

(5) 分割したblobを送りつける S3::uploadPart

 
FileReaderにいきなり巨大なfileオブジェクト食わすと多分2GBあたりでブラウザが音も立てず死ぬ。
chunk送信用のループ内でFile::slice(startByte , endByte)してblob化して都度捨てるのがミソ

 
この部分しかメモリ上に乗らないようになる。これが可能なのよく知らなかった。
並列アップロード数のロジックとかchunkごとの大きさ(5MB以上)とかは様子見ながら好きにカスタムしたらいいんじゃないかな。
 
この時帰ってくるETag値と、送ったPartNumber値はこういう形式でとっておく。

const multipartMap= { Parts: [] };

//...ループ内
multipartMap.Parts[partNumber - 1] = {
    ETag: 返ってきたETag値,
    PartNumber: partNumber
};

(6) 完了 S3::completeMultipartUpload

 
すべて送り終わったら、completeMultipartUploadリクエストを送る。
この時、先ほどのmultipartMapを同梱して送りつける。
これで、初めてS3のバケット上にオブジェクトができる。
 

Code

4-6まではこんな感じになります。今回はaws-sdk使用。全部Promise返すモードがいつの間にかついてて良い。
(並列アップロードのロジック、chunkごとの大きさ指定(5MB以上)とかエラー処理は記事のためあえて省いてる。)

const upload = async (s3 ,  s3Params , file)=>{

    const mime = Mime.lookup(file.name);
    const multiPartParams = s3Params.ContentType ? s3Params : {ContentType : mime , ...s3Params};
    const allSize = file.size;

    const partSize = 1024 * 1024 * 5; // 5MB/chunk

    const multipartMap = {
        Parts: []
    };

    /*  (4)   */
    const multiPartUploadResult = await s3.createMultipartUpload(multiPartParams).promise();
    const uploadId = multiPartUploadResult.UploadId;

    /*  (5)  */
    let partNum = 0;
    const {ContentType , ...otherParams} = multiPartParams;
    for (let rangeStart = 0; rangeStart < allSize; rangeStart += partSize) {
        partNum++;
        const end = Math.min(rangeStart + partSize, allSize);

        const sendData = await new Promise((resolve)=>{
            let fileReader =  new FileReader();

            fileReader.onload = (event)=>{
                const data = event.target.result;
                let byte = new Uint8Array(data);
                resolve(byte);
                fileReader.abort();
            };
            const blob2 = this.file.slice(rangeStart , end);
            fileReader.readAsArrayBuffer(blob2);
        })

        const progress = end / file.size;
        console.log(`今,${progress * 100}%だよ`);

        const partParams = {
            Body: sendData,
            PartNumber: String(partNum),
            UploadId: uploadId,
            ...otherParams,
        };
        const partUpload = await s3.uploadPart(partParams).promise();

        multipartMap.Parts[partNum - 1] = {
            ETag: partUpload.ETag,
            PartNumber: partNum
        };
    }

    /* (6) */
    const doneParams = {
        ...otherParams,
        MultipartUpload: multipartMap,
        UploadId: uploadId
    };

    await s3.completeMultipartUpload(doneParams)
        .promise()
        .then(()=> alert("Complete!!!!!!!!!!!!!"))
}

 
理屈上、fileObjectの参照が残ってれば,中断やキャンセル、断片ごとのリトライも可能なはず。
S3 Multipart Upload Request のタイムアウト設定を長めにしておいてもいいかも。
 
 

2.めんどくさいのでOSSを使う

 
上記までをライブラリ化しようと思ってたけど、ある意味当然のごとく既に存在した。。
仕様も非同期チェーンも特に覚えたくない人は、早くお家帰りたいのでこっちのありものを使う。
 
EvaporateJS
 
一時停止やキャンセル、リトライ、chunkサイズの指定や同時送信数も指定可能。
webWorker上の別スレッドで動かすモードはついてないみたいだけど、よくまとまっててすごい。
ただしPromise使うので、typescriptやbabel等のプリプロセッサ通さない人は、es6-promiseあたりのポリフィルを念のため入れること。
以上です。

Read More