作り始めたBoltアプリケーションをAWSサーバーレス環境にデプロイ可能にするためにCDKを後から適用してみた

2020.04.24

CX事業本部の阿部です。

先日から少しずつSlack Appを作っています。 バックエンドはBoltフレームワークを使って作っているのですが、テストをしている間はngrokを使ってローカルホストで動かしているサーバーにアクセスさせていました。

初期段階での利用に目処がたったので、AWSにデプロイ、できればLambdaを使ってサーバーレスな環境で運用したくなりました。 なおかつ、インフラ周りの構成はできればCDKに寄せたい。

ということで、一旦デプロイ先を考えずに素で作り始めたBoltアプリケーションのインフラ構成をするために後からCDKを適用してみました。

今回のゴール

  • インフラ構成用とランタイム用の package.json を切り離す
  • ランタイム用の依存ライブラリとソースをデプロイする

実際にBoltアプリケーションのバックエンドをAWS上でどう構成するかはアーキテクチャの問題なので、まずは構成とアプリケーションを切り離してCDKを使ってデプロイ可能な状態を目指します。

前提

現在のアプリケーションはよくあるGetting Startedから拡張して作っています。

そのため、プロジェクトのディレクトリのトップにある package.json やその他設定ファイルなどはBoltアプリケーションのための構成となっています。 アプリケーションのコードは src 配下に集約してあります。

$ ls -lart
total 200
-rw-r--r--    1 abe.shinsuke  staff    857  3  3 17:54 .eslintrc
-rw-r--r--    1 abe.shinsuke  staff    198  3  3 17:58 .eslintignore
-rw-r--r--    1 abe.shinsuke  staff    124  3 11 10:05 .env
-rw-r--r--    1 abe.shinsuke  staff    514  4  3 09:44 tsconfig.json
drwxr-xr-x    8 abe.shinsuke  staff    256  4 23 11:08 ..
-rw-r--r--    1 abe.shinsuke  staff     25  4 24 09:47 .gitignore
-rw-r--r--    1 abe.shinsuke  staff  73832  4 24 09:47 package-lock.json
drwxr-xr-x   15 abe.shinsuke  staff    480  4 24 09:47 .
-rw-r--r--    1 abe.shinsuke  staff    790  4 24 09:47 package.json
drwxr-xr-x    7 abe.shinsuke  staff    224  4 24 09:47 src
drwxr-xr-x   13 abe.shinsuke  staff    416  4 24 09:47 .git

package.json の内容は以下です。

{
  "name": "cx-job-offer-meeting-tools",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "npm run build && npm run server",
    "build": "npx tsc",
    "server": "node --require dotenv/config dist/app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@slack/bolt": "^1.6.0",
    "dotenv": "^8.2.0"
  },
  "devDependencies": {
    "@types/eslint": "^6.1.8",
    "@types/node": "^13.7.7",
    "@typescript-eslint/eslint-plugin": "^2.22.0",
    "@typescript-eslint/parser": "^2.22.0",
    "eslint": "^6.8.0",
    "eslint-config-prettier": "^6.10.0",
    "eslint-plugin-prettier": "^3.1.2",
    "prettier": "^1.19.1",
    "typescript": "^3.8.3"
  }
}

課題になりそうなところ(事前の検討)

前提を踏まえた上で、CDK適用の課題になりそうなところをリストアップしてみます。

  • cdk init とかそのままやって大丈夫か?
  • Boltアプリケーションの起動の仕組みがLambdaと共存するか?
  • CDKのインフラ構成のためのコードと設定の適用

下調べ

実際のプランを検討する前に、情報を集めて下調べをしておきましょう。

BoltのアプリケーションをLambdaで呼ぶためのコードについて確認してみる

Serverless Frameworkを使って、Boltアプリケーションをクラウドにデプロイする際のサンプルがSlackの瀬良さんの資料の中で公開されています。

【レポート】Bolt を使った Slack 連携アプリの開発からデプロイ・運用まで超入門 – Developers.IO TOKYO 2019 #cmdevio

当該部分を抜粋します。

const expressReceiver = new ExpressReceiver ({
  signingSecret: サイニングシークレット,
  endpoints: '/events'
});

const app = new App({
  receiver: expressReceiver,
  token: トークン
})

const awsServerlessExpress = require('aws-serverless-express');
const server = awsServerlessExpress.createServer(expressReceiver.app);
module.exports.app = (event, context) => {
  awsServerlessExpress.proxy(server, event, context);
}

BoltアプリケーションはExpressで動いているので、 aws-serverless-express でハンドラ関数からプロキシすれば良いようです。

cdk init で作成されるリソースを確認してみる

次に、CDKでサービスを構成するために必要な物を整理しましょう。

cdk init で作成されるファイル、プロジェクト構成は以下のようになっています。

