AWS Lambda で、なでしこが動いた

※この記事は [AWS Lambda Custom Runtimes芸人 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/lambda-custom-runtimes) の 5 日目です。

AWS Lambda で COBOL がサポートされたというニュースが話題になりましたが、実際には任意の処理系で Lambda 関数を書けるようにする仕組みがサードパーティに解放された、というのが正しい理解です。であればやはりなるべく多くの処理系で Lambda 関数が書けるようにするというのが正しいエンジニアの態度ではないでしょうか。

ということで、日本語プログラミング言語である「[なでしこ](https://nadesi.com/doc3/) (v3)」で Lambda 関数を書けるようにしてみました。実際のところなでしこを触ったのはこれが初めてなのですが。

## 使い方

### コードを書く

まず、手元のエディタでなでしこのコードを書きます。

```function.nako
●（イベントデータとコンテキストの）ハンドラとは
　「イベントデータ：」と表示
　イベントデータを表示
　「コンテキスト：」と表示
　コンテキストを表示
　「こんにちは」を戻す
ここまで
```

他の言語の Lambda 関数と同様に、イベントデータとコンテキストの 2 つの引数を受け取って、結果を返す関数を定義します。上記の例では関数名は「ハンドラ」です。

コードが書けたら圧縮して zip ファイルにします。

### Lambda 関数を作成する

注: Advent Calendar の他の記事とほぼ同様なはずです

コンソールから普通に関数を作成します。ランタイムを「独自のランタイムを使用する」にする以外はいつもと同じです。

<img width="1297" alt="スクリーンショット 2018-12-03 0.06.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/20615/1d3fb802-f74f-81d7-8397-47e03f2827b8.png">

関数ができたら、まずはなでしこを実行するレイヤーを指定します。デザイナー画面に以前はなかった「Layers」という項目が増えているのでクリックします。

<img width="1303" alt="スクリーンショット 2018-12-03 0.07.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/20615/29eebe8d-5bfc-cc3a-5c67-401b8bbe598d.png">

「関数にレイヤーを追加」という画面になるので、「レイヤーバージョン ARN を提供」を選び、なでしこを実行する Custom Runtime が定義されたレイヤーの ARN を入力します。

<img width="1320" alt="スクリーンショット 2018-12-03 0.07.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/20615/f27185dc-b124-84eb-fb66-5df8d36be184.png">

あとは他の言語と同様に zip ファイルをアップロードして、ハンドラを指定すれば OK です。ファイル function.nako で関数「ハンドラ」を定義したので、ハンドラは `function.ハンドラ` を指定します (日本語！)。

<img width="1307" alt="スクリーンショット 2018-12-03 0.08.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/20615/c1d77406-31e0-7fb6-6806-df92d90ebfd0.png">

テストイベントを実行すると、きちんと実行できます。

<img width="1302" alt="スクリーンショット 2018-12-03 0.10.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/20615/c54b5378-2121-85f3-7203-4b14d3cabff2.png">


## Custom Runtime のレイヤー

https://github.com/tmy/aws-lambda-runtime-nadesiko

特に難しいことはあまりなくて、任意の処理系で HTTP の API を叩いてそのデータを書いた関数に渡せば良いだけです。関数本体に Custom Runtime を同梱することもできますが、新機能のレイヤーとして分離する方が良いでしょう。

  * Custom Runtime のエントリポイントは `bootstrap` という実行可能ファイル。
  * 関数に渡すコンテキストに必要な情報は環境変数と API レスポンスのヘッダに含まれる。
  * 関数本体は環境変数 `$LAMBDA_TASK_ROOT` で渡される場所に展開される
  * レイヤーも関数本体と同様に zip ファイルでアップロードし、実行時には `/opt` に展開される

なでしこの処理系の実体は npm パッケージなので、普通に Node.js で Custom Runtime を実装します。

Node.js のバイナリ一式をレイヤーの zip ファイルに含め、`bootstrap` から起動します。

```bash:bootstrap
#!/bin/bash
layer_root=$(dirname "$0")
cd "$LAMBDA_TASK_ROOT"
exec "$layer_root/node-v10.14.1-linux-x64/bin/node" "$layer_root/runtime.js"
```

ランタイム本体は、起動時になでしこのファイルをコンパイル・実行して、なでしこで定義された関数 (JavaScript の関数にコンパイル済み) を処理系から取り出してメインループで実行する、という処理を書きました。

```js:runtime.js
const CNako3 = require('nadesiko3/src/cnako3');
const fetch = require('node-fetch');
const fs = require('fs');

// https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html
const API = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01`;

async function nextInvocation() {
  const next = await fetch(`${API}/runtime/invocation/next`);
  const requestId = next.headers.get('Lambda-Runtime-Aws-Request-Id');
  const eventData = await next.json();
  const context = {
    リクエストID: requestId,
    タイムアウト: parseInt(next.headers.get('Lambda-Runtime-Deadline-Ms'), 10),
    関数ARN: next.headers.get('Lambda-Runtime-Invoked-Function-Arn'),
    ロググループ名: process.env.AWS_LAMBDA_LOG_GROUP_NAME,
    ログストリーム名: process.env.AWS_LAMBDA_LOG_STREAM_NAME,
    関数名: process.env.AWS_LAMBDA_FUNCTION_NAME,
    関数バージョン: process.env.AWS_LAMBDA_FUNCTION_VERSION,
    関数メモリサイズ: parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, 10),
  };
  return { requestId, eventData, context };
}

async function invocationResponse(requestId, response) {
  const body = typeof response === 'object' ? JSON.stringify(response) : response;
  await fetch(`${API}/runtime/invocation/${requestId}/response`, {
    method: 'post',
    body,
  });
}

async function invocationError(requestId, error) {
  body = JSON.stringify({ errorMessage: error.message, errorType: error.name });
  await fetch(`${API}/runtime/invocation/${requestId}/error`, {
    method: 'post',
    body,
  });
}

async function initializationError(error) {
  body = JSON.stringify({ errorMessage: error.message, errorType: error.name });
  await fetch(`${API}/runtime/init/error`, {
    method: 'post',
    body,
  });
}

async function initialize() {
  try {
    const compiler = new CNako3();
    const [fileName, methodName] = process.env._HANDLER.split('.');
    const code = fs.readFileSync(`./${fileName}.nako`, { encoding: 'utf-8'});
    compiler.runReset(code);
    const methods = compiler.__varslist[1];
    return methods[methodName];
  } catch (error) {
    try {
      await initializationError(error);
    } catch (reportError) {
      console.error(error);
    }
    console.error(error);
    process.exit(1);
  }
}

async function invoke(method) {
  const { requestId, eventData, context } = await nextInvocation();
  try {
    const response = method(eventData, context);
    await invocationResponse(requestId, response); 
  } catch (error) {
    console.error(error);
    try {
      invocationError(requestId, error);
    } catch (reportError) {
      console.error(error);
    }
  }
}

async function main() {
  const method = await initialize();
  while (true) {
    await invoke(method);
  }
}

main();
```

## 感想

想像以上に Custom Runtime の実装は簡単なので、Amazon Linux で動くありとあらゆる処理系を Lambda 化できるはずです。

それどころか関数の内容を別のどこかに転送すれば Linux 以外の何か、すなわち別の OS だったり別のクラウドサービスで関数を実行させることさえ可能でしょう。流石にそこまですると Lambda なのにスケールしないかもしれませんが。