시뻘건 개발 도전기

AWS AppSync 알아보기 본문

기타/AWS Tech

AWS AppSync 알아보기

시뻘건볼때기 2024. 3. 18. 19:31
반응형

AWS AppSync란?

AWS AppSync를 사용하면 개발자가 안전한 서버리스 고성능 GraphQL 및 Pub/Sub API를 사용하여 애플리케이션과 서비스를 데이터 및 이벤트에 연결할 수 있습니다.

 

  • AppSync는 GraphQL을 기반으로 간소화된 데이터 액세스를 할 수 있고 쿼리를 사용할 수 있다.
    • GrapghQL에 대한 내용은 공식 docs를 참고힌다.
  • Pub/Sub을 위한 webSocket을 제공한다.
  • 인메모리 캐시를 사용할 수 있다.
  • js, ts를 지원한다.
  • VPC를 통해 비공개 api 보안
  • API Key를 제공하여 IAM, Lambda 권한 부여를 지원하는 제어 기능이 있다.
그 밖에 AppSync의 기능은 공식 docs를 참고한다.

 

GraphQL API 설계

스키마

각 GraphQL api는 요청의 데이터가 채워지는 방식을 설명하는 유형과 필드를 포함하는 단일 스키마로 정의한다. API를 통한 데이터 흐름과 수행되는 작업은 스키마를 기준으로 검증되어야한다.

 

GraphQL은 API용 쿼리 언어이자 기존 데이터로 이러한 쿼리를 수행하기 위한 런타임입니다. GraphQL은 API의 데이터에 대한 완전하고 이해하기 쉬운 설명을 제공하고, 클라이언트가 필요한 사항과 더 이상 필요하지 않은 사항을 정확히 요청할 수 있는 기능을 제공하며, 시간이 지남에 따라 API를 더 쉽게 발전시킬 수 있게 하고, 강력한 개발자 도구를 사용할 수 있도록 합니다.

 

  • 유형(Types)은 GraphQL이 데이터의 형태와 동작을 정의하는 방식. (나는 어떤 데이터를 가지고 이런 일을 할꺼야!)
  • 필드(Fileds)는 유형의 범위 내에 존재하며 GraphQL 서비스에서 요청한 값을 보유합니다. (나는 이런 데이터를 가질꺼야!)

스키마의 데이터 범주는 기본적으로 세 가지가 있다.

  1. 스키마 루트: 추가, 삭제, 수정과 같은 데이터에 대한 일부 작업을 수행할 필드를 가르키며 스키마의 진입점을 정의한다.
  2. 유형: 데이터의 헝태. (객체나 추상적 표현)
  3. 특수 객체 유형: 스키마의 작업 동작을 정의한다. GraphQL에는 기본적으로 쿼리, 변형, 구독이라는 세 가지가 있다.
각 데이터의 타입은 공식 docs를 참고한다.

예시

schema {  # Book과 Author 유형을 사용한 스키마
  query: Query
  mutation: Mutation
}

type Author { # Author라는 유형
  # 필드
  authorName: String
  Books: [Book]
}

# 입력 값을 파라미터로 사용여 변형 생성 (입력 유형)
input AuthorInput {
  authorName: String
  Books: [BookInput]
}

type Book { # Book이라는 유형
  # 필드
  bookName: String
  Authors: [Author]
}

# 입력 값을 파라미터로 사용여 변형 생성 (입력 유형)
input BookInput {
  bookName: String
  Authors: [AuthorInput]
}

# 특수 객체 유형 Query
type Query {
  getAuthor(authorName: String): Author
  getBook(bookName: String): Book
}

# 특수 객체 유형 Mutation
type Mutation {
  addAuthor(input: [BookInput]): Author
  addBook(input: [AuthorInput]): Book
}

다른 형태의 스키마 생성은 공식 docs를 참고한다. (consle, CLI, CDK)

  • 라이브 환경의 경우 CDK 사용을 권장하고있다.

열거형

열거형을 사용하면 스키마의 기능을 향상시킬 수 있다.

schema {
  query: Query
  mutation: Mutation
}

type Post {
  id: ID!
  title: String
  date: AWSDateTime
  poststatus: PostStatus
}

type Mutation {
  addPost(id: ID!, title: String, date: AWSDateTime, poststatus: PostStatus): Post
}

type Query {
  getPosts: [Post]
}

# PostStatus라는 열거형
enum PostStatus {  
  success
  pending
  error
}

관계 및 페이지 매김

대량의 데이터를 조회하게되면 커서 방식, 페이징 방식 등을 사용하여 일부분씩 가져오도록 하여 성능을 개선한다. GraphQL에서도 가능하다. 지금까지는 모든 데이터를 조회할 수 있도록 스키마를 설정해두었지만, 아래와 같이 스키마를 설정해두면 페이징을 사용할 수 있다. (물론 뒤에 알아볼 “해석기”와도 관련이 있다.)

