Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

࿨ా୎ਓ JEUXBEB !U@XBEB HJUIVCUXBEB

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

// 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 ʹҠ ߦ͍ͨ͠

Slide 10

Slide 10 text

// 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;

Slide 11

Slide 11 text

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.

Slide 12

Slide 12 text

// 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);

Slide 13

Slide 13 text

// 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(

Slide 14

Slide 14 text

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"); } ); };

Slide 15

Slide 15 text

͜ͷ-BNCEB͸UFTUBCMFͩΖ͏͔

Slide 16

Slide 16 text

ςετखॱॻ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ؔ਺ͷϩάΛ֬ೝ ॴཁ࣌ؒ෼

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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 ॴཁ࣌ؒ෼

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

ࠓճ͸'BLF0CKFDU͕ΧΪ IUUQYVOJUQBUUFSOTDPN5FTU%PVCMFIUNM

Slide 26

Slide 26 text

IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

Slide 27

Slide 27 text

IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

Slide 28

Slide 28 text

IUUQTHJUIVCDPNBUMBTTJBOMPDBMTUBDL

Slide 29

Slide 29 text

$ 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 すら欺ける

Slide 30

Slide 30 text

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ͷֶशςετΛॻ͍ͯΈΔ 学習テスト: ライブラリ等の 学習のみに特化したテスト

Slide 31

Slide 31 text

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 をパス形式に変えておく (ハマりポイント)

Slide 32

Slide 32 text

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 機能をモリモリ試す

Slide 33

Slide 33 text

উͯͦ͏ͳؾ͕͖ͯͨ͠

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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 # " %

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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)); }); }); ͜ΕͰςετ͕ॻ͚Δ

Slide 39

Slide 39 text

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)); }); }); ઀߹෦Λ׆༻ͯ͠ςετΛॻ͘

Slide 40

Slide 40 text

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); }); }); });

Slide 41

Slide 41 text

ख࡞ۀσϓϩΠ໨ࢹςετ 4".ͰσϓϩΠ BXTDMJͰςετ ෼ ෼ MPDBMTUBDLΛ࢖ͬͨ ϩʔΧϧͰͷςετ ඵ

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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 ݱঢ়֬ೝͷͨΊΧόϨοδଌఆ

Slide 44

Slide 44 text

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 ); ྫ֎ܥ͕ςετ͞Εͯͳ͍͜ͱ͕Θ͔Δ

Slide 45

Slide 45 text

l5FTU4J[FTzBU(PPHMF IUUQTUFTUJOHHPPHMFCMPHDPNUFTUTJ[FTIUNM

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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Ͱྫ֎ܥΛςετ͢Δ

Slide 48

Slide 48 text

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 ̄

Slide 49

Slide 49 text

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; }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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 定義をブランチ毎に生成する

Slide 52

Slide 52 text

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؀ڥʹର͢Δςετ

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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ʹґଘͨ͠ཁૉΛઙ͍֊૚ͰҾ͖ണ͕͢

Slide 58

Slide 58 text

ଟஈࣜΤϥʔϓϧʔϑ IUUQPSHBDIFNIBUFOBCMPHDPNFOUSZ

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

IUUQTNBSUJOGPXMFSDPNBSUJDMFTRBJOQSPEVDUJPOIUNM 2"JO1SPEVDUJPO

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

NJDSPTFSWJDFؒͷςετ&&9- IUUQNBSUJOGPXMFSDPNBSUJDMFTNJDSPTFSWJDFUFTUJOH

Slide 64

Slide 64 text

$POTVNFS%SJWFO$POUSBDU5FTUJOH IUUQTBTTFUTUIPVHIUXPSLTDPNBTTFUTUFDIOPMPHZSBEBSNBZFOQEG

Slide 65

Slide 65 text

1BDU IUUQTHJUIVCDPNSFBMFTUBUFDPNBVQBDU

Slide 66

Slide 66 text

$POTVNFSଆΛNPDLTUVCײ֮Ͱॻ͘ IUUQEJVTDPNBVTJNQMJGZJOHNJDSPTFSWJDFUFTUJOHXJUIQBDUT

Slide 67

Slide 67 text

ͦͷ૝ఆΛ1SPWJEFSଆͰWFSJGZ IUUQEJVTDPNBVTJNQMJGZJOHNJDSPTFSWJDFUFTUJOHXJUIQBDUT

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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