Lambda Node.js 16 EOL 対応とは / AWS

# Node.js よりも前に自分が EOL

気が付けば2024年になりました。

昨年は途中で気持ちが折れたものの現役ビジネスマンとして
有効期限が切れないように今年もよろしくお願いいたします。

# Lambda のランタイムを探る

今更次郎さんではありますが
現在新規に作成する Node.js は v20 が初期設定になっているみたい

かつて環境構築時に最新であった Node.js 16 は
気持ちに含まれる OpenSSL1.1.1 が 2023年9月11日にEOL で完了。

ただやさしい AWSでは気にせずサポートを継続してくれておりました
ただ気が付けば 2024年6月12日が廃止日。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136487/f7038555-3dca-97bc-b28f-75ff53fa834f.png)
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html

そのまま利用できるとはいえ、なにかセキュリティインシデント到来時
あのときに変更しておけばと悲しむこと請け合い。

ひとまず誰が作ったのか知らない CloudwatchAlert からslackに通報する
古の Lambda関数を利用してバージョンの差異を確認しようと思います。

# 全体構成

ていうかよくありすぎてつまらない図。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136487/20a6d283-74cf-d126-0196-5da01cb86fc3.png)

Cloudwatch Alert でメトリクスを設定
サーバが壊れたり、高負荷になったら SNS Topicを発動
そこから Lambda関数で slackの webhookを呼び出す仕組みです。

※どっかから拾ってきたような英語のコメントソースが稼働中。

<details>
<summary>ソースコード全体</summary>

```python
const url = require('url');
const https = require('https');

// Update with the actual Slack WebHook URL from environment variables
const hookUrl = process.env.slackWebHookUrl;

const processEvent = function(event, context) {
    const message = JSON.parse(event.Records[0].Sns.Message);

    // Customize the message before posting to Slack
    let status = message.NewStateValue;
    let color = "danger";

    // Set the color based on the status of the message
    if (status === "ALARM") {
        color = "danger"; // Use red color for alarming status
    }
    if (status === "OK") {
        color = "good"; // Use green color for clear status
    }

    // Create the pretext for the Slack message
    const pretext = `*[${status}] ${message.Trigger.MetricName}*`;
    
    // Construct the text for the Slack message with details
    const text = `*Instanceid:* ${message.Trigger.Dimensions[0].value}\n\n` +
                 `*AlarmName:* ${message.AlarmName}\n\n` +
                 `*Timestamp:* ${message.StateChangeTime}\n\n` +
                 `*Status:* ${message.OldStateValue}→${status}\n\n` +
                 `*Reason:* ${message.NewStateReason}\n\n` +
                 "```";

    const slackMessage = {
        username: "Amazon SNS",
        attachments: [
            {
                color: color,
                pretext: pretext,
                text: text
            }
        ]
    };

    // Post the message to Slack
    postMessage(slackMessage, function(response) {
        if (response.statusCode < 400) {
            console.info('Message posted successfully');
            context.succeed();
        } else if (response.statusCode < 500) {
            console.error(`Client error occurred when processing message: ${response.statusCode} - ${response.statusMessage}`);
            context.succeed(); // Do not retry for client errors (4xx)
        } else {
            // Server error encountered, retry the function
            context.fail(`Server error when processing message: ${response.statusCode} - ${response.statusMessage}`);
        }
    });
};

// Function to post the constructed message to Slack
const postMessage = function(message, callback) {
    const body = JSON.stringify(message);
    const options = url.parse(hookUrl);
    options.method = 'POST';
    options.headers = {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
    };

    const postReq = https.request(options, function(res) {
        const chunks = [];
        res.setEncoding('utf8');
        res.on('data', (chunk) => {
            chunks.push(chunk);
        });
        res.on('end', () => {
            const body = chunks.join('');
            if (callback) {
                callback({
                    body: body,
                    statusCode: res.statusCode,
                    statusMessage: res.statusMessage
                });
            }
        });
    });

    postReq.write(body);
    postReq.end();
};