조회 필드에 nextToken이라는 반복자와 limit라는 반복 제한자라는 입력 인수를 추가하며 반복자, 반복 제한자를 포함하는 신규 유형을(PostIterator) 생성한다. 이렇게되면 조회 필드의 리턴 형태는 PostIterator가 되어야할 것이다.

schema {
  query: Query
  mutation: Mutation
}

type Post {
  id: ID!
  title: String
  date: AWSDateTime
  poststatus: PostStatus
}

type Mutation {
  addPost(id: ID!, title: String, date: AWSDateTime, poststatus: PostStatus): Post
}

type Query {
  getPosts(limit: Int, nextToken: String): PostIterator
}

enum PostStatus {
  success
  pending
  error
}

type PostIterator {
  posts: [Post]
  nextToken: String
}

데이터 원본 연결

데이터 원본은 GraphQL이 상호작용할 수 있는 리소스로써, Lambda, DynamoDB, Aurora, OpenSearch, HTTP EndPoint와 같은 다양한 데이터 원본을 지원한다. 위에서 “간단한 API 만들어보기”를 진행한다면 데이터 원본을 연결해볼 수 있다.

DynamoDB

Amazon DynamoDB는 확장 가능한 애플리케이션을 위한 AWS의 주요 스토리지 솔루션 중 하나입니다.

 

DynamoDB를 GraphQL API에 연결했을 때 사용자가 시스템에서 변경하는 내용이 FE에 반영되면 GraphQL API는 변경 사항을 검색하여 다른 사용자에게 실시간으로 브로드캐스트한다.

Lambda

Lambda는 이벤트에 대한 응답으로 코드를 실행하는 데 필요한 리소스를 자동으로 빌드하는 이벤트 기반 서비스입니다.

 

함수는 트리거를 감지하면 자동으로 실행된다. 함수가 트리거되면 수정할 데이터가 포함된 이벤트를 처리한다. 그렇기 때문에 코드를 실핼하는 데에 적합하며 리소스를 프로비저닝할 필요가 없다.

RDS

Amazon RDS를 사용하면 관계형 데이터베이스를 빠르게 빌드하고 구성할 수 있습니다. Amazon RDS에서는 클라우드의 격리된 데이터베이스 환경으로 사용할 일반 데이터베이스 인스턴스를 만들게 됩니다. 이 인스턴스에서는 실제 RDBMS 소프트웨어(PostgreSQL, MySQL 등)인 DB 엔진을 사용합니다. 이 서비스는 AWS 인프라를 이용한 확장성, 패칭 및 암호화와 같은 보안 서
비스, 낮은 배포 관리 비용을 제공함으로써 백엔드 작업을 상당히 줄여줍니다.

 

AppSync의 GraphQL API를 RDS에 직접 연결하는 해석기도 있다. 해석기는 필드에 연결되며 데이터베이스에 액세스하는 데 필요한 SQL문을 구현한다.

EventBridge

EventBridge에서는 연결한 서비스 또는 애플리케이션(이벤트 소스)으로부터 이벤트를 수신하고 정해진 규칙에 따라 이벤트를 처리하는 파이프라인인 이벤트 버스를 만들게 됩니다. 이벤트는 실행 환경의 상태가 일부 변하는 것을 의미하고, 규칙은 이벤트에 대한 필터 집합입니다. 규칙은 이벤트 패턴 또는 이벤트 상태 변화의 메타데이터(ID, 리전, 계정 번호, ARN 등)를 따릅니다. 이벤트가 이벤트 패턴과 매칭되면 EventBridge는 파이프라인을 통해 대상 서비스(대상)로 이벤트를 보내고 규칙에 지정된 작업을 트리거합니다.

 

상태가 변경되는 작업을 일부 다른 서비스로 라우팅하는데에 적합하다. (주문 또는 발송 처리)

OpenSearch

Amazon OpenSearch Service는 전체 텍스트 검색, 데이터 시각화 및 로깅을 구현하는 도구 모음입니다. 이 서비스를 사용하여 업로드한 구조화된 데이터를 쿼리할 수 있습니다.

 

OpenSearch의 인스턴스를 생성(노드)하고 인덱스를 하나 이상 추가한다. 데이터가 인덱스에 쌓이면 하나 이상의 샤드에 데이터를 인덱싱한다. 인덱싱된 document를 쿼리할 수 있다.

HTTP End-point

파라미터 및 페이로드와 같은 관련 정보와 함께 End-point에 요청을 보낼 수 있다.

 

