Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testable Lambda: Working Effectively with Legacy Lambda

Testable Lambda: Working Effectively with Legacy Lambda

AWS Dev Day Tokyo 2017 Day4 Track1 15:20 - 16:00

#AWSSummit #testlambda

当日のセッション録画:
Testable Lambda|AWS Summit Tokyo 2017 - YouTube
https://www.youtube.com/watch?v=C0zNc4bdWhY

Takuto Wada
PRO

June 02, 2017
Tweet

More Decks by Takuto Wada

Other Decks in Programming

Transcript

  1. 5FTUBCMF-BNCEB
    8PSLJOH&GGFDUJWFMZXJUI-FHBDZ-BNCEB
    ࿨ా୎ਓ !U@XBEB

    +VO !"84%FW%BZ5PLZP

    View Slide

  2. #AWSSummit
    #testlambda
    ࡱӨ0,ʢͨͩ͠ɺγϟολʔԻΛ߇͑Ίʹʣ
    ࢿྉө૾ެ։͋Γ
    ࣮گ΋େ׻ܴͰ͢

    View Slide

  3. ࿨ా୎ਓ
    JEUXBEB
    !U@XBEB
    HJUIVCUXBEB

    View Slide

  4. 4QFDJBM5IBOLTUP1*95"
    ߨԋʹࡍͯ͠1*95"༷ʹ͝ڠྗ͍͖ͨͩ·ͨ͠

    View Slide

  5. ۀքͰͷཱͪҐஔಠΓา͖͢Δελϯυ
    ΑΖ͓͘͠ئ͍͠·͢

    View Slide

  6. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    αΠζͱϐϥϛουͱϧʔϓ
    ͔ͦ͜Βઌ΁

    View Slide

  7. l"84-BNCEBͷࣗಈςετʹؔ͢
    ΔϕετϓϥΫςΟε͸·ͩͳ͍z

    View Slide

  8. ࠓ೔ͷ͓୊-BNCEBͷެࣜνϡʔτϦΞϧ
    IUUQEPDTBXTBNB[PODPNKB@KQMBNCEBMBUFTUEHXJUITFYBNQMFIUNM

    View Slide

  9. // dependencies
    var async = require('async');
    var AWS = require('aws-sdk');
    var gm = require('gm')
    .subClass({ imageMagick: true }); // Enable ImageMagick integration.
    var util = require('util');
    // constants
    var MAX_WIDTH = 100;
    var MAX_HEIGHT = 100;
    // get reference to S3 client
    var s3 = new AWS.S3();
    exports.handler = function(event, context, callback) {
    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
    var srcBucket = event.Records[0].s3.bucket.name;
    // Object key may have spaces or unicode non-ASCII characters.
    var srcKey =
    decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
    var dstBucket = srcBucket + "resized";
    var dstKey = "resized-" + srcKey;
    // Sanity check: validate that source and destination are different buckets.
    if (srcBucket == dstBucket) {
    callback("Source and destination buckets are the same.");
    return;
    }
    // Infer the image type.
    var typeMatch = srcKey.match(/\.([^.]*)$/);
    if (!typeMatch) {
    callback("Could not determine the image type.");
    return;
    }
    var imageType = typeMatch[1];
    if (imageType != "jpg" && imageType != "png") {
    callback('Unsupported image type: ${imageType}');
    return;
    }
    // Download the image from S3, transform, and upload to a different S3 bucket.
    async.waterfall([
    function download(next) {
    // Download the image from S3 into a buffer.
    s3.getObject({
    Bucket: srcBucket,
    Key: srcKey
    },
    next);
    },
    function transform(response, next) {
    gm(response.Body).size(function(err, size) {
    // Infer the scaling factor to avoid stretching the image unnaturally.
    var scalingFactor = Math.min(
    MAX_WIDTH / size.width,
    MAX_HEIGHT / size.height
    );
    var width = scalingFactor * size.width;
    var height = scalingFactor * size.height;
    // Transform the image buffer in memory.
    this.resize(width, height)
    .toBuffer(imageType, function(err, buffer) {
    if (err) {
    next(err);
    } else {
    next(null, response.ContentType, buffer);
    }
    });
    });
    },
    function upload(contentType, data, next) {
    // Stream the transformed image to a different S3 bucket.
    s3.putObject({
    Bucket: dstBucket,
    Key: dstKey,
    Body: data,
    ContentType: contentType
    },
    next);
    }
    ], function (err) {
    if (err) {
    console.error(
    'Unable to resize ' + srcBucket + '/' + srcKey +
    ' and upload to ' + dstBucket + '/' + dstKey +
    ' due to an error: ' + err
    );
    } else {
    console.log(
    'Successfully resized ' + srcBucket + '/' + srcKey +
    ' and uploaded to ' + dstBucket + '/' + dstKey
    );
    }
    callback(null, "message");
    }
    );
    };
    w ߦͷίʔυɺͭͷؔ਺
    w ݹ͍ίʔσΟϯάελΠϧ &4 BTZOD

    w Ͳ͏΍Βσουίʔυ͕͋Δ
    w ؍ଌ͢Δҙຯ߹͍ͷগͳ͍໭Γ஋
    w ˠࣗಈςετΛॻ͍ͯৼΔ෣͍͕มΘͬͯͳ͍
    ͜ͱΛ͔֬Ίͳ͕Βɺ։ൃܧଓʹඋ͑ͯ৽͍͠
    ίʔσΟϯάελΠϧ &4 1SPNJTF
    ʹҠ
    ߦ͍ͨ͠

    View Slide

  10. // dependencies
    var async = require('async');
    var AWS = require('aws-sdk');
    var gm = require('gm')
    .subClass({ imageMagick: true }); // Enable ImageMagick integration.
    var util = require('util');
    // constants
    var MAX_WIDTH = 100;
    var MAX_HEIGHT = 100;
    // get reference to S3 client
    var s3 = new AWS.S3();
    exports.handler = function(event, context, callback) {
    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
    var srcBucket = event.Records[0].s3.bucket.name;
    // Object key may have spaces or unicode non-ASCII characters.
    var srcKey =
    decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
    var dstBucket = srcBucket + "resized";
    var dstKey = "resized-" + srcKey;

    View Slide

  11. var dstBucket = srcBucket + "resized";
    var dstKey = "resized-" + srcKey;
    // Sanity check: validate that source and destination are different buckets.
    if (srcBucket == dstBucket) {
    callback("Source and destination buckets are the same.");
    return;
    }
    // Infer the image type.
    var typeMatch = srcKey.match(/\.([^.]*)$/);
    if (!typeMatch) {
    callback("Could not determine the image type.");
    return;
    }
    var imageType = typeMatch[1];
    if (imageType != "jpg" && imageType != "png") {
    callback('Unsupported image type: ${imageType}');
    return;
    }
    // Download the image from S3, transform, and upload to a different S3 bucket.
    async.waterfall([
    function download(next) {
    // Download the image from S3 into a buffer.

    View Slide

  12. // Download the image from S3, transform, and upload to a different S3 bucket.
    async.waterfall([
    function download(next) {
    // Download the image from S3 into a buffer.
    s3.getObject({
    Bucket: srcBucket,
    Key: srcKey
    },
    next);
    },
    function transform(response, next) {
    gm(response.Body).size(function(err, size) {
    // Infer the scaling factor to avoid stretching the image unnaturally.
    var scalingFactor = Math.min(
    MAX_WIDTH / size.width,
    MAX_HEIGHT / size.height
    );
    var width = scalingFactor * size.width;
    var height = scalingFactor * size.height;
    // Transform the image buffer in memory.
    this.resize(width, height)
    .toBuffer(imageType, function(err, buffer) {
    if (err) {
    next(err);

    View Slide

  13. // Transform the image buffer in memory.
    this.resize(width, height)
    .toBuffer(imageType, function(err, buffer) {
    if (err) {
    next(err);
    } else {
    next(null, response.ContentType, buffer);
    }
    });
    });
    },
    function upload(contentType, data, next) {
    // Stream the transformed image to a different S3 bucket.
    s3.putObject({
    Bucket: dstBucket,
    Key: dstKey,
    Body: data,
    ContentType: contentType
    },
    next);
    }
    ], function (err) {
    if (err) {
    console.error(

    View Slide

  14. Bucket: dstBucket,
    Key: dstKey,
    Body: data,
    ContentType: contentType
    },
    next);
    }
    ], function (err) {
    if (err) {
    console.error(
    'Unable to resize ' + srcBucket + '/' + srcKey +
    ' and upload to ' + dstBucket + '/' + dstKey +
    ' due to an error: ' + err
    );
    } else {
    console.log(
    'Successfully resized ' + srcBucket + '/' + srcKey +
    ' and uploaded to ' + dstBucket + '/' + dstKey
    );
    }
    callback(null, "message");
    }
    );
    };

    View Slide

  15. ͜ͷ-BNCEB͸UFTUBCMFͩΖ͏͔

    View Slide

  16. ςετखॱॻWFS
    w 4όέοτ४උ
    w TPVSDFόέοτͱEFTUJOBUJPOόέοτΛ࡞੒
    w TPVSDFόέοτʹαϯϓϧΦϒδΣΫτΛΞοϓϩʔυ͓ͯ͘͠
    w -BNCEBؔ਺Λ࡞੒ͯ͠σϓϩΠ
    w σϓϩΠύοέʔδΛ࡞੒
    w OQNJOTUBMMBTZODHN
    w σΟϨΫτϦؙ͝ͱ;*1ѹॖ
    w ࣮ߦϩʔϧΛ࡞੒
    w *".ίϯιʔϧͰ<3PMF/BNF> <4FMFDU3PMF5ZQF> <"UUBDI1PMJDZ>Λઃఆ
    w ϩʔϧͷ"3/ΛϝϞ
    w -BNCEBؔ਺ΛσϓϩΠ
    w BXTMBNCEBDSFBUFGVODUJPO
    w ؔ਺ͷ"3/ΛϝϞ
    w ໨ࢹͰςετ
    w "NB[PO4αϯϓϧΠϕϯτσʔλΛϑΝΠϧʹอଘ
    w -BNCEB$-*JOWPLFίϚϯυΛ࣮ߦͯؔ͠਺Λݺͼग़͠
    w αϜωΠϧ͕λʔήοτόέοτʹ࡞੒͞Εͨ͜ͱΛ֬ೝ
    w "84-BNCEBίϯιʔϧͰ-BNCEBؔ਺ͷϩάΛ֬ೝ
    ॴཁ࣌ؒ෼

    View Slide

  17. ͦ͜Ͱ4".Ͱ͢Α
    ৄ͘͠͸٢ా͞ΜͷηογϣϯΛ
    IUUQTHJUIVCDPNBXTMBCTTFSWFSMFTTBQQMJDBUJPONPEFM

    View Slide

  18. AWSTemplateFormatVersion: '2010-09-09'
    Transform: AWS::Serverless-2016-10-31
    Description: Image resize example
    Resources:
    SourceBucket:
    Type: AWS::S3::Bucket
    Properties:
    BucketName: images
    DestinationBucket:
    Type: AWS::S3::Bucket
    Properties:
    BucketName: imagesresized
    ProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
    Handler: index.handler
    Runtime: nodejs6.10
    CodeUri: ./code
    Policies: AmazonS3FullAccess
    Events:
    ImageUpload:
    Type: S3
    Properties:
    Bucket: !Ref SourceBucket
    Events: s3:ObjectCreated:*
    ςετखॱॻWFS
    w 4".ఆٛϑΝΠϧ࡞੒
    w 4".σϓϩΠ༻4όέοτ४උ
    w -BNCEBؔ਺Λ࡞੒ͯ͠σϓϩΠ
    w BXTDMPVEGPSNBUJPOQBDLBHF
    w BXTDMPVEGPSNBUJPOEFQMPZ
    w BXTDMJͰςετ
    w BXTMPHTHFUBXTMBNCEBŠXBUDI
    w BXTTDQIPHFQOHTJNBHFTIPHFQOH
    w BXTTMTJNBHFTSFTJ[FE
    ॴཁ࣌ؒ෼

    View Slide

  19. ख࡞ۀσϓϩΠ໨ࢹςετ
    4".ͰσϓϩΠ
    BXTDMJͰςετ
    σϓϩΠͳ͠Ͱɺ
    ϩʔΧϧͰςετ
    ͍ͨ͠ʂʂʂʂʂ


    View Slide

  20. ςετࣗಈԽϐϥϛουͱΞϯνύλʔϯ
    IUUQTXBUJSNFMPOCMPHJOUSPEVDJOHUIFTPGUXBSFUFTUJOHJDFDSFBNDPOF

    View Slide

  21. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    αΠζͱϐϥϛουͱϧʔϓ
    ͔ͦ͜Βઌ΁

    View Slide

  22. ϨΨγʔίʔυͱͦͷδϨϯϚ
    lςετ͕ͳ͍ίʔυ͸ϨΨγʔίʔυͩz
    lίʔυΛมߋ͢ΔͨΊʹ͸ςε
    τΛ੔උ͢Δඞཁ͕͋Δɻଟ͘ͷ
    ৔߹ɺςετΛ੔උ͢ΔͨΊʹ͸ɺ
    ίʔυΛมߋ͢Δඞཁ͕͋Δz

    View Slide

  23. ϨΨγʔίʔυͱϢχοτςετ
    lϦϑΝΫλϦϯά͢ΔલʹϢχοτςε
    τΛॻ͘ͷ͸ɺͱ͖ʹ͸ෆՄೳͰ͋Γɺ
    ͠͹͠͹ແҙຯͰ͋Δz

    View Slide

  24. ઃܭͷՄಈҬΛ֬อ͢Δ
    wςετ͕ͳ͍ͷ͸طʹઃܭ͕ѱ͍ஹީ
    wઃܭ࣮૷Λม͑Δͷ͕લఏ
    w࣮૷ͷςετΛॻ͔ͳ͍͜ͱ
    wςετ͕Χόʔ͢Δൣғʹ༡ͼΛ࣋ͨͤɺΧόʔ
    ൣғ಺ΛϦϑΝΫλϦϯά
    wঢ়گʹԠͯ͡ૈཻ͍౓ͷςετΛ࢖͍͜ͳ͢
    IUUQTXXXTMJEFTIBSFOFUU@XBEBUFTUTUSBUFHZBOEUBDUJDT

    View Slide

  25. ࠓճ͸'BLF0CKFDU͕ΧΪ
    IUUQYVOJUQBUUFSOTDPN5FTU%PVCMFIUNM

    View Slide

  26. IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

    View Slide

  27. IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

    View Slide

  28. IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

    View Slide

  29. $ SERVICES=s3 make infra
    . .venv/bin/activate; PYTHONPATH=. exec localstack/mock/infra.py
    Starting local dev environment. CTRL-C to quit.
    Starting mock S3 (port 4572)...
    Ready.
    $ aws --endpoint-url=http://localhost:4572 s3 ls
    $ aws --endpoint-url=http://localhost:4572 s3 mb s3://my-test-bucket
    make_bucket: my-test-bucket
    $ aws --endpoint-url=http://localhost:4572 s3 ls
    2006-02-04 01:45:09 my-test-bucket
    $ aws --endpoint-url=http://localhost:4572 s3 cp ~/hoge.png s3://my-test-bucket
    upload: hoge.png to s3://my-test-bucket/hoge.png
    $ aws --endpoint-url=http://localhost:4572 s3 ls s3://my-test-bucket
    2017-06-02 11:39:22 7538 hoge.png
    MPDBMTUBDLΛNBLF΍EPDLFSͰಈ͔͢
    endpoint-url を指定すれば aws cli すら欺ける

    View Slide

  30. const assert = require('assert');
    const fs = require('fs');
    const path = require('path');
    const AWS = require('aws-sdk');
    const s3 = new AWS.S3({
    s3ForcePathStyle: true,
    logger: console,
    endpoint: new AWS.Endpoint('http://localhost:4572')
    });
    describe('localstack learning', () => {
    before(() => {
    return s3.createBucket({Bucket: 'test-bucket'}).promise().then(() => {
    return s3.putObject({
    Bucket: 'test-bucket', Key: 'TQ_LOGO.png', ContentType: 'image/png',
    Body: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'TQ_LOGO.png'))
    }).promise();
    });
    });
    it('s3.getObject', () => {
    return s3.getObject({Bucket: 'test-bucket', Key: 'TQ_LOGO.png'}).promise().then((res) => {
    assert(res);
    });
    });
    });
    MPDBMTUBDLͱ1SPNJTFͷֶशςετΛॻ͍ͯΈΔ
    学習テスト: ライブラリ等の
    学習のみに特化したテスト

    View Slide

  31. const assert = require('assert');
    const fs = require('fs');
    const path = require('path');
    const AWS = require('aws-sdk');
    const s3 = new AWS.S3({
    s3ForcePathStyle: true,
    logger: console,
    endpoint: new AWS.Endpoint('http://localhost:4572')
    });
    describe('localstack learning', () => {
    before(() => {
    return s3.createBucket({Bucket: 'test-bucket'}).promise().then(() => {
    return s3.putObject({
    Bucket: 'test-bucket', Key: 'TQ_LOGO.png', ContentType: 'image/png',
    Body: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'TQ_LOGO.png'))
    }).promise();
    });
    });
    it('s3.getObject', () => {
    return s3.getObject({Bucket: 'test-bucket', Key: 'TQ_LOGO.png'}).promise().then((res) => {
    assert(res);
    });
    });
    });
    endpoint を localstack に
    向けるだけ!
    S3 のバケット URL をパス形式に変えておく
    (ハマりポイント)

    View Slide

  32. const assert = require('assert');
    const fs = require('fs');
    const path = require('path');
    const AWS = require('aws-sdk');
    const s3 = new AWS.S3({
    s3ForcePathStyle: true,
    logger: console,
    endpoint: new AWS.Endpoint('http://localhost:4572')
    });
    describe('localstack learning', () => {
    before(() => {
    return s3.createBucket({Bucket: 'test-bucket'}).promise().then(() => {
    return s3.putObject({
    Bucket: 'test-bucket', Key: 'TQ_LOGO.png', ContentType: 'image/png',
    Body: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'TQ_LOGO.png'))
    }).promise();
    });
    });
    it('s3.getObject', () => {
    return s3.getObject({Bucket: 'test-bucket', Key: 'TQ_LOGO.png'}).promise().then((res) => {
    assert(res);
    });
    });
    });
    aws-sdk の Promise 機能をモリモリ試す

    View Slide

  33. উͯͦ͏ͳؾ͕͖ͯͨ͠

    View Slide

  34. ϨΨγʔίʔυͷδϨϯϚ
    lίʔυΛมߋ͢ΔͨΊʹ͸ςετΛ੔උ͢Δ
    ඞཁ͕͋Δɻଟ͘ͷ৔߹ɺςετΛ੔උ͢Δ
    ͨΊʹ͸ɺίʔυΛมߋ͢Δඞཁ͕͋Δz

    View Slide

  35. var MAX_HEIGHT = 100;
    // get reference to S3 client
    var s3;
    if (process.env.NODE_ENV === 'production') {
    s3 = new AWS.S3();
    } else {
    s3 = new AWS.S3({
    s3ForcePathStyle: true,
    logger: console,
    endpoint: new AWS.Endpoint('http://localhost:4572')
    });
    }
    exports.handler = function(event, context, callback) {
    // Read options from the event.
    Ξϯνύλʔϯ5FTU-PHJDJO1SPEVDUJPO
    #
    "
    %

    View Slide

  36. ઀߹෦ʢ4FBNʣ
    l઀߹෦ʢ4FBNʣͱ͸ɺͦͷ৔ॴΛ௚઀ฤू
    ͠ͳͯ͘΋ɺϓϩάϥϜͷৼΔ෣͍Λม͑Δ
    ͜ͱͷͰ͖Δ৔ॴͰ͋Δz

    View Slide

  37. var AWS = require('aws-sdk');
    var s3 = new AWS.S3();
    var onObjectCreated = require('./on-object-created');
    exports.handler = function(event, context, callback) {
    onObjectCreated({s3, event, callback});
    };
    ॲཧຊମΛผϞδϡʔϧʹग़͠ɺҾ਺Λ઀߹෦ʹ͢Δ
    (
    0
    0
    %
    // dependencies
    var async = require('async');
    var gm = require('gm')
    .subClass({ imageMagick: true }); // Enable ImageMagick integration.
    var util = require('util');
    // constants
    var MAX_WIDTH = 100;
    var MAX_HEIGHT = 100;
    module.exports = function({s3, event, callback}) {
    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
    var srcBucket = event.Records[0].s3.bucket.name;
    ৽نϑΝΠϧPOPCKFDUDSFBUFEKT
    JOEFYKT

    View Slide

  38. const assert = require('assert');
    const AWS = require('aws-sdk');
    const s3 = new AWS.S3({s3ForcePathStyle: true, endpoint: new AWS.Endpoint('http://localhost:4572')});
    const onObjectCreated = require('../on-object-created');
    describe('localstack based test suite for happy path cases', () => {
    let now, event;
    beforeEach(() => {
    now = new Date().getTime();
    event = {
    Records: [
    { s3: { bucket: { name: `test-bucket-${now}` }, object: { key: `TQ_LOGO_${now}.png` } } }
    ]
    };
    return s3.createBucket({Bucket: `test-bucket-${now}`}).promise()
    .then(() => s3.createBucket({Bucket: `test-bucket-${now}resized`}).promise())
    .then(() => s3.putObject({
    Bucket: `test-bucket-${now}`, Key: `TQ_LOGO_${now}.png`, ContentType: 'image/png',
    Body: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'TQ_LOGO.png'))
    }).promise());
    });
    it('onObjectCreated callback', () => {
    return new Promise((resolve, reject) => {
    const callback = (err, message) => err ? reject(err) : resolve(message);
    onObjectCreated({s3, event, callback});
    }).then((message) => {
    assert(/^message/.test(message));
    });
    }); ͜ΕͰςετ͕ॻ͚Δ

    View Slide

  39. const assert = require('assert');
    const AWS = require('aws-sdk');
    const s3 = new AWS.S3({s3ForcePathStyle: true, endpoint: new AWS.Endpoint('http://localhost:4572')});
    const onObjectCreated = require('../on-object-created');
    describe('localstack based test suite for happy path cases', () => {
    let now, event;
    beforeEach(() => {
    now = new Date().getTime();
    event = {
    Records: [
    { s3: { bucket: { name: `test-bucket-${now}` }, object: { key: `TQ_LOGO_${now}.png` } } }
    ]
    };
    return s3.createBucket({Bucket: `test-bucket-${now}`}).promise()
    .then(() => s3.createBucket({Bucket: `test-bucket-${now}resized`}).promise())
    .then(() => s3.putObject({
    Bucket: `test-bucket-${now}`, Key: `TQ_LOGO_${now}.png`, ContentType: 'image/png',
    Body: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'TQ_LOGO.png'))
    }).promise());
    });
    it('onObjectCreated callback', () => {
    return new Promise((resolve, reject) => {
    const callback = (err, message) => err ? reject(err) : resolve(message);
    onObjectCreated({s3, event, callback});
    }).then((message) => {
    assert(/^message/.test(message));
    });
    }); ઀߹෦Λ׆༻ͯ͠ςετΛॻ͘

    View Slide

  40. Body: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'TQ_LOGO.png'))
    }).promise());
    });
    it('onObjectCreated callback', () => {
    return new Promise((resolve, reject) => {
    const callback = (err, message) => err ? reject(err) : resolve(message);
    onObjectCreated({s3, event, callback});
    }).then((message) => {
    assert(/^message/.test(message));
    });
    });
    it('onObjectCreated creates and puts thumbnail into destination bucket', () => {
    return new Promise((resolve, reject) => {
    const callback = (err, message) => err ? reject(err) : resolve(message);
    onObjectCreated({s3, event, callback});
    })
    .then(() => {
    return s3.waitFor('objectExists', {
    Bucket: `test-bucket-${now}resized`,
    Key: `resized-TQ_LOGO_${now}.png`
    }).promise();
    })
    .then((data) => {
    assert(data);
    });
    });
    });

    View Slide

  41. ख࡞ۀσϓϩΠ໨ࢹςετ
    4".ͰσϓϩΠ
    BXTDMJͰςετ


    MPDBMTUBDLΛ࢖ͬͨ
    ϩʔΧϧͰͷςετ

    View Slide

  42. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    αΠζͱϐϥϛουͱϧʔϓ
    ͔ͦ͜Βઌ΁

    View Slide

  43. module.exports = function({s3, event, callback}) {
    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
    var srcBucket = event.Records[0].s3.bucket.name;
    // Object key may have spaces or unicode non-ASCII characters.
    var srcKey =
    decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));
    var dstBucket = srcBucket + "resized";
    var dstKey = "resized-" + srcKey;
    // Sanity check: validate that source and destination are different buckets.
    if (srcBucket == dstBucket) {
    callback("Source and destination buckets are the same.");
    return;
    }
    // Infer the image type.
    var typeMatch = srcKey.match(/\.([^.]*)$/);
    if (!typeMatch) {
    callback("Could not determine the image type.");
    return;
    }
    var imageType = typeMatch[1];
    if (imageType != "jpg" && imageType != "png") {
    callback('Unsupported image type: ${imageType}');
    return;
    }
    IUUQTHJUIVCDPNUXBEBDPWFSMBZFM
    ݱঢ়֬ೝͷͨΊΧόϨοδଌఆ

    View Slide

  44. IUUQTHJUIVCDPNUXBEBDPWFSMBZFM
    // Transform the image buffer in memory.
    this.resize(width, height)
    .toBuffer(imageType, function(err, buffer) {
    if (err) {
    next(err);
    } else {
    next(null, response.ContentType, buffer);
    }
    });
    });
    },
    function upload(contentType, data, next) {
    // Stream the transformed image to a different S3 bucket.
    s3.putObject({
    Bucket: dstBucket,
    Key: dstKey,
    Body: data,
    ContentType: contentType
    },
    next);
    }
    ], function (err) {
    if (err) {
    console.error(
    'Unable to resize ' + srcBucket + '/' + srcKey +
    ' and upload to ' + dstBucket + '/' + dstKey +
    ' due to an error: ' + err
    );
    ྫ֎ܥ͕ςετ͞Εͯͳ͍͜ͱ͕Θ͔Δ

    View Slide

  45. l5FTU4J[FTzBU(PPHMF
    IUUQTUFTUJOHHPPHMFCMPHDPNUFTUTJ[FTIUNM

    View Slide

  46. medium
    small
    ઌఔॻ͍ͨ
    MPDBMTUBDLΛ׆༻ͨ͠ςετΛ
    NFEJVNαΠζͱఆٛ͢Δ
    MPDBMTUBDL͢Βඞཁͳ͍
    ྫ֎ܥͷςετΛ
    TNBMMαΠζͱߟ͑Δ

    View Slide

  47. const assert = require('assert');
    const onObjectCreated = require('../on-object-created');
    describe('small sized test suite for exceptional cases:', () => {
    it('file without extension', () => {
    const event = {
    Records: [
    { s3: { bucket: { name: `test-bucket` }, object: { key: `TQ_LOGO` } } }
    ]
    };
    onObjectCreated({event, callback: (err, message) => {
    assert(err === 'Could not determine the image type.');
    }});
    });
    it('extension is not `.png` nor `.jpg`', () => {
    const event = {
    Records: [
    { s3: { bucket: { name: `test-bucket` }, object: { key: `TQ_LOGO.txt` } } }
    ]
    };
    onObjectCreated({event, callback: (err, message) => {
    assert(err === 'Unsupported image type: txt');
    }});
    });
    }); MPDBMTUBDL͢Β࢖Θͳ͍TNBMMUFTUͰྫ֎ܥΛςετ͢Δ

    View Slide

  48. IUUQTHJUIVCDPNQPXFSBTTFSUKTQPXFSBTTFSU
    > mocha --require intelli-espower-loader 'test/small/**/*.js'
    small sized test suite for exceptional cases:
    ✓ file without extension
    1) extension is not `.png` nor `.jpg`
    1 passing (47ms)
    1 failing
    1) small sized test suite for exceptional cases: extension is not `.png` nor `.jpg`:
    AssertionError: # test/small/exceptional_cases_test.js:25
    assert(err === 'Unsupported image type: txt')
    | |
    | false
    "Unsupported image type: ${imageType}"
    --- [string] 'Unsupported image type: txt'
    +++ [string] err
    @@ -21,7 +21,16 @@
    pe:
    -txt
    +${imageType}
    _人人人人人人人人人人_
    > 突然のテスト失敗 <
     ̄YYYYYYYYYY ̄

    View Slide

  49. IUUQTHJUIVCDPNQPXFSBTTFSUKTQPXFSBTTFSU
    > mocha --require intelli-espower-loader 'test/small/**/*.js'
    small sized test suite for exceptional cases:
    ✓ file without extension
    1) extension is not `.png` nor `.jpg`
    1 passing (47ms)
    1 failing
    1) small sized test suite for exceptional cases: extension is not `.png` nor `.jpg`:
    AssertionError: # test/small/exceptional_cases_test.js:25
    assert(err === 'Unsupported image type: txt')
    | |
    | false
    "Unsupported image type: ${imageType}"
    --- [string] 'Unsupported image type: txt'
    +++ [string] err
    @@ -21,7 +21,16 @@
    pe:
    -txt
    +${imageType} ڭ܇ςετͯ͠ͳ͍ίʔυ͸ಈ͔ͳ͍
    if (imageType != "jpg" && imageType != "png") {
    callback('Unsupported image type: ${imageType}');
    return;
    }

    View Slide

  50. medium
    small
    large
    σϓϩΠΛߦ͍
    ຊ෺ͷ"84Λ࢖͏ςετΛ
    MBSHFαΠζͱߟ͑Δ

    View Slide

  51. AWSTemplateFormatVersion: '2010-09-09'
    Transform: AWS::Serverless-2016-10-31
    Description: Image resize example
    Resources:
    SourceBucket:
    Type: AWS::S3::Bucket
    Properties:
    BucketName: test-{{branchName}}
    DestinationBucket:
    Type: AWS::S3::Bucket
    Properties:
    BucketName: test-{{branchName}}resized
    ProcessorFunction:
    Type: AWS::Serverless::Function
    Properties:
    Handler: index.handler
    Runtime: nodejs6.10
    CodeUri: ./code
    Policies: AmazonS3FullAccess
    Events:
    ImageUpload:
    Type: S3
    Properties:
    Bucket: !Ref SourceBucket
    Events: s3:ObjectCreated:*
    ϒϥϯνຖʹҟͳΔ؀ڥΛ࡞Δ
    手に馴染んだテンプレートエンジンを使って
    SAM 定義をブランチ毎に生成する

    View Slide

  52. const assert = require('assert');
    const fs = require('fs');
    const path = require('path');
    const AWS = require('aws-sdk');
    const s3 = new AWS.S3({
    logger: console
    });
    const execSync = require('child_process').execSync;
    const branch = execSync('git rev-parse --abbrev-ref @').toString().replace(/\n$/, '');
    describe('large sized test suite', () => {
    it('happy path', () => {
    const now = new Date().getTime();
    return Promise.resolve()
    .then(() => {
    return s3.putObject({
    Bucket: `test-${branch}`,
    Key: `TQ_LOGO_${now}.png`,
    Body: fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'TQ_LOGO.png')),
    ContentType: 'image/png'
    }).promise();
    })
    .then(() => {
    return s3.waitFor('objectExists', {
    Bucket: `test-${branch}resized`,
    Key: `resized-TQ_LOGO_${now}.png`
    }).promise();
    })
    .then((data) => {
    assert(data);
    });
    });
    });
    MBSHFϒϥϯνผͷ"84؀ڥʹର͢Δςετ

    View Slide

  53. MBSHF
    NFEJVN
    TNBMM
    σϓϩΠࠐΈͰ෼ʙ෼
    NT
    NT

    View Slide

  54. Agenda
    ݱঢ়֬ೝ
    ςελϏϦςΟΛ͚͋͜͡Δ
    αΠζͱϐϥϛουͱϧʔϓ
    ͔ͦ͜Βઌ΁

    View Slide

  55. IUUQTUIMJHIUDPNCMPHVODMFCPCUIFDMFBOBSDIJUFDUVSFIUNM
    ςελϏϦςΟ͕୲อ͞ΕͨͷͰɺ಺෦͸վળ͠์୊

    View Slide

  56. IUUQRJJUBDPN,PLVEPSJJUFNTBDGBC
    όάͱྫ֎Λ੾Γ෼͚Δ -BNCEB͸ϋϯυϦϯά͕ಛघ

    View Slide

  57. const gm = require('gm').subClass({ imageMagick: true });
    const util = require('util');
    const resize = require('./resize');
    module.exports = function onObjectCreated ({s3, event, callback}) {
    console.log('Reading options from event:\n', util.inspect(event, {depth: 5}));
    const srcBucket = event.Records[0].s3.bucket.name;
    const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    const dstBucket = srcBucket + 'resized';
    const dstKey = 'resized-' + srcKey;
    resize({s3, gm, srcBucket, srcKey, dstBucket, dstKey})
    .then((message) => {
    console.log(message);
    callback(null, message);
    })
    .catch((err) => {
    const message = `Unable to resize ${srcBucket}/${srcKey} and upload to ${dstBucket}/
    ${dstKey} due to an error: ${err}`;
    console.error(message);
    callback(null, message);
    });
    };
    "84-BNCEBʹґଘͨ͠ཁૉΛઙ͍֊૚ͰҾ͖ണ͕͢

    View Slide

  58. ଟஈࣜΤϥʔϓϧʔϑ
    IUUQPSHBDIFNIBUFOBCMPHDPNFOUSZ

    View Slide

  59. l؂ࢹͱ͸ܧଓతͳςετͰ͋Δz
    IUUQEFWFMPQFSDZCP[VDPKQBSDIJWFTLB[VIPDSPOMPHGIUNM
    CZ,B[VIP0LV

    View Slide

  60. IUUQTNBSUJOGPXMFSDPNBSUJDMFTRBJOQSPEVDUJPOIUNM
    2"JO1SPEVDUJPO

    View Slide

  61. IUUQUXBEBIBUFOBCMPHKQFOUSZEFCVHHJOHUFTUT
    ຊ൪؀ڥͷෆ۩߹Λςετʹࣸ͠औΔ

    View Slide

  62. medium
    small
    large
    XL
    ຊ෺ͷαʔϏεؒ࿈ܞΛ
    Ͳ͏ςετ͍͔ͯ͘͠

    View Slide

  63. NJDSPTFSWJDFؒͷςετ&&9-
    IUUQNBSUJOGPXMFSDPNBSUJDMFTNJDSPTFSWJDFUFTUJOH

    View Slide

  64. $POTVNFS%SJWFO$POUSBDU5FTUJOH
    IUUQTBTTFUTUIPVHIUXPSLTDPNBTTFUTUFDIOPMPHZSBEBSNBZFOQEG

    View Slide

  65. 1BDU
    IUUQTHJUIVCDPNSFBMFTUBUFDPNBVQBDU

    View Slide

  66. $POTVNFSଆΛNPDLTUVCײ֮Ͱॻ͘
    IUUQEJVTDPNBVTJNQMJGZJOHNJDSPTFSWJDFUFTUJOHXJUIQBDUT

    View Slide

  67. ͦͷ૝ఆΛ1SPWJEFSଆͰWFSJGZ
    IUUQEJVTDPNBVTJNQMJGZJOHNJDSPTFSWJDFUFTUJOHXJUIQBDUT

    View Slide

  68. 1BDUΛ࢖ͬͨ$%$5FTUJOHͷશମ૾
    IUUQUFDICMPHOFXTXFBWFSDPNXIZTIPVMEZPVVTFDPOTVNFSESJWFODPOUSBDUTGPSNJDSPTFSWJDFTJOUFHSBUJPOUFTUT

    View Slide

  69. ·ͱΊ
    w ϕετϓϥΫςΟε͸·ͩͳ͍
    w ςελϏϦςΟͷΧΪ͸ϩʔΧϧ࣮ߦ
    w ςεταΠζΛఆٛ͠ɺ൒ܘͷҟͳΔ
    ϑΟʔυόοΫαΠΫϧΛܗ੒͢Δ
    w ӡ༻؂ࢹ΋ςετͱߟ͑Δ
    ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠

    View Slide