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

AWS Elastic TranscoderとKey Management Serviceを使って素敵にHTTP Live Streaming

HTTP Live Streamingとは

HTTP Live Streaming(HLS)というのがあります。
https://developer.apple.com/streaming/

Apple神が作った映像や音声のストリーミングプロトコルです。
ざっくり言うと、音声ファイルを短く分割したリソースファイル(.ts)と、分割したファイルを管理するプレイリストファイル(.m3u8)の2つを使って、HTTPプロトコルにのっとってダウンロードしつつ再生すればいいじゃん、的なやつです。
既存のプロトコルベースだし実装が単純なので色んな所で使われています。
最近だとAbemaTVとか。

これが便利なのは、リソースの暗号化と、回線に応じたリソースファイルの出し分けが規定されているのです。
https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-4.3.2.4
https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-4.3.4.2

この規定にのっとって、複数のビットレートのリソースファイルと暗号化キーを用意しておけば、あとは何も考えずに対応するプレイヤーにぶち込めが勝手にいい感じに再生してくれる、と。
あら便利。
というわけで、コイツをAWSのElastic Transcoderと、暗号化キーを管理するKey Management Serviceを使って、リソースファイルと暗号化を自動化できないかなー、と思ってやってみたらできたのでメモ。

作り方

  1. Identity and Access Management(IAM)のページのメニューにある、Encryption keys(日本語だと暗号化キー)の項目からKey Management Serviceに飛ぶ。(わかるかこんなの)
  2. KMSでで暗号化キーを作る。キモはTranscoderで使うS3のバケットと同じリージョンで作ることと、キー管理者とキーユーザーに”Elastic_Transcoder_Default_Role”を指定すること。これをやらないと何度やってもエンコードエラーになる。
  3. TranscoderでPipelineを作る。ここではEncryptionの項目に、先程KMSで設定したマスターキーのARNの設定をすること。

  1. TranscoderのJobを作る。出力するビットレート毎にOutput Detailsを作り、Playlistの項目は一つだけでOutputs in Master Playlistの項目に、Output Detailsで設定したOutputをすべて追加する。

  1. おもむろにCreate New Jobを押して出来上がりを待つ

確認

ファイルの確認

今回エンコードされたファイルはこんな感じ。

Test001
├── 160k
│   ├── track.key
│   ├── track.m3u8
│   ├── track00000.ts
│   ├── track00001.ts
│   ├── track00002.ts
│   ├...
│   └── track00064.ts
├── 64k
│   ├── track.key
│   ├── track.m3u8
│   ├── track00000.ts
│   ├── track00001.ts
│   ├── track00002.ts
│   ├...
│   └── track00064.ts
└── Test001.m3u8

マスタープレイリストのTest001.m3u8の中身はこんな。

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=189000,CODECS="mp4a.40.2"
160k/track.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=89000,CODECS="mp4a.40.5"
64k/track.m3u8

それぞれのリソースのプレイリストはこんな感じ。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:11
#EXT-X-KEY:METHOD=AES-128,URI="track.key",IV=0x420473ab30beeaabfe1aa878fda4b312
#EXTINF:10.007800,
track00000.ts
#EXTINF:10.007789,
track00001.ts
#EXTINF:9.984567,
track00002.ts
...
#EXTINF:0.116089,
track00064.ts
#EXT-X-ENDLIST

問題なさげ。

再生テスト

Test001ディレクトリー以下を全部ダウンロードしてきて、マスタープレイリストをVLCメディアプレイヤーにつっこんだらちゃんと再生できるか確認。

あと、
http://dev.classmethod.jp/smartphone/iphone/network-link-conditioner/
にあるNetwork Link Conditionerを使って回線速度を制限して、ちゃんと64kbpsのファイルがダウンロードされるかどうかを串刺して確認。

できた。

Read More

Golang or NodeJSで AWS Step Functions のActivityを書く

AWS Step Functionsによるピタゴラスイッチの終点で、「通常のEC2上でStep Functionsを待ち受け、RDS等になんか処理をするためのタスク」を書いている。

