결론부터
CDK Typescript Code Example (Github) : https://github.com/allssu/cdk-appsync-js-resolver
AWS Appsync에서 JavaScript를 통해 Resolver를 만들 수 있는 기능이 출시되었다. CDK를 통해 Appsync를 프로비저닝 하고, 간단한 Dog API를 통해 HTTP Resolver을 자바스크립트로 작성해봤다. Appsync는 JavaScript Resolver 기능 출시로 인해 기존에 VTL을 사용하던 서비스보다 훨씬 편하고 매력적인 서비스가 되었다.
AWS Appsync?
AWS Appsync는 GraphQL을 AWS 완전 관리형의 서버리스 서비스로 사용할 수 있는 서비스인데, 기존에는 GraphQL Resolver의 개발 프로그래밍 언어를 VTL(Velocity Tamplate Language)만 사용할 수 있도록 되어 있었다.
아래와 같은 예제 코드로 구성되는데, 템플릿 언어라 그런지 사용 방법이 일반적인 프로그래밍 언어와는 많이 다르다😂.
Appsync에서의 VTL(Velocity Template Language) 사용 예시
#set($ids = [])
#foreach($id in ${ctx.args.ids})
#set($map = {})
$util.qr($map.put("id", $util.dynamodb.toString($id)))
$util.qr($ids.add($map))
#end
{
"version" : "2018-05-29",
"operation" : "BatchGetItem",
"tables" : {
"Posts": {
"keys": $util.toJson($ids),
"consistentRead": true
}
}
}
위의 예제는 DynamoDB에서 BatchGetItem API를 사용하기 위한 변수 세팅과 변수에 값 추가하는 방법인데, Java나 Javascript 등 기존 프로그래밍 언어를 사용하는 개발자들은 한눈에 들어오지 않을 것 같다.
VTL을 사용하는 것이 많은 사용자에게 반감이 있었는지, Reddit에서 AWS Appsync 얘기를 하면 VTL 언어에 대한 반감과 단점들이 나열된 것을 볼 수 있다.
그리고 깃헙의 github/aws-appsync-community 리파지토리에는 JavaScript로 된 Resolver를 RFC로 요청하는 다음과 같은 이슈가 올라와 있었다. 많은 개발자가 자바스크립트로 Resolver를 작성하는걸 원했고, 그 결과…
AWS Appsync Javascript Resolvers
AWS AppSync GraphQL API, JavaScript 리졸버 지원 | Amazon Web Services
작년 11월 AWS Appsync에서 JavaScript로 Resolver을 작성할 수 있는 기능이 출시됐다. 이어서 최근 aws-cdk가 v2.60.0로 업데이트되면서 Appsync 모듈에 JavaScript Resolver 기능이 포함되었다. 동시에 CDK 실험 레벨이었던 Appsync 또한 정식 버전으로 출시됐고, 이제 CDK에서 Appsync GraphQL 기능을 안정적으로 사용할 수 있게 되었다🤩
CDK 레벨로 출시되기 전에 Cloudformation으로 먼저 출시가 돼서 CDK L1 기능으로 미리 체험하고 있었는데, Github aws-cdk 리파지토리 이슈에 있는 mtliendo의 댓글을 참고해서 도움받았다. mtliendo의 리파지토리를 통해 정식 출시 되기 전 Javascript Resolver를 사용할 수 있었고, 이번에 정식 출시가 되어 Appsync를 통해 간단한 GraphQL Resolver를 만들었다.
출시된 Javascript Resolver가 JavaScript의 모든 기능을 지원하는 건 아니다. Appsync에서 사용할 수 있는 기능들을 AWS는 APPSYNC_JS 런타임이라고 이름을 붙이고 제한적인 기능들을 제공한다. ECMAScript 6과 유사한 기능들을 이용한다고는 하지만, 모던 자바스크립트(ECMAScript 2015/ES6 포함한 상위버전)에서 사용할 수 있는 기술들과는 다르다. 대표적으로 Promise를 포함한 비동기 작업을 지원 하지 않고, JS에서 에러를 처리하는 throw 대신에 자체적인 에러처리를 지원한다. Promise의 경우 한 개의 Resolver에서 여러 데이터 소스를 처리하는 비동기 작업 자체가 없다 보니까 필요가 없어서 빠졌을 것 같다. 비동기 작업이 필요한 경우 현재로서는 Lambda 또는 HTTP를 데이터소스로 선택해서 비동기 처리하는 게 필요하다.
아래 캡처와 같이 throw를 포함해서 여러 가지 키워드를 지원 하지 않는 것도 확인할 수 있다.
AWS Appsync 데이터 소스
Appsync Resolver는 VTL(Velocity Template Language) 및 JavaScript를 통해서 개발할 수 있고, 사용사례에 따라 아래의 데이터 소스를 포함해서 Resolver 함수를 개발할 수 있도록 제공하고 있다. (2023년 01월 기준)
- Amazon DynamoDB: DynamoDB Table을 읽어서 데이터를 제어하는 기능
- AWS Lambda: Lambda 함수를 invoke 할 수 있다. AWS의 여러 가지 서비스를 연결하거나, 외부 API 또한 연동이 가능하다.
- Amazon OpenSearch Service: AWS가 ElasticSearch의 상용 라이센스를 이유로 ElasticSearch 분기 된 Opensearch를 만들었는데, 해당 서비스를 Appsync Resolver에서 직접 제어할 수 있다.
- Amazon Aurora Serverless: RDB를 읽을 수도 있는데, Aurora Serverless를 제어할 수 있도록 기능을 제공하고 있다. 아직 JavaScript 리졸버 레퍼런스에서는 Aurora Serverless에 대한 내용이 없는데, 직접 테스트 해보거나 Github Issue에서 찾아봐야 할 것 같다. 사용할 예정이 아니니 패스.
- HTTP: 비교적 자유롭게 쓸 수 있는 Lambda뿐만 아니라 Http API를 Resolver으로 사용할 수 있는데 PUT, POST, GET, DELETE, PATCH 메소드에 대해서 지원을 하고 있다. 파라미터는 query, headers, body를 지원하고 있고 일반적인 API들은 대부분 사용이 가능할 것으로 보인다.
- None : 이름 그대로 아무것도 없는 데이터 소스인데, 상수를 리턴하거나 Pipeline Resolver에서 로그를 찍는 등의 사용사례로 쓸 수 있다.
여러 가지 데이터소스 중, Javascript Resolver를 사용해 보기 위해 HTTP 데이터소스로 간단하게 개발해봤다. 평소에 즐겨 쓰는 Dog API를 사용했는데, GET 메소드에 아무런 헤더나 바디가 필요 없이 바로 호출하면 간단한 데이터(state, message)를 리턴하는 API를 연동할 수 있다.
CDK를 통해 Appsync 개발하기
AWS CDK에서 Appsync가 정식 출시되어, 이제는 따로 실험용 디펜던시를 추가하지 않고도 aws-cdk-lib에서 사용이 가능하다. 먼저 AWS CDK를 타입스크립트로 생성했다. CLI 한 줄이면 CDK 프로젝트가 세팅된다.
cdk init app --language typescript
다음으로, lib/graphql이라는 새로운 폴더를 만들고 Dog API를 통해서 받아올 graphql 스키마를 정의했다. status와 message는 필수로 리턴된다고 가정하고, Query에 status와 message가 포함된 Dog라는 type을 정의했다.
Dog API를 사용하기 위한 GraphQL 스키마 개발
#
# /lib/graphql/schema.graphql
#
type Dog {
status: String!
message: String!
}
type Query {
getDog: Dog
}
다음으로 위에서 정의한 스키마 파일을 읽도록 Appsync Stack을 개발하면 된다.
Appsync Stack (CDK v2 / typescript)
/**
* /lib/appsync-stack.ts
*/
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { aws_appsync as appsync, aws_logs as logs } from "aws-cdk-lib";
import * as path from "path";
/**
* @see https://github.com/allssu/cdk-appsync-js-resolver
* @description Example of developing an AWS Appsync JavaScript resolver using the AWS CDK
*/
export class AppsyncStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Appsync GraphQL APIa
const graphqlApi = new appsync.GraphqlApi(this, "GraphqlApi", {
name: "graphql-api",
schema: appsync.SchemaFile.fromAsset(
path.join(__dirname, "./graphql/schema.graphql") // GraphQL 스키마 경로를 지정해준다.
),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.API_KEY, // 잠깐 쓸거라서 API Key 타입으로 생성한다.
apiKeyConfig: {
name: "exampleKey",
expires: cdk.Expiration.after(cdk.Duration.days(7)), // API Key가 탈취되어도 오늘부터 일주일만 사용할 수 있다.
description: "graphql api example key",
},
},
},
logConfig: {
fieldLogLevel: appsync.FieldLogLevel.ALL, // console.log 및 console.error을 사용하기 위해 필요하다.
retention: logs.RetentionDays.ONE_WEEK, // 잠깐 쓸거라서 로그는 오래 보관될 필요가 없다.
},
});
// HTTP Data source (Dog API)
const dogDataSource = graphqlApi.addHttpDataSource(
"HttpDataSource",
"https://dog.ceo",
{
name: "DogApiSource",
description: "Dog API",
}
);
// HTTP Function (Dog API)
const getDogFunction = dogDataSource.createFunction("GetDogFunction", {
name: "getDogFunction",
code: appsync.Code.fromInline(`
import { util } from '@aws-appsync/utils'
export function request(ctx) {
return {
method: 'GET',
resourcePath: '/api/breeds/image/random',
};
}
export function response(ctx) {
return JSON.parse(ctx.result.body);
}
`),
runtime: appsync.FunctionRuntime.JS_1_0_0,
});
// Get Dog API Resolver
new appsync.Resolver(this, "GetDogResolver", {
api: graphqlApi,
typeName: "Query",
fieldName: "getDog",
code: appsync.Code.fromInline(`
export function request(ctx) {
return {}
}
export function response(ctx) {
return ctx.prev.result;
}
`),
runtime: appsync.FunctionRuntime.JS_1_0_0,
pipelineConfig: [getDogFunction], // 한 개의 Resolver에서 여러 가지 Function을 사용할 수 있다.
});
}
}
만약 Resolver 또는 Function 파일을 따로 분리한다면, 다음 예제를 참고하면 된다.
AWS 콘솔에서 GraphQL 호출해보기
Aws Management Console에 접속하면 Appsync에서 만든 GraphQL API를 테스트해 볼 수 있는 환경이 구성되어있다. 쿼리 할 GraphQL을 Explorer에서 클릭하거나 작성해서 실행하면 아래와 같이 Dog API를 호출한 데이터가 GraphQL로 리턴되는 것을 볼 수 있다.
콘솔에서 실행하지 않아도 Appsync 엔드포인트를 로컬 환경에서 GraphiQL로 사용하는 방법도 있다.
정리 및 회고
- JavaScript라서 굉장히 편하다. VTL로 작성하던 Resolver보다 Javascript 리졸버는 대부분의 개발자에게 훨씬 익숙하고 친근한 대안이 될 것이다. 여러 개발자가 Javascript 또는 익숙한 언어로 된 Resolver 출시를 기다렸던 입장인데, 앞으로 누가 JavaScript 대신에 VTL을 쓸까?
- 여러가지 GraphQL 엔진(Hasura, Prisma, Apollo Server 등)을 선택할 때, 이전까지 Appsync는 VTL 때문에 대부분의 개발자에게 외면받아왔으리라 생각되고, Lambda Resolver을 붙여서 래핑을 하는 것이 선호됐다. 하지만 이번 JavaScript Resolvers 출시로 인해 Appsync 또한 GraphQL 엔진을 선택함에 있어서 많은 개발자에게 하나의 대안이 될 것으로 생각한다. (AWS 또한 VTL의 여러 가지 단점과 핵망폭망💣을 알고 출시했지 않았을까?)
- 하나의 Function에서 비동기 호출이 불가한 부분이 아쉽다. Appsync Pipeline Resolver를 쓰더라도 순차 실행밖에 불가해서 결국 비동기를 위해선 Lambda Resolver를 연결할 수밖에 없는데, Appsync 자체적으로 비동기에 대한 지원이 가능할까?
- 여러 단점도 있지만, Appsync는 GraphQL을 사용하면서 비용적으로 굉장히 매력적인 서비스이다. 비즈니스 로직이 작성된 함수 호출 100만건당 4달러라 굉장히 저렴하다.
100만번을 호출해도 4딸라라니. - 대체 Appsync는 호출에 대한 동시성을 어떻게 처리하는걸까? Lambda는 동시성 제어를 하면서 Appsync Resolver에 대한 동시성은 왜 제한이 없지? Lambda보다 비교적 비싼 이유가 이것일까?
- Lambda Resolver를 사용하는 Appsync는 이중으로 과금되는데, 모든 Appsync Resolver를 Lambda로 만들면 GraphQL을 지원한다는 것 외에는 다른 이점이 없을 것 같다. 필요할 때(AWS 서비스를 연동하거나 비동기 처리를 할 때 등) 빼고는 이러한 사용사례를 안만드는 것이 중요할 것 같다.