ブラウザから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

AWS CodeBuildで分課金のコンテナ実行環境を手に入れる

伏見です。この記事はWanoグループアドベントカレンダー2016の21日目の記事になります。年末感。

書くこと

  • NightmareでのWebサイトE2Eテスト実行環境を、Dockerで構築・CodeBuildで実行してみた
  • 実行時に環境変数も渡せるし割とCodeBuildには使いどころありそう
  • CodeBuild Tokyoリージョン はよ

AWS CodeBuild発表、着想

先日のAWSのイベントre:invent 2016 にて『CodeBuild』 の発表が行われた。
モバイル/Webアプリケーションのビルド/テストのCIのパイプライン上に載せることが想定されているようだ。
現在(2016/12/20) Tokyoリージョンはなく、バージニア、オレゴン、アイルランドでのサービス提供になる。

気になったポイントは2つ。

  • 自分で用意したDockerの実行環境が使える。
  • (ビルド)タスクの実行時間に応じた「分単位での課金体系」である。

自作Dockerイメージが使える

AWS Lambdaのようにコードをサクッと動かしたい時に自作dockerコンテナが使えないかなあ、というのはたまに思っていた。
CodeBuildで実行環境自作のDockerイメージが使えることにより、インスタント環境として AWS Lambda より便利そうな点がある。

  • 使用言語の制限がなくなる。
  • あらかじめカスタマイズ済のイメージを構築しておける。

0.005$~ /分の課金

AWS CodeBuild 料金
CodeBuildはビルド実行時間(分単位 , 1分あたり 0.005$)での課金と転送量での課金になる。
インスタンスのプランをあげると0.02$/分まで上がる。

課金体系がこの通りなら、用途によってはいろいろ捗りそう。

リリース予定の AWS Batch も優秀そうだし、実行環境に信頼度がおけそうな感じがあるが、
あっちは(多分)普通にEC2借りてて1時間単位での課金だし、スポットインスタンス狙っていく作業がちょっと大げさな時もあるかと思う。

何ができそうか考える

  • 普通にAndroidアプリとかのビルド/ DeviceFarmにテスト投げる
  • 最悪止まっても問題なさそうなE2Eテストやスクレイプ作業
  • apiなどの監視タスク

いわゆるCIの用途以外でも考えようとしたけど…。
まあ他にも即時性なくてもいいけどDockerでサクッと動かしたいタスクで何かしら使いどころはあるんじゃないだろうか。

動かしてみるもの

ちょうどWebサイト周りのE2Eテストについて勉強中なので、その実行環境にCodeBuildを是非使ってみたい。
ブラウザE2Eテスト実行環境を整えたDockerイメージを用意し、AWSコンソール上でCodeBuildを実行/テストを走らせてみるとこまでやってみる。

E2Eテストを仕込んだDockerイメージの用意

Nightmare基盤

テストフレームワークNightmareをdockerのコンテナ上で動かす上で,レンダリングのためにxvfbなどの仮想フレームをインストールしたり、その他必要なモジュールをとってこなければならない。
この辺のインストール作業は冗長で、そもそもの興味範囲とは違うところであれがない、これがない、とハマりがち。
その辺をあらかじめ織り込んだDockerfileを公開してくれている人はガンガンいるので、それを参考にイメージを構築することにする。

いざとなったらDockerfileにはlinux上でのモジュールのインストール手順がそのまま記載されているので,別にdocker使わずとも学ぶことが多い。

nightmareのDockerfileを参考に、以下のようなDockerfileを構築する。

FROM node:7.2
MAINTAINER mr.hoge <xxxx.co.jp>

# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN groupadd --system nightmare && useradd --system --create-home --gid nightmare nightmare
ENV HOME "/home/nightmare"

ENV DEBUG=nightmare
ENV ARGUMENTS=()

