AWS CDKでLambda Function用のTypeScriptのバンドルを簡単に行う

はじめに

おはようございます、加藤です。先日リリースされたAWS CDK 1.23から、aws-lambda-nodejsというモジュールが追加されました。これを使う事で、Lambda Function用のTypeScriptのトランスコンパイルとバンドルを簡単に行う事ができるのでご紹介します。

aws-lambda-nodejs ってなに?

現在このモジュールはベータ版です。ご注意ください

Node.jsでLambda Functionを作る為のHigh level Constructです。Lambda Functionに外部モジュールを参照するコードをデプロイする場合は、当然それらを一緒にデプロイするかLambda Layerにデプロイする必要があります。TypeScriptで書いている場合は合わせてトランスコンパイルも必要になります。

なので、AWS CDKでTypeScriptのLambda Functionを書く場合は、以下から方法を選択する必要があります。

  • tscでトランスコンパイルして、node_modulesはLayerで持つ
  • tscでトランスコンパイルして、node_modulesを全てのFunctionに持たせる
  • webpackやparcelでトランスコンパイル&バンドル

aws-lambda-nodejsはAWS CDK側でJavaScript/TypeScriptをParcelで、バンドルしてくれるモジュールです。これによって開発者はJavaScript/TypeScriptを書く事に集中でき、「あっ、トランスコンパイルするの忘れた。。。」が起きなくなります(私は良くやります。。。)

AWS CDKはTypeScriptで書いた場合、ts-nodeで実行するのでユーザー側で意識してトランスコンパイルする必要がありません。このモジュールを使う場合はユーザーはトランスコンパイルやバンドルを一切考えなくて良くなります。

  • ドキュメント: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-nodejs-readme.html
  • プルリクエスト: https://github.com/aws/aws-cdk/pull/5532

使い方

AWS CDKで下記の様に書けばOKです。

lib/stack.ts

1
2
3
4
5
import {NodejsFunction} from '@aws-cdk/aws-lambda-nodejs';
 
const fnDemo = new NodejsFunction(this, 'demo', {
  entry: 'src/lambda/handlers/demo.ts',
});

お試しで下記の様にLambda Functionを書きます。外部ライブラリを使用するTypeScriptなので、このままではLambda Function上で動作させる事ができません。

src/lambda/handlers/demo.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyResult} from 'aws-lambda';
import {v4 as uuid} from 'uuid';
 
export async function handler(
  event: APIGatewayProxyEvent,
  context: APIGatewayEventRequestContext
): Promise<APIGatewayProxyResult> {
 
  return {
    statusCode: 201,
    headers: event.headers,
    body: JSON.stringify({
      id: uuid(),
      method: event.httpMethod,
      query: event.queryStringParameters,
    })
  }
}

cdk synthでどのようにバンドルされるか確認します。

1
yarn run cdk synth