$ ls -lart
total 504
-rw-r--r--    1 abe.shinsuke  staff     135  4 22 10:18 .gitignore
-rw-r--r--    1 abe.shinsuke  staff      65  4 22 10:18 .npmignore
-rw-r--r--    1 abe.shinsuke  staff     543  4 22 10:18 README.md
drwxr-xr-x    3 abe.shinsuke  staff      96  4 22 10:18 bin
-rw-r--r--    1 abe.shinsuke  staff     130  4 22 10:18 jest.config.js
drwxr-xr-x    3 abe.shinsuke  staff      96  4 22 10:18 lib
-rw-r--r--    1 abe.shinsuke  staff     536  4 22 10:18 package.json
drwxr-xr-x    3 abe.shinsuke  staff      96  4 22 10:18 test
-rw-r--r--    1 abe.shinsuke  staff     596  4 22 10:18 tsconfig.json
-rw-r--r--    1 abe.shinsuke  staff     157  4 22 10:18 cdk.json
drwxr-xr-x   12 abe.shinsuke  staff     384  4 22 10:18 .git
drwxr-xr-x  467 abe.shinsuke  staff   14944  4 22 10:18 node_modules
-rw-r--r--    1 abe.shinsuke  staff  227332  4 22 10:18 package-lock.json
drwxr-xr-x   15 abe.shinsuke  staff     480  4 22 10:18 .
drwxr-xr-x    8 abe.shinsuke  staff     256  4 23 11:08 ..

この中で注目すべきは、 package.json の記載内容と bin 配下のファイルと lib 配下のファイルです。

package.json を見ていきます。

{
  "name": "cdk-test",
  "version": "0.1.0",
  "bin": {
    "cdk-test": "bin/cdk-test.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
  "devDependencies": {
    "@aws-cdk/assert": "1.34.0",
    "@types/jest": "^25.2.1",
    "@types/node": "10.17.5",
    "jest": "^25.3.0",
    "ts-jest": "^25.3.1",
    "aws-cdk": "1.34.0",
    "ts-node": "^8.1.0",
    "typescript": "~3.7.2"
  },
  "dependencies": {
    "@aws-cdk/core": "1.34.0",
    "source-map-support": "^0.5.16"
  }
}

CDKのプロジェクトなので、インフラ構成をするためのコードを実行するための依存関係になっています。 CDKのプロジェクトで実行時に依存関係を持ったライブラリがあるLambdaをデプロイする場合、そのままnode_modulesを一緒にデプロイしてしまうとノイズが多いので、ランタイム用の package.json を切り離す必要があります。

次に bin ディレクトリの配下ですが、CDKのConstructを実行するコードが置かれています。

$ ls -lart bin
total 8
drwxr-xr-x   3 abe.shinsuke  staff   96  4 22 10:18 .
-rw-r--r--   1 abe.shinsuke  staff  217  4 22 10:18 cdk-test.ts
drwxr-xr-x  15 abe.shinsuke  staff  480  4 22 10:18 ..
$ cat bin/cdk-test.ts 
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { CdkTestStack } from '../lib/cdk-test-stack';

const app = new cdk.App();
new CdkTestStack(app, 'CdkTestStack');

そして、 lib ディレクトリの配下ですが、Construct毎の構成コードが置かれています。

$ ls -lart lib
total 8
drwxr-xr-x   3 abe.shinsuke  staff   96  4 22 10:18 .
-rw-r--r--   1 abe.shinsuke  staff  245  4 22 10:18 cdk-test-stack.ts
drwxr-xr-x  15 abe.shinsuke  staff  480  4 22 10:18 ..
$ cat lib/cdk-test-stack.ts 
import * as cdk from '@aws-cdk/core';

export class CdkTestStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
  }
}

移行のアプローチ

さて、今まで見てきた情報から移行のアプローチを決めます。

  1. CDKのプロジェクト構成リソースの追加
  2. package.json の分離
  3. インフラコードの追加とCDKのブートストラップ
  4. BoltアプリケーションをプロキシするLambdaのハンドラコードを追加
  5. デプロイしてみる

CDKのプロジェクト構成リソースの追加

ものは試しで今回対象のプロジェクトで cdk init を実行してみましたが、予想はしていたものの何がしかファイルのある状態での実行はできないようでエラーになりました。

ということで手作業で以下作りました。

  • ckd.json
  • bin ディレクトリとその配下のコード(名前を変えて)
  • lib ディレクトリとその配下のコード(名前を変えて)

そのほかのファイルについてはとりあえずデプロイを確認するまでであれば不要でしょう。

現段階では何もインフラ構成のコードは書かれていません。

package.json の分離

次に、実行時に依存するライブラリの分離とLambda Layerとしてデプロイするための準備をします。

まず、 bundle/nodejs ディレクトリを作成して、カレントディレクトにした後 npm init を実行して package.json を作ります。

次に、プロジェクトのルートに戻り、以下二つのパッケージをアンインストールします。

  • @slack/bolt
  • dotenv

この時に、一時期にコンパイルエラーになる箇所が発生しますが、次の段階での対処ですぐに解消します。

