Code EngineのFunctionsでPackageを導入する

# はじめに
[前回の記事]()では、Code EngineのFunctionsに非同期処理を実装する方法を紹介しましたが、今回は `npm` でパッケージのモジュールを導入したアプリを公開する方法を紹介します。
ただ、この内容だけだと少ないので、私が作った際に用意ソースコードや、テスト用のコード等を紹介します。

# 開発環境
- OS : Windows 11 Enterprise
- Node : 18.17.0
- npm : 9.6.7
- Editor : Visual Studio Code (拡張機能で [Rest Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) を利用)

# アプリ
[IBMソリューションブログ](https://www.ibm.com/blogs/solutions/jp-ja/)のRSSからランダムで選んだ1件の記事の情報をjsonで応答する

# 開発手順
対象のコードを開発する手順自体は普通です
1. `npm init`で開発環境のディレクトリに `package.json`を生成する
2. `npm install xxx` で必要なパッケージモジュールを導入する
    今回は `dotenv`、`rss-parser`をインストールしています
3. `main.js`を作成し、必要な機能を実装する
4. テスト呼び出し用の `driver.js`を作成し動作検証する
5. Code EngineのFunctionsに登録する
6. Rest Clientで動作検証する

## 1.`npm init`で開発環境のディレクトリに `package.json`を生成する
`npm init`で、(index.js)? と聞かれる部分だけ `main.js`を指定しています。いろいろ指定したい方は自由に設定ください。
もうちょっと書くべきところを書いても良いのですが、今回は簡易に進めたいと思います。
こんな感じの`package.json`になっていれば大丈夫です。
```json:package.json
{
  "name": "qiita-solution-blog-function",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
  }
}
```

## 2. `npm install xxx` で必要なパッケージモジュールを導入する
今回紹介するコードでは、 `.env` から環境変数を取得するのと、IBMソリューションブログのRSSの情報を取得したいため、 `dotenv`、`rss-parser`をインストールしています。
- `npm install dotenv`
- `npm install rss-parser`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/bbf373f0-4236-3727-4e7a-8a9d897cb204.png)

## 3. `main.js`を作成し、必要な機能を実装する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/bdad4aa5-1c7e-3c76-c18b-25b576a7208d.png)

やりたいことだけであれば、末尾10行ちょっとくらいで良いのですが、インターネット上にこのサービスを公開する場合に、URL知っていると誰でも呼び出されると困る、ということもあると思います。なので、簡易に `Bearer` の認証を入れるようにしています。
環境変数として定義された `BEARER_VALUE` の値と同じものがリクエストヘッダーの `Authorization` に設定されていないと弾く仕組みにしています。あと、 `POST` でリクエストされていない場合も弾いています。機能内容的には `GET` で良いと思いますが一応。
```javascript:main.js
require('dotenv').config();
const Parser = require('rss-parser');
const parser = new Parser();

const bearerValue = process.env.BEARER_VALUE; 

const main = async (args) => {
  if(args.__ce_method !== "POST" || 
    !args.__ce_headers.Authorization){
    return {
      statusCode: 400,
      headers: { 
        'Content-Type': 'application/json', 
        'WWW-Authenticate': 'Bearer error="invalid_request""'
      },
      body: {
        text:"Bad Request"
      },
    };
  }
  const reqAuthorization = args.__ce_headers.Authorization;
  if(!reqAuthorization.match(/^[bB]earer /)){
    return {
      statusCode: 401,
      headers: { 
        'Content-Type': 'application/json', 
        'WWW-Authenticate': 'Bearer realm="token_required"'
      },
      body: {
        text:"Unauthorized"
      },
    };
  }
  const reqBearerToken = reqAuthorization.replace(/^[bB]earer /,'');
  if(reqBearerToken !== bearerValue){
    return {
      statusCode: 401,
      headers: { 
        'Content-Type': 'application/json', 
        'WWW-Authenticate': 'Bearer error="invalid_token"'
      },
      body: {
        text:"Unauthorized"
      },
    };
  }
  const feed = await parser.parseURL("https://www.ibm.com/blogs/solutions/jp-ja/rss");
  const items = feed.items.map((data) => {
    return data;
  });
  const targetNum = Math.floor(Math.random() * (items.length));
  const body = items[targetNum];
  return {
    statusCode: 200,
    headers: { 
      'Content-Type': 'application/json', 
    },
    body,
  };
}

module.exports.main = main;
```
また、環境変数用の `.env`ファイルも作成します。
`BEARER_VALUE`は今回キーボードを適当にグシャグシャして設定しました。他の人に類推できなければ何でも良いと思います。
```javascript:.env
BEARER_VALUE=3zawx4se5drfv6t7b6numxsECRVt7by8nu9imO9oi8UumZYT5cRVaTByjIxrcvbK
```

