サーバーレスアプリケーション向けのAWS CDK、Serverless Stackを触ってみました

先日Serverless StackというAWSでサーバーレスアプリケーションを簡単に構築できるフレームワークの存在を同僚に教えて貰いました。どうやらAWS CDKをラップして実装しているらしく興味をもったのでさっそく触ってみました。
2021.03.16

はじめに

おはようございます、加藤です。先日Serverless StackというAWSでサーバーレスアプリケーションを簡単に構築できるフレームワークの存在を同僚に教えて貰いました。どうやらAWS CDKをラップして実装しているらしく興味をもったのでさっそく触ってみました。

概要

公式サイト: https://serverless-stack.com/

Serverless StackはAWSで簡単にサーバーレスアプリを構築するためのフレームワークです。

GitHubのスター数の変化を見てみると2021年1月から注目を浴びたようです。

AWS CDKをベースにサーバーレスアプリケーションの為に提供される特別な高レベルのコンストラクトを使い構築を行います。

ローカルでLambdaのコードを書き換えると即座に変更が適用されるLive Lambda Developmentという開発中のデバッグを強力に支援する機能が含まれているのが大きな特徴です。

LambdaはJavaScript(TypeScript)で書く必要がありますが、その代わりゼロコンフィグでトランスコンパイル、依存パッケージを含めたバンドリングを行ってくれます。

前提

  • Node.js >= 10.15.1
  • AWSアカウント(ローカルかAWS CLI/SDKでアクセスできるように設定されていること)

入門

プロジェクトを初期生成します。NPMではなくYarnを使っている方は—use-yarnオプションを付与して実行してください。

npx create-serverless-stack@latest my-sst-app --language typescript
cd my-sst-app

ディレクトリ構成はこんな感じです。AWS CDKでcdk initした場合と似ています。

lib: アプリケーションを動かす為のAWSインフラストラクチャを格納するディレクトリです。

src: Lambda上動く、アプリケーションコードを格納するディレクトリです。

my-sst-app
├── README.md
├── node_modules
├── .gitignore
├── package.json
├── sst.json
├── test
│   └── MyStack.test.js
├── lib
|   ├── MyStack.js
|   └── index.js
└── src
    └── lambda.js

インフラストラクチャ(lib)

インフラストラクチャを構築する為のエンドポイントです。見た目はAWS CDKと同じですが、Serverless Stackが提供するクラスを使用します。(これは以降も同様です)

lib/index.ts

import MyStack from "./MyStack";
import * as sst from "@serverless-stack/resources";

export default function main(app: sst.App): void {
  new MyStack(app, "my-stack");

  // Add more stacks
}

sst.ApiでHTTP API(API Gateway v2)だけでなくLambda関数を作成しルーティングの設定まで行えます。AWS CDKのL2 Constructと比べると抽象度が非常に高いです。

lib/MyStack.ts

import * as cdk from "@aws-cdk/core";
import * as sst from "@serverless-stack/resources";

export default class MyStack extends sst.Stack {
  constructor(scope: sst.App, id: string, props?: sst.StackProps) {
    super(scope, id, props);

    // Create the HTTP API
    const api = new sst.Api(this, "Api", {
      routes: {
        "GET /": "src/lambda.handler",
      },
    });

    // Show API endpoint in output
    new cdk.CfnOutput(this, "ApiEndpoint", {
      value: api.httpApi.apiEndpoint,
    });
  }
}

関数(src)

プレーンテキストを返すシンプルなAPI Gateway用のLambda関数です。

src/lambda.ts

import { APIGatewayProxyResult } from "aws-lambda";

export async function handler(): Promise<APIGatewayProxyResult> {
  return {
    statusCode: 200,
    body: "Hello World!",
    headers: { "Content-Type": "text/plain" },
  };
}

Live Lambda Development

Live Lambda Development用のデバッグ環境とアプリケーションがCloud Formationで作成され、APIのURLが表示されます。ターミナルはそのまま閉じずに、ブラウザか別ターミナルからcurlでURLにアクセスするとレスポンスを受け取ることができます。また元々のターミナルにはアクセスログが表示されています。

npx sst start

# Stack dev-my-sst-app-my-stack
#   Status: deployed
#   Outputs:
#     ApiEndpoint: https://xxxxxxxx.execute-api.us-east-1.amazonaws.com

src/lambda.tsHello World!を任意の文字に書き換え再度アクセスすると即座にレスポンスが変更されることが確認できます。これはデバッグ環境には本来のコードではなくスタブがデプロイされており、これが呼ばれるとWeb Socketを通じてローカルのLambda関数を実行することで実現されています。