해석기 구성

스키마에서 각종 유형과 필드를 정의하고 데이터 원본에서 데이터를 관리하는데, 어떻게 동작하는지에 대한 내용을 다루지 않았다. “이렇게 동작하세요.”는 해석기가 해준다.

모든 작업의 덩작은 해석기의 담당이며 해석기가 실행되는 런타임에 동작을 수행하는 필드로 연결된다. 런타임에 따라 해석기를 작성할 언어가 결정된다.

  • APPSYNC_JS(JavaScript)
  • Apache Velocity Template Language(VTL)입

해석기를 구현할 때 아래와 같은 일반적인 구조를 따른다.

  1. 단계 이전: 클라이언트가 요청을 보내면 사용 중인 스키마 필드(일반적으로 쿼리, 뮤테이션, 구독)의 해석기에 요청 데이터가 전달됩니다. 해석기는 데이터가 해석기를 통해 이동하기 전에 일부 사전 처리 작업을 수행할 수 있는 전 단계 핸들러를 사용하여 요청 데이터 처리를 시작합니다.
  2. 함수: 이전 단계가 실행되면 요청이 함수 목록으로 전달됩니다. 목록의 첫 번째 함수는 데이터 소스에 대해 실행됩니다. 함수는 자체 요청 및 응답 핸들러를 포함하는 해석기 코드의 하위 집합입니다. 요청 핸들러는 요청 데이터를 가져와 데이터 소스에 대해 작업을 수행합니다. 응답 핸들러는 데이터 소스의 응답을 다시 목록으로 전달하기 전에 해당 데이터 소스의 응답을 처리합니다. 함수가 두 개 이상인 경우 요청 데이터는 목록의 다음 함수로 전송되어 실행됩니다. 목록에 있는 함수는 개발자가 정의한 순서대로 순차적으로 실행됩니다. 모든 함수가 실행되면 최종 결과가 after step.res로 전달됩니다.
  3. 단계 이후: 이후 단계는 GraphQL 응답으로 전달하기 전에 최종 함수의 응답에 대한 몇 가지 최종 작업을 수행할 수 있는 핸들러 함수입니다.

자주 사용하는 기초 기능 (OpenSearch 편)

페이징

OpenSearch는 기본적으로 대용량의 데이터를 다루는 데이터 소스이다. 그러다보니 조회 시에 페이징을 간혹 사용할 때가 많다. 페이징을 사용하여 원하는 size만큼씩만 조회하는 방법을 소개한다.

OpenSearch는 Elasticcsearch를 기반으로 하기때문에 페이징 쿼리는 Elasticsearch의 docs를 참고한다.

스키마

아래와 같이 페이징을 위해 요청받을 Paging input을 정의하고 getAllPaging에서 파라미터로 받을 수 있게 한다. 응답의 경우 페이징 관련 데이터(총 개수 등)와 documents를 가진 PasingMyList를 선언하여 getAllPaging가 리턴하게 하도록 한다.

input Paging {
	from: AWSDateTime
	to: AWSDateTime
	size: Int
	afterId: String
}

type PasingMyList {
	count: Int!
	list: [MyList]
	isLast: Boolean!
	afterId: String
}

type MyList {
	seq: Int
	createdat: String
	sessionid: String
}

type Query {
	getAll: [MyList]
	getAllPaging(paging: Paging): PasingMyList
	getAllPagingWithParam(paging: Paging): PasingMyList
}

해석기

요청을 받은 데이터는 ctx.args에 담겨져있다. 우린 paging이라는 이름으로 넘겼기 때문에 ctx.args.paging으로 접근할 수 있게된다.

  • elasticsearch의 search_after를 사용해서 페이징을 시도한다.
  • 연속된 물음표 사용(??): java의 3항 연산자와 살짝 비슷하다. “a ?? b”의 의미는 a가 null이면 b를 사용하고 null이 아니면 a를 사용하겠다는 의미다. (ECMA 20 버전부터 사용할 수 있는 syntax)
  • request로 받은 데이터를 reponse function에서 사용: 간혹 client로부터 받은 데이터가 응답을 구성할 때 사용되기도 하다. 그런데 AppSync의 해석기에서는 명확하게 request와 response가 분리되어 있는 것 처럼 보인다. 하지만 response function에서 ctx를 통해 client가 전달한 데이터에 접근이 가능하다. 이는 ctx.args로 접근할 수 있다.
import { util } from '@aws-appsync/utils';
 
/**
 * Gets a document by `id`
 * @param {import('@aws-appsync/utils').Context} ctx the context
 * @returns {*} the request
 */
