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** // // Inspired by https://github.com/LiosK/UUID.js 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を使ってバンドルしています。
- Given entry file
- A .ts file named as the defining file with id as suffix (defining-file.id.ts)
- 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は開発が活発で触っていて本当に楽しいです!!
以上でした!