元々のTypeScriptが置いてあるディレクトリに .build というディレクトリが作成されバンドルされたJavaScriptが生成されました。また、実際にデプロイして動作する事も確認しました。下記が生成されたJavaScriptです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// modules are defined as an array
// [ module function, map of requires ]
//
// map of requires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the require for previous bundles
parcelRequire = (function (modules, cache, entry, globalName) {
  // Save the require from previous bundle to this closure if any
  var previousRequire = typeof parcelRequire === 'function' && parcelRequire;
  var nodeRequire = typeof require === 'function' && require;
 
  function newRequire(name, jumped) {
    if (!cache[name]) {
      if (!modules[name]) {
        // if we cannot find the module within our internal map or
        // cache jump to the current global require ie. the last bundle
        // that was added to the page.
        var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
        if (!jumped && currentRequire) {
          return currentRequire(name, true);
        }
 
        // If there are other bundles on this page the require from the
        // previous one is saved to 'previousRequire'. Repeat this as
        // many times as there are bundles until the module is found or
        // we exhaust the require chain.
        if (previousRequire) {
          return previousRequire(name, true);
        }
 
        // Try the node require function if it exists.
        if (nodeRequire && typeof name === 'string') {
          return nodeRequire(name);
        }
 
        var err = new Error('Cannot find module \'' + name + '\'');
        err.code = 'MODULE_NOT_FOUND';
        throw err;
      }
 
      localRequire.resolve = resolve;
      localRequire.cache = {};
 
      var module = cache[name] = new newRequire.Module(name);
 
      modules[name][0].call(module.exports, localRequire, module, module.exports, this);
    }
 
    return cache[name].exports;
 
    function localRequire(x){
      return newRequire(localRequire.resolve(x));
    }
 
    function resolve(x){
      return modules[name][1][x] || x;
    }
  }
 
  function Module(moduleName) {
    this.id = moduleName;
    this.bundle = newRequire;
    this.exports = {};
  }
 
  newRequire.isParcelRequire = true;
  newRequire.Module = Module;
  newRequire.modules = modules;
  newRequire.cache = cache;
  newRequire.parent = previousRequire;
  newRequire.register = function (id, exports) {
    modules[id] = [function (require, module) {
      module.exports = exports;
    }, {}];
  };
 
  var error;
  for (var i = 0; i < entry.length; i++) {
    try {
      newRequire(entry[i]);
    } catch (e) {
      // Save first error but execute all entries
      if (!error) {
        error = e;
      }
    }
  }
 
  if (entry.length) {
    // Expose entry point to Node, AMD or browser globals
    var mainExports = newRequire(entry[entry.length - 1]);
 
    // CommonJS
    if (typeof exports === "object" && typeof module !== "undefined") {
      module.exports = mainExports;
 
    // RequireJS
    } else if (typeof define === "function" && define.amd) {
     define(function () {
       return mainExports;
     });
 
    // <script>
    } else if (globalName) {
      this[globalName] = mainExports;
    }
  }
 
  // Override the current require with this new one
  parcelRequire = newRequire;
 
  if (error) {
    // throw error from earlier, _after updating parcelRequire_
    throw error;
  }
 
  return newRequire;
})({"Ls8Z":[function(require,module,exports) {
// Unique ID creation requires a high quality random # generator.  In node.js
// this is pretty straight-forward - we use the crypto API.
 
var crypto = require('crypto');
 
module.exports = function nodeRNG() {
  return crypto.randomBytes(16);
};
 
},{}],"bRX8":[function(require,module,exports) {
/**
 * Convert array of 16 byte values to UUID string format of the form:
 * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
 */
var byteToHex = [];
for (var i = 0; i < 256; ++i) {
  byteToHex[i] = (i + 0x100).toString(16).substr(1);
}
 
function bytesToUuid(buf, offset) {
  var i = offset || 0;
  var bth = byteToHex;
  // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4
  return ([
    bth[buf[i++]], bth[buf[i++]],
    bth[buf[i++]], bth[buf[i++]], '-',
    bth[buf[i++]], bth[buf[i++]], '-',
    bth[buf[i++]], bth[buf[i++]], '-',
    bth[buf[i++]], bth[buf[i++]], '-',
    bth[buf[i++]], bth[buf[i++]],
    bth[buf[i++]], bth[buf[i++]],
    bth[buf[i++]], bth[buf[i++]]
  ]).join('');
}
 
module.exports = bytesToUuid;
 
},{}],"Hr1T":[function(require,module,exports) {
var rng = require('./lib/rng');
var bytesToUuid = require('./lib/bytesToUuid');
 
// **`v1()` - Generate time-based UUID**
//
 
var _nodeId;
var _clockseq;
 
// Previous uuid creation time
var _lastMSecs = 0;
var _lastNSecs = 0;
 
// See https://github.com/uuidjs/uuid for API details
function v1(options, buf, offset) {
  var i = buf && offset || 0;
  var b = buf || [];
 
  options = options || {};
  var node = options.node || _nodeId;
  var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
 
  // node and clockseq need to be initialized to random values if they're not
  // specified.  We do this lazily to minimize issues related to insufficient
  // system entropy.  See #189
  if (node == null || clockseq == null) {
    var seedBytes = rng();
    if (node == null) {
      // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
      node = _nodeId = [
        seedBytes[0] | 0x01,
        seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]
      ];
    }
    if (clockseq == null) {
      // Per 4.2.2, randomize (14 bit) clockseq
      clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
    }
  }
 
  // UUID timestamps are 100 nano-second units since the Gregorian epoch,
  // (1582-10-15 00:00).  JSNumbers aren't precise enough for this, so
  // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
  // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
  var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
 
  // Per 4.2.1.2, use count of uuid's generated during the current clock
  // cycle to simulate higher resolution clock
  var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1;
 
  // Time since last uuid creation (in msecs)
  var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
 
  // Per 4.2.1.2, Bump clockseq on clock regression
  if (dt < 0 && options.clockseq === undefined) {
    clockseq = clockseq + 1 & 0x3fff;
  }
 
  // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
  // time interval
  if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) {
    nsecs = 0;
  }
 
  // Per 4.2.1.2 Throw error if too many uuids are requested
  if (nsecs >= 10000) {
    throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
  }
 
  _lastMSecs = msecs;
  _lastNSecs = nsecs;
  _clockseq = clockseq;
 
  // Per 4.1.4 - Convert from unix epoch to Gregorian epoch
  msecs += 12219292800000;
 
  // `time_low`
  var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
  b[i++] = tl >>> 24 & 0xff;
  b[i++] = tl >>> 16 & 0xff;
  b[i++] = tl >>> 8 & 0xff;
  b[i++] = tl & 0xff;
 
  // `time_mid`
  var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
  b[i++] = tmh >>> 8 & 0xff;
  b[i++] = tmh & 0xff;
 
  // `time_high_and_version`
  b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
  b[i++] = tmh >>> 16 & 0xff;
 
  // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
  b[i++] = clockseq >>> 8 | 0x80;
 
  // `clock_seq_low`
  b[i++] = clockseq & 0xff;
 
  // `node`
  for (var n = 0; n < 6; ++n) {
    b[i + n] = node[n];
  }
 
  return buf ? buf : bytesToUuid(b);
}
 
module.exports = v1;
 
},{"./lib/rng":"Ls8Z","./lib/bytesToUuid":"bRX8"}],"SC1p":[function(require,module,exports) {
var rng = require('./lib/rng');
var bytesToUuid = require('./lib/bytesToUuid');
 
function v4(options, buf, offset) {
  var i = buf && offset || 0;
 
  if (typeof(options) == 'string') {
    buf = options === 'binary' ? new Array(16) : null;
    options = null;
  }
  options = options || {};
 
  var rnds = options.random || (options.rng || rng)();
 
  // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
  rnds[6] = (rnds[6] & 0x0f) | 0x40;
  rnds[8] = (rnds[8] & 0x3f) | 0x80;
 
  // Copy bytes to buffer, if provided
  if (buf) {
    for (var ii = 0; ii < 16; ++ii) {
      buf[i + ii] = rnds[ii];
    }
  }
 
  return buf || bytesToUuid(rnds);
}
 
module.exports = v4;
 
},{"./lib/rng":"Ls8Z","./lib/bytesToUuid":"bRX8"}],"SkFz":[function(require,module,exports) {
var v1 = require('./v1');
var v4 = require('./v4');
 
var uuid = v4;
uuid.v1 = v1;
uuid.v4 = v4;
 
module.exports = uuid;
 
},{"./v1":"Hr1T","./v4":"SC1p"}],"EWEi":[function(require,module,exports) {
"use strict";
 
Object.defineProperty(exports, "__esModule", {
  value: true
});
 
const uuid_1 = require("uuid");
 
async function handler(event, context) {
  return {
    statusCode: 201,
    headers: event.headers,
    body: JSON.stringify({
      id: uuid_1.v4(),
      method: event.httpMethod,
      query: event.queryStringParameters
    })
  };
}
 
exports.handler = handler;
},{"uuid":"SkFz"}]},{},["EWEi"], "handler")

