TypeScriptマネージドなApp RunnerをCDKで構築する

AWS CDK Advent Calendar 2022」の8日目の記事になります。(空いていたので後から埋めました。最後の1日でした!)

qiita.com


目次

目次


概要

前回、「Go言語をマネージドランタイムとして選択したAWS App Runnerを、AWS CDK for Goで作ってみる」という記事を書きました。

go-to-k.hatenablog.com


(※ちなみにマネージドランタイムというのは、DockerfileもECRも必要のない、GitHubへのpushをトリガー(または手動で)にデプロイしてくれる便利なものです。)


そこで今回はせっかくなので、「TypeScriptマネージドランタイムとして選択したAWS App Runnerを、AWS CDK for TypeScriptで作ってみる」というのもやってみました。

AWS CDK for TypeScriptって書きましたが、もはやfor TypeScript無くてもいいですかね。。。


また現在2022年12月16日時点で、CDKのApp RunnerのL2 Constructはα版ですが、「L1 Construct」と「L2 Constructのα版」どちらの方法でも構築してみました。


前提

今回、CDKのスタックの構成・書き方・リソース定義は、Goで作ったものとほぼ同じです(できるだけ、コンストラクタからパラメータまでかなり寄せました)。

そのため本記事ではそれほど細かい説明はしませんが、細かい説明の方は上記Go版の方の記事をご覧ください。


TypeScriptとGoで、CDKの書き方がどう変わるのか比べたりも面白いので、興味がある方はぜひ見てみてください。


GitHub

全コードはGitHubに公開しています。

github.com


大まかな内容

以下のようなことをやっています。

  • AutoScalingConfiguration(スケーリング設定)をカスタムリソースLambdaで作成
    • CDKモジュールのAwsCustomResourceでなくLambdaで作っているので、AutoScalingConfigurationの作成だけでなく更新・削除も可能
  • App Runnerインスタンスロール作成
  • AppRunner Service作成
    • L1, L2(α)どちらも作成
    • 自動デプロイ:オン
  • VPC Connector作成
    • L1, L2(α)どちらも作成
  • AWS SDKGitHub接続ARNの取得


(Go版で作ったものと)違う点

カスタムリソースLambdaの定義

これはTypeScript(Node.js)お馴染み、aws-lambda-nodejsのNodejsFunctionを使っています。

そのため、Lambdaのpackage.jsonはCDKのものと共有して、シングルパッケージにしています。(※App Runnerで公開するアプリケーションのpackage.jsonは別で作成)

    const customResourceLambda = new NodejsFunction(this, "custom", {
      runtime: Runtime.NODEJS_16_X,
      bundling: {
        forceDockerBundling: false,
      },
      timeout: Duration.seconds(900),
      initialPolicy: [
        new PolicyStatement({
          actions: [
            "apprunner:*AutoScalingConfiguration*",
            "apprunner:UpdateService",
            "apprunner:ListOperations",
          ],
          resources: ["*"],
        }),
        new PolicyStatement({
          actions: ["cloudformation:DescribeStacks"],
          resources: [this.stackId],
        }),
      ],
    });


カスタムリソースLambdaのProvider

Goの方ではCustomResourceコンストラクタに、LambdaのArnをそのままServiceTokenとして渡してカスタムリソースを作ったのですが、TypeScriptの方では(CustomResourceの)Providerというものを作り、そのプロパティのserviceTokenを渡しています。

  • (CDK定義)
    const autoScalingConfigurationProvider = new Provider(
      this,
      "AutoScalingConfigurationProvider",
      {
        onEventHandler: customResourceLambda,
      },
    );

    // (省略)

    const autoScalingConfiguration = new CustomResource(this, "AutoScalingConfiguration", {
      resourceType: "Custom::AutoScalingConfiguration",
      properties: autoScalingConfigurationProperties,
      serviceToken: autoScalingConfigurationProvider.serviceToken,
    });


これはTypeScriptではLambdaのハンドラーの型にCdkCustomResourceHandlerというものが提供されていて使ったところ、Provider経由でないとうまく動かなかったためです。

  • (Lambdaコード)
