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

9f3a83db74bee75a64b5e6ed106a775c?s=128

Takuto Wada

June 02, 2017
Tweet

Transcript

  1. 5FTUBCMF-BNCEB 8PSLJOH&GGFDUJWFMZXJUI-FHBDZ-BNCEB ࿨ా୎ਓ !U@XBEB  +VO !"84%FW%BZ5PLZP

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

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

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

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

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

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

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

  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 ʹҠ ߦ͍ͨ͠
  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;
  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.
  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);
  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(
  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"); } ); };
  15. ͜ͷ-BNCEB͸UFTUBCMFͩΖ͏͔

  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ؔ਺ͷϩάΛ֬ೝ ॴཁ࣌ؒ෼
  17. ͦ͜Ͱ4".Ͱ͢Α ৄ͘͠͸٢ా͞ΜͷηογϣϯΛ IUUQTHJUIVCDPNBXTMBCTTFSWFSMFTTBQQMJDBUJPONPEFM

  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<MPHJE>ŠXBUDI w BXTTDQIPHFQOHTJNBHFTIPHFQOH w BXTTMTJNBHFTSFTJ[FE ॴཁ࣌ؒ෼
  19. ख࡞ۀσϓϩΠ໨ࢹςετ 4".ͰσϓϩΠ BXTDMJͰςετ σϓϩΠͳ͠Ͱɺ ϩʔΧϧͰςετ ͍ͨ͠ʂʂʂʂʂ ෼ ෼

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

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

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

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

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

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

  26. IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

  27. IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

  28. IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

  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 すら欺ける
  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ͷֶशςετΛॻ͍ͯΈΔ 学習テスト: ライブラリ等の 学習のみに特化したテスト
  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 をパス形式に変えておく (ハマりポイント)
  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 機能をモリモリ試す
  33. উͯͦ͏ͳؾ͕͖ͯͨ͠

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

  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 # " %
  36. ઀߹෦ʢ4FBNʣ l઀߹෦ʢ4FBNʣͱ͸ɺͦͷ৔ॴΛ௚઀ฤू ͠ͳͯ͘΋ɺϓϩάϥϜͷৼΔ෣͍Λม͑Δ ͜ͱͷͰ͖Δ৔ॴͰ͋Δz

  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
  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)); }); }); ͜ΕͰςετ͕ॻ͚Δ
  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)); }); }); ઀߹෦Λ׆༻ͯ͠ςετΛॻ͘
  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); }); }); });
  41. ख࡞ۀσϓϩΠ໨ࢹςετ 4".ͰσϓϩΠ BXTDMJͰςετ ෼ ෼ MPDBMTUBDLΛ࢖ͬͨ ϩʔΧϧͰͷςετ ඵ

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

  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 ݱঢ়֬ೝͷͨΊΧόϨοδଌఆ
  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 ); ྫ֎ܥ͕ςετ͞Εͯͳ͍͜ͱ͕Θ͔Δ
  45. l5FTU4J[FTzBU(PPHMF IUUQTUFTUJOHHPPHMFCMPHDPNUFTUTJ[FTIUNM

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

  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Ͱྫ֎ܥΛςετ͢Δ
  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 ̄
  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; }
  50. medium small large σϓϩΠΛߦ͍ ຊ෺ͷ"84Λ࢖͏ςετΛ MBSHFαΠζͱߟ͑Δ

  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 定義をブランチ毎に生成する
  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؀ڥʹର͢Δςετ
  53. MBSHF NFEJVN TNBMM σϓϩΠࠐΈͰ෼ʙ෼ NT NT

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

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

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

  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ʹґଘͨ͠ཁૉΛઙ͍֊૚ͰҾ͖ണ͕͢
  58. ଟஈࣜΤϥʔϓϧʔϑ IUUQPSHBDIFNIBUFOBCMPHDPNFOUSZ

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

  60. IUUQTNBSUJOGPXMFSDPNBSUJDMFTRBJOQSPEVDUJPOIUNM 2"JO1SPEVDUJPO

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

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

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

  64. $POTVNFS%SJWFO$POUSBDU5FTUJOH IUUQTBTTFUTUIPVHIUXPSLTDPNBTTFUTUFDIOPMPHZSBEBSNBZFOQEG

  65. 1BDU IUUQTHJUIVCDPNSFBMFTUBUFDPNBVQBDU

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

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

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

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

    ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