{
  TaskToken:xxxxx
  Input:<前段のタスクで処理したresponse>
}

ワーカーを作動させていると、こういう感じのデータがStepFunctionsから渡ってくるので、それを元に好きに処理して、SuccessやFailureリクエストをStep Functions側に返せばいい。
とりあえずDBへの登録処理とか、データの読み取りなしで実装してみる。

NodeJS

単純にNodeJSでやるならこういう感じ。

src/worker.ts

const AWS = require("aws-sdk");

const stepfunctions  : AWS.StepFunctions = new AWS.StepFunctions({
    apiVersion: '2016-11-23',
    region : "ap-northeast-1"
});


(async ()=>{
    while (1) {
        await new Promise(async (resolve) => {
            const stepFunctionParam = {
                activityArn: 'arn:aws:states:ap-northeast-1:xxxxxxx:activity:StepFunction-Called', /* required */
            };

            const data = await stepfunctions.getActivityTask(stepFunctionParam).promise();

            if (data != null && data.taskToken != null){
                console.log(data)

         /* ... Do Something !!!!!!! */

                const sendParams = {
                    output: "true", /* required */
                    taskToken: data.taskToken, /* required */
                };   

                await stepfunctions.sendTaskSuccess(sendParams).promise()

            }
            resolve();
        }).catch(error => console.error(error))
    }
})()

 
走れ。
 

AWS_PROFILE=my_aws_profile NODE_ENV=test node build/worker.js

 

タスク部分並列処理にしたかったらawaitやらresolveの位置いじっておく。
とりあえずNodeJSで動作確認したが、この手のコードはgolangがポ〜タビリティ高くて良さそうなので、次はGo言語の入門がてら書いてみる。

Go

あとgoのパッケージマネージャツール(vendor管理)glideも使ってみた。

glide create
glide get github.com/aws/aws-sdk-go

しておく。

main.go

package main

import (
    "fmt"
    "github.com/aws/aws-sdk-go/service/sfn"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/aws"
    "time"
)

func asyncTask(sfnSession *sfn.SFN , resp *sfn.GetActivityTaskOutput){

        /* ... Do Something !!!!!!! */

    params := &sfn.SendTaskSuccessInput{
        Output:    aws.String("true"),      // Required
        TaskToken: resp.TaskToken, // Required
    }
    sfnSession.SendTaskSuccess(params)

}

func loop() {

    awsConfig :=  &aws.Config{Region: aws.String("ap-northeast-1")}

    sess, err := session.NewSession(awsConfig)
    if err != nil {
        fmt.Println("failed to create session,", err)
        return
    }

    sfnSession := sfn.New(sess)

    params := &sfn.GetActivityTaskInput{
        ActivityArn: aws.String("arn:aws:states:ap-northeast-1:xxxxxxxxx:activity:StepFunction-Called"), // Required
    }
    resp, err := sfnSession.GetActivityTask(params)

    if err != nil {
        fmt.Println(err.Error())
        return
    } else if resp.TaskToken != nil {
        fmt.Println("success")

        // 並列処理
        go asyncTask(sfnSession , resp)
    }

    fmt.Println(resp)

}

func main() {

    for {
        loop()
    }

}

 
走れ。

AWS_PROFILE=my_aws_profile go run main.go

awsのconfigとかloopの外でよかったかな。
Go初めてなので参照周りやセッションとかもうちょっと調べる。

 

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

AWS Step Functionsでマイクロサービスから状態管理を(なるべく)取り除きたい

書くこと

「プロジェクトごとに何度も作るような処理の一部をマイクロサービス化する」という仕事があって、その一環でStep Functionを使ったので、途中までやっていたことまとめ。
 
 
example project

ピタゴラスイッチ、その辛み

AWS Lambdaは便利だが、個別の非常に短いFunctionを繋いで1つの大きなフローを組む場合、状態管理に非常に気を使う側面があった。
エントリーポイントになるFunctionがそれぞれの個別タスクの処理結果を待ち受けするような処理にすれば話は簡単になるが、
親タスクが子タスクの状態を管理するような構成にすると、エントリーポイントそのもののタイムアウト時間を気にする必要も出てくる。
基本的にはタスク発火のエントリーポイントでは「処理依頼を投げたら投げっぱなし」にしてさっさとその役割を終えたい。

