๐Ÿ„ AWS CDK 101 - ๐Ÿฒ dynamodb์™€ ํ•จ๊ป˜ AppSync๋ฅผ ์‚ฌ์šฉํ•˜๋Š” GraphQL

26535 ๋‹จ์–ด awstypescriptserverlessgraphql
๐Ÿ”ฐ AWS CDK๋ฅผ ์ฒ˜์Œ ์‚ฌ์šฉํ•˜๋Š” ์ดˆ๋ณด์ž๋Š” ์ด ์‹œ๋ฆฌ์ฆˆ์˜ ์ด์ „ ๊ธฐ์‚ฌ๋ฅผ ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์‹ญ์‹œ์˜ค.

์ด์ „ ๊ธฐ์‚ฌ๋ฅผ ๋†“์นœ ๊ฒฝ์šฐ ์•„๋ž˜ ๋งํฌ์—์„œ ์ฐพ์œผ์‹ญ์‹œ์˜ค.

๐Ÿ” ๐Ÿ”—์˜ ์›๋ณธ ์ด์ „ ๊ฒŒ์‹œ๋ฌผDev Post

๐Ÿ” ์ด์ „ ๊ฒŒ์‹œ๋ฌผ์„ ๐Ÿ”—์— ๋‹ค์‹œ ๊ฒŒ์‹œํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ธฐ์‚ฌ์—์„œ๋Š” dynamodb ํ…Œ์ด๋ธ” ์œ„์— ๋ž˜ํผ๋กœ โ€‹โ€‹๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค ๊ณ„์ธต์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ๋Š” ํŠนํžˆ ๊ธฐ๋ณธ ๋ชฉ๋ก ํ•ญ๋ชฉ์„ ์ˆ˜ํ–‰ํ•˜๊ณ  dynamodb์—์„œ ํ•ญ๋ชฉ์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด AWS appsync๋ฅผ ์‚ฌ์šฉํ•˜๋Š” graphql์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฑด์„ค ๐Ÿ—๏ธ



์ƒˆ ์Šคํƒ์„ ์œ„ํ•œ ์ƒˆ ํŒŒ์ผlib/appsync-stack.ts์„ ์ƒ์„ฑํ•˜์—ฌ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.



์ด ์ƒˆ๋กœ์šด ์Šคํƒ์— ์‚ฌ์šฉ๋œ ๊ฐ€์ ธ์˜ค๊ธฐ โ›ฉ๏ธ



์—ฌ๊ธฐ์„œ ์ •์˜์— ๋„์›€์ด ๋˜๋„๋ก ์•„๋ž˜ ๊ฐœ์ฒด๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.

import { Duration, Expiration, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Table } from 'aws-cdk-lib/aws-dynamodb';
import { GraphqlApi, MappingTemplate, Schema, FieldLogLevel, AuthorizationType } from 'aws-lib-cdk/aws-appsync-alpha';


โ›บ ์ •์˜๊ฐ€ ์žˆ๋Š” Appsync ๊ตฌ์„ฑ



์•„๋ž˜์™€ ๊ฐ™์ด appsync ๋์ ์„ ์ •์˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์ƒˆ ์Šคํƒ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์—์„œ ์ฃผ๊ธฐ์ ์œผ๋กœ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์ด 7์ผ์ธ ๊ธฐ๋ณธ API ํ‚ค๋ฅผ ๋ฐ›๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๊ทธ ์™ธ์—๋„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ํ”„๋กœ์„ธ์Šค๋ฅผ ํ›จ์”ฌ ๋” ์ž˜ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก API๋ฅผ ์ถฉ๋ถ„ํžˆ ๊ธฐ๋กํ–ˆ์Šต๋‹ˆ๋‹ค.


export class GqlStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const AppSyncApi = new GraphqlApi(this, 'gqlApi', {
      name: 'gqlApi',
      schema: Schema.fromAsset('assets/messages-schema.gql'),
      xrayEnabled: true,
      logConfig: {
            excludeVerboseContent: false,
            fieldLogLevel: FieldLogLevel.ALL,
      },
      authorizationConfig: {
          defaultAuthorization: {
              authorizationType: AuthorizationType.API_KEY,
              apiKeyConfig: {
                  name: 'default-api-key',
                  description: 'default-api-key-description',
                  expires: Expiration.after(Duration.days(7))
              }
          }
      }
    })

  }
}


์Šคํ‚ค๋งˆ ์ •์˜ ํŒŒ์ผ ๐ŸŽข



ํ™•์‹คํ•˜๊ฒŒ ์•Œ๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ CDK ์ž์‚ฐ ํŒŒ์ผ์—์„œ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์ „์— AWS ์ฝ˜์†”์„ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํ‚ค๋งˆ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

type Message {
    message: AWSJSON!
}

type MessagesTable {
    createdAt: AWSTimestamp!
    messageId: String!
    event: Message
}

type MessagesTableConnection {
    items: [MessagesTable]
    nextToken: String
    scannedCount: Int
}