// Export the handler function as a CommonJS module
module.exports.handler = function(event, context) {
    // Check if the Slack Hook URL and event records exist before processing
    if (hookUrl && event.Records && event.Records.length > 0) {
        processEvent(event, context);
    } else if (!hookUrl) {
        // Fail the context if the Slack Hook URL is missing
        context.fail('Missing Slack Hook URL.');
    } else {
        // Fail the context if the event records are missing or empty
        context.fail('Event records are missing or empty.');
    }
};
```
</details>

# まずはランタイムの更新だ

### 更新方法

Lambdaの設定画面を開くと Node.js 16 って書いています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136487/6fdaa5cd-82ac-110f-6acc-380cbcfdaf63.png)

編集を押下して

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136487/794fdce2-db1b-c9c6-9b1d-58ff1f4c2186.png)

おお。 Node.js 20 が選択できるって奥さま。知ってる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136487/cab9e2db-e3bf-6d5b-60ba-9bf45df45f7f.png)

Node.js 20 を選択して保存ボタン押下。完了。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136487/e53202ab-6a1a-1e4e-27be-e8895cddbb38.png)

どこかでみたような緑色のバーが画面上部に表示されて移行完了です。


# とにかくエラーはいて落ちる

## require は使えない

```javascript
{
  "errorType": "ReferenceError",
  "errorMessage": "require is not defined in ES module scope, you can use import instead",
  "trace": [
    "ReferenceError: require is not defined in ES module scope, you can use import instead",
    "    at file:///var/task/index.mjs:1:11",
    "    at ModuleJob.run (node:internal/modules/esm/module_job:217:25)",
    "    at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)",
    "    at async _tryAwaitImport (file:///var/runtime/index.mjs:1008:16)",
    "    at async _tryRequire (file:///var/runtime/index.mjs:1057:86)",
    "    at async _loadUserApp (file:///var/runtime/index.mjs:1081:16)",
    "    at async UserFunction.js.module.exports.load (file:///var/runtime/index.mjs:1119:21)",
    "    at async start (file:///var/runtime/index.mjs:1282:23)",
    "    at async file:///var/runtime/index.mjs:1288:1"
  ]
```

そう、Node.js 16 と Node.js 20 は完全互換ではないのです orz


## 何が違うのか。

:::note
Node.js 16 
・CommonJS（Node.jsのモジュールシステム）
・動的構造: CommonJSモジュールは実行時に解析されます。これにより、条件付きでの読み込みや動的な読み込みが可能になります。
・同期ローディング: 通常、CommonJSモジュールは同期的にロードされます。これはサーバーサイドのNode.js環境で利点となりますが、ブラウザ環境では性能の問題を引き起こす可能性があります。

Node.js 20
・ESモジュール（ECMAScriptモジュール）
・静的構造: ESモジュールは静的に解析されます。つまり、importとexportはファイルのトップレベルに記述する必要があり、条件付きでの読み込みや動的な読み込みは限定的です。
・非同期ローディング: ブラウザや最新のJavaScript環境では、ESモジュールは非同期的にロードすることが可能です。
:::

いやいや知らないから、笑

:::note warn
ESモジュールとCommonJSモジュールは互換性が完全ではないため、一緒に使用する際には注意が必要です。特に、CommonJSからESモジュールを読み込む場合やその逆の場合には、特定の方法でコードを書く必要があります。
Node.jsはバージョン13.2.0以降、ESモジュールのサポートを含むようになりましたが、既存のCommonJSモジュールとの完全な互換性はまだありません。
:::

そんなこと言われてもだれが作ったのか分からないソースを解析する暇ないし。
ひとまず以下を変更すれば動く（安直

```python
Node.js 16
例: const express = require('express');
例: module.exports = myFunction;

Node.js 20
例: import express from 'express';
例: export function myFunction() { ... }
```

結果これまで Node.js 16 で動作していた監視用の Lambda関数は
気が付けば Node.js 20 で動作するようになったのでした。めでたし。


# Lambda 関数の変更方法

そういえば。
なんとなく適当に作られた Lambda 関数は相当数から利用されており
Node.js のバージョンを変更するのに気が引けていたのですが

よくみたら Lambda関数にもバージョンを付与して運用できるのですね。
たしかに障害出てから元に戻すのに再度デプロイってかなり草。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3136487/3cf03385-9dbc-3d2b-68a2-134810e0bf06.png)
※本番環境の画面が荒野に轟く図

すでに何か所から利用されているのか分からないので鬼直球で変更したものの
これから構築、運用するならバージョン管理を管理画面からした上で
エイリアスを利用するようにしていただけませんか。

お願い、えらいひと・・。そうしないとあなたもEOLにｗ

今度はちゃんと時間をとって設定を見直してやるに一票。
でも今日は徹夜明けなので何もしないをしよう。いつも心に太陽を・・・・・。