export const handler: CdkCustomResourceHandler = async function (event) {
    // (省略)


[追記]

こちらの記事を書いたところ、Twitterでお世話になっている ゆっきー (@WinterYukky) / Xさん からCdkCustomResourceに関して教えて頂いたので(ありがとうございます!)、補足します。


Providerを経由しない場合にCdkCustomResourceが利用できない理由は、「通常のCloudFormationのカスタムリソースとして利用されるため」、だそうです。

通常のカスタムリソースは、CloudFormationCustomResourceHandler型が使えるとのことです。


CDKとCloudFormationのハンドラでそれぞれ役割が分かれているんですね。(ご丁寧にこんなに分かりやすい図まで頂き感激です;;)

またProviderを利用する場合、以下のようなメリットがあるそうです。

  • Handles responses to AWS CloudFormation and protects against blocked deployments
  • Validates handler return values to help with correct handler implementation
  • Supports asynchronous handlers to enable operations that require a long waiting period for a resource, which can exceed the AWS Lambda timeout
  • Implements default behavior for physical resource IDs.


最初はなんとなく使っていたのですが、色々と恩恵のある機能なんですね。3つ目の、非同期ハンドラによるLambdaのタイムアウトを超えるような長時間の処理が可能になるのは、色々カスタムリソースのユースケースの幅が広がりそうで良さそうです。

ゆっきーさんありがとうございました!


GitHub接続の扱い方

Go版の方では、

  • CDKデプロイの際にGitHub接続があるか確認し、なかったら作る・あれば使う
  • 作った場合や"PENDING_HANDSHAKE"の場合はprompt待ちにして、コンソールにボタンを押しに行く時間を作り、その後Enterを押したらCDK処理を再開させる

というちょっと凝ったことをしていたのですが、今回そこまでしなくてもいいかと思い、単純に「あれば取得、なければエラー」というような事前に作るのを前提にしました。


GitHub接続は、アプリケーションと同じ単位のこともあればAWSアカウントなどもう少し大きい単位で使いまわしたりすることもあるかなと考え、カスタムリソースとしてアプリケーションのスタックに組み込むなどはやりませんでした。


また、一応GitHub接続を作成するシェルスクリプトは作っておいたので、それを走らせてからCDK叩く感じになります。

#!/bin/bash
set -eu

cd `dirname $0`

PROFILE=""
PROFILE_OPTION=""
REGION="ap-northeast-1"
CONNECTION_NAME=""
PROVIDER_TYPE="GITHUB"

while getopts p:c: OPT; do
    case $OPT in
        p)
            PROFILE="$OPTARG"
            ;;
        c)
            CONNECTION_NAME="$OPTARG"
            ;;
    esac
done

if [ -z "${CONNECTION_NAME}" ]; then
    echo "CONNECTION_NAME (-c) is required"
    exit 1
fi

if [ -n "${PROFILE}" ]; then
    PROFILE_OPTION="--profile ${PROFILE}"
fi

aws apprunner create-connection \
  --connection-name ${CONNECTION_NAME} \
  --provider-type ${PROVIDER_TYPE} \
  ${PROFILE_OPTION}


ビルド

マネージドランタイムでApp Runnerを使う場合、ビルドコマンド・起動コマンドを指定する必要があります。

ビルドコマンド

今回TypeScriptなので、esbuildを使ってビルド(トランスパイル)しました。

cd app && yarn install --non-interactive --frozen-lockfile && yarn build


yarn buildは、package.jsonのscriptsでこんな感じで定義しています。

esbuild ./index.ts --bundle --outfile=./index.js --platform=node --minify --allow-overwrite


起動コマンド

トランスパイル済みのjsファイルをnodeコマンドで起動します。

node app/index.js



最後に

App RunnerのマネージドランタイムをGo版で作ったので、TypeScriptでも書いてみたら面白いんじゃないかと思い書いてみました。

TypeScriptとGoのCDKの違いなどもわかって良い経験になりました。