自身のLINE環境を更にリファクタリングしてみた

自身の LINE ボット環境を見直しました。プロジェクトのベースとして使えそうであればお使いいただければ幸いです。
2024.04.25

こんにちは、高崎@アノテーション です。

はじめに

LINE ボットの練習用として作成し、ディレクトリ構成を DDD のポリシーを元に見直した環境を下記のブログで発表しました。

この環境ではビルドやデプロイといった作業や Lambda に少しソースを追加する場合には不自由しないのですが、「フロントエンドのソースを入れたい」といった、まとまったモジュールを結合したい場合に整理出来ないリスクがあります。

そこで、ディレクトリ構成と package.json、tsconfig.json を見直したので記事にしました。

環境

まず始めに、今回リファクタリングした結果の環境を展開いたします。

見直した内容

ディレクトリ構成

変更前は下記でした。

たたみました

変更前の構成

/
┣ bin
┃ ┗ line_bot_test.ts
┣ lib
┃ ┗ line_bot_test-stack.ts
┣ src
┃ ┗ lambda
┃   ┣ di-container
┃   ┃ ┗ register-container.ts
┃   ┣ domain
┃   ┃ ┣ model
┃   ┃ ┃ ┣ imageCraft
┃   ┃ ┃ ┃ ┣ imageCraft.ts
┃   ┃ ┃ ┃ ┗ imageCragt-repository.ts
┃   ┃ ┃ ┗ memoStore
┃   ┃ ┃   ┣ memoStore.ts
┃   ┃ ┃   ┗ memoStore-repository.ts
┃   ┃ ┗ support
┃   ┃   ┗ line-bot
┃   ┃     ┗ line-bot.ts
┃   ┣ handler
┃   ┃  ┗ line-bot
┃   ┃    ┗ line-bot-handler.ts
┃   ┣ infrastracture
┃   ┃ ┣ line-bot
┃   ┃ ┃ ┗ line-bot-impl.ts
┃   ┃ ┗ repository
┃   ┃   ┣ imageCraft-bedrock-s3-repository.ts
┃   ┃   ┗ memoStore-dynamodb-repository.ts
┃   ┣ use-case
┃   ┃    ┗ use-case.ts
┃   ┣ package-lock.json
┃   ┗ package.json
┣ test
┃ ┗ line_bot_test.test.ts
┣ .eslintignore
┣ .gitignore
┣ .npmignore
┣ cdk.json
┣ jest.config.js
┣ LICENSE
┣ package-lock.json
┣ package.json
┣ README.md
┗ tsconfig.json

基本的な考え方としては、現在は IaC 用のソースと Lambda 用のソースに分かれていますので、この構成を IaC ソースはiac、Lambda ソースはbackendとして下記のように分けます。

入れ替え後のソース構成

/
┣ backend
┃ ┣ src
┃ ┃ ┣ di-container
┃ ┃ ┃ ┗ register-container.ts
┃ ┃ ┣ domain
┃ ┃ ┃ ┣ model
┃ ┃ ┃ ┃ ┣ imageCraft
┃ ┃ ┃ ┃ ┃ ┣ imageCraft.ts
┃ ┃ ┃ ┃ ┃ ┗ imageCragt-repository.ts
┃ ┃ ┃ ┃ ┗ memoStore
┃ ┃ ┃ ┃   ┣ memoStore.ts
┃ ┃ ┃ ┃   ┗ memoStore-repository.ts
┃ ┃ ┃ ┗ support
┃ ┃ ┃   ┗ line-bot
┃ ┃ ┃     ┗ line-bot.ts
┃ ┃ ┣ handler
┃ ┃ ┃ ┗ line-bot
┃ ┃ ┃   ┗ line-bot-handler.ts
┃ ┃ ┣ infrastracture
┃ ┃ ┃ ┣ line-bot
┃ ┃ ┃ ┃ ┗ line-bot-impl.ts
┃ ┃ ┃ ┗ repository
┃ ┃ ┃   ┣ imageCraft-bedrock-s3-repository.ts
┃ ┃ ┃   ┗ memoStore-dynamodb-repository.ts
┃ ┃ ┗ use-case
┃ ┃   ┗ use-case.ts
┃ ┗ package.json
┣ iac
┃ ┣ bin
┃ ┃ ┗ line_bot_test.ts
┃ ┣ lib
┃ ┃ ┗ line_bot_test-stack.ts
┃ ┣ test
┃ ┃ ┗ line_bot_test.test.ts
┃ ┣ cdk.json
┃ ┣ package.json
┃ ┗ tsconfig.json
┣ .eslintignore
┣ .gitignore
┣ .npmignore
┣ jest.config.js
┣ LICENSE
┣ package-lock.json
┣ package.json
┣ README.md
┗ tsconfig.json