export function request(ctx) {
    const index = 'my_view-2024-03-11';
    const paging = ctx.args.paging ?? {
        createdatDate: 0,
        afterId: 0,
        size: 2
    };
    return {
        operation: 'GET',
        path: `/${index}/_search`,
        params: {
            body: {
                search_after: [paging.afterId ?? 0],
                sort: [
                    {seq: "asc"}
                ],
                from: 0,
                size: paging.size ?? 2
            }
        }
    };
}

/**
 * Returns the fetched item
 * @param {import('@aws-appsync/utils').Context} ctx the context
 * @returns {*} the result
 */
export function response(ctx) {
    if (ctx.error) {
        util.error(ctx.error.message, ctx.error.type);
    }
    let list = ctx.result.hits.hits.map((hit) => hit._source);
    let isLast = false
    let afterId = 0;
    const count = ctx.result.hits.total.value ?? 0;
    if(count < 0) {
        return {
            count: count,
            list: {},
            isLast: true,
            afterId: 0
        }
    }
    
    if(list.length < ctx.args.paging.size) {
        isLast = true;
    }
    
    if (list.length > 0) {
        afterId = list[list.length - 1].seq;
    }
    
    return {
        count: count,
        list: list,
        isLast: isLast,
        afterId: afterId
    };
}

쿼리

파라미터가 있을 경우가 있을 것이고 없는 경우가 있을 것이다.

없는 경우에는 모두 Default 값으로 쿼리 조회가 될 것이고, 파라미터가 있는 경우에는 없는 파라미터만 default 값으로 쿼리 조회가 이루어진다.

query getAllPaging {
  getAllPaging {
    afterId
    count
    isLast
    list {
      seq
      createdat
      sessionid
  }
}

query getAllPagingWithParam {
  getAllPaging(paging: {size: 2, afterId: "2370801080"}) {
    afterId
    count
    isLast
    list {
      seq
      createdat
      sessionid
    }
  }
}

custom domain names

api호출을 위해 도메인을 커스텀할 수 있다. 그 전에 커스텀을 하지 않았을 경우 내가 만든 GraphQL api를 어떻게 외부에서 호출하는지 알아보자.

api call

우리의 궁극적인 목적인 GraphQL api의 호출을 해보자. api를 호출하기 위해서는 여러가지의 인증과 권한이 필요하다.

인증과 권한에 대한 자세한 내용은 AWS docs를 살펴보자.

권한 부여 유형은 5가지가 있다. (각각의 권한 부여 내용은 상기 docs 참고)

  • API_KEY
    • 좌측 “설정”탭에서 API Key를 확인할 수 있다.
  • AWS_LABDA
  • AWS_IAM
  • OPENID_CONNECT
  • AMAZON_COGNITO_USER_POOLS

여기서는 가장 간단한 API_KEY를 사용해서 api를 call해보겠다. 아래 예시는 우리가 만든 getAllPagingWithParam query를 호출해본다면, [그림 2]와 같이 응답이 정상적으로 리턴되는 것을 볼 수 있다.

  curl -XPOST \
  -H "Content-Type:application/graphql" \
  -H "x-api-key:{MY_API_KEY}" \
  -d '{ "query": "query getAllPaging { getAllPaging { afterId count isLast list { seq createdat sessionid } }}" }' \
  https://{MY_END_POINT}/graphql

 

좌측 설정탭으로 진입하여 [그림 1]과 같이 end-point와 api key를 찾을 수 있다.
[그림 1] 앤드포인트와 api key

 

custom domain

외부에 노출이 되는 api에 대해서 어떤 role을 만들어 가느냐에 따라 다르지만, 개인적으로 api-key가 아무리 최대 365일 만료라고 하더라도 숨기거나 내부적으로만 사용되면 좋을 것같다. 또한, end-point에 대한 내용과 client가 직접 쿼리를 가지고 요청하는 것도 어색하다. 따라서 실제 api를 사용핧 client와 server 사이에 gateway 혹은 proxy와 같은 Third-party server를 두고 운영하는 것이 적절해보인다.

만약 Third-party server를 둔다면, 굳이 domain을 커스텀할 필요까지는 없을 것이다. 왜냐하면 과금이 되기 때문이다. 내부에서만 사용하는데 굳이 도메인을 커스텀할 필요가 있을까? 그 외, 외부에서 AppSync의 GraphQL api를 사용해야한다면 이야기가 다를 것이다.

도메인을 커스텀 하기 위해서는 ACM Certificate를 사용한다. 기본적으로 ACM 셋업을 하게되면 과금이 된다.

따라서, 공식 docs로 대체한다.

반응형

'기타 > AWS Tech' 카테고리의 다른 글

AWS AppSync with OpenSearch  (1) 2024.03.29
AWS AppSync with DynamoDB  (0) 2024.03.22
Comments