だが、こういうピタゴラスイッチ的な構成を組もうとすると、動作させたい個別のタスクごとにいちいちSQSやらDynamoやらでタスクキュー的な実装をする必要があった。1つの役割をするためだけに、なんか個々のサービスの関係性が複雑になってくる。
 

StepFunctions

昨年末発表のAWS Step Functions で、使ってみたらこの辺がだいぶ楽になりそうだった。
詳細は弊社橋本さんのこれを参照。
 

serverless-stepfunctions

Serverless Framework上でStep Functionsを定義するプラグインを早速作っていた方がいて、とても便利なので使っていく。

テスト的に組んだフローはこんな感じ

  • エントリーポイントのfunctionsではタスク設定を書いたobjectを生成する
  • Parallelステートで同じFunctionに処理を分岐。Filters機能を使って、エントリーポイントのoutputからで反応すべきkey指定
  • 個々のFunctionは確率的にFail、リトライ
  • 並行的に走らせたすべてのタスクが終わったら最後のFunctionsを起動
     

Parallelでは動的に並列タスクを増減できないのが弱点。
 
ここまでが出来るようになったServerlessFrameworkのexampleプロジェクトを、許可を取ったのでリポジトリ公開する。

処理の定義はこんな感じ。yaml見やすい。
 
serverless.yml
 

service: step-test

provider:
  name: aws
  runtime: nodejs4.3
  stage: dev # replace your setting
  profile: dev # replace your setting
  region: ap-northeast-1 # replace your setting

package:
  exclude:
   - src/**
   - node_modules/** # 今回は特に実行時依存モジュールないので外しておく
   - .local/**
   - .git/**

plugins:
  - serverless-step-functions
  - serverless-offline

stepFunctions:
 stateMachines:
   testStateMachine-v5:
     Comment: "Input and Parallel Function"
     StartAt: EntryState
     States:
       EntryState:
         Type: Task
         Resource: entry
         Next: ChildState
       ChildState:
         Type: Parallel
         Next: EndState
         Branches:
          - StartAt: Child0
            States:
              Child0:
                Type: Task
                InputPath: "$.0"
                Resource: childTask
                End: true
                Retry:
                - ErrorEquals:
                  - HandledError
                  IntervalSeconds: 5
                  MaxAttempts: 6
                  BackoffRate: 1
                - ErrorEquals:
                  - States.TaskFailed
                  IntervalSeconds: 5
                  MaxAttempts: 6
                  BackoffRate: 1
          - StartAt: Child1
            States:
              Child1:
                Type: Task
                InputPath: "$.1"
                Resource: childTask
                End: true
                Retry:
                - ErrorEquals:
                  - HandledError
                  IntervalSeconds: 5
                  MaxAttempts: 6
                  BackoffRate: 1
                - ErrorEquals:
                  - States.TaskFailed
                  IntervalSeconds: 5
                  MaxAttempts: 6
                  BackoffRate: 1
          - StartAt: Child2
            States:
              Child2:
                Type: Task
                InputPath: "$.2"
                Resource: childTask
                End: true
                Retry:
                - ErrorEquals:
                  - HandledError
                  IntervalSeconds: 5
                  MaxAttempts: 6
                  BackoffRate: 1
                - ErrorEquals:
                  - States.TaskFailed
                  IntervalSeconds: 5
                  MaxAttempts: 6
                  BackoffRate: 1
       EndState:
         Type: Task
         Resource: lastTask
         End: true

functions:
  entry:
    handler: build/src/testFuncHandler.entry
    timeout :30
    memorySize : 128

  childTask:
    handler: build/src/testFuncHandler.childTask
    timeout : 30
    memorySize : 128

  lastTask:
    handler: build/src/testFuncHandler.lastTask
    timeout : 30
    memorySize : 128



 

エントリーポイント、個別タスクのFunctionの定義はこんな感じ。

module.exports.entry = async (event ,context , callback)=>{

    const hoge = {
        0:{
            id : 0,
            name: "hoge"
        },
        1:{
            id : 1,
            name: "fuga"
        },
        2:{
            id : 2,
            name: "piyo"
        }
    };

    callback(null , hoge);
}

/*
 serverless logs -f childTask --region ap-northeast-1 --profile dev --stage dev --tail --interval 500
 * */