それぞれのディレクトリに package.json や tsconfig.json といった設定 JSON が分かれますので、以下、後述します。

package.json

ディレクトリ分けしてしまうと、ライブラリアップデートやビルド、デプロイの時にそれぞれのディレクトリに入って行う必要があり非効率です。

そこで、ルート上に package.json を用意し、それぞれをワークスペースとして分けるようにして、それぞれの配下に package.json を置いて、ルート上からあらゆる操作を可能にしました。

また、ライブラリの配分も共通で使用されるものとそれぞれでのみ使用されるものを振り分けました。

  • ルートの package.json

package.json

{
  "name": "line_bot_test",
  "version": "0.1.0",
  "workspaces": [
    "backend",
    "iac"
  ],
  "scripts": {
    "build": "npm run build -ws",
    "watch": "tsc -w",
    "update:pkgs": "ncu -u && npm run update:pkgs -ws && npm i",
    "check:lint": "eslint --cache --max-warnings 0 '**/*.ts'"
  },
  "devDependencies": {
    "@tsconfig/recommended": "^1.0.6",
    "@types/node": "20.12.7",
    "@typescript-eslint/eslint-plugin": "^7.7.0",
    "@typescript-eslint/parser": "^7.7.0",
    "esbuild": "^0.20.2",
    "eslint": "^8.57.0",
    "eslint-plugin-react": "^7.34.1",
    "typescript": "5.4.5",
    "ts-node": "^10.9.2",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.2",
    "@types/jest": "^29.5.12",
    "@types/mocha": "^10.0.6"
  },
  "dependencies": {
    "npm-check-updates": "^16.14.18",
    "source-map-support": "^0.5.21"
  }
}
  • backend の package.json

backend/package.json

{
  "name": "lambda",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "echo \"Error: no test specified\" && exit 1",
    "update:pkgs": "ncu -u"
  },
  "author": "",
  "dependencies": {
    "@aws-sdk/client-bedrock-runtime": "^3.556.0",
    "@aws-sdk/client-s3": "^3.556.0",
    "@aws-sdk/client-ssm": "^3.556.0",
    "@aws-sdk/lib-dynamodb": "^3.556.0",
    "@aws-sdk/s3-request-presigner": "^3.556.0",
    "@line/bot-sdk": "^9.2.0",
    "@types/aws-lambda": "^8.10.137",
    "inversify": "^6.0.2"
  }
}

こちらは aws-sdk を基本線に、Lambda 上や(今は作っていないですが)テストコードを動かす時に備えて配置しています。

  • iac の package.json

iac/package.json

{
  "name": "iac",
  "version": "0.1.0",
  "bin": {
    "line_bot_test": "bin/line_bot_test.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "cdk": "cdk",
    "deploy": "cdk deploy",
    "destroy": "cdk destroy",
    "bootstrap": "cdk bootstrap",
    "update:pkgs": "ncu -u",
    "check:lint": "eslint --cache --max-warnings 0 '**/*.ts'"
  },
  "devDependencies": {
    "aws-cdk": "2.138.0"
  },
  "dependencies": {
    "aws-cdk-lib": "2.138.0",
    "constructs": "^10.3.0"
  }
}

こちらは cdk を中心に揃えました。

※2024/04/25 現在のソースですので、今後ライブラリアップデートが入るとバージョンが変わります。

tsconfig.json

tsconfig.json はビルド先のディレクトリ指定やビルドの設定を行いますが、それぞれ以下に設定するようにしました。

  • ルートの tsconfig.json

tsconfig.json

{
  "extends": "@tsconfig/recommended/tsconfig.json",
}
  • backend の tsconfig.json

backend/tsconfig.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    },
    "outDir": "tsc-output",
    "target": "ES2020",
    "module": "commonjs",
    "lib": [
      "es2020",
      "dom"
    ],
    "declaration": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "typeRoots": [
      "../node_modules/@types"
    ]
  },
  "exclude": [
    "node_modules",
    "tsc-output",
  ]
}
  • iac の tsconfig.json

iac/tsconfig.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "tsc-output",
    "declaration": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "typeRoots": [
      "../node_modules/@types"
    ]
  },
  "exclude": [
    "node_modules",
    "tsc-output",
    "cdk.out"
  ]
}