仕組み

どういう仕組みかコードを見てみます。

aws-cdk/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export class NodejsFunction extends lambda.Function {
  constructor(scope: cdk.Construct, id: string, props: NodejsFunctionProps = {}) {
    if (props.runtime && props.runtime.family !== lambda.RuntimeFamily.NODEJS) {
      throw new Error('Only `NODEJS` runtimes are supported.');
    }
 
    const entry = findEntry(id, props.entry);
    const handler = props.handler || 'handler';
    const buildDir = props.buildDir || path.join(path.dirname(entry), '.build');
    const handlerDir = path.join(buildDir, crypto.createHash('sha256').update(entry).digest('hex'));
    const defaultRunTime = nodeMajorVersion() >= 12
    ? lambda.Runtime.NODEJS_12_X
    : lambda.Runtime.NODEJS_10_X;
    const runtime = props.runtime || defaultRunTime;
 
    // Build with Parcel
    build({
      entry,
      outDir: handlerDir,
      global: handler,
      minify: props.minify,
      sourceMaps: props.sourceMaps,
      cacheDir: props.cacheDir,
      nodeVersion: extractVersion(runtime),
    });
 
    super(scope, id, {
      ...props,
      runtime,
      code: lambda.Code.fromAsset(handlerDir),
      handler: `index.${handler}`,
    });
  }
}