module.exports.childTask = async (event ,context , callback)=>{

    const num = Math.random();
    if (num < 0.25){
        console.log("ERROR!!!")
        const error = new Error("something is wrong");
        return callback(error);
    }

    const name= event.name
    callback(null , {Name : name});
}

/*
 serverless logs -f lastTask --region ap-northeast-1 --profile dev --stage dev --tail --interval 500
* */
module.exports.lastTask = async (event ,context , callback)=>{
    console.log(event)
    callback(null , event);
}

 
AWS console上でこのフローをイメージ化するとこんな感じになる。
 


 

いざタスクをinvokeすると、

sls invoke stepf --state testStateMachine-v5 --stage dev --profile dev --regeon ap-northeast-1 

 
最後のタスクのeventには各並列処理の実行結果の配列が入ってくる。
 

{ executionArn: 'arn:aws:states:ap-northeast-1:xxxxxxx:execution:step-test-dev-testStateMachine-v5:xxxxxxxxx',
  stateMachineArn: 'arn:aws:states:ap-northeast-1:xxxxxxxxxx:stateMachine:step-test-dev-testStateMachine-v5',
  name: 'xxxxxxxxxxxxxxxxxxx',
  status: 'SUCCEEDED',
  startDate: xxxxxx,
  stopDate: xxxxxx,
  input: '{}',
  output: '[{"Name":"hoge"},{"Name":"fuga"},{"Name":"piyo"}]' }

 

まとめ、ToDo

各Functionのリトライ周りがすっきりした。すごい。
今回の要件が限定的なこと、StepFunctionsのテスト的なこともあって「常に複数のParallel Functionを静的に確保する」構成を作ってみたが、やはりここは何か気持ち悪い。
やっぱこれ、動的にParallel叩けるべきじゃないっすかねえ…。

次はこの辺考える。

  • フロー自体がFailした時どこに投げるのが使用側が楽か
  • マイクロサービス化する上で使用側のAPIはどうするべきなのか
  • 最終的にActivtyステートでGoのワーカーを待ち受けさせ、RDSにデータを入れる

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

DynamoDB使ったり調べたまとめ (2017Early)

書くこと

サ〜バ〜レスな構成にお安く気軽なデータストアが欲しかったのでちょっと調べた。
世間的にはDynamoDBを割と併用してるっぽいので。


1.小規模用途で安いデータストアが欲しいだけ

初期コスト以外の要素をあまり気にしない方向で考える場合。

基本

  • 最小は1読みキャパ/1書きキャパで約0.6$ / 月
  • でもアカウントごとに無料枠は25ユニット無料
  • この確保量が月額かかるので別に使用量従属課金というわけでは全然ない

読み込み

Get

  • まあKVSなので、基本はユニークなHashKeyのみでアクセスできるようにする
  • プライマリキーはHashKey(順不同),かHashKey + RangeKey(範囲指定、Sort可)から成る
  • ユーザーごとに独立してるデータとか。
  • indexは グローバルセカンダリインデックスと HashKeyに従属するローカルセカンダリインデックスがある。
  • 安く済ませたいならグローバルセカンダリインデックス(ユニークキーを含まないindex)は諦めろ

Scan、Query

  • 小規模ってわかってるなら理性捨ててScan(index全件取得)するのもあり
  • 読み込み時に強い読み込み整合性モードを使えば、直前の書き込み全て反映されたレコードとして取得できる、(ただしRCU消費2倍)
  • 範囲指定やソートができるのはRange Keyのみ。
  • 基本検索はそこまで強くないと思っておく