## 4. テスト呼び出し用の `driver.js`を作成し動作検証する
さて、Code EngineのFunctionsに登録するためのモジュールは `main()`を外から実行するようにコーディングする関係で、このコード単体では動作しません。
なので、このコードを呼び出すための `driver.js`を作成します。

呼び出す際のパラメータに以下を設定しています
- `__ce_method` : Code EngineのFunctionsにどのメソッドで呼び出されたか識別するもの
- `__ce_headers` : Code EngineのFunctionsが呼び出された際のヘッダーを識別するもの
```javascript:driver.js
const mainJs = require('./main');
const main = async () => {
  const returnMain = await mainJs.main({
    "__ce_method": "POST",
    "__ce_headers": {"Authorization":"Bearer 3zawx4se5drfv6t7b6numxsECRVt7by8nu9imO9oi8UumZYT5cRVaTByjIxrcvbK"}
  });
  console.log(returnMain);
}
main()
```
試しに `driver.js` を呼び出すとRSSからランダムで1つの情報を返してくれることがわかります。

```js
C:\node-apps\node-apps\code-engine\qiita-solution-blog-function>node driver.js
{
  statusCode: 200,
  headers: { 'Content-Type': 'application/json' },
  body: {
    creator: 'masayayagihashi',
    title: 'ダイバーシティーハッカソン勉強会 with 株式会社Raise the Flag. | PwDA+クロス3',
    pubDate: 'Thu, 02 Nov 2023 12:25:52 +0000',
    'dc:creator': 'masayayagihashi',
    content: '<p>＜もくじ＞  見えないことは不便なだけで、不幸ではない  「もうこれなしの生活には戻れない」 &#124; みずいろクリップがヒット商品に  「SYN+（シンプラス）」 &#124; 視覚に代わる感覚を与える次世代型デバイス  障がいの有無 [&#8230;]</p>\n' +
      '<p>The post <a rel="nofollow" href="https://admin02.prod.blogs.cis.ibm.net/blogs/solutions/jp-ja/iot-pwda9/">ダイバーシティーハッカソ ン勉強会 with 株式会社Raise the Flag. | PwDA+クロス3</a> appeared first on <a rel="nofollow" href="https://admin02.prod.blogs.cis.ibm.net/blogs/solutions/jp-ja">IBM ソリューション ブログ</a>.</p>\n',
    contentSnippet: '＜もくじ＞  見えないことは不便なだけで、不幸ではない  「もうこれなしの生活には戻れない」 | みずいろクリップがヒット商品 に  「SYN+（シンプラス）」 | 視覚に代わる感覚を与える次世代型デバイス  障がいの有無 […]\n' +
      'The post ダイバーシティーハッカソン勉強会 with 株式会社Raise the Flag. | PwDA+クロス3 appeared first on IBM ソリューション ブログ.',  
    guid: 'https://admin02.prod.blogs.cis.ibm.net/blogs/solutions/jp-ja/?p=225250',
    categories: [
      'IBM Sustainability Software',
      'ソサエティ5.0',
      'ソーシャル・アジェンダ',
      '社会課題解決'
    ],
    isoDate: '2023-11-02T12:25:52.000Z'
  }
}
```
## 5. Code EngineのFunctionsに登録する
前回の記事では、モジュール1つを指定して登録しましたが、今回はこのコードが置いてあるディレクトリ全体を登録したいと思います。
ただ、Code Engineに必要な情報だけ登録できれば良いので、Code Engineには不要だと宣言する `.ceignore`ファイルを作成します。`.gitignore`や`.dockerignore`と同じような感じです。
```:.ceignore
node_modules
driver.js
.env
*.rest
```
`node_modules`は言わずもがな、環境変数を定義する `.env`も不要です。Code Engineでの環境変数の定義はこの後紹介します。
先ほど作成した `driver.js`もテスト用なので不要です。
`*.rest`は、Code EngineでFunctionsを作成した後にテスト用に作成するので、それをあらかじめ登録しています。想定外の動きでソースコード修正して、再度Functionsを登録する際に、誤ってテスト用のモジュールが登録されないようにするためです。

まず、今 `.env`に定義している環境変数をCode EngineのSecretに登録しましょう。
IBM Cloud CLIで登録します。事前に以下を満たすようにしてください。
- `ibmcloud login`で事前にIBM Cloudにログインしている
- `ibmcloud target -r [Code Engineのインスタンスが存在するリージョン] -g [Code Engineのインスタンスが紐づくリソースグループ]`の操作でターゲットを選んでいる
- `ibmcloud ce project select --name [Code Engineのインスタンス名]`の操作で対象のCode Engineを操作可能にしている
- ターミナルのカレントディレクトリをこれらのモジュールがあるディレクトリにしていること

