🐩

CDKでJSリゾルバーとLambdaリゾルバーのデプロイ方法を揃える

2023/09/23に公開

AppSyncの構築をCDKでやっている際に、Typescriptで書いたJavaScriptリゾルバーのソースをデプロイする手段について悩んでいたのですが公式ドキュメントを確認したところ、esbuildを使う方法が紹介されていたのでそれに倣いCDKのカスタムコンストラクトを用意してTypescriptのリゾルバーをビルドからデプロイまで行いたいと思います。

今回は以下の3つの種類のリゾルバーのコンストラクトを用意したいと思います。

  • ユニットリゾルバー
  • パイプラインリゾルバー
  • Lambdaリゾルバー

コンパイル用のutilityを用意

まずはコンパイル用のutilityを用意します。
compileFileと言う関数をutilityに用意してカスタムコンストラクトからこれを呼び出すようにしています。

utilities.ts

import type { OutputFile } from 'esbuild'
import { buildSync } from 'esbuild'

export const compileFile = (entryPoint: string): OutputFile => {
  const result = buildSync({
    entryPoints: [entryPoint],
    bundle: true,
    write: false,
    external: ['@aws-appsync/utils'],
    format: 'esm',
    target: 'esnext',
    platform: 'node',
    sourcemap: 'inline',
    sourcesContent: false,
  })

  if (result.errors.length > 0) {
    throw new Error(
      `Failed to build ${entryPoint}: ${result.errors.join('\n')}`,
    )
  }
  if (!result.outputFiles[0]) {
    throw new Error(`Failed to build ${entryPoint}: no output files`)
  }

  return result.outputFiles[0]
}

ユニットリゾルバーのコンストラクト

次にユニットリゾルバー用のカスタムコンストラクトです。
propsでdataSourceとfieldName, typeName, resolverCodeのファイルパスを渡してcompileFileを実行するような構成になってます。

jsUnitResolverConstruct.ts

import * as appsync from 'aws-cdk-lib/aws-appsync'
import { Construct } from 'constructs'
import { compileFile } from './utilities'

interface JsUnitResolverProps {
  api: appsync.GraphqlApi
  dataSource: appsync.BaseDataSource
  typeName: 'Query' | 'Mutation' | string
  fieldName: string
  source: string
  resolverCode?: string
}

/**
 * JSのユニットリゾルバーをデプロイするカスタムコンストラクト
 */
export class JsUnitResolver extends Construct {
  public readonly resolver: appsync.Resolver
  constructor(scope: Construct, id: string, props: JsUnitResolverProps) {
    super(scope, id)

    const runtime = appsync.FunctionRuntime.JS_1_0_0
    this.resolver = props.dataSource.createResolver(
      `${props.fieldName}${props.typeName}Resolver`,
      {
        runtime,
        typeName: props.typeName,
        fieldName: props.fieldName,
        code: appsync.Code.fromInline(compileFile(props.source).text),
      },
    )
  }
}

パイプラインリゾルバーのコンストラクト

次にパイプラインリゾルバーです。
こちらもユニットリゾルバーと要領は同じでAppsyncFunctionの定義を追加しています。

jsPipelineResolverConstruct.ts

import * as appsync from 'aws-cdk-lib/aws-appsync'
import { Construct } from 'constructs'
import { compileFile } from './utilities'

interface JsPipelineResolverProps {
  api: appsync.GraphqlApi
  dataSource: appsync.BaseDataSource
  typeName: 'Query' | 'Mutation' | string
  fieldName: string
  functions: {
    name?: string
    dataSource?: appsync.BaseDataSource
    filePath: string
  }[]
  resolverCode: string
}

/**
 * 複数の関数で構成されるJSパイプラインリゾルバーをデプロイするカスタムコンストラクト
 */
export class JsPipelineResolver extends Construct {
  public readonly resolver: appsync.Resolver
  constructor(scope: Construct, id: string, props: JsPipelineResolverProps) {
    super(scope, id)

    const resolverCode = compileFile(props.resolverCode).text
    const runtime = appsync.FunctionRuntime.JS_1_0_0
    const funcs = props.functions.map((func, idx) => {
      const funcName =
        func.name ?? `${props.fieldName + props.typeName}Func${idx + 1}`
      return new appsync.AppsyncFunction(this, funcName, {
        api: props.api,
        dataSource: func.dataSource ?? props.dataSource,
        name: funcName,
        code: appsync.Code.fromInline(compileFile(func.filePath).text),
        runtime,
      })
    })

    this.resolver = props.dataSource.createResolver(`${props.fieldName}${props.typeName}Resolver`, {
      runtime: appsync.FunctionRuntime.JS_1_0_0,
      typeName: props.typeName,
      fieldName: props.fieldName,
      pipelineConfig: funcs,
      code: appsync.Code.fromInline(resolverCode),
    })
  }
}

Lambdaリゾルバーのコンストラクト

Lambdaリゾルバーの場合はデータソースもLambda毎に変わることになるのでコンストラクト内でdataSourceを作成しています。
関数によってアクセス先は制御したいのでexecutionRoleも各リゾルバーで設定するようにしています。

lambdaResolverConstruct.ts

import * as appsync from 'aws-cdk-lib/aws-appsync'
import { IRole } from 'aws-cdk-lib/aws-iam'
import { Runtime } from 'aws-cdk-lib/aws-lambda'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import { Construct } from 'constructs'

interface lambdaResolverProps {
  api: appsync.GraphqlApi
  typeName: 'Query' | 'Mutation' | string
  fieldName: string
  entry: string
  executionRole?: IRole
  environment?: { [key: string]: string }
}

/**
 * Lambdaリゾルバーをデータソース, Lambda関数含めてビルド・デプロイするカスタムコンストラクト
 */
export class lambdaResolver extends Construct {
  public readonly resolver: appsync.Resolver
  constructor(scope: Construct, id: string, props: lambdaResolverProps) {
    super(scope, id)

    const dataSource = props.api.addLambdaDataSource(
      'LambdaDataSource',
      new NodejsFunction(this, 'Lambda', {
        functionName: props.fieldName,
        entry: props.entry,
        handler: 'handler',
        runtime: Runtime.NODEJS_18_X,
        role: props.executionRole,
        environment: props.environment,
      }),
    )

    this.resolver = dataSource.createResolver('LambdaResolver', {
      typeName: props.typeName,
      fieldName: props.fieldName,
    })
  }
}

参考

https://zenn.dev/ashizaki/articles/8ad1bc443b6c56
https://artofserverless.com/appsync-js-resolvers/

Discussion