書き込み

  • 普通に update column += 1 とか使える (これと先の”強い整合性”があるだけでもS3にJSON突っ込んでデータストア代わりにするのと話は違ってそれなりに意味はある気がする。)
  • 条件付きアップデートも可能
  • レコード間でのトランザクションはもちろん保証されない
  • 特に複数テーブル書き込みに関してはオレオレトランザクション化要るので基本諦めろ

2.大規模用途での可用性や整合性の担保についてメモ

読み込み

Get

  • 基本はこっちでもユニークなキーのみでのget,シンプルなKVSとしての利用にすることを心がける
  • HASH KEY(の頭文字)は散らす。データサイズがでかくなるとHASH KEYをもとに自動でパーティションが切られるから。
  • グローバルセカンダリインデックス多用しそう。この場合インデックスごとにキャパシティユニット設定する。
  • データサイズは小さくしよう。

Scan , Queryについて

重要

  • Scanはセカンダリインデックス全件取得、QueryはRangeKeyやIndexの内容に従って走査する
  • 1度のレコード取得クエリに対してデータが1MB上限あるので、テーブルがでかくなると一度にScanできない可能性はある
  • LIMITやフィルタ機能はRCU消費を何ら抑えることはできない,あくまでSQLのWHERE文的にクエリ書きやすくなるだけっぽい
  • Queryはともかく特に大規模用途だとScanは使わないに越したことがない。

クエリキャッシュ

  • 同じテーブルが参照されまくるとRCU消費するのでElastiCacheとか前に置いてるケースを散見。結局そうなるんか。

書き込み

DynamoDB ベストプラクティス 参考にした
DynamoDBでのポイントまとめ

複数テーブル同時更新

  • テーブル分割時、複数テーブル書き込み必要な時は自前でACID整備することとなる。NoSQL…
  • 更新依頼書テーブル的な実装/タスクキュー的な実装の上でupdateという形をとる(辛そう)
  • スループット確保のためどうしてもテーブル分けたり非正規化しなければ場面も出てくるので、全く触れなくて済むというわけでもないかも????
  • DynamoDB Stream も使えるそう。

同じレコード(というよりテーブル)に対する大量のupdate

  • 同じレコードに同時に大量にincrement指定がくるような場合、そのパーティションのスループットが非常に下がる。
  • ここでも更新リクエストをタスクキュー的に実装しておいて、読み込み時にそれらをSUMするか、1つのレコードにまとめる実装をする必要がある。

感想

  • 単なるKVS以上の何かを求め始めた場合、結構留意することありそう。(うまい話はない)
  • トランザクションって素敵だね…
  • 他のNoSQLとしてAWS SimpleDBってのもあったけど死んでしまったの?
  • GCP周りのサービスも調べたい

Read More

serverless frameworkでのLambdaファンクション縮小/デプロイ速度向上

serverless framework (v1.4)使用。

include/exclude

普通にデプロイすると、node_modules以下が全てパッケージングされ、各Functionのサイズが大きくなってしまったり、デプロイに時間がかかったりする。
結論としては、パッケージングの設定をきちんとすること。
 
公式ドキュメント参照のこと。
 
この設定はもちろん各Functionごとに独立させることもできるみたい。
 
開発時にwebpackを使っている方なら、(作り方にもよるが)そもそも初めから各エンドポイントに必要なものだけrequireしてbundleする構成にはなってるかと思うので、
一括でnode_modules以下をexcludeしても構わないかもしれない。

serverless.yml

service: hogehoge