```
ibmcloud ce secret create --name qiita-bearer-env -e .env
```
- `--name` : 作成するSecretの名前
- `-e` : `.env`形式のファイルから生成する場合のオプション

```console
C:\node-apps\node-apps\code-engine\qiita-solution-blog-function>ibmcloud ce secret create --name qiita-bearer-env -e .env
Creating generic secret 'qiita-bearer-env'...
OK

C:\node-apps\node-apps\code-engine\qiita-solution-blog-function>
```
作成されたSecretは以下のコマンドで確認できます
```console
C:\node-apps\node-apps\code-engine\qiita-solution-blog-function>ibmcloud ce secret get --name qiita-bearer-env   
シークレット「qiita-bearer-env」を取得しています...
OK

名前:             qiita-bearer-env
ID:               dd2a3c42-80f1-48b6-91f0-bf2c668f0373
フォーマット:     generic
プロジェクト名:   xxxxxxxxx
プロジェクト ID:  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
経過時間:         4m43s
作成日時:         2023-11-08 04:10:14 +0000 UTC

データ:
---
BEARER_VALUE: M3phd3g0c2U1ZHJmdjZ0N2I2bnVteHNFQ1JWdDdieThudTlpbU85b2k4VXVtWllUNWNSVmFUQnlqSXhyY3ZiSw==

```
表示される値はBase64でEncodeされています。コマンドに `--decode` のオプションを入れるとデコードして表示してくれます。

では、このSecretを使うようにFunctionsを作成します。

```
ibmcloud ce fn create --name qiita-solution-blog --runtime nodejs-18 --cpu 0.25 --memory 1G --env-sec qiita-bearer-env --build-source .
```
- `--env-sec` : 事前に作成されたSecretを指定して環境に組み込む
- `--build-source .` : カレントディレクトリにあるリソースのうち `.ceignore`で指定されていないものを対象とする

```console
C:\node-apps\node-apps\code-engine\qiita-solution-blog-function>ibmcloud ce fn create --name qiita-solution-blog --runtime nodejs-18 --cpu 0.25 --memory 1G --env-sec qiita-bearer-env --build-source .
ビルド・プッシュ用に関数「qiita-solution-blog」を準備しています ...
機能「qiita-solution-blog」を作成しています...
パス「.」からアップロードするファイルのパッケージ化...
ビルド実行 'qiita-solution-blog-run-231108-140127591'を送信中...
イメージ 'private.jp2.icr.io/ce--xxxxx-xxxxxxxxxx/function-qiita-solution-blog:231108-0501-gkq44' を作成しています ...
ビルド実行が完了するのを待機しています...
ビルド実行状況: '実行中'
ビルド実行が正常に完了しました。
ビルド実行の状況を確認するには、'ibmcloud ce buildrun get -n qiita-solution-blog-run-231108-140127591' を実行してください。
関数「qiita-solution-blog」が作動可能になるのを待っています ...
関数「qiita-solution-blog」は作動可能です。
OK
詳細を表示するには、'ibmcloud ce function get -n qiita-solution-blog' を実行してください。

https://qiita-solution-blog.xxxxxxxxxx.jp-osa.codeengine.appdomain.cloud

C:\node-apps\node-apps\code-engine\qiita-solution-blog-function>
```
公開できないものはマスキングしていますが、無事にFunctionを作成することが出来ました。

## 6. Rest Clientで動作検証する
`test.rest`ファイルを作成して、中に以下のような感じで記述します。
```:test.rest
@url=https://qiita-solution-blog.xxxxxxxxxx.jp-osa.codeengine.appdomain.cloud/
@token=3zawx4se5drfv6t7b6numxsECRVt7by8nu9imO9oi8UumZYT5cRVaTByjIxrcvbK

POST {{url}}
Authorization: Bearer {{token}}
Accept: application/json
Content-Type: application/json
```
`POST`以下を選択してリクエストを送ってみましょう。

無事にRSSからランダムで1件、その情報をjson形式で取得できていますね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/862995b9-1b89-effd-e840-41e1a722f975.png)

例えば、Bearerで設定している値を違うものにすると `Unauthorized`で応答されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/9450a062-d658-71f2-93b0-70f52fc1f0c9.png)

というわけで無事に登録したFunctionをローカルPCから動作検証することが出来ました。

# さいごに
いかがでしたでしょうか？Code EngineのFunctionsに任意のPackageを導入したアプリを公開することができるので、アプリの実装の幅が無限大に増えましたね。次の記事ではこのFunctionsを使って安価に使えるLINE Botの作り方でも公開できればと考えています。

また次回のCode Engineの記事をお楽しみに！