type Query {
    getMessage(messageId: String!, createdAt: AWSTimestamp!): MessagesTable
    listMessages(filter: TableMessagesTableFilterInput, limit: Int, nextToken: String): MessagesTableConnection
}

input TableAWSTimestampFilterInput {
    ne: AWSTimestamp
    eq: AWSTimestamp
    le: AWSTimestamp
    lt: AWSTimestamp
    ge: AWSTimestamp
    gt: AWSTimestamp
    contains: AWSTimestamp
    notContains: AWSTimestamp
    between: [AWSTimestamp]
}

input TableMessagesTableFilterInput {
    createdAt: TableAWSTimestampFilterInput
    messageId: TableStringFilterInput
}

input TableStringFilterInput {
    ne: String
    eq: String
    le: String
    lt: String
    ge: String
    gt: String
    contains: String
    notContains: String
    between: [String]
    beginsWith: String
}


๋ฐ์ดํ„ฐ ์†Œ์Šค๋กœ์„œ์˜ Dynamodb ์—ฐ๊ฒฐ ๐Ÿ›ถ



์—ฌ๊ธฐ์„œ ์šฐ๋ฆฌ๋Š” dynamodb API๋ฅผ graphql๊ณผ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋กœ ์ง์ ‘ ํ†ตํ•ฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

const messages = Table.fromTableName(this,'MessagesTableImport', 'MessagesTable');

const MessagesDS = AppSyncApi.addDynamoDbDataSource("MessagesDataSource", messages);




VTL ๋งคํ•‘ ํ…œํ”Œ๋ฆฟ ๐Ÿ›ฉ๏ธ



์—ฌ๊ธฐ์—์„œ VTL(Velocity Template Language)์„ ์‚ฌ์šฉํ•˜์—ฌ ์š”์ฒญ์„ ๋ณ€ํ™˜/์กฐ์ž‘ํ•˜๊ณ  ์•„๋ž˜ ๋ฆฌ์กธ๋ฒ„์—์„œ ๋ณด๋‚ด๊ฑฐ๋‚˜ ๋ฐ›๋Š” ์‘๋‹ต์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” appsync ๋ฐ API ๊ฒŒ์ดํŠธ์›จ์ด๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋งŽ์€ ๊ณณ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์€ ์ „๋žต์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ AWS ์ฝ˜์†”์„ ์‚ฌ์šฉํ•˜์—ฌ CDK ์ž์‚ฐ ํŒŒ์ผ์—์„œ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์ „์— ๋กœ๊ทธ์˜ ์ƒ˜ํ”Œ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋Ÿฌํ•œ ๋ณ€ํ™˜์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.







๋ฉ”์‹œ์ง€ ๋ฆฌ์กธ๋ฒ„ ๋ฐ›๊ธฐ ๐Ÿš 



์—ฌ๊ธฐ์—์„œ xray ์ถ”์ ์„ ์กฐ์‚ฌํ•˜์—ฌ ์ด๋Ÿฌํ•œ ๋ธ”๋ก์ด getMessage ๋ฆฌ์กธ๋ฒ„์— ํ™œ์šฉ๋˜๋Š” ์‹œ๊ธฐ๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

MessagesDS.createResolver({
      typeName: 'Query',
      fieldName: 'getMessage',
      requestMappingTemplate: MappingTemplate.fromFile('assets/getMessageRequest.vtl'),
      responseMappingTemplate: MappingTemplate.fromFile('assets/getMessageResponse.vtl'),
})


getMessageRequest VTL ํ…œํ”Œ๋ฆฟ ๐ŸŒŸ




{
    "version": "2017-02-28",
    "operation": "GetItem",
    "key": {
        "messageId": $util.dynamodb.toDynamoDBJson($ctx.args.messageId),
        "createdAt": $util.dynamodb.toDynamoDBJson($ctx.args.createdAt)
    }
}


getMessageResponse VTL ํ…œํ”Œ๋ฆฟ โ›ฑ๏ธ





#set($ctx.result.event = $util.parseJson($ctx.result.event))

$util.toJson($ctx.result)





๋ชฉ๋ก ๋ฉ”์‹œ์ง€ ๋ฆฌ์กธ๋ฒ„ ๐Ÿคก



์—ฌ๊ธฐ์—์„œ xray ์ถ”์ ์„ ์กฐ์‚ฌํ•˜์—ฌ ์ด๋Ÿฌํ•œ ๋ธ”๋ก์ด listMessages ๋ฆฌ์กธ๋ฒ„์— ํ™œ์šฉ๋˜๋Š” ์‹œ๊ธฐ๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

MessagesDS.createResolver({
        typeName: 'Query',
        fieldName: 'listMessages',
        requestMappingTemplate: MappingTemplate.fromFile('assets/listMessagesRequest.vtl'),
        responseMappingTemplate: MappingTemplate.fromFile('assets/listMessagesResponse.vtl'),
})


listMessagesRequest VTL ํ…œํ”Œ๋ฆฟ ๐ŸŽˆ