scripts 設定

package.json の scripts セクションにいくつか設定し、ルート上で以下のコマンドでそれぞれ実行するようにしました。

  • ビルド
npm run build
  • デプロイ
npm run deploy -w iac
  • ライブラリアップデート
npm run update:pkgs

いざデプロイ

デプロイはすんなり通りました。

さて、LINE Developers の検証を行ってみるとエラーが出ましたので CloudWatch Logs を見てみますと…。

ログ抜粋

ERROR	Uncaught Exception 	{
    "errorType": "Runtime.ImportModuleError",
    "errorMessage": "Error: Cannot find module 'line-bot'\nRequire stack:\n- /var/runtime/index.mjs",
    "stack": [
        "Runtime.ImportModuleError: Error: Cannot find module 'line-bot'",
        "Require stack:",
        "- /var/runtime/index.mjs",
        "    at _loadUserApp (file:///var/runtime/index.mjs:1087:17)",
        "    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"
    ]
}

どうやら line-bot のライブラリが Lambda 上にデプロイされていないためエラーが発生。

今までの環境ですと Lambda ソースにインストールしたライブラリディレクトリの node_modules もあったため、code パラメータに AssetFrom 設定することで配下一式をデプロイ出来ていました。

しかし、今回 package.json を統合によりルート上に node_modules が展開されてしまったためにデプロイされなくなりました。

Lambda の定義を見直す

この記事が非常に参考になりましたが、cdk における Lambda の定義をFunction関数からnodejsFunction関数に入れ替えました。

この関数を使用することで entry にハンドラソースを指定するだけで他はいい感じでデプロイしてくれます。

iac/lib/line_bot_test-stack.ts(抜粋)

    const lambdaMemoBot = new lambdaNodejs.NodejsFunction(this, "LineMemoBot", {
      runtime: lambda.Runtime.NODEJS_20_X,
      entry: "../backend/src/handler/line-bot/line-bot.ts",
      environment: {
        SECRET_ID: "LineAccessInformation",
        TABLE_MAXIMUM_NUMBER_OF_RECORD: "5",
      },
      role: roleMemoBot,
      timeout: cdk.Duration.seconds(120),
    });

更に、もう一工夫

このままデプロイすると、以下のログのように docker が走り出しました。

npm run deploy -w iac

> iac@0.1.0 deploy
> cdk deploy

[+] Building 3.3s (14/14) FINISHED                                                                                             docker:rancher-desktop
 => [internal] load build definition from Dockerfile                                                                                             0.0s
 => => transferring dockerfile: 1.30kB                                                                                                           0.0s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                  0.0s
 => [internal] load metadata for public.ecr.aws/sam/build-nodejs20.x:latest                                                                      3.3s
 => [ 1/10] FROM public.ecr.aws/sam/build-nodejs20.x@sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX                     0.0s
      中略
 => => naming to docker.io/library/cdk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX                                          0.0s
Bundling asset LineBotTestStack/LineMemoBot/Code/Stage...
esbuild cannot run locally. Switching to Docker bundling.
WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested

  asset-output/index.js  277.7kb

⚡ Done in 708ms

✨  Synthesis time: 8.52s

LineBotTestStack: deploying... [1/1]
LineBotTestStack: creating CloudFormation changeset...

 ✅  LineBotTestStack (no changes)

✨  Deployment time: 1.72s

Outputs:
LineBotTestStack.LineMemoApiEndpointXXXXXXXX = https://xxxxxxxxxx.execute-api.xxxxxxxxxxxxxx.amazonaws.com/prod/
Stack ARN:
arn:aws:cloudformation:xxxxxxxxxxxxxx:xxxxxxxxxxxx:stack/LineBotTestStack/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

✨  Total time: 10.24s

幸い、自分の環境に rancher desktop をインストールしていたので動作しましたが、docker をインストールしていない環境ではエラーになります。

そこで、ログにもesbuild cannot run locally. Switching to Docker bundling.と出ていますが、下記の記事を参考に esbuild をインストールすることで回避し、無事ビルド・デプロイが出来ました。

おわりに

自身の LINE 環境を見直してみました。

JSON へ追加での微調整が必要かもしれませんが、今後は LIFF を使ってのフロントエンドのソースや、バックエンドへ機能を追加する際はこの環境をベースにしようと思います。

本記事がソース整理のご参考になれば幸いです。

アノテーション株式会社について

アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。
サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。