次に、再度 bundle/nodejs をカレントディレクトリにして、以下三つのパッケージをインストールします。

  • @slack/bolt
  • dotenv
  • aws-serverless-express

最後に、プロジェクトルートで、 npm install --save-dev bundle/nodejs を実行してください。

これで、実行時に依存するライブラリとインフラ構成を含めたプロジェクト全体で依存するライブラリの分離は完了です。

インフラコードの追加とCDKのブートストラップ

次に、インフラ構成のコードを書きます。

この段階ではまずシンプルに依存関係を処理するLambda LayerとLambda Functionを作成します。

元ネタに使ったBoltアプリケーションの tsconfig の設定で、アプリケーションのビルドの結果は dist 以下に出力されるようになっているのでLambda Functionの作成時のアセットは dist を指定しています。

また、Lambda Layerとして bundle 配下をデプロイします。 Layerとしてデプロイするディレクトリでは事前に npm --prefix /bundle/nodejs install /bundle/nodejs を実行して node_modules を作成しておくようにしましょう。 私は、 cdk deploy コマンドで一気にできるようにプリプロセスとして上記を実行するコードを書きました。

以下、実際のコードの抜粋です。

(インフラ構成のコード)

export class CxJobOfferMeetingToolsStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        var region: string;
        if(props && props.env && props.env.region) {
            region = props.env.region;
        } else {
            region = "ap-northeast-1";
        }

        // Environment variable for lambda function
        require("dotenv").config();
        var lambdaSlackAppEnvironment = {
            SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN,
            SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET
        };

        // create lambda layer
        const bundleLayer = new lambda.LayerVersion(this, 'lambdaBundleLayer', {
            layerVersionName: 'cx-job-offer-meeting-tools-layer',
            code: new lambda.AssetCode(PreProcess.BUNDLE_LAYER_BASE_DIR),
            compatibleRuntimes: [lambda.Runtime.NODEJS_10_X],
        });

        const lambdaFunction = new lambda.Function(this, 'jobOfferMeetingSlackApp', {
            code: lambda.Code.asset('dist/'),
            handler: `app.handler`,
            runtime: lambda.Runtime.NODEJS_10_X,
            timeout: Duration.seconds(3),
            environment: lambdaSlackAppEnvironment,
            layers: [bundleLayer],
        });
    }
}

(プリプロセス)

import * as child_process from 'child_process';

export class PreProcess {
    public static BUNDLE_LAYER_BASE_DIR = process.cwd() + "/bundle";
    public static BUNDLE_LAYER_RUNTIME_DIR_NAME = "/nodejs"

    public static generateBundlePackage() {
        console.log(
            child_process.execSync(
                `npm --prefix ${this.getModuleInstallDir()} install ${this.getModuleInstallDir()}`).toString());
    }

    private static getModuleInstallDir(): string {
        return `${this.BUNDLE_LAYER_BASE_DIR}${this.BUNDLE_LAYER_RUNTIME_DIR_NAME}`;
    }
}

さて、ここまでで一度CDKのブートストラップをしましょう。

Lambda Functionとしてはまだ未完成ですが、少なくともCDKプロジェクトのファイル構成として、致命的な欠落がないかどうかは確認が取れます。 いくつかタイポがあったため、エラーになりましたがそれらを解消した後の実行結果が以下です。

$ cdk bootstrap
npx: 8個のパッケージを1.408秒でインストールしました。
npm WARN cx-job-offer-meeting-tools-bundle-layer@1.0.0 No repository field.

audited 188 packages in 1.404s
found 0 vulnerabilities


 ⏳  Bootstrapping environment aws://xxxxxxxxxxxxx/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 10:28:58 | CREATE_IN_PROGRESS   | AWS::S3::BucketPolicy | StagingBucketPolicy 
 0/2 | 10:28:59 | CREATE_IN_PROGRESS   | AWS::S3::BucketPolicy | StagingBucketPolicy Resource creation Initiated
 1/2 | 10:29:00 | CREATE_COMPLETE      | AWS::S3::BucketPolicy | StagingBucketPolicy 
 1/2 | 10:29:02 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | CDKToolkit 
 2/2 | 10:29:02 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
 ✅  Environment aws://xxxxxxxxxxxxx/ap-northeast-1 bootstrapped.

CDKプロジェクトとしてのブートストラップも完了したようです。

BoltアプリケーションをプロキシするLambdaのハンドラコードを追加

ここはサンプル通りに変更するだけです。

デプロイしてみる

ここまでくればデプロイですが、ブートストラップが成功しているので特になんの問題もなくデプロイできました。

Lambda Layerもデプロイされて、しっかりLambda Functionとも関連づけられています。Lambda Functionとしてデプロイされているコードも、ビルドした dist 配下のものでした。

まとめ

結構大手術になるかな、と予想していたのですが、思っていたよりも

もちろん、アーキテクチャー面で検討しなければならない面は残っていますが、早いうちにデプロイ先を切り替えて整理できる状態に持って行けたのは良いことだと思います。