RUN apt-get update && apt-get install -y \
  vim  gcc make cmake g++ \
  software-properties-common vim wget curl unzip zip \
  xvfb \
  x11-xkb-utils \
  xfonts-100dpi \
  xfonts-75dpi \
  xfonts-scalable \
  xfonts-cyrillic \
  x11-apps \
  clang \
  libdbus-1-dev \
  libgtk2.0-dev \
  libnotify-dev \
  libgnome-keyring-dev \
  libgconf2-dev \
  libasound2-dev \
  libcap-dev \
  libcups2-dev \
  libxtst-dev \
  libxss1 \
  libnss3-dev \
  gcc-multilib \
  g++-multilib && \
    rm -rf /var/lib/apt/lists/* && \
        find /usr/share/doc -depth -type f ! -name copyright | xargs rm || true && \
        find /usr/share/doc -empty | xargs rmdir || true && \
        rm -rf /usr/share/man/* /usr/share/groff/* /usr/share/info/* && \
        rm -rf /usr/share/lintian/* /usr/share/linda/* /var/cache/man/*
RUN apt-get clean

WORKDIR ${HOME}
COPY ./package.json ./
RUN npm install

VOLUME ${HOME}

オリジナルと違うところは、イメージのベースをNodeJS7系にしているところや、とりあえずvimやwgetも入れているところくらい。

E2Eテストに使うモジュールの準備

package.json

{
  "name": "docker-my-nightmare",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "./node_modules/.bin/mocha .test.js --timeout 6000000",
    "e2e_with_debug" : "DEBUG=nightmare:*,electron:* xvfb-run --server-args='-screen 0 1024x768x24' ./node_modules/.bin/mocha --harmony ./test.js  --timeout 600000",
    "e2e_on_docker" : "xvfb-run --server-args='-screen 0 1024x768x24' ./node_modules/.bin/mocha --harmony ./test.js  --timeout 600000"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "mocha": "^3.2.0",
    "mocha-co": "^1.17.2",
    "nightmare": "^2.8.1",
    "power-assert": "^1.4.2"
  }
}

ついでにテスト発火用コマンドをpackage.jsonに記載しておく。

テストスクリプトの用意

ブラウザテストそのものや、Nightmare周りについては今回は触れない。
簡単なjavascriptのコードだけ載せる。

test.js

const mocha = require('mocha-co');
const Nightmare = require('nightmare');
const assert = require('power-assert');

const sleep = (ms)=>{
  return new Promise((resolve , err)=>{
    setTimeout(()=>{
      resolve();
    },ms);
  });
};

describe("1st Test", function() {  

  let nightmare;

  before(function*() {
    console.log("Start Testing... #####################");
    nightmare = Nightmare();
  });

  it('googleでhogeを検索', function*() {
    const title = yield nightmare
      .viewport(1024,768)
      .goto('https://www.google.co.jp/?gfe_rd=cr&ei=qTBaWIX5OYqL8QeryomIDg#q=hoge&btnK=Google+%E6%A4%9C%E7%B4%A2')
      .title();

    assert(title === 'Google');
  });
});

ローカルのdocker上でちゃんと動くようであれば、おもむろにコンテナをcommitして、オールインワンなイメージを作っておく。
1.3GBほどになってしまった。

AWS ECRへの dockerリポジトリの登録

DockerHubのAWSバージョンと言えるAmazon EC2 Container Registry にDockerイメージを置くことにする。
ECRは、dockerのプライベートリポジトリを置く上で、割と安いサービス。

CodeBuildをOregon(us-nor)で立てているため、リポジトリもOregonに置く。
Tokyoリージョンに置いたリポジトリがCodeBuild上でうまく指定できなかったためだ。
この辺は後でIAM設定見直すが、できればCodeBuildが早くTokyoリージョンに来てほしい。

先ほどのビルド済イメージをECRにpushしておく。

docker push xxxxxxx.us-west-2.amazonaws.com/yyyyy:latest

ビルドソース準備

CodeBuildはビルドソースとしてS3/GitHub/CodeCommit(AWSのGitホスティング)が使える。
今回はアプリケーションのビルドをするわけじゃないのでソース設定は正直なんでもいいのだが、勉強のためにS3にする。
CodeBuildの設定ファイルであるbuildspec.ymlをS3に置き、そこに書かれているコマンドをdockerで実行する、という構成にしてみる。

buildspec.yml

version: 0.1
phases:
  install:
    commands:
  pre_build:
    commands:
      - echo $MY_VAR
  build:
    commands:
      - cd /home/nightmare && npm run e2e_on_docker
  post_build:
    commands:
      - echo Done!

中でディレクトリ移動しているのは、Dockerファイルに記載されているWORK_DIRがCodeBuildでの実行時に適用されずにnpmの実行エラーを起こしたため。

S3へのビルド対象のアップロード

先ほどのbuildspec.ymlをzipとしてS3にアップロードする。

zip buildspec.yml > buildspec.zip
aws s3 cp buildspec.zip s3://~~~~oregon-bucket/codebuild/

ここで注意点があって、S3のbucketのオリジンもCodeBuildと同じリージョンになければならない。
(IAMの設定割と見直したんだけど、CodeBuildでのビルド実行時にエラーが出てうまくいかなかった。)

CodeBuildのプロジェクトを作る

今回はConsole上で設定する。
regeonはオレゴン(us-west-2)を選択。

実行環境指定

Source: What to build

  • Source provider
    • Oregonに立てたbucketを指定

Environment: How to build

  • Environment image
    • Specify a Docker image
  • Custom image type
    • AmazonECR
  • Amazon ECR repository
    • ECRのリポジトリを指定
  • Amazon ECR image
    • イメージのタグ指定。履歴を残さなければlatestだけでいいはず
  • Build specification
    • ここに直接コマンドも書けるが、buildspec.ymlの設定を使うことにする。

Artifacts: Where to put the artifacts from this build project

  • Artifacts type
    • 本来ならここでビルド成果の置き場所を指定するが、No artifactsにする。
    • 今回のようなテスト用途なら、E2Eテストの結果やサイトのキャプチャをまとめてzipにするといいかもしれない。

Service role

  • ECR,S3に接続するための実行権限をここで付与する。

role周りについての設定は
Amazon ECR Sample for AWS CodeBuild
を参照。

continue -> OK を押してプロジェクト完成!

ビルド設定

“Start Build”ボタンでビルド設定画面に入る。
S3をビルドソースにしている場合、Source versionとしてs3 objectのバージョニング機能が使えるが、これで運用するのもあんまりうまくない気がする。

Environment variablesでコンテナ実行時に環境変数が渡せるため、これを積極的に使っていくことになりそう。(もちろんこの機能はaws-cliのcodebuildからも使える。)
name,valueは実際には

export MY_VAR=hogehoge

されているようなので、普通に$MY_VARとかで受け取れる。
コンテナの条件は変えずに、「動的にスクリプトやコマンドをダウンロードしてきてテストを実行する」基盤を整えるとなかなか便利になりそう。

実行

待つ。待つ。

上が実行ログの例。
テスト実行中のログはコンソール上で見れる他,CloudWatchのログとしても残る。

試算

1.3GBほどのDockerイメージで、実行開始までに1分半ほどかかった。
E2Eのjavascriptコード自体は短いので20秒ほど。
2分 * 0.005$ で 1回あたりの実行料金1円くらいになる。
これ自体はまあ大したことない。
一方のECRの転送料金がいまいち読めないが、こっちは1.3GB転送しているのでこれは金額的に大きそう。
EC2でECRを使うとき、同じリージョンの場合に EC2 <=> ECRの転送自体は無料なので、 CodeBuildでもこのプランが適用されているといいなーというか、してないときついと思う。

まとめ

CodeBuildはいろんな用途に使えそうですが、東京リージョン早くほしいです。
ついでにbitbucket対応もしてほしい。
(最後雑)

Read More

Lambda@Edgeを使って色々と試してみようとした

この記事はWanoグループ Advent Calendar 2016の18日目の記事です。

はじめに

Wanoでは、webサーバーの前にCloudFrontを配置し、全てのリクエストを一旦CloudFrontで受けて極力キャッシュを返す形を取ることが良くあります。
この構成のメリットは、サーバーサイドでキャッシュの仕組みを構築しなくても、CloudFrontを前に配置するだけで、EC2にリクエストを流すことなくパフォーマンスを改善できることにあります。

ただ、この構成には弱点も当然あり、ログイン機能が無いサービスの運用には最適なのですが、ログイン機能があるサービスの場合、ログイン状態によってキャッシュを返すかどうかの判定ができないため、ユーザーの9割がログインせずに使用するサービスでも、この構成が一切使えないといった弱点があります。

そんな中、先日のRe:Inventで発表されたのが、このLambda@Edgeです。
本当はLambda@Edgeを実際に使ってみていろいろ試してみようと思っていたんですが、プレビューへの申込みが遅かったためかまだ実際に動かすことができなかったので、ひとまず現時点で試したこと、わかったことを書いておきたいと思います。

Lambda@Edgeの設定

実際にCloudFrontに設定して動かすことはできなかったのですが、辛うじてLambdaのテスト機能で動かすことはできたので、blueprintのコードをベースに、少しだけ試してみました。
まずは設定画面です。

Lambda@Edgeは、CloudFrontのDistributionに対して設定するので、Distribution IDを指定する画面になっています。
その他にも、BehaviorとEventが指定できました。Behaviorが指定できるということは、LambdaFunctionを動かすURLと動かさないURLを分けることができるということなので、静的コンテンツ用にはBehaviorを分けて、LambdaFunctionを実行させずに、パフォーマンスとコスト面への考慮ができそうです。

Eventですが、以下の4つを指定できました。

今回は、この4つのEventからViewer Requestを指定してLambdaのblueprintの中から、cloudfront-ab-testを選択して設定します。

Viewer Request

ブラウザ等のクライアントからCloudFrontがリクエストを受け付けた時に発火するイベントです。
個々のリクエストに対して何か処理を追加する場面で使用するイベントになるため、4種類のイベントの中で最も使う頻度が多いのではないでしょうか。

cloudfront-ab-testを読んでみて

以下のコードが使い方を端的に表しているんじゃないでしょうか。
まだ動かせていないので、実際の挙動はわからないのですが、CookieやHeaderを見て条件に応じてURLをRewriteする。というパターンは使い勝手がありそうな気がします。

let modifiedUri = false;
if (headers.Cookie) {
    for (let i = 0; i < headers.Cookie.length; i++) {
        const experimentIndex = headers.Cookie[i].indexOf(experimentCookieName);
        console.error(experimentIndex);
        if (experimentIndex >= 0) {
            if (headers.Cookie[i][experimentIndex + experimentCookieName.length] === groupA) {
                request.uri = groupAObject;
                modifiedUri = true;
            } else if (headers.Cookie[i][experimentIndex + experimentCookieName.length] === groupB) {
                request.uri = groupBObject;
                modifiedUri = true;
            }
        }
    }
}

書いていて気づいたんですが、Wanoグループ Advent Calendar 2016の前日の記事nginx で特定のIPのみ別の upstream を使うについてもLambda@Edgeで同じようなことができそうですね。

少し前に発表されて、既にTokyoリージョンでも使用できるようになっているALBと組み合わせると、リクエストの捌き方をこれまで以上に柔軟に構成できそうです。

元々やりたかった、ログイン状態に応じてキャッシュを返すかどうかを切り替える場合も、少なくともRewriteすれば対応できそうなので、弊社でも使えるようになったら実際の挙動とパフォーマンスをしっかりチェックしたいと思います。

Read More