Lambda以外はAWS上にデプロイされているので、LocalStackの様なエミュレーターではなく実物でデバッグが行えます。しかし、実行時に外部への通信が必要になるのでユニットテストおよびインテグレーションテストには不向きで、E2Eテストをするならば開発環境にデプロイすれば良いです。つまりテストには不向きであくまでもデバッグ用です。

しかし、サーバーレス開発において手元でLambdaのコードを書き換えると即座に反映されるというのはものすごく大きなメリットです。私はハマったときは、マネジメントコンソール上から直接編集して動作確認し、正常に動いたらローカルで編集してCommitということを良くやっていました。そしてこれをやるために依存パッケージのバンドリングではなくトランスコンパイル&Lambdaレイヤーでの依存パッケージ管理のパターンを選択していました。

しかし、ローカルで実行されるのでVPC Lambdaの場合でも、VPC内リソースにアクセスできないことに注意してください。アクセスしたい場合は、AWS ClientVPNでローカルとVPCをつなぐと事で対処ができます。

より応用的な構成で試す

Live Lambda Developmentで説明した構成を実際に試してみます。

Serverless Stackでは、SNSトピックを宣言し同時にsubscribersとしてトピックを購読するLambda関数を宣言することができます。

HTTP APIの宣言を先ほどは文字列型のみで行っていましたが、環境変数など詳細まで指定する場合はオブジェクトで指定します。attachPermissionsで他のリソースに対するアクセス権限を付与することができます。

lib/MyStack.ts

import * as cdk from "@aws-cdk/core";
import * as sst from "@serverless-stack/resources";

export default class MyStack extends sst.Stack {
  constructor(scope: sst.App, id: string, props?: sst.StackProps) {
    super(scope, id, props);

    // Create the SNS Topic
    const topic = new sst.Topic(this, "topic", {
      subscribers: ["src/sns.handler"],
    });

    // Create the HTTP API
    const api = new sst.Api(this, "Api", {
      routes: {
        "GET /": {
          function: {
            handler: "src/api.handler",
            environment: { TOPIC_ARN: topic.snsTopic.topicArn },
          },
        },
      },
    });
    api.attachPermissionsToRoute("GET /", [topic]);

    // Show API endpoint in output
    new cdk.CfnOutput(this, "ApiEndpoint", {
      value: api.httpApi.apiEndpoint,
    });
  }
}

API用のLambda関数を作成します。トピックへPublishするためにAWS SDKをインストールします。

npm install --save-dev @aws-sdk/client-sns

src/api.ts

import { SNSClient, PublishCommand } from "@aws-sdk/client-sns";
import { APIGatewayProxyHandlerV2 } from "aws-lambda";

const client = new SNSClient({});

export const handler: APIGatewayProxyHandlerV2 = async (event) => {
  console.log(
    `Logging from inside the API Lambda for route: ${event.routeKey}`
  );
  const command = new PublishCommand({
    Message: "Hello from the API Lambda.",
    MessageStructure: "string",
    TopicArn: process.env.TOPIC_ARN,
  });

  await client.send(command);

  return {
    statusCode: 200,
    body: "Hello World",
    headers: { "Content-Type": "text/plain" },
  };
};

トピックを購読するLambda関数を作成します。

src/sns.ts

import { SNSHandler } from "aws-lambda";

export const handler: SNSHandler = (event) => {
  console.log(
    `Logging from inside the SNS Lambda with event message: "${event.Records[0].Sns.Message}"`
  );
};

構成の変更が完了したので、デバッグ環境を起動します。先ほどとインフラストラクチャが変更されているため、初回はCloud Formationによるデプロイに時間がかかります。

npx sst start

URLにアクセスするとAPIのLambda関数とトピックを購読しているLambda関数が順次起動していることがアクセスログで確認できます。

最後にデプロイは下記のコマンドで行えます。

npx sst deploy

感想

Serverless Stackによって提供されるコンストラクトでサーバーレスアプリケーションを構築することは確かにコード量は減りますが、プロジェクトの初期フェイズの負担が少し減るだけで走り出してしまえば対して負担にならないのでメジャーなAWS CDKではなくマイナーなこちらを使うメリットとしては弱いと感じました。

Live Lambda Developmentはめちゃくちゃ便利です。またドキュメントによるとローカルで実行されるがLambdaに指定された環境変数を引き継いでくれます。

まとめると、現時点ではLive Lambda Developmentは非常に魅力的だがAWS CDKと比べてマイナーなServerless Stackをプロダクションで利用は躊躇してしまいます。もう少しバージョンが上がり安定したらまた触ってみようと思います。

以上でした。