NodejsFunction は、 Function を継承していました。 以下の優先順位でjs/tsファイルを探し、Parcelを使ってバンドルしています。

  1. Given entry file
  2. A .ts file named as the defining file with id as suffix (defining-file.id.ts)
  3. A .js file name as the defining file with id as suffix (defining-file.id.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export function build(options: BuildOptions): void {
  const pkgPath = findPkgPath();
  let originalPkg;
 
  try {
    if (options.nodeVersion && pkgPath) {
      // Update engines.node (Babel target)
      originalPkg = updatePkg(pkgPath, {
        engines: { node: `>= ${options.nodeVersion}` }
      });
    }
 
    const args = [
      'build', options.entry,
      '--out-dir', options.outDir,
      '--out-file', 'index.js',
      '--global', options.global,
      '--target', 'node',
      '--bundle-node-modules',
      '--log-level', '2',
      !options.minify && '--no-minify',
      !options.sourceMaps && '--no-source-maps',
      ...options.cacheDir
        ? ['--cache-dir', options.cacheDir]
        : [],
    ].filter(Boolean) as string[];
 
    const parcel = spawnSync('parcel', args);
 
    if (parcel.error) {
      throw parcel.error;
    }
 
    if (parcel.status !== 0) {
      throw new Error(parcel.stderr.toString().trim());
    }
  } catch (err) {
    throw new Error(`Failed to build file at ${options.entry}: ${err}`);
  } finally { // Always restore package.json to original
    if (pkgPath && originalPkg) {
      fs.writeFileSync(pkgPath, originalPkg);
    }
  }
}

spawnSync を使ってシンプルにParcelを呼び出していますね。

まとめ

こういうモジュールを作れる所が、AWS CDKが真のInfrastructure as Codeというか、Infrastructure is Codeだなぁと思いました。念の為、セルフフォローすると、他のツールをディスっているんじゃなくて、YAMLやHCLはプログラミング言語じゃないよねって意味です。
オレはWebpack使いたいだ!!って人もこのモジュールのプルリクエストを参考にすれば追加できそうですね。AWS CDKは開発が活発で触っていて本当に楽しいです!! 以上でした!

Well Architected 動画セミナー
PR満足度98%