{
  "version": "2017-02-28",
  "operation": "Scan",
  "filter": #if($context.args.filter) $util.transform.toDynamoDBFilterExpression($ctx.args.filter) #else null #end,
  "limit": $util.defaultIfNull($ctx.args.limit, 20),
  "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null)),
}


listMessagesResponse VTL ํ…œํ”Œ๋ฆฟ ๐ŸŽฃ




#set($children = [])
#foreach($item in $ctx.result.items)
  #set($item.event = $util.parseJson($item.event))
  $util.qr($children.add($item))
#end
#set($ctx.result.items = $children)
$util.toJson($ctx.result)




graphQl โ„๏ธ์„ ํƒ์ƒ‰ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ





Appsync ํƒ์ƒ‰๊ธฐ ์ฟผ๋ฆฌ โ™จ๏ธ



AWS ์ฝ˜์†”์—์„œ appsync๋ฅผ ํƒ์ƒ‰ํ•˜๊ณ  ์ฟผ๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— ์žˆ๋Š” ํ•œ ๊ฐ€์ง€ ์žฅ์ ์€ ํ™•์ธํ•˜๋ ค๋Š” ๊ฒฝ์šฐ์— ๋Œ€๋น„ํ•˜์—ฌ ํด๋ผ์šฐ๋“œ ๊ฐ์‹œ ๋ฐ ์ถ”์  ๋กœ๊ทธ๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.





์•„ํด๋กœ ์ŠคํŠœ๋””์˜ค ์ฟผ๋ฆฌ ๐Ÿ•๏ธ



๊ทธ๋Ÿฌ๋‚˜ ์ผ๋ฐ˜์ ์œผ๋กœ apollo graphql ์ŠคํŠœ๋””์˜ค์—์„œ ์–ด๋‘์šด ๋ชจ๋“œ๋ฅผ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค. ์›ํ•˜๋Š” ๊ฒฝ์šฐ ์‹œ๋„ํ•ด ๋ณผ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์–ธ์  ๊ฐ€๋Š” AWS ์ฝ˜์†”์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.





๊ฒฐ๋ก  ๐Ÿ’ซ



์ด ์—ฐ์Šต์—์„œ๋Š” ๊ฒ€์ƒ‰ ๋ฐ ์ฟผ๋ฆฌ ์ž‘์—…์˜ ํšจ์œจ์„ฑ์„ ์ฐจ๋ณ„ํ™”ํ•˜๊ธฐ ์œ„ํ•ด get ๋ฐ list ์ž‘์—…๋งŒ ์‹œ๋„ํ–ˆ์Šต๋‹ˆ๋‹ค.
scannedCount ์œ„์˜ ๊ฒฐ๊ณผ ๊ฐ’์€ ํ…Œ์ด๋ธ”์ด ์ปค์งˆ์ˆ˜๋ก ๋ฆฌ์ŠคํŠธ ์—ฐ์‚ฐ์— ๋“œ๋Š” ๋น„์šฉ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ์Šค์บ”ํ•œ ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ํ•„ํ„ฐ ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.



์ตœ์ ์˜ ์†๋„์™€ ๋ฆฌ์†Œ์Šค ํ™œ์šฉ์„ ๋‹ฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ๊ธฐ์‚ฌ์—์„œ ์ด๋ฅผ ๊ฐœ์„ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ์Šคํƒ์— ๋” ๋งŽ์€ ์—ฐ๊ฒฐ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ƒˆ๋กœ์šด ๊ตฌ์„ฑ์„ ๋งŒ๋“ค์–ด ๋‹ค์Œ ๊ธฐ์‚ฌ์—์„œ ๋” ์œ ์šฉํ•˜๊ฒŒ ๋งŒ๋“ค ๊ฒƒ์ด๋ฏ€๋กœ ์ œ ๋‰ด์Šค๋ ˆํ„ฐ๋ฅผ ํŒ”๋กœ์šฐํ•˜๊ณ  ๊ตฌ๋…ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์‹ญ์‹œ์˜ค.

โญ ์„œ๋ฒ„๋ฆฌ์Šค์— ๋Œ€ํ•œ ๋‹ค์Œ ๊ธฐ์‚ฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ํ™•์ธํ•˜์‹ญ์‹œ์˜ค.

๐ŸŽ‰ ์‘์›ํ•ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ™

โ˜• Buy Me a Coffee์„ ์›ํ•˜์‹œ๋ฉด ์ œ ๋…ธ๋ ฅ์„ ๐Ÿ˜.





๐Ÿ” ์›๋ณธ ๊ฒŒ์‹œ๋ฌผ ๐Ÿ”—Dev Post

๐Ÿ” Reposted at ๐Ÿ”—


๐Ÿ„ AWS CDK 101 - ๐Ÿฒ GraphQL using AppSync with dynamodb
{ by }

Checkout more such in my pagehttps://t.co/CuYxnKr0Ighttps://t.co/phzOKFlXXO https://t.co/yeqeLZQt8A

โ€” Aravind V (@Aravind_V7)

์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