package:
  exclude:
    - node_modules/**

...

serverless-webpack排除

2016年末段階ではserverless-webpackプラグインを常用していたが、

serverless deploy function -f <functionName>

に対応していない?ため、全functionの一括デプロイしか手段がなかった。
普通に自前のwebpackでjsを吐いた方が個別にアップロードできるため何かと融通がきくかもしれない。

このプラグインは、

serverless webpack serve

でローカルのテストが簡単にできる利点があったが、serverless-offlineに移行すること。

Read More

Serverless FrameworkでCognito UserPoolの認証をしばくときのメモ

  • 社内向けのサーバーレスなサービスが想定で、API GatewayでセキュアなAPIを作ることを考える。
  • そこでCognito UserPoolを使った場合のメモ

Severless Framework

  auth:
    handler: auth.default
    timeout : 5
    memorySize : 256

  secureCheck:
    handler: handler.secureCheck # 動かすlambda関数名
    timeout : 5
    memorySize : 128
    events:
      - http:
          method: get
          path: secureCheck
          cors : true
          authorizer: auth #authorizer: xxx:xxx:Lambda-Name も可能
          response:
            headers:
              Content-Type: "'application/json'"
            template: $input.path('$')

/secureCheck と云うAPIは,authと云うLambda Functionの認証を経てsecureCheck Functionに至るようになる。
そうでなければ403が返る。

この形式でsecureCheck APIにアクセスするとき、リクエストのAuthorizationヘッダーに
Cognito UserPoolでログイン認証したアクセストークンをつけること。
クライアント側の処理は今回は書かない。

auth.ts

const aws = require("aws-sdk");

var cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({
    apiVersion: '2016-04-18',
    region: 'ap-northeast-1'
});

const auth = (event, context, callback)=> {

    const token = event.authorizationToken != null ? event.authorizationToken : event.headers.Authorization

    var params = {
        AccessToken: token
    };
    cognitoidentityserviceprovider.getUser(params, function (err, data) {
        if (err) {
            console.log(err)
            callback(null, generatePolicy('user', 'Deny', event.methodArn , token));
            //callback(null, generatePolicy('user', 'Allow', event.methodArn, data));
        } else {
            console.log(data)
            callback(null, generatePolicy('user', 'Allow', event.methodArn , token));
        }
        return;
    });
};

//"methodArn":"arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/<method>/<resourcePath>"
const generatePolicy = function generatePolicy(principalId, effect, resource , token) {
  return {
    principalId:principalId,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: "arn:aws:execute-api:ap-northeast-1:*:<API Gateway Id>/*” //CHECK!! =>  http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/permissions.html#api-gateway-calling-api-permissions
      }]
    }
  };
};

export default  auth;

generatePolicyメソッドで、ユーザーに対して必要なAWSの権限をつけている。
ここでは、該当のAPI GateWayの各エンドポイントのinvoke権がつくように組んだ。

他メモ

Cognito UserPoolのAccessTokenやIdTokenのexpireはぴったり1時間なので、
APIを叩くたびに Cognateアクセストークンのexpireチェック => 必要ならばCognitoのrefresh叩いてToken更新 => セキュアなAPI gateway叩く、というフローを経ることになる。
Dynamoにセッションとか入れとくよりは楽なのだろうけど割とラフに非同期チェーンが入ってめんどくさい。

毎回リクエストヘッダにアクセストークンつけるのも、Ajax以外でのエンドポイントのアクセスが制限されてしまうので、なんとかCookie使う方向で今後模索してみる。

Read More

AWS Step FunctionsでLambda Functionでのサムネイル画像作成をシンプルに

この記事はWanoグループ Advent Calendar 2016の23日目の記事です。
Wanoグループ内でのLambda推しエンジニアとして、前回のLambda@Edgeに引き続き、2016年Re:Inventで発表された、AWS Step Functionsについての書いてみたいと思います。

AWS Step Functionsで解決できること

なんと言っても以下の2点だと思います。

  • Lambda Functionを小さい単位で維持できる
  • Lambda Function間の連携にSQSやDynamoDB等を使用する必要がなくなる

Wanoでも複数のLambda Functionを活用していますが、1つのFunctionの中でいろいろなことをやりすぎたため、他のサービスにそのまま転用できなかったり、コードの見通しが悪くなったりと言ったことが出てきました。
1つ1つのFunctionを細かく分けて、SQS等を使用して連携されることも検討しましたが、リトライや分岐のためにFunctionを用意しないといけないといったことを考えると、腰が重くなっていました。
その問題点を解決してくれるのがAWS Step Functionsです。

Lambda Functionの事例で見かけるサムネイル画像作成をStep Functionsで

Lambda Functionsの事例として、サムネイル画像を作成するサンプルをよく見かけます。
この記事でもサムネイル画像作成を例にし、以下のサムネイル画像あるあるを解決する処理をLambda FunctionとStep Functionsで実現したいと思います。

サムネイル画像あるあるとは

  1. せっかくサムネイル画像で小さくしたのに、Google PageSpeedで調べたら、ロスレス圧縮したら46%小さくなるよと言われる
  2. 適切なContent-Typeヘッダーが付いておらずブラウザで想定外の挙動をする
  3. Cache-Controllヘッダーが付いておらずブラウザキャッシュが効かない

勝手にあるあると言っているだけで、私が良く忘れるだけという可能性が大きいです。。。が、今回はこれを解決することを前提として、進めたいと思います。

Lambda Functionを実装する

Lambda Functionを小さい単位で維持することが重要なので、以下の機能で分けてLambda Functionを実装したいと思います。

  1. ResizeImage: 画像をリサイズするFunction
  2. MinifyJpeg: JPEG画像をロスレス圧縮するFunction
  3. MinifyPng: PNG画像をロスレス圧縮するFunction
  4. SetMeta: メタデータを設定するFunction

実装できたら、これらのLambda Functionを予めデプロイしておきます。
ちなみに私は以前apexを使っていましたが、最近serverless frameworkに乗り換えを進めています。
その内この辺りも記事にできたらなと。

AWS Step Functionsを設定する

State Machineを作る

Step Functionsの設定画面でState Machineを作ります。
設定ファイルは以下です。

注意
2016年12月23日時点で、State Machineの修正はできません。State Machineの設定ファイルもバージョン管理対象としておくことを強くおすすめします(泣きを見ました。。。)
{
  "Comment": "An example of the Amazon States Language using a choice state.",
  "StartAt": "ResizeImage",
  "States": {
    "ResizeImage": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "Next": "JpegOrPng"
    },
    "JpegOrPng": {
      "Type" : "Choice",
      "Choices": [
        {
          "Variable": "$.object.fileType",
          "StringEquals": "jpeg",
          "Next": "MinifyJpeg"
        },
        {
          "Variable": "$.object.fileType",
          "StringEquals": "png",
          "Next": "MinifyPng"
        }
      ],
      "Default": "UnsupportedImage"
    },
    "MinifyJpeg": {
      "Type" : "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:MinifyJpeg",
      "Next": "SetMeta"
    },
    "MinifyPng": {
      "Type" : "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:MinifyPng",
      "Next": "SetMeta"
    },
    "UnsupportedImage": {
      "Type": "Fail",
      "Cause": "No Matches!"
    },
    "SetMeta": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:SetMeta",
      "End": true
    }
  }
}

上記ファイルでの設定結果として以下のState Machineができます。

State Machineを実行する

State Machineができたら、早速実行してみましょう。
Lambda Function同様コンソールからJSONを指定して実行できますので、そちらで実行します。

動きました!!
今回は、JPEG画像をイベントとして渡したので、MinifyJpegに遷移しています。
PNG画像を渡せばMinifyPngに遷移する。。。はず(もう23日が終わりそうなので、試す時間がなかったり。。。)

State Machine実行時の注意点

注意点としては、Lambda Functionの実行ではなく、State Machineの実行という点です。
今回StartAtに指定したResizeImageが起動するイベントを実行してもState Machineは実行されず、State Machineの起動が必要になります。
残念ながら、現時点ではState Machineをイベントドリブンにすることはできないようです。
実運用では監視したいイベントを割り当てたLambda Functionを用意して、そちらからState Machineを実行するような構成にする必要があります。

まとめ

いくつかの課題はあるものの、AWS Step Functionsを使うことで、Lambda Function間の連携がシンプルになることは間違いありません。
課題自体も恐らく時間の問題で改善されると思いますし、serverless frameworkといった周辺ツールも続々対応してくると思うので、今後も注目です!!

Read More