AWS CDKで配布可能なサーバレスアプリを作る

これまでAWSのSAM (Serverless Application Model) などを使ってサーバレスアプリを作成することが多かったのですが、サーバレスの構成情報とそこに必要となるパラーメタなどが密に結合してしまいがちで、どこでも使えるようなアプリが作りづらいという問題がありました。定義ファイルに後から値を埋め込めるParameterなどの機能はあったものの、YAMLやJSON上で無理くりやっている感があってわりと消耗が多かったです。

これに対してAWS CDK (Cloud Development Kit) では定義ファイルに相当する Constrcut をコードで記述することができ、かなり柔軟性高く利用できます。配布可能な形でいくつかのサーバレスアプリを作成してみたので、その際の知見を記事にまとめてみます。

CDKそのものの解説はこちら https://docs.aws.amazon.com/cdk/latest/guide/home.html

## CDKのプロジェクトを作成する

今回は `onigiri` という名前のモジュールを作成するとします。まず普通のCDKプロジェクトを作成します。

```bash
% mkdir onigiri
% cd onigiri
% cdk init --language typescript
```

## CDK Constructを作る

Construct は最終的にCloudFormationでデプロイされるStackのテンプレートです。 `onigiri` という名前でinitすると、 `lib/onigiri-stack.ts` というファイルが生成されるのでそこに記述します。以下の例は、一定時間ごとに起動されるLambdaが何かをクエリして、その結果をDynamoDBに書き込む、という想定のConstructです。Lambda Functionを1つ、DynamoDBを1つデプロイします。

```ts
import * as cdk from "@aws-cdk/core";
import * as dynamodb from "@aws-cdk/aws-dynamodb";
import * as lambda from "@aws-cdk/aws-lambda";
import * as events from "@aws-cdk/aws-events";
import * as eventsTargets from "@aws-cdk/aws-events-targets";
import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs";

import * as path from "path";

export interface properties extends cdk.StackProps {
  someParameter: string;
}

export class OnigiriStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: properties) {
    super(scope, id, props);

    const cacheTable = new dynamodb.Table(this, "cacheTable", {
      partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
    });

    const lambdaQuery = new NodejsFunction(this, "query", {
      entry: path.join(__dirname, "lambda/query.js"),
      handler: "main",
      environment: {
        TABLE_NAME: cacheTable.tableName,
        SOME_PARAM: props.someParameter,
      },
    });

    cacheTable.grantReadWriteData(lambdaQuery);

    new events.Rule(this, "periodicQuery", {
      schedule: events.Schedule.rate(cdk.Duration.minutes(10)),
      targets: [new eventsTargets.LambdaFunction(lambdaQuery)],
    });
  }
}
```

Constructを書く上でのコツは以下の通りです。

- 今回はLambda functionもTypeScriptで作成しするため、 `./node_modules` 以下のモジュールも同時にデプロイするために `@aws-cdk/aws-lambda-nodejs` パッケージにある `NodejsFunction` を利用します。他にも別レイヤーに `./node_modules` を格納する方法などがあるようですが、今回は割愛。
  - 本来 `NodejsFunction` の `path` パラメータはこのファイルからの相対パスが使えますが、モジュールとして配布した際に相対パスの基準が変わってしまうため、 `__dirname` をjoinして絶対パス指定にしています。
  - pathを指定する際には `.js` ファイルにします。これはモジュール配布後に `NodejsFunction` 経由でトランスコンパイルしようとするとうまくいかない場合があるからです
- `@aws-cdk/aws-lambda` などCDKのモジュールを追加する場合は、必ずinit時に追加された `aws-cdk/core` とバージョンを揃えてインストールします。わずかでもずれると `tsc` でエラーになります。
- 単体のCDKプロジェクトとして使う場合、`@aws-cdk/aws-lambda` などCDKのモジュールを追加は `devDependencies` になるよう `npm i --save-dev @aws-cdk/aws-lambda` などとしますが、今回はモジュール配布先で利用する必要があるため、 `npm i @aws-cdk/aws-lambda` でインストールします。

また、配布用のconstructではパラメータを可変にしておくのが良いので、もともと `OnigiriStack` の引数である `cdk.StackProps` を拡張して `properties` というinterfaceを作っています。これによってなんらかの環境変数を渡したり、外部リソースを参照したりしたい場合に、情報を引数として渡すことが出来ます。

同時にLambda Functionを作成します。今回は `./onigiri/lib/lambda/query.ts` にLambda Functionのファイルがある想定です。


## package.json を編集する

`package.json` に以下の変更を加えます。

- `files: ["lib"]` を追加： `lib` 以下にトランスコンパイル済みのコードを配置するため
- `"main": "./lib/onigiri-stack.js"` を追加：配布先のプロジェクトからconstructのコードが読み込めるように
- `"type": "./lib/onigiri-stack.d.ts"`を追加：同上
- `scripts` 内に `"prepare": "tsc"` を追加：パッケージ公開時やgithubからのインストール時にトランスコンパイルを実行させるため

トランスコンパイルの出力先を `dist` にするというやりかたもありますが、そのあたりはお好みで調整して下さい。出来上がりのサンプルは https://github.com/m-mizutani/dnstrack/blob/master/package.json などをご参照下さい。

## デプロイ

この記事の目的は作成したCDK constructをモジュールとして配布することですが、作成途中でテストとしてデプロイしたい場合があるかと思います。そのための1つの方法として、 `bin/onigiri.ts` は環境変数でデプロイできるようになっていると、小回りがきいて便利です。例として、 `bin/onigiri.ts` を以下のようにします。

```ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import { OnigiriStack } from "../lib/onigiri-stack";

const stackName = process.env.ONIGIRI_STACK_NAME || "onigiri";
const app = new cdk.App();
new OnigiriStack(app, stackName, {
  someParameter: process.env.ONIGIRI_PARAM!,
});
```

こうしておくことで、環境変数で指定して開発中のレポジトリからそのままデプロイ出来つつ、開発環境のパラメータを埋め込むのを防ぐことができます。

```bash
% env ONIGIRI_STACK_NAME=onigiri-1 ONIGIRI_PARAM=nanika cdk deploy
```

## パッケージ公開＆配布

ここまでの設定ができていればあとは `npm publish` でパッケージ公開、あるいはプッシュされたgithubからインストールすることで、作成したCDK constructを外部プロジェクトで再利用することができるようになります。`npm i dareka/onigiri` などでインストールし、配布先の `./bin/nanika.ts` などで以下のように記述して利用できます。

```ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import { OnigiriStack } from "onigiri";

const app = new cdk.App();
new OnigiriStack(app, "wagaya-no-onigiri", {
  someParameter: "nanika-sugoi-guzai",
});
```

## サンプル

上記の手法にそって作成したconstructが以下になりますので、ご参考までに。

- DNS track: https://github.com/m-mizutani/dnstrack